diff --git a/.dmtlint.yaml b/.dmtlint.yaml new file mode 100644 index 0000000..3e86481 --- /dev/null +++ b/.dmtlint.yaml @@ -0,0 +1,27 @@ +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_dev.yml b/.github/workflows/build_dev.yml new file mode 100644 index 0000000..552f77a --- /dev/null +++ b/.github/workflows/build_dev.yml @@ -0,0 +1,232 @@ +name: Build and Push for Dev + +on: + workflow_dispatch: + inputs: + pr_number: + description: | + Pull request number, like 563, or leave empty and choose a branch + For branches main, release-*, tag will be generated as branch name + required: false + type: number + svace_enabled: + description: "Enable svace build" + type: boolean + required: false + pull_request: + types: [opened, reopened, synchronize, labeled, unlabeled] + push: + branches: + - main + - release-* + tags: + - "v*" + +jobs: + lint: + runs-on: [self-hosted, large] + continue-on-error: true + name: Lint + steps: + - uses: actions/checkout@v4 + - uses: deckhouse/modules-actions/lint@main + env: + DMT_METRICS_URL: ${{ secrets.DMT_METRICS_URL }} + DMT_METRICS_TOKEN: ${{ secrets.DMT_METRICS_TOKEN }} + + lint_go: + runs-on: [self-hosted, large] + name: Run golangci-lint + steps: + - name: Set up Go ${{ vars.GO_VERSION }} + uses: actions/setup-go@v5 + with: + go-version: "${{ vars.GO_VERSION }}" + + - uses: actions/checkout@v4 + + - name: Install golangci-lint + run: | + echo "Installing golangci-lint..." + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v${{ vars.GOLANGCI_LINT_VERSION}} + echo "$(go env GOPATH)/bin" >> $GITHUB_PATH + echo "golangci-lint v${{ vars.GOLANGCI_LINT_VERSION}} installed successfully!" + + - name: Run golangci-lint in every directory with .golangci.yaml + shell: bash + run: | + # set -eo pipefail + set -e + + # Find directories containing .golangci.yaml + mapfile -t config_dirs < <( + find . \ + -type f -name '.golangci.yaml' -printf '%h\0' | \ + xargs -0 -n1 | sort -u + ) + count=${#config_dirs[@]} + echo "::notice title=Lint Setup::🔍 Found $count directories with linter configurations" + + report="" + error_count=0 + + for dir in "${config_dirs[@]}"; do + find_errors=0 + cd "$dir" || { echo "::error::Failed to access directory $dir"; continue; } + + if ! output=$(golangci-lint run); then + error_count=$(( error_count + 1 )) + echo "::group::📂 Linting directory ❌: $dir" + echo -e "❌ Errors:\n$output\n" + else + echo "::group::📂 Linting directory ✅: $dir" + echo -e "✅ All check passed\n" + fi + + cd - &>/dev/null + + echo "::endgroup::" + done + + has_errors=$( [[ "$error_count" -gt 0 ]] && echo true || echo false) + echo "has_errors=$has_errors" >> "$GITHUB_OUTPUT" + + if [ $error_count -gt 0 ]; then + echo "$error_count error more than 0, exit 1" + exit 1 + fi + + build_dev: + runs-on: [self-hosted, large] + name: Build and Push images + outputs: + MODULES_MODULE_TAG: ${{ steps.modules_module_tag.outputs.MODULES_MODULE_TAG }} + steps: + - name: Set vars + id: modules_module_tag + run: | + if [[ "${{ github.ref_name }}" == 'main' ]]; then + MODULES_MODULE_TAG="${{ github.ref_name }}" + elif [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+ ]]; then + MODULES_MODULE_TAG="${{ github.ref_name }}" + elif [[ -n "${{ github.event.pull_request.number }}" ]]; then + MODULES_MODULE_TAG="pr${{ github.event.pull_request.number }}" + elif [[ -n "${{ github.event.inputs.pr_number }}" ]]; then + MODULES_MODULE_TAG="pr${{ github.event.inputs.pr_number }}" + else + echo "::error title=Module image tag is required::Can't detect module tag from workflow context. Dev build uses branch name as tag for main and release branches, and PR number for builds from pull requests. Check workflow for correctness." + exit 1 + fi + + echo "MODULES_MODULE_TAG=$MODULES_MODULE_TAG" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v4 + + - uses: deckhouse/modules-actions/setup@main + with: + registry: ${{ vars.DEV_REGISTRY }} + registry_login: ${{ vars.DEV_MODULES_REGISTRY_LOGIN }} + registry_password: ${{ secrets.DEV_MODULES_REGISTRY_PASSWORD }} + + - uses: deckhouse/modules-actions/build@main + with: + module_source: dev-registry.deckhouse.io/sys/deckhouse-oss/modules + module_name: ${{ vars.MODULES_MODULE_NAME }} + module_tag: ${{ steps.modules_module_tag.outputs.MODULES_MODULE_TAG }} + svace_enabled: false + + show_dev_manifest: + runs-on: [self-hosted, large] + name: Show manifest + needs: build_dev + steps: + - name: Show dev config + run: | + cat << OUTER + Create ModuleConfig and ModulePullOverride resources to test this MR: + + cat < /dev/null; then + echo "$TAG is a valid release tag" + else + echo "Error: Invalid tag format. Use format vX.Y.Z" + exit 1 + fi + shell: bash + + job-CE: + name: Edition CE + runs-on: [self-hosted, large] + needs: print-vars + if: inputs.ce && !inputs.check_only + steps: + - name: Setup Docker config + run: | + echo "DOCKER_CONFIG=$(mktemp -d)" >> $GITHUB_ENV + + - run: echo "CE" + - name: SET VAR + id: set_vars + run: | + echo "MODULES_MODULE_SOURCE=$MODULES_REGISTRY/$MODULE_SOURCE_NAME/ce/modules" >> "$GITHUB_ENV" + echo "MODULE_EDITION=CE" >> "$GITHUB_ENV" + echo "MODULES_MODULE_SOURCE=$MODULES_REGISTRY/$MODULE_SOURCE_NAME/ce/modules" >> "$GITHUB_OUTPUT" + echo "MODULE_EDITION=CE" >> "$GITHUB_OUTPUT" + - name: ECHO VAR + run: | + echo $MODULES_MODULE_SOURCE + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.inputs.tag }} + + - name: Login to PROD_REGISTRY + uses: deckhouse/modules-actions/setup@v2 + with: + registry: ${{ vars.PROD_REGISTRY }} + registry_login: ${{ vars.PROD_MODULES_REGISTRY_LOGIN }} + registry_password: ${{ secrets.PROD_MODULES_REGISTRY_PASSWORD }} + - name: Login to DEV_REGISTRY + uses: deckhouse/modules-actions/setup@v2 + with: + registry: ${{ vars.DEV_REGISTRY }} + registry_login: ${{ vars.DEV_MODULES_REGISTRY_LOGIN }} + registry_password: ${{ secrets.DEV_MODULES_REGISTRY_PASSWORD }} + + - if: ${{ inputs.enableBuild }} + uses: deckhouse/modules-actions/build@v4 + with: + module_source: ${{ steps.set_vars.outputs.MODULES_MODULE_SOURCE }} + module_name: ${{ vars.MODULES_MODULE_NAME }} + module_tag: ${{ github.event.inputs.tag }} + + - uses: deckhouse/modules-actions/deploy@v2 + with: + module_source: ${{ steps.set_vars.outputs.MODULES_MODULE_SOURCE }} + module_name: ${{ vars.MODULES_MODULE_NAME }} + module_tag: ${{ github.event.inputs.tag }} + release_channel: ${{ github.event.inputs.channel }} + + - name: Cleanup Docker config + run: | + rm -rf $DOCKER_CONFIG + + job-EE: + name: Edition EE + needs: print-vars + runs-on: [self-hosted, large] + if: inputs.ee && !inputs.check_only + steps: + - name: Setup Docker config + run: | + echo "DOCKER_CONFIG=$(mktemp -d)" >> $GITHUB_ENV + + - run: echo "EE" + - name: SET VAR + id: set_vars + run: | + echo "MODULES_MODULE_SOURCE=$MODULES_REGISTRY/$MODULE_SOURCE_NAME/ee/modules" >> "$GITHUB_ENV" + echo "MODULE_EDITION=EE" >> "$GITHUB_ENV" + echo "MODULES_MODULE_SOURCE=$MODULES_REGISTRY/$MODULE_SOURCE_NAME/ee/modules" >> "$GITHUB_OUTPUT" + echo "MODULE_EDITION=EE" >> "$GITHUB_OUTPUT" + - name: ECHO VAR + run: | + echo $MODULES_MODULE_SOURCE + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.inputs.tag }} + + - uses: deckhouse/modules-actions/setup@v2 + with: + registry: ${{ vars.PROD_REGISTRY }} + registry_login: ${{ vars.PROD_MODULES_REGISTRY_LOGIN }} + registry_password: ${{ secrets.PROD_MODULES_REGISTRY_PASSWORD }} + + - name: Login to PROD_REGISTRY + uses: deckhouse/modules-actions/setup@v2 + with: + registry: ${{ vars.PROD_REGISTRY }} + registry_login: ${{ vars.PROD_MODULES_REGISTRY_LOGIN }} + registry_password: ${{ secrets.PROD_MODULES_REGISTRY_PASSWORD }} + - name: Login to DEV_REGISTRY + uses: deckhouse/modules-actions/setup@v2 + with: + registry: ${{ vars.DEV_REGISTRY }} + registry_login: ${{ vars.DEV_MODULES_REGISTRY_LOGIN }} + registry_password: ${{ secrets.DEV_MODULES_REGISTRY_PASSWORD }} + + - if: ${{ inputs.enableBuild }} + uses: deckhouse/modules-actions/build@v4 + with: + module_source: ${{ steps.set_vars.outputs.MODULES_MODULE_SOURCE }} + module_name: ${{ vars.MODULES_MODULE_NAME }} + module_tag: ${{ github.event.inputs.tag }} + + - uses: deckhouse/modules-actions/deploy@v2 + with: + module_source: ${{ steps.set_vars.outputs.MODULES_MODULE_SOURCE }} + module_name: ${{ vars.MODULES_MODULE_NAME }} + module_tag: ${{ github.event.inputs.tag }} + release_channel: ${{ github.event.inputs.channel }} + + - name: Cleanup Docker config + run: | + rm -rf $DOCKER_CONFIG + + job-SE-Plus: + name: Edition SE Plus + needs: job-EE + runs-on: [self-hosted, large] + steps: + - name: Setup Docker config + run: | + echo "DOCKER_CONFIG=$(mktemp -d)" >> $GITHUB_ENV + + - run: echo "SE Plus" + - name: SET VAR + id: set_vars + run: | + echo "MODULES_MODULE_SOURCE=$MODULES_REGISTRY/$MODULE_SOURCE_NAME/se-plus/modules" >> "$GITHUB_ENV" + echo "MODULE_EDITION=EE" >> "$GITHUB_ENV" + echo "MODULES_MODULE_SOURCE=$MODULES_REGISTRY/$MODULE_SOURCE_NAME/se-plus/modules" >> "$GITHUB_OUTPUT" + echo "MODULE_EDITION=EE" >> "$GITHUB_OUTPUT" + - name: ECHO VAR + run: | + echo $MODULES_MODULE_SOURCE + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.inputs.tag }} + + - name: Login to PROD_REGISTRY + uses: deckhouse/modules-actions/setup@v2 + with: + registry: ${{ vars.PROD_REGISTRY }} + registry_login: ${{ vars.PROD_MODULES_REGISTRY_LOGIN }} + registry_password: ${{ secrets.PROD_MODULES_REGISTRY_PASSWORD }} + - name: Login to DEV_REGISTRY + uses: deckhouse/modules-actions/setup@v2 + with: + registry: ${{ vars.DEV_REGISTRY }} + registry_login: ${{ vars.DEV_MODULES_REGISTRY_LOGIN }} + registry_password: ${{ secrets.DEV_MODULES_REGISTRY_PASSWORD }} + + - if: ${{ inputs.enableBuild }} + uses: deckhouse/modules-actions/build@v4 + with: + module_source: ${{ steps.set_vars.outputs.MODULES_MODULE_SOURCE }} + module_name: ${{ vars.MODULES_MODULE_NAME }} + module_tag: ${{ github.event.inputs.tag }} + + - uses: deckhouse/modules-actions/deploy@v2 + with: + module_source: ${{ steps.set_vars.outputs.MODULES_MODULE_SOURCE }} + module_name: ${{ vars.MODULES_MODULE_NAME }} + module_tag: ${{ github.event.inputs.tag }} + release_channel: ${{ github.event.inputs.channel }} + + - name: Cleanup Docker config + run: | + rm -rf $DOCKER_CONFIG + + job-FE: + name: Edition FE + needs: job-EE + runs-on: [self-hosted, large] + steps: + - name: Setup Docker config + run: | + echo "DOCKER_CONFIG=$(mktemp -d)" >> $GITHUB_ENV + + - run: echo "FE" + - name: SET VAR + id: set_vars + run: | + echo "MODULES_MODULE_SOURCE=$MODULES_REGISTRY/$MODULE_SOURCE_NAME/fe/modules" >> "$GITHUB_ENV" + echo "MODULE_EDITION=EE" >> "$GITHUB_ENV" + echo "MODULES_MODULE_SOURCE=$MODULES_REGISTRY/$MODULE_SOURCE_NAME/fe/modules" >> "$GITHUB_OUTPUT" + echo "MODULE_EDITION=EE" >> "$GITHUB_OUTPUT" + - name: ECHO VAR + run: | + echo $MODULES_MODULE_SOURCE + - name: Validation for tag + run: | + echo ${{ github.event.inputs.tag }} | grep -P '^v\d+\.\d+\.\d+' + shell: bash + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.inputs.tag }} + + - name: Login to PROD_REGISTRY + uses: deckhouse/modules-actions/setup@v2 + with: + registry: ${{ vars.PROD_REGISTRY }} + registry_login: ${{ vars.PROD_MODULES_REGISTRY_LOGIN }} + registry_password: ${{ secrets.PROD_MODULES_REGISTRY_PASSWORD }} + - name: Login to DEV_REGISTRY + uses: deckhouse/modules-actions/setup@v2 + with: + registry: ${{ vars.DEV_REGISTRY }} + registry_login: ${{ vars.DEV_MODULES_REGISTRY_LOGIN }} + registry_password: ${{ secrets.DEV_MODULES_REGISTRY_PASSWORD }} + + - if: ${{ inputs.enableBuild }} + uses: deckhouse/modules-actions/build@v4 + with: + module_source: ${{ steps.set_vars.outputs.MODULES_MODULE_SOURCE }} + module_name: ${{ vars.MODULES_MODULE_NAME }} + module_tag: ${{ github.event.inputs.tag }} + + - uses: deckhouse/modules-actions/deploy@v2 + with: + module_source: ${{ steps.set_vars.outputs.MODULES_MODULE_SOURCE }} + module_name: ${{ vars.MODULES_MODULE_NAME }} + module_tag: ${{ github.event.inputs.tag }} + release_channel: ${{ github.event.inputs.channel }} + + - name: Cleanup Docker config + run: | + rm -rf $DOCKER_CONFIG + + check-version-on-release-channel: + name: Check version on release channel + runs-on: ubuntu-latest + env: + GO_VERSION: "1.24.13" + input_channel: ${{ github.event.inputs.channel }} + input_version: ${{ github.event.inputs.tag }} + needs: + - job-EE + - job-SE-Plus + - job-FE + - job-CE + if: ${{ always() && ! contains(needs.*.result, 'failure') || inputs.check_only }} + strategy: + matrix: + check: ["registry", "releases", "documentation"] + steps: + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 + with: + go-version: "${{ env.GO_VERSION }}" + + - name: Login to PROD_REGISTRY + uses: deckhouse/modules-actions/setup@v2 + with: + registry: ${{ vars.PROD_READ_REGISTRY }} + registry_login: ${{ secrets.PROD_READ_REGISTRY_USER }} + registry_password: ${{ secrets.PROD_READ_REGISTRY_PASSWORD }} + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check version in registry + if: ${{ matrix.check == 'registry' && always() }} + run: | + CHANNEL=$input_channel \ + VERSION=$input_version \ + task -d tools/moduleversions check:registry + + - name: Check version on site ${{ matrix.check }} + if: ${{ matrix.check == 'releases' && always() }} + env: + input_check_only: ${{ inputs.check_only }} + run: | + # When check_only is true, we don't want to wait 120 seconds + # because we just want to check the version we're interested in on the website + if [ $input_check_only = false ]; then + echo "Waiting for site to update (versions are usually updated within 2 minutes)..." + sleep 120 + fi + + echo "Test that version deployed on site, retrying 5 times with delay of 60 seconds" + CHANNEL=$input_channel \ + VERSION=$input_version \ + COUNT=5 \ + task -d tools/moduleversions check:releases + - name: Check version on site ${{ matrix.check }} + if: ${{ matrix.check == 'documentation' && always() }} + env: + input_check_only: ${{ inputs.check_only }} + run: | + # When check_only is true, we don't want to wait 300 seconds + # because we just want to check the version we're interested in on the website + if [ $input_check_only = false ]; then + echo "Waiting for site to update (versions are usually updated within 5 minutes)..." + sleep 300 + fi + + echo "Test that version deployed on site, retrying 5 times with delay of 60 seconds" + CHANNEL=$input_channel \ + VERSION=$input_version \ + COUNT=5 \ + task -d tools/moduleversions check:docs + + send-release-results-to-loop: + name: Send release results to Loop + runs-on: ubuntu-latest + needs: + - job-CE + - job-EE + - job-SE-Plus + - job-FE + - check-version-on-release-channel + if: ${{ always() && inputs.send_results_to_loop }} + steps: + - name: Send results to Loop + env: + LOOP_WEBHOOK_URL: ${{ secrets.LOOP_WEBHOOK_URL }} + TAG: ${{ github.event.inputs.tag }} + CHANNEL: ${{ github.event.inputs.channel }} + CE_ENABLED: ${{ inputs.ce }} + EE_ENABLED: ${{ inputs.ee }} + CE_RESULT: ${{ needs.job-CE.result }} + EE_RESULT: ${{ needs.job-EE.result }} + SE_PLUS_RESULT: ${{ needs.job-SE-Plus.result }} + FE_RESULT: ${{ needs.job-FE.result }} + # will be `success` only if all jobs in the matrix have succeeded + CHECK_RESULT: ${{ needs.check-version-on-release-channel.result }} + run: | + export TZ=Europe/Moscow + DATE=$(date +"%Y-%m-%d %H:%M:%S UTC+03:00") + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + # Determine overall status + OVERALL_STATUS=":white_check_mark: **SUCCESS!**" + if [[ "$CE_RESULT" == "failure" ]] || [[ "$EE_RESULT" == "failure" ]] || \ + [[ "$SE_PLUS_RESULT" == "failure" ]] || [[ "$FE_RESULT" == "failure" ]] || \ + [[ "$CHECK_RESULT" == "failure" ]]; then + OVERALL_STATUS=":x: **FAILED!**" + elif [[ "$CE_RESULT" == "cancelled" ]] || [[ "$EE_RESULT" == "cancelled" ]] || \ + [[ "$SE_PLUS_RESULT" == "cancelled" ]] || [[ "$FE_RESULT" == "cancelled" ]] || \ + [[ "$CHECK_RESULT" == "cancelled" ]]; then + OVERALL_STATUS=":warning: **CANCELLED!**" + fi + + # Build editions status + get_status_emoji() { + case "$1" in + success) echo ":white_check_mark:" ;; + failure) echo ":x:" ;; + cancelled) echo ":warning:" ;; + skipped) echo ":fast_forward:" ;; + *) echo ":grey_question:" ;; + esac + } + + EDITIONS_STATUS="" + if [[ "$CE_ENABLED" == "true" ]]; then + EDITIONS_STATUS+="| CE | $(get_status_emoji $CE_RESULT) **${CE_RESULT^^}** |\n" + fi + if [[ "$EE_ENABLED" == "true" ]]; then + EDITIONS_STATUS+="| EE | $(get_status_emoji $EE_RESULT) **${EE_RESULT^^}** |\n" + EDITIONS_STATUS+="| SE Plus | $(get_status_emoji $SE_PLUS_RESULT) **${SE_PLUS_RESULT^^}** |\n" + EDITIONS_STATUS+="| FE | $(get_status_emoji $FE_RESULT) **${FE_RESULT^^}** |\n" + fi + + RELEASE_SUMMARY="## :dvp: **DVP | Release ${TAG} to ${CHANNEL}**\n\n" + RELEASE_SUMMARY+="**Status:** ${OVERALL_STATUS}\n" + RELEASE_SUMMARY+="**Date:** ${DATE}\n\n" + + if [[ -n "$EDITIONS_STATUS" ]]; then + RELEASE_SUMMARY+="| Edition | Status |\n" + RELEASE_SUMMARY+="|---|---|\n" + RELEASE_SUMMARY+="${EDITIONS_STATUS}" + RELEASE_SUMMARY+="\n" + fi + + RELEASE_SUMMARY+="**Version Check:** $(get_status_emoji $CHECK_RESULT) **${CHECK_RESULT^^}**\n" + RELEASE_SUMMARY+="[:link: GitHub Actions Output](${RUN_URL})\n\n" + + echo -e "$RELEASE_SUMMARY" + curl --request POST --header 'Content-Type: application/json' --data "{\"text\": \"${RELEASE_SUMMARY}\"}" $LOOP_WEBHOOK_URL diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0798ce6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# 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/ + +# Go +go.work +go.work.sum + 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/CHANGELOG/v0.0.1.yaml b/CHANGELOG/v0.0.1.yaml new file mode 100644 index 0000000..6508513 --- /dev/null +++ b/CHANGELOG/v0.0.1.yaml @@ -0,0 +1,5 @@ +features: + - initial release with basic capabilities +fixes: [] +security: [] +chore: [] diff --git a/CHANGELOG/v0.0.2.yaml b/CHANGELOG/v0.0.2.yaml new file mode 100644 index 0000000..7b55dca --- /dev/null +++ b/CHANGELOG/v0.0.2.yaml @@ -0,0 +1,5 @@ +features: + - apply deckhouse runtime time review recommendations +fixes: [] +security: [] +chore: [] 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..a279874 --- /dev/null +++ b/Chart.yaml @@ -0,0 +1,6 @@ +name: operator-helm +version: 0.0.2 +dependencies: + - name: deckhouse_lib_helm + version: 1.71.2 + repository: https://deckhouse.github.io/lib-helm diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..84e67aa --- /dev/null +++ b/LICENSE @@ -0,0 +1,214 @@ +Copyright (c) 2026 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..c860715 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,6 @@ +# Core maintainers + +| Name | Email | GitHub | +|---------------|---------------------------|----------------------------------------| +| Ilya Drey | ilya.drey@flant.com | [@drey](https://github.com/drey) | +| Evgeniy Frolov | evgeniy.frolov@flant.com | [@Fral738](https://github.com/Fral738) | \ No newline at end of file diff --git a/README.md b/README.md index 3a20abb..f997fab 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,3 @@ [Deckhouse Kubernetes Platform](https://deckhouse.io/) module to deploy helm applications declaratively. -## Description - - - -### Resource requirements: - - - -## What do I need to enable the module? - - 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..5b462cb --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,92 @@ +version: "3" + +silent: true + +vars: + deckhouse_lib_helm_ver: 1.71.2 + target: "" + VALIDATION_FILES: "tools/validation/{main,messages,diff,doc_changes}.go" + +includes: + e2e: + taskfile: ./tests/e2e/Taskfile.dist.yaml + dir: ./tests/e2e + +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 reference + - | + 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\"" + + lint: + cmds: + - task: lint:doc-ru + - task: lint:prettier:yaml + + 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 reference + - | + 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:doc-changes: + desc: "Doc-changes" + cmds: + - go run {{ .VALIDATION_FILES }} --type doc-changes diff --git a/api/Taskfile.dist.yaml b/api/Taskfile.dist.yaml new file mode 100644 index 0000000..f58ee84 --- /dev/null +++ b/api/Taskfile.dist.yaml @@ -0,0 +1,42 @@ +version: "3" + +silent: false + +tasks: + generate: + desc: "Regenerate all" + cmds: + - ./scripts/update-codegen.sh all + - task: format:yaml + + generate:v1alpha1: + desc: "Regenerate code for core components." + cmd: ./scripts/update-codegen.sh v1alpha1 + + ci:generate: + desc: "Run generations and check git diff to ensure all files are committed" + cmds: + - task: generate + - task: _ci:verify-gen + + generate:crds: + desc: "Regenerate crds" + cmds: + - ./scripts/update-codegen.sh crds + # - task: format:yaml + + format:yaml: + desc: "Format non-templated YAML files, e.g. CRDs" + cmds: + # TODO: replace prettier image + - | + cd ../ && docker run --rm \ + -v "$(pwd):/tmp/operator-helm" ghcr.io/deckhouse/virtualization/prettier:3.2.5 \ + sh -c "cd /tmp/operator-helm ; prettier -w \"crds/*.yaml\"" + + _ci:verify-gen: + desc: "Check generated files are up-to-date." + internal: true + cmds: + - | + git diff --exit-code || (echo "Please run task gen:api and commit changes" && exit 1) diff --git a/api/client/generated/clientset/versioned/clientset.go b/api/client/generated/clientset/versioned/clientset.go new file mode 100644 index 0000000..93bd3a5 --- /dev/null +++ b/api/client/generated/clientset/versioned/clientset.go @@ -0,0 +1,120 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package versioned + +import ( + fmt "fmt" + http "net/http" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + discovery "k8s.io/client-go/discovery" + rest "k8s.io/client-go/rest" + flowcontrol "k8s.io/client-go/util/flowcontrol" +) + +type Interface interface { + Discovery() discovery.DiscoveryInterface + HelmV1alpha1() helmv1alpha1.HelmV1alpha1Interface +} + +// Clientset contains the clients for groups. +type Clientset struct { + *discovery.DiscoveryClient + helmV1alpha1 *helmv1alpha1.HelmV1alpha1Client +} + +// HelmV1alpha1 retrieves the HelmV1alpha1Client +func (c *Clientset) HelmV1alpha1() helmv1alpha1.HelmV1alpha1Interface { + return c.helmV1alpha1 +} + +// Discovery retrieves the DiscoveryClient +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + if c == nil { + return nil + } + return c.DiscoveryClient +} + +// NewForConfig creates a new Clientset for the given config. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfig will generate a rate-limiter in configShallowCopy. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*Clientset, error) { + configShallowCopy := *c + + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + + // share the transport between all clients + httpClient, err := rest.HTTPClientFor(&configShallowCopy) + if err != nil { + return nil, err + } + + return NewForConfigAndClient(&configShallowCopy, httpClient) +} + +// NewForConfigAndClient creates a new Clientset for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfigAndClient will generate a rate-limiter in configShallowCopy. +func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { + configShallowCopy := *c + if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { + if configShallowCopy.Burst <= 0 { + return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") + } + configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) + } + + var cs Clientset + var err error + cs.helmV1alpha1, err = helmv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + + cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + return &cs, nil +} + +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *Clientset { + cs, err := NewForConfig(c) + if err != nil { + panic(err) + } + return cs +} + +// New creates a new Clientset for the given RESTClient. +func New(c rest.Interface) *Clientset { + var cs Clientset + cs.helmV1alpha1 = helmv1alpha1.New(c) + + cs.DiscoveryClient = discovery.NewDiscoveryClient(c) + return &cs +} diff --git a/api/client/generated/clientset/versioned/fake/clientset_generated.go b/api/client/generated/clientset/versioned/fake/clientset_generated.go new file mode 100644 index 0000000..947f1d9 --- /dev/null +++ b/api/client/generated/clientset/versioned/fake/clientset_generated.go @@ -0,0 +1,105 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + clientset "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + helmv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + fakehelmv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/testing" +) + +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any field management, validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +// +// Deprecated: NewClientset replaces this with support for field management, which significantly improves +// server side apply testing. NewClientset is only available when apply configurations are generated (e.g. +// via --with-applyconfig). +func NewSimpleClientset(objects ...runtime.Object) *Clientset { + o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + var opts metav1.ListOptions + if watchAction, ok := action.(testing.WatchActionImpl); ok { + opts = watchAction.ListOptions + } + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns, opts) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +// Clientset implements clientset.Interface. Meant to be embedded into a +// struct to get a default implementation. This makes faking out just the method +// you want to test easier. +type Clientset struct { + testing.Fake + discovery *fakediscovery.FakeDiscovery + tracker testing.ObjectTracker +} + +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + return c.discovery +} + +func (c *Clientset) Tracker() testing.ObjectTracker { + return c.tracker +} + +// IsWatchListSemanticsSupported informs the reflector that this client +// doesn't support WatchList semantics. +// +// This is a synthetic method whose sole purpose is to satisfy the optional +// interface check performed by the reflector. +// Returning true signals that WatchList can NOT be used. +// No additional logic is implemented here. +func (c *Clientset) IsWatchListSemanticsUnSupported() bool { + return true +} + +var ( + _ clientset.Interface = &Clientset{} + _ testing.FakeClient = &Clientset{} +) + +// HelmV1alpha1 retrieves the HelmV1alpha1Client +func (c *Clientset) HelmV1alpha1() helmv1alpha1.HelmV1alpha1Interface { + return &fakehelmv1alpha1.FakeHelmV1alpha1{Fake: &c.Fake} +} diff --git a/api/client/generated/clientset/versioned/fake/doc.go b/api/client/generated/clientset/versioned/fake/doc.go new file mode 100644 index 0000000..06b4977 --- /dev/null +++ b/api/client/generated/clientset/versioned/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated fake clientset. +package fake diff --git a/api/client/generated/clientset/versioned/fake/register.go b/api/client/generated/clientset/versioned/fake/register.go new file mode 100644 index 0000000..0b233ce --- /dev/null +++ b/api/client/generated/clientset/versioned/fake/register.go @@ -0,0 +1,56 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + +var localSchemeBuilder = runtime.SchemeBuilder{ + helmv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(scheme)) +} diff --git a/api/client/generated/clientset/versioned/scheme/doc.go b/api/client/generated/clientset/versioned/scheme/doc.go new file mode 100644 index 0000000..14d115f --- /dev/null +++ b/api/client/generated/clientset/versioned/scheme/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package contains the scheme of the automatically generated clientset. +package scheme diff --git a/api/client/generated/clientset/versioned/scheme/register.go b/api/client/generated/clientset/versioned/scheme/register.go new file mode 100644 index 0000000..a1c34af --- /dev/null +++ b/api/client/generated/clientset/versioned/scheme/register.go @@ -0,0 +1,56 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package scheme + +import ( + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) +var ParameterCodec = runtime.NewParameterCodec(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + helmv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(Scheme)) +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/api_client.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/api_client.go new file mode 100644 index 0000000..29edccb --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/api_client.go @@ -0,0 +1,111 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + http "net/http" + + scheme "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/scheme" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + rest "k8s.io/client-go/rest" +) + +type HelmV1alpha1Interface interface { + RESTClient() rest.Interface + HelmClusterAddonsGetter + HelmClusterAddonChartsGetter + HelmClusterAddonRepositoriesGetter +} + +// HelmV1alpha1Client is used to interact with features provided by the helm.deckhouse.io group. +type HelmV1alpha1Client struct { + restClient rest.Interface +} + +func (c *HelmV1alpha1Client) HelmClusterAddons() HelmClusterAddonInterface { + return newHelmClusterAddons(c) +} + +func (c *HelmV1alpha1Client) HelmClusterAddonCharts() HelmClusterAddonChartInterface { + return newHelmClusterAddonCharts(c) +} + +func (c *HelmV1alpha1Client) HelmClusterAddonRepositories() HelmClusterAddonRepositoryInterface { + return newHelmClusterAddonRepositories(c) +} + +// NewForConfig creates a new HelmV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*HelmV1alpha1Client, error) { + config := *c + setConfigDefaults(&config) + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new HelmV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*HelmV1alpha1Client, error) { + config := *c + setConfigDefaults(&config) + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &HelmV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new HelmV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *HelmV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new HelmV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *HelmV1alpha1Client { + return &HelmV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) { + gv := apiv1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *HelmV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/doc.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/doc.go new file mode 100644 index 0000000..51e5450 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/doc.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/doc.go new file mode 100644 index 0000000..ea82301 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go new file mode 100644 index 0000000..5b3bbb8 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go @@ -0,0 +1,48 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeHelmV1alpha1 struct { + *testing.Fake +} + +func (c *FakeHelmV1alpha1) HelmClusterAddons() v1alpha1.HelmClusterAddonInterface { + return newFakeHelmClusterAddons(c) +} + +func (c *FakeHelmV1alpha1) HelmClusterAddonCharts() v1alpha1.HelmClusterAddonChartInterface { + return newFakeHelmClusterAddonCharts(c) +} + +func (c *FakeHelmV1alpha1) HelmClusterAddonRepositories() v1alpha1.HelmClusterAddonRepositoryInterface { + return newFakeHelmClusterAddonRepositories(c) +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeHelmV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddon.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddon.go new file mode 100644 index 0000000..909f672 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddon.go @@ -0,0 +1,52 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + v1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeHelmClusterAddons implements HelmClusterAddonInterface +type fakeHelmClusterAddons struct { + *gentype.FakeClientWithList[*v1alpha1.HelmClusterAddon, *v1alpha1.HelmClusterAddonList] + Fake *FakeHelmV1alpha1 +} + +func newFakeHelmClusterAddons(fake *FakeHelmV1alpha1) apiv1alpha1.HelmClusterAddonInterface { + return &fakeHelmClusterAddons{ + gentype.NewFakeClientWithList[*v1alpha1.HelmClusterAddon, *v1alpha1.HelmClusterAddonList]( + fake.Fake, + "", + v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddons"), + v1alpha1.SchemeGroupVersion.WithKind("HelmClusterAddon"), + func() *v1alpha1.HelmClusterAddon { return &v1alpha1.HelmClusterAddon{} }, + func() *v1alpha1.HelmClusterAddonList { return &v1alpha1.HelmClusterAddonList{} }, + func(dst, src *v1alpha1.HelmClusterAddonList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.HelmClusterAddonList) []*v1alpha1.HelmClusterAddon { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.HelmClusterAddonList, items []*v1alpha1.HelmClusterAddon) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonchart.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonchart.go new file mode 100644 index 0000000..f0bf23d --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonchart.go @@ -0,0 +1,52 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + v1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeHelmClusterAddonCharts implements HelmClusterAddonChartInterface +type fakeHelmClusterAddonCharts struct { + *gentype.FakeClientWithList[*v1alpha1.HelmClusterAddonChart, *v1alpha1.HelmClusterAddonChartList] + Fake *FakeHelmV1alpha1 +} + +func newFakeHelmClusterAddonCharts(fake *FakeHelmV1alpha1) apiv1alpha1.HelmClusterAddonChartInterface { + return &fakeHelmClusterAddonCharts{ + gentype.NewFakeClientWithList[*v1alpha1.HelmClusterAddonChart, *v1alpha1.HelmClusterAddonChartList]( + fake.Fake, + "", + v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddoncharts"), + v1alpha1.SchemeGroupVersion.WithKind("HelmClusterAddonChart"), + func() *v1alpha1.HelmClusterAddonChart { return &v1alpha1.HelmClusterAddonChart{} }, + func() *v1alpha1.HelmClusterAddonChartList { return &v1alpha1.HelmClusterAddonChartList{} }, + func(dst, src *v1alpha1.HelmClusterAddonChartList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.HelmClusterAddonChartList) []*v1alpha1.HelmClusterAddonChart { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.HelmClusterAddonChartList, items []*v1alpha1.HelmClusterAddonChart) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonrepository.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonrepository.go new file mode 100644 index 0000000..bee2b28 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonrepository.go @@ -0,0 +1,52 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + v1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeHelmClusterAddonRepositories implements HelmClusterAddonRepositoryInterface +type fakeHelmClusterAddonRepositories struct { + *gentype.FakeClientWithList[*v1alpha1.HelmClusterAddonRepository, *v1alpha1.HelmClusterAddonRepositoryList] + Fake *FakeHelmV1alpha1 +} + +func newFakeHelmClusterAddonRepositories(fake *FakeHelmV1alpha1) apiv1alpha1.HelmClusterAddonRepositoryInterface { + return &fakeHelmClusterAddonRepositories{ + gentype.NewFakeClientWithList[*v1alpha1.HelmClusterAddonRepository, *v1alpha1.HelmClusterAddonRepositoryList]( + fake.Fake, + "", + v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddonrepositories"), + v1alpha1.SchemeGroupVersion.WithKind("HelmClusterAddonRepository"), + func() *v1alpha1.HelmClusterAddonRepository { return &v1alpha1.HelmClusterAddonRepository{} }, + func() *v1alpha1.HelmClusterAddonRepositoryList { return &v1alpha1.HelmClusterAddonRepositoryList{} }, + func(dst, src *v1alpha1.HelmClusterAddonRepositoryList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.HelmClusterAddonRepositoryList) []*v1alpha1.HelmClusterAddonRepository { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.HelmClusterAddonRepositoryList, items []*v1alpha1.HelmClusterAddonRepository) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/generated_expansion.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/generated_expansion.go new file mode 100644 index 0000000..911c8ea --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/generated_expansion.go @@ -0,0 +1,25 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type HelmClusterAddonExpansion interface{} + +type HelmClusterAddonChartExpansion interface{} + +type HelmClusterAddonRepositoryExpansion interface{} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddon.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddon.go new file mode 100644 index 0000000..05aeac5 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddon.go @@ -0,0 +1,70 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + scheme "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/scheme" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// HelmClusterAddonsGetter has a method to return a HelmClusterAddonInterface. +// A group's client should implement this interface. +type HelmClusterAddonsGetter interface { + HelmClusterAddons() HelmClusterAddonInterface +} + +// HelmClusterAddonInterface has methods to work with HelmClusterAddon resources. +type HelmClusterAddonInterface interface { + Create(ctx context.Context, helmClusterAddon *apiv1alpha1.HelmClusterAddon, opts v1.CreateOptions) (*apiv1alpha1.HelmClusterAddon, error) + Update(ctx context.Context, helmClusterAddon *apiv1alpha1.HelmClusterAddon, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddon, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, helmClusterAddon *apiv1alpha1.HelmClusterAddon, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddon, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*apiv1alpha1.HelmClusterAddon, error) + List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.HelmClusterAddonList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apiv1alpha1.HelmClusterAddon, err error) + HelmClusterAddonExpansion +} + +// helmClusterAddons implements HelmClusterAddonInterface +type helmClusterAddons struct { + *gentype.ClientWithList[*apiv1alpha1.HelmClusterAddon, *apiv1alpha1.HelmClusterAddonList] +} + +// newHelmClusterAddons returns a HelmClusterAddons +func newHelmClusterAddons(c *HelmV1alpha1Client) *helmClusterAddons { + return &helmClusterAddons{ + gentype.NewClientWithList[*apiv1alpha1.HelmClusterAddon, *apiv1alpha1.HelmClusterAddonList]( + "helmclusteraddons", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *apiv1alpha1.HelmClusterAddon { return &apiv1alpha1.HelmClusterAddon{} }, + func() *apiv1alpha1.HelmClusterAddonList { return &apiv1alpha1.HelmClusterAddonList{} }, + ), + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonchart.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonchart.go new file mode 100644 index 0000000..8db564d --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonchart.go @@ -0,0 +1,70 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + scheme "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/scheme" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// HelmClusterAddonChartsGetter has a method to return a HelmClusterAddonChartInterface. +// A group's client should implement this interface. +type HelmClusterAddonChartsGetter interface { + HelmClusterAddonCharts() HelmClusterAddonChartInterface +} + +// HelmClusterAddonChartInterface has methods to work with HelmClusterAddonChart resources. +type HelmClusterAddonChartInterface interface { + Create(ctx context.Context, helmClusterAddonChart *apiv1alpha1.HelmClusterAddonChart, opts v1.CreateOptions) (*apiv1alpha1.HelmClusterAddonChart, error) + Update(ctx context.Context, helmClusterAddonChart *apiv1alpha1.HelmClusterAddonChart, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddonChart, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, helmClusterAddonChart *apiv1alpha1.HelmClusterAddonChart, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddonChart, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*apiv1alpha1.HelmClusterAddonChart, error) + List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.HelmClusterAddonChartList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apiv1alpha1.HelmClusterAddonChart, err error) + HelmClusterAddonChartExpansion +} + +// helmClusterAddonCharts implements HelmClusterAddonChartInterface +type helmClusterAddonCharts struct { + *gentype.ClientWithList[*apiv1alpha1.HelmClusterAddonChart, *apiv1alpha1.HelmClusterAddonChartList] +} + +// newHelmClusterAddonCharts returns a HelmClusterAddonCharts +func newHelmClusterAddonCharts(c *HelmV1alpha1Client) *helmClusterAddonCharts { + return &helmClusterAddonCharts{ + gentype.NewClientWithList[*apiv1alpha1.HelmClusterAddonChart, *apiv1alpha1.HelmClusterAddonChartList]( + "helmclusteraddoncharts", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *apiv1alpha1.HelmClusterAddonChart { return &apiv1alpha1.HelmClusterAddonChart{} }, + func() *apiv1alpha1.HelmClusterAddonChartList { return &apiv1alpha1.HelmClusterAddonChartList{} }, + ), + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonrepository.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonrepository.go new file mode 100644 index 0000000..99494aa --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonrepository.go @@ -0,0 +1,72 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + scheme "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/scheme" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// HelmClusterAddonRepositoriesGetter has a method to return a HelmClusterAddonRepositoryInterface. +// A group's client should implement this interface. +type HelmClusterAddonRepositoriesGetter interface { + HelmClusterAddonRepositories() HelmClusterAddonRepositoryInterface +} + +// HelmClusterAddonRepositoryInterface has methods to work with HelmClusterAddonRepository resources. +type HelmClusterAddonRepositoryInterface interface { + Create(ctx context.Context, helmClusterAddonRepository *apiv1alpha1.HelmClusterAddonRepository, opts v1.CreateOptions) (*apiv1alpha1.HelmClusterAddonRepository, error) + Update(ctx context.Context, helmClusterAddonRepository *apiv1alpha1.HelmClusterAddonRepository, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddonRepository, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, helmClusterAddonRepository *apiv1alpha1.HelmClusterAddonRepository, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddonRepository, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*apiv1alpha1.HelmClusterAddonRepository, error) + List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.HelmClusterAddonRepositoryList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apiv1alpha1.HelmClusterAddonRepository, err error) + HelmClusterAddonRepositoryExpansion +} + +// helmClusterAddonRepositories implements HelmClusterAddonRepositoryInterface +type helmClusterAddonRepositories struct { + *gentype.ClientWithList[*apiv1alpha1.HelmClusterAddonRepository, *apiv1alpha1.HelmClusterAddonRepositoryList] +} + +// newHelmClusterAddonRepositories returns a HelmClusterAddonRepositories +func newHelmClusterAddonRepositories(c *HelmV1alpha1Client) *helmClusterAddonRepositories { + return &helmClusterAddonRepositories{ + gentype.NewClientWithList[*apiv1alpha1.HelmClusterAddonRepository, *apiv1alpha1.HelmClusterAddonRepositoryList]( + "helmclusteraddonrepositories", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *apiv1alpha1.HelmClusterAddonRepository { return &apiv1alpha1.HelmClusterAddonRepository{} }, + func() *apiv1alpha1.HelmClusterAddonRepositoryList { + return &apiv1alpha1.HelmClusterAddonRepositoryList{} + }, + ), + } +} diff --git a/api/client/generated/informers/externalversions/api/interface.go b/api/client/generated/informers/externalversions/api/interface.go new file mode 100644 index 0000000..2977c43 --- /dev/null +++ b/api/client/generated/informers/externalversions/api/interface.go @@ -0,0 +1,46 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package api + +import ( + v1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/api/v1alpha1" + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddon.go b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddon.go new file mode 100644 index 0000000..8a5f46d --- /dev/null +++ b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddon.go @@ -0,0 +1,101 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + versioned "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/listers/api/v1alpha1" + operatorhelmapiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterAddonInformer provides access to a shared informer and lister for +// HelmClusterAddons. +type HelmClusterAddonInformer interface { + Informer() cache.SharedIndexInformer + Lister() apiv1alpha1.HelmClusterAddonLister +} + +type helmClusterAddonInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewHelmClusterAddonInformer constructs a new informer for HelmClusterAddon type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewHelmClusterAddonInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredHelmClusterAddonInformer constructs a new informer for HelmClusterAddon type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredHelmClusterAddonInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddons().List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddons().Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddons().List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddons().Watch(ctx, options) + }, + }, client), + &operatorhelmapiv1alpha1.HelmClusterAddon{}, + resyncPeriod, + indexers, + ) +} + +func (f *helmClusterAddonInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *helmClusterAddonInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&operatorhelmapiv1alpha1.HelmClusterAddon{}, f.defaultInformer) +} + +func (f *helmClusterAddonInformer) Lister() apiv1alpha1.HelmClusterAddonLister { + return apiv1alpha1.NewHelmClusterAddonLister(f.Informer().GetIndexer()) +} diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonchart.go b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonchart.go new file mode 100644 index 0000000..5e14371 --- /dev/null +++ b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonchart.go @@ -0,0 +1,101 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + versioned "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/listers/api/v1alpha1" + operatorhelmapiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterAddonChartInformer provides access to a shared informer and lister for +// HelmClusterAddonCharts. +type HelmClusterAddonChartInformer interface { + Informer() cache.SharedIndexInformer + Lister() apiv1alpha1.HelmClusterAddonChartLister +} + +type helmClusterAddonChartInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewHelmClusterAddonChartInformer constructs a new informer for HelmClusterAddonChart type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewHelmClusterAddonChartInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonChartInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredHelmClusterAddonChartInformer constructs a new informer for HelmClusterAddonChart type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredHelmClusterAddonChartInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonCharts().List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonCharts().Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonCharts().List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonCharts().Watch(ctx, options) + }, + }, client), + &operatorhelmapiv1alpha1.HelmClusterAddonChart{}, + resyncPeriod, + indexers, + ) +} + +func (f *helmClusterAddonChartInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonChartInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *helmClusterAddonChartInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&operatorhelmapiv1alpha1.HelmClusterAddonChart{}, f.defaultInformer) +} + +func (f *helmClusterAddonChartInformer) Lister() apiv1alpha1.HelmClusterAddonChartLister { + return apiv1alpha1.NewHelmClusterAddonChartLister(f.Informer().GetIndexer()) +} diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonrepository.go b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonrepository.go new file mode 100644 index 0000000..b314028 --- /dev/null +++ b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonrepository.go @@ -0,0 +1,101 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + versioned "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/listers/api/v1alpha1" + operatorhelmapiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterAddonRepositoryInformer provides access to a shared informer and lister for +// HelmClusterAddonRepositories. +type HelmClusterAddonRepositoryInformer interface { + Informer() cache.SharedIndexInformer + Lister() apiv1alpha1.HelmClusterAddonRepositoryLister +} + +type helmClusterAddonRepositoryInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewHelmClusterAddonRepositoryInformer constructs a new informer for HelmClusterAddonRepository type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewHelmClusterAddonRepositoryInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonRepositoryInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredHelmClusterAddonRepositoryInformer constructs a new informer for HelmClusterAddonRepository type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredHelmClusterAddonRepositoryInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonRepositories().List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonRepositories().Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonRepositories().List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonRepositories().Watch(ctx, options) + }, + }, client), + &operatorhelmapiv1alpha1.HelmClusterAddonRepository{}, + resyncPeriod, + indexers, + ) +} + +func (f *helmClusterAddonRepositoryInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonRepositoryInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *helmClusterAddonRepositoryInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&operatorhelmapiv1alpha1.HelmClusterAddonRepository{}, f.defaultInformer) +} + +func (f *helmClusterAddonRepositoryInformer) Lister() apiv1alpha1.HelmClusterAddonRepositoryLister { + return apiv1alpha1.NewHelmClusterAddonRepositoryLister(f.Informer().GetIndexer()) +} diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/interface.go b/api/client/generated/informers/externalversions/api/v1alpha1/interface.go new file mode 100644 index 0000000..e8deecc --- /dev/null +++ b/api/client/generated/informers/externalversions/api/v1alpha1/interface.go @@ -0,0 +1,59 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // HelmClusterAddons returns a HelmClusterAddonInformer. + HelmClusterAddons() HelmClusterAddonInformer + // HelmClusterAddonCharts returns a HelmClusterAddonChartInformer. + HelmClusterAddonCharts() HelmClusterAddonChartInformer + // HelmClusterAddonRepositories returns a HelmClusterAddonRepositoryInformer. + HelmClusterAddonRepositories() HelmClusterAddonRepositoryInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// HelmClusterAddons returns a HelmClusterAddonInformer. +func (v *version) HelmClusterAddons() HelmClusterAddonInformer { + return &helmClusterAddonInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} + +// HelmClusterAddonCharts returns a HelmClusterAddonChartInformer. +func (v *version) HelmClusterAddonCharts() HelmClusterAddonChartInformer { + return &helmClusterAddonChartInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} + +// HelmClusterAddonRepositories returns a HelmClusterAddonRepositoryInformer. +func (v *version) HelmClusterAddonRepositories() HelmClusterAddonRepositoryInformer { + return &helmClusterAddonRepositoryInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} diff --git a/api/client/generated/informers/externalversions/factory.go b/api/client/generated/informers/externalversions/factory.go new file mode 100644 index 0000000..df93e62 --- /dev/null +++ b/api/client/generated/informers/externalversions/factory.go @@ -0,0 +1,263 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + reflect "reflect" + sync "sync" + time "time" + + versioned "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + api "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/api" + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// SharedInformerOption defines the functional option type for SharedInformerFactory. +type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory + +type sharedInformerFactory struct { + client versioned.Interface + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc + lock sync.Mutex + defaultResync time.Duration + customResync map[reflect.Type]time.Duration + transform cache.TransformFunc + + informers map[reflect.Type]cache.SharedIndexInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[reflect.Type]bool + // wg tracks how many goroutines were started. + wg sync.WaitGroup + // shuttingDown is true when Shutdown has been called. It may still be running + // because it needs to wait for goroutines. + shuttingDown bool +} + +// WithCustomResyncConfig sets a custom resync period for the specified informer types. +func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + for k, v := range resyncConfig { + factory.customResync[reflect.TypeOf(k)] = v + } + return factory + } +} + +// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. +func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.tweakListOptions = tweakListOptions + return factory + } +} + +// WithNamespace limits the SharedInformerFactory to the specified namespace. +func WithNamespace(namespace string) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.namespace = namespace + return factory + } +} + +// WithTransform sets a transform on all informers. +func WithTransform(transform cache.TransformFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.transform = transform + return factory + } +} + +// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync) +} + +// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. +// Listers obtained via this SharedInformerFactory will be subject to the same filters +// as specified here. +// +// Deprecated: Please use NewSharedInformerFactoryWithOptions instead +func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) +} + +// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. +func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { + factory := &sharedInformerFactory{ + client: client, + namespace: v1.NamespaceAll, + defaultResync: defaultResync, + informers: make(map[reflect.Type]cache.SharedIndexInformer), + startedInformers: make(map[reflect.Type]bool), + customResync: make(map[reflect.Type]time.Duration), + } + + // Apply all options + for _, opt := range options { + factory = opt(factory) + } + + return factory +} + +func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + if f.shuttingDown { + return + } + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + f.wg.Add(1) + // We need a new variable in each loop iteration, + // otherwise the goroutine would use the loop variable + // and that keeps changing. + informer := informer + go func() { + defer f.wg.Done() + informer.Run(stopCh) + }() + f.startedInformers[informerType] = true + } + } +} + +func (f *sharedInformerFactory) Shutdown() { + f.lock.Lock() + f.shuttingDown = true + f.lock.Unlock() + + // Will return immediately if there is nothing to wait for. + f.wg.Wait() +} + +func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + informers := func() map[reflect.Type]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[reflect.Type]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer + } + } + return informers + }() + + res := map[reflect.Type]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// InformerFor returns the SharedIndexInformer for obj using an internal +// client. +func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informerType := reflect.TypeOf(obj) + informer, exists := f.informers[informerType] + if exists { + return informer + } + + resyncPeriod, exists := f.customResync[informerType] + if !exists { + resyncPeriod = f.defaultResync + } + + informer = newFunc(f.client, resyncPeriod) + informer.SetTransform(f.transform) + f.informers[informerType] = informer + + return informer +} + +// SharedInformerFactory provides shared informers for resources in all known +// API group versions. +// +// It is typically used like this: +// +// ctx, cancel := context.WithCancel(context.Background()) +// defer cancel() +// factory := NewSharedInformerFactory(client, resyncPeriod) +// defer factory.WaitForStop() // Returns immediately if nothing was started. +// genericInformer := factory.ForResource(resource) +// typedInformer := factory.SomeAPIGroup().V1().SomeType() +// factory.Start(ctx.Done()) // Start processing these informers. +// synced := factory.WaitForCacheSync(ctx.Done()) +// for v, ok := range synced { +// if !ok { +// fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v) +// return +// } +// } +// +// // Creating informers can also be created after Start, but then +// // Start must be called again: +// anotherGenericInformer := factory.ForResource(resource) +// factory.Start(ctx.Done()) +type SharedInformerFactory interface { + internalinterfaces.SharedInformerFactory + + // Start initializes all requested informers. They are handled in goroutines + // which run until the stop channel gets closed. + // Warning: Start does not block. When run in a go-routine, it will race with a later WaitForCacheSync. + Start(stopCh <-chan struct{}) + + // Shutdown marks a factory as shutting down. At that point no new + // informers can be started anymore and Start will return without + // doing anything. + // + // In addition, Shutdown blocks until all goroutines have terminated. For that + // to happen, the close channel(s) that they were started with must be closed, + // either before Shutdown gets called or while it is waiting. + // + // Shutdown may be called multiple times, even concurrently. All such calls will + // block until all goroutines have terminated. + Shutdown() + + // WaitForCacheSync blocks until all started informers' caches were synced + // or the stop channel gets closed. + WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + + // ForResource gives generic access to a shared informer of the matching type. + ForResource(resource schema.GroupVersionResource) (GenericInformer, error) + + // InformerFor returns the SharedIndexInformer for obj using an internal + // client. + InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer + + Helm() api.Interface +} + +func (f *sharedInformerFactory) Helm() api.Interface { + return api.New(f, f.namespace, f.tweakListOptions) +} diff --git a/api/client/generated/informers/externalversions/generic.go b/api/client/generated/informers/externalversions/generic.go new file mode 100644 index 0000000..aca9bbb --- /dev/null +++ b/api/client/generated/informers/externalversions/generic.go @@ -0,0 +1,66 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + fmt "fmt" + + v1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// GenericInformer is type of SharedIndexInformer which will locate and delegate to other +// sharedInformers based on type +type GenericInformer interface { + Informer() cache.SharedIndexInformer + Lister() cache.GenericLister +} + +type genericInformer struct { + informer cache.SharedIndexInformer + resource schema.GroupResource +} + +// Informer returns the SharedIndexInformer. +func (f *genericInformer) Informer() cache.SharedIndexInformer { + return f.informer +} + +// Lister returns the GenericLister. +func (f *genericInformer) Lister() cache.GenericLister { + return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) +} + +// ForResource gives generic access to a shared informer of the matching type +// TODO extend this to unknown resources with a client pool +func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { + switch resource { + // Group=helm.deckhouse.io, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddons"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Helm().V1alpha1().HelmClusterAddons().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddoncharts"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Helm().V1alpha1().HelmClusterAddonCharts().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddonrepositories"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Helm().V1alpha1().HelmClusterAddonRepositories().Informer()}, nil + + } + + return nil, fmt.Errorf("no informer found for %v", resource) +} diff --git a/api/client/generated/informers/externalversions/internalinterfaces/factory_interfaces.go b/api/client/generated/informers/externalversions/internalinterfaces/factory_interfaces.go new file mode 100644 index 0000000..77db6c7 --- /dev/null +++ b/api/client/generated/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -0,0 +1,40 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package internalinterfaces + +import ( + time "time" + + versioned "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" +) + +// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. +type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer + +// SharedInformerFactory a small interface to allow for adding an informer without an import cycle +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer +} + +// TweakListOptionsFunc is a function that transforms a v1.ListOptions. +type TweakListOptionsFunc func(*v1.ListOptions) diff --git a/api/client/generated/listers/api/v1alpha1/expansion_generated.go b/api/client/generated/listers/api/v1alpha1/expansion_generated.go new file mode 100644 index 0000000..8e4f30f --- /dev/null +++ b/api/client/generated/listers/api/v1alpha1/expansion_generated.go @@ -0,0 +1,31 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// HelmClusterAddonListerExpansion allows custom methods to be added to +// HelmClusterAddonLister. +type HelmClusterAddonListerExpansion interface{} + +// HelmClusterAddonChartListerExpansion allows custom methods to be added to +// HelmClusterAddonChartLister. +type HelmClusterAddonChartListerExpansion interface{} + +// HelmClusterAddonRepositoryListerExpansion allows custom methods to be added to +// HelmClusterAddonRepositoryLister. +type HelmClusterAddonRepositoryListerExpansion interface{} diff --git a/api/client/generated/listers/api/v1alpha1/helmclusteraddon.go b/api/client/generated/listers/api/v1alpha1/helmclusteraddon.go new file mode 100644 index 0000000..421d60b --- /dev/null +++ b/api/client/generated/listers/api/v1alpha1/helmclusteraddon.go @@ -0,0 +1,48 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterAddonLister helps list HelmClusterAddons. +// All objects returned here must be treated as read-only. +type HelmClusterAddonLister interface { + // List lists all HelmClusterAddons in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddon, err error) + // Get retrieves the HelmClusterAddon from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.HelmClusterAddon, error) + HelmClusterAddonListerExpansion +} + +// helmClusterAddonLister implements the HelmClusterAddonLister interface. +type helmClusterAddonLister struct { + listers.ResourceIndexer[*apiv1alpha1.HelmClusterAddon] +} + +// NewHelmClusterAddonLister returns a new HelmClusterAddonLister. +func NewHelmClusterAddonLister(indexer cache.Indexer) HelmClusterAddonLister { + return &helmClusterAddonLister{listers.New[*apiv1alpha1.HelmClusterAddon](indexer, apiv1alpha1.Resource("helmclusteraddon"))} +} diff --git a/api/client/generated/listers/api/v1alpha1/helmclusteraddonchart.go b/api/client/generated/listers/api/v1alpha1/helmclusteraddonchart.go new file mode 100644 index 0000000..ee09591 --- /dev/null +++ b/api/client/generated/listers/api/v1alpha1/helmclusteraddonchart.go @@ -0,0 +1,48 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterAddonChartLister helps list HelmClusterAddonCharts. +// All objects returned here must be treated as read-only. +type HelmClusterAddonChartLister interface { + // List lists all HelmClusterAddonCharts in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddonChart, err error) + // Get retrieves the HelmClusterAddonChart from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.HelmClusterAddonChart, error) + HelmClusterAddonChartListerExpansion +} + +// helmClusterAddonChartLister implements the HelmClusterAddonChartLister interface. +type helmClusterAddonChartLister struct { + listers.ResourceIndexer[*apiv1alpha1.HelmClusterAddonChart] +} + +// NewHelmClusterAddonChartLister returns a new HelmClusterAddonChartLister. +func NewHelmClusterAddonChartLister(indexer cache.Indexer) HelmClusterAddonChartLister { + return &helmClusterAddonChartLister{listers.New[*apiv1alpha1.HelmClusterAddonChart](indexer, apiv1alpha1.Resource("helmclusteraddonchart"))} +} diff --git a/api/client/generated/listers/api/v1alpha1/helmclusteraddonrepository.go b/api/client/generated/listers/api/v1alpha1/helmclusteraddonrepository.go new file mode 100644 index 0000000..5577c5f --- /dev/null +++ b/api/client/generated/listers/api/v1alpha1/helmclusteraddonrepository.go @@ -0,0 +1,48 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterAddonRepositoryLister helps list HelmClusterAddonRepositories. +// All objects returned here must be treated as read-only. +type HelmClusterAddonRepositoryLister interface { + // List lists all HelmClusterAddonRepositories in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddonRepository, err error) + // Get retrieves the HelmClusterAddonRepository from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.HelmClusterAddonRepository, error) + HelmClusterAddonRepositoryListerExpansion +} + +// helmClusterAddonRepositoryLister implements the HelmClusterAddonRepositoryLister interface. +type helmClusterAddonRepositoryLister struct { + listers.ResourceIndexer[*apiv1alpha1.HelmClusterAddonRepository] +} + +// NewHelmClusterAddonRepositoryLister returns a new HelmClusterAddonRepositoryLister. +func NewHelmClusterAddonRepositoryLister(indexer cache.Indexer) HelmClusterAddonRepositoryLister { + return &helmClusterAddonRepositoryLister{listers.New[*apiv1alpha1.HelmClusterAddonRepository](indexer, apiv1alpha1.Resource("helmclusteraddonrepository"))} +} diff --git a/api/doc.go b/api/doc.go new file mode 100644 index 0000000..8cb1e54 --- /dev/null +++ b/api/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..63d1f0c --- /dev/null +++ b/api/go.mod @@ -0,0 +1,72 @@ +module github.com/deckhouse/operator-helm/api + +go 1.25.0 + +tool ( + k8s.io/code-generator + k8s.io/kube-openapi/cmd/openapi-gen + sigs.k8s.io/controller-tools/cmd/controller-gen +) + +require ( + k8s.io/apiextensions-apiserver v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + 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/gobuffalo/flect v1.0.3 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/gomega v1.38.3 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.40.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.35.1 // indirect + k8s.io/code-generator v0.35.1 // indirect + k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/controller-tools v0.17.2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..ab95caa --- /dev/null +++ b/api/go.sum @@ -0,0 +1,154 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.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/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/code-generator v0.35.1 h1:yLKR2la7Z9cWT5qmk67ayx8xXLM4RRKQMnC8YPvTWRI= +k8s.io/code-generator v0.35.1/go.mod h1:F2Fhm7aA69tC/VkMXLDokdovltXEF026Tb9yfQXQWKg= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-tools v0.17.2 h1:jNFOKps8WnaRKZU2R+4vRCHnXyJanVmXBWqkuUPFyFg= +sigs.k8s.io/controller-tools v0.17.2/go.mod h1:4q5tZG2JniS5M5bkiXY2/potOiXyhoZVw/U48vLkXk0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/api/scripts/boilerplate.go.txt b/api/scripts/boilerplate.go.txt new file mode 100644 index 0000000..cc60635 --- /dev/null +++ b/api/scripts/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ diff --git a/api/scripts/update-codegen.sh b/api/scripts/update-codegen.sh new file mode 100755 index 0000000..d7be88a --- /dev/null +++ b/api/scripts/update-codegen.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +# Copyright 2026 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -o errexit +set -o nounset +set -o pipefail + +function usage { + cat <:`. + 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/crds/helmclusteraddoncharts.yaml b/crds/helmclusteraddoncharts.yaml new file mode 100644 index 0000000..ab56175 --- /dev/null +++ b/crds/helmclusteraddoncharts.yaml @@ -0,0 +1,132 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + labels: + heritage: deckhouse + module: operator-helm + name: helmclusteraddoncharts.helm.deckhouse.io +spec: + group: helm.deckhouse.io + names: + categories: + - all + - operator-helm + kind: HelmClusterAddonChart + listKind: HelmClusterAddonChartList + plural: helmclusteraddoncharts + singular: helmclusteraddonchart + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: + HelmClusterAddonChart represents a Helm chart and its versions + from specific repository. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + properties: + conditions: + description: + Conditions represent the latest available observations + of the addon chart state. + items: + description: + Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: + Generation represents resource generation that was last + processed by the controller. + format: int64 + type: integer + versions: + description: Available helm chart versions + items: + properties: + version: + description: Helm chart version + minLength: 1 + type: string + required: + - version + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/crds/helmclusteraddonrepositories.yaml b/crds/helmclusteraddonrepositories.yaml new file mode 100644 index 0000000..4c69883 --- /dev/null +++ b/crds/helmclusteraddonrepositories.yaml @@ -0,0 +1,166 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + labels: + heritage: deckhouse + module: operator-helm + name: helmclusteraddonrepositories.helm.deckhouse.io +spec: + group: helm.deckhouse.io + names: + categories: + - all + - operator-helm + kind: HelmClusterAddonRepository + listKind: HelmClusterAddonRepositoryList + plural: helmclusteraddonrepositories + singular: helmclusteraddonrepository + scope: Cluster + versions: + - additionalPrinterColumns: + - description: The readiness status of the repository + jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Status + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: + HelmClusterAddonRepository represents a Helm or an OCI compliant + repository with Helm charts. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + auth: + description: Auth contains authentication credentials for the repository. + properties: + password: + description: Repository authentication password. + minLength: 1 + type: string + username: + description: Repository authentication username. + minLength: 1 + type: string + required: + - password + - username + type: object + caCertificate: + description: + CACertificate is the PEM encoded CA certificate for TLS + verification. + type: string + tlsVerify: + default: true + description: TLSVerify enables or disables TLS certificate verification. + type: boolean + url: + description: + URL of the Helm repository. Supports http(s):// and oci:// + protocols. + type: string + x-kubernetes-validations: + - message: + URL must have a valid protocol (http, https, oci) and a + non-empty path + rule: self.matches('^(https?|oci)://.+$') + required: + - url + type: object + status: + properties: + conditions: + description: + Conditions represent the latest available observations + of the repository state. + items: + description: + Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: + Generation represents resource generation that was last + processed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/crds/helmclusteraddons.yaml b/crds/helmclusteraddons.yaml new file mode 100644 index 0000000..f976fca --- /dev/null +++ b/crds/helmclusteraddons.yaml @@ -0,0 +1,208 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + labels: + heritage: deckhouse + module: operator-helm + name: helmclusteraddons.helm.deckhouse.io +spec: + group: helm.deckhouse.io + names: + categories: + - all + - operator-helm + kind: HelmClusterAddon + listKind: HelmClusterAddonList + plural: helmclusteraddons + singular: helmclusteraddon + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Helm release chart name. + jsonPath: .spec.chart.helmClusterAddonChart + name: Chart Name + type: string + - description: Helm release chart version. + jsonPath: .spec.chart.version + name: Chart Version + type: string + - description: The readiness status of the addon + jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Status + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: + HelmClusterAddon represents a Helm addon that is installed across + the whole cluster. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + chart: + properties: + helmClusterAddonChart: + description: |- + Specifies the name of the Helm chart to be installed + from the defined repository (e.g., "ingress-nginx" or "redis"). + minLength: 1 + type: string + helmClusterAddonRepository: + description: |- + Specifies the name of the HelmClusterAddonRepository custom resource that contains + the connection details and credentials for the repository where + the chart is located. + maxLength: 63 + minLength: 3 + type: string + version: + description: Versions holds the HelmClusterAddon chart version. + type: string + required: + - helmClusterAddonChart + - helmClusterAddonRepository + type: object + maintenance: + description: |- + Maintenance specifies the reconciliation strategy for the resource. + When set to "NoResourceReconciliation", the controller will stop updating the + underlying resources, allowing for manual intervention or maintenance + without the operator overwriting changes. + When empty (""), standard reconciliation is active. + enum: + - "" + - NoResourceReconciliation + type: string + namespace: + default: default + description: Namespace to deploy cluster addon release + maxLength: 63 + minLength: 3 + type: string + values: + description: Values holds the values for this HelmClusterAddon release. + x-kubernetes-preserve-unknown-fields: true + required: + - chart + type: object + status: + properties: + conditions: + description: + Conditions represent the latest available observations + of the addon state. + items: + description: + Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastAppliedChart: + description: + LastAppliedChart represents the latest chart that triggered + addon install or update. + properties: + helmClusterAddonChart: + description: |- + Specifies the name of the Helm chart to be installed + from the defined repository (e.g., "ingress-nginx" or "redis"). + type: string + helmClusterAddonRepository: + description: |- + Specifies the name of the HelmClusterAddonRepository custom resource that contains + the connection details and credentials for the repository where + the chart is located. + type: string + version: + description: Versions holds the HelmClusterAddon chart version. + type: string + type: object + lastAppliedValues: + description: + LastAppliedValues represents the latest values that triggered + addon install or update. + x-kubernetes-preserve-unknown-fields: true + observedGeneration: + description: + Generation represents resource generation that was last + processed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000..80620c1 --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,5 @@ +--- +title: "Configuration" +description: "Deckhouse Kubernetes Platform — configuration parameters of the operator-helm module." +weight: 20 +--- diff --git a/docs/CONFIGURATION.ru.md b/docs/CONFIGURATION.ru.md new file mode 100644 index 0000000..7ceffe5 --- /dev/null +++ b/docs/CONFIGURATION.ru.md @@ -0,0 +1,5 @@ +--- +title: "Настройки" +description: "Deckhouse Kubernetes Platform, параметры конфигурации модуля operator-helm." +weight: 20 +--- diff --git a/docs/CR.md b/docs/CR.md new file mode 100644 index 0000000..ea293af --- /dev/null +++ b/docs/CR.md @@ -0,0 +1,5 @@ +--- +title: "Custom Resources" +description: "Deckhouse Kubernetes Platform — Custom resources of the operator-helm module." +weight: 60 +--- diff --git a/docs/CR.ru.md b/docs/CR.ru.md new file mode 100644 index 0000000..e22d7ed --- /dev/null +++ b/docs/CR.ru.md @@ -0,0 +1,5 @@ +--- +title: "Кастомные ресурсы" +description: "Deckhouse Kubernetes Platform, кастомные ресурсы (custom resources) модуля operator-helm." +weight: 60 +--- diff --git a/docs/EXAMPLE.md b/docs/EXAMPLE.md new file mode 100644 index 0000000..60973f7 --- /dev/null +++ b/docs/EXAMPLE.md @@ -0,0 +1,83 @@ +--- +title: "Examples" +description: "Deckhouse Kubernetes Platform — usage examples for the operator-helm module." +weight: 30 +--- + +## Adding a Helm repository + +To add a repository, create a HelmClusterAddonRepository resource: + +```yaml +apiVersion: helm.deckhouse.io/v1alpha1 +kind: HelmClusterAddonRepository +metadata: + name: podinfo +spec: + url: https://stefanprodan.github.io/podinfo +``` + +After creating the repository, view the available Helm charts: + +```shell +d8 k get helmclusteraddoncharts.helm.deckhouse.io -l repository=podinfo +``` + +Example output: + +```text +NAME AGE +podinfo-podinfo 56s +``` + +To view the list of versions available for a specific chart: + +```shell +d8 k get helmclusteraddonchart podinfo-podinfo -o yaml +``` + +Example output: + +```yaml +apiVersion: helm.deckhouse.io/v1alpha1 +kind: HelmClusterAddonChart +metadata: + labels: + chart: podinfo + heritage: deckhouse + repository: podinfo + name: podinfo-podinfo +status: + versions: + - digest: a5c4b7381a0907128243354ab100d2eecc480d7dcac5014ff7272b0acef03780 + pulled: false + version: 6.11.0 + - digest: 9f1cdb52fc5a57848f377b146919f8eb2c4a2c0ab8815bd019ec41c1d1895c0c + pulled: false + version: 6.10.2 +``` + +## Deploying an application + +To deploy an application, create a HelmClusterAddon resource specifying the repository name, chart name and version, and the target namespace: + +```yaml +apiVersion: helm.deckhouse.io/v1alpha1 +kind: HelmClusterAddon +metadata: + name: podinfo +spec: + namespace: test + chart: + helmClusterAddonChart: podinfo + helmClusterAddonRepository: podinfo + version: 6.10.2 +``` + +{{< alert level="warning" >}} +Only one instance of HelmClusterAddon using a specific Helm chart from a specific repository can be deployed at a time. Different Helm charts from the same repository can be deployed simultaneously. +{{< /alert >}} + +{{< alert level="info" >}} +The `.spec.chart.version` parameter is optional. If omitted, the latest available version of the chart will be installed. +{{< /alert >}} diff --git a/docs/EXAMPLE.ru.md b/docs/EXAMPLE.ru.md new file mode 100644 index 0000000..bfa9f62 --- /dev/null +++ b/docs/EXAMPLE.ru.md @@ -0,0 +1,83 @@ +--- +title: "Примеры" +description: "Deckhouse Kubernetes Platform — примеры использования модуля operator-helm." +weight: 30 +--- + +## Добавление Helm-репозитория + +Для добавления репозитория создайте ресурс HelmClusterAddonRepository: + +```yaml +apiVersion: helm.deckhouse.io/v1alpha1 +kind: HelmClusterAddonRepository +metadata: + name: podinfo +spec: + url: https://stefanprodan.github.io/podinfo +``` + +После создания репозитория можно просмотреть доступные в нём Helm-чарты: + +```shell +d8 k get helmclusteraddoncharts.helm.deckhouse.io -l repository=podinfo +``` + +Пример вывода: + +```text +NAME AGE +podinfo-podinfo 56s +``` + +Для просмотра списка версий, доступных для заданного чарта: + +```shell +d8 k get helmclusteraddonchart podinfo-podinfo -o yaml +``` + +Пример вывода: + +```yaml +apiVersion: helm.deckhouse.io/v1alpha1 +kind: HelmClusterAddonChart +metadata: + labels: + chart: podinfo + heritage: deckhouse + repository: podinfo + name: podinfo-podinfo +status: + versions: + - digest: a5c4b7381a0907128243354ab100d2eecc480d7dcac5014ff7272b0acef03780 + pulled: false + version: 6.11.0 + - digest: 9f1cdb52fc5a57848f377b146919f8eb2c4a2c0ab8815bd019ec41c1d1895c0c + pulled: false + version: 6.10.2 +``` + +## Развёртывание приложения + +Для развёртывания приложения создайте ресурс HelmClusterAddon, указав имя репозитория, имя и версию чарта, а также целевое пространство имён: + +```yaml +apiVersion: helm.deckhouse.io/v1alpha1 +kind: HelmClusterAddon +metadata: + name: podinfo +spec: + namespace: test + chart: + helmClusterAddonChart: podinfo + helmClusterAddonRepository: podinfo + version: 6.10.2 +``` + +{{< alert level="warning" >}} +Одновременно допускается развёртывание только одного экземпляра HelmClusterAddon, использующего заданный Helm-чарт из заданного репозитория. При этом из одного репозитория одновременно могут быть развёрнуты разные Helm-чарты. +{{< /alert >}} + +{{< alert level="info" >}} +Параметр `.spec.chart.version` является необязательным. Если он не указан, будет установлена последняя доступная версия чарта. +{{< /alert >}} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..a73f907 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,20 @@ +--- +title: "Module operator-helm" +description: "Deckhouse Kubernetes Platform — the operator-helm module for declarative Helm chart management." +weight: 10 +--- + +The `operator-helm` module provides declarative management of Helm chart deployments for cluster administrators and DevOps engineers. It deploys applications through custom resources, reducing the amount of manual configuration required. + +The module acts as a Kubernetes operator that reconciles the desired state described in HelmClusterAddon resources with the actual Helm releases in the cluster. + +## Main Features + +- Deploying Helm charts from classic HTTP/HTTPS repositories and OCI registries through a unified declarative API. +- Automatic chart version discovery and tracking via HelmClusterAddonChart resources. +- Configurable chart values through HelmClusterAddon resources. +- Maintenance mode to pause reconciliation on managed releases. +- TLS verification and authentication support for private Helm and OCI repositories. +- Management through CLI (`d8 k`) or the Deckhouse web interface. + +See [usage examples](example.html) for practical scenarios. diff --git a/docs/README.ru.md b/docs/README.ru.md new file mode 100644 index 0000000..55befb0 --- /dev/null +++ b/docs/README.ru.md @@ -0,0 +1,20 @@ +--- +title: "Модуль operator-helm" +description: "Deckhouse Kubernetes Platform — модуль operator-helm для декларативного управления Helm-чартами." +weight: 10 +--- + +Модуль `operator-helm` предназначен для декларативного управления развёртыванием Helm-чартов. Ориентирован на администраторов кластеров и DevOps-инженеров, которым необходимо автоматизировать установку приложений через кастомные ресурсы. + +Модуль работает как оператор Kubernetes, приводя фактическое состояние Helm-релизов в соответствие с описанным в ресурсах HelmClusterAddon. + +## Основные возможности + +- Развёртывание Helm-чартов из классических HTTP/HTTPS-репозиториев и OCI-репозиториев через единый декларативный API. +- Автоматическое обнаружение и отслеживание версий чартов через ресурсы HelmClusterAddonChart. +- Настройка параметров чартов через ресурсы HelmClusterAddon. +- Режим обслуживания для приостановки согласования и ручного вмешательства в управляемые релизы. +- Поддержка проверки TLS-сертификатов и аутентификации для приватных OCI и Helm репозиториев. +- Управление через CLI (`d8 k`) или веб-интерфейс Deckhouse. + +Примеры использования приведены в разделе [примеры использования](example.html). diff --git a/docs/images/.keep b/docs/images/.keep new file mode 100644 index 0000000..e69de29 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/hooks/.golangci.yaml b/images/hooks/.golangci.yaml new file mode 100644 index 0000000..6806dc1 --- /dev/null +++ b/images/hooks/.golangci.yaml @@ -0,0 +1,109 @@ +# https://golangci-lint.run/usage/configuration/ +version: "2" + +run: + concurrency: 4 + timeout: 10m + +issues: + # Show all errors. + max-issues-per-linter: 0 + max-same-issues: 0 + exclude: + - "don't use an underscore in package name" + +output: + sort-results: true + +exclusions: + paths: + - "^zz_generated.*" + +formatters: + enable: + - gci + - gofmt + - goimports + settings: + gci: + sections: + - standard + - default + - prefix(github.com/deckhouse/) + - prefix(hooks) + no-inline-comments: true + custom-order: true + goimports: + local-prefixes: github.com/deckhouse/ + +linters: + default: none + enable: + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - contextcheck # [maybe too many false positives] checks the function whether use a non-inherited context + - dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - copyloopvar # detects places where loop variables are copied (Go 1.22+) + - gocritic # provides diagnostics that check for bugs, performance and style issues + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - misspell # finds commonly misspelled English words in comments + - nolintlint # reports ill-formed or insufficient nolint directives + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - testifylint # checks usage of github.com/stretchr/testify + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - unused # checks for unused constants, variables, functions and types + - usetesting # reports uses of functions with replacement inside the testing package + - testableexamples # checks if examples are testable (have an expected output) + - thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - whitespace # detects leading and trailing whitespace + - wastedassign # finds wasted assignment statements + - importas # checks import aliases against the configured convention + settings: + errcheck: + exclude-functions: + - "(*os.File).Close" + - "(*net.TCPConn).Close" + - "(io.ReadCloser).Close" + - "(net.Listener).Close" + - "(net.Conn).Close" + - "(net.Conn).Close" + - "(*golang.org/x/crypto/ssh.Session).Close" + - "(*github.com/fsnotify/fsnotify.Watcher).Close" + staticcheck: + dot-import-whitelist: + - github.com/onsi/ginkgo/v2 + - github.com/onsi/gomega + revive: + rules: + - name: dot-imports + disabled: true + - name: exported + disabled: true + - name: package-comments + disabled: true + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [funlen, gocognit, lll] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + importas: + # Do not allow unaliased imports of aliased packages. + # Default: false + no-unaliased: true + # Do not allow non-required aliases. + # Default: false + no-extra-aliases: false \ No newline at end of file diff --git a/images/hooks/cmd/operator-helm-module-hooks/main.go b/images/hooks/cmd/operator-helm-module-hooks/main.go new file mode 100644 index 0000000..e0fcf75 --- /dev/null +++ b/images/hooks/cmd/operator-helm-module-hooks/main.go @@ -0,0 +1,25 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/deckhouse/module-sdk/pkg/app" +) + +func main() { + app.Run() +} diff --git a/images/hooks/cmd/operator-helm-module-hooks/register.go b/images/hooks/cmd/operator-helm-module-hooks/register.go new file mode 100644 index 0000000..8c21530 --- /dev/null +++ b/images/hooks/cmd/operator-helm-module-hooks/register.go @@ -0,0 +1,21 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + _ "hooks/pkg/hooks/tls-certificates-controller" +) diff --git a/images/hooks/go.mod b/images/hooks/go.mod new file mode 100644 index 0000000..95ec1c6 --- /dev/null +++ b/images/hooks/go.mod @@ -0,0 +1,97 @@ +module hooks + +go 1.25.0 + +require github.com/deckhouse/module-sdk v0.10.2 + +require ( + github.com/DataDog/gostackparse v0.7.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/caarlos0/env/v11 v11.3.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudflare/cfssl v1.6.5 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckhouse/deckhouse/pkg/log v0.2.0 // indirect + github.com/docker/cli v28.2.2+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/ettle/strcase v0.2.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/gojuno/minimock/v3 v3.4.7 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/certificate-transparency-go v1.1.7 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.20.6 // indirect + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/gomega v1.38.3 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pelletier/go-toml v1.9.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/sylabs/oci-tools v0.7.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/vbatts/tar-split v0.12.1 // indirect + github.com/weppos/publicsuffix-go v0.30.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300 // indirect + github.com/zmap/zlint/v3 v3.5.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.12.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.35.1 // indirect + k8s.io/apiextensions-apiserver v0.35.1 // indirect + k8s.io/apimachinery v0.35.1 // indirect + k8s.io/client-go v0.35.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/controller-runtime v0.23.1 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/images/hooks/go.sum b/images/hooks/go.sum new file mode 100644 index 0000000..fcecf5b --- /dev/null +++ b/images/hooks/go.sum @@ -0,0 +1,320 @@ +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= +github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI= +github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4= +github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckhouse/deckhouse/pkg/log v0.2.0 h1:6tmZQLwNb1o/hP1gzJQBjcwfA/bubbgObovXzxq+Exo= +github.com/deckhouse/deckhouse/pkg/log v0.2.0/go.mod h1:pbAxTSDcPmwyl3wwKDcEB3qdxHnRxqTV+J0K+sha8bw= +github.com/deckhouse/module-sdk v0.10.2 h1:jYxFTgjdaZ9NKWKbFP95RvD55WJvhwjPAeSMFKhZb0o= +github.com/deckhouse/module-sdk v0.10.2/go.mod h1:Z1jfmd0fICoYww0daMijWAU+OZTxeJUXfMciKKuYAYA= +github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= +github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= +github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/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-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +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/gojuno/minimock/v3 v3.4.7 h1:vhE5zpniyPDRT0DXd5s3DbtZJVlcbmC5k80izYtj9lY= +github.com/gojuno/minimock/v3 v3.4.7/go.mod h1:QxJk4mdPrVyYUmEZGc2yD2NONpqM/j4dWhsy9twjFHg= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw= +github.com/google/certificate-transparency-go v1.1.7/go.mod h1:FSSBo8fyMVgqptbfF6j5p/XNdgQftAhSmXcIxV9iphE= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= +github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= +github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= +github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= +github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +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/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/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/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= +github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/sylabs/oci-tools v0.7.0 h1:SIisUvcEL+Vpa9/kmQDy1W3AwV2XVGad83sgZmXLlb0= +github.com/sylabs/oci-tools v0.7.0/go.mod h1:Ry6ngChflh20WPq6mLvCKSw2OTd9iDB5aR8OQzeq4hM= +github.com/sylabs/sif/v2 v2.15.0 h1:Nv0tzksFnoQiQ2eUwpAis9nVqEu4c3RcNSxX8P3Cecw= +github.com/sylabs/sif/v2 v2.15.0/go.mod h1:X1H7eaPz6BAxA84POMESXoXfTqgAnLQkujyF/CQFWTc= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/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/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= +github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/weppos/publicsuffix-go v0.12.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.30.0 h1:QHPZ2GRu/YE7cvejH9iyavPOkVCB4dNxp2ZvtT+vQLY= +github.com/weppos/publicsuffix-go v0.30.0/go.mod h1:kBi8zwYnR0zrbm8RcuN1o9Fzgpnnn+btVN8uWPMyXAY= +github.com/weppos/publicsuffix-go/publicsuffix/generator v0.0.0-20220927085643-dc0d00c92642/go.mod h1:GHfoeIdZLdZmLjMlzBftbTDntahTttUMWjxZwQJhULE= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk= +github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= +github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= +github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300 h1:DZH5n7L3L8RxKdSyJHZt7WePgwdhHnPhQFdQSJaHF+o= +github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300/go.mod h1:mOd4yUMgn2fe2nV9KXsa9AyQBFZGzygVPovsZR+Rl5w= +github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= +github.com/zmap/zlint/v3 v3.5.0 h1:Eh2B5t6VKgVH0DFmTwOqE50POvyDhUaU9T2mJOe1vfQ= +github.com/zmap/zlint/v3 v3.5.0/go.mod h1:JkNSrsDJ8F4VRtBZcYUQSvnWFL7utcjDIn+FE64mlBI= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= +sigs.k8s.io/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 v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/images/hooks/pkg/hooks/tls-certificates-controller/hook.go b/images/hooks/pkg/hooks/tls-certificates-controller/hook.go new file mode 100644 index 0000000..b8ea981 --- /dev/null +++ b/images/hooks/pkg/hooks/tls-certificates-controller/hook.go @@ -0,0 +1,41 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tls_certificates_controller + +import ( + "fmt" + + tlscertificate "github.com/deckhouse/module-sdk/common-hooks/tls-certificate" + + "hooks/pkg/settings" +) + +var _ = tlscertificate.RegisterInternalTLSHookEM(tlscertificate.GenSelfSignedTLSHookConf{ + CN: settings.ControllerCertCN, + TLSSecretName: "operator-helm-controller-tls", + Namespace: settings.ModuleNamespace, + SANs: tlscertificate.DefaultSANs([]string{ + "localhost", + "127.0.0.1", + settings.ControllerCertCN, + fmt.Sprintf("%s.%s", settings.ControllerCertCN, settings.ModuleNamespace), + fmt.Sprintf("%s.%s.svc", settings.ControllerCertCN, settings.ModuleNamespace), + }), + + FullValuesPathPrefix: fmt.Sprintf("%s.internal.controller.cert", settings.ModuleName), + CommonCAValuesPath: fmt.Sprintf("%s.internal.rootCA", settings.ModuleName), +}) diff --git a/images/hooks/pkg/settings/certificate.go b/images/hooks/pkg/settings/certificate.go new file mode 100644 index 0000000..7c437fc --- /dev/null +++ b/images/hooks/pkg/settings/certificate.go @@ -0,0 +1,21 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package settings + +const ( + ControllerCertCN string = "operator-helm-controller" +) diff --git a/images/hooks/pkg/settings/module.go b/images/hooks/pkg/settings/module.go new file mode 100644 index 0000000..ecae271 --- /dev/null +++ b/images/hooks/pkg/settings/module.go @@ -0,0 +1,23 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package settings + +// Essential module constants. +const ( + ModuleNamespace string = "d8-operator-helm" + ModuleName string = "operatorHelm" +) diff --git a/images/hooks/werf.inc.yaml b/images/hooks/werf.inc.yaml new file mode 100644 index 0000000..59f011f --- /dev/null +++ b/images/hooks/werf.inc.yaml @@ -0,0 +1,52 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact +final: false +fromImage: builder/src +git: + - add: {{ .ModuleDir }}/images/{{ .ImageName }} + to: /src/images/hooks + stageDependencies: + install: + - "**/*" + - add: {{ .ModuleDir }}/images/operator-helm-artifact + to: /src/images/operator-helm-artifact + stageDependencies: + install: + - "**/*" + - add: {{ .ModuleDir }}/api + to: /src/api + stageDependencies: + install: + - "**/*" +shell: + install: + - cd /src +--- +image: {{ .ModuleNamePrefix }}go-hooks-artifact +final: false +fromImage: builder/golang-bookworm-1.25 +import: + - image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact + add: /src + to: /app + before: install +mount: + - fromPath: ~/go-pkg-cache + to: /go/pkg +secrets: + - id: GOPROXY + value: {{ .GOPROXY }} +shell: + install: + - export GOPROXY=$(cat /run/secrets/GOPROXY) + - cd /app/images/hooks + - go mod download + setup: + - cd /app/images/hooks + - | + export GOOS=linux + export GOARCH=amd64 + export CGO_ENABLED=0 + export TAGS="{{ printf "-tags %s" .MODULE_EDITION }}" + {{- $_ := set $ "ProjectName" (list $.ImageName "hooks" | join "/") }} + {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w" $TAGS -a -o /go-hooks/operator-helm-module-hooks ./cmd/operator-helm-module-hooks`) | nindent 6 }} diff --git a/images/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..a3c6033 --- /dev/null +++ b/images/kube-api-rewriter/STRUCTURE.md @@ -0,0 +1,3 @@ +# kube-api-rewriter structure + +_WIP_ diff --git a/images/kube-api-rewriter/Taskfile.dist.yaml b/images/kube-api-rewriter/Taskfile.dist.yaml new file mode 100644 index 0000000..8ba8a68 --- /dev/null +++ b/images/kube-api-rewriter/Taskfile.dist.yaml @@ -0,0 +1,111 @@ +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: + 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..dc80460 --- /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" + + 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" + "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 := operatornelm.OperatorNelmRewriteRules + 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..1a6e730 --- /dev/null +++ b/images/kube-api-rewriter/go.mod @@ -0,0 +1,73 @@ +module github.com/deckhouse/kube-api-rewriter + +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.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.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 +) + +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/google/gnostic-models 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.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.66.1 // indirect + github.com/prometheus/procfs v0.17.0 // 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.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.30.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.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-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/v6 v6.3.2-0.20260122202528-d9cc6641c482 // 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..6dd7074 --- /dev/null +++ b/images/kube-api-rewriter/go.sum @@ -0,0 +1,164 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/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/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/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/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/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.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.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.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.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.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= +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= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/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.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.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.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.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-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 v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/images/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..eefff43 --- /dev/null +++ b/images/kube-api-rewriter/mount-points.yaml @@ -0,0 +1 @@ +dirs: [] 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/operatornelm/operatornelm_rules.go b/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules.go new file mode 100644 index 0000000..c527d40 --- /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{"v2beta1", "v2beta2", "v2"}, + PreferredVersion: "v2", + Categories: []string{}, + ShortNames: []string{"hr"}, + }, + }, + }, +} + +var OperatorNelmWebhooks = map[string]WebhookRule{} diff --git a/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules_test.go b/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules_test.go new file mode 100644 index 0000000..876ed3f --- /dev/null +++ b/images/kube-api-rewriter/pkg/operatornelm/operatornelm_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 operatornelm + +import ( + "fmt" + "testing" + + "sigs.k8s.io/yaml" +) + +func TestOperatorNelmRulesToYAML(t *testing.T) { + b, err := yaml.Marshal(OperatorNelmRewriteRules) + if err != nil { + 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/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..72c1dc6 --- /dev/null +++ b/images/kube-api-rewriter/pkg/proxy/handler.go @@ -0,0 +1,573 @@ +/* +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 { + 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)) + } + 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") { + // 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) + } + if len(filteredTypes) > 0 { + newAccept = append(newAccept, strings.Join(filteredTypes, ",")) + } + } + + // Ensure Accept is not empty: fall back to application/json. + if len(newAccept) == 0 { + newAccept = append(newAccept, "application/json") + } + + req.Header["Accept"] = newAccept + } + + // 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 +} + +// 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: +// +// { +// "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/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..f599ce6 --- /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(fmt.Sprintf("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..50693bb --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/resource.go @@ -0,0 +1,165 @@ +/* +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) { + patch, err := RewritePatchSourceRefs(rules, patch) + if err != nil { + return nil, err + } + 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..e9bd4be --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/rule_rewriter.go @@ -0,0 +1,431 @@ +/* +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. + // 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) + }) + }) +} + +// 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..f03265b --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/rules.go @@ -0,0 +1,438 @@ +/* +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"` + + // 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 + 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 +} + +// 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/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/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/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..812ff0e --- /dev/null +++ b/images/kube-api-rewriter/werf.inc.yaml @@ -0,0 +1,62 @@ +--- +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: builder/golang-bookworm-1.25 +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/images/operator-helm-artifact/.gitignore b/images/operator-helm-artifact/.gitignore new file mode 100644 index 0000000..9f0f3a1 --- /dev/null +++ b/images/operator-helm-artifact/.gitignore @@ -0,0 +1,30 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +Dockerfile.cross + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work + +# Kubernetes Generated files - skip generated files, except for vendored files +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ + +# Kubeconfig might contain secrets +*.kubeconfig diff --git a/images/operator-helm-artifact/.golangci.yaml b/images/operator-helm-artifact/.golangci.yaml new file mode 100644 index 0000000..4260e0d --- /dev/null +++ b/images/operator-helm-artifact/.golangci.yaml @@ -0,0 +1,109 @@ +# https://golangci-lint.run/usage/configuration/ +version: "2" + +run: + concurrency: 4 + timeout: 10m + +issues: + # Show all errors. + max-issues-per-linter: 0 + max-same-issues: 0 + exclude: + - "don't use an underscore in package name" + +output: + sort-results: true + +exclusions: + paths: + - "^zz_generated.*" + +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + settings: + gci: + sections: + - standard + - default + - prefix(github.com/deckhouse/) + gofumpt: + extra-rules: true + goimports: + local-prefixes: github.com/deckhouse/ + +linters: + default: none + enable: + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - contextcheck # [maybe too many false positives] checks the function whether use a non-inherited context + - dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - copyloopvar # detects places where loop variables are copied (Go 1.22+) + - gocritic # provides diagnostics that check for bugs, performance and style issues + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - misspell # finds commonly misspelled English words in comments + - nolintlint # reports ill-formed or insufficient nolint directives + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - testifylint # checks usage of github.com/stretchr/testify + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - unused # checks for unused constants, variables, functions and types + - usetesting # reports uses of functions with replacement inside the testing package + - testableexamples # checks if examples are testable (have an expected output) + - thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - whitespace # detects leading and trailing whitespace + - wastedassign # finds wasted assignment statements + - importas # checks import aliases against the configured convention + settings: + errcheck: + exclude-functions: + - "(*os.File).Close" + - "(*net.TCPConn).Close" + - "(io.ReadCloser).Close" + - "(net.Listener).Close" + - "(net.Conn).Close" + - "(net.Conn).Close" + - "(*golang.org/x/crypto/ssh.Session).Close" + - "(*github.com/fsnotify/fsnotify.Watcher).Close" + staticcheck: + dot-import-whitelist: + - github.com/onsi/ginkgo/v2 + - github.com/onsi/gomega + revive: + rules: + - name: dot-imports + disabled: true + - name: exported + disabled: true + - name: package-comments + disabled: true + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [funlen, gocognit, lll] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + importas: + # Do not allow unaliased imports of aliased packages. + # Default: false + no-unaliased: true + # Do not allow non-required aliases. + # Default: false + no-extra-aliases: false \ No newline at end of file diff --git a/images/operator-helm-artifact/cmd/operator-helm-controller/main.go b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go new file mode 100644 index 0000000..ee75a9a --- /dev/null +++ b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go @@ -0,0 +1,119 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "os" + + helmv2 "github.com/werf/3p-helm-controller/api/v2" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/internal/controller/helmclusteraddon" + "github.com/deckhouse/operator-helm/internal/controller/helmclusteraddonrepository" + "github.com/deckhouse/operator-helm/internal/utils" + helmclusteraddonwebhook "github.com/deckhouse/operator-helm/internal/webhook/helmclusteraddon" +) + +var scheme = runtime.NewScheme() + +func init() { + _ = clientgoscheme.AddToScheme(scheme) + _ = helmv1alpha1.AddToScheme(scheme) + _ = sourcev1.AddToScheme(scheme) + _ = helmv2.AddToScheme(scheme) +} + +func main() { + var ( + metricsAddr string + healthProbeAddr string + enableLeaderElection bool + ) + + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metrics endpoint binds to.") + flag.StringVar(&healthProbeAddr, "health-probe-bind-address", ":9440", "The address the health probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager.") + opts := zap.Options{Development: false} + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + logger := ctrl.Log.WithName("setup") + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: metricsAddr, + }, + HealthProbeBindAddress: healthProbeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "operator-helm-controller.helm.deckhouse.io", + }) + if err != nil { + logger.Error(err, "unable to create manager") + os.Exit(1) + } + + if err := utils.SetupAddonRepositoryIndex(mgr); err != nil { + logger.Error(err, "unable to setup addon repository index") + os.Exit(1) + } + + if err := helmclusteraddonrepository.SetupWithManager(mgr); err != nil { + logger.Error(err, "unable to setup HelmClusterAddonRepository controller") + os.Exit(1) + } + + if err := helmclusteraddon.SetupWithManager(mgr); err != nil { + logger.Error(err, "unable to setup HelmClusterAddon controller") + os.Exit(1) + } + + if err = helmclusteraddonwebhook.SetupIndexes(mgr); err != nil { + logger.Error(err, "unable to setup indexes", "webhook", "HelmClusterAddon") + os.Exit(1) + } + + if err = helmclusteraddonwebhook.SetupWebhookWithManager(mgr); err != nil { + logger.Error(err, "unable to create webhook", "webhook", "HelmClusterAddon") + os.Exit(1) + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + logger.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + logger.Error(err, "unable to set up ready check") + os.Exit(1) + } + + logger.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + logger.Error(err, "manager exited with error") + os.Exit(1) + } +} diff --git a/images/operator-helm-artifact/go.mod b/images/operator-helm-artifact/go.mod new file mode 100644 index 0000000..9936fb1 --- /dev/null +++ b/images/operator-helm-artifact/go.mod @@ -0,0 +1,95 @@ +module github.com/deckhouse/operator-helm + +go 1.25.0 + +replace github.com/deckhouse/operator-helm/api => ../../api + +require ( + github.com/Masterminds/semver/v3 v3.4.0 + github.com/deckhouse/operator-helm/api v0.0.0-00010101000000-000000000000 + github.com/google/go-containerregistry v0.20.6 + github.com/opencontainers/go-digest v1.0.0 + github.com/stretchr/testify v1.11.1 + github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 + github.com/werf/3p-fluxcd-pkg/chartutil v1.17.0-nelm.1 + github.com/werf/3p-helm-controller/api v0.1.4 + github.com/werf/nelm-source-controller/api v0.1.4 + go.yaml.in/yaml/v3 v3.0.4 + helm.sh/helm/v3 v3.20.1 + k8s.io/api v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 + sigs.k8s.io/controller-runtime v0.23.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v28.2.2+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/vbatts/tar-split v0.12.1 // indirect + github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 // indirect + github.com/werf/3p-fluxcd-pkg/apis/kustomize v1.14.0-nelm.1 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.12.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.4.0 // indirect + k8s.io/apiextensions-apiserver v0.35.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/images/operator-helm-artifact/go.sum b/images/operator-helm-artifact/go.sum new file mode 100644 index 0000000..11bb0ed --- /dev/null +++ b/images/operator-helm-artifact/go.sum @@ -0,0 +1,236 @@ +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= +github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/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/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= +github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.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/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +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.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= +github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 h1:b1P4avYWjjWuzPSOv6QZtk1ffl/iBfWBGK4qNAxaA94= +github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1/go.mod h1:00dBUg4SN+4Xu4LWrbQm5LdmRKVP9Fjbvb+rvqjHrVI= +github.com/werf/3p-fluxcd-pkg/apis/kustomize v1.14.0-nelm.1 h1:edZ5ugpeUvmjG+g9laet8qTBqDdQPl18aNr6k0xqdYY= +github.com/werf/3p-fluxcd-pkg/apis/kustomize v1.14.0-nelm.1/go.mod h1:dAboSMVeohict/XrpXrqyZodq+8Qp6dwafzkBzoCHcU= +github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 h1:rYX8cMeryBHH7sNPVSQm1IAVES08TiWvADaZsDj98Wk= +github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1/go.mod h1:14co1+Ub5rW0Bp3Qo4IzCHwEcaw06StyMu7Rv5pMVCY= +github.com/werf/3p-fluxcd-pkg/chartutil v1.17.0-nelm.1 h1:ua0xt66rxKptzbG1zxy3u96qfV8XsFT9Jd2PU8L6mc8= +github.com/werf/3p-fluxcd-pkg/chartutil v1.17.0-nelm.1/go.mod h1:fodaCyMGXGxYYSIdWvokrjki8e+DAhgu6BtzHbH2VJ8= +github.com/werf/3p-helm-controller/api v0.1.4 h1:s7g9UQOrDMUzVE+JtWOP2xApnPOKYlNe1tXkkWCisAw= +github.com/werf/3p-helm-controller/api v0.1.4/go.mod h1:tiPvDerlc5SwKIDmXB8L3kIMJHse+wigueoEGQq+588= +github.com/werf/nelm-source-controller/api v0.1.4 h1:/k3RT+hHdwKHntoebdcjhO+zboJIlljHJZlbcumoY08= +github.com/werf/nelm-source-controller/api v0.1.4/go.mod h1:++j7xw4YVDE8gR9x1HWhIagpo68jE1oEd4+6tMAgXgs= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +helm.sh/helm/v3 v3.20.1 h1:T8PodUaH1UwNvE+imUA2mIKjJItY8g7CVvLVP5g4NzI= +helm.sh/helm/v3 v3.20.1/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= +sigs.k8s.io/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 v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/images/operator-helm-artifact/internal/client/repository/client.go b/images/operator-helm-artifact/internal/client/repository/client.go new file mode 100644 index 0000000..99a1ee7 --- /dev/null +++ b/images/operator-helm-artifact/internal/client/repository/client.go @@ -0,0 +1,66 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repository + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/internal/utils" +) + +type ClientInterface interface { + FetchCharts(ctx context.Context, url string, config *RepoConfig) (map[string][]helmv1alpha1.HelmClusterAddonChartVersion, error) +} + +func NewClient(repoType utils.InternalRepositoryType) (ClientInterface, error) { + switch repoType { + case utils.InternalHelmRepository: + return HelmRepositoryDefaultClient, nil + case utils.InternalOCIRepository: + return OCIRepositoryDefaultClient, nil + default: + return nil, fmt.Errorf("unknown repository type: %s", repoType) + } +} + +type RepoConfig struct { + Username string + Password string + CACertificate string + Insecure bool +} + +func BuildTLSTransport(config *RepoConfig) *http.Transport { + tlsConfig := &tls.Config{} + + if config.Insecure { + tlsConfig.InsecureSkipVerify = true + } + + if config.CACertificate != "" { + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM([]byte(config.CACertificate)) + tlsConfig.RootCAs = caCertPool + } + + return &http.Transport{TLSClientConfig: tlsConfig} +} diff --git a/images/operator-helm-artifact/internal/client/repository/helm.go b/images/operator-helm-artifact/internal/client/repository/helm.go new file mode 100644 index 0000000..ae6ce07 --- /dev/null +++ b/images/operator-helm-artifact/internal/client/repository/helm.go @@ -0,0 +1,120 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repository + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + "go.yaml.in/yaml/v3" + "k8s.io/apimachinery/pkg/util/wait" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" +) + +var HelmRepositoryDefaultClient ClientInterface = &helmRepositoryClient{} + +type helmRepositoryClient struct{} + +func (c *helmRepositoryClient) FetchCharts(ctx context.Context, url string, config *RepoConfig) (map[string][]helmv1alpha1.HelmClusterAddonChartVersion, error) { + if !strings.HasSuffix(url, "/index.yaml") { + url += "/index.yaml" + } + + var indexFile HelmRepositoryIndex + + httpClient := http.DefaultClient + if config != nil && (config.CACertificate != "" || config.Insecure) { + httpClient = &http.Client{Transport: BuildTLSTransport(config)} + } + + backoff := wait.Backoff{ + Duration: 1 * time.Second, + Factor: 2.0, + Jitter: 0.1, + Steps: 3, + } + + ctx, cancel := context.WithTimeout(ctx, 8*time.Second) + defer cancel() + + err := wait.ExponentialBackoffWithContext(ctx, backoff, func(ctx context.Context) (done bool, err error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return true, fmt.Errorf("creating request: %w", err) + } + + if config != nil && config.Username != "" { + req.SetBasicAuth(config.Username, config.Password) + } + + resp, err := httpClient.Do(req) + if err != nil { + return false, nil + } + defer resp.Body.Close() + + if resp.StatusCode >= 500 { + return false, nil + } + + if resp.StatusCode >= 400 { + return true, fmt.Errorf("fatal client error: received status %d", resp.StatusCode) + } + + if err := yaml.NewDecoder(resp.Body).Decode(&indexFile); err != nil { + return true, fmt.Errorf("cannot decode response: %w", err) + } + + return true, nil + }) + if err != nil { + return nil, fmt.Errorf("helm repository index.yaml request failed: %w", err) + } + + charts := make(map[string][]helmv1alpha1.HelmClusterAddonChartVersion) + + for chartName, chartInfo := range indexFile.Entries { + charts[chartName] = make([]helmv1alpha1.HelmClusterAddonChartVersion, 0) + + for _, chartVersion := range chartInfo { + if chartVersion.Removed { + continue + } + + charts[chartName] = append(charts[chartName], helmv1alpha1.HelmClusterAddonChartVersion{ + Version: chartVersion.Version, + }) + } + } + + return charts, nil +} + +type HelmRepositoryIndex struct { + APIVersion string `json:"apiVersion"` + Entries map[string][]HelmRepositoryChartVersion `json:"entries"` +} + +type HelmRepositoryChartVersion struct { + Version string `json:"version"` + Digest string `json:"digest"` + Removed bool `json:"removed,omitempty"` +} diff --git a/images/operator-helm-artifact/internal/client/repository/oci.go b/images/operator-helm-artifact/internal/client/repository/oci.go new file mode 100644 index 0000000..0a07a1b --- /dev/null +++ b/images/operator-helm-artifact/internal/client/repository/oci.go @@ -0,0 +1,124 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repository + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" +) + +var OCIRepositoryDefaultClient ClientInterface = &ociRepositoryClient{} + +type ociRepositoryClient struct{} + +func (c *ociRepositoryClient) FetchCharts(ctx context.Context, url string, config *RepoConfig) (map[string][]helmv1alpha1.HelmClusterAddonChartVersion, error) { + url = trimSchemaPrefixes(url) + url = strings.TrimSuffix(url, "/") + + if !strings.Contains(url, "/") { + return nil, errors.New("url must contain chart/image name") + } + + urlParts := strings.Split(url, "/") + chartName := urlParts[len(urlParts)-1] + + if len(chartName) == 0 { + return nil, errors.New("failed to parse chart/image name from the url") + } + + repo, err := name.NewRepository(url) + if err != nil { + return nil, fmt.Errorf("failed to parse repository url: %w", err) + } + + options := []remote.Option{ + remote.WithContext(ctx), + remote.WithUserAgent("operator-helm-controller"), + remote.WithRetryBackoff(remote.Backoff{ + Duration: 1.0 * time.Second, + Factor: 3.0, + Jitter: 0.1, + Steps: 3, + }), + } + + if config != nil && config.Username != "" { + options = append(options, remote.WithAuth(authn.FromConfig(authn.AuthConfig{ + Username: config.Username, + Password: config.Password, + }))) + } + + if config != nil && (config.CACertificate != "" || config.Insecure) { + options = append(options, remote.WithTransport(BuildTLSTransport(config))) + } + + tags, err := remote.List(repo, options...) + if err != nil { + return nil, fmt.Errorf("listing image tags: %w", err) + } + + var chartVersions []helmv1alpha1.HelmClusterAddonChartVersion + + for _, tag := range tags { + if isCosignTag(tag) || !isSemverCompliantTag(tag) { + continue + } + + // Do not obtain digests as they are currently not used and require a HEAD request per tag. + chartVersions = append(chartVersions, helmv1alpha1.HelmClusterAddonChartVersion{ + Version: tag, + }) + } + + return map[string][]helmv1alpha1.HelmClusterAddonChartVersion{ + chartName: chartVersions, + }, nil +} + +func trimSchemaPrefixes(url string) string { + for _, prefix := range []string{"oci://", "http://", "https://"} { + url = strings.TrimPrefix(url, prefix) + } + + return url +} + +func isSemverCompliantTag(tag string) bool { + _, err := semver.NewVersion(tag) + return err == nil +} + +func isCosignTag(tag string) bool { + for _, suffix := range []string{".att", ".sbom", ".sig"} { + if strings.HasSuffix(tag, suffix) { + return true + } + } + + return false +} diff --git a/images/operator-helm-artifact/internal/controller/helmclusteraddon/controller.go b/images/operator-helm-artifact/internal/controller/helmclusteraddon/controller.go new file mode 100644 index 0000000..852ffe8 --- /dev/null +++ b/images/operator-helm-artifact/internal/controller/helmclusteraddon/controller.go @@ -0,0 +1,99 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddon + +import ( + helmv2 "github.com/werf/3p-helm-controller/api/v2" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/internal/manager/status" + reconcile "github.com/deckhouse/operator-helm/internal/reconcile/helmclusteraddon" + "github.com/deckhouse/operator-helm/internal/services" + "github.com/deckhouse/operator-helm/internal/utils" +) + +const ( + ControllerName = "helmclusteraddon-controller" +) + +func SetupWithManager(mgr ctrl.Manager) error { + client := mgr.GetClient() + + r := reconcile.New( + mgr.GetClient(), + services.NewChartService(client, mgr.GetScheme(), helmv1alpha1.TargetNamespace), + services.NewOCIRepoService(client, mgr.GetScheme(), helmv1alpha1.TargetNamespace), + services.NewReleaseService(client, mgr.GetScheme(), helmv1alpha1.TargetNamespace), + services.NewMaintenanceService(client, mgr.GetScheme(), helmv1alpha1.TargetNamespace), + status.NewManager(client), + ) + + return ctrl.NewControllerManagedBy(mgr). + Named(ControllerName). + WithOptions(controller.Options{MaxConcurrentReconciles: 2}). + For(&helmv1alpha1.HelmClusterAddon{}). + Watches( + &sourcev1.HelmChart{}, + handler.EnqueueRequestsFromMapFunc( + utils.MapInternalResources( + ControllerName, + helmv1alpha1.TargetNamespace, + helmv1alpha1.LabelManagedBy, + helmv1alpha1.LabelManagedByValue, + helmv1alpha1.HelmClusterAddonLabelSourceName, + ), + ), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Watches( + &helmv2.HelmRelease{}, + handler.EnqueueRequestsFromMapFunc( + utils.MapInternalResources( + ControllerName, + helmv1alpha1.TargetNamespace, + helmv1alpha1.LabelManagedBy, + helmv1alpha1.LabelManagedByValue, + helmv1alpha1.HelmClusterAddonLabelSourceName, + ), + ), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Watches( + &sourcev1.OCIRepository{}, + handler.EnqueueRequestsFromMapFunc( + utils.MapInternalResources( + ControllerName, + helmv1alpha1.TargetNamespace, + helmv1alpha1.LabelManagedBy, + helmv1alpha1.LabelManagedByValue, + helmv1alpha1.HelmClusterAddonLabelSourceName, + ), + ), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})). + Watches( + &helmv1alpha1.HelmClusterAddonRepository{}, + handler.EnqueueRequestsFromMapFunc(utils.MapRepositoryToAddons(client)), + builder.WithPredicates(predicate.GenerationChangedPredicate{}), + ). + Complete(r) +} diff --git a/images/operator-helm-artifact/internal/controller/helmclusteraddonrepository/controller.go b/images/operator-helm-artifact/internal/controller/helmclusteraddonrepository/controller.go new file mode 100644 index 0000000..17b8b1b --- /dev/null +++ b/images/operator-helm-artifact/internal/controller/helmclusteraddonrepository/controller.go @@ -0,0 +1,87 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddonrepository + +import ( + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + corev1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/internal/manager/status" + reconcile "github.com/deckhouse/operator-helm/internal/reconcile/helmclusteraddonrepository" + "github.com/deckhouse/operator-helm/internal/services" + "github.com/deckhouse/operator-helm/internal/utils" +) + +const ( + ControllerName = "helmclusteraddonrepository-controller" +) + +func SetupWithManager(mgr ctrl.Manager) error { + client := mgr.GetClient() + + r := reconcile.New( + client, + services.NewHelmRepoService(client, mgr.GetScheme(), helmv1alpha1.TargetNamespace), + services.NewOCIRepoService(client, mgr.GetScheme(), helmv1alpha1.TargetNamespace), + services.NewRepoSyncService(client, mgr.GetScheme()), + status.NewManager(client), + ) + + return ctrl.NewControllerManagedBy(mgr). + Named(ControllerName). + WithOptions(controller.Options{MaxConcurrentReconciles: 2}). + For(&helmv1alpha1.HelmClusterAddonRepository{}). + Watches( + &sourcev1.HelmRepository{}, + handler.EnqueueRequestsFromMapFunc( + utils.MapInternalResources( + ControllerName, + helmv1alpha1.TargetNamespace, + helmv1alpha1.LabelManagedBy, + helmv1alpha1.LabelManagedByValue, + helmv1alpha1.HelmClusterAddonRepositoryLabelSourceName), + ), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc( + utils.MapInternalResources( + ControllerName, + helmv1alpha1.TargetNamespace, + helmv1alpha1.LabelManagedBy, + helmv1alpha1.LabelManagedByValue, + helmv1alpha1.HelmClusterAddonRepositoryLabelSourceName), + ), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Watches( + &helmv1alpha1.HelmClusterAddonChart{}, + handler.EnqueueRequestForOwner( + mgr.GetScheme(), + mgr.GetRESTMapper(), + &helmv1alpha1.HelmClusterAddonRepository{}, + handler.OnlyControllerOwner(), + ), + ).Complete(r) +} diff --git a/images/operator-helm-artifact/internal/manager/status/condition_rules.go b/images/operator-helm-artifact/internal/manager/status/condition_rules.go new file mode 100644 index 0000000..890eae9 --- /dev/null +++ b/images/operator-helm-artifact/internal/manager/status/condition_rules.go @@ -0,0 +1,69 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package status + +import ( + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" +) + +// ErrorConditionRule defines how a specific child condition type should be +// treated as an error for the parent object. +type ErrorConditionRule struct { + Type string + TriggerStatus metav1.ConditionStatus + Reason string +} + +// ProcessChildConditions inspects a set of child conditions and returns a +// Status reflecting the aggregate state. Error rules are checked first (in +// order), then Reconciling, then Ready. If nothing matches, an Unknown status +// with ReasonReconciling is returned. +func ProcessChildConditions( + conditions []metav1.Condition, + generation int64, + parentObj client.Object, + errorRules []ErrorConditionRule, +) Status { + for _, rule := range errorRules { + cond := meta.FindStatusCondition(conditions, rule.Type) + if cond != nil && cond.Status == rule.TriggerStatus { + return Failed(parentObj, rule.Reason, cond.Message, nil) + } + } + + reconcilingCond := meta.FindStatusCondition(conditions, "Reconciling") + if reconcilingCond != nil && reconcilingCond.Status == metav1.ConditionTrue { + return Unknown(parentObj, helmv1alpha1.ReasonReconciling) + } + + cond, observed := IsConditionObserved(conditions, helmv1alpha1.ConditionTypeReady, generation) + if observed { + return Status{ + Observed: true, + Status: cond.Status, + ObservedGeneration: parentObj.GetGeneration(), + Reason: cond.Reason, + Message: cond.Message, + } + } + + return Unknown(parentObj, helmv1alpha1.ReasonReconciling) +} diff --git a/images/operator-helm-artifact/internal/manager/status/helpers.go b/images/operator-helm-artifact/internal/manager/status/helpers.go new file mode 100644 index 0000000..e7739aa --- /dev/null +++ b/images/operator-helm-artifact/internal/manager/status/helpers.go @@ -0,0 +1,77 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package status + +import ( + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" +) + +type statusProxy struct { + Provider + newType string +} + +func (p statusProxy) GetConditionType() string { return p.newType } + +func AsCondition(res Provider, conditionType string) Provider { + return statusProxy{Provider: res, newType: conditionType} +} + +func Success(obj client.Object) Status { + return Status{ + Observed: true, + Status: metav1.ConditionTrue, + Reason: helmv1alpha1.ReasonSuccess, + ObservedGeneration: obj.GetGeneration(), + } +} + +func Failed(obj client.Object, reason, message string, err error) Status { + return Status{ + Observed: true, + Status: metav1.ConditionFalse, + Reason: reason, + ObservedGeneration: obj.GetGeneration(), + Message: message, + Err: err, + } +} + +func Unknown(obj client.Object, reason string) Status { + return Status{ + Status: metav1.ConditionUnknown, + Reason: reason, + ObservedGeneration: obj.GetGeneration(), + } +} + +func Empty() Status { + return Status{} +} + +func IsConditionObserved(conditions []metav1.Condition, conditionType string, generation int64) (*metav1.Condition, bool) { + cond := meta.FindStatusCondition(conditions, conditionType) + if cond == nil || cond.ObservedGeneration != generation { + return cond, false + } + + return cond, true +} diff --git a/images/operator-helm-artifact/internal/manager/status/manager.go b/images/operator-helm-artifact/internal/manager/status/manager.go new file mode 100644 index 0000000..78f92b9 --- /dev/null +++ b/images/operator-helm-artifact/internal/manager/status/manager.go @@ -0,0 +1,216 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package status + +import ( + "context" + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type ObjectWithConditions interface { + client.Object + GetConditions() *[]metav1.Condition + GetGeneration() int64 + GetObservedGeneration() int64 + SetObservedGeneration(int64) + GetConditionTypesForUpdate() []string + GetStatus() interface{} +} + +type Provider interface { + GetStatus() Status + GetConditionType() string +} + +type GenerationProvider interface { + GetObservedGeneration() int64 +} + +type Manager struct { + client.Client +} + +func NewManager(c client.Client) *Manager { + return &Manager{ + Client: c, + } +} + +type MutatorFunc func(ObjectWithConditions, []Provider) (ObjectWithConditions, []Provider) + +var NoopStatusMutator = MutatorFunc(func(o ObjectWithConditions, s []Provider) (ObjectWithConditions, []Provider) { return o, s }) + +type MapperFunc func(string, Status) Status + +var NoopStatusMapper = MapperFunc(func(_ string, status Status) Status { + return status +}) + +func (s *Manager) Update(ctx context.Context, obj ObjectWithConditions, mutatorFunc MutatorFunc, statusMapperFunc MapperFunc, results ...Provider) error { + logger := log.FromContext(ctx) + + oldObj := obj.DeepCopyObject().(ObjectWithConditions) + + if mutatorFunc != nil { + obj, results = mutatorFunc(obj, results) + } + + conditions := obj.GetConditions() + currentGen := obj.GetGeneration() + minObservedGen := currentGen + + for _, res := range results { + if res == nil { + continue + } + + status := res.GetStatus() + + status = statusMapperFunc(res.GetConditionType(), status) + + if status.Status == "" || status.Reason == "" { + continue + } + + if status.Err != nil { + logger.Error(status.Err, status.Message, + "condition", res.GetConditionType(), + "reason", status.Reason) + } + + meta.SetStatusCondition(conditions, metav1.Condition{ + Type: res.GetConditionType(), + Status: status.Status, + Reason: status.Reason, + Message: status.Message, + ObservedGeneration: status.ObservedGeneration, + }) + + if status.ObservedGeneration < minObservedGen { + minObservedGen = status.ObservedGeneration + } + } + + oldObservedGen := oldObj.GetObservedGeneration() + if minObservedGen > oldObservedGen { + obj.SetObservedGeneration(minObservedGen) + } else { + obj.SetObservedGeneration(oldObservedGen) + } + + if reflect.DeepEqual(obj.GetStatus(), oldObj.GetStatus()) { + return nil + } + + return s.Status().Patch(ctx, obj, client.MergeFrom(oldObj)) +} + +func (s *Manager) InitializeConditions(ctx context.Context, obj ObjectWithConditions, conditionTypes ...string) error { + oldObj := obj.DeepCopyObject().(ObjectWithConditions) + patchBase := client.MergeFrom(oldObj) + + conditions := obj.GetConditions() + changed := false + + for _, t := range conditionTypes { + if meta.FindStatusCondition(*conditions, t) == nil { + meta.SetStatusCondition(conditions, metav1.Condition{ + Type: t, + Status: metav1.ConditionUnknown, + Reason: "Initialized", + }) + changed = true + } + } + + if changed { + logger := log.FromContext(ctx) + logger.Info("Initializing conditions", "name", obj.GetName(), "types", conditionTypes) + + if err := s.Client.Status().Patch(ctx, obj, patchBase); err != nil { + return fmt.Errorf("initializing conditions: %w", err) + } + } + + return nil +} + +func DetermineConditions(obj ObjectWithConditions, results ...Provider) []Provider { + var result []Provider + + conditionTypes := obj.GetConditionTypesForUpdate() + if len(results) == 0 { + return result + } + + var decisionRes Provider + for _, res := range results { + if res == nil { + continue + } + + status := res.GetStatus() + if status.Status == "" || status.Reason == "" { + continue + } + + if status.NotReflectable { + result = append(result, res) + continue + } + + decisionRes = res + if !status.IsReady() { + break + } + } + + if decisionRes == nil { + return result + } + + for _, conditionType := range conditionTypes { + result = append(result, AsCondition(decisionRes, conditionType)) + } + + return result +} + +type Status struct { + ConditionType string + Observed bool + Status metav1.ConditionStatus + ObservedGeneration int64 + Reason string + Message string + // NotReflectable marks a result that is appended as its own condition directly, + // bypassing the "decision result" logic in DetermineConditions. When true, the + // result does not participate in selecting the single decision result that gets + // projected across all condition types returned by GetConditionTypesForUpdate. + NotReflectable bool + Err error +} + +func (s Status) IsReady() bool { + return s.Status == metav1.ConditionTrue && s.Observed +} diff --git a/images/operator-helm-artifact/internal/reconcile/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/internal/reconcile/helmclusteraddon/reconciler.go new file mode 100644 index 0000000..76f8b24 --- /dev/null +++ b/images/operator-helm-artifact/internal/reconcile/helmclusteraddon/reconciler.go @@ -0,0 +1,306 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddon + +import ( + "context" + "fmt" + + "github.com/opencontainers/go-digest" + "github.com/werf/3p-fluxcd-pkg/chartutil" + helmchartutil "helm.sh/helm/v3/pkg/chartutil" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/internal/manager/status" + "github.com/deckhouse/operator-helm/internal/services" + "github.com/deckhouse/operator-helm/internal/utils" +) + +func New( + client client.Client, + chartService *services.ChartService, + ociRepositoryService *services.OCIRepoService, + releaseService *services.ReleaseService, + maintenanceService *services.MaintenanceService, + statusManager *status.Manager, +) *Reconciler { + return &Reconciler{ + Client: client, + chartService: chartService, + ociRepositoryService: ociRepositoryService, + releaseService: releaseService, + maintenanceService: maintenanceService, + statusManager: statusManager, + } +} + +type Reconciler struct { + client.Client + + chartService *services.ChartService + ociRepositoryService *services.OCIRepoService + releaseService *services.ReleaseService + maintenanceService *services.MaintenanceService + statusManager *status.Manager +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger := log.FromContext(ctx) + ctx = log.IntoContext(ctx, logger) + + addon := &helmv1alpha1.HelmClusterAddon{} + if err := r.Get(ctx, req.NamespacedName, addon); err != nil { + if apierrors.IsNotFound(err) { + return reconcile.Result{}, nil + } + return reconcile.Result{}, fmt.Errorf("getting helm cluster addon: %w", err) + } + + if !addon.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, addon) + } + + if !controllerutil.ContainsFinalizer(addon, helmv1alpha1.FinalizerName) { + controllerutil.AddFinalizer(addon, helmv1alpha1.FinalizerName) + if err := r.Update(ctx, addon); err != nil { + return reconcile.Result{}, fmt.Errorf("adding finalizer: %w", err) + } + return reconcile.Result{}, nil + } + + err := r.statusManager.InitializeConditions(ctx, addon, + helmv1alpha1.ConditionTypeReady, + helmv1alpha1.ConditionTypeManaged, + helmv1alpha1.ConditionTypeInstalled, + helmv1alpha1.ConditionTypeUpdateInstalled, + helmv1alpha1.ConditionTypeConfigurationApplied, + helmv1alpha1.ConditionTypePartiallyDegraded, + ) + if err != nil { + return reconcile.Result{}, err + } + + if r.maintenanceService.IsMaintenanceModeChangeRequired(addon) { + maintenanceRes := r.maintenanceService.EnsureMaintenanceMode(ctx, addon) + return reconcile.Result{}, r.statusManager.Update(ctx, addon, status.NoopStatusMutator, status.NoopStatusMapper, maintenanceRes) + } + + repo := &helmv1alpha1.HelmClusterAddonRepository{} + if err := r.Get(ctx, types.NamespacedName{Name: addon.Spec.Chart.HelmClusterAddonRepository}, repo); err != nil { + return reconcile.Result{}, r.statusManager.Update(ctx, addon, status.NoopStatusMutator, status.NoopStatusMapper, services.ReleaseResult{Status: status.Failed( + addon, + helmv1alpha1.ReasonFailed, + "Failed to get internal repository", + fmt.Errorf("getting internal repository: %w", err), + )}) + } + + repoType, err := utils.GetRepositoryType(repo.Spec.URL) + if err != nil { + return reconcile.Result{}, r.statusManager.Update(ctx, addon, status.NoopStatusMutator, status.NoopStatusMapper, services.ReleaseResult{Status: status.Failed( + addon, + helmv1alpha1.ReasonFailed, + fmt.Sprintf("Failed to parse repository type: %s", err.Error()), + err, + )}) + } + + var chartRes services.ChartResult + var repoRes services.OCIRepoResult + var releaseRes services.ReleaseResult + + switch repoType { + case utils.InternalHelmRepository: + // URL change in the HelmClusterAddonRepository may lead to repository type change. + // If repository type changed from OCI to Helm, we need to remove previously created OCI repository. + if err := r.ociRepositoryService.RemoveOCIRepository(ctx, addon); err != nil { + chartRes = services.ChartResult{ + Status: status.Failed(addon, helmv1alpha1.ReasonFailed, "Repository change failed", err), + } + break + } + + chartRes = r.chartService.EnsureHelmChart(ctx, addon) + if !chartRes.IsPartiallyDegraded() { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: helmv1alpha1.ConditionTypePartiallyDegraded, + Status: metav1.ConditionFalse, + ObservedGeneration: addon.Generation, + Reason: helmv1alpha1.ReasonSuccess, + }) + } + case utils.InternalOCIRepository: + if err := r.chartService.CleanupHelmChart(ctx, addon); err != nil { + chartRes = services.ChartResult{ + Status: status.Failed(addon, helmv1alpha1.ReasonFailed, "Repository change failed", err), + } + break + } + + repoRes = r.ociRepositoryService.EnsureInternalOCIRepository(ctx, addon, repo) + if !repoRes.IsPartiallyDegraded() { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: helmv1alpha1.ConditionTypePartiallyDegraded, + Status: metav1.ConditionFalse, + ObservedGeneration: addon.Generation, + Reason: helmv1alpha1.ReasonSuccess, + }) + } + default: + return reconcile.Result{}, r.statusManager.Update(ctx, addon, status.NoopStatusMutator, status.NoopStatusMapper, services.ReleaseResult{Status: status.Failed( + addon, + helmv1alpha1.ReasonFailed, + fmt.Sprintf("Unsupported repository type: %s", repoType), + fmt.Errorf("unsupported repository type: %s", repoType), + )}) + } + + if chartRes.HasArtifact() || repoRes.HasArtifact() { + releaseRes = r.releaseService.EnsureHelmRelease(ctx, addon, repoType) + } + + if err := r.statusManager.Update( + ctx, + addon, + setStatusAttrs(repoType, chartRes, repoRes, releaseRes), + mapResourceStatus(), + chartRes, + repoRes, + releaseRes, + ); client.IgnoreNotFound(err) != nil { + return reconcile.Result{}, fmt.Errorf("failed to update status: %w", err) + } + + return reconcile.Result{}, nil +} + +func (r *Reconciler) reconcileDelete(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + if !controllerutil.ContainsFinalizer(addon, helmv1alpha1.FinalizerName) { + return reconcile.Result{}, nil + } + + if err := r.ociRepositoryService.RemoveOCIRepository(ctx, addon); client.IgnoreNotFound(err) != nil { + return reconcile.Result{}, err + } + + if err := r.chartService.CleanupHelmChart(ctx, addon); client.IgnoreNotFound(err) != nil { + return reconcile.Result{}, err + } + + if err := r.releaseService.CleanupHelmRelease(ctx, addon); client.IgnoreNotFound(err) != nil { + return reconcile.Result{}, err + } + + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + latestAddon := &helmv1alpha1.HelmClusterAddon{} + if err := r.Get(ctx, client.ObjectKeyFromObject(addon), latestAddon); err != nil { + return client.IgnoreNotFound(err) + } + + if controllerutil.RemoveFinalizer(latestAddon, helmv1alpha1.FinalizerName) { + if err := r.Update(ctx, latestAddon); err != nil { + return err + } + } + return nil + }); err != nil { + return reconcile.Result{}, fmt.Errorf("removing finalizer: %w", err) + } + + logger.Info("Cleanup complete") + + return reconcile.Result{}, nil +} + +func setStatusAttrs(repoType utils.InternalRepositoryType, chartRes services.ChartResult, repoRes services.OCIRepoResult, releaseRes services.ReleaseResult) status.MutatorFunc { + return func(obj status.ObjectWithConditions, results []status.Provider) (status.ObjectWithConditions, []status.Provider) { + results = status.DetermineConditions(obj, results...) + addon := obj.(*helmv1alpha1.HelmClusterAddon) + + var updateChart bool + + switch repoType { + case utils.InternalHelmRepository: + if chartRes.HasArtifact() && releaseRes.IsReady() { + if addon.Status.LastAppliedChart == nil || (addon.IsChartStatusInfoOutdated() && chartRes.IsReady()) { + updateChart = true + } + } + case utils.InternalOCIRepository: + if repoRes.HasArtifact() && releaseRes.IsReady() { + if addon.Status.LastAppliedChart == nil || (addon.IsChartStatusInfoOutdated() && repoRes.IsReady()) { + updateChart = true + } + } + } + + if updateChart { + addon.Status.LastAppliedChart = &helmv1alpha1.HelmClusterAddonLastAppliedChartRef{ + HelmClusterAddonChartName: addon.Spec.Chart.HelmClusterAddonChartName, + HelmClusterAddonRepository: addon.Spec.Chart.HelmClusterAddonRepository, + Version: addon.Spec.Chart.Version, + } + } + + latestRelease := releaseRes.History.Latest() + if releaseRes.IsReady() && latestRelease != nil { + rawValues := []byte(`{}`) + if addon.Spec.Values != nil { + rawValues = addon.Spec.Values.Raw + } + + addonValues, _ := helmchartutil.ReadValues(rawValues) + if latestRelease.Status == "deployed" && latestRelease.ConfigDigest == chartutil.DigestValues(digest.Canonical, addonValues).String() { + if addon.Spec.Values == nil { + addon.Status.LastAppliedValues = nil + } else { + addon.Status.LastAppliedValues = addon.Spec.Values.DeepCopy() + } + } + } + + return obj, results + } +} + +func mapResourceStatus() status.MapperFunc { + return func(conditionType string, status status.Status) status.Status { + if conditionType == helmv1alpha1.ConditionTypePartiallyDegraded { + switch status.Status { + // ConditionTrue means that HelmChartSucceeded, resetting status would exclude it from result. + case metav1.ConditionTrue: + status.Status = "" + // ConditionFalse means that chart failed, change Status to True, to raise ConditionTypePartiallyDegraded condition. + case metav1.ConditionFalse: + status.Status = metav1.ConditionTrue + } + } + + return status + } +} diff --git a/images/operator-helm-artifact/internal/reconcile/helmclusteraddonrepository/reconciler.go b/images/operator-helm-artifact/internal/reconcile/helmclusteraddonrepository/reconciler.go new file mode 100644 index 0000000..ba436da --- /dev/null +++ b/images/operator-helm-artifact/internal/reconcile/helmclusteraddonrepository/reconciler.go @@ -0,0 +1,197 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddonrepository + +import ( + "context" + "fmt" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/internal/manager/status" + "github.com/deckhouse/operator-helm/internal/services" + "github.com/deckhouse/operator-helm/internal/utils" +) + +func New( + client client.Client, + helmRepositoryService *services.HelmRepoService, + ociRepositoryService *services.OCIRepoService, + chartSyncService *services.RepoSyncService, + statusManager *status.Manager, +) *Reconciler { + return &Reconciler{ + Client: client, + helmRepositoryService: helmRepositoryService, + ociRepositoryService: ociRepositoryService, + chartSyncService: chartSyncService, + statusManager: statusManager, + } +} + +type Reconciler struct { + client.Client + + helmRepositoryService *services.HelmRepoService + ociRepositoryService *services.OCIRepoService + chartSyncService *services.RepoSyncService + statusManager *status.Manager +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger := log.FromContext(ctx) + ctx = log.IntoContext(ctx, logger) + + var repo helmv1alpha1.HelmClusterAddonRepository + if err := r.Get(ctx, req.NamespacedName, &repo); err != nil { + if apierrors.IsNotFound(err) { + return reconcile.Result{}, nil + } + return reconcile.Result{}, fmt.Errorf("getting helm cluster addon repository: %w", err) + } + + repoType, err := utils.GetRepositoryType(repo.Spec.URL) + if err != nil { + logger.Error(err, "failed to determine repository type") + return reconcile.Result{}, err + } + + if !repo.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, &repo, repoType) + } + + if !controllerutil.ContainsFinalizer(&repo, helmv1alpha1.FinalizerName) { + controllerutil.AddFinalizer(&repo, helmv1alpha1.FinalizerName) + + if err := r.Update(ctx, &repo); err != nil { + return reconcile.Result{}, fmt.Errorf("adding finalizer: %w", err) + } + return r.requeueAtSyncInterval(&repo) + } + + if err := r.statusManager.InitializeConditions(ctx, &repo, + helmv1alpha1.ConditionTypeReady, + helmv1alpha1.ConditionTypeSynced, + ); err != nil { + return reconcile.Result{}, err + } + + var helmRepoRes services.HelmRepoResult + var ociRepoRes services.OCIRepoResult + var chartSyncRes services.RepoSyncResult + + switch repoType { + case utils.InternalHelmRepository: + helmRepoRes = r.helmRepositoryService.EnsureInternalHelmRepository(ctx, &repo) + case utils.InternalOCIRepository: + if err := r.helmRepositoryService.RemoveHelmRepository(ctx, repo.Name); err != nil { + ociRepoRes = services.OCIRepoResult{ + Status: status.Failed(&repo, helmv1alpha1.ReasonFailed, "Repository change failed", err), + } + break + } + ociRepoRes = r.ociRepositoryService.EnsureRepositorySecrets(ctx, &repo) + default: + err := fmt.Errorf("unsupported repository type: %q", repoType) + helmRepoRes = services.HelmRepoResult{Status: status.Failed(&repo, "UnsupportedRepositoryType", err.Error(), err)} + } + + if helmRepoRes.IsReady() || ociRepoRes.IsReady() { + chartSyncRes = r.chartSyncService.EnsureAddonCharts(ctx, &repo, repoType) + } else { + chartSyncRes = services.RepoSyncResult{Status: status.Failed(&repo, helmv1alpha1.ReasonRepositoryNotReady, helmRepoRes.Status.Message, nil)} + } + + if err := r.statusManager.Update( + ctx, + &repo, + status.NoopStatusMutator, + status.NoopStatusMapper, + helmRepoRes, + ociRepoRes, + chartSyncRes, + ); client.IgnoreNotFound(err) != nil { + return reconcile.Result{}, fmt.Errorf("failed to update status: %w", err) + } + + return r.requeueAtSyncInterval(&repo) +} + +func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType utils.InternalRepositoryType) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + if !controllerutil.ContainsFinalizer(repo, helmv1alpha1.FinalizerName) { + return reconcile.Result{}, nil + } + + switch repoType { + case utils.InternalHelmRepository: + if err := r.helmRepositoryService.CleanupHelmRepository(ctx, repo.Name); err != nil && !apierrors.IsNotFound(err) { + _ = r.statusManager.Update(ctx, repo, status.NoopStatusMutator, status.NoopStatusMapper, services.HelmRepoResult{ + Status: status.Failed(repo, helmv1alpha1.ReasonFailed, "Failed to remove dependencies", err), + }) + return reconcile.Result{}, err + } + case utils.InternalOCIRepository: + if err := r.ociRepositoryService.CleanupOCIRepository(ctx, repo.Name); err != nil && !apierrors.IsNotFound(err) { + _ = r.statusManager.Update(ctx, repo, status.NoopStatusMutator, status.NoopStatusMapper, services.HelmRepoResult{ + Status: status.Failed(repo, helmv1alpha1.ReasonFailed, "Failed to remove dependencies", err), + }) + return reconcile.Result{}, err + } + } + + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + latestRepo := &helmv1alpha1.HelmClusterAddonRepository{} + if err := r.Get(ctx, client.ObjectKeyFromObject(repo), latestRepo); err != nil { + return client.IgnoreNotFound(err) + } + + if controllerutil.RemoveFinalizer(latestRepo, helmv1alpha1.FinalizerName) { + if err := r.Update(ctx, latestRepo); err != nil { + return err // This will trigger a retry if it's a conflict + } + } + return nil + }); err != nil { + return reconcile.Result{}, fmt.Errorf("removing finalizer: %w", err) + } + + logger.Info("Cleanup complete") + + return reconcile.Result{}, nil +} + +func (r *Reconciler) requeueAtSyncInterval(repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { + repoSyncCond := apimeta.FindStatusCondition(repo.Status.Conditions, helmv1alpha1.ConditionTypeSynced) + if repoSyncCond != nil { + remaining := time.Until(repoSyncCond.LastTransitionTime.Add(services.ChartsSyncInterval)) + if remaining > 0 { + return reconcile.Result{RequeueAfter: remaining}, nil + } + } + + return reconcile.Result{RequeueAfter: services.ChartsSyncInterval}, nil +} diff --git a/images/operator-helm-artifact/internal/services/base.go b/images/operator-helm-artifact/internal/services/base.go new file mode 100644 index 0000000..077ef80 --- /dev/null +++ b/images/operator-helm-artifact/internal/services/base.go @@ -0,0 +1,131 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/internal/utils" +) + +type BaseService struct { + Client client.Client + Scheme *runtime.Scheme +} + +func (s *BaseService) ensureResourceDeleted(ctx context.Context, nn types.NamespacedName, obj client.Object) error { + err := s.Client.Get(ctx, nn, obj) + if err != nil { + return client.IgnoreNotFound(err) + } + + if err := s.Client.Delete(ctx, obj); client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to delete resource %s/%s: %w", nn.Namespace, nn.Name, err) + } + + return nil +} + +type BaseRepoService struct { + BaseService + + TargetNamespace string +} + +func (s *BaseRepoService) reconcileAuthSecret(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) error { + secretName := utils.GetInternalRepositoryAuthSecretName(repo.Name) + + if repo.Spec.Auth == nil { + nn := types.NamespacedName{Name: secretName, Namespace: s.TargetNamespace} + if err := s.ensureResourceDeleted(ctx, nn, &corev1.Secret{}); err != nil { + return fmt.Errorf("deleting obsolete auth secret: %w", err) + } + return nil + } + + authSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: s.TargetNamespace, + }, + } + + if _, err := controllerutil.CreateOrPatch(ctx, s.Client, authSecret, func() error { + authSecret.Labels = map[string]string{ + helmv1alpha1.LabelManagedBy: helmv1alpha1.LabelManagedByValue, + helmv1alpha1.HelmClusterAddonRepositoryLabelSourceName: repo.Name, + } + + authSecret.StringData = map[string]string{ + "username": repo.Spec.Auth.Username, + "password": repo.Spec.Auth.Password, + } + + return nil + }); err != nil { + return fmt.Errorf("creating auth secret: %w", err) + } + + return nil +} + +func (s *BaseRepoService) reconcileTLSSecret(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) error { + secretName := utils.GetInternalRepositoryTLSSecretName(repo.Name) + + if repo.Spec.CACertificate == "" { + nn := types.NamespacedName{Name: secretName, Namespace: s.TargetNamespace} + if err := s.ensureResourceDeleted(ctx, nn, &corev1.Secret{}); err != nil { + return fmt.Errorf("deleting obsolete tls secret: %w", err) + } + return nil + } + + // TODO: consider adding CA certificate format validation + + tlsSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: s.TargetNamespace, + }, + } + + if _, err := controllerutil.CreateOrPatch(ctx, s.Client, tlsSecret, func() error { + tlsSecret.Labels = map[string]string{ + helmv1alpha1.LabelManagedBy: helmv1alpha1.LabelManagedByValue, + helmv1alpha1.HelmClusterAddonRepositoryLabelSourceName: repo.Name, + } + + tlsSecret.StringData = map[string]string{ + "ca.crt": repo.Spec.CACertificate, + } + + return nil + }); err != nil { + return fmt.Errorf("cannot reconcile tls secret: %w", err) + } + + return nil +} diff --git a/images/operator-helm-artifact/internal/services/chart_service.go b/images/operator-helm-artifact/internal/services/chart_service.go new file mode 100644 index 0000000..23b3499 --- /dev/null +++ b/images/operator-helm-artifact/internal/services/chart_service.go @@ -0,0 +1,154 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + "fmt" + + "github.com/werf/3p-fluxcd-pkg/apis/meta" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/internal/manager/status" + "github.com/deckhouse/operator-helm/internal/utils" +) + +var helmChartErrorRules = []status.ErrorConditionRule{ + {Type: "FetchFailed", TriggerStatus: metav1.ConditionTrue, Reason: helmv1alpha1.ReasonChartFetchFailed}, + {Type: "StorageOperationFailed", TriggerStatus: metav1.ConditionTrue, Reason: helmv1alpha1.ReasonChartStorageFailed}, +} + +type ChartService struct { + BaseService + + TargetNamespace string +} + +func NewChartService(client client.Client, scheme *runtime.Scheme, targetNamespace string) *ChartService { + return &ChartService{ + BaseService: BaseService{ + Client: client, + Scheme: scheme, + }, + TargetNamespace: targetNamespace, + } +} + +var _ status.Provider = (*ChartResult)(nil) + +type ChartResult struct { + Status status.Status + Artifact *meta.Artifact +} + +func (r ChartResult) GetStatus() status.Status { + return r.Status +} + +func (r ChartResult) IsReady() bool { + return r.Artifact != nil && r.Status.Observed && r.Status.Status == metav1.ConditionTrue +} + +func (r ChartResult) IsPartiallyDegraded() bool { + return r.Artifact != nil && r.Status.Status != metav1.ConditionTrue && r.Status.Observed +} + +func (r ChartResult) HasArtifact() bool { + return r.Artifact != nil && r.Status.Observed +} + +func (r ChartResult) GetConditionType() string { + return helmv1alpha1.ConditionTypePartiallyDegraded +} + +func (s *ChartService) EnsureHelmChart(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) ChartResult { + logger := log.FromContext(ctx) + + existing := &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.GetInternalHelmChartName(addon.Name), + Namespace: s.TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, s.Client, existing, func() error { + applyHelmChartSpec(addon, existing) + + return nil + }) + if err != nil { + return ChartResult{Status: status.Failed( + addon, + helmv1alpha1.ReasonHelmChartFailed, + "Failed to create helm chart", + fmt.Errorf("creating or updating helm chart: %w", err), + )} + } + + if op != controllerutil.OperationResultNone { + logger.Info("Reconciled helm chart", "operation", op) + } + + processedStatus := status.ProcessChildConditions( + existing.GetConditions(), existing.Generation, addon, helmChartErrorRules, + ) + processedStatus.NotReflectable = existing.Status.Artifact != nil + + if processedStatus.IsReady() { + logger.Info("Successfully reconciled helm chart", "operation", op, "chart", addon.Spec.Chart.HelmClusterAddonChartName) + } + + return ChartResult{ + Artifact: existing.Status.Artifact, + Status: processedStatus, + } +} + +func (s *ChartService) CleanupHelmChart(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) error { + nn := types.NamespacedName{Name: utils.GetInternalHelmChartName(addon.Name), Namespace: s.TargetNamespace} + if err := s.ensureResourceDeleted(ctx, nn, &sourcev1.HelmChart{}); err != nil { + return fmt.Errorf("failed to delete helm chart: %w", err) + } + + return nil +} + +func applyHelmChartSpec(addon *helmv1alpha1.HelmClusterAddon, existing *sourcev1.HelmChart) { + if existing.Labels == nil { + existing.Labels = map[string]string{} + } + + existing.Labels[helmv1alpha1.LabelManagedBy] = helmv1alpha1.LabelManagedByValue + existing.Labels[helmv1alpha1.HelmClusterAddonLabelSourceName] = addon.Name + existing.Labels[helmv1alpha1.HelmClusterAddonChartLabelSourceName] = utils.GetHelmClusterAddonChartName( + addon.Spec.Chart.HelmClusterAddonRepository, addon.Spec.Chart.HelmClusterAddonChartName) + + existing.Spec.Chart = addon.Spec.Chart.HelmClusterAddonChartName + existing.Spec.Version = addon.Spec.Chart.Version + + existing.Spec.SourceRef = sourcev1.LocalHelmChartSourceReference{ + Kind: sourcev1.HelmRepositoryKind, + Name: utils.GetInternalHelmRepositoryName(addon.Spec.Chart.HelmClusterAddonRepository), + } +} diff --git a/images/operator-helm-artifact/internal/services/helm_repo_service.go b/images/operator-helm-artifact/internal/services/helm_repo_service.go new file mode 100644 index 0000000..0578c46 --- /dev/null +++ b/images/operator-helm-artifact/internal/services/helm_repo_service.go @@ -0,0 +1,191 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + "fmt" + "time" + + "github.com/werf/3p-fluxcd-pkg/apis/meta" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/internal/manager/status" + "github.com/deckhouse/operator-helm/internal/utils" +) + +const ( + InternalRepositoryInterval = 5 * time.Minute + ChartsSyncInterval = 5 * time.Minute +) + +type HelmRepoService struct { + BaseRepoService +} + +func NewHelmRepoService(client client.Client, scheme *runtime.Scheme, namespace string) *HelmRepoService { + return &HelmRepoService{ + BaseRepoService: BaseRepoService{ + BaseService: BaseService{ + Client: client, + Scheme: scheme, + }, + TargetNamespace: namespace, + }, + } +} + +var _ status.Provider = (*HelmRepoResult)(nil) + +type HelmRepoResult struct { + Status status.Status +} + +func (r HelmRepoResult) GetStatus() status.Status { + return r.Status +} + +func (r HelmRepoResult) IsReady() bool { + return r.Status.IsReady() +} + +func (r HelmRepoResult) GetConditionType() string { + return helmv1alpha1.ConditionTypeReady +} + +func (s *HelmRepoService) EnsureInternalHelmRepository(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) HelmRepoResult { + logger := log.FromContext(ctx) + + if err := s.reconcileAuthSecret(ctx, repo); err != nil { + return HelmRepoResult{Status: status.Failed(repo, helmv1alpha1.ReasonFailed, "Failed to reconcile auth secret", err)} + } + + if err := s.reconcileTLSSecret(ctx, repo); err != nil { + return HelmRepoResult{Status: status.Failed(repo, helmv1alpha1.ReasonFailed, "Failed to reconcile tls secret", err)} + } + + existing := &sourcev1.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.GetInternalHelmRepositoryName(repo.Name), + Namespace: s.TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, s.Client, existing, func() error { + applyHelmRepositorySpec(repo, existing) + + return nil + }) + if err != nil { + return HelmRepoResult{ + Status: status.Failed( + repo, + helmv1alpha1.ReasonFailed, + "Failed to reconcile helm repository", + fmt.Errorf("creating helm repository: %w", err)), + } + } + + if op != controllerutil.OperationResultNone { + logger.Info("Reconciled helm repository", "operation", op) + } + + if cond, ok := status.IsConditionObserved(existing.Status.Conditions, helmv1alpha1.ConditionTypeReady, existing.Generation); ok { + return HelmRepoResult{Status: status.Status{ + Observed: ok, + Status: cond.Status, + ObservedGeneration: repo.Generation, + Reason: cond.Reason, + Message: cond.Message, + }} + } + + return HelmRepoResult{Status: status.Unknown(repo, helmv1alpha1.ReasonReconciling)} +} + +func (s *HelmRepoService) RemoveHelmRepository(ctx context.Context, repoName string) error { + name := utils.GetInternalHelmRepositoryName(repoName) + nn := types.NamespacedName{Name: name, Namespace: s.TargetNamespace} + if err := s.ensureResourceDeleted(ctx, nn, &sourcev1.HelmRepository{}); err != nil { + return fmt.Errorf("removing helm repository: %w", err) + } + + return nil +} + +func (s *HelmRepoService) CleanupHelmRepository(ctx context.Context, repoName string) error { + resources := []struct { + name string + obj client.Object + }{ + { + name: utils.GetInternalRepositoryAuthSecretName(repoName), + obj: &corev1.Secret{}, + }, + { + name: utils.GetInternalRepositoryTLSSecretName(repoName), + obj: &corev1.Secret{}, + }, + { + name: utils.GetInternalHelmRepositoryName(repoName), + obj: &sourcev1.HelmRepository{}, + }, + } + + for _, r := range resources { + nn := types.NamespacedName{Name: r.name, Namespace: s.TargetNamespace} + if err := s.ensureResourceDeleted(ctx, nn, r.obj); err != nil { + return fmt.Errorf("cleaning up %T %s: %w", r.obj, r.name, err) + } + } + + return nil +} + +func applyHelmRepositorySpec(repo *helmv1alpha1.HelmClusterAddonRepository, existing *sourcev1.HelmRepository) { + existing.Spec.URL = repo.Spec.URL + existing.Spec.Interval = metav1.Duration{Duration: InternalRepositoryInterval} + existing.Spec.Insecure = !repo.Spec.TLSVerify + existing.Spec.CertSecretRef = nil + existing.Spec.SecretRef = nil + + if repo.Spec.Auth != nil { + existing.Spec.SecretRef = &meta.LocalObjectReference{ + Name: utils.GetInternalRepositoryAuthSecretName(repo.Name), + } + existing.Spec.PassCredentials = true + } + + if repo.Spec.CACertificate != "" { + existing.Spec.CertSecretRef = &meta.LocalObjectReference{ + Name: utils.GetInternalRepositoryTLSSecretName(repo.Name), + } + } + + existing.Labels = map[string]string{ + helmv1alpha1.LabelManagedBy: helmv1alpha1.LabelManagedByValue, + helmv1alpha1.HelmClusterAddonRepositoryLabelSourceName: repo.Name, + } +} diff --git a/images/operator-helm-artifact/internal/services/maintenance_service.go b/images/operator-helm-artifact/internal/services/maintenance_service.go new file mode 100644 index 0000000..1ace641 --- /dev/null +++ b/images/operator-helm-artifact/internal/services/maintenance_service.go @@ -0,0 +1,137 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + "fmt" + + helmv2 "github.com/werf/3p-helm-controller/api/v2" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + statusmgr "github.com/deckhouse/operator-helm/internal/manager/status" + "github.com/deckhouse/operator-helm/internal/utils" +) + +type MaintenanceService struct { + BaseService + + TargetNamespace string +} + +func NewMaintenanceService(client client.Client, scheme *runtime.Scheme, targetNamespace string) *MaintenanceService { + return &MaintenanceService{ + BaseService: BaseService{ + Client: client, + Scheme: scheme, + }, + TargetNamespace: targetNamespace, + } +} + +var _ statusmgr.Provider = (*MaintenanceResult)(nil) + +type MaintenanceResult struct { + Status statusmgr.Status +} + +func (r MaintenanceResult) GetStatus() statusmgr.Status { + return r.Status +} + +func (r MaintenanceResult) IsReady() bool { + return r.Status.IsReady() +} + +func (r MaintenanceResult) GetConditionType() string { + return helmv1alpha1.ConditionTypeManaged +} + +func (s *MaintenanceService) EnsureMaintenanceMode(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) MaintenanceResult { + logger := log.FromContext(ctx) + + suspendState := addon.MaintenanceModeActivated() + status := metav1.ConditionTrue + reason := helmv1alpha1.ReasonMaintenanceModeInactive + + if suspendState { + logger.Info("Enabling maintenance mode") + status = metav1.ConditionFalse + reason = helmv1alpha1.ReasonMaintenanceModeActive + } else { + logger.Info("Disabling maintenance mode") + } + + err := s.updateHelmReleaseSuspendState(ctx, addon, suspendState) + if err != nil { + return MaintenanceResult{Status: statusmgr.Failed(addon, helmv1alpha1.ReasonFailed, "Failed to change maintenance mode", err)} + } + return MaintenanceResult{ + Status: statusmgr.Status{ + Observed: true, + Status: status, + ObservedGeneration: addon.Generation, + Reason: reason, + }, + } +} + +func (s *MaintenanceService) IsMaintenanceModeChangeRequired(addon *helmv1alpha1.HelmClusterAddon) bool { + if addon.MaintenanceModeActivated() && !addon.MaintenanceModeEnabled() { + return true + } + + if !addon.MaintenanceModeActivated() && (addon.MaintenanceModeEnabled() || + apimeta.IsStatusConditionPresentAndEqual(addon.Status.Conditions, helmv1alpha1.ConditionTypeManaged, metav1.ConditionUnknown)) { + return true + } + + return false +} + +func (s *MaintenanceService) updateHelmReleaseSuspendState(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, suspend bool) error { + helmRelease := &helmv2.HelmRelease{} + if err := s.Client.Get(ctx, types.NamespacedName{ + Name: utils.GetInternalHelmReleaseName(addon.Name), + Namespace: s.TargetNamespace, + }, helmRelease); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return fmt.Errorf("getting helm release: %w", err) + } + + if helmRelease.Spec.Suspend == suspend { + return nil + } + + base := helmRelease.DeepCopy() + helmRelease.Spec.Suspend = suspend + + if err := s.Client.Patch(ctx, helmRelease, client.MergeFrom(base)); err != nil { + return fmt.Errorf("setting helm release suspend state: %w", err) + } + + return nil +} diff --git a/images/operator-helm-artifact/internal/services/oci_repo_service.go b/images/operator-helm-artifact/internal/services/oci_repo_service.go new file mode 100644 index 0000000..f83042f --- /dev/null +++ b/images/operator-helm-artifact/internal/services/oci_repo_service.go @@ -0,0 +1,238 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + "fmt" + + "github.com/werf/3p-fluxcd-pkg/apis/meta" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/internal/manager/status" + "github.com/deckhouse/operator-helm/internal/utils" +) + +var ociRepositoryErrorRules = []status.ErrorConditionRule{ + {Type: "FetchFailed", TriggerStatus: metav1.ConditionTrue, Reason: helmv1alpha1.ReasonOCIFetchFailed}, + {Type: "IncludeUnavailable", TriggerStatus: metav1.ConditionTrue, Reason: helmv1alpha1.ReasonOCIIncludeUnavailable}, + {Type: "StorageOperationFailed", TriggerStatus: metav1.ConditionTrue, Reason: helmv1alpha1.ReasonOCIStorageFailed}, + {Type: "SourceVerified", TriggerStatus: metav1.ConditionFalse, Reason: helmv1alpha1.ReasonOCIVerificationFailed}, +} + +type OCIRepoService struct { + BaseRepoService +} + +func NewOCIRepoService(client client.Client, scheme *runtime.Scheme, namespace string) *OCIRepoService { + return &OCIRepoService{ + BaseRepoService: BaseRepoService{ + BaseService: BaseService{ + Client: client, + Scheme: scheme, + }, + TargetNamespace: namespace, + }, + } +} + +var _ status.Provider = (*OCIRepoResult)(nil) + +type OCIRepoResult struct { + Status status.Status + Artifact *meta.Artifact +} + +func (r OCIRepoResult) GetStatus() status.Status { + return r.Status +} + +func (r OCIRepoResult) IsReady() bool { + return r.Artifact != nil && r.Status.Observed && r.Status.Status == metav1.ConditionTrue +} + +func (r OCIRepoResult) IsPartiallyDegraded() bool { + return r.Artifact != nil && r.Status.Status != metav1.ConditionTrue && r.Status.Observed +} + +func (r OCIRepoResult) HasArtifact() bool { + return r.Artifact != nil && r.Status.Observed +} + +func (r OCIRepoResult) GetConditionType() string { + if r.Status.ConditionType == "" { + return helmv1alpha1.ConditionTypePartiallyDegraded + } + return r.Status.ConditionType +} + +func (s *OCIRepoService) EnsureInternalOCIRepository(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, repo *helmv1alpha1.HelmClusterAddonRepository) OCIRepoResult { + logger := log.FromContext(ctx) + + existing := &sourcev1.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.GetInternalOCIRepositoryName(addon.Name), + Namespace: s.TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, s.Client, existing, func() error { + applyOCIRepositorySpec(addon, repo, existing) + + return nil + }) + if err != nil { + return OCIRepoResult{ + Status: status.Failed( + addon, + helmv1alpha1.ReasonFailed, + "Failed to reconcile oci repository", + fmt.Errorf("creating oci repository: %w", err)), + } + } + + if op != controllerutil.OperationResultNone { + logger.Info("Reconciled oci repository", "operation", op) + } + + processedStatus := status.ProcessChildConditions( + existing.Status.Conditions, existing.Generation, addon, ociRepositoryErrorRules, + ) + processedStatus.NotReflectable = existing.Status.Artifact != nil + + return OCIRepoResult{ + Artifact: existing.Status.Artifact, + Status: processedStatus, + } +} + +func (s *OCIRepoService) EnsureRepositorySecrets(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) OCIRepoResult { + if err := s.reconcileAuthSecret(ctx, repo); err != nil { + return OCIRepoResult{ + Status: status.Status{ + ConditionType: helmv1alpha1.ConditionTypeReady, + Observed: true, + Status: metav1.ConditionFalse, + ObservedGeneration: repo.Generation, + Reason: helmv1alpha1.ReasonFailed, + Message: "Failed to reconcile auth secret", + Err: err, + }, + } + } + + if err := s.reconcileTLSSecret(ctx, repo); err != nil { + return OCIRepoResult{ + Status: status.Status{ + ConditionType: helmv1alpha1.ConditionTypeReady, + Observed: true, + Status: metav1.ConditionFalse, + ObservedGeneration: repo.Generation, + Reason: helmv1alpha1.ReasonFailed, + Message: "Failed to reconcile tls secret", + Err: err, + }, + } + } + + return OCIRepoResult{ + Artifact: &meta.Artifact{}, + Status: status.Status{ + ConditionType: helmv1alpha1.ConditionTypeReady, + Observed: true, + Status: metav1.ConditionTrue, + ObservedGeneration: repo.Generation, + Reason: helmv1alpha1.ReasonSuccess, + }, + } +} + +func (s *OCIRepoService) CleanupOCIRepository(ctx context.Context, repoName string) error { + resources := []struct { + name string + obj client.Object + }{ + { + name: utils.GetInternalRepositoryAuthSecretName(repoName), + obj: &corev1.Secret{}, + }, + { + name: utils.GetInternalRepositoryTLSSecretName(repoName), + obj: &corev1.Secret{}, + }, + } + + for _, r := range resources { + nn := types.NamespacedName{Name: r.name, Namespace: s.TargetNamespace} + if err := s.ensureResourceDeleted(ctx, nn, r.obj); err != nil { + return fmt.Errorf("cleaning up %T %s: %w", r.obj, r.name, err) + } + } + + return nil +} + +func (s *OCIRepoService) RemoveOCIRepository(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) error { + name := utils.GetInternalOCIRepositoryName(addon.Name) + nn := types.NamespacedName{Name: name, Namespace: s.TargetNamespace} + if err := s.ensureResourceDeleted(ctx, nn, &sourcev1.OCIRepository{}); err != nil { + return fmt.Errorf("removing oci repository: %w", err) + } + + return nil +} + +func applyOCIRepositorySpec(addon *helmv1alpha1.HelmClusterAddon, repo *helmv1alpha1.HelmClusterAddonRepository, existing *sourcev1.OCIRepository) { + existing.Spec.URL = repo.Spec.URL + existing.Spec.Reference = &sourcev1.OCIRepositoryRef{ + Tag: addon.Spec.Chart.Version, + } + existing.Spec.Interval = metav1.Duration{Duration: InternalRepositoryInterval} + existing.Spec.Insecure = !repo.Spec.TLSVerify + existing.Spec.CertSecretRef = nil + existing.Spec.SecretRef = nil + + if repo.Spec.Auth != nil { + existing.Spec.SecretRef = &meta.LocalObjectReference{ + Name: utils.GetInternalRepositoryAuthSecretName(repo.Name), + } + } + + if repo.Spec.CACertificate != "" { + existing.Spec.CertSecretRef = &meta.LocalObjectReference{ + Name: utils.GetInternalRepositoryTLSSecretName(repo.Name), + } + } + + existing.Spec.LayerSelector = &sourcev1.OCILayerSelector{ + MediaType: "application/vnd.cncf.helm.chart.content.v1.tar+gzip", + Operation: "copy", + } + + existing.Labels = map[string]string{ + helmv1alpha1.LabelManagedBy: helmv1alpha1.LabelManagedByValue, + helmv1alpha1.HelmClusterAddonLabelSourceName: addon.Name, + } +} diff --git a/images/operator-helm-artifact/internal/services/release_service.go b/images/operator-helm-artifact/internal/services/release_service.go new file mode 100644 index 0000000..62a0621 --- /dev/null +++ b/images/operator-helm-artifact/internal/services/release_service.go @@ -0,0 +1,159 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + "fmt" + + helmv2 "github.com/werf/3p-helm-controller/api/v2" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/internal/manager/status" + "github.com/deckhouse/operator-helm/internal/utils" +) + +var helmReleaseErrorRules = []status.ErrorConditionRule{ + {Type: "Released", TriggerStatus: metav1.ConditionFalse, Reason: helmv1alpha1.ReasonReleaseFailed}, + {Type: "TestSuccess", TriggerStatus: metav1.ConditionFalse, Reason: helmv1alpha1.ReasonTestFailed}, + {Type: "Remediated", TriggerStatus: metav1.ConditionTrue, Reason: helmv1alpha1.ReasonRemediated}, +} + +type ReleaseService struct { + BaseService + + TargetNamespace string +} + +func NewReleaseService(client client.Client, scheme *runtime.Scheme, targetNamespace string) *ReleaseService { + return &ReleaseService{ + BaseService: BaseService{ + Client: client, + Scheme: scheme, + }, + TargetNamespace: targetNamespace, + } +} + +var _ status.Provider = (*ReleaseResult)(nil) + +type ReleaseResult struct { + Status status.Status + History helmv2.Snapshots +} + +func (r ReleaseResult) GetStatus() status.Status { + return r.Status +} + +func (r ReleaseResult) IsReady() bool { + return r.Status.IsReady() +} + +func (r ReleaseResult) GetConditionType() string { + return helmv1alpha1.ConditionTypeReady +} + +func (s *ReleaseService) EnsureHelmRelease(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, repoType utils.InternalRepositoryType) ReleaseResult { + logger := log.FromContext(ctx) + + existing := &helmv2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.GetInternalHelmReleaseName(addon.Name), + Namespace: s.TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, s.Client, existing, func() error { + return applyHelmReleaseSpec(addon, existing, repoType, s.TargetNamespace) + }) + if err != nil { + return ReleaseResult{Status: status.Failed( + addon, + helmv1alpha1.ReasonReleaseFailed, + "Failed to create helm release", + fmt.Errorf("reconciling helm release: %w", err), + )} + } + + processedStatus := status.ProcessChildConditions( + existing.GetConditions(), existing.Generation, addon, helmReleaseErrorRules, + ) + + if processedStatus.IsReady() { + logger.Info("Successfully reconciled helm release", "operation", op) + } + + return ReleaseResult{ + History: existing.Status.History, + Status: processedStatus, + } +} + +func (s *ReleaseService) CleanupHelmRelease(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) error { + nn := types.NamespacedName{Name: utils.GetInternalHelmReleaseName(addon.Name), Namespace: s.TargetNamespace} + if err := s.ensureResourceDeleted(ctx, nn, &helmv2.HelmRelease{}); err != nil { + return fmt.Errorf("failed to delete helm release: %w", err) + } + + return nil +} + +func applyHelmReleaseSpec(addon *helmv1alpha1.HelmClusterAddon, existing *helmv2.HelmRelease, repoType utils.InternalRepositoryType, targetNamespace string) error { + if existing.Labels == nil { + existing.Labels = map[string]string{} + } + + existing.Labels[helmv1alpha1.LabelManagedBy] = helmv1alpha1.LabelManagedByValue + existing.Labels[helmv1alpha1.HelmClusterAddonLabelSourceName] = addon.Name + + existing.Spec.ReleaseName = addon.Name + existing.Spec.TargetNamespace = addon.Spec.Namespace + existing.Spec.Values = addon.Spec.Values + + existing.Spec.Suspend = false + + if addon.Spec.Maintenance == string(helmv1alpha1.NoResourceReconciliation) { + existing.Spec.Suspend = true + } + + switch repoType { + case utils.InternalHelmRepository: + existing.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{ + Kind: sourcev1.HelmChartKind, + Name: utils.GetInternalHelmChartName(addon.Name), + Namespace: targetNamespace, + } + case utils.InternalOCIRepository: + existing.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{ + Kind: sourcev1.OCIRepositoryKind, + Name: utils.GetInternalOCIRepositoryName(addon.Name), + Namespace: targetNamespace, + } + default: + return fmt.Errorf("invalid repository type: %s", repoType) + } + + return nil +} diff --git a/images/operator-helm-artifact/internal/services/repo_sync_service.go b/images/operator-helm-artifact/internal/services/repo_sync_service.go new file mode 100644 index 0000000..364b032 --- /dev/null +++ b/images/operator-helm-artifact/internal/services/repo_sync_service.go @@ -0,0 +1,239 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + "fmt" + "time" + + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + repoclient "github.com/deckhouse/operator-helm/internal/client/repository" + "github.com/deckhouse/operator-helm/internal/manager/status" + "github.com/deckhouse/operator-helm/internal/utils" +) + +const ( + + // LabelRepositoryName stores HelmClusterAddonRepository name. + LabelRepositoryName = "repository" + + // LabelChartName stores chart name. + LabelChartName = "chart" +) + +type RepoSyncService struct { + BaseService +} + +func NewRepoSyncService(client client.Client, scheme *runtime.Scheme) *RepoSyncService { + return &RepoSyncService{ + BaseService: BaseService{ + Client: client, + Scheme: scheme, + }, + } +} + +var _ status.Provider = (*RepoSyncResult)(nil) + +type RepoSyncResult struct { + Status status.Status +} + +func (r RepoSyncResult) GetStatus() status.Status { + return r.Status +} + +func (r RepoSyncResult) IsReady() bool { + return r.Status.IsReady() +} + +func (r RepoSyncResult) GetConditionType() string { + return helmv1alpha1.ConditionTypeSynced +} + +func (s *RepoSyncService) EnsureAddonCharts(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType utils.InternalRepositoryType) RepoSyncResult { + logger := log.FromContext(ctx) + + if !isRepoSyncRequired(repo) { + return RepoSyncResult{Status: status.Empty()} + } else if !isRepoSyncInProgress(repo) { + return RepoSyncResult{Status: status.Unknown(repo, helmv1alpha1.ReasonReconciling)} + } + + repoClient, err := repoclient.NewClient(repoType) + if err != nil { + return RepoSyncResult{ + Status: status.Failed( + repo, + helmv1alpha1.ReasonSyncFailed, + "Failed to get repository client on chart sync", + fmt.Errorf("getting repository client: %w", err), + ), + } + } + + var repoConfig *repoclient.RepoConfig + if repo.Spec.Auth != nil || repo.Spec.CACertificate != "" || !repo.Spec.TLSVerify { + repoConfig = &repoclient.RepoConfig{ + Insecure: !repo.Spec.TLSVerify, + } + if repo.Spec.Auth != nil { + repoConfig.Username = repo.Spec.Auth.Username + repoConfig.Password = repo.Spec.Auth.Password + } + if repo.Spec.CACertificate != "" { + repoConfig.CACertificate = repo.Spec.CACertificate + } + } + + charts, err := repoClient.FetchCharts(ctx, repo.Spec.URL, repoConfig) + if err != nil { + return RepoSyncResult{ + Status: status.Failed( + repo, + helmv1alpha1.ReasonSyncFailed, + "Failed to fetch charts from repository", + fmt.Errorf("fetching charts: %w", err), + ), + } + } + + desiredCharts := make(map[string]struct{}, len(charts)) + + for chart, versions := range charts { + addonChartName := utils.GetHelmClusterAddonChartName(repo.Name, chart) + existing := &helmv1alpha1.HelmClusterAddonChart{ + ObjectMeta: metav1.ObjectMeta{ + Name: addonChartName, + }, + } + + desiredCharts[existing.Name] = struct{}{} + + op, err := controllerutil.CreateOrPatch(ctx, s.Client, existing, func() error { + existing.OwnerReferences = []metav1.OwnerReference{ + { + APIVersion: repo.APIVersion, + Kind: repo.Kind, + Name: repo.Name, + UID: repo.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + } + existing.Labels = map[string]string{ + helmv1alpha1.LabelDeckhouseHeritage: helmv1alpha1.LabelDeckhouseHeritageValue, + LabelRepositoryName: repo.Name, + LabelChartName: chart, + } + return nil + }) + if err != nil { + return RepoSyncResult{ + Status: status.Failed( + repo, + helmv1alpha1.ReasonSyncFailed, + fmt.Sprintf("Failed to create HelmClusterAddonChart %q", addonChartName), + fmt.Errorf("cannot create or update HelmClusterAddonChart: %w", err), + ), + } + } + + if op != controllerutil.OperationResultNone { + logger.Info("Reconciled HelmClusterAddonChart", "operation", op, "addonChartName", addonChartName) + } + + base := existing.DeepCopy() + existing.Status.Versions = versions + + if err := s.Client.Status().Patch(ctx, existing, client.MergeFrom(base)); err != nil { + return RepoSyncResult{ + Status: status.Failed( + repo, + helmv1alpha1.ReasonSyncFailed, + fmt.Sprintf("Failed to update HelmClusterAddonChart %q versions", addonChartName), + fmt.Errorf("updating chart versions: %w", err), + ), + } + } + + logger.Info("Successfully synced HelmClusterAddonChart versions", "operation", op, "addonChartName", addonChartName) + } + + var existingCharts helmv1alpha1.HelmClusterAddonChartList + if err := s.Client.List(ctx, &existingCharts, client.MatchingLabels{LabelRepositoryName: repo.Name}); err != nil { + return RepoSyncResult{ + Status: status.Failed( + repo, + helmv1alpha1.ReasonSyncFailed, + "Failed to list stale charts for pruning", + fmt.Errorf("listing existing charts for pruning: %w", err), + ), + } + } + + for _, chart := range existingCharts.Items { + if _, wanted := desiredCharts[chart.Name]; wanted { + continue + } + + if err := s.ensureResourceDeleted(ctx, types.NamespacedName{Name: chart.Name}, &chart); err != nil { + return RepoSyncResult{ + Status: status.Failed( + repo, + helmv1alpha1.ReasonSyncFailed, + "Failed to delete stale charts", + fmt.Errorf("deleting stale charts: %w", err), + ), + } + } + } + + logger.Info(fmt.Sprintf("Scheduling next repo sync in %s", ChartsSyncInterval)) + + return RepoSyncResult{ + Status: status.Success(repo), + } +} + +func isRepoSyncRequired(repo *helmv1alpha1.HelmClusterAddonRepository) bool { + syncCond := apimeta.FindStatusCondition(repo.Status.Conditions, helmv1alpha1.ConditionTypeSynced) + if syncCond != nil && syncCond.Status == metav1.ConditionTrue && syncCond.LastTransitionTime.UTC().Add(ChartsSyncInterval).After(time.Now().UTC()) { + return false + } + return true +} + +func isRepoSyncInProgress(repo *helmv1alpha1.HelmClusterAddonRepository) bool { + syncCond := apimeta.FindStatusCondition(repo.Status.Conditions, helmv1alpha1.ConditionTypeSynced) + if syncCond != nil && syncCond.Status == metav1.ConditionUnknown && syncCond.Reason == helmv1alpha1.ReasonReconciling { + return true + } + + return false +} diff --git a/images/operator-helm-artifact/internal/utils/mapper.go b/images/operator-helm-artifact/internal/utils/mapper.go new file mode 100644 index 0000000..01255cd --- /dev/null +++ b/images/operator-helm-artifact/internal/utils/mapper.go @@ -0,0 +1,94 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "context" + + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" +) + +func MapInternalResources(controllerName, targetNamespace, labelManagedBy, labelManagedByValue, labelSourceName string) handler.MapFunc { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + + if obj.GetNamespace() != targetNamespace { + return nil + } + + labels := obj.GetLabels() + if labels[labelManagedBy] != labelManagedByValue { + return nil + } + + sourceName := labels[labelSourceName] + if sourceName == "" { + logger.Info("resource missing source label, skipping", + "controller", controllerName, "name", obj.GetName(), "namespace", obj.GetNamespace()) + + return nil + } + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: sourceName, + Namespace: "", + }, + }, + } + } +} + +const AddonRepositoryIndex = ".spec.chart.helmClusterAddonRepository" + +func SetupAddonRepositoryIndex(mgr ctrl.Manager) error { + return mgr.GetFieldIndexer().IndexField(context.Background(), &helmv1alpha1.HelmClusterAddon{}, AddonRepositoryIndex, + func(obj client.Object) []string { + addon := obj.(*helmv1alpha1.HelmClusterAddon) + if addon.Spec.Chart.HelmClusterAddonRepository == "" { + return nil + } + return []string{addon.Spec.Chart.HelmClusterAddonRepository} + }, + ) +} + +func MapRepositoryToAddons(c client.Client) handler.MapFunc { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + addonList := &helmv1alpha1.HelmClusterAddonList{} + if err := c.List(ctx, addonList, client.MatchingFields{AddonRepositoryIndex: obj.GetName()}); err != nil { + log.FromContext(ctx).Error(err, "Failed to list HelmClusterAddons for repository mapping") + return nil + } + + requests := make([]reconcile.Request, 0, len(addonList.Items)) + for _, addon := range addonList.Items { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: addon.Name}, + }) + } + return requests + } +} diff --git a/images/operator-helm-artifact/internal/utils/name.go b/images/operator-helm-artifact/internal/utils/name.go new file mode 100644 index 0000000..a703602 --- /dev/null +++ b/images/operator-helm-artifact/internal/utils/name.go @@ -0,0 +1,145 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "crypto/sha256" + "fmt" + "strings" +) + +func GetHash(s string) string { + h := sha256.New() + h.Write([]byte(s)) + + return fmt.Sprintf("%x", h.Sum(nil))[:12] +} + +func GetInternalRepositoryAuthSecretName(internalRepoName string) string { + prefix := "hcar-auth" + + hash := GetHash(fmt.Sprintf("%s-%s", prefix, internalRepoName)) + + var result, postfix string + + result = prefix + "-" + + if len(internalRepoName) > 53 { + result += internalRepoName[:40] + postfix = "-" + hash + } else { + result += internalRepoName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetInternalRepositoryTLSSecretName(internalRepoName string) string { + prefix := "hcar-tls" + + hash := GetHash(fmt.Sprintf("%s-%s", prefix, internalRepoName)) + + var result, postfix string + + result = prefix + "-" + + if len(internalRepoName) > 54 { + result += internalRepoName[:41] + postfix = "-" + hash + } else { + result += internalRepoName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetHelmClusterAddonChartName(repoName, addonName string) string { + hash := GetHash(fmt.Sprintf("%s-%s", repoName, addonName)) + + var result, postfix string + + if len(repoName) > 24 { + result += repoName[:24] + postfix = "-" + hash + } else { + result += repoName + } + + if len(addonName) > 24 { + result += "-" + addonName[:24] + postfix = "-" + hash + } else { + result += "-" + addonName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetInternalHelmReleaseName(addonName string) string { + prefix := "hca" + hash := GetHash(fmt.Sprintf("%s-%s", prefix, addonName)) + + result := prefix + "-" + postfix := "" + + if len(addonName) > 59 { + result += addonName[:46] + postfix = "-" + hash + } else { + result += addonName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetInternalHelmChartName(addonName string) string { + return GetInternalHelmReleaseName(addonName) +} + +func GetInternalOCIRepositoryName(addonName string) string { + prefix := "hca" + hash := GetHash(fmt.Sprintf("%s-%s", prefix, addonName)) + + result := prefix + "-" + postfix := "" + + if len(addonName) > 59 { + result += addonName[:46] + postfix = "-" + hash + } else { + result += addonName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetInternalHelmRepositoryName(addonRepositoryName string) string { + prefix := "hcar" + hash := GetHash(fmt.Sprintf("%s-%s", prefix, addonRepositoryName)) + + result := prefix + "-" + postfix := "" + + if len(addonRepositoryName) > 58 { + result += addonRepositoryName[:45] + postfix = "-" + hash + } else { + result += addonRepositoryName + } + + return strings.TrimRight(result, "-") + postfix +} diff --git a/images/operator-helm-artifact/internal/utils/repository.go b/images/operator-helm-artifact/internal/utils/repository.go new file mode 100644 index 0000000..ad6da7c --- /dev/null +++ b/images/operator-helm-artifact/internal/utils/repository.go @@ -0,0 +1,45 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "fmt" + "net/url" +) + +type InternalRepositoryType string + +const ( + InternalHelmRepository InternalRepositoryType = "helm" + InternalOCIRepository InternalRepositoryType = "oci" +) + +func GetRepositoryType(s string) (InternalRepositoryType, error) { + parsedURL, err := url.Parse(s) + if err != nil { + return "", fmt.Errorf("cannot parse url: %w", err) + } + + switch parsedURL.Scheme { + case "http", "https": + return InternalHelmRepository, nil + case "oci": + return InternalOCIRepository, nil + default: + return "", fmt.Errorf("unsupported repository schema in use: %s", parsedURL.Scheme) + } +} diff --git a/images/operator-helm-artifact/internal/webhook/helmclusteraddon/webhook.go b/images/operator-helm-artifact/internal/webhook/helmclusteraddon/webhook.go new file mode 100644 index 0000000..188fbc5 --- /dev/null +++ b/images/operator-helm-artifact/internal/webhook/helmclusteraddon/webhook.go @@ -0,0 +1,81 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddon + +import ( + "context" + "fmt" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" +) + +const addonChartIndex = ".spec.chart.repoAndChart" + +func SetupIndexes(mgr ctrl.Manager) error { + return mgr.GetFieldIndexer().IndexField(context.Background(), &helmv1alpha1.HelmClusterAddon{}, addonChartIndex, + func(obj client.Object) []string { + addon := obj.(*helmv1alpha1.HelmClusterAddon) + return []string{addon.Spec.Chart.HelmClusterAddonRepository + "/" + addon.Spec.Chart.HelmClusterAddonChartName} + }, + ) +} + +func SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr, &helmv1alpha1.HelmClusterAddon{}). + WithValidator(&UniqRepositoryAndChartNameWebhookValidator{Client: mgr.GetClient()}). + Complete() +} + +type UniqRepositoryAndChartNameWebhookValidator struct { + Client client.Client +} + +func (v *UniqRepositoryAndChartNameWebhookValidator) ValidateCreate(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) (admission.Warnings, error) { + return nil, v.checkUniqueness(ctx, addon) +} + +func (v *UniqRepositoryAndChartNameWebhookValidator) ValidateUpdate(ctx context.Context, _, newObj *helmv1alpha1.HelmClusterAddon) (admission.Warnings, error) { + return nil, v.checkUniqueness(ctx, newObj) +} + +func (v *UniqRepositoryAndChartNameWebhookValidator) ValidateDelete(_ context.Context, _ *helmv1alpha1.HelmClusterAddon) (admission.Warnings, error) { + return nil, nil +} + +func (v *UniqRepositoryAndChartNameWebhookValidator) checkUniqueness(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) error { + list := &helmv1alpha1.HelmClusterAddonList{} + indexValue := addon.Spec.Chart.HelmClusterAddonRepository + "/" + addon.Spec.Chart.HelmClusterAddonChartName + + if err := v.Client.List(ctx, list, client.MatchingFields{addonChartIndex: indexValue}); err != nil { + return err + } + + for _, existing := range list.Items { + if existing.Name != addon.Name { + return fmt.Errorf( + "chart %s is already used by helmclusteraddon/%s", + addon.Spec.Chart.HelmClusterAddonChartName, existing.Name, + ) + } + } + + return nil +} diff --git a/images/operator-helm-artifact/werf.inc.yaml b/images/operator-helm-artifact/werf.inc.yaml new file mode 100644 index 0000000..4784ac4 --- /dev/null +++ b/images/operator-helm-artifact/werf.inc.yaml @@ -0,0 +1,48 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact +final: false +fromImage: builder/src +git: +- add: {{ .ModuleDir }}/api + to: /src/api + stageDependencies: + install: + - go.mod + - go.sum + setup: + - "**/*.go" +- add: {{ .ModuleDir }}/images/{{ .ImageName }} + to: /src/images/operator-helm-artifact + stageDependencies: + install: + - go.mod + - go.sum + setup: + - "**/*.go" +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +final: false +fromImage: builder/golang-bookworm-1.25 +import: +- image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact + add: /src + to: /src + before: install +shell: + install: + - cd /src/images/operator-helm-artifact + - go mod download + setup: + - cd /src/images/operator-helm-artifact + - mkdir /out + - export GOOS=linux + - export GOARCH=amd64 + - export CGO_ENABLED=0 + + - | + echo "Build operator-helm-controller binary" + {{- $_ := set $ "ProjectName" (list $.ImageName "operator-helm-controller" | join "/") }} + + {{- $buildCommand := printf "go build -ldflags=\"-s -w\" -tags %s -v -a -o /out/operator-helm-controller ./cmd/operator-helm-controller" .MODULE_EDITION -}} + {{- include "image-build.build" (set $ "BuildCommand" $buildCommand) | nindent 4 }} + diff --git a/images/operator-helm-controller/mount-points.yaml b/images/operator-helm-controller/mount-points.yaml new file mode 100644 index 0000000..eefff43 --- /dev/null +++ b/images/operator-helm-controller/mount-points.yaml @@ -0,0 +1 @@ +dirs: [] diff --git a/images/operator-helm-controller/werf.inc.yaml b/images/operator-helm-controller/werf.inc.yaml new file mode 100644 index 0000000..76d8d4f --- /dev/null +++ b/images/operator-helm-controller/werf.inc.yaml @@ -0,0 +1,14 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +fromImage: base/distroless +git: + {{- include "image mount points" . }} +import: +- image: {{ .ModuleNamePrefix }}operator-helm-artifact + add: /out/operator-helm-controller + to: /app/operator-helm-controller + after: install +imageSpec: + config: + workingDir: "/app" + entrypoint: ["/app/operator-helm-controller"] diff --git a/module.yaml b/module.yaml new file mode 100644 index 0000000..cca5d37 --- /dev/null +++ b/module.yaml @@ -0,0 +1,20 @@ +name: operator-helm +stage: Experimental +requirements: + deckhouse: ">= 1.74" +subsystems: + - delivery +namespace: d8-operator-helm +descriptions: + en: An operator to deploy Helm applications declaratively. + ru: Оператор для декларативного развертывания Helm-приложений. +tags: ["delivery"] +disable: + confirmation: true + message: "Disabling of this module can cause disruptions in deployed applications operation." +accessibility: + editions: + _default: + available: true + enabledInBundles: + - Minimal diff --git a/openapi/config-values.yaml b/openapi/config-values.yaml new file mode 100644 index 0000000..55c0b06 --- /dev/null +++ b/openapi/config-values.yaml @@ -0,0 +1,10 @@ +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). diff --git a/openapi/doc-ru-config-values.yaml b/openapi/doc-ru-config-values.yaml new file mode 100644 index 0000000..747bafb --- /dev/null +++ b/openapi/doc-ru-config-values.yaml @@ -0,0 +1,8 @@ +type: object +properties: + highAvailability: + description: | + Ручное управление режимом отказоустойчивости. + + По умолчанию режим отказоустойчивости определяется автоматически. + Подробнее про режим отказоустойчивости можно прочитать в разделе [Высокая надежность и доступность](/products/kubernetes-platform/documentation/v1/admin/configuration/high-reliability-and-availability/enable.html#включение-режима-ha-для-отдельных-компонентов). diff --git a/openapi/values.yaml b/openapi/values.yaml new file mode 100644 index 0000000..47187f8 --- /dev/null +++ b/openapi/values.yaml @@ -0,0 +1,38 @@ +x-extend: + schema: config-values.yaml +type: object +properties: + internal: + type: object + default: {} + properties: + controller: + type: object + default: {} + properties: + cert: + type: object + default: {} + properties: + ca: + type: string + default: "" + crt: + type: string + default: "" + key: + type: string + default: "" + rootCA: + type: object + default: {} + properties: + ca: + type: string + default: "" + crt: + type: string + default: "" + key: + type: string + default: "" \ No newline at end of file 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..de02090 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,6 @@ +dependencies: +- name: deckhouse_lib_helm + repository: https://deckhouse.github.io/lib-helm + version: 1.71.2 +digest: sha256:c32b540da9793f919714fc648420a7f1c4ab94496f5e4b032e5f34024f17576d +generated: "2026-03-18T16:53:20.460359+03:00" diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl new file mode 100644 index 0000000..38cd5d2 --- /dev/null +++ b/templates/_helpers.tpl @@ -0,0 +1,19 @@ +{{- /* Return logLevel as a string. */}} +{{- define "moduleLogLevel" -}} +{{- dig "logLevel" "" .Values.operatorHelm -}} +{{- 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/admision-policy.yaml b/templates/admision-policy.yaml new file mode 100644 index 0000000..9339379 --- /dev/null +++ b/templates/admision-policy.yaml @@ -0,0 +1,62 @@ +{{- $kubeVersion := .Values.global.discovery.kubernetesVersion }} +{{- $apiVersion := "" }} +{{- if semverCompare ">=1.30.0" $kubeVersion }} +{{- $apiVersion = "admissionregistration.k8s.io/v1" }} +{{- else if semverCompare ">=1.28.0" $kubeVersion }} +{{- $apiVersion = "admissionregistration.k8s.io/v1beta1" }} +{{- else if semverCompare ">=1.26.0" $kubeVersion }} +{{- $apiVersion = "admissionregistration.k8s.io/v1alpha1" }} +{{- end }} + +{{- if $apiVersion }} +apiVersion: {{ $apiVersion }} +kind: ValidatingAdmissionPolicy +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: operator-helm-restricted-access-policy +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - "helm.internal.operator-helm.deckhouse.io" + - "source.internal.operator-helm.deckhouse.io" + apiVersions: ["*"] + operations: + - "CREATE" + - "UPDATE" + - "DELETE" + resources: ["*"] + - apiGroups: + - "helm.deckhouse.io" + apiVersions: ["*"] + operations: + - "CREATE" + - "UPDATE" + - "DELETE" + resources: + - "helmclusteraddoncharts" + validations: + - expression: | + request.userInfo.username.startsWith("system:serviceaccount:kube-system:") || + request.userInfo.username.startsWith("system:serviceaccount:d8-system:") || + request.userInfo.username in [ + "system:serviceaccount:d8-operator-helm:operator-helm-controller", + "system:serviceaccount:d8-operator-helm:nelm-source-controller", + "system:serviceaccount:d8-operator-helm:helm-controller", + ] + message: "Operation forbidden for this user." +--- +apiVersion: {{ $apiVersion }} +kind: ValidatingAdmissionPolicyBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: operator-helm-restricted-access-policy-binding +spec: + policyName: operator-helm-restricted-access-policy + validationActions: + - "Deny" + matchResources: + namespaceSelector: {} + objectSelector: {} +{{- end }} \ No newline at end of file diff --git a/templates/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..5add5f9 --- /dev/null +++ b/templates/helm-controller/deployment.yaml @@ -0,0 +1,136 @@ +{{- $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-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..d53b00b --- /dev/null +++ b/templates/nelm-source-controller/deployment.yaml @@ -0,0 +1,142 @@ +{{- $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-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..c8e9cd9 --- /dev/null +++ b/templates/nelm-source-controller/rbac-for-us.yaml @@ -0,0 +1,154 @@ +--- +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: + - 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/operator-helm-controller/deployment.yaml b/templates/operator-helm-controller/deployment.yaml new file mode 100644 index 0000000..521a640 --- /dev/null +++ b/templates/operator-helm-controller/deployment.yaml @@ -0,0 +1,140 @@ +{{- $priorityClassName := include "priorityClassName" . }} + +{{- define "operator_helm_controller_resources" }} +cpu: 50m +memory: 64Mi +{{- end }} + +{{- if (.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller" "workload-resource-policy.deckhouse.io" "master")) | nindent 2 }} +spec: + targetRef: + apiVersion: "apps/v1" + kind: Deployment + name: operator-helm-controller + updatePolicy: + updateMode: {{ include "vpa.policyUpdateMode" . }} + resourcePolicy: + containerPolicies: + {{- include "kube_api_rewriter.vpa_container_policy" . | nindent 4 }} + {{- include "kube_rbac_proxy.vpa_container_policy" . | nindent 4 }} + - containerName: operator-helm-controller + minAllowed: + {{- include "operator_helm_controller_resources" . | nindent 8 }} + maxAllowed: + cpu: 1000m + memory: 1Gi +{{- end }} + +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller" )) | nindent 2 }} +spec: + minAvailable: {{ include "helm_lib_is_ha_to_value" (list . 1 0) }} + selector: + matchLabels: + app: operator-helm-controller + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +spec: + replicas: {{ include "helm_lib_is_ha_to_value" (list . 3 1) }} + {{- if (include "helm_lib_ha_enabled" .) }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + {{- end }} + revisionHistoryLimit: 2 + selector: + matchLabels: + app: operator-helm-controller + template: + metadata: + labels: + app: operator-helm-controller + annotations: + kubectl.kubernetes.io/default-container: operator-helm-controller + spec: + containers: + {{- include "kube_api_rewriter.sidecar_container" . | nindent 8 }} + - name: operator-helm-controller + {{- include "helm_lib_module_container_security_context_read_only_root_filesystem_capabilities_drop_all_pss_restricted" . | nindent 10 }} + image: {{ include "helm_lib_module_image" (list . "operatorHelmController") }} + imagePullPolicy: IfNotPresent + args: + - --leader-elect + - --metrics-bind-address=:8080 + - --health-probe-bind-address=:9440 + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: admission-webhook-secret + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 12 }} + ports: + - containerPort: 9443 + name: controller + protocol: TCP + - containerPort: 8080 + name: metrics + protocol: TCP + - containerPort: 9440 + name: healthz + protocol: TCP + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 14 }} + {{- if not ( .Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} + {{- include "operator_helm_controller_resources" . | nindent 14 }} + {{- end }} + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 12 }} + livenessProbe: + httpGet: + path: /healthz + port: healthz + scheme: HTTP + initialDelaySeconds: 10 + readinessProbe: + httpGet: + path: /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" "operator-helm-controller") + (dict "upstream" "http://127.0.0.1:9090/metrics" "path" "/proxy/metrics" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/healthz" "path" "/proxy/healthz" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/readyz" "path" "/proxy/readyz" "name" "kube-api-rewriter") + ) }} + {{- include "kube_rbac_proxy.sidecar_container" (tuple . $kubeRbacProxySettings) | nindent 8 }} + dnsPolicy: ClusterFirst + serviceAccountName: operator-helm-controller + {{- include "helm_lib_module_pod_security_context_run_as_user_deckhouse" . | nindent 6 }} + {{- include "helm_lib_priority_class" (tuple . $priorityClassName) | nindent 6 }} + {{- include "helm_lib_node_selector" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_tolerations" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_pod_anti_affinity_for_ha" (list . (dict "app" "operator-helm-controller")) | nindent 6 }} + volumes: + - name: admission-webhook-secret + secret: + secretName: operator-helm-controller-tls + {{- include "kube_api_rewriter.kubeconfig_volume" . | nindent 8 }} diff --git a/templates/operator-helm-controller/rbac-for-us.yaml b/templates/operator-helm-controller/rbac-for-us.yaml new file mode 100644 index 0000000..d693fad --- /dev/null +++ b/templates/operator-helm-controller/rbac-for-us.yaml @@ -0,0 +1,202 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +imagePullSecrets: +- name: operator-helm-module-registry +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:operator-helm-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: d8:{{ .Chart.Name }}:operator-helm-controller +subjects: +- kind: ServiceAccount + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:operator-helm-controller +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - helm.deckhouse.io + resources: + - helmclusteraddons + - helmclusteraddons/status + - helmclusteraddoncharts + - helmclusteraddoncharts/status + - helmclusteraddonrepositories + - helmclusteraddonrepositories/status + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases/status + verbs: + - get + - patch + - update +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets + - internalnelmoperatorgitrepositories + - internalnelmoperatorhelmcharts + - internalnelmoperatorhelmrepositories + - internalnelmoperatorocirepositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets/finalizers + - internalnelmoperatorgitrepositories/finalizers + - internalnelmoperatorhelmcharts/finalizers + - internalnelmoperatorhelmrepositories/finalizers + - internalnelmoperatorocirepositories/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets/status + - internalnelmoperatorgitrepositories/status + - internalnelmoperatorhelmcharts/status + - internalnelmoperatorhelmrepositories/status + - internalnelmoperatorocirepositories/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} +rules: +- apiGroups: + - "" + resources: + - configmaps + - secrets + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create +- apiGroups: + - "coordination.k8s.io" + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: operator-helm-controller +subjects: +- kind: ServiceAccount + name: operator-helm-controller diff --git a/templates/operator-helm-controller/secret-tls.yaml b/templates/operator-helm-controller/secret-tls.yaml new file mode 100644 index 0000000..b2f69a1 --- /dev/null +++ b/templates/operator-helm-controller/secret-tls.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: operator-helm-controller-tls + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +type: kubernetes.io/tls +data: + ca.crt: {{ .Values.operatorHelm.internal.controller.cert.ca | b64enc }} + tls.crt: {{ .Values.operatorHelm.internal.controller.cert.crt | b64enc }} + tls.key: {{ .Values.operatorHelm.internal.controller.cert.key | b64enc }} diff --git a/templates/operator-helm-controller/service-metrics.yaml b/templates/operator-helm-controller/service-metrics.yaml new file mode 100644 index 0000000..89b9345 --- /dev/null +++ b/templates/operator-helm-controller/service-metrics.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: operator-helm-controller-metrics + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +spec: + ports: + - name: metrics + port: 8080 + protocol: TCP + targetPort: https-metrics + selector: + app: operator-helm-controller + diff --git a/templates/operator-helm-controller/service-monitor.yaml b/templates/operator-helm-controller/service-monitor.yaml new file mode 100644 index 0000000..a17f1db --- /dev/null +++ b/templates/operator-helm-controller/service-monitor.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: operator-helm-controller + namespace: d8-monitoring + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller" "prometheus" "main")) | nindent 2 }} +spec: + endpoints: + - bearerTokenSecret: + key: token + name: prometheus-token + path: /metrics + port: metrics + scheme: https + tlsConfig: + insecureSkipVerify: true + namespaceSelector: + matchNames: + - d8-{{ .Chart.Name }} + selector: + matchLabels: + app: "operator-helm-controller" + diff --git a/templates/operator-helm-controller/service.yaml b/templates/operator-helm-controller/service.yaml new file mode 100644 index 0000000..b1356b6 --- /dev/null +++ b/templates/operator-helm-controller/service.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +spec: + ports: + - name: admission-webhook + port: 443 + targetPort: controller + protocol: TCP + - name: controller + port: 9443 + targetPort: controller + protocol: TCP + selector: + app: operator-helm-controller diff --git a/templates/operator-helm-controller/validation-webhook.yaml b/templates/operator-helm-controller/validation-webhook.yaml new file mode 100644 index 0000000..b1e3ed9 --- /dev/null +++ b/templates/operator-helm-controller/validation-webhook.yaml @@ -0,0 +1,23 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} + name: "operator-helm-controller-admission-webhook" +webhooks: + - name: "helmclusteraddons.operator-helm-controller.validate.d8-operator-helm" + rules: + - apiGroups: ["helm.deckhouse.io"] + apiVersions: ["v1alpha1"] + operations: ["CREATE", "UPDATE"] + resources: ["helmclusteraddons"] + scope: "Cluster" + clientConfig: + service: + namespace: d8-{{ .Chart.Name }} + name: operator-helm-controller + path: /validate-helm-deckhouse-io-v1alpha1-helmclusteraddon + port: 443 + caBundle: | + {{ .Values.operatorHelm.internal.controller.cert.ca | b64enc }} + admissionReviewVersions: ["v1"] + sideEffects: None diff --git a/templates/rbac-to-us.yaml b/templates/rbac-to-us.yaml new file mode 100644 index 0000000..ed3697f --- /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: ["operator-helm-controller", "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/tests/e2e/.golangci.yaml b/tests/e2e/.golangci.yaml new file mode 100644 index 0000000..4260e0d --- /dev/null +++ b/tests/e2e/.golangci.yaml @@ -0,0 +1,109 @@ +# https://golangci-lint.run/usage/configuration/ +version: "2" + +run: + concurrency: 4 + timeout: 10m + +issues: + # Show all errors. + max-issues-per-linter: 0 + max-same-issues: 0 + exclude: + - "don't use an underscore in package name" + +output: + sort-results: true + +exclusions: + paths: + - "^zz_generated.*" + +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + settings: + gci: + sections: + - standard + - default + - prefix(github.com/deckhouse/) + gofumpt: + extra-rules: true + goimports: + local-prefixes: github.com/deckhouse/ + +linters: + default: none + enable: + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - contextcheck # [maybe too many false positives] checks the function whether use a non-inherited context + - dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - copyloopvar # detects places where loop variables are copied (Go 1.22+) + - gocritic # provides diagnostics that check for bugs, performance and style issues + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - misspell # finds commonly misspelled English words in comments + - nolintlint # reports ill-formed or insufficient nolint directives + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - testifylint # checks usage of github.com/stretchr/testify + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - unused # checks for unused constants, variables, functions and types + - usetesting # reports uses of functions with replacement inside the testing package + - testableexamples # checks if examples are testable (have an expected output) + - thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - whitespace # detects leading and trailing whitespace + - wastedassign # finds wasted assignment statements + - importas # checks import aliases against the configured convention + settings: + errcheck: + exclude-functions: + - "(*os.File).Close" + - "(*net.TCPConn).Close" + - "(io.ReadCloser).Close" + - "(net.Listener).Close" + - "(net.Conn).Close" + - "(net.Conn).Close" + - "(*golang.org/x/crypto/ssh.Session).Close" + - "(*github.com/fsnotify/fsnotify.Watcher).Close" + staticcheck: + dot-import-whitelist: + - github.com/onsi/ginkgo/v2 + - github.com/onsi/gomega + revive: + rules: + - name: dot-imports + disabled: true + - name: exported + disabled: true + - name: package-comments + disabled: true + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [funlen, gocognit, lll] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + importas: + # Do not allow unaliased imports of aliased packages. + # Default: false + no-unaliased: true + # Do not allow non-required aliases. + # Default: false + no-extra-aliases: false \ No newline at end of file diff --git a/tests/e2e/Taskfile.dist.yaml b/tests/e2e/Taskfile.dist.yaml new file mode 100644 index 0000000..3d1c93a --- /dev/null +++ b/tests/e2e/Taskfile.dist.yaml @@ -0,0 +1,38 @@ +version: '3' + +vars: + TIMEOUT: '{{.TIMEOUT | default "30m"}}' + KIND_CLUSTER_NAME: '{{.KIND_CLUSTER_NAME | default "d8-operator-helm"}}' + +tasks: + deps:install:ginkgo: + desc: 'Install ginkgo binary. Important vars: "paths".' + cmds: + - | + cd {{.paths | default "./"}} + version="$(go list -m -f '{{ printf `{{ .Version }}` }}' github.com/onsi/ginkgo/v2)" + go install {{.CLI_ARGS}} github.com/onsi/ginkgo/v2/ginkgo@${version} + + kind:ci:setup: + desc: Setup kind in CI + cmds: + - ./scripts/kind-d8-ci.sh --channel $KIND_DECKHOUSE_CHANNEL + env: + KIND_DECKHOUSE_CHANNEL: '{{.KIND_DECKHOUSE_CHANNEL | default "Stable"}}' + MODULE_TAG_NAME: '{{.MODULE_TAG_NAME | default "main"}}' + DEV_REGISTRY_URL: '{{.DEV_REGISTRY_URL | default "dev-registry.deckhouse.io/sys/deckhouse-oss/modules"}}' + DEV_REGISTRY_DOCKER_CONFIG: '{{.DEV_REGISTRY_DOCKER_CONFIG}}' + + kind:ci:cleanup: + desc: Delete kind cluster in CI + cmds: + - './kind/bin/kind delete cluster --name {{.KIND_CLUSTER_NAME}} || exit 0' + + tests: + desc: Run e2e tests + cmds: + - | + args="-v --race --timeout={{.TIMEOUT}}" + go tool ginkgo $args ./... + env: + E2E_CLUSTERTRANSPORT_KUBECONFIG: './kind/{{.KIND_CLUSTER_NAME}}/kubeconfig-external' diff --git a/tests/e2e/default_config.yaml b/tests/e2e/default_config.yaml new file mode 100644 index 0000000..bd6e2ba --- /dev/null +++ b/tests/e2e/default_config.yaml @@ -0,0 +1,14 @@ +clusterTransport: + kubeConfig: "" + +controllers: + - name: "operator-helm-controller" + namespace: "d8-operator-helm" + labelSelector: "app=operator-helm-controller" + containers: + - "operator-helm-controller" + logFilters: + exclude: + # On cascade deletion after tests, HelmClusterAddon reconcile can be triggered after repository removal. + - "Failed to get internal repository" + excludeRegexp: [] diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go new file mode 100644 index 0000000..d243f4e --- /dev/null +++ b/tests/e2e/e2e_test.go @@ -0,0 +1,44 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + _ "github.com/deckhouse/operator-helm/tests/e2e/helmclusteraddon" + _ "github.com/deckhouse/operator-helm/tests/e2e/helmclusteraddonrepository" + "github.com/deckhouse/operator-helm/tests/e2e/internal/controller" +) + +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "E2E Suite") +} + +var _ = SynchronizedBeforeSuite(func() { + controller.StartAll() + controller.SaveRestartCounts() +}, func() {}) + +var _ = SynchronizedAfterSuite(func() {}, func() { + controller.StopAll() + controller.AssertNoErrors() + controller.AssertNoRestarts() +}) diff --git a/tests/e2e/go.mod b/tests/e2e/go.mod new file mode 100644 index 0000000..fcc81cc --- /dev/null +++ b/tests/e2e/go.mod @@ -0,0 +1,81 @@ +module github.com/deckhouse/operator-helm/tests/e2e + +go 1.25.0 + +tool github.com/onsi/ginkgo/v2/ginkgo + +require ( + github.com/deckhouse/operator-helm/api v0.0.0 + github.com/onsi/ginkgo/v2 v2.27.2 + github.com/onsi/gomega v1.38.3 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.35.1 + k8s.io/apiextensions-apiserver v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/cli-runtime v0.35.1 + k8s.io/client-go v0.35.1 + sigs.k8s.io/controller-runtime v0.23.1 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-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/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/sergi/go-diff v1.3.1 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.40.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace github.com/deckhouse/operator-helm/api => ../../api diff --git a/tests/e2e/go.sum b/tests/e2e/go.sum new file mode 100644 index 0000000..b94bf4b --- /dev/null +++ b/tests/e2e/go.sum @@ -0,0 +1,214 @@ +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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/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/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/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/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/cli-runtime v0.35.1 h1:uKcXFe8J7AMAM4Gm2JDK4mp198dBEq2nyeYtO+JfGJE= +k8s.io/cli-runtime v0.35.1/go.mod h1:55/hiXIq1C8qIJ3WBrWxEwDLdHQYhBNRdZOz9f7yvTw= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= +sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/tests/e2e/helmclusteraddon/lifecycle.go b/tests/e2e/helmclusteraddon/lifecycle.go new file mode 100644 index 0000000..7c04605 --- /dev/null +++ b/tests/e2e/helmclusteraddon/lifecycle.go @@ -0,0 +1,353 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddon + +import ( + "context" + "fmt" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/tests/e2e/internal/controller" + "github.com/deckhouse/operator-helm/tests/e2e/internal/framework" + "github.com/deckhouse/operator-helm/tests/e2e/internal/util" +) + +func DefineLifecycleTests(repoType, repoURL string) { + Describe(fmt.Sprintf("Using %s repository", repoType), Ordered, func() { + f := framework.NewFramework("addon-lifecycle") + cfg := framework.GetConfig() + + repoName := "e2e-test-repo-" + strings.ToLower(repoType) + addonName := "e2e-test-addon-" + strings.ToLower(repoType) + chartName := "podinfo" + + labelSelector := fmt.Sprintf("app.kubernetes.io/name=%s-%s", addonName, chartName) + + BeforeAll(func() { + DeferCleanup(f.After) + f.Before() + + By("Verifying all controllers are running") + for _, ctrl := range cfg.Controllers { + util.UntilControllerReady(ctrl.Namespace, ctrl.LabelSelector, framework.LongTimeout) + } + }) + + AfterEach(func() { + By("Verifying no errors in operator-helm-controller logs") + controller.AssertNoErrorsFor("operator-helm-controller") + }) + + It("should create HelmClusterAddonRepository and reach Ready", func() { + repo := &apiv1alpha1.HelmClusterAddonRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repoName, + }, + Spec: apiv1alpha1.HelmClusterAddonRepositorySpec{ + URL: repoURL, + TLSVerify: true, + }, + } + + created, err := f.OperatorClient().HelmV1alpha1(). + HelmClusterAddonRepositories(). + Create(context.Background(), repo, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + f.DeferDeleteFunc(func() error { + return f.OperatorClient().HelmV1alpha1(). + HelmClusterAddonRepositories(). + Delete(context.Background(), created.Name, metav1.DeleteOptions{}) + }) + + By("Waiting for repository to become Ready") + util.UntilConditionTrue( + apiv1alpha1.ConditionTypeReady, + framework.LongTimeout, + created, + ) + + By("Waiting for HelmClusterAddonRepository to become Synced") + util.UntilConditionTrue( + apiv1alpha1.ConditionTypeSynced, + framework.LongTimeout, + created, + ) + }) + + It("should verify target namespace does not have addon pods yet", func() { + By(fmt.Sprintf("Checking namespace %q has no addon pods", f.NamespaceName())) + pods, err := f.KubeClient().CoreV1(). + Pods(f.NamespaceName()). + List(context.Background(), metav1.ListOptions{ + LabelSelector: fmt.Sprintf("app.kubernetes.io/name=%s-%s", addonName, chartName), + }) + Expect(err).NotTo(HaveOccurred()) + Expect(pods.Items).To(BeEmpty()) + }) + + It("should create HelmClusterAddon and wait for installation", func() { + addon := &apiv1alpha1.HelmClusterAddon{ + ObjectMeta: metav1.ObjectMeta{ + Name: addonName, + }, + Spec: apiv1alpha1.HelmClusterAddonSpec{ + Chart: apiv1alpha1.HelmClusterAddonChartRef{ + HelmClusterAddonChartName: chartName, + HelmClusterAddonRepository: repoName, + Version: "6.10.2", + }, + Namespace: f.NamespaceName(), + }, + } + + created, err := f.OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Create(context.Background(), addon, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + f.DeferDeleteFunc(func() error { + return f.OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Delete(context.Background(), created.Name, metav1.DeleteOptions{}) + }) + + By("Waiting for addon to be installed") + util.UntilConditionTrue( + apiv1alpha1.ConditionTypeReady, + framework.LongTimeout, + created, + ) + + By("Verifying Installed condition reason is Success") + util.UntilConditionReason( + apiv1alpha1.ConditionTypeInstalled, + "InstallSucceeded", + framework.ShortTimeout, + created, + ) + + By("Checking all pods are ready") + util.UntilAllPodsReady(f.NamespaceName(), labelSelector, 1, framework.LongTimeout) + }) + + It("should update chart version and apply changes", func() { + By("Updating addon chart version") + updated := util.UpdateHelmClusterAddon(addonName, func(addon *apiv1alpha1.HelmClusterAddon) { + addon.Spec.Chart.Version = "6.10.0" + }) + + By("Waiting for update to be applied") + util.UntilConditionTrue( + apiv1alpha1.ConditionTypeUpdateInstalled, + framework.LongTimeout, + updated, + ) + + By("Verifying the version was applied") + updated, err := f.OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Get(context.Background(), addonName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(updated.Status.LastAppliedChart).NotTo(BeNil()) + Expect(updated.Status.LastAppliedChart.Version).To(Equal("6.10.0")) + + By("Verifying pods are still running after update") + util.UntilPodCount(f.NamespaceName(), labelSelector, 1, framework.LongTimeout) + }) + + It("should update last applied values", func() { + expectedValues := `{"replicaCount": 2}` + + By("Updating last applied values") + updated := util.UpdateHelmClusterAddon(addonName, func(addon *apiv1alpha1.HelmClusterAddon) { + addon.Spec.Values = &apiextensionsv1.JSON{Raw: []byte(expectedValues)} + }) + + By("Waiting for update to be applied") + util.UntilConditionTrue( + apiv1alpha1.ConditionTypeConfigurationApplied, + framework.LongTimeout, + updated, + ) + + By("Verifying that values are applied") + updated, err := f.OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Get(context.Background(), addonName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(updated.Status.LastAppliedValues).NotTo(BeNil()) + Expect(updated.Status.LastAppliedValues.Raw).To(MatchJSON(expectedValues)) + + By("Verifying pods number changed after values update") + util.UntilPodCount(f.NamespaceName(), labelSelector, 2, framework.LongTimeout) + }) + + It("should not update chart version on invalid chart version", func() { + addon, err := f.OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Get(context.Background(), addonName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Should have PartiallyDegraded condition inactive") + util.UntilConditionStatus( + apiv1alpha1.ConditionTypePartiallyDegraded, + string(metav1.ConditionFalse), + framework.LongTimeout, + addon, + ) + + invalidChartVersion := "invalid-version" + + By("Updating addon chart to invalid version") + updated := util.UpdateHelmClusterAddon(addonName, func(addon *apiv1alpha1.HelmClusterAddon) { + addon.Spec.Chart.Version = invalidChartVersion + }) + + By("Should have PartiallyDegraded condition active") + util.UntilConditionTrue( + apiv1alpha1.ConditionTypePartiallyDegraded, + framework.LongTimeout, + updated, + ) + + By("Should not update chart info") + updated, err = f.OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Get(context.Background(), addonName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(updated.Status.LastAppliedValues).NotTo(BeNil()) + Expect(updated.Status.LastAppliedChart.Version).NotTo(Equal(invalidChartVersion)) + + By("Verifying pods number changed after invalid chart info set") + util.UntilPodCount(f.NamespaceName(), labelSelector, 2, framework.LongTimeout) + }) + + It("Should redeem on reverting chart version", func() { + addon, err := f.OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Get(context.Background(), addonName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Should have PartiallyDegraded condition active") + util.UntilConditionTrue( + apiv1alpha1.ConditionTypePartiallyDegraded, + framework.LongTimeout, + addon, + ) + + validChartVersion := "6.10.2" + + By("Updating addon chart to invalid version") + updated := util.UpdateHelmClusterAddon(addonName, func(addon *apiv1alpha1.HelmClusterAddon) { + addon.Spec.Chart.Version = validChartVersion + }) + + By("Waiting for addon to be upgraded") + util.UntilConditionTrue( + apiv1alpha1.ConditionTypeReady, + framework.LongTimeout, + updated, + ) + + By("Should have PartiallyDegraded condition inactive") + util.UntilConditionStatus( + apiv1alpha1.ConditionTypePartiallyDegraded, + string(metav1.ConditionFalse), + framework.LongTimeout, + updated, + ) + + By("Should update chart info") + updated, err = f.OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Get(context.Background(), addonName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(updated.Status.LastAppliedValues).NotTo(BeNil()) + Expect(updated.Status.LastAppliedChart.Version).To(Equal(validChartVersion)) + + By("Verifying pods number changed after invalid chart info set") + util.UntilPodCount(f.NamespaceName(), labelSelector, 2, framework.LongTimeout) + }) + + It("Should fail on invalid values set", func() { + invalidValues := `{"replicaCount": "no"}` + + By("Updating addon chart version") + updated := util.UpdateHelmClusterAddon(addonName, func(addon *apiv1alpha1.HelmClusterAddon) { + addon.Spec.Values = &apiextensionsv1.JSON{Raw: []byte(invalidValues)} + }) + + By("Waiting for update to be applied") + util.UntilConditionStatus( + apiv1alpha1.ConditionTypeReady, + string(metav1.ConditionFalse), + framework.LongTimeout, + updated, + ) + + By("Verifying invalid values were not applied") + updated, err := f.OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Get(context.Background(), addonName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(updated.Status.LastAppliedValues).NotTo(BeNil()) + Expect(updated.Status.LastAppliedValues.Raw).NotTo(MatchJSON(invalidValues)) + + By("Verifying pods are still running after update") + util.UntilPodCount(f.NamespaceName(), labelSelector, 2, framework.LongTimeout) + }) + + It("Should redeem on reverting values", func() { + validValues := `{"replicaCount": 3}` + + By("Updating addon chart version") + updated := util.UpdateHelmClusterAddon(addonName, func(addon *apiv1alpha1.HelmClusterAddon) { + addon.Spec.Values = &apiextensionsv1.JSON{Raw: []byte(validValues)} + }) + + By("Waiting for update to be applied") + util.UntilConditionTrue( + apiv1alpha1.ConditionTypeConfigurationApplied, + framework.LongTimeout, + updated, + ) + + By("Verifying valid values were applied") + updated, err := f.OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Get(context.Background(), addonName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(updated.Status.LastAppliedValues).NotTo(BeNil()) + Expect(updated.Status.LastAppliedValues.Raw).To(MatchJSON(validValues)) + + By("Verifying pods are running after update") + util.UntilPodCount(f.NamespaceName(), labelSelector, 3, framework.LongTimeout) + }) + }) +} + +var _ = Describe("HelmClusterAddon lifecycle", Ordered, func() { + DefineLifecycleTests("Helm", "https://stefanprodan.github.io/podinfo") + DefineLifecycleTests("OCI", "oci://ghcr.io/stefanprodan/charts/podinfo") +}) diff --git a/tests/e2e/helmclusteraddonrepository/lifecycle.go b/tests/e2e/helmclusteraddonrepository/lifecycle.go new file mode 100644 index 0000000..11d3411 --- /dev/null +++ b/tests/e2e/helmclusteraddonrepository/lifecycle.go @@ -0,0 +1,151 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddonrepository + +import ( + "context" + "fmt" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/tests/e2e/internal/controller" + "github.com/deckhouse/operator-helm/tests/e2e/internal/framework" + "github.com/deckhouse/operator-helm/tests/e2e/internal/util" +) + +func DefineLifecycleTests(repoType, repoURL string) { + Describe(fmt.Sprintf("Testing %s repository", repoType), Ordered, func() { + f := framework.NewFramework("repository-lifecycle") + cfg := framework.GetConfig() + + repoName := "e2e-test-repo-" + strings.ToLower(repoType) + + BeforeAll(func() { + DeferCleanup(f.After) + f.Before() + + By("Verifying all controllers are running") + for _, ctrl := range cfg.Controllers { + util.UntilControllerReady(ctrl.Namespace, ctrl.LabelSelector, framework.LongTimeout) + } + }) + + AfterEach(func() { + By("Verifying no errors in operator-helm-controller logs") + controller.AssertNoErrorsFor("operator-helm-controller") + }) + + It("should create HelmClusterAddonRepository", func() { + repo := &apiv1alpha1.HelmClusterAddonRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repoName, + }, + Spec: apiv1alpha1.HelmClusterAddonRepositorySpec{ + URL: repoURL, + TLSVerify: true, + }, + } + + created, err := f.OperatorClient().HelmV1alpha1(). + HelmClusterAddonRepositories(). + Create(context.Background(), repo, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + f.DeferDeleteFunc(func() error { + return f.OperatorClient().HelmV1alpha1(). + HelmClusterAddonRepositories(). + Delete(context.Background(), created.Name, metav1.DeleteOptions{}) + }) + + By("Waiting for repository to become Ready") + util.UntilConditionTrue( + apiv1alpha1.ConditionTypeReady, + framework.LongTimeout, + created, + ) + + By("Waiting for HelmClusterAddonRepository to become Synced") + util.UntilConditionTrue( + apiv1alpha1.ConditionTypeSynced, + framework.LongTimeout, + created, + ) + + By("Should have existing HelmClusterAddonChart") + labelSelector := fmt.Sprintf("repository=%s", repoName) + charts, err := f.OperatorClient(). + HelmV1alpha1(). + HelmClusterAddonCharts(). + List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector}) + Expect(err).NotTo(HaveOccurred()) + Expect(len(charts.Items)).To(BeNumerically(">=", 1), + "waiting for >= %d charts, got %d", 1, len(charts.Items)) + + By("HelmClusterAddonChart should have versions") + for _, chart := range charts.Items { + Expect(chart.Status.Versions).NotTo(BeEmpty()) + } + }) + }) +} + +var _ = Describe("HelmClusterAddonRepository lifecycle", Ordered, func() { + DefineLifecycleTests("Helm", "https://stefanprodan.github.io/podinfo") + DefineLifecycleTests("OCI", "oci://ghcr.io/stefanprodan/charts/podinfo") +}) + +var _ = Describe("Create HelmClusterAddonRepository with invalid url", Ordered, func() { + f := framework.NewFramework("repository-lifecycle") + cfg := framework.GetConfig() + + BeforeAll(func() { + DeferCleanup(f.After) + f.Before() + + By("Verifying all controllers are running") + for _, ctrl := range cfg.Controllers { + util.UntilControllerReady(ctrl.Namespace, ctrl.LabelSelector, framework.LongTimeout) + } + }) + + AfterEach(func() { + By("Verifying no errors in operator-helm-controller logs") + controller.AssertNoErrorsFor("operator-helm-controller") + }) + + It("should create HelmClusterAddonRepository with invalid url", func() { + By("Creating HelmClusterAddonRepository with invalid url") + repo := &apiv1alpha1.HelmClusterAddonRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "repo-with-invalid-url", + }, + Spec: apiv1alpha1.HelmClusterAddonRepositorySpec{ + URL: "invalid-url", + }, + } + + _, err := f.OperatorClient().HelmV1alpha1(). + HelmClusterAddonRepositories(). + Create(context.Background(), repo, metav1.CreateOptions{}) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring("is invalid: spec.url"))) + }) +}) diff --git a/tests/e2e/internal/controller/logwatch.go b/tests/e2e/internal/controller/logwatch.go new file mode 100644 index 0000000..a6855d9 --- /dev/null +++ b/tests/e2e/internal/controller/logwatch.go @@ -0,0 +1,248 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "bufio" + "context" + "fmt" + "strings" + "sync" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/operator-helm/tests/e2e/internal/framework" +) + +// LogError stores a single error line found in controller logs. +type LogError struct { + Controller string + Pod string + Container string + Line string + Timestamp time.Time +} + +func (e LogError) String() string { + return fmt.Sprintf("[%s] %s/%s: %s", e.Controller, e.Pod, e.Container, e.Line) +} + +// controllerWatcher monitors logs for a single controller. +type controllerWatcher struct { + config framework.ControllerConfig + errors []LogError + mu sync.Mutex + cancel context.CancelFunc + excludeStrings []string +} + +// LogWatchManager manages watchers for all controllers. +type LogWatchManager struct { + watchers map[string]*controllerWatcher +} + +var manager *LogWatchManager + +// StartAll begins streaming logs for all controllers defined in config. +func StartAll() { + cfg := framework.GetConfig() + manager = &LogWatchManager{ + watchers: make(map[string]*controllerWatcher, len(cfg.Controllers)), + } + + for _, ctrlCfg := range cfg.Controllers { + w := newControllerWatcher(ctrlCfg) + manager.watchers[ctrlCfg.Name] = w + w.start() + } +} + +// StopAll stops all log watchers. +func StopAll() { + if manager == nil { + return + } + for _, w := range manager.watchers { + w.stop() + } +} + +// AssertNoErrors fails the test if any controller logged errors. +func AssertNoErrors() { + GinkgoHelper() + if manager == nil { + return + } + + var allErrors []LogError + for _, w := range manager.watchers { + allErrors = append(allErrors, w.getErrors()...) + } + + if len(allErrors) > 0 { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("Found %d error(s) in controller logs:\n\n", len(allErrors))) + for i, e := range allErrors { + sb.WriteString(fmt.Sprintf(" %d. %s\n", i+1, e.String())) + } + Fail(sb.String()) + } +} + +// GetErrors returns collected errors for a specific controller. +func GetErrors(controllerName string) []LogError { + if manager == nil { + return nil + } + w, ok := manager.watchers[controllerName] + if !ok { + return nil + } + return w.getErrors() +} + +// AssertNoErrorsFor fails the test if the named controller has errors. +func AssertNoErrorsFor(controllerName string) { + GinkgoHelper() + errs := GetErrors(controllerName) + Expect(errs).To(BeEmpty(), + "controller %q has %d error(s) in logs:\n%v", controllerName, len(errs), errs) +} + +func newControllerWatcher(cfg framework.ControllerConfig) *controllerWatcher { + return &controllerWatcher{ + config: cfg, + excludeStrings: cfg.LogFilters.Exclude, + } +} + +func (w *controllerWatcher) start() { + ctx, cancel := context.WithCancel(context.Background()) + w.cancel = cancel + + pods, err := framework.GetClients().KubeClient().CoreV1(). + Pods(w.config.Namespace). + List(ctx, metav1.ListOptions{LabelSelector: w.config.LabelSelector}) + if err != nil { + GinkgoWriter.Printf("WARNING: cannot list pods for controller %q: %v\n", w.config.Name, err) + return + } + + for _, pod := range pods.Items { + containers := w.config.Containers + if len(containers) == 0 { + for _, c := range pod.Spec.Containers { + containers = append(containers, c.Name) + } + } + + for _, container := range containers { + go w.streamLogs(ctx, pod.Name, container) + } + } +} + +func (w *controllerWatcher) stop() { + if w.cancel != nil { + w.cancel() + } +} + +func (w *controllerWatcher) streamLogs(ctx context.Context, podName, containerName string) { + sinceTime := metav1.Now() + stream, err := framework.GetClients().KubeClient().CoreV1(). + Pods(w.config.Namespace). + GetLogs(podName, &corev1.PodLogOptions{ + Container: containerName, + Follow: true, + SinceTime: &sinceTime, + }).Stream(ctx) + if err != nil { + GinkgoWriter.Printf("WARNING: cannot stream logs for %s/%s/%s: %v\n", + w.config.Name, podName, containerName, err) + return + } + defer stream.Close() + + scanner := bufio.NewScanner(stream) + for scanner.Scan() { + line := scanner.Text() + if w.isError(line) && !w.isExcluded(line) { + w.addError(LogError{ + Controller: w.config.Name, + Pod: podName, + Container: containerName, + Line: line, + Timestamp: time.Now(), + }) + } + } +} + +func (w *controllerWatcher) isError(line string) bool { + lower := strings.ToLower(line) + + if strings.Contains(lower, `"level":"error"`) || + strings.Contains(lower, `"level":"fatal"`) || + strings.Contains(lower, `"level":"dpanic"`) { + return true + } + + // klog format: E0324 10:00:00.000000 ... + if len(line) > 1 && line[0] == 'E' && line[1] >= '0' && line[1] <= '9' { + return true + } + + if strings.Contains(lower, "level=error") || + strings.Contains(lower, "panic:") { + return true + } + + return false +} + +func (w *controllerWatcher) isExcluded(line string) bool { + for _, s := range w.excludeStrings { + if strings.Contains(line, s) { + return true + } + } + for _, re := range w.config.CompiledRegexps() { + if re.MatchString(line) { + return true + } + } + return false +} + +func (w *controllerWatcher) addError(err LogError) { + w.mu.Lock() + defer w.mu.Unlock() + w.errors = append(w.errors, err) +} + +func (w *controllerWatcher) getErrors() []LogError { + w.mu.Lock() + defer w.mu.Unlock() + copied := make([]LogError, len(w.errors)) + copy(copied, w.errors) + return copied +} diff --git a/tests/e2e/internal/controller/restarts.go b/tests/e2e/internal/controller/restarts.go new file mode 100644 index 0000000..0997848 --- /dev/null +++ b/tests/e2e/internal/controller/restarts.go @@ -0,0 +1,105 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/operator-helm/tests/e2e/internal/framework" +) + +type restartSnapshot struct { + Controller string + Pod string + Container string + Count int32 +} + +var initialRestarts []restartSnapshot + +// SaveRestartCounts records the current restart count for all controller containers. +func SaveRestartCounts() { + cfg := framework.GetConfig() + initialRestarts = nil + + for _, ctrl := range cfg.Controllers { + pods, err := framework.GetClients().KubeClient().CoreV1(). + Pods(ctrl.Namespace). + List(context.Background(), metav1.ListOptions{LabelSelector: ctrl.LabelSelector}) + if err != nil { + GinkgoWriter.Printf("WARNING: cannot list pods for controller %q: %v\n", ctrl.Name, err) + continue + } + + for _, pod := range pods.Items { + for _, cs := range pod.Status.ContainerStatuses { + initialRestarts = append(initialRestarts, restartSnapshot{ + Controller: ctrl.Name, + Pod: pod.Name, + Container: cs.Name, + Count: cs.RestartCount, + }) + } + } + } +} + +// AssertNoRestarts fails the test if any controller container restarted during the suite. +func AssertNoRestarts() { + GinkgoHelper() + cfg := framework.GetConfig() + + var restartMessages []string + + for _, ctrl := range cfg.Controllers { + pods, err := framework.GetClients().KubeClient().CoreV1(). + Pods(ctrl.Namespace). + List(context.Background(), metav1.ListOptions{LabelSelector: ctrl.LabelSelector}) + Expect(err).NotTo(HaveOccurred(), "failed to list pods for controller %q", ctrl.Name) + + for _, pod := range pods.Items { + for _, cs := range pod.Status.ContainerStatuses { + initial := findInitialCount(ctrl.Name, pod.Name, cs.Name) + if cs.RestartCount > initial { + restartMessages = append(restartMessages, fmt.Sprintf( + "controller %q pod %s container %s: restarts before=%d after=%d", + ctrl.Name, pod.Name, cs.Name, initial, cs.RestartCount, + )) + } + } + } + } + + if len(restartMessages) > 0 { + Fail(fmt.Sprintf("Controller restarts detected:\n %s", strings.Join(restartMessages, "\n "))) + } +} + +func findInitialCount(controllerName, pod, container string) int32 { + for _, s := range initialRestarts { + if s.Controller == controllerName && s.Pod == pod && s.Container == container { + return s.Count + } + } + return 0 +} diff --git a/tests/e2e/internal/framework/cleanup.go b/tests/e2e/internal/framework/cleanup.go new file mode 100644 index 0000000..567417e --- /dev/null +++ b/tests/e2e/internal/framework/cleanup.go @@ -0,0 +1,25 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package framework + +import "os" + +const PostCleanUpEnv = "POST_CLEANUP" + +func IsCleanUpNeeded() bool { + return os.Getenv(PostCleanUpEnv) != "no" +} diff --git a/tests/e2e/internal/framework/client.go b/tests/e2e/internal/framework/client.go new file mode 100644 index 0000000..cfec64f --- /dev/null +++ b/tests/e2e/internal/framework/client.go @@ -0,0 +1,96 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package framework + +import ( + apiruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorhelmclient "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" +) + +var clients Clients + +func GetClients() Clients { + return clients +} + +type Clients struct { + kubeClient kubernetes.Interface + operatorClient operatorhelmclient.Interface + generic client.Client + dynamic dynamic.Interface +} + +func (c Clients) KubeClient() kubernetes.Interface { + return c.kubeClient +} + +func (c Clients) OperatorClient() operatorhelmclient.Interface { + return c.operatorClient +} + +func (c Clients) GenericClient() client.Client { + return c.generic +} + +func (c Clients) DynamicClient() dynamic.Interface { + return c.dynamic +} + +func init() { + onceLoadConfig() + + restConfig, err := conf.ClusterTransport.RestConfig() + if err != nil { + panic(err) + } + + clients.kubeClient, err = kubernetes.NewForConfig(restConfig) + if err != nil { + panic(err) + } + + clients.operatorClient, err = operatorhelmclient.NewForConfig(restConfig) + if err != nil { + panic(err) + } + + clients.dynamic, err = dynamic.NewForConfig(restConfig) + if err != nil { + panic(err) + } + + scheme := apiruntime.NewScheme() + for _, addToScheme := range []func(*apiruntime.Scheme) error{ + clientgoscheme.AddToScheme, + apiv1alpha1.AddToScheme, + } { + if err := addToScheme(scheme); err != nil { + panic(err) + } + } + + clients.generic, err = client.New(restConfig, client.Options{Scheme: scheme}) + if err != nil { + panic(err) + } +} diff --git a/tests/e2e/internal/framework/config.go b/tests/e2e/internal/framework/config.go new file mode 100644 index 0000000..4d81f0a --- /dev/null +++ b/tests/e2e/internal/framework/config.go @@ -0,0 +1,156 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package framework + +import ( + "os" + "regexp" + "strconv" + "sync" + + yamlv3 "gopkg.in/yaml.v3" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/rest" +) + +var ( + conf *Config + once sync.Once +) + +func onceLoadConfig() { + once.Do(func() { + c, err := loadConfig() + if err != nil { + panic(err) + } + conf = c + }) +} + +func GetConfig() *Config { + onceLoadConfig() + copied := *conf + return &copied +} + +func loadConfig() (*Config, error) { + cfgPath := "./default_config.yaml" + if e, ok := os.LookupEnv("E2E_CONFIG"); ok { + cfgPath = e + } + + data, err := os.ReadFile(cfgPath) + if err != nil { + return nil, err + } + + var cfg Config + if err := yamlv3.Unmarshal(data, &cfg); err != nil { + return nil, err + } + + cfg.applyEnvOverrides() + cfg.compileRegexps() + + return &cfg, nil +} + +type Config struct { + ClusterTransport ClusterTransport `yaml:"clusterTransport"` + Controllers []ControllerConfig `yaml:"controllers"` +} + +type ControllerConfig struct { + Name string `yaml:"name"` + Namespace string `yaml:"namespace"` + LabelSelector string `yaml:"labelSelector"` + Containers []string `yaml:"containers"` + LogFilters LogFilters `yaml:"logFilters"` + + compiledRegexps []*regexp.Regexp +} + +func (c *ControllerConfig) CompiledRegexps() []*regexp.Regexp { + return c.compiledRegexps +} + +type LogFilters struct { + Exclude []string `yaml:"exclude"` + ExcludeRegexp []string `yaml:"excludeRegexp"` +} + +type ClusterTransport struct { + KubeConfig string `yaml:"kubeConfig"` + Token string `yaml:"token"` + Endpoint string `yaml:"endpoint"` + CertificateAuthority string `yaml:"certificateAuthority"` + InsecureTLS bool `yaml:"insecureTls"` +} + +func (c ClusterTransport) RestConfig() (*rest.Config, error) { + flags := genericclioptions.ConfigFlags{} + if c.KubeConfig != "" { + flags.KubeConfig = &c.KubeConfig + } + if c.Token != "" { + flags.BearerToken = &c.Token + } + if c.InsecureTLS { + flags.Insecure = &c.InsecureTLS + } + if c.CertificateAuthority != "" { + flags.CAFile = &c.CertificateAuthority + } + if c.Endpoint != "" { + flags.APIServer = &c.Endpoint + } + return flags.ToRESTConfig() +} + +func (c *Config) applyEnvOverrides() { + if e, ok := os.LookupEnv("E2E_CLUSTERTRANSPORT_KUBECONFIG"); ok { + c.ClusterTransport.KubeConfig = e + } + if e, ok := os.LookupEnv("E2E_CLUSTERTRANSPORT_TOKEN"); ok { + c.ClusterTransport.Token = e + } + if e, ok := os.LookupEnv("E2E_CLUSTERTRANSPORT_ENDPOINT"); ok { + c.ClusterTransport.Endpoint = e + } + if e, ok := os.LookupEnv("E2E_CLUSTERTRANSPORT_CERTIFICATEAUTHORITY"); ok { + c.ClusterTransport.CertificateAuthority = e + } + if e, ok := os.LookupEnv("E2E_CLUSTERTRANSPORT_INSECURETLS"); ok { + v, err := strconv.ParseBool(e) + if err == nil { + c.ClusterTransport.InsecureTLS = v + } + } +} + +func (c *Config) compileRegexps() { + for i := range c.Controllers { + ctrl := &c.Controllers[i] + for _, pattern := range ctrl.LogFilters.ExcludeRegexp { + re, err := regexp.Compile(pattern) + if err == nil { + ctrl.compiledRegexps = append(ctrl.compiledRegexps, re) + } + } + } +} diff --git a/tests/e2e/internal/framework/dump.go b/tests/e2e/internal/framework/dump.go new file mode 100644 index 0000000..2d5e4e1 --- /dev/null +++ b/tests/e2e/internal/framework/dump.go @@ -0,0 +1,141 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package framework + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + . "github.com/onsi/ginkgo/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func (f *Framework) saveDump() { + testName := sanitizeTestName(CurrentSpecReport().FullText()) + dir := getTmpDir() + + f.dumpNamespaceResources(testName, dir) + f.dumpAllControllerLogs(testName, dir) +} + +func (f *Framework) dumpNamespaceResources(testName, dir string) { + if f.namespace == nil { + return + } + + type gvr struct { + group, version, resource string + } + resources := []gvr{ + {"", "v1", "pods"}, + {"", "v1", "services"}, + {"", "v1", "configmaps"}, + {"", "v1", "events"}, + {"apps", "v1", "deployments"}, + } + for _, r := range resources { + list, err := f.dynamic.Resource( + schema.GroupVersionResource{Group: r.group, Version: r.version, Resource: r.resource}, + ).Namespace(f.namespace.Name).List(context.Background(), metav1.ListOptions{}) + if err != nil { + GinkgoWriter.Printf("Failed to list %s in namespace %s: %v\n", r.resource, f.namespace.Name, err) + continue + } + if len(list.Items) == 0 { + continue + } + + fileName := fmt.Sprintf("%s/e2e_failed__%s__%s.yaml", dir, testName, r.resource) + data, err := list.MarshalJSON() + if err != nil { + GinkgoWriter.Printf("Failed to marshal %s: %v\n", r.resource, err) + continue + } + if err := os.WriteFile(fileName, data, 0o644); err != nil { + GinkgoWriter.Printf("Failed to write %s dump: %v\n", r.resource, err) + } + } +} + +func (f *Framework) dumpAllControllerLogs(testName, dir string) { + cfg := GetConfig() + for _, ctrl := range cfg.Controllers { + pods, err := f.kubeClient.CoreV1(). + Pods(ctrl.Namespace). + List(context.Background(), metav1.ListOptions{LabelSelector: ctrl.LabelSelector}) + if err != nil { + GinkgoWriter.Printf("WARNING: cannot list pods for %s: %v\n", ctrl.Name, err) + continue + } + + for _, pod := range pods.Items { + containers := ctrl.Containers + if len(containers) == 0 { + for _, c := range pod.Spec.Containers { + containers = append(containers, c.Name) + } + } + + for _, container := range containers { + f.dumpContainerLogs(testName, dir, ctrl.Name, pod.Name, pod.Namespace, container) + } + } + } +} + +func (f *Framework) dumpContainerLogs(testName, dir, controllerName, podName, namespace, container string) { + stream, err := f.kubeClient.CoreV1(). + Pods(namespace). + GetLogs(podName, &corev1.PodLogOptions{Container: container}). + Stream(context.Background()) + if err != nil { + GinkgoWriter.Printf("Failed to get logs for %s/%s/%s: %v\n", controllerName, podName, container, err) + return + } + defer stream.Close() + + data, err := io.ReadAll(stream) + if err != nil { + GinkgoWriter.Printf("Failed to read logs for %s/%s/%s: %v\n", controllerName, podName, container, err) + return + } + + fileName := fmt.Sprintf("%s/e2e_failed__%s__%s__%s__%s.log", dir, testName, controllerName, podName, container) + if err := os.WriteFile(fileName, data, 0o644); err != nil { + GinkgoWriter.Printf("Failed to save logs for %s/%s/%s: %v\n", controllerName, podName, container, err) + } +} + +func sanitizeTestName(name string) string { + r := strings.NewReplacer( + " ", "_", ":", "_", "[", "_", "]", "_", + "(", "_", ")", "_", "|", "_", "`", "", "'", "", + ) + return r.Replace(strings.ToLower(name)) +} + +func getTmpDir() string { + if dir := os.Getenv("RUNNER_TEMP"); dir != "" { + return dir + } + return "/tmp" +} diff --git a/tests/e2e/internal/framework/framework.go b/tests/e2e/internal/framework/framework.go new file mode 100644 index 0000000..0eef062 --- /dev/null +++ b/tests/e2e/internal/framework/framework.go @@ -0,0 +1,164 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package framework + +import ( + "context" + "fmt" + "maps" + "slices" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + NamespacePrefix = "e2e" + E2ELabel = "e2e-test" +) + +type Framework struct { + Clients + + namespacePrefix string + namespace *corev1.Namespace + objectsToDelete []client.Object + deferredDeletes []func() error +} + +func NewFramework(prefix string) *Framework { + return &Framework{ + Clients: GetClients(), + namespacePrefix: prefix, + } +} + +// Before creates an isolated namespace for the test. +// Pass empty prefix to NewFramework to skip namespace creation. +func (f *Framework) Before() { + GinkgoHelper() + if f.namespacePrefix == "" { + return + } + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-%s-", NamespacePrefix, f.namespacePrefix), + Labels: map[string]string{E2ELabel: "true"}, + }, + } + err := f.generic.Create(context.Background(), ns) + Expect(err).NotTo(HaveOccurred()) + By(fmt.Sprintf("Namespace %q has been created", ns.Name)) + f.namespace = ns +} + +// After handles cleanup and dump on failure. +func (f *Framework) After() { + GinkgoHelper() + + if CurrentSpecReport().Failed() { + f.saveDump() + } + + if !IsCleanUpNeeded() { + return + } + + for _, fn := range f.deferredDeletes { + _ = fn() + } + + slices.Reverse(f.objectsToDelete) + + for _, obj := range f.objectsToDelete { + _ = f.generic.Delete(context.Background(), obj) + } + f.waitDeleted(f.objectsToDelete) + + if f.namespace != nil { + By("Cleanup: delete namespace") + err := f.generic.Delete(context.Background(), f.namespace) + if err != nil && !k8serrors.IsNotFound(err) { + Expect(err).NotTo(HaveOccurred()) + } + } +} + +func (f *Framework) Namespace() *corev1.Namespace { + return f.namespace +} + +func (f *Framework) NamespaceName() string { + if f.namespace == nil { + return "" + } + return f.namespace.Name +} + +// Create creates resources via the generic client and registers them for cleanup. +func (f *Framework) Create(ctx context.Context, objs ...client.Object) error { + for _, obj := range objs { + labels := obj.GetLabels() + if labels == nil { + labels = make(map[string]string) + } + maps.Copy(labels, map[string]string{E2ELabel: f.namespacePrefix}) + obj.SetLabels(labels) + + if err := f.generic.Create(ctx, obj); err != nil { + return err + } + f.objectsToDelete = append(f.objectsToDelete, obj) + } + return nil +} + +// DeferDelete registers objects for cleanup in After(). +func (f *Framework) DeferDelete(objs ...client.Object) { + f.objectsToDelete = append(f.objectsToDelete, objs...) +} + +// DeferDeleteFunc registers a custom cleanup function. +func (f *Framework) DeferDeleteFunc(fn func() error) { + f.deferredDeletes = append(f.deferredDeletes, fn) +} + +func (f *Framework) waitDeleted(objs []client.Object) { + for _, obj := range objs { + key := types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + } + _ = wait.PollUntilContextTimeout( + context.Background(), time.Second, LongTimeout, true, + func(ctx context.Context) (bool, error) { + err := f.generic.Get(ctx, key, obj) + if k8serrors.IsNotFound(err) { + return true, nil + } + return false, nil + }, + ) + } +} diff --git a/tests/e2e/internal/framework/timeout.go b/tests/e2e/internal/framework/timeout.go new file mode 100644 index 0000000..310c773 --- /dev/null +++ b/tests/e2e/internal/framework/timeout.go @@ -0,0 +1,48 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package framework + +import ( + "os" + "time" +) + +const ( + shortTimeoutEnv = "E2E_SHORT_TIMEOUT" + middleTimeoutEnv = "E2E_MIDDLE_TIMEOUT" + longTimeoutEnv = "E2E_LONG_TIMEOUT" + maxTimeoutEnv = "E2E_MAX_TIMEOUT" +) + +var ( + ShortTimeout = getTimeout(shortTimeoutEnv, 30*time.Second) + MiddleTimeout = getTimeout(middleTimeoutEnv, 60*time.Second) + LongTimeout = getTimeout(longTimeoutEnv, 300*time.Second) + MaxTimeout = getTimeout(maxTimeoutEnv, 600*time.Second) + PollingInterval = 1 * time.Second +) + +func getTimeout(env string, defaultTimeout time.Duration) time.Duration { + if e, ok := os.LookupEnv(env); ok { + t, err := time.ParseDuration(e) + if err != nil { + return defaultTimeout + } + return t + } + return defaultTimeout +} diff --git a/tests/e2e/internal/util/namespace.go b/tests/e2e/internal/util/namespace.go new file mode 100644 index 0000000..9f43627 --- /dev/null +++ b/tests/e2e/internal/util/namespace.go @@ -0,0 +1,97 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/operator-helm/tests/e2e/internal/framework" +) + +// AssertNamespaceAbsent verifies the namespace does not exist. +func AssertNamespaceAbsent(name string) { + GinkgoHelper() + _, err := framework.GetClients().KubeClient().CoreV1(). + Namespaces().Get(context.Background(), name, metav1.GetOptions{}) + Expect(k8serrors.IsNotFound(err)).To(BeTrue(), + "namespace %q should not exist, but it does", name) +} + +// UntilNamespaceAbsent waits for the namespace to be fully deleted. +func UntilNamespaceAbsent(name string, timeout time.Duration) { + GinkgoHelper() + Eventually(func(g Gomega) { + _, err := framework.GetClients().KubeClient().CoreV1(). + Namespaces().Get(context.Background(), name, metav1.GetOptions{}) + g.Expect(k8serrors.IsNotFound(err)).To(BeTrue(), + "namespace %q still exists", name) + }).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed()) +} + +// AssertNamespaceExists verifies the namespace exists. +func AssertNamespaceExists(name string) { + GinkgoHelper() + _, err := framework.GetClients().KubeClient().CoreV1(). + Namespaces().Get(context.Background(), name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred(), "namespace %q should exist", name) +} + +// EnsureNamespace creates a namespace if it does not already exist. +func EnsureNamespace(name string, labels map[string]string) *corev1.Namespace { + GinkgoHelper() + + existing, err := framework.GetClients().KubeClient().CoreV1(). + Namespaces().Get(context.Background(), name, metav1.GetOptions{}) + if err == nil { + return existing + } + Expect(k8serrors.IsNotFound(err)).To(BeTrue(), + "unexpected error checking namespace %q: %v", name, err) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: labels, + }, + } + created, err := framework.GetClients().KubeClient().CoreV1(). + Namespaces().Create(context.Background(), ns, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred(), "failed to create namespace %q", name) + return created +} + +// DeleteNamespace deletes a namespace and optionally waits for it to disappear. +func DeleteNamespace(name string, wait bool, timeout time.Duration) { + GinkgoHelper() + err := framework.GetClients().KubeClient().CoreV1(). + Namespaces().Delete(context.Background(), name, metav1.DeleteOptions{}) + if k8serrors.IsNotFound(err) { + return + } + Expect(err).NotTo(HaveOccurred(), "failed to delete namespace %q", name) + + if wait { + UntilNamespaceAbsent(name, timeout) + } +} diff --git a/tests/e2e/internal/util/pod.go b/tests/e2e/internal/util/pod.go new file mode 100644 index 0000000..ee4cf21 --- /dev/null +++ b/tests/e2e/internal/util/pod.go @@ -0,0 +1,127 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/operator-helm/tests/e2e/internal/framework" +) + +// UntilControllerReady waits for all controller pods to be Running with all +// containers Ready and zero restarts. +func UntilControllerReady(namespace, labelSelector string, timeout time.Duration) { + GinkgoHelper() + Eventually(func(g Gomega) { + pods, err := framework.GetClients().KubeClient().CoreV1(). + Pods(namespace). + List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector}) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(pods.Items).NotTo(BeEmpty(), + "no controller pods found with selector %s in namespace %s", labelSelector, namespace) + + for _, pod := range pods.Items { + g.Expect(pod.Status.Phase).To(Equal(corev1.PodRunning), + "pod %s is %s, not Running", pod.Name, pod.Status.Phase) + + for _, cs := range pod.Status.ContainerStatuses { + g.Expect(cs.Ready).To(BeTrue(), + "container %s in pod %s is not ready", cs.Name, pod.Name) + g.Expect(cs.RestartCount).To(BeZero(), + "container %s in pod %s has %d restarts", cs.Name, pod.Name, cs.RestartCount) + } + } + }).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed()) +} + +// AssertPodsExist verifies that at least minCount pods matching the selector +// exist in the namespace. +func AssertPodsExist(namespace, labelSelector string, minCount int) { + GinkgoHelper() + pods, err := framework.GetClients().KubeClient().CoreV1(). + Pods(namespace). + List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector}) + Expect(err).NotTo(HaveOccurred()) + Expect(len(pods.Items)).To(BeNumerically(">=", minCount), + "expected >= %d pods in %s with selector %s, got %d", + minCount, namespace, labelSelector, len(pods.Items)) +} + +// UntilPodsExist waits until at least minCount pods appear. +func UntilPodsExist(namespace, labelSelector string, minCount int, timeout time.Duration) { + GinkgoHelper() + Eventually(func(g Gomega) { + pods, err := framework.GetClients().KubeClient().CoreV1(). + Pods(namespace). + List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector}) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(pods.Items)).To(BeNumerically(">=", minCount), + "waiting for >= %d pods, got %d", minCount, len(pods.Items)) + }).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed()) +} + +// UntilPodCount waits for exactly expectedCount Running pods (excluding +// pods being deleted). +func UntilPodCount(namespace, labelSelector string, expectedCount int, timeout time.Duration) { + GinkgoHelper() + Eventually(func(g Gomega) { + pods, err := framework.GetClients().KubeClient().CoreV1(). + Pods(namespace). + List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector}) + g.Expect(err).NotTo(HaveOccurred()) + + runningCount := 0 + for _, pod := range pods.Items { + if pod.Status.Phase == corev1.PodRunning && pod.DeletionTimestamp == nil { + runningCount++ + } + } + + g.Expect(runningCount).To(Equal(expectedCount), + "expected %d running pods, got %d (total listed: %d)", + expectedCount, runningCount, len(pods.Items)) + }).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed()) +} + +// UntilAllPodsReady waits for exactly expectedCount pods to be Running and +// all their containers Ready. +func UntilAllPodsReady(namespace, labelSelector string, expectedCount int, timeout time.Duration) { + GinkgoHelper() + Eventually(func(g Gomega) { + pods, err := framework.GetClients().KubeClient().CoreV1(). + Pods(namespace). + List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector}) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(pods.Items)).To(Equal(expectedCount), + "expected %d pods, got %d", expectedCount, len(pods.Items)) + + for _, pod := range pods.Items { + g.Expect(pod.Status.Phase).To(Equal(corev1.PodRunning), + "pod %s phase: %s", pod.Name, pod.Status.Phase) + for _, cs := range pod.Status.ContainerStatuses { + g.Expect(cs.Ready).To(BeTrue(), + "pod %s container %s not ready", pod.Name, cs.Name) + } + } + }).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed()) +} diff --git a/tests/e2e/internal/util/resource.go b/tests/e2e/internal/util/resource.go new file mode 100644 index 0000000..b1369cf --- /dev/null +++ b/tests/e2e/internal/util/resource.go @@ -0,0 +1,172 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/operator-helm/tests/e2e/internal/framework" +) + +// UntilObjectPhase waits for all objects to reach the expected status.phase. +func UntilObjectPhase(expectedPhase string, timeout time.Duration, objs ...client.Object) { + GinkgoHelper() + untilObjectField("status.phase", expectedPhase, timeout, objs...) +} + +// UntilObjectState waits for all objects to reach the expected status.state. +func UntilObjectState(expectedState string, timeout time.Duration, objs ...client.Object) { + GinkgoHelper() + untilObjectField("status.state", expectedState, timeout, objs...) +} + +// UntilConditionTrue waits for the specified condition type to become True +// on all provided objects. +func UntilConditionTrue(conditionType string, timeout time.Duration, objs ...client.Object) { + GinkgoHelper() + UntilConditionStatus(conditionType, string(metav1.ConditionTrue), timeout, objs...) +} + +// UntilConditionStatus waits for the specified condition to reach the given status. +func UntilConditionStatus(conditionType, expectedStatus string, timeout time.Duration, objs ...client.Object) { + GinkgoHelper() + Eventually(func(g Gomega) { + for _, obj := range objs { + u := toUnstructured(obj) + err := framework.GetClients().GenericClient().Get( + context.Background(), client.ObjectKeyFromObject(obj), u, + ) + g.Expect(err).NotTo(HaveOccurred()) + + conditions, found, err := unstructured.NestedSlice(u.Object, "status", "conditions") + g.Expect(err).NotTo(HaveOccurred(), + "failed to access status.conditions of %s", u.GetName()) + g.Expect(found).To(BeTrue(), + "no status.conditions found on %s", u.GetName()) + + var matched bool + for _, c := range conditions { + m, ok := c.(map[string]interface{}) + if !ok { + continue + } + if t, _ := m["type"].(string); t == conditionType { + observedGeneration, _ := m["observedGeneration"].(int64) + g.Expect(observedGeneration).To(BeNumerically("==", obj.GetGeneration())) + + status, _ := m["status"].(string) + g.Expect(status).To(Equal(expectedStatus), + "object %s condition %s status is %q, expected %q", + u.GetName(), conditionType, status, expectedStatus) + matched = true + break + } + } + g.Expect(matched).To(BeTrue(), + "condition %s not found on %s", conditionType, u.GetName()) + } + }).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed()) +} + +// UntilConditionReason waits for the specified condition to have the expected reason. +func UntilConditionReason(conditionType, expectedReason string, timeout time.Duration, objs ...client.Object) { + GinkgoHelper() + Eventually(func(g Gomega) { + for _, obj := range objs { + u := toUnstructured(obj) + err := framework.GetClients().GenericClient().Get( + context.Background(), client.ObjectKeyFromObject(obj), u, + ) + g.Expect(err).NotTo(HaveOccurred()) + + conditions, found, _ := unstructured.NestedSlice(u.Object, "status", "conditions") + g.Expect(found).To(BeTrue()) + + var matched bool + for _, c := range conditions { + m, ok := c.(map[string]interface{}) + if !ok { + continue + } + if t, _ := m["type"].(string); t == conditionType { + reason, _ := m["reason"].(string) + g.Expect(reason).To(Equal(expectedReason), + "object %s condition %s reason is %q, expected %q", + u.GetName(), conditionType, reason, expectedReason) + matched = true + break + } + } + g.Expect(matched).To(BeTrue(), + "condition %s not found on %s", conditionType, u.GetName()) + } + }).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed()) +} + +func untilObjectField(fieldPath, expected string, timeout time.Duration, objs ...client.Object) { + GinkgoHelper() + Eventually(func(g Gomega) { + for _, obj := range objs { + u := toUnstructured(obj) + err := framework.GetClients().GenericClient().Get( + context.Background(), client.ObjectKeyFromObject(obj), u, + ) + g.Expect(err).NotTo(HaveOccurred(), + "failed to get %s", obj.GetName()) + + path := strings.Split(fieldPath, ".") + value, found, _ := unstructured.NestedString(u.Object, path...) + actual := "Unknown" + if found { + actual = value + } + g.Expect(actual).To(Equal(expected), + "object %s %s is %q, expected %q", + u.GetName(), fieldPath, actual, expected) + } + }).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed()) +} + +func toUnstructured(obj client.Object) *unstructured.Unstructured { + if u, ok := obj.(*unstructured.Unstructured); ok { + return u.DeepCopy() + } + + objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + Expect(err).NotTo(HaveOccurred(), "failed to convert object to unstructured") + u := &unstructured.Unstructured{Object: objMap} + + c := framework.GetClients().GenericClient() + gvks, _, err := c.Scheme().ObjectKinds(obj) + if err == nil && len(gvks) > 0 { + u.SetGroupVersionKind(gvks[0]) + } else { + u.SetGroupVersionKind(schema.GroupVersionKind{}) + } + + return u +} diff --git a/tests/e2e/internal/util/update.go b/tests/e2e/internal/util/update.go new file mode 100644 index 0000000..ea163b5 --- /dev/null +++ b/tests/e2e/internal/util/update.go @@ -0,0 +1,75 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/tests/e2e/internal/framework" +) + +// UpdateHelmClusterAddon performs a read-modify-write cycle on a HelmClusterAddon +// with automatic retry on conflict. +func UpdateHelmClusterAddon(name string, mutate func(*apiv1alpha1.HelmClusterAddon)) *apiv1alpha1.HelmClusterAddon { + GinkgoHelper() + + var updated *apiv1alpha1.HelmClusterAddon + Eventually(func(g Gomega) { + current, err := framework.GetClients().OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Get(context.Background(), name, metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + mutate(current) + + updated, err = framework.GetClients().OperatorClient().HelmV1alpha1(). + HelmClusterAddons(). + Update(context.Background(), current, metav1.UpdateOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + }).WithTimeout(framework.ShortTimeout).WithPolling(time.Second).Should(Succeed()) + + return updated +} + +// UpdateHelmClusterAddonRepository performs a read-modify-write cycle on a HelmClusterAddonRepository +// with automatic retry on conflict. +func UpdateHelmClusterAddonRepository(name string, mutate func(*apiv1alpha1.HelmClusterAddonRepository)) *apiv1alpha1.HelmClusterAddonRepository { + GinkgoHelper() + + var updated *apiv1alpha1.HelmClusterAddonRepository + Eventually(func(g Gomega) { + current, err := framework.GetClients().OperatorClient().HelmV1alpha1(). + HelmClusterAddonRepositories(). + Get(context.Background(), name, metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + mutate(current) + + updated, err = framework.GetClients().OperatorClient().HelmV1alpha1(). + HelmClusterAddonRepositories(). + Update(context.Background(), current, metav1.UpdateOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + }).WithTimeout(framework.ShortTimeout).WithPolling(time.Second).Should(Succeed()) + + return updated +} diff --git a/tests/e2e/scripts/kind-d8-ci.sh b/tests/e2e/scripts/kind-d8-ci.sh new file mode 100755 index 0000000..293219e --- /dev/null +++ b/tests/e2e/scripts/kind-d8-ci.sh @@ -0,0 +1,574 @@ +#!/bin/bash + +# Copyright 2022 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Colors to identify the chip +BOLD='\033[1m' +GREEN='\033[0;32m' +PURPLE='\033[0;35m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +# Checking OS and getting a chip name +if uname -s | grep -q "Darwin"; then + chip_info=$(sysctl -n machdep.cpu.brand_string) + if [[ "$chip_info" == *"Apple M"* ]]; then + # Retrieving the processor generation for Apple on the M + chip_model=$(echo "$chip_info" | awk -F'Apple ' '{print $2}' | cut -d' ' -f1-2 | sed 's/ / /') + # Display an alert for Apple on M + echo -e "${BOLD}${PURPLE}Warning. ${CYAN}Your computer has been identified as: ${GREEN}Apple $chip_model ${NC} + ${YELLOW}Disable Rosetta support in Docker Desktop before installation. + To do this, in Docker Desktop go to ${CYAN}Settings > General > Virtual Machine Options ${YELLOW}and uncheck the ${CYAN}Use Rosetta for x86_64/amd64 emulation on Apple Silicon ${YELLOW}option.${NC}" + fi +fi + +PARENT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." &> /dev/null && pwd) + +KIND_CLUSTER_NAME=${KIND_CLUSTER_NAME:-d8-operator-helm} +KIND_CONFIG_DIR=${KIND_CONFIG_DIR:-$PARENT_DIR/kind}/$KIND_CLUSTER_NAME +KIND_IMAGE=kindest/node:v1.31.6@sha256:28b7cbb993dfe093c76641a0c95807637213c9109b761f1d422c2400e22b8e87 + +D8_RELEASE_CHANNEL_TAG=stable +D8_RELEASE_CHANNEL_NAME=Stable +D8_REGISTRY_ADDRESS=registry.deckhouse.io +D8_REGISTRY_PATH=${D8_REGISTRY_ADDRESS}/deckhouse/ce +D8_LICENSE_KEY= + +KIND_INSTALL_DIRECTORY=$PARENT_DIR/kind/bin +KIND_PATH=kind +KIND_VERSION=v0.27.0 + +KUBECTL_INSTALL_DIRECTORY=$PARENT_DIR/kind/bin +KUBECTL_PATH=kubectl +KUBECTL_VERSION=v1.31.6 + +REQUIRE_MEMORY_MIN_BYTES=4000000000 # 4GB + +usage() { + printf " + Usage: %s [--channel ] [--key ] [--os ] + + --channel + Deckhouse Kubernetes Platform release channel name. + Possible values: Alpha, Beta, EarlyAccess, Stable, RockSolid. + Default: Stable. + + --key + Deckhouse Kubernetes Platform Enterprise Edition license key. + If no license key specified, Deckhouse Kubernetes Platform Community Edition will be installed. + + --os + Override the OS detection. + + --help|-h + Print this message. + +" "$0" +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --channel) + case "$2" in + "") + echo "Release channel is empty. Please specify the release channel name." + usage + exit 1 + ;; + *) + if [[ "$2" =~ ^(Alpha|Beta|EarlyAccess|Stable|RockSolid)$ ]]; then + D8_RELEASE_CHANNEL_NAME="$2" + D8_RELEASE_CHANNEL_TAG=$(echo ${D8_RELEASE_CHANNEL_NAME} | sed 's/EarlyAccess/early-access/; s/RockSolid/rock-solid/' | tr '[:upper:]' '[:lower:]') + else + echo "Incorrect release channel. Use Alpha, Beta, EarlyAccess, Stable or RockSolid." + usage + exit 1 + fi + shift + ;; + esac + ;; + --key) + case "$2" in + "") + echo "License key is empty. Please specify the license key or don't use the --key parameter to install Deckhouse Kubernetes Platform Community Edition." + usage + exit 1 + ;; + *) + D8_LICENSE_KEY="$2" + D8_REGISTRY_PATH=${D8_REGISTRY_ADDRESS}/deckhouse/ee + shift + ;; + esac + ;; + --os) + case "$2" in + "") + echo "Please specify 'linux' or 'mac' for the --os parameter." + usage + exit 1 + ;; + *) + OS_NAME="$2" + shift + ;; + esac + ;; + --help | -h) + usage + exit 1 + ;; + --*) + echo "Illegal option $1" + usage + exit 1 + ;; + esac + shift $(($# > 0 ? 1 : 0)) + done +} + +os_detect() { + if [[ (-z "$OS_NAME") ]]; then + # some systems dont have lsb-release yet have the lsb_release binary and + # vice-versa + if [ -e /etc/lsb-release ]; then + . /etc/lsb-release + + OS_NAME=${DISTRIB_ID} + + elif [ "$(which lsb_release 2>/dev/null)" ]; then + OS_NAME=$(lsb_release -i | cut -f2 | awk '{ print tolower($1) }') + + elif [ -e /etc/debian_version ]; then + # some Debians have jessie/sid in their /etc/debian_version + # while others have '6.0.7' + OS_NAME=$(cat /etc/issue | head -1 | awk '{ print tolower($1) }') + + elif [[ "$OSTYPE" == 'darwin'* ]]; then + OS_NAME=mac + + else + noop # Unknown OS + fi + fi + + OS_NAME="${OS_NAME// /}" + + # Supported on ... + if [[ ("$OS_NAME" == "Ubuntu") || ("$OS_NAME" == "ubuntu") || ("$OS_NAME" == "Debian") || ("$OS_NAME" == "debian") ]]; then + OS_NAME=linux + elif [[ ("$OS_NAME" != "mac") && ("$OS_NAME" != "linux") ]]; then + OS_NAME= + fi + + if [ -z "$OS_NAME" ]; then + printf "Your operating system distribution and version might not supported by this script. + +You can override the OS detection by setting the --os parameter to running this script. + +E.g, to force Linux: --os linux +" + + exit 1 + fi + + MACHINE_ARCH=$(uname -m) + + echo "Detected operating system as $OS_NAME (${MACHINE_ARCH:-unknown})." +} + +prerequisites_check() { + echo "Checking for docker..." + if command -v docker >/dev/null; then + echo "Detected docker..." + else + echo "docker is not installed. Please install docker. You may go to https://docs.docker.com/engine/install/ for details." + exit 1 + fi + + memory_check + kubectl_check + kind_check + preinstall_checks +} + +memory_check() { + if [[ "$OS_NAME" == "linux" ]]; then + MEMORY_TOTAL_BYTES=$(free --bytes 2>/dev/null | grep -i mem | awk '{print $2}' 2>/dev/null) + else + MEMORY_TOTAL_BYTES=$(sysctl -n hw.memsize 2>/dev/null) + fi + + if [[ ("$MEMORY_TOTAL_BYTES" -gt "0") && ("$MEMORY_TOTAL_BYTES" -lt "$REQUIRE_MEMORY_MIN_BYTES") ]]; then + echo "Insufficient memory to install Deckhouse Kubernetes Platform." + echo "Deckhouse Kubernetes Platform requires at least 4 gigabytes of memory." + exit 1 + fi + + if [[ ("$MEMORY_TOTAL_BYTES" -eq "0") || (-z "$MEMORY_TOTAL_BYTES") ]]; then + echo "Can't get the total memory value." + echo "Note, that Deckhouse Kubernetes Platform requires at least 4 gigabytes of memory." + echo "Press enter to continue..." + read + fi +} + +kubectl_check() { + echo "Checking for kubectl..." + if command -v kubectl >/dev/null; then + echo "Detected kubectl..." + elif command -v ${KUBECTL_INSTALL_DIRECTORY}/kubectl >/dev/null; then + echo "Detected ${KUBECTL_INSTALL_DIRECTORY}/kubectl..." + KUBECTL_PATH=${KUBECTL_INSTALL_DIRECTORY}/kubectl + else + echo "kubectl is not installed." + echo "Installing the latest stable kubectl version to ${KUBECTL_INSTALL_DIRECTORY}/kubectl ..." + + mkdir -p $KUBECTL_INSTALL_DIRECTORY + curl -sLO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/${OS_NAME/mac/darwin}/${MACHINE_ARCH/x86_64/amd64}/kubectl" + + if [ "$?" -ne "0" ]; then + echo "Unable to download kubectl." + exit 1 + fi + + install -m 0755 kubectl "${KUBECTL_INSTALL_DIRECTORY}"/kubectl + if [ "$?" -ne "0" ]; then + echo "Insufficient permissions to install kubectl. Trying again with sudo..." + sudo install -m 0755 kubectl "${KUBECTL_INSTALL_DIRECTORY}"/kubectl + if [ "$?" -ne "0" ]; then + echo "Unable to install kubectl. Check installation path and permissions." + exit 1 + fi + fi + + KUBECTL_PATH=${KUBECTL_INSTALL_DIRECTORY}/kubectl + fi +} + +kind_check() { + echo "Checking for kind $KIND_VERSION..." + if [[ "v$(kind version -q 2>/dev/null)" == "$KIND_VERSION" ]]; then + echo "Detected kind $KIND_VERSION..." + elif [[ "v$(${KIND_INSTALL_DIRECTORY}/kind version -q 2>/dev/null)" == "$KIND_VERSION" ]]; then + echo "Detected ${KIND_INSTALL_DIRECTORY}/kind..." + KIND_PATH=${KIND_INSTALL_DIRECTORY}/kind + else + echo "Installing kind to ${KIND_INSTALL_DIRECTORY}/kind ..." + + mkdir -p ${KIND_INSTALL_DIRECTORY} + + curl -sLo ./kind-binary "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-${OS_NAME/mac/darwin}-${MACHINE_ARCH/x86_64/amd64}" + + if [ "$?" -ne "0" ]; then + echo "Unable to download kind." + exit 1 + fi + + install -m 0755 kind-binary "${KIND_INSTALL_DIRECTORY}"/kind + + if [ "$?" -ne "0" ]; then + echo "Insufficient permissions to install kind. Trying again with sudo..." + sudo install -m 0755 kind-binary "${KIND_INSTALL_DIRECTORY}"/kind + if [ "$?" -ne "0" ]; then + echo "Unable to install kind. Check installation path and permissions." + exit 1 + fi + fi + + KIND_PATH=${KIND_INSTALL_DIRECTORY}/kind + fi +} + +preinstall_checks() { + local cluster_exist=true + + while [[ "$cluster_exist" == "true" ]]; do + + # Check if a kind cluster with the name `d8` exist + ${KIND_PATH} get clusters | grep -q "^${KIND_CLUSTER_NAME}$" &>/dev/null + + if [ "$?" -eq "0" ]; then + cluster_exist=true + else + cluster_exist=false + fi + + if [[ "$cluster_exist" == "true" ]]; then + ${KIND_PATH} delete cluster --name "${KIND_CLUSTER_NAME}" + sleep 3 + fi + done +} + +configs_create() { + mkdir -p ${KIND_CONFIG_DIR} + + echo "Creating kind config file (${KIND_CONFIG_DIR}/kind.cfg)..." + cat <${KIND_CONFIG_DIR}/kind.cfg +apiVersion: kind.x-k8s.io/v1alpha4 +kind: Cluster +featureGates: + "ValidatingAdmissionPolicy": true +runtimeConfig: + "admissionregistration.k8s.io/v1alpha1": true +nodes: +- role: control-plane +EOF + + echo "Creating Deckhouse Kubernetes Platform installation config file (${KIND_CONFIG_DIR}/config.yml)..." + cat <${KIND_CONFIG_DIR}/config.yml +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: deckhouse +spec: + version: 1 + enabled: true + settings: + bundle: Minimal + releaseChannel: EarlyAccess + logLevel: Info +--- +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: global +spec: + version: 2 + settings: + modules: + publicDomainTemplate: "%s.127.0.0.1.sslip.io" + https: + mode: Disabled +--- +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: cert-manager +spec: + version: 1 + enabled: true +--- +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: operator-prometheus-crd +spec: + enabled: true +--- +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: prometheus-crd +spec: + enabled: true +--- +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: prometheus +spec: + version: 2 + enabled: true + settings: + longtermRetentionDays: 0 +--- +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: ingress-nginx +spec: + enabled: true +--- +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: operator-prometheus +spec: + enabled: true +--- +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: monitoring-kubernetes +spec: + enabled: true +--- +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: monitoring-deckhouse +spec: + enabled: true +--- +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: monitoring-kubernetes-control-plane +spec: + enabled: true +EOF + + if [[ -n "$D8_LICENSE_KEY" ]]; then + generate_ee_access_string "$D8_LICENSE_KEY" + cat <>${KIND_CONFIG_DIR}/config.yml +--- +apiVersion: deckhouse.io/v1 +kind: InitConfiguration +deckhouse: + imagesRepo: $D8_REGISTRY_PATH + registryDockerCfg: $D8_EE_ACCESS_STRING +EOF + fi + + echo "Creating Deckhouse Kubernetes Platform resource file (${KIND_CONFIG_DIR}/resources.yml)..." + cat <${KIND_CONFIG_DIR}/resources.yml +apiVersion: deckhouse.io/v1 +kind: IngressNginxController +metadata: + name: nginx +spec: + ingressClass: nginx + inlet: HostPort +EOF +} + +cluster_deletion_info() { + + printf " +To delete created cluster use the following command: + + ${KIND_PATH} delete cluster --name "${KIND_CLUSTER_NAME}" + +" +} + +cluster_create() { + + ${KIND_PATH} create cluster --name "${KIND_CLUSTER_NAME}" --image "${KIND_IMAGE}" --config "${KIND_CONFIG_DIR}/kind.cfg" + + if [ "$?" -ne "0" ]; then + printf " +Error creating cluster. If error is like '...port is already allocated' or '... address already in use', then you need to free ports 80 and 443. +E.g., you can find programs that use these ports using the following command: + + sudo lsof -n -i TCP@0.0.0.0:80,443 -s TCP:LISTEN + +" + cluster_deletion_info + exit 1 + fi + + ${KIND_PATH} get kubeconfig --internal --name "${KIND_CLUSTER_NAME}" >${KIND_CONFIG_DIR}/kubeconfig + +} + +deckhouse_install() { + echo "Running Deckhouse installation..." + + # Use the --debug flag to see exactly why it's failing + docker run --pull=always --rm --network kind \ + -v "${KIND_CONFIG_DIR}/config.yml:/config.yml" \ + -v "${KIND_CONFIG_DIR}/resources.yml:/resources.yml" \ + -v "${KIND_CONFIG_DIR}/kubeconfig:/kubeconfig" \ + ${D8_REGISTRY_PATH}/install:${D8_RELEASE_CHANNEL_TAG} \ + bash -c "dhctl bootstrap-phase install-deckhouse --kubeconfig=/kubeconfig --kubeconfig-context=kind-${KIND_CLUSTER_NAME} --config=/config.yml" + + # If that fails with the CRD error, we might need to wait 30s and try the second phase manually + if [ "$?" -ne "0" ]; then + echo "First phase might have timed out. Waiting 30s for CRDs to settle..." + sleep 30 + # Try the resource creation phase separately + docker run --rm --network kind -v "${KIND_CONFIG_DIR}/resources.yml:/resources.yml" -v "${KIND_CONFIG_DIR}/kubeconfig:/kubeconfig" \ + ${D8_REGISTRY_PATH}/install:${D8_RELEASE_CHANNEL_TAG} \ + dhctl bootstrap-phase create-resources --kubeconfig=/kubeconfig --kubeconfig-context=kind-${KIND_CLUSTER_NAME} --resources=/resources.yml + fi +} + +macos_force_qemu() { + if [ "$OS_NAME" = "mac" ] + then ${KUBECTL_PATH} --context kind-"${KIND_CLUSTER_NAME}" patch daemonset node-exporter -n d8-monitoring --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/1/env/-", "value": {"name": "EXPERIMENTAL_DOCKER_DESKTOP_FORCE_QEMU", "value": "1"}}]' 2>/dev/null + fi +} + +generate_ee_access_string() { + if [ "$OS_NAME" != "mac" ]; then B64_ARG="-w0"; else B64_ARG=""; fi + auth_part=$(echo -n "license-token:$1" | base64 $B64_ARG) + D8_EE_ACCESS_STRING=$(echo -n "{\"auths\": { \"$D8_REGISTRY_ADDRESS\": { \"username\": \"license-token\", \"password\": \"$1\", \"auth\": \"$auth_part\"}}}" | base64 $B64_ARG) + + if [ "$?" -ne "0" ]; then + echo "Error generation container registry access string for Deckhouse Kubernetes Platform Enterprise Edition" + exit 1 + fi +} + +setup_operator_helm() { + echo "Enabling operator-helm module..." + + ${KUBECTL_PATH} --context "kind-${KIND_CLUSTER_NAME}" create -f - < "${KIND_CONFIG_DIR}/kubeconfig-external" +} + +main() { + parse_args "$@" + + os_detect + prerequisites_check + configs_create + cluster_create + deckhouse_install + macos_force_qemu + setup_operator_helm + extract_kubectl_context +} + +main "$@" 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..97b9036 --- /dev/null +++ b/tools/validation/doc_changes.go @@ -0,0 +1,139 @@ +/* +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/`) +var docsDirAllowedFileRe = regexp.MustCompile(`docs/(CONFIGURATION|CR|README|RELEASE_NOTES|USAGE|EXAMPLE)(\.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: + CONFIGURATION.md + CR.md + EXAMPLE.md + USAGE.md + README.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/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..ff8d125 --- /dev/null +++ b/werf.yaml @@ -0,0 +1,121 @@ +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 +import: +- image: prepare-bundle + add: /prep-bundle + to: / + after: install + includePaths: + - module.yaml +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