diff --git a/.dmtlint.yaml b/.dmtlint.yaml new file mode 100644 index 0000000..7c2aab5 --- /dev/null +++ b/.dmtlint.yaml @@ -0,0 +1,31 @@ +global: + linters-settings: + documentation: + impact: error +linters-settings: + openapi: + exclude-rules: + enum: + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.sts.properties.provider" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.provider" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.provider" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.sts.properties.provider" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.upgrade.properties.remediation.properties.strategy.properties" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.uninstall.properties.deletionPropagation" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.driftDetection.properties.mode" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.upgrade.properties.remediation.properties.strategy" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.chart.properties.spec.properties.verify.properties.provider" + - "spec.versions[0].schema.openAPIV3Schema.properties.status.properties.lastAttemptedReleaseAction" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.chart.properties.spec.properties.verify.properties.provider" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.driftDetection.properties.mode" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.postRenderers.items.properties.kustomize.properties.patchesJson6902.items.properties.patch.items.properties.op" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.uninstall.properties.deletionPropagation" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.upgrade.properties.remediation.properties.strategy" + - "spec.versions[1].schema.openAPIV3Schema.properties.status.properties.lastAttemptedReleaseAction" + - "properties.logLevel" + - "properties.logFormat" + rbac: + exclude-rules: + wildcards: + - kind: ClusterRole + name: d8:operator-helm:helm-controller diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..9c8e0f2 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,44 @@ +name: Build + +on: [push] + +env: + CI_COMMIT_REF_NAME: ${{ github.ref_name }} + +jobs: + lint: + runs-on: ubuntu-latest + continue-on-error: true + name: Lint + steps: + - uses: actions/checkout@v4 + - uses: deckhouse/modules-actions/lint@main + # TODO: change after MVP + # env: + # DMT_METRICS_URL: ${{ secrets.DMT_METRICS_URL }} + # DMT_METRICS_TOKEN: ${{ secrets.DMT_METRICS_TOKEN }} + + build: + runs-on: ubuntu-latest + name: Build and Push images + steps: + - uses: actions/checkout@v4 + + - uses: deckhouse/modules-actions/setup@main + with: + registry: ghcr.io + registry_login: ${{ github.actor }} + registry_password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get the repository name + id: repo_name + run: echo "REPO_NAME=$(echo '${{ github.repository }}' | cut -d'/' -f2)" >> $GITHUB_OUTPUT + + - uses: deckhouse/modules-actions/build@main + with: + # TODO: change after MVP + # module_source: ghcr.io/${{ github.repository_owner }}/modules + module_source: ghcr.io/deckhouse/${{ steps.repo_name.outputs.REPO_NAME }} + module_name: ${{ steps.repo_name.outputs.REPO_NAME }} + module_tag: ${{ github.ref_name }} + svace_enabled: false diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..b7919d5 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,45 @@ +name: Deploy + +on: + workflow_dispatch: + inputs: + release_channel: + description: "Select the release channel" + type: choice + default: alpha + options: + - "alpha" + - "beta" + - "early-access" + - "stable" + - "rock-solid" + tag: + description: "Tag of the module, e.g., v1.21.1" + type: string + required: true + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy the module + steps: + - uses: actions/checkout@v4 + + - uses: deckhouse/modules-actions/setup@main + with: + registry: ghcr.io + registry_login: ${{ github.actor }} + registry_password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get the repository name + id: repo_name + run: echo "REPO_NAME=$(echo '${{ github.repository }}' | cut -d'/' -f2)" >> $GITHUB_OUTPUT + + - uses: deckhouse/modules-actions/deploy@main + with: + # TODO: change after MVP + # module_source: ghcr.io/${{ github.actor }}/modules + module_source: ghcr.io/deckhouse/${{ steps.repo_name.outputs.REPO_NAME }} + module_name: ${{ steps.repo_name.outputs.REPO_NAME }} + module_tag: ${{ github.event.inputs.tag }} + release_channel: ${{ github.event.inputs.release_channel }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3bed631 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +community@deckhouse.io. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7c2fa4b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,166 @@ +# Contributing + +## Feedback + +The first thing we recommend is to check the existing [issues](https://github.com/deckhouse/operator-helm/issues) — there may already be a discussion or solution on your topic. If not, choose the appropriate way to address the issue on [the new issue form](https://github.com/deckhouse/operator-helm/issues/new/choose). + +## Code contributions + +1. Prepare an environment. To build and run common workflows locally, you'll need to _at least_ have the following installed: + + - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) + - [Go](https://golang.org/doc/install) + - [Docker](https://docs.docker.com/get-docker/) + - [go-task](https://taskfile.dev/installation/) (task runner) + - [ginkgo](https://onsi.github.io/ginkgo/#installing-ginkgo) (testing framework required to run tests) + +2. [Fork the project](https://github.com/deckhouse/operator-helm/fork). + +3. Clone the project: + + ```shell + git clone https://github.com/[GITHUB_USERNAME]/operator-helm + ``` + +4. Create branch following the [branch name convention](#branch-name): + + ```shell + git checkout -b feat/core/add-new-feature + ``` + +5. Make changes. + +6. Commit changes: + + - Follow [the commit message convention](#commit-message). + - Sign off every commit you contributed as an acknowledgment of the [DCO](https://developercertificate.org/). + +7. Push commits. + +8. Create a pull request following the [pull request name convention](#pull-request-name). + +## Images + +The module images are located in the ./images directory. + +Images, such as build images or images with binary artifacts, should not be included in the module. To do so, they must be labeled as follows in the `werf.inc.yaml` file: `final: false`. + +## Conventions + +### Commit message + + + +**Examples:** + + + +#### Type + +Must be one of the following: + +* **feat**: new features or capabilities that enhance the user's experience. +* **fix**: bug fixes that enhance the user's experience. +* **refactor**: a code changes that neither fixes a bug nor adds a feature. +* **docs**: updates or improvements to documentation. +* **test**: additions or corrections to tests. +* **chore**: updates that don't fit into other types. + +#### Scope + +Scope indicates the area of the project affected by the changes. The scope can consist of a top-level scope, which broadly categorizes the changes, and can optionally include nested scopes that provide further detail. + +Supported scopes are the following: + + + +#### Subject + +The subject contains a succinct description of the change: + + - use the imperative, present tense: "change" not "changed" nor "changes" + - don't capitalize the first letter + - no dot (.) at the end + +#### Body + +Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". +The body should include the motivation for the change and contrast this with previous behavior. + +### Branch name + +Each branch name consists of a [**type**](#type), [**scope**](#scope), and a [**short-description**](#short-description): + +``` +// +``` + +When naming branches, only the top-level scope should be used. Multiple or nested scopes are not allowed in branch names, ensuring that each branch is clearly associated with a broad area of the project. + +**Examples:** + + + +### Changes Block + +When submitting a pull request, include a **changes block** to document modifications for the changelog. This block helps automate the release changelog creation, tracks updates, and prepares release notes. + +#### Format + +The changes block consists of YAML documents, each detailing a specific change. Use the following structure: + +```` +```changes +section: +type: +summary: +impact_level: # Optional +impact: | + +``` +```` + +#### Fields Description + + - **section**: (Required) Specifies the affected scope of the project. Should be in kebab-case, choose one of [available scopes](#scope). If PR affects multiple scopes, add change block for each scope. + - Examples: `api`, `core`, `ci` + + - **type**: (Required) Defines the nature of the change: + - `feature`: Adds new functionality. + - `fix`: Resolves user-facing issues. + - `chore`: Maintenance tasks without direct user impact. + - `docs`: Changes to documentation. + + - **summary**: (Required) A concise explanation of the change, ending with a period. + + - **impact_level**: (Optional) Indicates the significance of the change. + - `high`: Requires an **impact** description and will be included in "Know before update" sections. + - `low`: Minor changes, omitted from user-facing changelogs. If this level is specified, all other fields are not validated by GitHub workflow. + + - **impact**: (Required if `impact_level` is high) Describes the change's effects, such as expected restarts or downtime. + - Examples: + - "Ingress controller will restart." + - "Expect slow downtime due to kube-apiserver restarts." + +#### Example + + + +For full guidelines, refer to [here](https://github.com/deckhouse/deckhouse/wiki/Guidelines-for-working-with-PRs). + +#### Short description + +A concise, hyphen-separated phrase in kebab-case that clearly describes the main focus of the branch. + +### Pull request name + +Each pull request title should clearly reflect the changes introduced, adhering to [**the header format** of a commit message](#commit-message), typically mirroring the main commit's text in the PR. + +**Examples** + + + +## Coding + + - [Effective Go](https://golang.org/doc/effective_go.html). + - [Go's commenting conventions](http://blog.golang.org/godoc-documenting-go-code). diff --git a/Chart.yaml b/Chart.yaml new file mode 100644 index 0000000..102d074 --- /dev/null +++ b/Chart.yaml @@ -0,0 +1,6 @@ +name: operator-helm +version: 0.0.1 +dependencies: + - name: deckhouse_lib_helm + version: 1.55.1 + repository: https://deckhouse.github.io/lib-helm diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f24a346 --- /dev/null +++ b/LICENSE @@ -0,0 +1,214 @@ +Copyright (c) 2023 Flant JSC + +Portions of this software are licensed as follows: + +* All content residing under the "docs/" directory of this repository + is licensed under "Creative Commons: CC BY-SA 4.0 license". +* All client-side JavaScript (when served directly or after being compiled, + arranged, augmented, or combined), is licensed under the "MIT Expat" license. +* All third party components incorporated into this software are licensed under + the original license provided by the owner of the applicable component. +* Content outside of the above mentioned directories or restrictions above + is available under the "Apache License 2.0." license as defined below. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..c80979e --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,6 @@ +# Core maintainers + +| Name | Email | GitHub | +| ---------------- | -------------------------- | ------------------------------------------------------- | +| Ilya Lesikov | ilya.lesikov@flant.com | [@ilya-lesikov](https://github.com/ilya-lesikov) | +| Aleksei Igrychev | aleksei.igrychev@flant.com | [@alexey-igrychev](https://github.com/alexey-igrychev) | diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..35c80b2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security + +Thank you for your concern regarding the security issues in Deckhouse project. + +Please submit any discovered vulnerabilities to security@deckhouse.io and wait for our reply within 48 hours. + +If we confirm an issue, a relevant private discussion will be created with you as its participant. Otherwise, we will reply to you, probably asking for clarifications needed to verify the security risk. diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..2b534e9 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,113 @@ +version: "3" + +silent: true + +vars: + deckhouse_lib_helm_ver: 1.55.1 + target: "" + VALIDATION_FILES: "tools/validation/{main,messages,diff,no_cyrillic,doc_changes}.go" + +tasks: + check-werf: + cmds: + - which werf >/dev/null || (echo "werf not found."; exit 1) + silent: true + + check-yq: + cmds: + - which yq >/dev/null || (echo "yq not found."; exit 1) + silent: true + + check-jq: + cmds: + - which jq >/dev/null || (echo "jq not found."; exit 1) + silent: true + + check-helm: + cmds: + - which helm >/dev/null || (echo "helm not found."; exit 1) + silent: true + + helm-update-subcharts: + deps: + - check-helm + cmds: + - helm repo add deckhouse https://deckhouse.github.io/lib-helm + - helm repo update deckhouse + - helm dep update + + helm-bump-helm-lib: + deps: + - check-yq + cmds: + - yq -i '.dependencies[] |= select(.name == "deckhouse_lib_helm").version = "{{ .deckhouse_lib_helm_ver }}"' Chart.yaml + - task: helm-update-subcharts + + build: + deps: + - check-werf + cmds: + - werf build {{ .target }} + + dev:format:yaml: + desc: "Format non-templated YAML files, e.g. CRDs" + cmds: + # TODO: update image referecne + - | + docker run --rm \ + -v ./:/tmp/operator-helm ghcr.io/deckhouse/virtualization/prettier:3.2.5 \ + sh -c "cd /tmp/operator-helm ; prettier -w \"**/*.yaml\" \"**/*.yml\"" + + dev:addlicense: + desc: |- + Add Flant CE license to files sh,go,py. Default directory is root of project, custom directory path can be passed like: "task dev:addlicense -- " + cmds: + - | + {{if .CLI_ARGS}} + go run tools/addlicense/{main,variables,msg,utils}.go -directory {{ .CLI_ARGS }} + {{else}} + go run tools/addlicense/{main,variables,msg,utils}.go -directory ./ + {{end}} + + lint: + cmds: + - task: lint:doc-ru + - task: lint:prettier:yaml + - task: virtualization-controller:dvcr:lint + - task: virtualization-controller:lint + + lint:doc-ru: + desc: "Check the correspondence between description fields in the original crd and the Russian language version" + cmds: + - | + docker run \ + --rm -it -v "$PWD:/src" docker.io/fl64/d8-doc-ru-linter:v0.0.1-dev0 \ + sh -c \ + 'for crd in /src/crds/*.yaml; do [[ "$(basename "$crd")" =~ ^doc-ru ]] || (echo ${crd}; /d8-doc-ru-linter -s "$crd" -d "/src/crds/doc-ru-$(basename "$crd")" -n /dev/null); done' + + lint:prettier:yaml: + desc: "Check if yaml files are prettier-formatted." + cmds: + # TODO: update image referecne + - | + docker run --rm \ + -v ./:/tmp/operator-nelm ghcr.io/deckhouse/virtualization/prettier:3.2.5 \ + sh -c "cd /tmp/operator-nelm ; prettier -c \"**/*.yaml\" \"**/*.yml\"" + + validation:no-cyrillic: + desc: "No cyrillic" + cmds: + - go run {{ .VALIDATION_FILES }} --type no-cyrillic + + validation:doc-changes: + desc: "Doc-changes" + cmds: + - go run {{ .VALIDATION_FILES }} --type doc-changes + + # TODO: implement for operator-helm + # validation:helm-templates: + # desc: "Check Helm templates" + # cmds: + # - | + # cd tools/kubeconform + # ./kubeconform.sh diff --git a/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..c4ab374 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,84 @@ +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 + sigs.k8s.io/controller-runtime v0.23.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + 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/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/x448/float16 v0.8.4 // indirect + 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 + 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.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..e4b8369 --- /dev/null +++ b/api/go.sum @@ -0,0 +1,171 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/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/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +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-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/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/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/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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/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= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/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-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= +sigs.k8s.io/controller-tools v0.17.2 h1:jNFOKps8WnaRKZU2R+4vRCHnXyJanVmXBWqkuUPFyFg= +sigs.k8s.io/controller-tools v0.17.2/go.mod h1:4q5tZG2JniS5M5bkiXY2/potOiXyhoZVw/U48vLkXk0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +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..544695f --- /dev/null +++ b/api/scripts/update-codegen.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +# Copyright 2024 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -o errexit +set -o nounset +set -o pipefail + +function usage { + cat <:`. + 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..75bd871 --- /dev/null +++ b/crds/helmclusteraddoncharts.yaml @@ -0,0 +1,133 @@ +--- +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 from specific repository. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + properties: + conditions: + description: + Conditions represent the latest available observations + of the repository state. + items: + description: + Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + versions: + description: Available helm chart versions + items: + properties: + digest: + description: Helm chart digest + minLength: 1 + type: string + pulled: + description: Chart pulled from repository + type: boolean + version: + description: Helm chart version + minLength: 1 + type: string + required: + - digest + - pulled + - version + type: object + type: array + 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..de1b5ba --- /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 Git, Helm or 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: + Generating a resource that was last processed by the + controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/crds/helmclusteraddons.yaml b/crds/helmclusteraddons.yaml new file mode 100644 index 0000000..46bfe43 --- /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 repository + jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Status + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: + HelmClusterAddon represents a Helm addon that is installed across + the whole cluster. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + chart: + properties: + helmClusterAddonChart: + 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 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 + lastAppliedChart: + description: + LastAppliedChart represents the latest chart that triggered + addon install or update. + properties: + helmClusterAddonChart: + description: |- + Specifies the name of the Helm chart to be installed + from the defined repository (e.g., "ingress-nginx" or "redis"). + type: string + helmClusterAddonRepository: + description: |- + Specifies the name of the HelmClusterAddonRepository custom resource that contains + the connection details and credentials for the repository where + the chart is located. + type: string + version: + description: Versions holds the HelmClusterAddon chart version. + type: string + type: object + lastAppliedValues: + description: + LastAppliedValues represents the latest values that triggered + addon install or update. + x-kubernetes-preserve-unknown-fields: true + observedGeneration: + description: + Generating a resource that was last processed by the + 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..2e159c7 --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,4 @@ +--- +title: "Module configuration" +weight: 30 +--- diff --git a/docs/CONFIGURATION_RU.md b/docs/CONFIGURATION_RU.md new file mode 100644 index 0000000..2e159c7 --- /dev/null +++ b/docs/CONFIGURATION_RU.md @@ -0,0 +1,4 @@ +--- +title: "Module configuration" +weight: 30 +--- diff --git a/docs/CR.md b/docs/CR.md new file mode 100644 index 0000000..4f1f169 --- /dev/null +++ b/docs/CR.md @@ -0,0 +1,4 @@ +--- +title: "Custom Resources" +weight: 60 +--- diff --git a/docs/CR_RU.md b/docs/CR_RU.md new file mode 100644 index 0000000..4f1f169 --- /dev/null +++ b/docs/CR_RU.md @@ -0,0 +1,4 @@ +--- +title: "Custom Resources" +weight: 60 +--- diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..aa916b9 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,16 @@ +--- +title: "operator-helm" +menuTitle: "operator-helm" +moduleStatus: General Availability +weight: 10 +--- + +The `operator-helm` module allows you to declaratively manage Helm applications and associated resources. + +## Usage scenarios + + + +## Architecture + + diff --git a/docs/README_RU.md b/docs/README_RU.md new file mode 100644 index 0000000..e88efe5 --- /dev/null +++ b/docs/README_RU.md @@ -0,0 +1,16 @@ +--- +title: "operator-helm" +menuTitle: "operator-helm" +moduleStatus: General Availability +weight: 10 +--- + +Модуль `operator-helm` позволяет декларативно управлять Helm приложениями и связанными с ними ресурсами. + +## Сценарии использования + + + +## Архитектура + + diff --git a/docs/images/.keep b/docs/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/internal/components_placement.md b/docs/internal/components_placement.md new file mode 100644 index 0000000..17bc1d2 --- /dev/null +++ b/docs/internal/components_placement.md @@ -0,0 +1,3 @@ +## Placement strategies + + diff --git a/hooks/.keep b/hooks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/images/distroless/werf.inc.yaml b/images/distroless/werf.inc.yaml new file mode 100644 index 0000000..99f6e11 --- /dev/null +++ b/images/distroless/werf.inc.yaml @@ -0,0 +1,53 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }}-artifact +fromImage: builder/alt +final: false +shell: + beforeInstall: + {{- include "alt packages proxy" . | nindent 2 }} + - | + apt-get install ca-certificates tzdata -y + {{- include "alt packages clean" . | nindent 2 }} + install: + - | + mkdir -p /relocate/etc/{pki,ssl} /relocate/usr/{bin,sbin,share,lib,lib64} + + cd /relocate + for dir in {bin,sbin,lib,lib64};do + ln -s usr/$dir $dir + done + # /var/run -> ../run symlink to prevent making /var/run a directory during the build. + # It is needed for better compatibility with containerd default top layer. + mkdir -p run + mkdir -p var + ln -s var/run ../run + cd / + + cp -pr /tmp /relocate + cp -pr /etc/passwd /etc/group /etc/hostname /etc/hosts /etc/shadow /etc/protocols /etc/services /etc/nsswitch.conf /relocate/etc + cp -pr /usr/share/ca-certificates /relocate/usr/share + cp -pr /usr/share/zoneinfo /relocate/usr/share + cp -pr /etc/pki/tls/cert.pem /relocate/etc/ssl + cp -pr /etc/pki/tls/certs /relocate/etc/ssl + cp -pr /etc/pki/ca-trust/ /relocate/etc/ + # Create 'deckhouse' user to run without root. + echo "deckhouse:x:64535:64535:deckhouse:/:/sbin/nologin" >> /relocate/etc/passwd + echo "deckhouse:x:64535:" >> /relocate/etc/group + echo "deckhouse:!::0:::::" >> /relocate/etc/shadow +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +final: false +fromImage: builder/scratch +import: + - image: {{ .ModuleNamePrefix }}{{ .ImageName }}-artifact + add: /relocate + to: / + before: setup +imageSpec: + config: + env: + PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + LANG: "" + LC_ALL: POSIX + user: 64535 + diff --git a/images/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/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..6404416 --- /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.0 + +require ( + github.com/DataDog/gostackparse v0.7.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/caarlos0/env/v11 v11.3.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudflare/cfssl v1.6.5 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckhouse/deckhouse/pkg/log v0.2.0 // indirect + github.com/docker/cli v28.2.2+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/emicklei/go-restful/v3 v3.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..dbc33f6 --- /dev/null +++ b/images/hooks/go.sum @@ -0,0 +1,273 @@ +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.0 h1:VPhYvMVQ3pT32I2WL1ITtQyrYdpiUR0RocLw7S4TfNg= +github.com/deckhouse/module-sdk v0.10.0/go.mod h1:Z1jfmd0fICoYww0daMijWAU+OZTxeJUXfMciKKuYAYA= +github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= +github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= +github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.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/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/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/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/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/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/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +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_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= +github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +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/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.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/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/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-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/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/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/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/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/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/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.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= +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/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/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= diff --git a/images/hooks/pkg/hooks/tls-certificates-controller/hook.go b/images/hooks/pkg/hooks/tls-certificates-controller/hook.go new file mode 100644 index 0000000..0265681 --- /dev/null +++ b/images/hooks/pkg/hooks/tls-certificates-controller/hook.go @@ -0,0 +1,40 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tls_certificates_controller + +import ( + "fmt" + "hooks/pkg/settings" + + tlscertificate "github.com/deckhouse/module-sdk/common-hooks/tls-certificate" +) + +var _ = tlscertificate.RegisterInternalTLSHookEM(tlscertificate.GenSelfSignedTLSHookConf{ + CN: settings.ControllerCertCN, + TLSSecretName: "operator-helm-controller-tls", + Namespace: settings.ModuleNamespace, + SANs: tlscertificate.DefaultSANs([]string{ + "localhost", + "127.0.0.1", + settings.ControllerCertCN, + fmt.Sprintf("%s.%s", settings.ControllerCertCN, settings.ModuleNamespace), + fmt.Sprintf("%s.%s.svc", settings.ControllerCertCN, settings.ModuleNamespace), + }), + + FullValuesPathPrefix: fmt.Sprintf("%s.internal.controller.cert", settings.ModuleName), + CommonCAValuesPath: fmt.Sprintf("%s.internal.rootCA", settings.ModuleName), +}) diff --git a/images/hooks/pkg/settings/certificate.go b/images/hooks/pkg/settings/certificate.go new file mode 100644 index 0000000..5d08591 --- /dev/null +++ b/images/hooks/pkg/settings/certificate.go @@ -0,0 +1,21 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package settings + +const ( + ControllerCertCN string = "operator-helm-controller" +) diff --git a/images/hooks/pkg/settings/module.go b/images/hooks/pkg/settings/module.go new file mode 100644 index 0000000..095d744 --- /dev/null +++ b/images/hooks/pkg/settings/module.go @@ -0,0 +1,23 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package settings + +// Essential module constants. +const ( + ModuleNamespace string = "d8-operator-helm" + ModuleName string = "operatorHelm" +) diff --git a/images/hooks/werf.inc.yaml b/images/hooks/werf.inc.yaml new file mode 100644 index 0000000..ade8485 --- /dev/null +++ b/images/hooks/werf.inc.yaml @@ -0,0 +1,52 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact +final: false +fromImage: builder/src +git: + - add: {{ .ModuleDir }}/images/{{ .ImageName }} + to: /src/images/hooks + stageDependencies: + install: + - "**/*" + - add: {{ .ModuleDir }}/images/operator-helm-artifact + to: /src/images/operator-helm-artifact + stageDependencies: + install: + - "**/*" + - add: {{ .ModuleDir }}/api + to: /src/api + stageDependencies: + install: + - "**/*" +shell: + install: + - cd /src +--- +image: {{ .ModuleNamePrefix }}go-hooks-artifact +final: false +fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-bookworm-1.25" "builder/golang-alt-svace-1.25" }} +import: + - image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact + add: /src + to: /app + before: install +mount: + - fromPath: ~/go-pkg-cache + to: /go/pkg +secrets: + - id: GOPROXY + value: {{ .GOPROXY }} +shell: + install: + - export GOPROXY=$(cat /run/secrets/GOPROXY) + - cd /app/images/hooks + - go mod download + setup: + - cd /app/images/hooks + - | + export GOOS=linux + export GOARCH=amd64 + export CGO_ENABLED=0 + export TAGS="{{ printf "-tags %s" .MODULE_EDITION }}" + {{- $_ := set $ "ProjectName" (list $.ImageName "hooks" | join "/") }} + {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w" $TAGS -a -o /go-hooks/operator-helm-module-hooks ./cmd/operator-helm-module-hooks`) | nindent 6 }} diff --git a/images/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..3a02526 --- /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: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-bookworm-1.25" "builder/golang-alt-svace-1.25" }} +import: + - image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact + add: /src + to: /src + before: install +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/cmd/operator-helm-controller/main.go b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go new file mode 100644 index 0000000..9c5e1f5 --- /dev/null +++ b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go @@ -0,0 +1,116 @@ +/* +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" + + "github.com/deckhouse/operator-helm/pkg/controller/helmclusteraddonchart" + helmv2 "github.com/werf/3p-helm-controller/api/v2" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/pkg/controller/helmclusteraddon" + "github.com/deckhouse/operator-helm/pkg/controller/helmclusteraddonrepository" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" +) + +var scheme = runtime.NewScheme() + +func init() { + _ = clientgoscheme.AddToScheme(scheme) + _ = helmv1alpha1.AddToScheme(scheme) + _ = sourcev1.AddToScheme(scheme) + _ = 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.") + + // TODO: replace zap by deckhouse logger + + opts := zap.Options{Development: false} + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + logger := ctrl.Log.WithName("setup") + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: metricsAddr, + }, + HealthProbeBindAddress: healthProbeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "operator-helm-controller.helm.deckhouse.io", + }) + if err != nil { + logger.Error(err, "unable to create manager") + os.Exit(1) + } + + if err := helmclusteraddonrepository.SetupWithManager(mgr); err != nil { + logger.Error(err, "unable to setup HelmClusterAddonRepository controller") + os.Exit(1) + } + + if err := helmclusteraddon.SetupWithManager(mgr); err != nil { + logger.Error(err, "unable to setup HelmClusterAddon controller") + os.Exit(1) + } + + if err = (&helmv1alpha1.HelmClusterAddon{}).SetupWebhookWithManager(mgr); err != nil { + logger.Error(err, "unable to create webhook", "webhook", "HelmClusterAddon") + os.Exit(1) + } + + if err := helmclusteraddonchart.SetupWithManager(mgr); err != nil { + logger.Error(err, "unable to setup HelmClusterAddonChart controller") + os.Exit(1) + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + logger.Error(err, "unable to set up health check") + os.Exit(1) + } + 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..0eb2c1b --- /dev/null +++ b/images/operator-helm-artifact/go.mod @@ -0,0 +1,83 @@ +module github.com/deckhouse/operator-helm + +go 1.25.0 + +replace github.com/deckhouse/operator-helm/api => ../../api + +require ( + github.com/deckhouse/operator-helm/api v0.0.0-00010101000000-000000000000 + github.com/opencontainers/go-digest v1.0.0 + github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 + github.com/werf/3p-fluxcd-pkg/chartutil v1.17.0-nelm.1 + github.com/werf/3p-helm-controller/api v0.1.4 + github.com/werf/nelm-source-controller/api v0.1.4 + go.yaml.in/yaml/v3 v3.0.4 + helm.sh/helm/v3 v3.19.2 + k8s.io/api v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 + k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 + sigs.k8s.io/controller-runtime v0.23.1 +) + +require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cyphar/filepath-securejoin v0.6.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + 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/mailru/easyjson v0.9.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 // indirect + github.com/werf/3p-fluxcd-pkg/apis/kustomize v1.14.0-nelm.1 // indirect + 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 + k8s.io/apiextensions-apiserver v0.35.1 // indirect + k8s.io/klog/v2 v2.130.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/operator-helm-artifact/go.sum b/images/operator-helm-artifact/go.sum new file mode 100644 index 0000000..e94cb26 --- /dev/null +++ b/images/operator-helm-artifact/go.sum @@ -0,0 +1,189 @@ +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/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= +github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/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/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/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/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 h1:b1P4avYWjjWuzPSOv6QZtk1ffl/iBfWBGK4qNAxaA94= +github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1/go.mod h1:00dBUg4SN+4Xu4LWrbQm5LdmRKVP9Fjbvb+rvqjHrVI= +github.com/werf/3p-fluxcd-pkg/apis/kustomize v1.14.0-nelm.1 h1:edZ5ugpeUvmjG+g9laet8qTBqDdQPl18aNr6k0xqdYY= +github.com/werf/3p-fluxcd-pkg/apis/kustomize v1.14.0-nelm.1/go.mod h1:dAboSMVeohict/XrpXrqyZodq+8Qp6dwafzkBzoCHcU= +github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 h1:rYX8cMeryBHH7sNPVSQm1IAVES08TiWvADaZsDj98Wk= +github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1/go.mod h1:14co1+Ub5rW0Bp3Qo4IzCHwEcaw06StyMu7Rv5pMVCY= +github.com/werf/3p-fluxcd-pkg/chartutil v1.17.0-nelm.1 h1:ua0xt66rxKptzbG1zxy3u96qfV8XsFT9Jd2PU8L6mc8= +github.com/werf/3p-fluxcd-pkg/chartutil v1.17.0-nelm.1/go.mod h1:fodaCyMGXGxYYSIdWvokrjki8e+DAhgu6BtzHbH2VJ8= +github.com/werf/3p-helm-controller/api v0.1.4 h1:s7g9UQOrDMUzVE+JtWOP2xApnPOKYlNe1tXkkWCisAw= +github.com/werf/3p-helm-controller/api v0.1.4/go.mod h1:tiPvDerlc5SwKIDmXB8L3kIMJHse+wigueoEGQq+588= +github.com/werf/nelm-source-controller/api v0.1.4 h1:/k3RT+hHdwKHntoebdcjhO+zboJIlljHJZlbcumoY08= +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= +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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +helm.sh/helm/v3 v3.19.2 h1:psQjaM8aIWrSVEly6PgYtLu/y6MRSmok4ERiGhZmtUY= +helm.sh/helm/v3 v3.19.2/go.mod h1:gX10tB5ErM+8fr7bglUUS/UfTOO8UUTYWIBH1IYNnpE= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +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/pkg/api/openapi/zz_generated.openapi.go b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go new file mode 100644 index 0000000..4acd2b1 --- /dev/null +++ b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go @@ -0,0 +1,3317 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +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 openapi-gen. DO NOT EDIT. + +package openapi + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + version "k8s.io/apimachinery/pkg/version" + common "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddon": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddon(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChart": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChart(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartList(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartRef": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartRef(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartSpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartSpec(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartStatus(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartVersion": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartVersion(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonLastAppliedChartRef": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonLastAppliedChartRef(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonList(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepository": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepository(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryAuth": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryAuth(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryList(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositorySpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositorySpec(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryStatus(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonSpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonSpec(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonValidator": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonValidator(ref), + v1.APIGroup{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroup(ref), + v1.APIGroupList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroupList(ref), + v1.APIResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResource(ref), + v1.APIResourceList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResourceList(ref), + v1.APIVersions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIVersions(ref), + v1.ApplyOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ApplyOptions(ref), + v1.Condition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Condition(ref), + v1.CreateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_CreateOptions(ref), + v1.DeleteOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_DeleteOptions(ref), + v1.Duration{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Duration(ref), + v1.FieldSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref), + v1.FieldsV1{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldsV1(ref), + v1.GetOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GetOptions(ref), + v1.GroupKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupKind(ref), + v1.GroupResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupResource(ref), + v1.GroupVersion{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersion(ref), + v1.GroupVersionForDiscovery{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), + v1.GroupVersionKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionKind(ref), + v1.GroupVersionResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionResource(ref), + v1.InternalEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_InternalEvent(ref), + v1.LabelSelector{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelector(ref), + v1.LabelSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), + v1.List{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_List(ref), + v1.ListMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListMeta(ref), + v1.ListOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListOptions(ref), + v1.ManagedFieldsEntry{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), + v1.MicroTime{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_MicroTime(ref), + v1.ObjectMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ObjectMeta(ref), + v1.OwnerReference{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_OwnerReference(ref), + v1.PartialObjectMetadata{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), + v1.PartialObjectMetadataList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), + v1.Patch{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Patch(ref), + v1.PatchOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PatchOptions(ref), + v1.Preconditions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Preconditions(ref), + v1.RootPaths{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_RootPaths(ref), + v1.ServerAddressByClientCIDR{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), + v1.Status{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Status(ref), + v1.StatusCause{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusCause(ref), + v1.StatusDetails{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusDetails(ref), + v1.Table{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Table(ref), + v1.TableColumnDefinition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableColumnDefinition(ref), + v1.TableOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableOptions(ref), + v1.TableRow{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRow(ref), + v1.TableRowCondition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRowCondition(ref), + v1.Time{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Time(ref), + v1.Timestamp{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Timestamp(ref), + v1.TypeMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TypeMeta(ref), + v1.UpdateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_UpdateOptions(ref), + v1.WatchEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_WatchEvent(ref), + version.Info{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_version_Info(ref), + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddon(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "HelmClusterAddon represents a Helm addon that is installed across the whole cluster.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonStatus"), + }, + }, + }, + Required: []string{"spec"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonSpec", "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonStatus", v1.ObjectMeta{}.OpenAPIModelName()}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChart(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "HelmClusterAddonChart represents a Helm chart from specific repository.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartStatus"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartStatus", v1.ObjectMeta{}.OpenAPIModelName()}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "HelmClusterAddonChartList contains a list of HelmClusterAddonCharts.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Description: "Items provides a list of HelmClusterAddonCharts.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChart"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChart", v1.ListMeta{}.OpenAPIModelName()}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartRef(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "helmClusterAddonChart": { + SchemaProps: spec.SchemaProps{ + Description: "Specifies the name of the Helm chart to be installed from the defined repository (e.g., \"ingress-nginx\" or \"redis\").", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "helmClusterAddonRepository": { + SchemaProps: spec.SchemaProps{ + Description: "Specifies the name of the HelmClusterAddonRepository custom resource that contains the connection details and credentials for the repository where the chart is located.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Description: "Versions holds the HelmClusterAddon chart version.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"helmClusterAddonChart", "helmClusterAddonRepository"}, + }, + }, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "chartName": { + SchemaProps: spec.SchemaProps{ + Description: "Helm chart name", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "repositoryName": { + SchemaProps: spec.SchemaProps{ + Description: "Name of HelmClusterAddonRepository where respective helm chart resides.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"chartName", "repositoryName"}, + }, + }, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "conditions": { + SchemaProps: spec.SchemaProps{ + Description: "Conditions represent the latest available observations of the repository state.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.Condition{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "versions": { + SchemaProps: spec.SchemaProps{ + Description: "Available helm chart versions", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartVersion"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartVersion", v1.Condition{}.OpenAPIModelName()}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartVersion(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "version": { + SchemaProps: spec.SchemaProps{ + Description: "Helm chart version", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "digest": { + SchemaProps: spec.SchemaProps{ + Description: "Helm chart digest", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "pulled": { + SchemaProps: spec.SchemaProps{ + Description: "Chart pulled from repository", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"version", "digest", "pulled"}, + }, + }, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonLastAppliedChartRef(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "helmClusterAddonChart": { + SchemaProps: spec.SchemaProps{ + Description: "Specifies the name of the Helm chart to be installed from the defined repository (e.g., \"ingress-nginx\" or \"redis\").", + Type: []string{"string"}, + Format: "", + }, + }, + "helmClusterAddonRepository": { + SchemaProps: spec.SchemaProps{ + Description: "Specifies the name of the HelmClusterAddonRepository custom resource that contains the connection details and credentials for the repository where the chart is located.", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Description: "Versions holds the HelmClusterAddon chart version.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "HelmClusterAddonList contains a list of HelmClusterAddons.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Description: "Items provides a list of HelmClusterAddons.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddon"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddon", v1.ListMeta{}.OpenAPIModelName()}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepository(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "HelmClusterAddonRepository represents a Git, Helm or OCI compliant repository with Helm charts.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositorySpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryStatus"), + }, + }, + }, + Required: []string{"spec"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositorySpec", "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryStatus", v1.ObjectMeta{}.OpenAPIModelName()}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryAuth(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "username": { + SchemaProps: spec.SchemaProps{ + Description: "Repository authentication username.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "password": { + SchemaProps: spec.SchemaProps{ + Description: "Repository authentication password.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"username", "password"}, + }, + }, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "HelmClusterAddonRepositoryList contains a list of HelmClusterRepositories.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Description: "Items provides a list of HelmClusterRepositories.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepository"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepository", v1.ListMeta{}.OpenAPIModelName()}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositorySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "url": { + SchemaProps: spec.SchemaProps{ + Description: "URL of the Helm repository. Supports http(s):// and oci:// protocols.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "auth": { + SchemaProps: spec.SchemaProps{ + Description: "Auth contains authentication credentials for the repository.", + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryAuth"), + }, + }, + "caCertificate": { + SchemaProps: spec.SchemaProps{ + Description: "CACertificate is the PEM encoded CA certificate for TLS verification.", + Type: []string{"string"}, + Format: "", + }, + }, + "tlsVerify": { + SchemaProps: spec.SchemaProps{ + Description: "TLSVerify enables or disables TLS certificate verification.", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"url"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryAuth"}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "conditions": { + SchemaProps: spec.SchemaProps{ + Description: "Conditions represent the latest available observations of the repository state.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.Condition{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "observedGeneration": { + SchemaProps: spec.SchemaProps{ + Description: "Generating a resource that was last processed by the controller.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.Condition{}.OpenAPIModelName()}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "chart": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartRef"), + }, + }, + "values": { + SchemaProps: spec.SchemaProps{ + Description: "Values holds the values for this HelmClusterAddon release.", + Ref: ref("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON"), + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace to deploy cluster addon release", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "maintenance": { + SchemaProps: spec.SchemaProps{ + Description: "Maintenance specifies the reconciliation strategy for the resource. When set to \"NoResourceReconciliation\", the controller will stop updating the underlying resources, allowing for manual intervention or maintenance without the operator overwriting changes. When empty (\"\"), standard reconciliation is active.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"chart"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartRef", "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON"}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastAppliedChart": { + SchemaProps: spec.SchemaProps{ + Description: "LastAppliedChart represents the latest chart that triggered addon install or update.", + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonLastAppliedChartRef"), + }, + }, + "lastAppliedValues": { + SchemaProps: spec.SchemaProps{ + Description: "LastAppliedValues represents the latest values that triggered addon install or update.", + Ref: ref("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON"), + }, + }, + "conditions": { + SchemaProps: spec.SchemaProps{ + Description: "Conditions represent the latest available observations of the repository state.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.Condition{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "observedGeneration": { + SchemaProps: spec.SchemaProps{ + Description: "Generating a resource that was last processed by the controller.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonLastAppliedChartRef", "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON", v1.Condition{}.OpenAPIModelName()}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonValidator(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "Client": { + SchemaProps: spec.SchemaProps{}, + }, + }, + Required: []string{"Client"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_APIGroup(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "APIGroup contains the name, the supported versions, and the preferred version of a group.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name is the name of the group.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "versions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "versions are the versions supported in this group.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.GroupVersionForDiscovery{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "preferredVersion": { + SchemaProps: spec.SchemaProps{ + Description: "preferredVersion is the version preferred by the API server, which probably is the storage version.", + Default: map[string]interface{}{}, + Ref: ref(v1.GroupVersionForDiscovery{}.OpenAPIModelName()), + }, + }, + "serverAddressByClientCIDRs": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "a map of client CIDR to server address that is serving this group. This is to help clients reach servers in the most network-efficient way possible. Clients can use the appropriate server address as per the CIDR that they match. In case of multiple matches, clients should use the longest matching CIDR. The server returns only those CIDRs that it thinks that the client can match. For example: the master will return an internal IP CIDR only, if the client reaches the server using an internal IP. Server looks at X-Forwarded-For header or X-Real-Ip header or request.RemoteAddr (in that order) to get the client IP.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.ServerAddressByClientCIDR{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + Required: []string{"name", "versions"}, + }, + }, + Dependencies: []string{ + v1.GroupVersionForDiscovery{}.OpenAPIModelName(), v1.ServerAddressByClientCIDR{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_APIGroupList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "APIGroupList is a list of APIGroup, to allow clients to discover the API at /apis.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "groups": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "groups is a list of APIGroup.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.APIGroup{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + Required: []string{"groups"}, + }, + }, + Dependencies: []string{ + v1.APIGroup{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_APIResource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "APIResource specifies the name of a resource and whether it is namespaced.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name is the plural name of the resource.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "singularName": { + SchemaProps: spec.SchemaProps{ + Description: "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "namespaced": { + SchemaProps: spec.SchemaProps{ + Description: "namespaced indicates if a resource is namespaced or not.", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "group": { + SchemaProps: spec.SchemaProps{ + Description: "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Description: "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "verbs": { + SchemaProps: spec.SchemaProps{ + Description: "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "shortNames": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "shortNames is a list of suggested short names of the resource.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "categories": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "storageVersionHash": { + SchemaProps: spec.SchemaProps{ + Description: "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "singularName", "namespaced", "kind", "verbs"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_APIResourceList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "groupVersion": { + SchemaProps: spec.SchemaProps{ + Description: "groupVersion is the group and version this APIResourceList is for.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "resources": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "resources contains the name of the resources and if they are namespaced.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.APIResource{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + Required: []string{"groupVersion", "resources"}, + }, + }, + Dependencies: []string{ + v1.APIResource{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_APIVersions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "APIVersions lists the versions that are available, to allow clients to discover the API at /api, which is the root path of the legacy v1 API.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "versions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "versions are the api versions that are available.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "serverAddressByClientCIDRs": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "a map of client CIDR to server address that is serving this group. This is to help clients reach servers in the most network-efficient way possible. Clients can use the appropriate server address as per the CIDR that they match. In case of multiple matches, clients should use the longest matching CIDR. The server returns only those CIDRs that it thinks that the client can match. For example: the master will return an internal IP CIDR only, if the client reaches the server using an internal IP. Server looks at X-Forwarded-For header or X-Real-Ip header or request.RemoteAddr (in that order) to get the client IP.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.ServerAddressByClientCIDR{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + Required: []string{"versions", "serverAddressByClientCIDRs"}, + }, + }, + Dependencies: []string{ + v1.ServerAddressByClientCIDR{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_ApplyOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ApplyOptions may be provided when applying an API object. FieldManager is required for apply requests. ApplyOptions is equivalent to PatchOptions. It is provided as a convenience with documentation that speaks specifically to how the options fields relate to apply.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "dryRun": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "force": { + SchemaProps: spec.SchemaProps{ + Description: "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people.", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "fieldManager": { + SchemaProps: spec.SchemaProps{ + Description: "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"force", "fieldManager"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_Condition(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Condition contains details for one aspect of the current state of this API Resource.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Description: "type of condition in CamelCase or in foo.example.com/CamelCase.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Description: "status of the condition, one of True, False, Unknown.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "observedGeneration": { + SchemaProps: spec.SchemaProps{ + 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.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "lastTransitionTime": { + SchemaProps: spec.SchemaProps{ + 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.", + Ref: ref(v1.Time{}.OpenAPIModelName()), + }, + }, + "reason": { + SchemaProps: spec.SchemaProps{ + 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.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Description: "message is a human readable message indicating details about the transition. This may be an empty string.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type", "status", "lastTransitionTime", "reason", "message"}, + }, + }, + Dependencies: []string{ + v1.Time{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_CreateOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "CreateOptions may be provided when creating an API object.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "dryRun": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "fieldManager": { + SchemaProps: spec.SchemaProps{ + Description: "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + Type: []string{"string"}, + Format: "", + }, + }, + "fieldValidation": { + SchemaProps: spec.SchemaProps{ + Description: "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_DeleteOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DeleteOptions may be provided when deleting an API object.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "gracePeriodSeconds": { + SchemaProps: spec.SchemaProps{ + Description: "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "preconditions": { + SchemaProps: spec.SchemaProps{ + Description: "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", + Ref: ref(v1.Preconditions{}.OpenAPIModelName()), + }, + }, + "orphanDependents": { + SchemaProps: spec.SchemaProps{ + Description: "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "propagationPolicy": { + SchemaProps: spec.SchemaProps{ + Description: "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + Type: []string{"string"}, + Format: "", + }, + }, + "dryRun": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "ignoreStoreReadErrorWithClusterBreakingPotential": { + SchemaProps: spec.SchemaProps{ + Description: "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.Preconditions{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_Duration(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Duration is a wrapper around time.Duration which supports correct marshaling to YAML and JSON. In particular, it marshals into strings, which can be used as map keys in json.", + Type: v1.Duration{}.OpenAPISchemaType(), + Format: v1.Duration{}.OpenAPISchemaFormat(), + }, + }, + } +} + +func schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "FieldSelectorRequirement is a selector that contains values, a key, and an operator that relates the key and values.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "key": { + SchemaProps: spec.SchemaProps{ + Description: "key is the field selector key that the requirement applies to.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "operator": { + SchemaProps: spec.SchemaProps{ + Description: "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. The list of operators may grow in the future.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "values": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"key", "operator"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_FieldsV1(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + Type: []string{"object"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GetOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GetOptions is the standard query options to the standard REST get call.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Description: "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupKind specifies a Group and a Kind, but does not force a version. This is useful for identifying concepts during lookup stages without having partially valid types", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"group", "kind"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupResource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupResource specifies a Group and a Resource, but does not force a version. This is useful for identifying concepts during lookup stages without having partially valid types", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "resource": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"group", "resource"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupVersion(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupVersion contains the \"group\" and the \"version\", which uniquely identifies the API.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"group", "version"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupVersion contains the \"group/version\" and \"version\" string of a version. It is made a struct to keep extensibility.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "groupVersion": { + SchemaProps: spec.SchemaProps{ + Description: "groupVersion specifies the API group and version in the form \"group/version\"", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Description: "version specifies the version in the form of \"version\". This is to save the clients the trouble of splitting the GroupVersion.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"groupVersion", "version"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupVersionKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupVersionKind unambiguously identifies a kind. It doesn't anonymously include GroupVersion to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"group", "version", "kind"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupVersionResource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "resource": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"group", "version", "resource"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_InternalEvent(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "InternalEvent makes watch.Event versioned", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "Type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "Object": { + SchemaProps: spec.SchemaProps{ + Description: "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Bookmark: the object (instance of a type being watched) where\n only ResourceVersion field is set. On successful restart of watch from a\n bookmark resourceVersion, client is guaranteed to not get repeat event\n nor miss any events.\n * If Type is Error: *api.Status is recommended; other types may make sense\n depending on context.", + }, + }, + }, + Required: []string{"Type", "Object"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_LabelSelector(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "matchLabels": { + SchemaProps: spec.SchemaProps{ + Description: "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "matchExpressions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.LabelSelectorRequirement{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, + }, + Dependencies: []string{ + v1.LabelSelectorRequirement{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "key": { + SchemaProps: spec.SchemaProps{ + Description: "key is the label key that the selector applies to.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "operator": { + SchemaProps: spec.SchemaProps{ + Description: "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "values": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"key", "operator"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_List(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "List holds a list of objects, which may not be known by the server.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Default: map[string]interface{}{}, + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Description: "List of objects", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + v1.ListMeta{}.OpenAPIModelName(), "k8s.io/apimachinery/pkg/runtime.RawExtension"}, + } +} + +func schema_pkg_apis_meta_v1_ListMeta(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "selfLink": { + SchemaProps: spec.SchemaProps{ + Description: "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Description: "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + Type: []string{"string"}, + Format: "", + }, + }, + "continue": { + SchemaProps: spec.SchemaProps{ + Description: "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", + Type: []string{"string"}, + Format: "", + }, + }, + "remainingItemCount": { + SchemaProps: spec.SchemaProps{ + Description: "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_ListOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ListOptions is the query options to a standard REST list call.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "labelSelector": { + SchemaProps: spec.SchemaProps{ + Description: "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + Type: []string{"string"}, + Format: "", + }, + }, + "fieldSelector": { + SchemaProps: spec.SchemaProps{ + Description: "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + Type: []string{"string"}, + Format: "", + }, + }, + "watch": { + SchemaProps: spec.SchemaProps{ + Description: "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "allowWatchBookmarks": { + SchemaProps: spec.SchemaProps{ + Description: "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Description: "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceVersionMatch": { + SchemaProps: spec.SchemaProps{ + Description: "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + Type: []string{"string"}, + Format: "", + }, + }, + "timeoutSeconds": { + SchemaProps: spec.SchemaProps{ + Description: "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "limit": { + SchemaProps: spec.SchemaProps{ + Description: "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "continue": { + SchemaProps: spec.SchemaProps{ + Description: "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + Type: []string{"string"}, + Format: "", + }, + }, + "sendInitialEvents": { + SchemaProps: spec.SchemaProps{ + Description: "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "manager": { + SchemaProps: spec.SchemaProps{ + Description: "Manager is an identifier of the workflow managing these fields.", + Type: []string{"string"}, + Format: "", + }, + }, + "operation": { + SchemaProps: spec.SchemaProps{ + Description: "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + Type: []string{"string"}, + Format: "", + }, + }, + "time": { + SchemaProps: spec.SchemaProps{ + Description: "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", + Ref: ref(v1.Time{}.OpenAPIModelName()), + }, + }, + "fieldsType": { + SchemaProps: spec.SchemaProps{ + Description: "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + Type: []string{"string"}, + Format: "", + }, + }, + "fieldsV1": { + SchemaProps: spec.SchemaProps{ + Description: "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", + Ref: ref(v1.FieldsV1{}.OpenAPIModelName()), + }, + }, + "subresource": { + SchemaProps: spec.SchemaProps{ + Description: "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.FieldsV1{}.OpenAPIModelName(), v1.Time{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_MicroTime(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "MicroTime is version of Time with microsecond level precision.", + Type: v1.MicroTime{}.OpenAPISchemaType(), + Format: v1.MicroTime{}.OpenAPISchemaFormat(), + }, + }, + } +} + +func schema_pkg_apis_meta_v1_ObjectMeta(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + Type: []string{"string"}, + Format: "", + }, + }, + "generateName": { + SchemaProps: spec.SchemaProps{ + Description: "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + Type: []string{"string"}, + Format: "", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + Type: []string{"string"}, + Format: "", + }, + }, + "selfLink": { + SchemaProps: spec.SchemaProps{ + Description: "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Description: "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + Type: []string{"string"}, + Format: "", + }, + }, + "generation": { + SchemaProps: spec.SchemaProps{ + Description: "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "creationTimestamp": { + SchemaProps: spec.SchemaProps{ + Description: "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + Ref: ref(v1.Time{}.OpenAPIModelName()), + }, + }, + "deletionTimestamp": { + SchemaProps: spec.SchemaProps{ + Description: "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + Ref: ref(v1.Time{}.OpenAPIModelName()), + }, + }, + "deletionGracePeriodSeconds": { + SchemaProps: spec.SchemaProps{ + Description: "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "labels": { + SchemaProps: spec.SchemaProps{ + Description: "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "annotations": { + SchemaProps: spec.SchemaProps{ + Description: "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "ownerReferences": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "uid", + }, + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.OwnerReference{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "finalizers": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "managedFields": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.ManagedFieldsEntry{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.ManagedFieldsEntry{}.OpenAPIModelName(), v1.OwnerReference{}.OpenAPIModelName(), v1.Time{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_OwnerReference(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "API version of the referent.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "controller": { + SchemaProps: spec.SchemaProps{ + Description: "If true, this reference points to the managing controller.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "blockOwnerDeletion": { + SchemaProps: spec.SchemaProps{ + Description: "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"apiVersion", "kind", "name", "uid"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_PartialObjectMetadata(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PartialObjectMetadata is a generic representation of any object with ObjectMeta. It allows clients to get access to a particular ObjectMeta schema without knowing the details of the version.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + Default: map[string]interface{}{}, + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.ObjectMeta{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PartialObjectMetadataList contains a list of objects containing only their metadata", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Default: map[string]interface{}{}, + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Description: "items contains each of the included items.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.PartialObjectMetadata{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + v1.ListMeta{}.OpenAPIModelName(), v1.PartialObjectMetadata{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_Patch(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", + Type: []string{"object"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_PatchOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PatchOptions may be provided when patching an API object. PatchOptions is meant to be a superset of UpdateOptions.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "dryRun": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "force": { + SchemaProps: spec.SchemaProps{ + Description: "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "fieldManager": { + SchemaProps: spec.SchemaProps{ + Description: "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + Type: []string{"string"}, + Format: "", + }, + }, + "fieldValidation": { + SchemaProps: spec.SchemaProps{ + Description: "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_Preconditions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "Specifies the target UID.", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Description: "Specifies the target ResourceVersion", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_RootPaths(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "RootPaths lists the paths available at root. For example: \"/healthz\", \"/apis\".", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "paths": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "paths are the paths available at root.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"paths"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ServerAddressByClientCIDR helps the client to determine the server address that they should use, depending on the clientCIDR that they match.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "clientCIDR": { + SchemaProps: spec.SchemaProps{ + Description: "The CIDR with which clients can match their IP to figure out the server address that they should use.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "serverAddress": { + SchemaProps: spec.SchemaProps{ + Description: "Address of this server, suitable for a client that matches the above CIDR. This can be a hostname, hostname:port, IP or IP:port.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"clientCIDR", "serverAddress"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_Status(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Status is a return value for calls that don't return other objects.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Default: map[string]interface{}{}, + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Description: "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Description: "A human-readable description of the status of this operation.", + Type: []string{"string"}, + Format: "", + }, + }, + "reason": { + SchemaProps: spec.SchemaProps{ + Description: "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", + Ref: ref(v1.StatusDetails{}.OpenAPIModelName()), + }, + }, + "code": { + SchemaProps: spec.SchemaProps{ + Description: "Suggested HTTP return code for this status, 0 if not set.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.ListMeta{}.OpenAPIModelName(), v1.StatusDetails{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_StatusCause(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "reason": { + SchemaProps: spec.SchemaProps{ + Description: "A machine-readable description of the cause of the error. If this value is empty there is no information available.", + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Description: "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", + Type: []string{"string"}, + Format: "", + }, + }, + "field": { + SchemaProps: spec.SchemaProps{ + Description: "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_StatusDetails(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", + Type: []string{"string"}, + Format: "", + }, + }, + "group": { + SchemaProps: spec.SchemaProps{ + Description: "The group attribute of the resource associated with the status StatusReason.", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + Type: []string{"string"}, + Format: "", + }, + }, + "causes": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.StatusCause{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "retryAfterSeconds": { + SchemaProps: spec.SchemaProps{ + Description: "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.StatusCause{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_Table(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Table is a tabular representation of a set of API resources. The server transforms the object into a set of preferred columns for quickly reviewing the objects.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Default: map[string]interface{}{}, + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), + }, + }, + "columnDefinitions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "columnDefinitions describes each column in the returned items array. The number of cells per row will always match the number of column definitions.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.TableColumnDefinition{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "rows": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "rows is the list of items in the table.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.TableRow{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + Required: []string{"columnDefinitions", "rows"}, + }, + }, + Dependencies: []string{ + v1.ListMeta{}.OpenAPIModelName(), v1.TableColumnDefinition{}.OpenAPIModelName(), v1.TableRow{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_TableColumnDefinition(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TableColumnDefinition contains information about a column returned in the Table.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name is a human readable name for the column.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "type is an OpenAPI type definition for this column, such as number, integer, string, or array. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "format": { + SchemaProps: spec.SchemaProps{ + Description: "format is an optional OpenAPI type modifier for this column. A format modifies the type and imposes additional rules, like date or time formatting for a string. The 'name' format is applied to the primary identifier column which has type 'string' to assist in clients identifying column is the resource name. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "description is a human readable description of this column.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "priority": { + SchemaProps: spec.SchemaProps{ + Description: "priority is an integer defining the relative importance of this column compared to others. Lower numbers are considered higher priority. Columns that may be omitted in limited space scenarios should be given a higher priority.", + Default: 0, + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + Required: []string{"name", "type", "format", "description", "priority"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_TableOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TableOptions are used when a Table is requested by the caller.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "includeObject": { + SchemaProps: spec.SchemaProps{ + Description: "includeObject decides whether to include each object along with its columnar information. Specifying \"None\" will return no object, specifying \"Object\" will return the full object contents, and specifying \"Metadata\" (the default) will return the object's metadata in the PartialObjectMetadata kind in version v1beta1 of the meta.k8s.io API group.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_TableRow(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TableRow is an individual row in a table.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "cells": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "cells will be as wide as the column definitions array and may contain strings, numbers (float64 or int64), booleans, simple maps, lists, or null. See the type field of the column definition for a more detailed description.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + "conditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "conditions describe additional status of a row that are relevant for a human user. These conditions apply to the row, not to the object, and will be specific to table output. The only defined condition type is 'Completed', for a row that indicates a resource that has run to completion and can be given less visual priority.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.TableRowCondition{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "object": { + SchemaProps: spec.SchemaProps{ + Description: "This field contains the requested additional information about each object based on the includeObject policy when requesting the Table. If \"None\", this field is empty, if \"Object\" this will be the default serialization of the object for the current API version, and if \"Metadata\" (the default) will contain the object metadata. Check the returned kind and apiVersion of the object before parsing. The media type of the object will always match the enclosing list - if this as a JSON table, these will be JSON encoded objects.", + Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + }, + }, + }, + Required: []string{"cells"}, + }, + }, + Dependencies: []string{ + v1.TableRowCondition{}.OpenAPIModelName(), "k8s.io/apimachinery/pkg/runtime.RawExtension"}, + } +} + +func schema_pkg_apis_meta_v1_TableRowCondition(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TableRowCondition allows a row to be marked with additional information.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Description: "Type of row condition. The only defined value is 'Completed' indicating that the object this row represents has reached a completed state and may be given less visual priority than other rows. Clients are not required to honor any conditions but should be consistent where possible about handling the conditions.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Description: "Status of the condition, one of True, False, Unknown.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "reason": { + SchemaProps: spec.SchemaProps{ + Description: "(brief) machine readable reason for the condition's last transition.", + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Description: "Human readable message indicating details about last transition.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type", "status"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_Time(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + Type: v1.Time{}.OpenAPISchemaType(), + Format: v1.Time{}.OpenAPISchemaFormat(), + }, + }, + } +} + +func schema_pkg_apis_meta_v1_Timestamp(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Timestamp is a struct that is equivalent to Time, but intended for protobuf marshalling/unmarshalling. It is generated into a serialization that matches Time. Do not use in Go structs.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "seconds": { + SchemaProps: spec.SchemaProps{ + Description: "Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive.", + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "nanos": { + SchemaProps: spec.SchemaProps{ + Description: "Non-negative fractions of a second at nanosecond resolution. Negative second values with fractions must still have non-negative nanos values that count forward in time. Must be from 0 to 999,999,999 inclusive. This field may be limited in precision depending on context.", + Default: 0, + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + Required: []string{"seconds", "nanos"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_TypeMeta(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TypeMeta describes an individual object in an API response or request with strings representing the type of the object and its API schema version. Structures that are versioned or persisted should inline TypeMeta.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_UpdateOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "UpdateOptions may be provided when updating an API object. All fields in UpdateOptions should also be present in PatchOptions.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "dryRun": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "fieldManager": { + SchemaProps: spec.SchemaProps{ + Description: "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + Type: []string{"string"}, + Format: "", + }, + }, + "fieldValidation": { + SchemaProps: spec.SchemaProps{ + Description: "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_WatchEvent(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Event represents a single event to a watched resource.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "object": { + SchemaProps: spec.SchemaProps{ + Description: "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", + Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + }, + }, + }, + Required: []string{"type", "object"}, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/runtime.RawExtension"}, + } +} + +func schema_k8sio_apimachinery_pkg_version_Info(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Info contains versioning information. how we'll want to distribute that information.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "major": { + SchemaProps: spec.SchemaProps{ + Description: "Major is the major version of the binary version", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "minor": { + SchemaProps: spec.SchemaProps{ + Description: "Minor is the minor version of the binary version", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "emulationMajor": { + SchemaProps: spec.SchemaProps{ + Description: "EmulationMajor is the major version of the emulation version", + Type: []string{"string"}, + Format: "", + }, + }, + "emulationMinor": { + SchemaProps: spec.SchemaProps{ + Description: "EmulationMinor is the minor version of the emulation version", + Type: []string{"string"}, + Format: "", + }, + }, + "minCompatibilityMajor": { + SchemaProps: spec.SchemaProps{ + Description: "MinCompatibilityMajor is the major version of the minimum compatibility version", + Type: []string{"string"}, + Format: "", + }, + }, + "minCompatibilityMinor": { + SchemaProps: spec.SchemaProps{ + Description: "MinCompatibilityMinor is the minor version of the minimum compatibility version", + Type: []string{"string"}, + Format: "", + }, + }, + "gitVersion": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "gitCommit": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "gitTreeState": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "buildDate": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "goVersion": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "compiler": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "platform": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"major", "minor", "gitVersion", "gitCommit", "gitTreeState", "buildDate", "goVersion", "compiler", "platform"}, + }, + }, + } +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go new file mode 100644 index 0000000..0c9ea8e --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go @@ -0,0 +1,73 @@ +/* +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 "time" + +const ( + // ControllerName is the name of this controller, used for leader election and logging. + ControllerName = "helmclusteraddon-controller" + + // TargetNamespace is the namespace where internal customer resources are created. + TargetNamespace = "d8-operator-helm" + + // FinalizerName is the finalizer added to HelmClusterRepository to ensure cleanup. + FinalizerName = "helm.deckhouse.io/cleanup" + + ConditionTypeReady = "Ready" + ConditionTypeManaged = "Managed" + ConditionTypeInstalled = "Installed" + ConditionTypeUpdateInstalled = "UpdateInstalled" + ConditionTypeConfigurationApplied = "ConfigurationApplied" + ConditionTypePartiallyDegraded = "PartiallyDegraded" + + ReasonInitializing = "Initializing" + ReasonUnmanagedModeActivated = "UnmanagedModeActivated" + ReasonManagedModeActivated = "ManagedModeActivated" + ReasonUpdateSucceeded = "UpdateSucceeded" + ReasonInstallSucceeded = "InstallSucceeded" + ReasonInstallationInProgress = "InstallationInProgress" + ReasonDownloading = "Downloading" + ReasonDownloadWasFailed = "DownloadWasFailed" + ReasonUpdateInProgress = "UpdateInProgress" + ReasonInstallFailed = "InstallFailed" + ReasonUpdateFailed = "UpdateFailed" + + // ReasonMirrorFailed indicates the internal HelmRepository create/update failed. + ReasonMirrorFailed = "MirrorFailed" + + // ReasonCleanupFailed indicates deletion of the internal HelmRepository failed. + ReasonCleanupFailed = "CleanupFailed" + + // ReasonProcessing indicates that facade resource is processing. + ReasonProcessing = "Processing" + + // LabelManagedBy marks resources as managed by this controller. + LabelManagedBy = "helm.deckhouse.io/managed-by" + + // LabelManagedByValue is the value for the managed-by label. + LabelManagedByValue = "operator-helm" + + // LabelSourceName stores the name of the source facade resource. + LabelSourceName = "helm.deckhouse.io/cluster-addon" + + // InternalReleaseDeployed indicates that specific release fon internal chart release history was deployed + InternalReleaseDeployed = "deployed" + + // ChartPullInterval how ofter to check if specific version of facade chart was pulled. + ChartPullInterval = 2 * time.Second +) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go new file mode 100644 index 0000000..53946c4 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go @@ -0,0 +1,49 @@ +/* +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 ( + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/pkg/utils" + helmv2 "github.com/werf/3p-helm-controller/api/v2" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +func SetupWithManager(mgr ctrl.Manager) error { + r := &Reconciler{ + Client: mgr.GetClient(), + } + + return ctrl.NewControllerManagedBy(mgr). + Named(ControllerName). + For(&helmv1alpha1.HelmClusterAddon{}). + Watches( + &sourcev1.HelmChart{}, + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Watches( + &helmv2.HelmRelease{}, + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Complete(r) +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go new file mode 100644 index 0000000..2966c94 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go @@ -0,0 +1,548 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddon + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/deckhouse/operator-helm/pkg/controller/helmclusteraddonchart" + "github.com/deckhouse/operator-helm/pkg/utils" + "github.com/opencontainers/go-digest" + "github.com/werf/3p-fluxcd-pkg/chartutil" + helmv2 "github.com/werf/3p-helm-controller/api/v2" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + helmchartutil "helm.sh/helm/v3/pkg/chartutil" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" +) + +type Reconciler struct { + Client client.Client +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusteraddon", req.Name) + + var addon helmv1alpha1.HelmClusterAddon + + if err := r.Client.Get(ctx, req.NamespacedName, &addon); err != nil { + if apierrors.IsNotFound(err) { + logger.Info("HelmClusterAddon not found, skipping") + + return reconcile.Result{}, nil + } + + return reconcile.Result{}, fmt.Errorf("getting HelmClusterAddon: %w", err) + } + + if meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeReady) == nil { + return r.initializeConditions(ctx, &addon) + } + + if !addon.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, &addon) + } + + if !controllerutil.ContainsFinalizer(&addon, FinalizerName) { + controllerutil.AddFinalizer(&addon, FinalizerName) + + if err := r.Client.Update(ctx, &addon); err != nil { + return reconcile.Result{}, fmt.Errorf("adding finalizer: %w", err) + } + + return reconcile.Result{}, nil + } + + managedCond := meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeManaged) + if managedCond == nil { + return reconcile.Result{}, fmt.Errorf("managed condition is not initialized") + } else if managedCond.Status == metav1.ConditionFalse && addon.Spec.Maintenance == string(helmv1alpha1.NoResourceReconciliation) { + return reconcile.Result{}, nil + } + + var repo helmv1alpha1.HelmClusterAddonRepository + + if err := r.Client.Get(ctx, types.NamespacedName{Name: addon.Spec.Chart.HelmClusterAddonRepository}, &repo); err != nil { + if apierrors.IsNotFound(err) { + return reconcile.Result{}, r.patchStatusError(ctx, &addon, fmt.Errorf("repository not found: %w", err), ReasonMirrorFailed) + } + + return reconcile.Result{}, fmt.Errorf("getting HelmClusterAddonRepository: %w", err) + } + + if err := r.reconcileInternalHelmChart(ctx, &addon, &repo); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, &addon, fmt.Errorf("internal helm chart reconcile failed: %w", err), ReasonMirrorFailed) + } + + return r.reconcileInternalHelmRelease(ctx, &addon) +} + +func (r *Reconciler) reconcileInternalHelmChart(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, repo *helmv1alpha1.HelmClusterAddonRepository) error { + logger := log.FromContext(ctx) + + repoType, _ := utils.GetRepositoryType(repo.Spec.URL) + + existing := &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.GetInternalHelmChartName(addon.Name), + Namespace: TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, r.Client, existing, func() error { + if existing.Labels == nil { + existing.Labels = map[string]string{} + } + + existing.Labels[LabelManagedBy] = LabelManagedByValue + existing.Labels[LabelSourceName] = addon.Name + existing.Labels[helmclusteraddonchart.LabelSourceName] = utils.GetHelmClusterAddonChartName( + repo.Name, addon.Spec.Chart.HelmClusterAddonChartName) + + existing.Spec.Chart = addon.Spec.Chart.HelmClusterAddonChartName + existing.Spec.Version = addon.Spec.Chart.Version + + switch repoType { + case utils.InternalHelmRepository: + existing.Spec.SourceRef = sourcev1.LocalHelmChartSourceReference{ + Kind: sourcev1.HelmRepositoryKind, + Name: addon.Spec.Chart.HelmClusterAddonRepository, + } + case utils.InternalOCIRepository: + existing.Spec.SourceRef = sourcev1.LocalHelmChartSourceReference{ + Kind: sourcev1.OCIRepositoryKind, + Name: addon.Spec.Chart.HelmClusterAddonRepository, + } + default: + return fmt.Errorf("invalid repository type: %s", repoType) + } + + return nil + }) + if err != nil { + return fmt.Errorf("cannot create or update internal release: %w", err) + } + + if op != controllerutil.OperationResultNone { + logger.Info("Successfully reconciled internal helm chart", "operation", op, "repository", repo.Name, "chart", addon.Spec.Chart.HelmClusterAddonChartName) + } + + reconcileCond := meta.FindStatusCondition(existing.Status.Conditions, "Reconciling") + if reconcileCond != nil { + if err := r.updateStatusOnInternalHelmChart(ctx, addon, metav1.ConditionFalse, reconcileCond.Reason, reconcileCond.Message, true); err != nil { + return fmt.Errorf("cannot update HelmClusterAddon status: %w", err) + } + + return reconcile.TerminalError(fmt.Errorf("internal helm chart %s is processing", existing.Name)) + } + + readyCond := meta.FindStatusCondition(existing.Status.Conditions, ConditionTypeReady) + if readyCond != nil && readyCond.Status == metav1.ConditionFalse { + if err := r.updateStatusOnInternalHelmChart(ctx, addon, metav1.ConditionFalse, readyCond.Reason, readyCond.Message, false); err != nil { + return fmt.Errorf("cannot update HelmClusterAddon status: %w", err) + } + + return reconcile.TerminalError(fmt.Errorf("internal helm chart %s is not ready", existing.Name)) + } + + return nil +} + +func (r *Reconciler) updateStatusOnInternalHelmChart(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, status metav1.ConditionStatus, reason, message string, inProgress bool) error { + base := addon.DeepCopy() + + installedCond := meta.FindStatusCondition(base.Status.Conditions, ConditionTypeInstalled) + updateInstalledCond := meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeUpdateInstalled) + if updateInstalledCond != nil || (installedCond != nil && installedCond.Status == metav1.ConditionTrue) { + if inProgress { + reason = ReasonUpdateInProgress + message = "" + } + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeUpdateInstalled, + Status: status, + Reason: reason, + Message: message, + }) + } else { + if inProgress { + reason = ReasonInstallationInProgress + message = "" + } + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeInstalled, + Status: status, + Reason: reason, + Message: message, + }) + } + + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeReady, + Status: status, + Reason: reason, + Message: message, + }) + + if err := r.Client.Status().Patch(ctx, addon, client.MergeFrom(base)); err != nil { + return fmt.Errorf("updating HelmClusterAddon status on success: %w", err) + } + + return nil +} + +func (r *Reconciler) reconcileInternalHelmRelease(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + var addonChart helmv1alpha1.HelmClusterAddonChart + + if err := r.Client.Get( + ctx, + types.NamespacedName{ + Name: utils.GetHelmClusterAddonChartName(addon.Spec.Chart.HelmClusterAddonRepository, + addon.Spec.Chart.HelmClusterAddonChartName), + }, + &addonChart, + ); err != nil { + if apierrors.IsNotFound(err) { + return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("addon chart not found: %w", err), ReasonMirrorFailed) + } + + return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("getting HelmClusterAddonChart: %w", err), ReasonMirrorFailed) + } + + var chartPulled bool + for _, chartInfo := range addonChart.Status.Versions { + if addon.Spec.Chart.Version == chartInfo.Version { + chartPulled = chartInfo.Pulled + } + } + + if !chartPulled { + return reconcile.Result{RequeueAfter: ChartPullInterval}, nil + } + + existing := &helmv2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.GetInternalHelmReleaseName(addon.Name), + Namespace: TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, r.Client, existing, func() error { + if existing.Labels == nil { + existing.Labels = map[string]string{} + } + + existing.Labels[LabelManagedBy] = LabelManagedByValue + existing.Labels[LabelSourceName] = 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 + } + + existing.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{ + Kind: sourcev1.HelmChartKind, + Name: utils.GetInternalHelmChartName(addon.Name), + Namespace: TargetNamespace, + } + + return nil + }) + if err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("reconcile internal helm release: %w", err), ReasonMirrorFailed) + } + + if op != controllerutil.OperationResultNone { + logger.Info("Successfully reconciled internal helm release", "operation", op, "chart", addon.Spec.Chart.HelmClusterAddonChartName) + } + + return r.updateStatusOnInternalRelease(ctx, addon, existing) +} + +func (r *Reconciler) ensureResourceDeleted(ctx context.Context, name, namespace string, obj client.Object) error { + if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, obj); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + + return fmt.Errorf("checking existence of obsolete resource: %w", err) + } + + if err := r.Client.Delete(ctx, obj); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("deleting obsolete resource: %w", err) + } + + return nil +} + +func (r *Reconciler) reconcileDelete(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusteraddon", addon.Name) + + if !controllerutil.ContainsFinalizer(addon, FinalizerName) { + return reconcile.Result{}, nil + } + + if err := r.ensureResourceDeleted(ctx, utils.GetInternalHelmReleaseName(addon.Name), TargetNamespace, &helmv2.HelmRelease{}); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("deleting internal helm release: %w", err), ReasonCleanupFailed) + } + + if err := r.ensureResourceDeleted(ctx, utils.GetInternalHelmChartName(addon.Name), TargetNamespace, &sourcev1.HelmChart{}); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("deleting internal helm chart: %w", err), ReasonCleanupFailed) + } + + controllerutil.RemoveFinalizer(addon, FinalizerName) + + if err := r.Client.Update(ctx, addon); err != nil { + return reconcile.Result{}, fmt.Errorf("removing finalizer: %w", err) + } + + logger.Info("Cleanup complete") + + return reconcile.Result{}, nil +} + +func (r *Reconciler) initializeConditions(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { + conditionTypes := []string{ + ConditionTypeReady, + ConditionTypeManaged, + } + + for _, t := range conditionTypes { + if meta.FindStatusCondition(addon.Status.Conditions, t) == nil { + meta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: t, + Status: metav1.ConditionUnknown, + Reason: ReasonInitializing, + }) + } + } + + if err := r.Client.Status().Update(ctx, addon); err != nil { + return reconcile.Result{}, fmt.Errorf("updating HelmClusterAddon status conditions: %w", err) + } + + return reconcile.Result{}, nil +} + +func (r *Reconciler) patchStatusError(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, reconcileErr error, reason string) error { + base := addon.DeepCopy() + + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeReady, + Status: metav1.ConditionFalse, + Reason: reason, + Message: reconcileErr.Error(), + }) + + updateInstalledCond := meta.FindStatusCondition(base.Status.Conditions, ConditionTypeUpdateInstalled) + if updateInstalledCond != nil { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeUpdateInstalled, + Status: metav1.ConditionFalse, + Reason: ReasonUpdateFailed, + Message: reconcileErr.Error(), + }) + } else { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeInstalled, + Status: metav1.ConditionFalse, + Reason: ReasonInstallFailed, + Message: reconcileErr.Error(), + }) + } + + if patchErr := r.Client.Status().Patch(ctx, addon, client.MergeFrom(base)); patchErr != nil { + return errors.Join(reconcileErr, fmt.Errorf("failed to patch status: %w", patchErr)) + } + + return reconcileErr +} + +func (r *Reconciler) updateStatusOnInternalRelease(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, internalHelmRelease *helmv2.HelmRelease) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + base := addon.DeepCopy() + + addonReadyCond := meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeReady) + + internalReadyCond := meta.FindStatusCondition(internalHelmRelease.Status.Conditions, ConditionTypeReady) + if internalReadyCond != nil { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeReady, + Status: internalReadyCond.Status, + Reason: internalReadyCond.Reason, + Message: internalReadyCond.Message, + }) + + switch internalReadyCond.Reason { + case helmv2.InstallSucceededReason: + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeInstalled, + Status: metav1.ConditionTrue, + Reason: ReasonInstallSucceeded, + Message: "", + }) + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeConfigurationApplied, + Status: metav1.ConditionTrue, + Reason: ReasonInstallSucceeded, + Message: "", + }) + + // Required if chart or repository was changed and there was an existing chart. + apimeta.RemoveStatusCondition(&addon.Status.Conditions, ConditionTypeUpdateInstalled) + + if addonReadyCond != nil && addonReadyCond.Status == metav1.ConditionTrue { + addon.Status.LastAppliedChart = &helmv1alpha1.HelmClusterAddonLastAppliedChartRef{ + HelmClusterAddonChartName: base.Spec.Chart.HelmClusterAddonChartName, + HelmClusterAddonRepository: base.Spec.Chart.HelmClusterAddonRepository, + Version: base.Spec.Chart.Version, + } + addon.Status.LastAppliedValues = base.Spec.Values + } + case helmv2.UpgradeSucceededReason: + if r.isUpdateInstalled(addon, internalHelmRelease) { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeUpdateInstalled, + Status: metav1.ConditionTrue, + Reason: ReasonUpdateSucceeded, + Message: "", + }) + if addonReadyCond != nil && addonReadyCond.Status == metav1.ConditionTrue { + addon.Status.LastAppliedChart = &helmv1alpha1.HelmClusterAddonLastAppliedChartRef{ + HelmClusterAddonChartName: base.Spec.Chart.HelmClusterAddonChartName, + HelmClusterAddonRepository: base.Spec.Chart.HelmClusterAddonRepository, + Version: base.Spec.Chart.Version, + } + } + } else { + if addonValues, err := helmchartutil.ReadValues(addon.Spec.Values.Raw); err != nil { + logger.Error(err, "failed to decode values on LastAppliedValues update: %w", err) + } else { + latestRelease := internalHelmRelease.Status.History.Latest() + + if latestRelease != nil && latestRelease.Status == InternalReleaseDeployed && + latestRelease.ConfigDigest == chartutil.DigestValues(digest.Canonical, addonValues).String() { + addon.Status.LastAppliedValues = addon.Spec.Values + + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeConfigurationApplied, + Status: metav1.ConditionTrue, + Reason: ReasonUpdateSucceeded, + LastTransitionTime: metav1.NewTime(time.Now()), + Message: "Applied configuration with values digest " + internalHelmRelease.Status.History.Latest().ConfigDigest, + }) + } + } + } + case helmv2.InstallFailedReason: + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeInstalled, + Status: metav1.ConditionFalse, + Reason: ReasonInstallFailed, + Message: internalReadyCond.Message, + }) + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeConfigurationApplied, + Status: metav1.ConditionFalse, + Reason: ReasonInstallFailed, + Message: internalReadyCond.Message, + }) + case helmv2.UpgradeFailedReason: + if r.isUpdateInstalled(addon, internalHelmRelease) { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeUpdateInstalled, + Status: metav1.ConditionFalse, + Reason: ReasonUpdateFailed, + Message: internalReadyCond.Message, + }) + } else { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeConfigurationApplied, + Status: metav1.ConditionFalse, + Reason: ReasonUpdateFailed, + Message: internalReadyCond.Message, + }) + } + } + } + + addon.Status.ObservedGeneration = addon.Generation + + if addon.Spec.Maintenance == string(helmv1alpha1.NoResourceReconciliation) { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeManaged, + Status: metav1.ConditionFalse, + Reason: ReasonUnmanagedModeActivated, + Message: "", + }) + } else { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeManaged, + Status: metav1.ConditionTrue, + Reason: ReasonManagedModeActivated, + Message: "", + }) + } + + if err := r.Client.Status().Patch(ctx, addon, client.MergeFrom(base)); err != nil { + return reconcile.Result{}, fmt.Errorf("updating HelmClusterAddon status on success: %w", err) + } + + return reconcile.Result{}, nil +} + +// isUpdateInstalled return true if new release was initiated due to chart name/version change, otherwise returns false. +func (r *Reconciler) isUpdateInstalled(addon *helmv1alpha1.HelmClusterAddon, internalHelmRelease *helmv2.HelmRelease) bool { + internalReadyCond := meta.FindStatusCondition(internalHelmRelease.Status.Conditions, ConditionTypeReady) + if internalReadyCond == nil { + return false + } + + if internalReadyCond.Status == metav1.ConditionTrue && internalHelmRelease.Status.History.Len() > 1 { + latest := internalHelmRelease.Status.History.Latest() + previous := internalHelmRelease.Status.History.Previous(true) + + if previous != nil && previous.Status == "superseded" && + latest != nil && + (latest.VersionedChartName() != previous.VersionedChartName() || + addon.Spec.Chart.HelmClusterAddonRepository != addon.Status.LastAppliedChart.HelmClusterAddonRepository) { + return true + } + } + + return false +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/constants.go new file mode 100644 index 0000000..7e8edd2 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/constants.go @@ -0,0 +1,34 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddonchart + +const ( + // ControllerName is the name of this controller, used for leader election and logging. + ControllerName = "helmclusteraddonchart-controller" + + // TargetNamespace is the namespace where internal customer resources are created. + TargetNamespace = "d8-operator-helm" + + // LabelManagedBy marks resources as managed by this controller. + LabelManagedBy = "helm.deckhouse.io/managed-by" + + // LabelManagedByValue is the value for the managed-by label. + LabelManagedByValue = "operator-helm" + + // LabelSourceName stores the name of the source facade resource. + LabelSourceName = "helm.deckhouse.io/cluster-addon-chart" +) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/controller.go new file mode 100644 index 0000000..db5d0d3 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/controller.go @@ -0,0 +1,43 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddonchart + +import ( + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/pkg/utils" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +func SetupWithManager(mgr ctrl.Manager) error { + r := &Reconciler{ + Client: mgr.GetClient(), + } + + return ctrl.NewControllerManagedBy(mgr). + Named(ControllerName). + For(&helmv1alpha1.HelmClusterAddonChart{}). + Watches( + &sourcev1.HelmChart{}, + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Complete(r) +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go new file mode 100644 index 0000000..1202ab1 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go @@ -0,0 +1,80 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddonchart + +import ( + "context" + "fmt" + + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" +) + +type Reconciler struct { + Client client.Client +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusteraddonchart", req.Name) + + chart := &helmv1alpha1.HelmClusterAddonChart{} + if err := r.Client.Get(ctx, req.NamespacedName, chart); client.IgnoreNotFound(err) != nil { + return ctrl.Result{}, fmt.Errorf("failed to get HelmClusterAddonChart: %w", err) + } + + if !chart.DeletionTimestamp.IsZero() { + return reconcile.Result{}, nil + } + + base := chart.DeepCopy() + + internalCharts := &sourcev1.HelmChartList{} + if err := r.Client.List(ctx, internalCharts, client.InNamespace(TargetNamespace)); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to list internal helm chart list: %w", err) + } + + needsUpdate := false + for i, v := range chart.Status.Versions { + found := false + for _, child := range internalCharts.Items { + if child.Spec.Version == v.Version && child.Status.Artifact != nil { + found = true + break + } + } + + if chart.Status.Versions[i].Pulled != found { + chart.Status.Versions[i].Pulled = found + needsUpdate = true + } + } + + if needsUpdate { + if err := r.Client.Status().Patch(ctx, chart, client.MergeFrom(base)); err != nil { + return reconcile.Result{}, fmt.Errorf("failed to update HelmClusterAddonChart status: %w", err) + } + + logger.Info("HelmClusterAddonChart successfully reconciled") + } + + return reconcile.Result{}, nil +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/client.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/client.go new file mode 100644 index 0000000..37b1651 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/client.go @@ -0,0 +1,111 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helmclusteraddonrepository + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "go.yaml.in/yaml/v3" + "k8s.io/apimachinery/pkg/util/wait" +) + +var HelmRepositoryDefaultClient HelmRepositoryClient + +type HelmRepositoryClient struct{} + +func (c *HelmRepositoryClient) FetchCharts(ctx context.Context, url string) (map[string][]helmv1alpha1.HelmClusterAddonChartVersion, error) { + if !strings.HasSuffix(url, "/index.yaml") { + url += "/index.yaml" + } + + var indexFile HelmRepositoryIndex + + backoff := wait.Backoff{ + Duration: 1 * time.Second, // Initial delay + Factor: 2.0, // Double the delay each time + Jitter: 0.1, // Add 10% randomness to prevent the thundering herd problem + Steps: 3, // Maximum number of retries (1s, 2s, 4s, 8s, 16s) + } + + ctx, cancel := context.WithTimeout(ctx, 8*time.Second) + defer cancel() + + err := wait.ExponentialBackoffWithContext(ctx, backoff, func(ctx context.Context) (done bool, err error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return true, fmt.Errorf("creating request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, nil + } + defer resp.Body.Close() + + if resp.StatusCode >= 500 { + return false, nil + } + + if resp.StatusCode >= 400 { + return true, fmt.Errorf("fatal client error: received status %d", resp.StatusCode) + } + + if err := yaml.NewDecoder(resp.Body).Decode(&indexFile); err != nil { + return true, fmt.Errorf("cannot decode response: %w", err) + } + + return true, nil + }) + if err != nil { + return nil, fmt.Errorf("helm repository index.yaml request failed: %w", err) + } + + charts := make(map[string][]helmv1alpha1.HelmClusterAddonChartVersion) + + for chartName, chartInfo := range indexFile.Entries { + charts[chartName] = make([]helmv1alpha1.HelmClusterAddonChartVersion, 0) + + for _, chartVersion := range chartInfo { + if chartVersion.Removed { + continue + } + + charts[chartName] = append(charts[chartName], helmv1alpha1.HelmClusterAddonChartVersion{ + Version: chartVersion.Version, + Digest: chartVersion.Digest, + }) + } + } + + return charts, nil +} + +type HelmRepositoryIndex struct { + APIVersion string `json:"apiVersion"` + Entries map[string][]HelmRepositoryChartVersion `json:"entries"` +} + +type HelmRepositoryChartVersion struct { + Version string `json:"version"` + Digest string `json:"digest"` + Removed bool `json:"removed,omitempty"` +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go new file mode 100644 index 0000000..7459ebe --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.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 helmclusteraddonrepository + +import "time" + +const ( + // ControllerName is the name of this controller, used for leader election and logging. + ControllerName = "helmclusteraddonrepository-controller" + + // TargetNamespace is the namespace where internal customer resources are created. + TargetNamespace = "d8-operator-helm" + + // FinalizerName is the finalizer added to HelmClusterRepository to ensure cleanup. + FinalizerName = "helm.deckhouse.io/cleanup" + + // ConditionTypeReady is the condition type for readiness. + ConditionTypeReady = "Ready" + + // ConditionTypeSynced is the condition type to track chart sync status + ConditionTypeSynced = "Synced" + + // ReasonMirrorFailed indicates the internal HelmRepository create/update failed. + ReasonMirrorFailed = "MirrorFailed" + + // ReasonSyncSucceeded indicates that chart sync was successfully completed. + ReasonSyncSucceeded = "SyncSucceeded" + + // ReasonSyncInProgress indicates that chart sync is in progress. + ReasonSyncInProgress = "ReasonSyncInProgress" + + // ReasonSyncFailed indicates that charts sync was failed. + ReasonSyncFailed = "SyncFailed" + + // ReasonCleanupFailed indicates deletion of the internal HelmRepository failed. + ReasonCleanupFailed = "CleanupFailed" + + // LabelManagedBy marks resources as managed by this controller. + LabelManagedBy = "helm.deckhouse.io/managed-by" + + // LabelManagedByValue is the value for the managed-by label. + LabelManagedByValue = "operator-helm" + + // LabelSourceName stores the name of the source facade resource. + LabelSourceName = "helm.deckhouse.io/cluster-addon-repository" + + // DefaultInterval is the default reconciliation interval for the internal repository. + DefaultInterval = 5 * time.Minute + + // DefaultSyncInterval is the default repository charts sync interval. + DefaultSyncInterval = 5 * time.Minute +) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go new file mode 100644 index 0000000..6509b09 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go @@ -0,0 +1,54 @@ +/* +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 ( + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/pkg/utils" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + corev1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +func SetupWithManager(mgr ctrl.Manager) error { + r := &Reconciler{ + Client: mgr.GetClient(), + } + + return ctrl.NewControllerManagedBy(mgr). + Named(ControllerName). + For(&helmv1alpha1.HelmClusterAddonRepository{}). + Watches( + &sourcev1.HelmRepository{}, + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Watches( + &sourcev1.OCIRepository{}, + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Complete(r) +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go new file mode 100644 index 0000000..9f39862 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go @@ -0,0 +1,519 @@ +/* +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" + "errors" + "fmt" + "time" + + "github.com/deckhouse/operator-helm/pkg/utils" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "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/werf/3p-fluxcd-pkg/apis/meta" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" +) + +// Reconciler reconciles HelmClusterRepository objects by mirroring them +// to namespaced HelmRepository resources in the target namespace. +type Reconciler struct { + Client client.Client +} + +// Reconcile implements reconcile.Reconciler. +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusterrepository", req.Name) + + var repo helmv1alpha1.HelmClusterAddonRepository + + if err := r.Client.Get(ctx, req.NamespacedName, &repo); err != nil { + if apierrors.IsNotFound(err) { + logger.Info("HelmClusterAddonRepository not found, skipping") + + return reconcile.Result{}, nil + } + + return reconcile.Result{}, fmt.Errorf("getting HelmClusterAddonRepository: %w", err) + } + + repoType, err := utils.GetRepositoryType(repo.Spec.URL) + if err != nil { + return reconcile.Result{}, err + } + + if !repo.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, &repo, repoType) + } + + if !controllerutil.ContainsFinalizer(&repo, FinalizerName) { + controllerutil.AddFinalizer(&repo, FinalizerName) + + if err := r.Client.Update(ctx, &repo); err != nil { + return reconcile.Result{}, fmt.Errorf("adding finalizer: %w", err) + } + + return r.requeueAtSyncInterval(&repo) + } + + switch repoType { + case utils.InternalHelmRepository: + return r.reconcileInternalHelmRepository(ctx, &repo) + case utils.InternalOCIRepository: + return r.reconcileInternalOCIRepository(ctx, &repo) + default: + return r.requeueAtSyncInterval(&repo) + } +} + +func (r *Reconciler) reconcileInternalHelmRepository(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + if err := r.reconcileInternalRepositoryAuthSecret(ctx, repo, utils.InternalHelmRepository); err != nil { + return reconcile.Result{}, err + } + + if err := r.reconcileInternalRepositoryTLSSecret(ctx, repo, utils.InternalHelmRepository); err != nil { + return reconcile.Result{}, err + } + + existing := &sourcev1.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repo.Name, + Namespace: TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, r.Client, existing, func() error { + existing.Spec.URL = repo.Spec.URL + existing.Spec.Interval = metav1.Duration{Duration: DefaultInterval} + existing.Spec.Insecure = !repo.Spec.TLSVerify + existing.Spec.CertSecretRef = nil + existing.Spec.SecretRef = nil + + if repo.Spec.Auth != nil { + existing.Spec.SecretRef = &meta.LocalObjectReference{ + Name: utils.GetInternalRepositoryAuthSecretName(utils.InternalHelmRepository, repo.Name), + } + existing.Spec.PassCredentials = true + } + + if repo.Spec.CACertificate != "" { + existing.Spec.CertSecretRef = &meta.LocalObjectReference{ + Name: utils.GetInternalRepositoryTLSSecretName(utils.InternalHelmRepository, repo.Name), + } + } + + existing.Labels = map[string]string{ + LabelManagedBy: LabelManagedByValue, + LabelSourceName: repo.Name, + } + + return nil + }) + if err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("reconciling helm repository: %w", err), ReasonMirrorFailed) + } + + if op != controllerutil.OperationResultNone { + logger.Info("Successfully reconciled helm repository", "operation", op) + } + + if changed, err := r.updateSuccessStatus(ctx, repo, existing.Status.Conditions); err != nil { + return reconcile.Result{}, fmt.Errorf("updating status after repository reconcile: %w", err) + } else if changed { + return r.requeueAtSyncInterval(repo) + } + + if apimeta.IsStatusConditionPresentAndEqual(repo.Status.Conditions, ConditionTypeReady, metav1.ConditionTrue) { + return r.reconcileHelmRepositoryCharts(ctx, repo) + } + + return r.requeueAtSyncInterval(repo) +} + +func (r *Reconciler) reconcileHelmRepositoryCharts(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + syncCond := apimeta.FindStatusCondition(repo.Status.Conditions, ConditionTypeSynced) + if syncCond != nil && syncCond.Status == metav1.ConditionTrue && syncCond.LastTransitionTime.UTC().Add(DefaultSyncInterval).After(time.Now().UTC()) { + return r.requeueAtSyncInterval(repo) + } else if syncCond == nil || syncCond.Reason != ReasonSyncInProgress { + if err := r.updateSyncCondition(ctx, repo, metav1.ConditionFalse, ReasonSyncInProgress, ""); err != nil { + return reconcile.Result{}, fmt.Errorf("updating sync condition: %w", err) + } + + return r.requeueAtSyncInterval(repo) + } + + charts, err := HelmRepositoryDefaultClient.FetchCharts(ctx, repo.Spec.URL) + if err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeSynced, fmt.Errorf("cannot fetch chart info from repository: %w", err), ReasonSyncFailed) + } + + for chart, versions := range charts { + existing := &helmv1alpha1.HelmClusterAddonChart{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.GetHelmClusterAddonChartName(repo.Name, chart), + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, r.Client, existing, func() error { + existing.OwnerReferences = []metav1.OwnerReference{ + { + APIVersion: repo.APIVersion, + Kind: repo.Kind, + Name: repo.Name, + UID: repo.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + } + + existing.Labels = map[string]string{ + LabelManagedBy: LabelManagedByValue, + LabelSourceName: repo.Name, + } + + return nil + }) + if err != nil { + if statusUpdateErr := r.updateSyncCondition(ctx, repo, metav1.ConditionFalse, ReasonSyncFailed, ""); statusUpdateErr != nil { + return reconcile.Result{}, fmt.Errorf("failed to update sync condition: %w", err) + } + + return reconcile.Result{}, fmt.Errorf("cannot create or update HelmClusterAddonChart: %w", err) + } + + existingVersionsMap := make(map[string]helmv1alpha1.HelmClusterAddonChartVersion) + for _, version := range existing.Status.Versions { + existingVersionsMap[version.Version] = version + } + + for i, version := range versions { + if existingVersion, found := existingVersionsMap[version.Version]; found && version.Digest == existingVersion.Digest { + versions[i].Pulled = existingVersion.Pulled + } + } + + if op != controllerutil.OperationResultNone { + logger.Info("Successfully reconciled HelmClusterAddonChart", "operation", op, "repository", repo.Name, "chart", chart) + } + + base := existing.DeepCopy() + existing.Status.Versions = versions + + if err := r.Client.Status().Patch(ctx, existing, client.MergeFrom(base)); err != nil { + if statusUpdateErr := r.updateSyncCondition(ctx, repo, metav1.ConditionFalse, ReasonSyncFailed, ""); statusUpdateErr != nil { + return reconcile.Result{}, fmt.Errorf("failed to update sync condition: %w", err) + } + + return reconcile.Result{}, fmt.Errorf("failed to update chart status: %w", err) + } + + logger.Info("Successfully sync HelmClusterAddonChart versions", "operation", op, "repository", repo.Name, "chart", chart) + } + + logger.Info(fmt.Sprintf("Scheduling next helm charts sync in %s", DefaultSyncInterval)) + + if err := r.updateSyncCondition(ctx, repo, metav1.ConditionTrue, ReasonSyncSucceeded, ""); err != nil { + return reconcile.Result{}, fmt.Errorf("updating sync condition: %w", err) + } + + return reconcile.Result{RequeueAfter: DefaultSyncInterval}, nil +} + +func (r *Reconciler) updateSyncCondition(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, status metav1.ConditionStatus, reason, message string) error { + base := repo.DeepCopy() + + apimeta.SetStatusCondition(&repo.Status.Conditions, metav1.Condition{ + Type: ConditionTypeSynced, + Status: status, + ObservedGeneration: repo.Generation, + Reason: reason, + Message: message, + }) + + if err := r.Client.Status().Patch(ctx, repo, client.MergeFrom(base)); err != nil { + return err + } + + return nil +} + +func (r *Reconciler) reconcileInternalOCIRepository(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + if err := r.reconcileInternalRepositoryAuthSecret(ctx, repo, utils.InternalOCIRepository); err != nil { + return reconcile.Result{}, err + } + + if err := r.reconcileInternalRepositoryTLSSecret(ctx, repo, utils.InternalOCIRepository); err != nil { + return reconcile.Result{}, err + } + + existing := &sourcev1.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repo.Name, + Namespace: TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, r.Client, existing, func() error { + existing.Spec.URL = repo.Spec.URL + existing.Spec.Interval = metav1.Duration{Duration: DefaultInterval} + existing.Spec.Insecure = !repo.Spec.TLSVerify + existing.Spec.CertSecretRef = nil + existing.Spec.SecretRef = nil + + if repo.Spec.Auth != nil { + existing.Spec.SecretRef = &meta.LocalObjectReference{ + Name: utils.GetInternalRepositoryAuthSecretName(utils.InternalOCIRepository, repo.Name), + } + } + + if repo.Spec.CACertificate != "" { + existing.Spec.CertSecretRef = &meta.LocalObjectReference{ + Name: utils.GetInternalRepositoryTLSSecretName(utils.InternalOCIRepository, repo.Name), + } + } + + existing.Labels = map[string]string{ + LabelManagedBy: LabelManagedByValue, + LabelSourceName: repo.Name, + } + + return nil + }) + if err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("reconciling oci repository: %w", err), ReasonMirrorFailed) + } + + if op != controllerutil.OperationResultNone { + logger.Info("Successfully reconciled oci repository", "operation", op) + } else { + // TODO: implement chats sync for OCI repository + } + + if _, err := r.updateSuccessStatus(ctx, repo, existing.Status.Conditions); err != nil { + return reconcile.Result{}, err + } + + return r.requeueAtSyncInterval(repo) +} + +func (r *Reconciler) reconcileInternalRepositoryAuthSecret(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType utils.InternalRepositoryType) error { + secretName := utils.GetInternalRepositoryAuthSecretName(repoType, repo.Name) + + if repo.Spec.Auth == nil { + if err := r.ensureResourceDeleted(ctx, secretName, TargetNamespace, &corev1.Secret{}); err != nil { + return fmt.Errorf("cannot delete obsolete auth secret: %w", err) + } + + return nil + } + + authSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: TargetNamespace, + }, + } + + if _, err := controllerutil.CreateOrPatch(ctx, r.Client, authSecret, func() error { + authSecret.Labels = map[string]string{ + LabelManagedBy: LabelManagedByValue, + LabelSourceName: repo.Name, + } + + authSecret.StringData = map[string]string{ + "username": repo.Spec.Auth.Username, + "password": repo.Spec.Auth.Password, + } + + return nil + }); err != nil { + return fmt.Errorf("cannot reconcile auth secret: %w", err) + } + + return nil +} + +func (r *Reconciler) reconcileInternalRepositoryTLSSecret(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType utils.InternalRepositoryType) error { + secretName := utils.GetInternalRepositoryTLSSecretName(repoType, repo.Name) + + if repo.Spec.CACertificate == "" { + if err := r.ensureResourceDeleted(ctx, secretName, TargetNamespace, &corev1.Secret{}); err != nil { + return fmt.Errorf("cannot delete obsolete tls secret: %w", err) + } + + return nil + } + + // TODO: consider adding CA certificate format validation + + tlsSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: TargetNamespace, + }, + } + + if _, err := controllerutil.CreateOrPatch(ctx, r.Client, tlsSecret, func() error { + tlsSecret.Labels = map[string]string{ + LabelManagedBy: LabelManagedByValue, + LabelSourceName: repo.Name, + } + + tlsSecret.StringData = map[string]string{ + "ca.crt": repo.Spec.CACertificate, + } + + return nil + }); err != nil { + return fmt.Errorf("cannot reconcile tls secret: %w", err) + } + + return nil +} + +func (r *Reconciler) ensureResourceDeleted(ctx context.Context, name, namespace string, obj client.Object) error { + if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, obj); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + + return fmt.Errorf("checking existence of obsolete resource: %w", err) + } + + if err := r.Client.Delete(ctx, obj); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("deleting obsolete resource: %w", err) + } + + return nil +} + +func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType utils.InternalRepositoryType) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusteraddonrepository", repo.Name) + + if !controllerutil.ContainsFinalizer(repo, FinalizerName) { + return reconcile.Result{}, nil + } + + if err := r.ensureResourceDeleted( + ctx, + utils.GetInternalRepositoryAuthSecretName(repoType, repo.Name), + TargetNamespace, + &corev1.Secret{}, + ); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("deleting internal auth secret: %w", err), ReasonCleanupFailed) + } + + if err := r.ensureResourceDeleted( + ctx, + utils.GetInternalRepositoryTLSSecretName(repoType, repo.Name), + TargetNamespace, + &corev1.Secret{}, + ); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("deleting internal tls secret: %w", err), ReasonCleanupFailed) + } + + var internalRepository client.Object + + switch repoType { + case utils.InternalHelmRepository: + internalRepository = &sourcev1.HelmRepository{} + case utils.InternalOCIRepository: + internalRepository = &sourcev1.OCIRepository{} + default: + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("cannot remove unsupported repisotory type: %s", repoType), ReasonCleanupFailed) + } + + if err := r.ensureResourceDeleted(ctx, repo.Name, TargetNamespace, internalRepository); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("deleting internal repository: %w", err), ReasonCleanupFailed) + } + + controllerutil.RemoveFinalizer(repo, FinalizerName) + + if err := r.Client.Update(ctx, repo); err != nil { + return reconcile.Result{}, fmt.Errorf("removing finalizer: %w", err) + } + + logger.Info("Cleanup complete") + + return reconcile.Result{}, nil +} + +func (r *Reconciler) patchStatusError(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, conditionType string, reconcileErr error, reason string) error { + base := repo.DeepCopy() + + apimeta.SetStatusCondition(&repo.Status.Conditions, metav1.Condition{ + Type: conditionType, + Status: metav1.ConditionFalse, + Reason: reason, + Message: reconcileErr.Error(), + }) + + if patchErr := r.Client.Status().Patch(ctx, repo, client.MergeFrom(base)); patchErr != nil { + return errors.Join(reconcileErr, fmt.Errorf("failed to patch status: %w", patchErr)) + } + + return reconcileErr +} + +func (r *Reconciler) requeueAtSyncInterval(repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { + repoSyncCond := apimeta.FindStatusCondition(repo.Status.Conditions, ConditionTypeSynced) + if repoSyncCond != nil { + remaining := time.Until(repoSyncCond.LastTransitionTime.Add(DefaultSyncInterval)) + if remaining > 0 { + return reconcile.Result{RequeueAfter: remaining}, nil + } + } + + return reconcile.Result{RequeueAfter: DefaultSyncInterval}, nil +} + +func (r *Reconciler) updateSuccessStatus(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, internalConditions []metav1.Condition) (bool, error) { + var changed bool + + base := repo.DeepCopy() + + internalReadyCond := apimeta.FindStatusCondition(internalConditions, meta.ReadyCondition) + if internalReadyCond != nil { + changed = apimeta.SetStatusCondition(&repo.Status.Conditions, *internalReadyCond) + } + + if changed { + repo.Status.ObservedGeneration = repo.Generation + + if err := r.Client.Status().Patch(ctx, repo, client.MergeFrom(base)); err != nil { + return false, fmt.Errorf("patching status: %w", err) + } + } + + return changed, nil +} diff --git a/images/operator-helm-artifact/pkg/utils/mapper.go b/images/operator-helm-artifact/pkg/utils/mapper.go new file mode 100644 index 0000000..c3f390b --- /dev/null +++ b/images/operator-helm-artifact/pkg/utils/mapper.go @@ -0,0 +1,59 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "context" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func MapInternalToFacade(targetNamespace, labelManagedBy, labelManagedByValue, labelSourceName string) handler.MapFunc { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + + if obj.GetNamespace() != targetNamespace { + return nil + } + + labels := obj.GetLabels() + if labels[labelManagedBy] != labelManagedByValue { + return nil + } + + sourceName := labels[labelSourceName] + if sourceName == "" { + logger.Info("resource missing source label, skipping", + "name", obj.GetName(), "namespace", obj.GetNamespace()) + + return nil + } + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: sourceName, + Namespace: "", + }, + }, + } + } +} diff --git a/images/operator-helm-artifact/pkg/utils/name.go b/images/operator-helm-artifact/pkg/utils/name.go new file mode 100644 index 0000000..b9cd970 --- /dev/null +++ b/images/operator-helm-artifact/pkg/utils/name.go @@ -0,0 +1,112 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "fmt" + "hash/fnv" + "strings" +) + +func GetHash(s string) string { + h := fnv.New32a() + + _, _ = h.Write([]byte(s)) + + return fmt.Sprintf("%x", h.Sum32()) +} + +func GetInternalRepositoryAuthSecretName(repoType InternalRepositoryType, internalRepoName string) string { + prefix := "auth" + + hash := GetHash(fmt.Sprintf("%s-%s-%s", prefix, repoType, internalRepoName)) + + var result, postfix string + + result = prefix + "-" + string(repoType) + "-" + + if len(internalRepoName) > 35 { + result += internalRepoName[:35] + postfix = "-" + hash + } else { + result += internalRepoName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetInternalRepositoryTLSSecretName(repoType InternalRepositoryType, internalRepoName string) string { + prefix := "tls" + + hash := GetHash(fmt.Sprintf("%s-%s-%s", prefix, repoType, internalRepoName)) + + var result, postfix string + + result = prefix + "-" + string(repoType) + "-" + + if len(internalRepoName) > 35 { + result += internalRepoName[:35] + postfix = "-" + hash + } else { + result += internalRepoName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetHelmClusterAddonChartName(repoName, addonName string) string { + hash := GetHash(fmt.Sprintf("%s-%s", repoName, addonName)) + + var result, postfix string + + if len(repoName) > 20 { + result += repoName[:20] + postfix = "-" + hash + } else { + result += repoName + } + + if len(addonName) > 20 { + result += "-" + addonName[:20] + postfix = "-" + hash + } else { + result += "-" + addonName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetInternalHelmReleaseName(addonName string) string { + prefix := "addon" + hash := GetHash(fmt.Sprintf("%s-%s", prefix, addonName)) + + result := prefix + "-" + postfix := "" + + if len(addonName) > 40 { + result += addonName[:40] + postfix = "-" + hash + } else { + result += addonName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetInternalHelmChartName(addonName string) string { + return GetInternalHelmReleaseName(addonName) +} diff --git a/images/operator-helm-artifact/pkg/utils/repository.go b/images/operator-helm-artifact/pkg/utils/repository.go new file mode 100644 index 0000000..ad6da7c --- /dev/null +++ b/images/operator-helm-artifact/pkg/utils/repository.go @@ -0,0 +1,45 @@ +/* +Copyright 2026 Flant JSC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "fmt" + "net/url" +) + +type InternalRepositoryType string + +const ( + InternalHelmRepository InternalRepositoryType = "helm" + InternalOCIRepository InternalRepositoryType = "oci" +) + +func GetRepositoryType(s string) (InternalRepositoryType, error) { + parsedURL, err := url.Parse(s) + if err != nil { + return "", fmt.Errorf("cannot parse url: %w", err) + } + + switch parsedURL.Scheme { + case "http", "https": + return InternalHelmRepository, nil + case "oci": + return InternalOCIRepository, nil + default: + return "", fmt.Errorf("unsupported repository schema in use: %s", parsedURL.Scheme) + } +} diff --git a/images/operator-helm-artifact/werf.inc.yaml b/images/operator-helm-artifact/werf.inc.yaml new file mode 100644 index 0000000..165af3d --- /dev/null +++ b/images/operator-helm-artifact/werf.inc.yaml @@ -0,0 +1,57 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact +final: false +fromImage: builder/src +git: +- add: {{ .ModuleDir }}/api + to: /src/api + stageDependencies: + install: + - go.mod + - go.sum + setup: + - "**/*.go" +- add: {{ .ModuleDir }}/images/{{ .ImageName }} + to: /src/images/operator-helm-artifact + stageDependencies: + install: + - go.mod + - go.sum + setup: + - "**/*.go" +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +final: false +fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-bookworm-1.25" "builder/golang-alt-svace-1.25" }} +import: +- image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact + add: /src + to: /src + before: install +# TODO: uncomment as soon as CI will be ready +# secrets: +# - id: GOPROXY +# value: {{ .GOPROXY }} +# mount: +# - fromPath: ~/go-pkg-cache +# to: /go/pkg +shell: + install: + # TODO: uncomment as soon as CI will be ready + # - export GOPROXY=$(cat /run/secrets/GOPROXY) + - cd /src/images/operator-helm-artifact + - go mod download + setup: + - cd /src/images/operator-helm-artifact + - mkdir /out + - export GOOS=linux + - export GOARCH=amd64 + - export CGO_ENABLED=0 + + - | + echo "Build operator-helm-controller binary" + {{- $_ := set $ "ProjectName" (list $.ImageName "operator-helm-controller" | join "/") }} + + {{- $buildCommand := printf "go build -ldflags=\"-s -w\" -tags %s -v -a -o /out/operator-helm-controller ./cmd/operator-helm-controller" .MODULE_EDITION -}} + {{- include "image-build.build" (set $ "BuildCommand" $buildCommand) | nindent 4 }} + diff --git a/images/operator-helm-controller/mount-points.yaml b/images/operator-helm-controller/mount-points.yaml new file mode 100644 index 0000000..eefff43 --- /dev/null +++ b/images/operator-helm-controller/mount-points.yaml @@ -0,0 +1 @@ +dirs: [] diff --git a/images/operator-helm-controller/werf.inc.yaml b/images/operator-helm-controller/werf.inc.yaml new file mode 100644 index 0000000..6656877 --- /dev/null +++ b/images/operator-helm-controller/werf.inc.yaml @@ -0,0 +1,16 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +fromImage: {{ .ModuleNamePrefix }}distroless +git: + {{- include "image mount points" . }} +import: +- image: {{ .ModuleNamePrefix }}operator-helm-artifact + add: /out/operator-helm-controller + to: /app/operator-helm-controller + after: install +imageSpec: + config: + user: 64535 + workingDir: "/app" + entrypoint: ["/app/operator-helm-controller"] + diff --git a/module.yaml b/module.yaml new file mode 100644 index 0000000..c71920a --- /dev/null +++ b/module.yaml @@ -0,0 +1,25 @@ +name: operator-helm +stage: Experimental +# TODO: should we mark module as critical? +# weight: 960 +requirements: + deckhouse: ">= 1.69" +subsystems: + # TODO: confirm with TL + - delivery +namespace: d8-operator-helm +descriptions: + en: An operator to deploy helm applications declaratively. + ru: Оператор для декларативного развертывания helm-приложений. +# TODO: confirm with TL +tags: ["delivery"] +disable: + confirmation: true + message: "Disabling of this module can cause disruptions in deployed applications operation." +accessibility: + editions: + # TODO: confirm with TL + _default: + available: true + enabledInBundles: + - Minimal diff --git a/openapi/config-values.yaml b/openapi/config-values.yaml new file mode 100644 index 0000000..7097068 --- /dev/null +++ b/openapi/config-values.yaml @@ -0,0 +1,26 @@ +type: object +properties: + highAvailability: + type: boolean + x-examples: [true, false] + description: | + Manually enable the high availability (HA) mode. + + By default, Deckhouse automatically decides whether to enable the HA mode. + To learn more about the HA mode, refer to [High reliability and availability](/products/kubernetes-platform/documentation/v1/admin/configuration/high-reliability-and-availability/enable.html#enabling-ha-mode-for-individual-components). + logLevel: + type: string + default: info + description: | + Sets a logging level. + + Working for this components: + - `helm-controller` + - `nelm-source-controller` + - `kube-api-rewriter` + - `deckhouse-helm-controller` + enum: + - "debug" + - "info" + - "warn" + - "error" diff --git a/openapi/doc-ru-config-values.yaml b/openapi/doc-ru-config-values.yaml new file mode 100644 index 0000000..f8c368c --- /dev/null +++ b/openapi/doc-ru-config-values.yaml @@ -0,0 +1,24 @@ +type: object +properties: + highAvailability: + description: | + Ручное управление режимом отказоустойчивости. + + По умолчанию режим отказоустойчивости определяется автоматически. + Подробнее про режим отказоустойчивости можно прочитать в разделе [Высокая надежность и доступность](/products/kubernetes-platform/documentation/v1/admin/configuration/high-reliability-and-availability/enable.html#включение-режима-ha-для-отдельных-компонентов). + logLevel: + type: string + default: info + description: | + Устанавливает уровень логирования. + + Работает для следующих компонентов: + - `helm-controller` + - `nelm-source-controller` + - `kube-api-rewriter` + - `deckhouse-helm-controller` + enum: + - "debug" + - "info" + - "warn" + - "error" diff --git a/openapi/values.yaml b/openapi/values.yaml new file mode 100644 index 0000000..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..8a9dc39 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,6 @@ +dependencies: +- name: deckhouse_lib_helm + repository: https://deckhouse.github.io/lib-helm + version: 1.55.1 +digest: sha256:5bdef3964d2672b8ff290f32e22569bc502e040e4e70274cab1762f27d9982e0 +generated: "2026-02-16T03:37:06.63855+03:00" diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl new file mode 100644 index 0000000..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..e333823 --- /dev/null +++ b/templates/helm-controller/deployment.yaml @@ -0,0 +1,137 @@ +{{- $priorityClassName := include "priorityClassName" . }} + +{{- define "helm_controller_resources" }} +cpu: 100m +memory: 64Mi +{{- end }} + +{{- if (.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller" "workload-resource-policy.deckhouse.io" "master")) | nindent 2 }} +spec: + targetRef: + apiVersion: "apps/v1" + kind: Deployment + name: helm-controller + updatePolicy: + updateMode: {{ include "vpa.policyUpdateMode" . }} + resourcePolicy: + containerPolicies: + {{- include "kube_api_rewriter.vpa_container_policy" . | nindent 4 }} + {{- include "kube_rbac_proxy.vpa_container_policy" . | nindent 4 }} + - containerName: helm-controller + minAllowed: + {{- include "helm_controller_resources" . | nindent 8 }} + maxAllowed: + cpu: 1000m + memory: 1Gi +{{- end }} + +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller" )) | nindent 2 }} +spec: + minAvailable: {{ include "helm_lib_is_ha_to_value" (list . 1 0) }} + selector: + matchLabels: + app: helm-controller + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller")) | nindent 2 }} +spec: + replicas: {{ include "helm_lib_is_ha_to_value" (list . 3 1) }} + {{- if (include "helm_lib_ha_enabled" .) }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + {{- end }} + revisionHistoryLimit: 2 + selector: + matchLabels: + app: helm-controller + template: + metadata: + labels: + app: helm-controller + annotations: + kubectl.kubernetes.io/default-container: helm-controller + spec: + containers: + {{- include "kube_api_rewriter.sidecar_container" . | nindent 8 }} + - name: helm-controller + {{- include "helm_lib_module_container_security_context_read_only_root_filesystem_capabilities_drop_all_pss_restricted" . | nindent 10 }} + image: {{ include "helm_lib_module_image" (list . "helmController") }} + imagePullPolicy: IfNotPresent + args: + - --watch-all-namespaces + - --log-level={{ include "moduleLogLevel" . }} + - --log-encoding=json + - --enable-leader-election + volumeMounts: + - mountPath: /tmp + name: temp + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 12 }} + ports: + - containerPort: 8080 + name: metrics + protocol: TCP + - containerPort: 9440 + name: healthz + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 14 }} + {{- if not ( .Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} + {{- include "helm_controller_resources" . | nindent 14 }} + {{- end }} + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 12 }} + {{- include "helm-controller.envs" . | nindent 12 }} + livenessProbe: + httpGet: + path: /healthz + port: healthz + scheme: HTTP + initialDelaySeconds: 10 + readinessProbe: + httpGet: + path: /readyz + port: healthz + scheme: HTTP + initialDelaySeconds: 10 + {{- $kubeRbacProxySettings := dict }} + {{- $_ := set $kubeRbacProxySettings "runAsUserNobody" false }} + {{- $_ := set $kubeRbacProxySettings "ignorePaths" "/proxy/healthz,/proxy/readyz" }} + {{- $_ := set $kubeRbacProxySettings "upstreams" (list + (dict "upstream" "http://127.0.0.1:8080/metrics" "path" "/metrics" "name" "helm-controller") + (dict "upstream" "http://127.0.0.1:9090/metrics" "path" "/proxy/metrics" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/healthz" "path" "/proxy/healthz" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/readyz" "path" "/proxy/readyz" "name" "kube-api-rewriter") + ) }} + {{- include "kube_rbac_proxy.sidecar_container" (tuple . $kubeRbacProxySettings) | nindent 8 }} + dnsPolicy: ClusterFirst + serviceAccountName: helm-controller + {{- include "helm_lib_module_pod_security_context_run_as_user_deckhouse" . | nindent 6 }} + {{- include "helm_lib_priority_class" (tuple . $priorityClassName) | nindent 6 }} + {{- include "helm_lib_node_selector" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_tolerations" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_pod_anti_affinity_for_ha" (list . (dict "app" "helm-controller")) | nindent 6 }} + volumes: + - emptyDir: {} + name: temp + {{- include "kube_api_rewriter.kubeconfig_volume" . | nindent 8 }} diff --git a/templates/helm-controller/rbac-for-us.yaml b/templates/helm-controller/rbac-for-us.yaml new file mode 100644 index 0000000..34432c1 --- /dev/null +++ b/templates/helm-controller/rbac-for-us.yaml @@ -0,0 +1,148 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller")) | nindent 2 }} +imagePullSecrets: +- name: operator-helm-module-registry +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:helm-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: d8:{{ .Chart.Name }}:helm-controller +subjects: +- kind: ServiceAccount + name: helm-controller + namespace: d8-{{ .Chart.Name }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:helm-controller +rules: +- apiGroups: ['*'] + resources: ['*'] + verbs: ['*'] +- nonResourceURLs: ['*'] + verbs: ['*'] +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases/status + verbs: + - get + - patch + - update +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmcharts + - internalnelmoperatorocirepositories + verbs: + - get + - list + - watch +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmcharts/status + - internalnelmoperatorocirepositories/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: helm-controller + namespace: d8-{{ .Chart.Name }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create +- apiGroups: + - "coordination.k8s.io" + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: helm-controller + namespace: d8-{{ .Chart.Name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: helm-controller +subjects: +- kind: ServiceAccount + name: helm-controller diff --git a/templates/helm-controller/service-metrics.yaml b/templates/helm-controller/service-metrics.yaml new file mode 100644 index 0000000..1f2652c --- /dev/null +++ b/templates/helm-controller/service-metrics.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: helm-controller-metrics + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller")) | nindent 2 }} +spec: + ports: + - name: metrics + port: 8080 + protocol: TCP + targetPort: https-metrics + selector: + app: helm-controller diff --git a/templates/helm-controller/service-monitor.yaml b/templates/helm-controller/service-monitor.yaml new file mode 100644 index 0000000..8161d87 --- /dev/null +++ b/templates/helm-controller/service-monitor.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: helm-controller + namespace: d8-monitoring + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller" "prometheus" "main")) | nindent 2 }} +spec: + endpoints: + - bearerTokenSecret: + key: token + name: prometheus-token + path: /metrics + port: metrics + scheme: https + tlsConfig: + insecureSkipVerify: true + namespaceSelector: + matchNames: + - d8-{{ .Chart.Name }} + selector: + matchLabels: + app: "helm-controller" diff --git a/templates/kube-api-rewriter/_customize_patch_helpers.tpl b/templates/kube-api-rewriter/_customize_patch_helpers.tpl new file mode 100644 index 0000000..72b1d18 --- /dev/null +++ b/templates/kube-api-rewriter/_customize_patch_helpers.tpl @@ -0,0 +1,69 @@ +{{- /* Helpers to create patches for component customizer in Kubevirt and CDI configurations. + +- kube_api_rewriter.pod_spec_strategic_patch_json - creates a JSON patch for a pod spec to add kube-api-rewriter sidecar container. +- kube_api_rewriter.service_spec_port_patch_json - creates a JSON patch for a service spec to point it to the kube-api-rewriter webhook proxy. +- kube_api_rewriter.webhook_spec_port_patch_json - creates a JSON patch for a validating or mutating webhook spec to point it to the kube-api-rewriter webhook proxy. + +*/ -}} + +{{- define "kube_api_rewriter.pod_spec_strategic_patch_json" -}} + '{{ include "kube_api_rewriter.pod_spec_strategic_patch" . | fromYaml | toJson }}' +{{- end }} + +{{- define "kube_api_rewriter.pod_spec_strategic_patch" -}} + {{- $ctx := index . 0 -}} + {{- $mainContainerName := index . 1 -}} + {{- $settings := dict -}} + {{- if ge (len .) 3 -}} + {{- $settings = index . 2 -}} + {{- end -}} + {{- $isWebhook := hasKey $settings "WEBHOOK_ADDRESS" -}} +spec: + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: {{ $mainContainerName }} + spec: + volumes: + {{- include "kube_api_rewriter.kubeconfig_volume" . | nindent 6 }} + containers: + {{- include "kube_api_rewriter.sidecar_container" (tuple $ctx $settings) | nindent 6 }} + - name: {{ $mainContainerName }} + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 8 }} + volumeMounts: + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 8 }} +{{- end -}} + + +{{- define "kube_api_rewriter.service_spec_port_patch_json" -}} + '{{ include "kube_api_rewriter.service_spec_port_patch" . | fromYaml | toJson }}' +{{- end }} + +{{- define "kube_api_rewriter.service_spec_port_patch" -}} +spec: + ports: + - name: {{ include "kube_api_rewriter.webhook_port_name" . }} + port: {{ include "kube_api_rewriter.webhook_port" . }} + protocol: TCP + targetPort: {{ include "kube_api_rewriter.webhook_port_name" . }} +{{- end }} + + +{{- define "kube_api_rewriter.webhook_spec_port_patch_json" -}} + '{{ include "kube_api_rewriter.webhook_spec_port_patch" . | fromYaml | toJson }}' +{{- end }} + +{{- define "kube_api_rewriter.webhook_spec_port_patch" -}} +{{- $webhookNames := list . -}} +{{- if (kindIs "slice" .) -}} +{{- $webhookNames = . -}} +{{- end -}} +webhooks: +{{- range $webhookNames }} +- name: {{ . }} + clientConfig: + service: + port: {{ include "kube_api_rewriter.webhook_port" . }} +{{- end -}} +{{- end -}} diff --git a/templates/kube-api-rewriter/_settings.tpl b/templates/kube-api-rewriter/_settings.tpl new file mode 100644 index 0000000..8f54135 --- /dev/null +++ b/templates/kube-api-rewriter/_settings.tpl @@ -0,0 +1,32 @@ +{{- define "kube_api_rewriter.sidecar_name" -}}proxy{{- end -}} + +{{- define "kube_api_rewriter.webhook_port" -}}24192{{- end -}} + +{{- /* Port name length must be no more than 15 characters. */ -}} +{{- define "kube_api_rewriter.webhook_port_name" -}}webhook-proxy{{- end -}} + +{{- define "kube_api_rewriter.pprof_port" -}}8129{{- end -}} + +{{- define "kube_api_rewriter.env" -}} +- name: LOG_LEVEL + value: {{ include "moduleLogLevel" . }} +{{- if eq (include "moduleLogLevel" .) "debug" }} +- name: PPROF_BIND_ADDRESS + value: ":{{ include "kube_api_rewriter.pprof_port" . }}" +{{- end }} +{{- end -}} + +{{- define "kube_api_rewriter.resources" -}} +cpu: 100m +memory: 30Mi +{{- end -}} + +{{- define "kube_api_rewriter.vpa_container_policy" -}} +- containerName: proxy + minAllowed: + cpu: 10m + memory: 30Mi + maxAllowed: + cpu: 20m + memory: 60Mi +{{- end -}} diff --git a/templates/kube-api-rewriter/_sidecar_helpers.tpl b/templates/kube-api-rewriter/_sidecar_helpers.tpl new file mode 100644 index 0000000..2ae379c --- /dev/null +++ b/templates/kube-api-rewriter/_sidecar_helpers.tpl @@ -0,0 +1,199 @@ +{{- /* Helpers to add kube-api-rewriter sidecar container to a pod. + +To connect to kube-api-rewriter main controller should has KUBECONFIG env, +volumeMount with kubeconfig, and Pod should has volume with kubeconfig ConfigMap. + +These settings are provided by helpers: + +- kube_api_rewriter.kubeconfig_env defines KUBECONFIG env with file from the + mounted ConfigMap. +- kube_api_rewriter.kubeconfig_volume_mount defines volumeMount for kubeconfig ConfigMap. +- kube_api_rewriter.kubeconfig_volume defines volume with kubeconfig ConfigMap. + +Kube-api-rewriter sidecar should be the first container in the Pod, to +main controller not fail on start. + +Kube-api-rewriter sidecar works in 2 modes: without webhook or with webhook rewriting. + +Sidecar without webhook is the simplest one: + +spec: + template: + spec: + containers: + {{ include "kube_api_rewriter.sidecar_container" . | nindent 8 }} + - name: main-controller + ... + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 12 }} + ... + volumeMounts: + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 12 }} + ... + volumes: + {{- include "kube_api_rewriter.kubeconfig_volume" | nindent 8 }} + ... + + +Webhook mode requires additional settings: + +- WEBHOOK_ADDRESS - address of the webhook in the main controller +- WEBHOOK_CERT_FILE - path to the webhook certificate file. +- WEBHOOK_KEY_FILE - path to the webhook key file. +- webhookCertsVolumeName - name of the Pod volume with webhook certificates. +- webhookCertsMountPath - path to mount the webhook certificates. + +The assumption here is that main controller has a webhook server and +certificates are already mounted in the Pod, so kube-api-rewriter +can use certificates from that volume to impersonate the webhook server. + +Example of adding kube-api-rewriter to the Deployment: + +spec: + template: + spec: + containers: + {{- $rewriterSettings := dict }} + {{- $_ := set $rewriterSettings "WEBHOOK_ADDRESS" "https://127.0.0.1:6443" }} + {{- $_ := set $rewriterSettings "WEBHOOK_CERT_FILE" "/etc/webhook-certificates/tls.crt" }} + {{- $_ := set $rewriterSettings "WEBHOOK_KEY_FILE" "/etc/webhook-certificates/tls.key" }} + {{- $_ := set $rewriterSettings "webhookCertsVolumeName" "webhook-certs" }} + {{- $_ := set $rewriterSettings "webhookCertsMountPath" "/etc/webhook-certificates" }} + {{- include "kube_api_rewriter.sidecar_container" (tuple . $rewriterSettings) | nindent 6 }} + - name: main-controller + ... + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 12 }} + ... + ports: + - containerPort: 6443 # Goes to the WEBHOOK_ADDRESS + name: webhooks + protocol: TCP + volumeMounts: + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 12 }} + - name: webhook-certs + mountPath: /etc/webhook-certificates # Goes to the webhookCertsMountPath + readOnly: true + ... + volumes: + {{- include "kube_api_rewriter.kubeconfig_volume" | nindent 8 }} + - name: webhook-certs # Name of the existing volume goes to the webhookCertsVolumeName. + secret: + optional: true + secretName: webhook-certs + ... + + */ -}} + +{{- define "kube_api_rewriter.image" -}} +{{- include "helm_lib_module_image" (list . "kubeApiRewriter") | toJson -}} +{{- end -}} + + +{{- define "kube_api_rewriter.kubeconfig_env" -}} +- name: KUBECONFIG + value: /kubeconfig.local/kube-api-rewriter.kubeconfig +{{- end }} + +{{- define "kube_api_rewriter.kubeconfig_volume" -}} +- name: kube-api-rewriter-kubeconfig + configMap: + defaultMode: 0644 + name: kube-api-rewriter-kubeconfig +{{- end }} + +{{- define "kube_api_rewriter.kubeconfig_volume_mount" -}} +- name: kube-api-rewriter-kubeconfig + mountPath: /kubeconfig.local +{{- end }} + + +{{- define "kube_api_rewriter.webhook_volume_mount" -}} +{{- $volumeName := index . 0 -}} +{{- $mountPath := index . 1 -}} +- mountPath: {{ $mountPath }} + name: {{ $volumeName }} + readOnly: true +{{- end }} + +{{- define "kube_api_rewriter.webhook_container_port" -}} +- containerPort: {{ include "kube_api_rewriter.webhook_port" . }} + name: {{ include "kube_api_rewriter.webhook_port_name" . }} + protocol: TCP +{{- end }} + +{{- /* Container port for the pprof server */ -}} +{{- define "kube_api_rewriter.pprof_container_port" -}} +- containerPort: {{ include "kube_api_rewriter.pprof_port" . }} + name: pprof + protocol: TCP +{{- end }} + +{{- /* Sidecar container spec with kube-api-rewriter */ -}} +{{- /* Usage without the webhook proxy: {{ include kube_api_rewriter.sidecar_container . }} */ -}} +{{- /* Usage with the webhook: {{ include kube_api_rewriter.sidecar_container (tuple . $webhookSettings) }} */ -}} +{{- define "kube_api_rewriter.sidecar_container" -}} + {{- $ctx := . -}} + {{- $settings := dict -}} + {{- if (kindIs "slice" .) -}} + {{- $ctx = index . 0 -}} + {{- if ge (len .) 2 -}} + {{- $settings = index . 1 -}} + {{- end -}} + {{- end -}} + {{- $isWebhook := hasKey $settings "WEBHOOK_ADDRESS" -}} +- name: {{ include "kube_api_rewriter.sidecar_name" $ctx }} + image: {{ include "kube_api_rewriter.image" $ctx }} + imagePullPolicy: IfNotPresent + env: + {{- if $isWebhook }} + - name: WEBHOOK_ADDRESS + value: "{{ $settings.WEBHOOK_ADDRESS }}" + - name: WEBHOOK_CERT_FILE + value: "{{ $settings.WEBHOOK_CERT_FILE }}" + - name: WEBHOOK_KEY_FILE + value: "{{ $settings.WEBHOOK_KEY_FILE }}" + {{- end }} + - name: MONITORING_BIND_ADDRESS + value: "127.0.0.1:9090" + {{- include "kube_api_rewriter.env" $ctx | nindent 4 }} + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 6 }} + {{- if not ( $ctx.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} + {{- include "kube_api_rewriter.resources" . | nindent 6 }} + {{- end }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + livenessProbe: + httpGet: + path: /proxy/healthz + port: 8082 + scheme: HTTPS + initialDelaySeconds: 10 + readinessProbe: + httpGet: + path: /proxy/readyz + port: 8082 + scheme: HTTPS + initialDelaySeconds: 10 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + {{- if $isWebhook }} + volumeMounts: + {{- include "kube_api_rewriter.webhook_volume_mount" (tuple $settings.webhookCertsVolumeName $settings.webhookCertsMountPath) | nindent 4 }} + {{- end }} + ports: + {{- if eq (include "moduleLogLevel" $ctx) "debug" }} + {{- include "kube_api_rewriter.pprof_container_port" . | nindent 4 }} + {{- end }} + {{- if $isWebhook -}} + {{- include "kube_api_rewriter.webhook_container_port" .| nindent 4 }} + {{- end -}} +{{- end -}} diff --git a/templates/kube-api-rewriter/cm-kubeconfig-local.yaml b/templates/kube-api-rewriter/cm-kubeconfig-local.yaml new file mode 100644 index 0000000..966a348 --- /dev/null +++ b/templates/kube-api-rewriter/cm-kubeconfig-local.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: kube-api-rewriter-kubeconfig + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} +data: + kube-api-rewriter.kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + server: http://127.0.0.1:23915 + name: kube-api-rewriter + contexts: + - context: + cluster: kube-api-rewriter + name: kube-api-rewriter + current-context: kube-api-rewriter diff --git a/templates/kube-rbac-proxy/_helpers.tpl b/templates/kube-rbac-proxy/_helpers.tpl new file mode 100644 index 0000000..ee21a1a --- /dev/null +++ b/templates/kube-rbac-proxy/_helpers.tpl @@ -0,0 +1,92 @@ +{{- define "kube_rbac_proxy.sidecar_container" -}} +{{- $ctx := index . 0 }} +{{- $settings := index . 1 }} +- name: {{ $settings.containerName | default "kube-rbac-proxy" }} + {{- include "helm_lib_module_container_security_context_read_only_root_filesystem_capabilities_drop_all_pss_restricted" $ctx | nindent 2 }} + {{- if eq $settings.runAsUserNobody true }} + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + {{- end }} + image: {{ include "helm_lib_module_common_image" (list $ctx "kubeRbacProxy") }} + imagePullPolicy: IfNotPresent + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + args: + - "--secure-listen-address=$(KUBE_RBAC_PROXY_LISTEN_ADDRESS):{{ $settings.listenPort | default "8082" }}" + - "--v={{ $settings.logLevel | default "2" }}" + - "--logtostderr=true" + - "--stale-cache-interval={{ $settings.staleCacheInterval | default "1h30m" }}" + {{- if hasKey $settings "ignorePaths" }} + - "--ignore-paths={{ $settings.ignorePaths }}" + {{- end }} + env: + - name: KUBE_RBAC_PROXY_LISTEN_ADDRESS + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + - name: KUBE_RBAC_PROXY_CONFIG + value: | + excludePaths: + - {{ $settings.excludePath | default "/config" }} + upstreams: + {{- range $settings.upstreams }} + - upstream: {{ .upstream }} + path: {{ .path }} + authorization: + resourceAttributes: + namespace: {{ .namespace | default "d8-operator-helm" }} + apiGroup: {{ .apiGroup | default "apps" }} + apiVersion: {{ .apiVersion | default "v1" }} + resource: {{ .resource | default "deployments" }} + subresource: {{ .subresource | default "prometheus-metrics" }} + name: {{ .name }} + {{- end }} + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" $ctx | nindent 6 }} + {{- if not ( $ctx.Values.global.enabledModules | has "vertical-pod-autoscaler") }} + {{- include "helm_lib_container_kube_rbac_proxy_resources" $ctx | nindent 6 }} + {{- end }} + ports: + - containerPort: {{ $settings.listenPort | default "8082" }} + name: {{ $settings.portName | default "https-metrics" }} + protocol: TCP + livenessProbe: + tcpSocket: + port: {{ $settings.portName | default "https-metrics" }} + initialDelaySeconds: 10 + readinessProbe: + tcpSocket: + port: {{ $settings.portName | default "https-metrics" }} + initialDelaySeconds: 10 +{{- end -}} + +{{- define "kube_rbac_proxy.pod_spec_strategic_patch" -}} +{{- $ctx := index . 0 }} +{{- $settings := index . 1 }} +spec: + template: + spec: + containers: + {{- include "kube_rbac_proxy.sidecar_container" (tuple $ctx $settings) | nindent 6 }} +{{- end }} + +{{- define "kube_rbac_proxy.image" -}} +{{- include "helm_lib_module_common_image" (list . "kubeRbacProxy") -}} +{{- end -}} + +{{- define "kube_rbac_proxy.vpa_container_policy" -}} +- containerName: {{ $.containerName | default "kube-rbac-proxy" }} + minAllowed: + cpu: 10m + memory: 15Mi + maxAllowed: + cpu: 20m + memory: 30Mi +{{- end -}} + +{{- define "kube_rbac_proxy.pod_spec_strategic_patch_json" -}} + '{{ include "kube_rbac_proxy.pod_spec_strategic_patch" . | fromYaml | toJson }}' +{{- end }} diff --git a/templates/namespace.yaml b/templates/namespace.yaml new file mode 100644 index 0000000..c9603eb --- /dev/null +++ b/templates/namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + {{- include "helm_lib_module_labels" (list . (dict "prometheus.deckhouse.io/rules-watcher-enabled" "true")) | nindent 2 }} + name: d8-{{ .Chart.Name }} +--- +{{- include "helm_lib_kube_rbac_proxy_ca_certificate" (list . (printf "d8-%s" .Chart.Name)) }} diff --git a/templates/nelm-source-controller/_helpers.tpl b/templates/nelm-source-controller/_helpers.tpl new file mode 100644 index 0000000..e0b5dc4 --- /dev/null +++ b/templates/nelm-source-controller/_helpers.tpl @@ -0,0 +1,8 @@ +{{- define "nelm-source-controller.envs" -}} +- name: RUNTIME_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace +- name: TUF_ROOT + value: /tmp/.sigstore +{{- end }} diff --git a/templates/nelm-source-controller/deployment.yaml b/templates/nelm-source-controller/deployment.yaml new file mode 100644 index 0000000..f8eef88 --- /dev/null +++ b/templates/nelm-source-controller/deployment.yaml @@ -0,0 +1,143 @@ +{{- $priorityClassName := include "priorityClassName" . }} + +{{- define "nelm_source_controller_resources" }} +cpu: 50m +memory: 64Mi +{{- end }} + +{{- if (.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller" "workload-resource-policy.deckhouse.io" "master")) | nindent 2 }} +spec: + targetRef: + apiVersion: "apps/v1" + kind: Deployment + name: nelm-source-controller + updatePolicy: + updateMode: {{ include "vpa.policyUpdateMode" . }} + resourcePolicy: + containerPolicies: + {{- include "kube_api_rewriter.vpa_container_policy" . | nindent 4 }} + {{- include "kube_rbac_proxy.vpa_container_policy" . | nindent 4 }} + - containerName: nelm-source-controller + minAllowed: + {{- include "nelm_source_controller_resources" . | nindent 8 }} + maxAllowed: + cpu: 1000m + memory: 1Gi +{{- end }} + +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller" )) | nindent 2 }} +spec: + minAvailable: {{ include "helm_lib_is_ha_to_value" (list . 1 0) }} + selector: + matchLabels: + app: nelm-source-controller + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller")) | nindent 2 }} +spec: + replicas: 1 + strategy: + type: Recreate + revisionHistoryLimit: 2 + selector: + matchLabels: + app: nelm-source-controller + template: + metadata: + labels: + app: nelm-source-controller + annotations: + kubectl.kubernetes.io/default-container: nelm-source-controller + spec: + containers: + {{- include "kube_api_rewriter.sidecar_container" . | nindent 8 }} + - name: nelm-source-controller + {{- include "helm_lib_module_container_security_context_read_only_root_filesystem_capabilities_drop_all_pss_restricted" . | nindent 10 }} + image: {{ include "helm_lib_module_image" (list . "nelmSourceController") }} + imagePullPolicy: IfNotPresent + args: + - --watch-all-namespaces + - --log-level={{ include "moduleLogLevel" . }} + - --log-encoding=json + - --enable-leader-election + - --storage-path=/data + - --storage-addr=:9091 + - --storage-adv-addr=nelm-source-controller.$(RUNTIME_NAMESPACE).svc.{{ .Values.global.discovery.clusterDomain }} + volumeMounts: + - mountPath: /data + name: data + - mountPath: /tmp + name: tmp + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 12 }} + ports: + - containerPort: 9091 + name: controller + protocol: TCP + - containerPort: 8080 + name: metrics + protocol: TCP + - containerPort: 9440 + name: healthz + protocol: TCP + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 14 }} + {{- if not ( .Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} + {{- include "nelm_source_controller_resources" . | nindent 14 }} + {{- end }} + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 12 }} + {{- include "nelm-source-controller.envs" . | nindent 12 }} + livenessProbe: + httpGet: + path: /healthz + port: healthz + scheme: HTTP + initialDelaySeconds: 10 + readinessProbe: + httpGet: + path: / + port: controller + scheme: HTTP + initialDelaySeconds: 10 + {{- $kubeRbacProxySettings := dict }} + {{- $_ := set $kubeRbacProxySettings "runAsUserNobody" false }} + {{- $_ := set $kubeRbacProxySettings "ignorePaths" "/proxy/healthz,/proxy/readyz" }} + {{- $_ := set $kubeRbacProxySettings "upstreams" (list + (dict "upstream" "http://127.0.0.1:8080/metrics" "path" "/metrics" "name" "nelm-source-controller") + (dict "upstream" "http://127.0.0.1:9090/metrics" "path" "/proxy/metrics" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/healthz" "path" "/proxy/healthz" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/readyz" "path" "/proxy/readyz" "name" "kube-api-rewriter") + ) }} + {{- include "kube_rbac_proxy.sidecar_container" (tuple . $kubeRbacProxySettings) | nindent 8 }} + dnsPolicy: ClusterFirst + serviceAccountName: nelm-source-controller + {{- include "helm_lib_module_pod_security_context_run_as_user_deckhouse" . | nindent 6 }} + {{- include "helm_lib_priority_class" (tuple . $priorityClassName) | nindent 6 }} + {{- include "helm_lib_node_selector" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_tolerations" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_pod_anti_affinity_for_ha" (list . (dict "app" "nelm-source-controller")) | nindent 6 }} + volumes: + - emptyDir: {} + name: data + - emptyDir: {} + name: tmp + {{- include "kube_api_rewriter.kubeconfig_volume" . | nindent 8 }} diff --git a/templates/nelm-source-controller/rbac-for-us.yaml b/templates/nelm-source-controller/rbac-for-us.yaml new file mode 100644 index 0000000..a05bf0e --- /dev/null +++ b/templates/nelm-source-controller/rbac-for-us.yaml @@ -0,0 +1,169 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller")) | nindent 2 }} +imagePullSecrets: +- name: operator-helm-module-registry +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:nelm-source-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: d8:{{ .Chart.Name }}:nelm-source-controller +subjects: +- kind: ServiceAccount + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:nelm-source-controller +rules: +- apiGroups: + - "" + resources: + - pods + - services + - secrets + - configmaps + verbs: + - get + - create + - update + - delete + - list + - watch + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets + - internalnelmoperatorgitrepositories + - internalnelmoperatorhelmcharts + - internalnelmoperatorhelmrepositories + - internalnelmoperatorocirepositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets/finalizers + - internalnelmoperatorgitrepositories/finalizers + - internalnelmoperatorhelmcharts/finalizers + - internalnelmoperatorhelmrepositories/finalizers + - internalnelmoperatorocirepositories/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets/status + - internalnelmoperatorgitrepositories/status + - internalnelmoperatorhelmcharts/status + - internalnelmoperatorhelmrepositories/status + - internalnelmoperatorocirepositories/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create +- apiGroups: + - "coordination.k8s.io" + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: nelm-source-controller +subjects: +- kind: ServiceAccount + name: nelm-source-controller diff --git a/templates/nelm-source-controller/service-metrics.yaml b/templates/nelm-source-controller/service-metrics.yaml new file mode 100644 index 0000000..dff6d72 --- /dev/null +++ b/templates/nelm-source-controller/service-metrics.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: nelm-source-controller-metrics + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller")) | nindent 2 }} +spec: + ports: + - name: metrics + port: 8080 + protocol: TCP + targetPort: https-metrics + selector: + app: nelm-source-controller diff --git a/templates/nelm-source-controller/service-monitor.yaml b/templates/nelm-source-controller/service-monitor.yaml new file mode 100644 index 0000000..6538666 --- /dev/null +++ b/templates/nelm-source-controller/service-monitor.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: nelm-source-controller + namespace: d8-monitoring + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller" "prometheus" "main")) | nindent 2 }} +spec: + endpoints: + - bearerTokenSecret: + key: token + name: prometheus-token + path: /metrics + port: metrics + scheme: https + tlsConfig: + insecureSkipVerify: true + namespaceSelector: + matchNames: + - d8-{{ .Chart.Name }} + selector: + matchLabels: + app: "nelm-source-controller" diff --git a/templates/nelm-source-controller/service.yaml b/templates/nelm-source-controller/service.yaml new file mode 100644 index 0000000..1d1bfa2 --- /dev/null +++ b/templates/nelm-source-controller/service.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller")) | nindent 2 }} +spec: + ports: + - name: controller + port: 80 + targetPort: controller + protocol: TCP + selector: + app: nelm-source-controller diff --git a/templates/operator-helm-controller/deployment.yaml b/templates/operator-helm-controller/deployment.yaml new file mode 100644 index 0000000..7e01c31 --- /dev/null +++ b/templates/operator-helm-controller/deployment.yaml @@ -0,0 +1,141 @@ +{{- $priorityClassName := include "priorityClassName" . }} + +{{- define "operator_helm_controller_resources" }} +cpu: 50m +memory: 64Mi +{{- end }} + +{{- if (.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller" "workload-resource-policy.deckhouse.io" "master")) | nindent 2 }} +spec: + targetRef: + apiVersion: "apps/v1" + kind: Deployment + name: operator-helm-controller + updatePolicy: + updateMode: {{ include "vpa.policyUpdateMode" . }} + resourcePolicy: + containerPolicies: + {{- include "kube_api_rewriter.vpa_container_policy" . | nindent 4 }} + {{- include "kube_rbac_proxy.vpa_container_policy" . | nindent 4 }} + - containerName: operator-helm-controller + minAllowed: + {{- include "operator_helm_controller_resources" . | nindent 8 }} + maxAllowed: + cpu: 1000m + memory: 1Gi +{{- end }} + +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller" )) | nindent 2 }} +spec: + minAvailable: {{ include "helm_lib_is_ha_to_value" (list . 1 0) }} + selector: + matchLabels: + app: operator-helm-controller + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +spec: + replicas: {{ include "helm_lib_is_ha_to_value" (list . 3 1) }} + {{- if (include "helm_lib_ha_enabled" .) }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + {{- end }} + revisionHistoryLimit: 2 + selector: + matchLabels: + app: operator-helm-controller + template: + metadata: + labels: + app: operator-helm-controller + annotations: + kubectl.kubernetes.io/default-container: operator-helm-controller + spec: + containers: + {{- include "kube_api_rewriter.sidecar_container" . | nindent 8 }} + - name: operator-helm-controller + {{- include "helm_lib_module_container_security_context_read_only_root_filesystem_capabilities_drop_all_pss_restricted" . | nindent 10 }} + image: {{ include "helm_lib_module_image" (list . "operatorHelmController") }} + imagePullPolicy: IfNotPresent + args: + {{/* TODO: add log level option */}} + - --leader-elect + - --metrics-bind-address=:8080 + - --health-probe-bind-address=:9440 + volumeMounts: + - 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..7bc6f85 --- /dev/null +++ b/templates/operator-helm-controller/rbac-for-us.yaml @@ -0,0 +1,216 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +imagePullSecrets: +- name: operator-helm-module-registry +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:operator-helm-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: d8:{{ .Chart.Name }}:operator-helm-controller +subjects: +- kind: ServiceAccount + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:operator-helm-controller +rules: +- apiGroups: + - "" + resources: + - pods + - services + - secrets + - configmaps + verbs: + - get + - create + - update + - delete + - list + - watch + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - helm.deckhouse.io + resources: + - helmclusteraddons + - helmclusteraddons/status + - helmclusteraddoncharts + - helmclusteraddoncharts/status + - helmclusteraddonrepositories + - helmclusteraddonrepositories/status + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases/status + verbs: + - get + - patch + - update +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets + - internalnelmoperatorgitrepositories + - internalnelmoperatorhelmcharts + - internalnelmoperatorhelmrepositories + - internalnelmoperatorocirepositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets/finalizers + - internalnelmoperatorgitrepositories/finalizers + - internalnelmoperatorhelmcharts/finalizers + - internalnelmoperatorhelmrepositories/finalizers + - internalnelmoperatorocirepositories/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets/status + - internalnelmoperatorgitrepositories/status + - internalnelmoperatorhelmcharts/status + - internalnelmoperatorhelmrepositories/status + - internalnelmoperatorocirepositories/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create +- apiGroups: + - "coordination.k8s.io" + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: operator-helm-controller +subjects: +- kind: ServiceAccount + name: operator-helm-controller diff --git a/templates/operator-helm-controller/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/tmp/mc-operator-helm.yaml b/tmp/mc-operator-helm.yaml new file mode 100644 index 0000000..24275b2 --- /dev/null +++ b/tmp/mc-operator-helm.yaml @@ -0,0 +1,8 @@ +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: operator-helm +spec: + enabled: false + source: operator-helm + version: 1 diff --git a/tmp/modulepulloverride.yaml b/tmp/modulepulloverride.yaml new file mode 100644 index 0000000..9f2f086 --- /dev/null +++ b/tmp/modulepulloverride.yaml @@ -0,0 +1,8 @@ +apiVersion: deckhouse.io/v1alpha2 +kind: ModulePullOverride +metadata: + name: operator-helm +spec: + imageTag: mvp + rollback: true + scanInterval: 15s diff --git a/tmp/modulesource.yaml b/tmp/modulesource.yaml new file mode 100644 index 0000000..9bf6aa5 --- /dev/null +++ b/tmp/modulesource.yaml @@ -0,0 +1,8 @@ +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleSource +metadata: + name: operator-helm +spec: + registry: + repo: ghcr.io/deckhouse/operator-helm + scheme: HTTPS diff --git a/tools/validation/diff.go b/tools/validation/diff.go new file mode 100644 index 0000000..516388b --- /dev/null +++ b/tools/validation/diff.go @@ -0,0 +1,149 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "fmt" + "io" + "regexp" + "strings" +) + +type DiffInfo struct { + Files []*DiffFileInfo +} + +func NewDiffInfo() *DiffInfo { + return &DiffInfo{ + Files: make([]*DiffFileInfo, 0), + } +} + +func (d *DiffInfo) Dump() string { + res := "" + for _, info := range d.Files { + res += fmt.Sprintf("%s -> %s, lines: %d\n", info.OldFileName, info.NewFileName, len(info.Lines)) + } + res += fmt.Sprintf("files: %d\n", len(d.Files)) + return res +} + +type DiffFileInfo struct { + NewFileName string + OldFileName string + Lines []string +} + +func (d *DiffFileInfo) IsAdded() bool { + return d.OldFileName == "/dev/null" +} + +func (d *DiffFileInfo) IsDeleted() bool { + return d.NewFileName == "/dev/null" +} + +func (d *DiffFileInfo) IsModified() bool { + return d.OldFileName != "/dev/null" && d.NewFileName != "/dev/null" && d.HasContent() +} + +func (d *DiffFileInfo) HasContent() bool { + return len(d.Lines) > 0 +} + +func (d *DiffFileInfo) NewLines() []string { + res := make([]string, 0) + for _, l := range d.Lines { + if strings.HasPrefix(l, "+") { + res = append(res, strings.TrimPrefix(l, "+")) + } + } + return res +} + +func NewDiffFileInfo() *DiffFileInfo { + return &DiffFileInfo{ + Lines: make([]string, 0), + } +} + +var diffStartRe = regexp.MustCompile(`^diff --git a/(.*) b/(.*)$`) +var oldFileNameRe = regexp.MustCompile(`^--- (/dev/null|a/(.*))$`) +var newFileNameRe = regexp.MustCompile(`^\+\+\+ (/dev/null|b/(.*))$`) +var endMetadataRe = regexp.MustCompile(`^@@[\-+ \d,]+@@(.*)$`) + +func ParseDiffOutput(r io.Reader) (*DiffInfo, error) { + res := NewDiffInfo() + tmp := NewDiffFileInfo() + firstLine := true + scanner := bufio.NewScanner(r) + metadataBlock := false + for scanner.Scan() { + text := scanner.Text() + + if diffStartRe.MatchString(text) { + if firstLine { + firstLine = false + } else { + // Append diffFileInfo when all lines are gathered and new diffFIleInfo is detected. + res.Files = append(res.Files, tmp) + tmp = NewDiffFileInfo() + } + metadataBlock = true + continue + } + + matches := newFileNameRe.FindStringSubmatch(text) + if len(matches) > 1 { + if matches[1] == "/dev/null" { + tmp.NewFileName = matches[1] + } else { + tmp.NewFileName = matches[2] + } + continue + } + + matches = oldFileNameRe.FindStringSubmatch(text) + if len(matches) > 1 { + if matches[1] == "/dev/null" { + tmp.OldFileName = matches[1] + } else { + tmp.OldFileName = matches[2] + } + continue + } + + if metadataBlock { + matches = endMetadataRe.FindStringSubmatch(text) + if len(matches) > 1 { + tmp.Lines = append(tmp.Lines, matches[1]) + metadataBlock = false + continue + } + } + + if !metadataBlock { + tmp.Lines = append(tmp.Lines, text) + } + } + // Push last diff info. + if tmp != nil { + res.Files = append(res.Files, tmp) + } + + return res, nil +} diff --git a/tools/validation/doc_changes.go b/tools/validation/doc_changes.go new file mode 100644 index 0000000..08c9c2c --- /dev/null +++ b/tools/validation/doc_changes.go @@ -0,0 +1,143 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "os" + "regexp" + "strings" +) + +var ( + resourceFileRe = regexp.MustCompile(`openapi/config-values.y[a]?ml$|crds/.+.y[a]?ml$`) + docFileRe = regexp.MustCompile(`\.md$`) + + excludeFileRe = regexp.MustCompile("crds/embedded/.+.y[a]?ml$") +) + +func RunDocChangesValidation(info *DiffInfo) (exitCode int) { + fmt.Printf("Run 'doc changes' validation ...\n") + + if len(info.Files) == 0 { + fmt.Printf("Nothing to validate, diff is empty\n") + return 0 + } + + exitCode = 0 + msgs := NewMessages() + for _, fileInfo := range info.Files { + if !fileInfo.HasContent() { + continue + } + + fileName := fileInfo.NewFileName + + if strings.Contains(fileName, "testdata") { + msgs.Add(NewSkip(fileName, "")) + continue + } + + if docFileRe.MatchString(fileName) { + msgs.Add(checkDocFile(fileName, info)) + continue + } + + if resourceFileRe.MatchString(fileName) && !excludeFileRe.MatchString(fileName) { + msgs.Add(checkResourceFile(fileName, info)) + continue + } + + msgs.Add(NewSkip(fileName, "")) + } + msgs.PrintReport() + + if msgs.CountErrors() > 0 { + exitCode = 1 + } + + return exitCode +} + +var possibleDocRootsRe = regexp.MustCompile(`docs/|docs/documentation`) +var docsDirAllowedFileRe = regexp.MustCompile(`docs/(CONFIGURATION|CR|FAQ|README|ADMIN_GUIDE|USER_GUIDE|CHARACTERISTICS_DESCRIPTION|INSTALL|RELEASE_NOTES)(\.ru)?.md`) +var docsDirFileRe = regexp.MustCompile(`docs/[^/]+.md`) + +func checkDocFile(fName string, diffInfo *DiffInfo) (msg Message) { + if !possibleDocRootsRe.MatchString(fName) { + return NewSkip(fName, "") + } + + if docsDirFileRe.MatchString(fName) && !docsDirAllowedFileRe.MatchString(fName) { + return NewError( + fName, + "name is not allowed", + `Rename this file or move it, for example, into 'internal' folder. +Only following file names are allowed in the module '/docs/' directory: + CLUSTER_CONFIGURATION.md + CONFIGURATION.md + CR.md + FAQ.md + README.md + RELEASE_NOTES.md + ADMIN_GUIDE.md + USER_GUIDE.md + CHARACTERISTICS_DESCRIPTION.md +(also their Russian versions ended with '.ru.md')`, + ) + } + + // Check if documentation for other language file is also modified. + var otherFileName = fName + if strings.HasSuffix(fName, `.ru.md`) { + otherFileName = strings.TrimSuffix(fName, ".ru.md") + ".md" + } else { + otherFileName = strings.TrimSuffix(fName, ".md") + ".ru.md" + } + return checkRelatedFileExists(fName, otherFileName, diffInfo) +} + +var docRuResourceRe = regexp.MustCompile(`doc-ru-.+.y[a]?ml$`) +var notDocRuResourceRe = regexp.MustCompile(`([^/]+\.y[a]?ml)$`) + +// Check if resource for other language is also modified. +func checkResourceFile(fName string, diffInfo *DiffInfo) (msg Message) { + otherFileName := fName + if docRuResourceRe.MatchString(fName) { + otherFileName = strings.Replace(fName, "doc-ru-", "", 1) + } else { + otherFileName = notDocRuResourceRe.ReplaceAllString(fName, `doc-ru-$1`) + } + return checkRelatedFileExists(fName, otherFileName, diffInfo) +} + +func checkRelatedFileExists(origName string, otherName string, diffInfo *DiffInfo) Message { + file, err := os.Open(otherName) + if err != nil { + return NewError(origName, "related is absent", fmt.Sprintf(`Documentation or resource file is changed +while related language file '%s' is absent.`, otherName)) + } + defer file.Close() + + for _, fileInfo := range diffInfo.Files { + if fileInfo.NewFileName == otherName { + return NewOK(origName) + } + } + return NewError(origName, "related not changed", fmt.Sprintf(`Documentation or resource file is changed +while related language file '%s' is not changed`, otherName)) +} diff --git a/tools/validation/go.mod b/tools/validation/go.mod new file mode 100644 index 0000000..3102e06 --- /dev/null +++ b/tools/validation/go.mod @@ -0,0 +1,3 @@ +module validation + +go 1.21.4 diff --git a/tools/validation/main.go b/tools/validation/main.go new file mode 100644 index 0000000..4829162 --- /dev/null +++ b/tools/validation/main.go @@ -0,0 +1,97 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "flag" + "fmt" + "os" + "os/exec" +) + +func main() { + var validationType string + flag.StringVar(&validationType, "type", "", "Validation type: cyrillic or doc-changes.") + var patchFile string + flag.StringVar(&patchFile, "file", "", "Patch file. git diff is executed if not passed.") + var title string + flag.StringVar(&title, "title", "", "Title string to check for cyrillic letters.") + var description string + flag.StringVar(&description, "description", "", "Description string to check for cyrillic letters.") + flag.Parse() + + var diffInfo *DiffInfo + var err error + if patchFile != "" { + // Parse file content. + diffInfo, err = readFile(patchFile) + if err != nil { + fmt.Printf("Read file '%s': %v", patchFile, err) + os.Exit(1) + } + } else { + // Parse 'git diff' output. + fmt.Printf("Run git diff ...\n") + diffInfo, err = executeGitDiff() + if err != nil { + fmt.Printf("Execute git diff: %v", err) + os.Exit(1) + } + } + + exitCode := 0 + switch validationType { + case "no-cyrillic": + exitCode = RunNoCyrillicValidation(diffInfo, title, description) + case "doc-changes": + exitCode = RunDocChangesValidation(diffInfo) + case "dump": + fmt.Printf("%s\n", diffInfo.Dump()) + default: + fmt.Printf("Unknown validation type '%s'\n", validationType) + os.Exit(2) + } + + if exitCode == 0 { + fmt.Printf("Validation successful.\n") + } else { + fmt.Printf("Validation failed.\n") + } + os.Exit(exitCode) +} + +func readFile(fName string) (*DiffInfo, error) { + content, err := os.ReadFile(fName) + if err != nil { + return nil, err + } + + br := bytes.NewReader(content) + return ParseDiffOutput(br) +} + +func executeGitDiff() (*DiffInfo, error) { + gitCmd := exec.Command("git", "diff", "origin/main...", "-w", "--ignore-blank-lines") + out, err := gitCmd.Output() + if err != nil { + return nil, err + } + + br := bytes.NewReader(out) + return ParseDiffOutput(br) +} diff --git a/tools/validation/messages.go b/tools/validation/messages.go new file mode 100644 index 0000000..6cd933a --- /dev/null +++ b/tools/validation/messages.go @@ -0,0 +1,176 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "strings" +) + +const OKType = "OK" +const SkipType = "Skip" +const ErrorType = "ERROR" + +type Message struct { + Type string + FileName string + Message string + Details string +} + +func NewOK(fileName string) Message { + return Message{ + Type: OKType, + FileName: fileName, + } +} + +func NewSkip(fileName string, msg string) Message { + return Message{ + Type: SkipType, + FileName: fileName, + Message: msg, + } +} + +func NewError(fileName string, msg string, details string) Message { + return Message{ + Type: ErrorType, + FileName: fileName, + Message: msg, + Details: details, + } +} + +func (msg Message) Format() string { + res := "" + if msg.Message == "" { + res += fmt.Sprintf(" * %s ... %s", msg.FileName, msg.Type) + } else { + res += fmt.Sprintf(" * %s ... %s: %s", msg.FileName, msg.Type, msg.Message) + } + if msg.Details != "" { + res += "\n" + indentTextBlock(msg.Details, 6) + } + return res +} + +func (msg Message) IsError() bool { + return msg.Type == ErrorType +} + +func (msg Message) IsSkip() bool { + return msg.Type == SkipType +} + +func (msg Message) IsOK() bool { + return msg.Type == OKType +} + +type Messages struct { + messages []Message +} + +func NewMessages() *Messages { + return &Messages{ + messages: make([]Message, 0), + } +} + +func (m *Messages) Add(msg Message) { + m.messages = append(m.messages, msg) +} + +func (m *Messages) Join(msgs *Messages) { + if msgs == nil { + return + } + for _, message := range msgs.messages { + m.Add(message) + } +} + +func (m *Messages) CountOK() int { + res := 0 + for _, msg := range m.messages { + if msg.IsOK() { + res++ + } + } + return res +} + +func (m *Messages) CountSkip() int { + res := 0 + for _, msg := range m.messages { + if msg.IsSkip() { + res++ + } + } + return res +} + +func (m *Messages) CountErrors() int { + res := 0 + for _, msg := range m.messages { + if msg.IsError() { + res++ + } + } + return res +} + +func (m *Messages) PrintReport() { + if m.CountSkip() > 0 { + fmt.Println("Skipped:") + for _, msg := range m.messages { + if msg.IsSkip() { + fmt.Println(msg.Format()) + } + } + } + if m.CountOK() > 0 { + fmt.Println("OK:") + for _, msg := range m.messages { + if msg.IsOK() { + fmt.Println(msg.Format()) + } + } + } + if m.CountErrors() > 0 { + fmt.Println("ERRORS:") + for _, msg := range m.messages { + if msg.IsError() { + fmt.Println(msg.Format()) + } + } + } +} + +func indentTextBlock(msg string, n int) string { + lines := strings.Split(msg, "\n") + var b strings.Builder + for i, line := range lines { + // leading newline and newlines between lines + if i > 0 { + b.WriteString("\n") + } + b.WriteString(strings.Repeat(" ", n)) + b.WriteString(line) + } + return b.String() +} diff --git a/tools/validation/no_cyrillic.go b/tools/validation/no_cyrillic.go new file mode 100644 index 0000000..d41c65a --- /dev/null +++ b/tools/validation/no_cyrillic.go @@ -0,0 +1,160 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "regexp" + "strings" +) + +var skipDocRe = regexp.MustCompile(`doc-ru-.+\.y[a]?ml$|\.ru\.md$`) +var skipI18NRe = regexp.MustCompile(`/i18n/`) +var skipSelfRe = regexp.MustCompile(`no_cyrillic(_test)?.go$`) + +func RunNoCyrillicValidation(info *DiffInfo, title string, description string) (exitCode int) { + fmt.Printf("Run 'no cyrillic' validation ...\n") + + exitCode = 0 + if title != "" { + fmt.Printf("Check title ... ") + msg, hasCyr := checkCyrillicLetters(title) + if hasCyr { + fmt.Printf("ERROR\n%s\n", msg) + exitCode = 1 + } else { + fmt.Printf("OK\n") + } + } + if description != "" { + // Here put cyrillic char -> C + fmt.Printf("Check description Сахар... ") + msg, hasCyr := checkCyrillicLetters(description) + if hasCyr { + fmt.Printf("ERROR\n%s\n", msg) + exitCode = 1 + } else { + fmt.Printf("OK\n") + } + } + // Some fishka + fmt.Printf("Check new and updated lines ... ") + if len(info.Files) == 0 { + fmt.Printf("OK, diff is empty\n") + } else { + fmt.Println("") + + msgs := NewMessages() + + //hasErrors := false + for _, fileInfo := range info.Files { + if !fileInfo.HasContent() { + continue + } + // Check only added or modified files + if !(fileInfo.IsAdded() || fileInfo.IsModified()) { + continue + } + + fileName := fileInfo.NewFileName + + if skipDocRe.MatchString(fileName) { + msgs.Add(NewSkip(fileName, "documentation")) + continue + } + + if skipI18NRe.MatchString(fileName) { + msgs.Add(NewSkip(fileName, "translation file")) + continue + } + + if skipSelfRe.MatchString(fileName) { + msgs.Add(NewSkip(fileName, "self")) + continue + } + + // Get added or modified lines + newLines := fileInfo.NewLines() + if len(newLines) == 0 { + msgs.Add(NewSkip(fileName, "no lines added")) + continue + } + + cyrMsg, hasCyr := checkCyrillicLettersInArray(newLines) + if hasCyr { + msgs.Add(NewError(fileName, "should not contain Cyrillic letters", cyrMsg)) + continue + } + + msgs.Add(NewOK(fileName)) + } + + msgs.PrintReport() + + if msgs.CountErrors() > 0 { + exitCode = 1 + } + } + + return exitCode +} + +var cyrRe = regexp.MustCompile(`[А-Яа-яЁё]+`) +var cyrPointerRe = regexp.MustCompile(`[А-Яа-яЁё]`) +var cyrFillerRe = regexp.MustCompile(`[^А-Яа-яЁё]`) + +func checkCyrillicLetters(in string) (string, bool) { + if strings.Contains(in, "\n") { + return checkCyrillicLettersInArray(strings.Split(in, "\n")) + } + return checkCyrillicLettersInString(in) +} + +// checkCyrillicLettersInString returns a fancy message if input string contains Cyrillic letters. +func checkCyrillicLettersInString(line string) (string, bool) { + if !cyrRe.MatchString(line) { + return "", false + } + + // Replace all tabs with spaces to prevent shifted cursor. + line = strings.Replace(line, "\t", " ", -1) + + // Make string with pointers to Cyrillic letters so user can detect hidden letters. + cursor := cyrFillerRe.ReplaceAllString(line, "-") + cursor = cyrPointerRe.ReplaceAllString(cursor, "^") + cursor = strings.TrimRight(cursor, "-") + + const formatPrefix = " " + + return formatPrefix + line + "\n" + formatPrefix + cursor, true +} + +// checkCyrillicLettersInArray returns a fancy message for each string in array that contains Cyrillic letters. +func checkCyrillicLettersInArray(lines []string) (string, bool) { + res := make([]string, 0) + + hasCyr := false + for _, line := range lines { + msg, has := checkCyrillicLettersInString(line) + if has { + hasCyr = true + res = append(res, msg) + } + } + + return strings.Join(res, "\n"), hasCyr +} diff --git a/tools/validation/no_cyrillic_test.go b/tools/validation/no_cyrillic_test.go new file mode 100644 index 0000000..f239014 --- /dev/null +++ b/tools/validation/no_cyrillic_test.go @@ -0,0 +1,96 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "strings" + "testing" +) + +func Test_found_msg(t *testing.T) { + // Simple check with one Cyrillic letter. + in := "fooБfoo" + expected := ` fooБfoo + ---^` + + actual, has := checkCyrillicLetters(in) + + if !has { + t.Errorf("Should detect cyrillic letters in string") + } + + if actual != expected { + t.Errorf("Expect '%s', got '%s'", expected, actual) + } + + // No Cyrillic letters. + in = "asdqwe 123456789 !@#$%^&*( ZXCVBNM" + expected = "" + actual, has = checkCyrillicLetters(in) + + if has { + t.Errorf("Should not detect cyrillic letters in string") + } + + if actual != expected { + t.Errorf("Expect '%s', got '%s'", expected, actual) + } + + // Multiple words with Cyrillic letters. + in = "asdqwe Там на qw q cheсk tеst qwd неведомых qqw" + expected = + " asdqwe Там на qw q cheсk tеst qwd неведомых qqw\n" + + " -------^^^-^^---------^---^-------^^^^^^^^^" + + actual, has = checkCyrillicLetters(in) + + if !has { + t.Errorf("Should detect cyrillic letters in string") + } + + if actual != expected { + fmt.Printf(" %s\n%s\n", + strings.Repeat("0123456789", len(actual)/2/10+1), + actual) + t.Errorf("Expect \n%s\n, got \n%s\n", expected, actual) + } + + // Multiple messages for string with '\n'. + in = "Lorem ipsum dolor sit amet,\n consectetur adipiscing elit,\n" + + "раскрою перед вами всю \nкартину и разъясню," + + "Ut enim ad minim veniam," + expected = + " раскрою перед вами всю \n" + + " ^^^^^^^-^^^^^-^^^^-^^^\n" + + " картину и разъясню,Ut enim ad minim veniam,\n" + + " ^^^^^^^-^-^^^^^^^^" + + actual, has = checkCyrillicLetters(in) + + if !has { + t.Errorf("Should detect cyrillic letters in string") + } + + if actual != expected { + fmt.Printf(" %s\n%s\n", + strings.Repeat("0123456789", len(actual)/2/10+1), + actual) + t.Errorf("Expect \n%s\n, got \n%s\n", expected, actual) + } + +} diff --git a/werf-giterminism.yaml b/werf-giterminism.yaml new file mode 100644 index 0000000..250f208 --- /dev/null +++ b/werf-giterminism.yaml @@ -0,0 +1,28 @@ +giterminismConfigVersion: 1 +config: + goTemplateRendering: # The rules for the Go-template functions + allowEnvVariables: + - /CI_.+/ + - GOPROXY + - MODULES_MODULE_TAG + - SOURCE_REPO + - SOURCE_REPO_GIT + - MODULE_EDITION + - DISTRO_PACKAGES_PROXY + - SVACE_ENABLED + - SVACE_ANALYZE_HOST + - SVACE_ANALYZE_SSH_USER + - DEBUG_COMPONENT + stapel: + mount: + allowBuildDir: true + allowFromPaths: + - ~/go-pkg-cache + secrets: + allowValueIds: + - SOURCE_REPO + - GOPROXY +helm: + allowUncommittedFiles: + - "Chart.lock" + - "charts/*.tgz" diff --git a/werf.yaml b/werf.yaml new file mode 100644 index 0000000..5b2d5a1 --- /dev/null +++ b/werf.yaml @@ -0,0 +1,114 @@ +project: operator-helm +configVersion: 1 +build: + imageSpec: + author: "Deckhouse Kubernetes Platform " + clearHistory: true + config: + keepEssentialWerfLabels: true + removeLabels: + - /.*/ +--- +# Base Images +{{- include "parse_base_images_map" . }} +--- +# Source repo settings +{{- $_ := set . "SOURCE_REPO" (env "SOURCE_REPO" "https://github.com") }} + +{{- $_ := set . "SOURCE_REPO_GIT" (env "SOURCE_REPO_GIT" "https://github.com") }} + +# Define packages proxy settings +{{- $_ := set . "DistroPackagesProxy" (env "DISTRO_PACKAGES_PROXY" "") }} + + +# svace analyze toggler +{{- $_ := set . "SVACE_ENABLED" (env "SVACE_ENABLED" "false") }} +{{- $_ := set . "SVACE_ANALYZE_HOST" (env "SVACE_ANALYZE_HOST" "example.host") }} +{{- $_ := set . "SVACE_ANALYZE_SSH_USER" (env "SVACE_ANALYZE_SSH_USER" "user") }} + +{{- $_ := set . "ImagesIDList" list }} + +{{- range $path, $content := .Files.Glob ".werf/*.yaml" }} + {{- tpl $content $ }} +{{- end }} +--- +image: images-digests +fromImage: builder/alpine +dependencies: + {{- range $ImageID := $.ImagesIDList }} + {{- $ImageNameCamel := $ImageID | splitList "/" | last | camelcase | untitle }} +- image: {{ $ImageID }} + before: setup + imports: + - type: ImageDigest + targetEnv: MODULE_IMAGE_DIGEST_{{ $ImageNameCamel }} + {{- end }} +shell: + beforeInstall: + - apk add --no-cache jq + setup: + - | + env | grep MODULE_IMAGE_DIGEST | jq -Rn ' + reduce inputs as $i ( + {}; + . * ( + $i | ltrimstr("MODULE_IMAGE_DIGEST_") | sub("=";"_") | + split("_") as [$imageName, $digest] | + {($imageName): $digest} + ) + ) + ' > /images_digests.json + cat images_digests.json +--- +image: bundle +fromImage: builder/scratch +import: +- image: prepare-bundle + add: /prep-bundle + to: / + after: setup +--- +image: prepare-bundle +fromImage: builder/alpine +import: +- image: images-digests + add: / + to: /prep-bundle + after: setup + includePaths: + - images_digests.json +- image: go-hooks-artifact + add: /go-hooks + to: /prep-bundle/hooks/go + after: setup +git: + - add: / + to: /prep-bundle + stageDependencies: + install: + - '**/*' + includePaths: + - charts + - crds + - build/components + - docs + - openapi + - monitoring + - templates + - Chart.yaml + - module.yaml + - .helmignore + excludePaths: + - build/components/README.md + - docs/images/*.drawio + - docs/images/*.sh + - docs/internal +shell: + install: + - ls -la /prep-bundle +--- +image: release-channel-version +fromImage: builder/scratch +shell: + install: + - echo '{"version":"{{ env "MODULES_MODULE_TAG" "dev" }}"}' > version.json diff --git a/werf_cleanup.yaml b/werf_cleanup.yaml new file mode 100644 index 0000000..9ca7769 --- /dev/null +++ b/werf_cleanup.yaml @@ -0,0 +1,18 @@ +configVersion: 1 +project: operator-helm +cleanup: + keepPolicies: + - references: + tag: /.*/ + limit: + in: 72h + - references: + branch: /.*/ + limit: + in: 168h # keep dev images build during last week which not main|pre-alpha + - references: + branch: /main|release-[0-9]+.*/ + limit: + last: 5 # keep 5 images for branches release-* and main + imagesPerReference: + last: 1