diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..9b1c8b133 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +/dist diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..15944daa0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: kluctl diff --git a/.github/ISSUE_TEMPLATE/FEATURE.yml b/.github/ISSUE_TEMPLATE/FEATURE.yml index 4bdf4b33c..fb5835416 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE.yml @@ -16,12 +16,10 @@ body: label: Command description: Which command will be affected by the feature request? (If you want to propose a new command, just leave all unchecked) options: - - label: "archive" - label: "check-image-updates" - label: "delete" - label: "deploy" - label: "diff" - - label: "downscale" - label: "helm-pull" - label: "helm-update" - label: "list-images" @@ -29,7 +27,6 @@ body: - label: "poke-images" - label: "prune" - label: "render" - - label: "seal" - label: "validate" - label: "version" - type: input diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..9c5eb1995 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,36 @@ +version: 2 + +updates: + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + groups: + golang-x: + patterns: + - "golang.org/x/*" + k8s-io: + patterns: + - "k8s.io/*" + aws-sdk: + patterns: + - "*aws-sdk*" + azure-sdk: + patterns: + - "*azure-sdk*" + - package-ecosystem: "gomod" + directory: "/lib" + schedule: + interval: "daily" + groups: + all: + patterns: + - "*" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index dc3dd31cb..205acfcdf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,6 +14,7 @@ Please delete options that are not relevant. - [ ] This change requires a documentation update - [ ] This change requires a new example + diff --git a/.github/workflows/check-links-cron.yml b/.github/workflows/check-links-cron.yml new file mode 100644 index 000000000..11cc27391 --- /dev/null +++ b/.github/workflows/check-links-cron.yml @@ -0,0 +1,18 @@ +name: Check Markdown links + +on: + push: + branches: + - main + schedule: + # Run everyday at 9:00 AM (See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07) + - cron: "0 9 * * *" + +jobs: + markdown-link-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check links on changed files + run: | + make markdown-link-check diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 000000000..68367793d --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,62 @@ +name: goreleaser-nightly + +on: + push: + branches: + - main + +permissions: + contents: write + packages: write + +concurrency: + group: goreleaser-nightly + cancel-in-progress: true + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch all tags + run: git fetch --force --tags + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: "**/*.sum" + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: pkg/webui/ui/package-lock.json + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + - name: Setup Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + - name: Setup Syft + uses: anchore/sbom-action/download-syft@v0 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: kluctlbot + password: ${{ secrets.GHCR_TOKEN }} + - name: Set .goreleaser.yaml's release.draft=false + shell: bash + run: | + cat .goreleaser.yaml | sed 's/draft: true/draft: false/g' > .goreleaser.yaml.tmp && mv .goreleaser.yaml.tmp .goreleaser.yaml + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser-pro + version: latest + args: release --nightly --clean + env: + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} diff --git a/.github/workflows/preview-proxy.yml b/.github/workflows/preview-proxy.yml new file mode 100644 index 000000000..b3a529a52 --- /dev/null +++ b/.github/workflows/preview-proxy.yml @@ -0,0 +1,162 @@ +name: preview-proxy + +on: + pull_request_target: + types: [labeled, synchronize] + branches: + - main + +permissions: + contents: read + issues: write + pull-requests: write + +concurrency: + group: preview-proxy-${{ github.pull_request.number }} + cancel-in-progress: true + +jobs: + preview-proxy: + if: ${{ github.event.label.name == 'preview' || contains(github.event.pull_request.labels.*.name, 'preview') }} + runs-on: ubuntu-latest + steps: + - name: Reset PR comment + uses: mshick/add-pr-comment@v2 + with: + issue: ${{ github.pull_request.number }} + message-id: preview-run-info + message: | + # 🤖 Preview Environment + The preview environment is starting up and will be available soon. + - uses: actions/checkout@v4 + with: + # Warning, do not try to checkout the base branch here as it would introduce enormous security risks! + # Also don't introduce a cache in this workflow! + ref: main + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: "**/*.sum" + - name: Install some tools + run: | + sudo apt update + sudo apt install -y ncat + wget https://github.com/FiloSottile/age/releases/download/v1.1.1/age-v1.1.1-linux-amd64.tar.gz + tar xzf age-v1.1.1-linux-amd64.tar.gz + sudo mv age/age* /usr/bin/ + - name: Install ipfs + run: | + wget -q https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz + tar -xvzf kubo_v0.20.0_linux-amd64.tar.gz + ./kubo/install.sh + - name: ipfs init + run: | + ipfs init + ipfs config --json Experimental.Libp2pStreamMounting true + ipfs config --json Ipns.UsePubsub true + ipfs daemon & + sleep 1 + while ! ipfs id &> /dev/null; do + echo "waiting for the daemon" + sleep 1 + done + - name: Install cloudflared + run: | + wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb + sudo dpkg -i cloudflared-linux-amd64.deb + - name: Start cloudflared tunnel + run: | + mkdir -p ~/.cloudflared + echo ${{ secrets.CLOUDFLARE_TUNNEL_CERT }} | base64 -d > ~/.cloudflared/cert.pem + cloudflared tunnel create kluctl-preview-${{ github.event.pull_request.number }} + + TUNNEL_ID=$(cd ~/.cloudflared && ls *.json | sed 's/\.json//g') + echo "TUNNEL_ID=$TUNNEL_ID" >> $GITHUB_ENV + + cat < ~/.cloudflared/config.yml + url: http://localhost:9090 + tunnel: $TUNNEL_ID + credentials-file: $HOME/.cloudflared/$TUNNEL_ID.json + EOF + + cloudflared tunnel route dns $TUNNEL_ID kluctl-preview-${{ github.event.pull_request.number }}.kluctl.com + cloudflared tunnel run $TUNNEL_ID & + - name: Build ipfs-exchange-info + run: | + (cd ./internal/ipfs-exchange-info && go install .) + - name: Receive info + id: info + run: | + echo "${{ secrets.KLUCTL_PREVIEW_AGE_KEY }}" > age.key + ipfs-exchange-info -mode subscribe \ + -topic kluctl-preview-${{ github.event.pull_request.number }} \ + -pr-number ${{ github.event.pull_request.number }} \ + -repo-name "${{ github.repository }}" \ + -age-key-file age.key \ + -out-file info.json + + cat info.json | jq ".ipfsId" -r > ipfs_id + echo "ipfs_id=$(cat ipfs_id)" + - name: IPFS forward + run: | + ipfs p2p forward /x/kluctl-webui /ip4/127.0.0.1/tcp/9090 /p2p/$(cat ipfs_id) + - name: Build comment text + run: | + cat < comment.md + # 🤖 Preview Environment + The preview environment is running. + + # Accessing the UI + The UI is now available at: https://kluctl-preview-${{ github.event.pull_request.number }}.kluctl.com + + When asked for credentials, use admin:admin or viewer:viewer. + + # Shutting it down + Either cancel the pipeline in the PR or simply wait for 30 minutes until it auto-terminates. + EOF + - uses: mshick/add-pr-comment@v2 + with: + issue: ${{ github.pull_request.number }} + message-id: preview-run-info + message-path: | + comment.md + - name: Wait for preview-run to exit + run: | + failures=0 + while true; do + if ! ipfs ping $(cat ipfs_id) -n1; then + failures=$(($failures+1)) + if [ "$failures" -ge "5" ]; then + break + fi + sleep 5 + else + sleep 10 + failures=0 + fi + done + - name: Cleanup cloudflare tunnel + if: ${{ always() }} + run: | + killall cloudflared || true + sleep 5 + + cloudflared tunnel cleanup $TUNNEL_ID || true + cloudflared tunnel delete $TUNNEL_ID || true + + token="${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}" + zone_id=$(curl -H "Authorization: Bearer $token" "https://api.cloudflare.com/client/v4/zones" | jq '.result[] | select(.name == "kluctl.com").id' -r) + echo zone_id=$zone_id + record_id=$(curl -H "Authorization: Bearer $token" "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records" | jq '.result[] | select(.name == "kluctl-preview-${{ github.event.pull_request.number }}.kluctl.com").id' -r) + echo record_id=$record_id + curl -X DELETE -H "Authorization: Bearer $token" "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$record_id" + - uses: mshick/add-pr-comment@v2 + if: ${{ always() }} + with: + issue: ${{ github.pull_request.number }} + message-id: preview-run-info + message: | + # 🤖 Preview Environment + The preview environment has been shut down. diff --git a/.github/workflows/preview-run.yml b/.github/workflows/preview-run.yml new file mode 100644 index 000000000..141e22970 --- /dev/null +++ b/.github/workflows/preview-run.yml @@ -0,0 +1,156 @@ +name: preview-run + +on: + pull_request: + types: [labeled, synchronize] + branches: + - main + +concurrency: + group: preview-run-${{ github.pull_request.number }} + cancel-in-progress: true + +jobs: + preview-run: + if: ${{ github.event.label.name == 'preview' || contains(github.event.pull_request.labels.*.name, 'preview') }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: "**/*.sum" + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: pkg/webui/ui/package-lock.json + - name: Install some tools + run: | + sudo apt update + sudo apt install -y ncat + wget https://github.com/FiloSottile/age/releases/download/v1.1.1/age-v1.1.1-linux-amd64.tar.gz + tar xzf age-v1.1.1-linux-amd64.tar.gz + sudo mv age/age* /usr/bin/ + # SETUP IPFS + - name: Install ipfs + run: | + wget -q https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz + tar -xvzf kubo_v0.20.0_linux-amd64.tar.gz + ./kubo/install.sh + - name: Setup ipfs + run: | + ipfs init + ipfs config --json Experimental.Libp2pStreamMounting true + ipfs config --json Ipns.UsePubsub true + ipfs daemon --enable-pubsub-experiment & + sleep 1 + while ! ipfs id &> /dev/null; do + echo "waiting for the daemon" + sleep 1 + done + # SETUP SSH + - name: Setup ssh + run: | + mkdir -p ~/.ssh + echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCtWJls6XlpszR5zMjiK7cdUnSBI/p7tWaEHykJZrlwHRepNIckPk4ftOsOfiLb+2K/TntPGa0NMWM0uccRNXJ1/hgT5uiA8MpR8d1SGV5QtVwzJkDTXN8iwcTu1zcIUoL2FvQUm/P4hHI4BdcS9GyokOnqh296RRtajnzWZlGtBHRMPt9S7eil9kl5sOuHQIsZHjYkqLb7PSyVWeeMzEEeI28L2ZDrfBBgBNiE4ibVBlRbUeArRO5coV2Vn9uafzOIXT13apo0bhacv5FEmmsEcDGelZWKVInoUQHDnsr7UQPDHS2OsdtZRCluvRYH5ZC4SvrDeuZe4AKjc8iDeNuZLlzn7cgwGZDNHJ1PwAWwEz4/yF0vshA7mfrXLhjJ4+vN4enlQqDYqvudJ3x4uKO67panc+Gmaq76mxh81bJHNnlothEs9K9WfGcXAlBBjuk/0kmIf6I1ICA/dxCKa0sAMbolZHoBuoVUszQdlVrDkwPmzNCenBX/MPDJl08FJFc= ablock@Alexanders-MacBook-Pro.local" >> ~/.ssh/authorized_keys + sudo systemctl enable ssh + sudo systemctl start ssh + ipfs p2p listen /x/ssh /ip4/127.0.0.1/tcp/22 + + ssh-keygen -f scp_key -P "" + cat scp_key.pub >> ~/.ssh/authorized_keys + # SETUP KIND + - name: Setup Kind cluster + run: | + cat < kind-config.yaml + kind: Cluster + apiVersion: kind.x-k8s.io/v1alpha4 + networking: + apiServerAddress: "127.0.0.1" + apiServerPort: 6443 + nodes: + - role: control-plane + kubeadmConfigPatches: + - | + kind: InitConfiguration + nodeRegistration: + kubeletExtraArgs: + node-labels: "ingress-ready=true" + extraPortMappings: + - containerPort: 80 + hostPort: 80 + protocol: TCP + - containerPort: 443 + hostPort: 443 + protocol: TCP + EOF + curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64 + kind create cluster --config kind-config.yaml + - name: Kind info + run: | + kubectl cluster-info + # BUILD + - name: Setup Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + - name: Build binary + run: | + make build-webui + make build-bin + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: false + load: true + tags: kluctl:preview + # LOAD IMAGE + - name: Load image to Kind + run: | + kind load docker-image kluctl:preview + # INSTALL + - name: Install Kluctl Controller + run: | + cd install/controller + ../../bin/kluctl deploy --yes -a kluctl_image=kluctl -a kluctl_version=preview + - name: Install Kluctl Webui + run: | + cd install/webui + ../../bin/kluctl deploy --yes -a kluctl_image=kluctl -a kluctl_version=preview \ + -a webui_args='[]' + kubectl -n kluctl-system create secret generic webui-secret \ + --from-literal auth-secret=secret \ + --from-literal admin-password=admin \ + --from-literal viewer-password=viewer + - name: Run kubectl port-forward + run: | + kubectl -n kluctl-system port-forward svc/kluctl-webui 9090:8080 & + - name: Listen ipfs/p2p + run: | + ipfs p2p listen /x/k /ip4/127.0.0.1/tcp/6443 + ipfs p2p listen /x/kluctl-webui /ip4/127.0.0.1/tcp/9090 + ipfs p2p listen --allow-custom-protocol /http /ip4/127.0.0.1/tcp/9090 + # SEND INFO + - name: Build ipfs-exchange-info + run: | + (cd ./internal/ipfs-exchange-info && go install .) + - name: send info + run: | + ipfs-exchange-info -mode publish \ + -topic kluctl-preview-${{ github.event.pull_request.number }} \ + -ipfs-id $(ipfs id -f "") \ + -pr-number ${{ github.event.pull_request.number }} \ + -age-pub-key age1dhueesr5qj8e8uy298k7z8x3ntv620rde89phumg0kvjx2t32elsm759z7 + env: + # we send the GITHUB_TOKEN to the preview-proxy workflow. This token is then verified to be from a workflow + # that runs inside this repo, which can only be a workflow that got manually approved. The token is encrypted + # via age + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # SLEEP + - name: Sleep + run: | + echo "Sleeping..." + sleep 1800 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 11588e3a3..6236ed100 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: goreleaser +name: goreleaser-release on: push: @@ -15,46 +15,36 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Fetch all tags run: git fetch --force --tags - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.18.1 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.10.2 + go-version: '1.24' + cache-dependency-path: "**/*.sum" - name: Setup QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v3 - name: Setup Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Setup Syft uses: anchore/sbom-action/download-syft@v0 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: kluctlbot password: ${{ secrets.GHCR_TOKEN }} - - uses: actions/cache@v2 - with: - path: | - ~/go/pkg/mod - ~/.cache/go-build - key: ${{ runner.os }}-goreleaser-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-goreleaser- - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v6 with: - distribution: goreleaser + distribution: goreleaser-pro version: latest - args: release --rm-dist + args: release --clean env: + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 759044bff..d758ebd07 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,273 +3,193 @@ name: tests on: push: branches: - - '**' + - main + - release-v* + pull_request: + branches: + - main jobs: - build: - runs-on: ubuntu-20.04 + generate-checks: + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v5 with: - go-version: '1.18.1' - - name: Set up Python - uses: actions/setup-python@v2 + go-version: '1.24' + cache-dependency-path: "**/*.sum" + - uses: actions/setup-node@v4 with: - python-version: 3.10.2 - - uses: actions/cache@v2 + node-version: 20 + cache: 'npm' + cache-dependency-path: pkg/webui/ui/package-lock.json + - name: Install some tools + run: | + PB_REL="https://github.com/protocolbuffers/protobuf/releases" + PB_VER="29.1" + curl -LO $PB_REL/download/v$PB_VER/protoc-$PB_VER-linux-x86_64.zip + sudo unzip protoc-$PB_VER-linux-x86_64.zip -d /usr/local + rm protoc-$PB_VER-linux-x86_64.zip + + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.35.2 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1 + - name: Check links on changed files + run: | + make markdown-link-check + - name: Verify go.mod and go.sum are clean + run: | + go mod tidy + if [ ! -z "$(git status --porcelain)" ]; then + echo "go mod tidy must be invoked and the result committed" + git status + git diff + exit 1 + fi + - name: Verify commands help is up-to-date + run: | + make replace-commands-help + if [ ! -z "$(git status --porcelain)" ]; then + echo "replace-commands-help must be invoked and the result committed" + git status + git diff + exit 1 + fi + - name: Verify generated source is up-to-date + run: | + make generate + if [ ! -z "$(git status --porcelain)" ]; then + echo "make generate must be invoked and the result committed" + git status + git diff + exit 1 + fi + - name: Verify generated manifests are up-to-date + run: | + make manifests + if [ ! -z "$(git status --porcelain)" ]; then + echo "make manifests must be invoked and the result committed" + git status + git diff + exit 1 + fi + - name: Verify generated api-docs are up-to-date + run: | + make api-docs + if [ ! -z "$(git status --porcelain)" ]; then + echo "make api-docs must be invoked and the result committed" + git status + git diff + exit 1 + fi + + check-npm-build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - path: | - ~/go/pkg/mod - ~/.cache/go-build - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Go Mod Vendor - run: | - go mod vendor - - name: Go Generate - run: | - go generate ./... - - name: Run unit tests - run: | - go test ./cmd/... ./pkg/... -v - - name: Build kluctl (linux) - run: | - export CGO_ENABLED=0 - export GOARCH=amd64 - export GOOS=linux - go build - go test -c ./e2e - mv kluctl kluctl-linux-amd64 - mv e2e.test e2e.test-linux-amd64 - - name: Build kluctl (darwin) + node-version: 20 + cache: 'npm' + cache-dependency-path: pkg/webui/ui/package-lock.json + - name: Verify webui build works run: | - export CGO_ENABLED=0 - export GOARCH=amd64 - export GOOS=darwin - go build - go test -c ./e2e - mv kluctl kluctl-darwin-amd64 - mv e2e.test e2e.test-darwin-amd64 - - name: Build kluctl (windows) - run: | - export CGO_ENABLED=0 - export GOARCH=amd64 - export GOOS=windows - go build - go test -c ./e2e - mv kluctl.exe kluctl-windows-amd64.exe - mv e2e.test.exe e2e.test-windows-amd64.exe - - name: Upload binaries - uses: actions/upload-artifact@v2 - with: - name: binaries - path: | - kluctl-linux-amd64 - kluctl-darwin-amd64 - kluctl-windows-amd64.exe - e2e.test-linux-amd64 - e2e.test-darwin-amd64 - e2e.test-windows-amd64.exe + make build-webui - docker-host: - if: "!startsWith(github.ref, 'refs/tags/')" - runs-on: ubuntu-20.04 - needs: - - build + check-docker-images: + strategy: + matrix: + include: + - docker_platform: linux/amd64 + goarch: amd64 + - docker_platform: linux/arm64 + goarch: arm64 + fail-fast: false + runs-on: ubuntu-latest + name: check-docker-images-${{ matrix.docker_platform }} steps: - name: Checkout - uses: actions/checkout@v2 - - name: Install zerotier - run: | - sudo apt update - sudo apt install -y gpg jq - curl -s https://install.zerotier.com | sudo bash - - name: Setup inotify limits - run: | - # see https://kind.sigs.k8s.io/docs/user/known-issues/#pod-errors-due-to-too-many-open-files - sudo sysctl fs.inotify.max_user_watches=524288 - sudo sysctl fs.inotify.max_user_instances=512 - - name: Stop docker - run: | - # Ensure docker is down and that the test jobs can wait for it to be available again after joining the network. - # Otherwise they might join and then docker restarts - sudo systemctl stop docker - - name: Create network - run: | - export CI_ZEROTIER_API_KEY=${{ secrets.CI_ZEROTIER_API_KEY }} - ./hack/zerotier-create-network.sh $GITHUB_RUN_ID - - name: Join network - run: | - export CI_ZEROTIER_API_KEY=${{ secrets.CI_ZEROTIER_API_KEY }} - ./hack/zerotier-join-network.sh $GITHUB_RUN_ID docker - - name: Restart ssh - run: | - sudo systemctl restart ssh - - name: Restart docker - run: | - sudo sed -i 's|-H fd://|-H fd:// -H tcp://0.0.0.0:2375|g' /lib/systemd/system/docker.service - sudo systemctl daemon-reload - sudo systemctl start docker - - name: Wait for other jobs to finish - run: | - export GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}" - while true; do - JOBS=$(gh api /repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs | jq '.jobs[] | select(.name | startswith("tests "))') - NON_COMPLETED=$(echo $JOBS | jq '. | select(.status != "completed")') - if [ "$NON_COMPLETED" == "" ]; then - break - fi - sleep 5 - done - - name: Delete network - if: always() - run: | - export CI_ZEROTIER_API_KEY=${{ secrets.CI_ZEROTIER_API_KEY }} - ./hack/zerotier-delete-network.sh $GITHUB_RUN_ID + uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: "**/*.sum" + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + - name: Setup Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + - name: Build kluctl + run: | + GOARCH=${{ matrix.goarch }} make build-bin + - name: Build docker images + run: | + docker build -t test-image --platform=${{ matrix.docker_platform }} . + - name: Test if git works inside container + run: | + # test if the git binary is still working, which keep breaking due to wolfi-base updates being missed + # If you see this failing, it's time to upgrade the base image in Dockerfile... + docker run --platform=${{ matrix.docker_platform }} --rm -i --entrypoint=/bin/sh test-image -c "cd && git clone https://github.com/kluctl/kluctl.git" + - name: Test if binary can be executed inside Github actions + if: matrix.goarch == 'amd64' + run: | + cd install/controller + ../../bin/kluctl render --offline-kubernetes tests: - if: "!startsWith(github.ref, 'refs/tags/')" strategy: matrix: include: - - os: ubuntu-20.04 - binary-suffix: linux-amd64 - - os: macos-11 - binary-suffix: darwin-amd64 - - os: windows-2019 - binary-suffix: windows-amd64 - os: [ubuntu-20.04, macos-11, windows-2019] + - os: ubuntu-22.04 + run_unit_tests: false + run_e2e_non_gitops: true + run_e2e_gitops: false + name: ubuntu-non-gitops + - os: ubuntu-22.04 + run_unit_tests: true + run_e2e_non_gitops: false + run_e2e_gitops: true + name: ubuntu-gitops + - os: macos-13 + run_unit_tests: true + # only run e2e tests on branches + run_e2e_non_gitops: ${{ github.event_name != 'pull_request' }} + run_e2e_gitops: ${{ github.event_name != 'pull_request' }} + name: macos + - os: windows-2022 + run_unit_tests: true + # only run e2e tests on branches + run_e2e_non_gitops: ${{ github.event_name != 'pull_request' }} + run_e2e_gitops: false # never run gitops tests on windows + name: windows fail-fast: false - needs: - - build runs-on: ${{ matrix.os }} + name: tests-${{ matrix.name }} steps: - name: Checkout - uses: actions/checkout@v2 - - name: Install zerotier (linux) - if: runner.os == 'Linux' - run: | - sudo apt update - sudo apt install -y gpg jq - curl -s https://install.zerotier.com | sudo bash - - name: Install zerotier (macOS) - if: runner.os == 'macOS' - run: | - brew install zerotier-one - - name: Install zerotier (windows) - if: runner.os == 'Windows' - shell: bash - run: | - choco install zerotier-one - echo "#!/usr/bin/env bash" > /usr/bin/zerotier-cli - echo '/c/ProgramData/ZeroTier/One/zerotier-one_x64.exe -q "$@"' >> /usr/bin/zerotier-cli - chmod +x /usr/bin/zerotier-cli - - choco install netcat - echo /c/ProgramData/chocolatey/bin >> $GITHUB_PATH - - name: Join network - shell: bash - run: | - export CI_ZEROTIER_API_KEY=${{ secrets.CI_ZEROTIER_API_KEY }} - ./hack/zerotier-join-network.sh $GITHUB_RUN_ID ${{ matrix.binary-suffix }} - - name: Determine DOCKER_HOST - shell: bash - run: | - export CI_ZEROTIER_API_KEY=${{ secrets.CI_ZEROTIER_API_KEY }} - ./hack/zerotier-setup-docker-host.sh $GITHUB_RUN_ID - - name: Setup TOOLS envs - shell: bash - run: | - if [ "${{ runner.os }}" != "Windows" ]; then - echo "SUDO=sudo" >> $GITHUB_ENV - fi - - TOOLS_EXE= - TOOLS_TARGET_DIR=$GITHUB_WORKSPACE/bin - mkdir $TOOLS_TARGET_DIR - - if [ "${{ runner.os }}" == "macOS" ]; then - TOOLS_OS=darwin - elif [ "${{ runner.os }}" == "Windows" ]; then - TOOLS_OS=windows - TOOLS_EXE=.exe - else - TOOLS_OS=linux - fi - echo "TOOLS_EXE=$TOOLS_EXE" >> $GITHUB_ENV - echo "TOOLS_OS=$TOOLS_OS" >> $GITHUB_ENV - echo "TOOLS_TARGET_DIR=$TOOLS_TARGET_DIR" >> $GITHUB_ENV - echo "$TOOLS_TARGET_DIR" >> $GITHUB_PATH - - name: "[Windows] Install openssh" - if: runner.os == 'Windows' - shell: bash - run: | - choco install openssh - - name: Provide required tools versions - shell: bash - run: | - echo "KUBECTL_VERSION=1.21.5" >> $GITHUB_ENV - echo "KIND_VERSION=0.11.1" >> $GITHUB_ENV - echo "DOCKER_VERSION=20.10.9" >> $GITHUB_ENV - - name: Download required tools - shell: bash - run: | - curl -L -o kubectl$TOOLS_EXE https://storage.googleapis.com/kubernetes-release/release/v$KUBECTL_VERSION/bin/${TOOLS_OS}/amd64/kubectl$TOOLS_EXE && \ - $SUDO mv kubectl$TOOLS_EXE "$TOOLS_TARGET_DIR/" - curl -L -o kind$TOOLS_EXE https://github.com/kubernetes-sigs/kind/releases/download/v${KIND_VERSION}/kind-${TOOLS_OS}-amd64 && \ - $SUDO mv kind$TOOLS_EXE "$TOOLS_TARGET_DIR/" - if [ "${{ runner.os }}" == "macOS" ]; then - curl -L -o docker.tar.gz https://download.docker.com/mac/static/stable/x86_64/docker-$DOCKER_VERSION.tgz - tar xzf docker.tar.gz - $SUDO mv docker/docker "$TOOLS_TARGET_DIR/" - rm -rf docker - elif [ "${{ runner.os }}" == "Windows" ]; then - curl -L -o docker.zip https://download.docker.com/win/static/stable/x86_64/docker-$DOCKER_VERSION.zip - unzip docker.zip - mv docker/docker.exe "$TOOLS_TARGET_DIR/" - rm -rf docker - fi - $SUDO chmod -R +x "$TOOLS_TARGET_DIR/" - - name: Test required tools + uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: "**/*.sum" + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Run unit and lib tests + if: matrix.run_unit_tests shell: bash run: | - kubectl version || true - kind version || true - - name: Prepare kind cluster variables + make test-unit test-lib + - name: Run e2e-non-gitops tests + if: matrix.run_e2e_non_gitops shell: bash run: | - if [ "${{ runner.os }}" == "Linux" ]; then - echo "KIND_API_PORT1=10000" >> $GITHUB_ENV - echo "KIND_API_PORT2=20000" >> $GITHUB_ENV - echo "KIND_EXTRA_PORTS_OFFSET1=30000" >> $GITHUB_ENV - echo "KIND_EXTRA_PORTS_OFFSET2=31000" >> $GITHUB_ENV - elif [ "${{ runner.os }}" == "Windows" ]; then - echo "KIND_API_PORT1=10001" >> $GITHUB_ENV - echo "KIND_API_PORT2=20001" >> $GITHUB_ENV - echo "KIND_EXTRA_PORTS_OFFSET1=30100" >> $GITHUB_ENV - echo "KIND_EXTRA_PORTS_OFFSET2=31100" >> $GITHUB_ENV - else - echo "KIND_API_PORT1=10002" >> $GITHUB_ENV - echo "KIND_API_PORT2=20002" >> $GITHUB_ENV - echo "KIND_EXTRA_PORTS_OFFSET1=30200" >> $GITHUB_ENV - echo "KIND_EXTRA_PORTS_OFFSET2=31200" >> $GITHUB_ENV - fi - KIND_CLUSTER_NAME_BASE=$(echo "kluctl-${{ runner.os }}" | awk '{{ print tolower($1) }}') - - echo "KIND_API_HOST1=$DOCKER_IP" >> $GITHUB_ENV - echo "KIND_API_HOST2=$DOCKER_IP" >> $GITHUB_ENV - echo "KIND_CLUSTER_NAME1=$KIND_CLUSTER_NAME_BASE-1" >> $GITHUB_ENV - echo "KIND_CLUSTER_NAME2=$KIND_CLUSTER_NAME_BASE-2" >> $GITHUB_ENV - - name: Download artifacts - uses: actions/download-artifact@v2 - - name: Run e2e tests + make test-e2e-non-gitops + - name: Run e2e-gitops tests + if: matrix.run_e2e_gitops shell: bash run: | - chmod +x ./binaries/* - export KLUCTL_EXE=./binaries/kluctl-${{ matrix.binary-suffix }}$TOOLS_EXE - ./binaries/e2e.test-${{ matrix.binary-suffix }}$TOOLS_EXE -test.v + make test-e2e-gitops diff --git a/.gitignore b/.gitignore index 3158e3eb0..7468eec00 100644 --- a/.gitignore +++ b/.gitignore @@ -5,13 +5,14 @@ *.kubeconfig .secrets.yml -.sealed-secrets -/vendor -/download-python /kluctl /kluctl.exe /e2e.test* /bin +/build /reports +/vendor dist/ +.vscode +cover.out diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2461d3dd0..c319382a0 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,12 +1,14 @@ +version: 2 + before: hooks: - - go mod tidy - - go generate ./... + - make build-webui builds: - <<: &build_defaults - binary: kluctl + binary: bin/kluctl env: - CGO_ENABLED=0 + main: ./cmd id: linux goos: - linux @@ -35,12 +37,14 @@ archives: format: tar.gz files: - none* + strip_binary_directory: true - name_template: "{{ .Binary }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}" id: windows builds: [windows] format: zip files: - none* + strip_binary_directory: true source: enabled: true name_template: '{{ .ProjectName }}_v{{ .Version }}_source_code' @@ -52,7 +56,12 @@ sboms: checksum: name_template: '{{ .ProjectName }}_v{{ .Version }}_checksums.txt' snapshot: - name_template: "{{ incpatch .Version }}-next" + name_template: "{{ incminor .Version }}-snapshot" +nightly: + name_template: '{{ incminor .Version }}-devel' + tag_name: devel + publish_release: true + keep_single_release: true changelog: sort: asc filters: @@ -67,7 +76,13 @@ changelog: release: draft: true prerelease: auto - name_template: "{{.ProjectName}}-{{.Tag}}" + name_template: "{{ .ProjectName }}-v{{ .Version }}" + header: | + {{- if .IsNightly -}} + ## Development build + This is a development build of the main branch and not meant for production use. + Docker images are also available via: `ghcr.io/kluctl/kluctl:v{{ .Version }}` + {{- end -}} dockers: - id: linux-amd64 @@ -75,7 +90,6 @@ dockers: goarch: amd64 build_flag_templates: - "--pull" - - "--build-arg=ARCH=linux-amd64" - "--label=org.opencontainers.image.created={{ .Date }}" - "--label=org.opencontainers.image.name={{ .ProjectName }}" - "--label=org.opencontainers.image.revision={{ .FullCommit }}" @@ -89,7 +103,6 @@ dockers: goarch: arm64 build_flag_templates: - "--pull" - - "--build-arg=ARCH=linux-arm64" - "--label=org.opencontainers.image.created={{ .Date }}" - "--label=org.opencontainers.image.name={{ .ProjectName }}" - "--label=org.opencontainers.image.revision={{ .FullCommit }}" @@ -100,24 +113,22 @@ dockers: - "ghcr.io/kluctl/kluctl:v{{ .Version }}-arm64" docker_manifests: - - name_template: ghcr.io/kluctl/kluctl:{{ .Tag }} + - name_template: ghcr.io/kluctl/kluctl:v{{ .Version }} image_templates: - "ghcr.io/kluctl/kluctl:v{{ .Version }}-amd64" - "ghcr.io/kluctl/kluctl:v{{ .Version }}-arm64" brews: - name: kluctl - tap: + repository: owner: kluctl name: homebrew-tap token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" - folder: Formula + directory: Formula homepage: "https://kluctl.io/" description: "kluctl" install: | bin.install "kluctl" - test: | - system "#{bin}/kluctl version" bash_output = Utils.safe_popen_read(bin/"kluctl", "completion", "bash") (bash_completion/"kluctl").write bash_output @@ -127,3 +138,5 @@ brews: fish_output = Utils.safe_popen_read(bin/"kluctl", "completion", "fish") (fish_completion/"kluctl.fish").write fish_output + test: | + system "#{bin}/kluctl version" diff --git a/.sv4git.yml b/.sv4git.yml new file mode 100644 index 000000000..92d3bea10 --- /dev/null +++ b/.sv4git.yml @@ -0,0 +1,2 @@ +tag: + filter: 'v[0-9]*' diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..5f90c194f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +## Code of Conduct + +Kluctl follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..bc4ff3e98 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,59 @@ +# Contributing + +As all Open Source projects, Kluctl lives from contributions. If you consider contributing, we are welcoming you to do so. + +## Communication + +Contributing to Open Source projects also means that you are communicating with other members of the community. Please +follow the [Code of Conduct](./CODE_OF_CONDUCT.md) whenever you communicate. + +## Creating issues / Reporting bugs + +Probably the easiest way to contribute is to create issues on Github. An issue could for example be a bug report or +a feature request. It's also fine to create an issue that requests some change, for example to documentation. Issues +can also be used as reminders/TODOs that something needs to be done in the future. + +The project is still in early stage when it comes to project management, so please bare with us if processes are still +a bit "loose". + +One thing we'd like to ask for is to first search for issues that might already represent what you plan to report. +In many cases it turns out that other people stumbled across the same thing already. If not, then you're "lucky" to +report first :) + +It might also be useful to ask inside the #kluctl channel of the [CNCF Slack](https://slack.cncf.io) if you are unsure +about your issue. + +## Finding issues to work on + +Check the [open issues](https://github.com/kluctl/kluctl/issues) and see if you can find something that you believe you +could help with. + +## Contributing/Modifying documentation + +The next level of contribution is modifying documentation. Fork the repository and start working on the changes locally. +When done, commit the changes, push them and create a pull request. If your pull request fixes an issue, add the proper +`Fixes: #xxx` line so that the issue can be auto-closed. + +Please note that we follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) when committing. + +## Contributing/Modifying source code + +To fix a bug or add a feature, follow the same procedure as described in the previous chapter. Read the +[DEVELOPMENT](./DEVELOPMENT.md) guidelines on how to build and run Kluctl from source. + +Additionally, you should ensure that your changes don't break anything. You can do this by running the +[test suite](./DEVELOPMENT.md#how-to-run-the-test-suite) and by testing your changes manually. Adding new tests for +fixed bugs and/or new features is also nice to see. Reviewers will also point out when tests would be of value. + +However, don't worry if you feel overwhelmed (e.g. with tests). You can also create a `[WIP]` pull request and ask for +help. + +Please note that we follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) when committing. + +## Conventional commits + +As mentioned before, we follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) when committing. +These commits are then used to create release notes and properly bump version numbers. + +We might reconsider this approach in the future, especially when we re-design the +[release process](./DEVELOPMENT.md#releasing-process). diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 000000000..f123f97a7 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,50 @@ +# Development + +## Installing required dependencies + +There are a number of dependencies required to be able to compile, run and test Kluctl: + +- [Install Go](https://golang.org/doc/install) + +In addition to the above, the following dependencies are also used by some of the `make` targets: + +- `goreleaser` (latest) +- `setup-envtest` (latest) + +If any of the above dependencies are not present on your system, the first invocation of a `make` target that requires them will install them. + +## How to run the test suite + +Prerequisites: +* Go >= 1.21 + +You can run the test suite by simply doing + +```sh +make test +``` + +Tests are separated between unit tests (found in-tree next to the tested code) and e2e tests (found below `./e2e`). +Running `make test` will run all these tests. To only run unit tests, run `make test-unit`. To only run e2e tests, run +`make test-e2e`. + +e2e tests rely on kubebuilders `envtest` package and thus also require `setup-envtest` and dependent binaries +(e.g. kube-apiserver) to be available as well. The Makefile will take care of downloading these if required. + +## How to build Kluctl + +Simply run `make build`, which will build the kluctl binary any put into `./bin/`. To use, either directly invoke it +with the relative or absolute path or update your `PATH` environment variable to point to the bin directory. + +## Contributions + +See [CONTRIBUTING](./CONTRIBUTING.md) for details. + +## Releasing process + +The release process is currently partly manual and partly automated. A maintainer has to create a version tag and +manually push it to Github. A Github workflow will then react to this tag by running `goreleaser`, which will then +create a draft release. The maintainer then has to manually update the pre-generated release notes and publish the +release. + +This process is going to be modified in the future to contain more automation and more collaboration friendly processes. diff --git a/Dockerfile b/Dockerfile index cd4912bac..7504e1ded 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,21 @@ -FROM alpine:3.15 as builder +# We must use a glibc based distro due to embedded python not supporting musl libc for aarch64 (only amd64+musl is supported) +# see https://github.com/indygreg/python-build-standalone/issues/87 +# use `docker buildx imagetools inspect cgr.dev/chainguard/wolfi-base:latest` to find latest sha256 of multiarch image +FROM --platform=$TARGETPLATFORM cgr.dev/chainguard/wolfi-base@sha256:3490ac41510e17846b30c9ebfc4a323dfdecbd9a35e7b0e4e745a8f496a18f25 -RUN apk add --no-cache ca-certificates curl +# See https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope +ARG TARGETPLATFORM -ARG ARCH=linux-amd64 +# We need git for kustomize to support overlays from git +# gpg is needed for SOPS +RUN apk update && apk add git tzdata gpg gpg-agent -ENV HELM_VERSION=v3.8.2 -RUN wget -O helm.tar.gz https://get.helm.sh/helm-$HELM_VERSION-$ARCH.tar.gz && \ - tar xzf helm.tar.gz && \ - mv $ARCH/helm / +# Ensure helm is not trying to access / +ENV HELM_CACHE_HOME=/tmp/helm-cache +ENV KLUCTL_CACHE_DIR=/tmp/kluctl-cache + +COPY bin/kluctl /usr/bin/ + +USER 65532:65532 -# We must use a glibc based distro due to embedded python not supporting musl libc for aarch64 -FROM debian:bullseye-slim -COPY --from=builder /helm /usr/bin -COPY kluctl /usr/bin/ ENTRYPOINT ["/usr/bin/kluctl"] diff --git a/LICENSE b/LICENSE index d64569567..5fc370f65 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright [2023] [The Kluctl authors] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 000000000..715056cf7 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,11 @@ +# Maintainers + +The maintainers are generally available in Slack at +https://cloud-native.slack.com in #klcutl +(obtain an invitation at https://slack.cncf.io/). + +Currently, the maintainers of Kluctl are: + +- Alexander Block (github: @codablock, slack: codablock) +- Aljoscha Poertner (github: @AljoschaP, slack: aljoshare) +- Mathias Gebbe (github: @matzegebbe, slack: matzeihnsein) diff --git a/Makefile b/Makefile index 6372eb9d5..8e35e4c04 100644 --- a/Makefile +++ b/Makefile @@ -1,108 +1,208 @@ # Based on the work of Thomas Poignant (thomaspoignant) # https://gist.github.com/thomaspoignant/5b72d579bd5f311904d973652180c705 -GOCMD=go -GOTEST=$(GOCMD) test -GOVET=$(GOCMD) vet -BINARY_NAME=kluctl -TEST_BINARY_NAME=kluctl-e2e -REQUIRED_ENV_VARS=GOOS GOARCH -EXPORT_RESULT?=false +GOOS=$(shell go env GOOS) +GOARCH=$(shell go env GOARCH) -GREEN := $(shell tput -Txterm setaf 2) -YELLOW := $(shell tput -Txterm setaf 3) -WHITE := $(shell tput -Txterm setaf 7) -CYAN := $(shell tput -Txterm setaf 6) -RESET := $(shell tput -Txterm sgr0) +EXE= +ifeq ($(GOOS), windows) +EXE=.exe +endif -.PHONY: all test build vendor check-kubectl check-helm check-kind +RACE= +ifneq ($(GOOS), windows) +RACE=-race +endif -all: help +PATHCONF=cat +ifeq ($(GOOS), windows) +PATHCONF=cygpath -f- -u +endif -## Check: +# Image URL to use all building/pushing image targets +IMG ?= kluctl/kluctl:latest +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.28.0 -check-kubectl: ## Checks if kubectl is installed - kubectl version --client=true +# This is the last version that supported go 1.22. We can switch back to latest when we don't need to support 1.22 anymore. +SETUP_ENVTEST_VERSION = v0.0.0-20241011141221-469837099f73 -check-helm: ## Checks if helm is installed - helm version +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif -check-kind: ## Checks if kind is installed - kind version +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec -## Build: -build: vendor generate build-go ## Run the complete build pipeline +.PHONY: all +all: build -build-go: ## Build your project and put the output binary in ./bin/ - mkdir -p ./bin - CGO_ENBALED=0 GO111MODULE=on $(GOCMD) build -mod vendor -o ./bin/$(BINARY_NAME) +##@ General -clean: ## Remove build related file - rm -fr ./bin - rm -fr ./out - rm -fr ./reports - rm -fr ./download-python +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php -vendor: ## Copy of all packages needed to support builds and tests in the vendor directory - $(GOCMD) mod vendor +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) -generate: ## Generating Python and Jinja2 support - $(GOCMD) generate ./... - $(GOCMD) generate -tags linux,darwin,windows,amd64,arm64 ./pkg/python +##@ Development -## Test: -test: test-unit test-e2e ## Runs the complete test suite +.PHONY: manifests +manifests: controller-gen kustomize ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=kluctl-controller-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + @echo "# Warning, this file is generated via \"make manifests\", don't edit it directly but instead change the files in config/crd" > install/controller/controller/crd.yaml + $(KUSTOMIZE) build config/crd >> install/controller/controller/crd.yaml + @echo "# Warning, this file is generated via \"make manifests\", don't edit it directly but instead change the files in config/rbac" > install/controller/controller/rbac.yaml + $(KUSTOMIZE) build config/rbac >> install/controller/controller/rbac.yaml + @echo "# Warning, this file is generated via \"make manifests\", don't edit it directly but instead change the files in config/manager" > install/controller/controller/manager.yaml + $(KUSTOMIZE) build config/manager >> install/controller/controller/manager.yaml -test-e2e: check-kubectl check-helm check-kind ## Runs the end to end tests - CGO_ENBALED=0 GO111MODULE=on $(GOCMD) test -o ./bin/$(TEST_BINARY_NAME) ./e2e +# Generate API reference documentation +api-docs: gen-crd-api-reference-docs + $(GEN_CRD_API_REFERENCE_DOCS) -v=4 -api-dir=./api/v1beta1 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/gitops/api/kluctl-controller.md -test-unit: ## Run the unit tests of the project -ifeq ($(EXPORT_RESULT), true) - mkdir -p reports/test-unit - GO111MODULE=off $(GOCMD) get -u github.com/jstemmer/go-junit-report - $(eval OUTPUT_OPTIONS = | tee /dev/tty | go-junit-report -set-exit-code > reports/test-unit/junit-report.xml) -endif - $(GOTEST) -v -race $(shell go list ./... | grep -v /e2e/ | grep -v /vendor/) $(OUTPUT_OPTIONS) - -coverage-unit: ## Run the unit tests of the project and export the coverage - $(GOTEST) -cover -covermode=count -coverprofile=reports/coverage-unit/profile.cov $(shell go list ./... | grep -v /e2e/ | grep -v /vendor/) - $(GOCMD) tool cover -func profile.cov -ifeq ($(EXPORT_RESULT), true) - mkdir -p reports/coverage-unit - GO111MODULE=off $(GOCMD) get -u github.com/AlekSi/gocov-xml - GO111MODULE=off $(GOCMD) get -u github.com/axw/gocov/gocov - gocov convert reports/coverage-unit/profile.cov | gocov-xml > reports/coverage-unit/coverage.xml -endif +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + go generate ./... -## Lint: -lint: lint-go ## Run all available linters +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... -lint-go: ## Use golintci-lint on your project -ifeq ($(EXPORT_RESULT), true) - mkdir -p reports/lint-go - $(eval OUTPUT_OPTIONS = $(shell echo "--out-format checkstyle ./... | tee /dev/tty > reports/lint-go/checkstyle-report.xml" )) -else - $(eval OUTPUT_OPTIONS = $(shell echo "")) +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: manifests generate test-unit test-lib test-e2e fmt vet ## Run all tests. + +.PHONY: test-unit +test-unit: envtest ## Run unit tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir=$(LOCALBIN) -p path | $(PATHCONF))" go test $(RACE) $(shell go list ./... | grep -v v2/e2e) -coverprofile cover.out -test.v + +.PHONY: test-lib +test-lib: ## Run lib tests. + cd lib && go test $(RACE) ./... -coverprofile cover.out -test.v + +.PHONY: test-e2e +test-e2e: envtest ## Run all e2e tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir=$(LOCALBIN) -p path | $(PATHCONF))" go test $(RACE) ./e2e -timeout 15m -coverprofile cover.out -test.v + +.PHONY: test-e2e-non-gitops +test-e2e-non-gitops: envtest ## Run non-gitops e2e tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir=$(LOCALBIN) -p path | $(PATHCONF))" go test $(RACE) ./e2e -timeout 15m -coverprofile cover.out -test.v -skip 'TestGitOps.*' + +.PHONY: test-e2e-gitops +test-e2e-gitops: envtest ## Run gitops e2e tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir=$(LOCALBIN) -p path | $(PATHCONF))" go test $(RACE) ./e2e -timeout 15m -coverprofile cover.out -test.v -run 'TestGitOps.*' + +replace-commands-help: ## Replace commands help in docs + go run ./internal/replace-commands-help --docs-dir ./docs/kluctl/commands + +LYCHEE_VERSION=0.14 +markdown-link-check: ## Check markdown files for dead links + docker run --init -i --rm -w /input -v ${PWD}:/input:ro lycheeverse/lychee:$(LYCHEE_VERSION) -- README.md docs install + +##@ Build + +.PHONY: build +build: manifests generate fmt vet build-webui build-bin ## Build manager binary. + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./cmd/main.go + +.PHONY: build-bin +build-bin: + go build -o bin/kluctl$(EXE) cmd/main.go + +.PHONY: build-webui +build-webui: + cd pkg/webui/ui && npm ci && npm run build + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false endif - docker run --rm -v $(shell pwd):/app -w /app golangci/golangci-lint:latest-alpine golangci-lint run $(OUTPUT_OPTIONS) - - -## Release: -version: ## Write next version into version file - $(GOCMD) install github.com/bvieira/sv4git/v2/cmd/git-sv@v2.7.0 - $(eval KLUCTL_VERSION:=$(shell git sv next-version)) - sed -ibak "s/0.0.0/$(KLUCTL_VERSION)/g" pkg/version/version.go - -changelog: ## Generating changelog - git sv changelog -n 1 > CHANGELOG.md - -## Help: -help: ## Show this help. - @echo '' - @echo 'Usage:' - @echo ' ${YELLOW}make${RESET} ${GREEN}${RESET}' - @echo '' - @echo 'Targets:' - @awk 'BEGIN {FS = ":.*?## "} { \ - if (/^[0-9a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \ - else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \ - }' $(MAKEFILE_LIST) \ No newline at end of file + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: deploy-kind +deploy-kind: manifests kustomize + GOOS=linux GOARCH=amd64 make build + docker build -t kluctl-kind:latest --build-arg BIN_PATH=./bin/kluctl . + kind load docker-image kluctl-kind:latest + cd config/manager && $(KUSTOMIZE) edit set image controller=kluctl-kind + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.0.3 +CONTROLLER_TOOLS_VERSION ?= v0.14.0 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. +$(KUSTOMIZE): $(LOCALBIN) + @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ + echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ + rm -rf $(LOCALBIN)/kustomize; \ + fi + test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) --output install_kustomize.sh && bash install_kustomize.sh $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); rm install_kustomize.sh; } + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +# Find or download gen-crd-api-reference-docs +GEN_CRD_API_REFERENCE_DOCS = $(LOCALBIN)/gen-crd-api-reference-docs +.PHONY: gen-crd-api-reference-docs +gen-crd-api-reference-docs: + GOBIN=$(LOCALBIN) go install github.com/ahmetb/gen-crd-api-reference-docs@v0.3.0 + +.PHONY: envtest +envtest: $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@$(SETUP_ENVTEST_VERSION) diff --git a/PROJECT b/PROJECT new file mode 100644 index 000000000..9a4660f2d --- /dev/null +++ b/PROJECT @@ -0,0 +1,20 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: kluctl.io +layout: +- go.kubebuilder.io/v4 +projectName: controller +repo: github.com/kluctl/kluctl/v2 +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: kluctl.io + group: gitops + kind: KluctlDeployment + path: github.com/kluctl/kluctl/v2/api/v1beta1 + version: v1beta1 +version: "3" diff --git a/README.md b/README.md index c19f8f86f..70a4824c7 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,10 @@ [![tests](https://github.com/kluctl/kluctl/workflows/tests/badge.svg)](https://github.com/kluctl/kluctl/actions) [![license](https://img.shields.io/github/license/kluctl/kluctl.svg)](https://github.com/kluctl/kluctl/blob/main/LICENSE) -[![release](https://img.shields.io/github/release/kluctl/kluctl/all.svg)](https://github.com/kluctl/kluctl/releases) +[![release](https://img.shields.io/github/release/kluctl/kluctl.svg)](https://github.com/kluctl/kluctl/releases) kluctl - - Kluctl is the missing glue that puts together your (and any third-party) deployments into one large declarative Kubernetes deployment, while making it fully manageable (deploy, diff, prune, delete, ...) via one unified command line interface. @@ -19,23 +17,60 @@ Kluctl is centered around "targets", which can be a cluster or a specific enviro or multiple clusters. Targets can be deployed, diffed, pruned, deleted, and so on. The idea is to have the same set of operations for every target, no matter how simple or complex the deployment and/or target is. -Kluctl does not depend on external operators/controllers and allows to use the same deployment wherever you want, +Kluctl does not strictly depend on a controller and allows to use the same deployment wherever you want, as long as access to the kluctl project and clusters is available. This means, that you can use it from your local machine, from your CI/CD pipelines or any automation platform/system that allows to call custom tools. -Flux support is in alpha stadium and available via the [flux-kluctl-controller](https://github.com/kluctl/flux-kluctl-controller). +## What can I do with Kluctl? + +Kluctl allows you to define a Kluctl project, which in turn defines Kluctl +deployments and sub-deployments. Each Kluctl deployment defines Kustomize deployments. + +A Kluctl project also defines targets, which represent your target environments +and/or clusters. + +The Kluctl CLI then allows to deploy, diff, prune, delete, ... your deployments. + +## GitOps + +If you want to follow a pull based [GitOps](https://kluctl.io/docs/gitops/) flow, then you can use the Kluctl +Controller, which then allows you to use `KluctlDeployment` custom resources to define your Kluctl deployments. + +## Kluctl Webui + +Kluctl also offers a [Webui](https://kluctl.io/docs/webui/) that allows you to visualise and control your Kluctl +deployments. It works for deployments performed by the CLI and for deployments performed via GitOps. + +[Here](https://kluctl.io/blog/2023/09/12/introducing-the-kluctl-webui/) is an introduction to the Webui together +with a tutorial. -## Installation +## Where do I start? -See [installation](./install). +Installation instructions can be found [here](docs/kluctl/installation.md). For a getting started guide, continue +[here](docs/kluctl/get-started.md). + +## Community + +Check the [community page](https://kluctl.io/community/) for details about the Kluctl community. + +In short: We use [Github Issues](https://github.com/kluctl/kluctl/issues) and +[Github Discussions](https://github.com/kluctl/kluctl/discussions) to track and discuss Kluctl related development. +You can also join the #kluctl channel inside the [CNCF Slack](https://slack.cncf.io) to get in contact with other +community members and contributors/developers. ## Documentation -Documentation can be found here: https://kluctl.io/docs +Documentation, news and blog posts can be found on https://kluctl.io. -## Kluctl in Short +The underlying documentation is synced from this repo (look into ./docs) to the website whenever something is merged +into main. + +## Development and contributions - +Please read [DEVELOPMENT](./DEVELOPMENT.md) and [CONTRIBUTIONS](./CONTRIBUTING.md) for details on how the Kluctl project +handles these matters. + +## Kluctl in Short | | | | --- | --- | @@ -54,4 +89,7 @@ Documentation can be found here: https://kluctl.io/docs | 🔐 Encrypted Secrets | Manage encrypted secrets for multiple target environments and clusters. | ## Demo -![](https://kluctl.io/asciinema/kluctl.gif) + +https://kluctl.io/vhs/demo-cut.mp4 + +Click on the link to play the video. \ No newline at end of file diff --git a/api/v1beta1/condition_types.go b/api/v1beta1/condition_types.go new file mode 100644 index 000000000..6db9f66bd --- /dev/null +++ b/api/v1beta1/condition_types.go @@ -0,0 +1,45 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +const ( + // DiffFailedReason represents the fact that the + // kluctl diff command failed. + DiffFailedReason string = "DiffFailed" + + // DeployFailedReason represents the fact that the + // kluctl deploy command failed. + DeployFailedReason string = "DeployFailed" + + // PruneFailedReason represents the fact that the + // kluctl prune command failed. + PruneFailedReason string = "PruneFailed" + + // ValidateFailedReason represents the fact that the + // validate of the KluctlDeployment failed. + ValidateFailedReason string = "ValidateFailed" + + // PrepareFailedReason represents failure in the kluctl preparation phase + PrepareFailedReason string = "PrepareFailed" + + // ReconciliationSucceededReason represents the fact that + // the reconciliation succeeded. + ReconciliationSucceededReason string = "ReconciliationSucceeded" + + // WaitingForLegacyMigrationReason means that the controller is waiting for the legacy controller to set `readyForMigration=true` + WaitingForLegacyMigrationReason string = "WaitingForLegacyMigration" +) diff --git a/api/v1beta1/doc.go b/api/v1beta1/doc.go new file mode 100644 index 000000000..5d3a94b0e --- /dev/null +++ b/api/v1beta1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta1 contains API Schema definitions for the gitops.kluctl.io v1beta1 API group. +// +kubebuilder:object:generate=true +// +groupName=gitops.kluctl.io +package v1beta1 diff --git a/api/v1beta1/groupversion_info.go b/api/v1beta1/groupversion_info.go new file mode 100644 index 000000000..1092fbc63 --- /dev/null +++ b/api/v1beta1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta1 contains API Schema definitions for the v1beta1 API group +// +kubebuilder:object:generate=true +// +groupName=gitops.kluctl.io +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "gitops.kluctl.io", Version: "v1beta1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1beta1/kluctldeployment_types.go b/api/v1beta1/kluctldeployment_types.go new file mode 100644 index 000000000..f71baaf53 --- /dev/null +++ b/api/v1beta1/kluctldeployment_types.go @@ -0,0 +1,657 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + gittypes "github.com/kluctl/kluctl/lib/git/types" + "github.com/kluctl/kluctl/lib/yaml" + "github.com/kluctl/kluctl/v2/pkg/types" + "github.com/kluctl/kluctl/v2/pkg/types/result" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "time" +) + +const ( + KluctlDeploymentKind = "KluctlDeployment" + KluctlDeploymentFinalizer = "finalizers.gitops.kluctl.io" + MaxConditionMessageLength = 20000 + + KluctlDeployModeFull = "full-deploy" + KluctlDeployPokeImages = "poke-images" +) + +// The following annotations are set by the CLI (gitops sub-commands) and the webui. The values contains a JSON serialized +// ManualRequest +const ( + KluctlRequestReconcileAnnotation = "kluctl.io/request-reconcile" + KluctlRequestDiffAnnotation = "kluctl.io/request-diff" + KluctlRequestDeployAnnotation = "kluctl.io/request-deploy" + KluctlRequestPruneAnnotation = "kluctl.io/request-prune" + KluctlRequestValidateAnnotation = "kluctl.io/request-validate" + + // SourceOverrideScheme is used when source overrides are setup via the CLI + SourceOverrideScheme = "grpc+source-override" +) + +type KluctlDeploymentSpec struct { + // Specifies the project source location + Source ProjectSource `json:"source"` + + // Specifies source overrides + // +optional + SourceOverrides []SourceOverride `json:"sourceOverrides,omitempty"` + + // Credentials specifies the credentials used when pulling sources + // +optional + Credentials ProjectCredentials `json:"credentials,omitempty"` + + // Decrypt Kubernetes secrets before applying them on the cluster. + // +optional + Decryption *Decryption `json:"decryption,omitempty"` + + // The interval at which to reconcile the KluctlDeployment. + // Reconciliation means that the deployment is fully rendered and only deployed when the result changes compared + // to the last deployment. + // To override this behavior, set the DeployInterval value. + // +required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" + Interval metav1.Duration `json:"interval"` + + // The interval at which to retry a previously failed reconciliation. + // When not specified, the controller uses the Interval + // value to retry failures. + // +optional + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" + RetryInterval *metav1.Duration `json:"retryInterval,omitempty"` + + // DeployInterval specifies the interval at which to deploy the KluctlDeployment, even in cases the rendered + // result does not change. + // +optional + DeployInterval *SafeDuration `json:"deployInterval,omitempty"` + + // ValidateInterval specifies the interval at which to validate the KluctlDeployment. + // Validation is performed the same way as with 'kluctl validate -t '. + // Defaults to the same value as specified in Interval. + // Validate is also performed whenever a deployment is performed, independent of the value of ValidateInterval + // +optional + ValidateInterval *SafeDuration `json:"validateInterval,omitempty"` + + // Timeout for all operations. + // Defaults to 'Interval' duration. + // +optional + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" + Timeout *metav1.Duration `json:"timeout,omitempty"` + + // This flag tells the controller to suspend subsequent kluctl executions, + // it does not apply to already started executions. Defaults to false. + // +optional + Suspend bool `json:"suspend,omitempty"` + + // HelmCredentials is a list of Helm credentials used when non pre-pulled Helm Charts are used inside a + // Kluctl deployment. + // DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.helm instead. + // +optional + HelmCredentials []HelmCredentials `json:"helmCredentials,omitempty"` + + // The name of the Kubernetes service account to use while deploying. + // If not specified, the default service account is used. + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` + + // The KubeConfig for deploying to the target cluster. + // Specifies the kubeconfig to be used when invoking kluctl. Contexts in this kubeconfig must match + // the context found in the kluctl target. As an alternative, specify the context to be used via 'context' + // +optional + KubeConfig *KubeConfig `json:"kubeConfig,omitempty"` + + // Target specifies the kluctl target to deploy. If not specified, an empty target is used that has no name and no + // context. Use 'TargetName' and 'Context' to specify the name and context in that case. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +optional + Target *string `json:"target,omitempty"` + + // TargetNameOverride sets or overrides the target name. This is especially useful when deployment without a target. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +optional + TargetNameOverride *string `json:"targetNameOverride,omitempty"` + + // If specified, overrides the context to be used. This will effectively make kluctl ignore the context specified + // in the target. + // +optional + Context *string `json:"context,omitempty"` + + // Args specifies dynamic target args. + // +optional + // +kubebuilder:pruning:PreserveUnknownFields + Args *runtime.RawExtension `json:"args,omitempty"` + + // Images contains a list of fixed image overrides. + // Equivalent to using '--fixed-images-file' when calling kluctl. + // +optional + Images []types.FixedImage `json:"images,omitempty"` + + // DryRun instructs kluctl to run everything in dry-run mode. + // Equivalent to using '--dry-run' when calling kluctl. + // +kubebuilder:default:=false + // +optional + DryRun bool `json:"dryRun,omitempty"` + + // NoWait instructs kluctl to not wait for any resources to become ready, including hooks. + // Equivalent to using '--no-wait' when calling kluctl. + // +kubebuilder:default:=false + // +optional + NoWait bool `json:"noWait,omitempty"` + + // ForceApply instructs kluctl to force-apply in case of SSA conflicts. + // Equivalent to using '--force-apply' when calling kluctl. + // +kubebuilder:default:=false + // +optional + ForceApply bool `json:"forceApply,omitempty"` + + // ReplaceOnError instructs kluctl to replace resources on error. + // Equivalent to using '--replace-on-error' when calling kluctl. + // +kubebuilder:default:=false + // +optional + ReplaceOnError bool `json:"replaceOnError,omitempty"` + + // ForceReplaceOnError instructs kluctl to force-replace resources in case a normal replace fails. + // Equivalent to using '--force-replace-on-error' when calling kluctl. + // +kubebuilder:default:=false + // +optional + ForceReplaceOnError bool `json:"forceReplaceOnError,omitempty"` + + // ForceReplaceOnError instructs kluctl to abort deployments immediately when something fails. + // Equivalent to using '--abort-on-error' when calling kluctl. + // +kubebuilder:default:=false + // +optional + AbortOnError bool `json:"abortOnError,omitempty"` + + // IncludeTags instructs kluctl to only include deployments with given tags. + // Equivalent to using '--include-tag' when calling kluctl. + // +optional + IncludeTags []string `json:"includeTags,omitempty"` + + // ExcludeTags instructs kluctl to exclude deployments with given tags. + // Equivalent to using '--exclude-tag' when calling kluctl. + // +optional + ExcludeTags []string `json:"excludeTags,omitempty"` + + // IncludeDeploymentDirs instructs kluctl to only include deployments with the given dir. + // Equivalent to using '--include-deployment-dir' when calling kluctl. + // +optional + IncludeDeploymentDirs []string `json:"includeDeploymentDirs,omitempty"` + + // ExcludeDeploymentDirs instructs kluctl to exclude deployments with the given dir. + // Equivalent to using '--exclude-deployment-dir' when calling kluctl. + // +optional + ExcludeDeploymentDirs []string `json:"excludeDeploymentDirs,omitempty"` + + // DeployMode specifies what deploy mode should be used. + // The options 'full-deploy' and 'poke-images' are supported. + // With the 'poke-images' option, only images are patched into the target without performing a full deployment. + // +kubebuilder:default:=full-deploy + // +kubebuilder:validation:Enum=full-deploy;poke-images + // +optional + DeployMode string `json:"deployMode,omitempty"` + + // Validate enables validation after deploying + // +kubebuilder:default:=true + // +optional + Validate bool `json:"validate"` + + // Prune enables pruning after deploying. + // +kubebuilder:default:=false + // +optional + Prune bool `json:"prune,omitempty"` + + // Delete enables deletion of the specified target when the KluctlDeployment object gets deleted. + // +kubebuilder:default:=false + // +optional + Delete bool `json:"delete,omitempty"` + + // Manual enables manual deployments, meaning that the deployment will initially start as a dry run deployment + // and only after manual approval cause a real deployment + // +optional + Manual bool `json:"manual,omitempty"` + + // ManualObjectsHash specifies the rendered objects hash that is approved for manual deployment. + // If Manual is set to true, the controller will skip deployments when the current reconciliation loops calculated + // objects hash does not match this value. + // There are two ways to use this value properly. + // 1. Set it manually to the value found in status.lastObjectsHash. + // 2. Use the Kluctl Webui to manually approve a deployment, which will set this field appropriately. + // +optional + ManualObjectsHash *string `json:"manualObjectsHash,omitempty"` +} + +// GetRetryInterval returns the retry interval +func (in KluctlDeploymentSpec) GetRetryInterval() time.Duration { + if in.RetryInterval != nil { + return in.RetryInterval.Duration + } + return in.Interval.Duration +} + +type ProjectSource struct { + // Git specifies a git repository as project source + // +optional + Git *ProjectSourceGit `json:"git,omitempty"` + + // Oci specifies an OCI repository as project source + // +optional + Oci *ProjectSourceOci `json:"oci,omitempty"` + + // Url specifies the Git url where the project source is located + // DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.url instead. + // +optional + URL *string `json:"url,omitempty"` + + // Ref specifies the branch, tag or commit that should be used. If omitted, the default branch of the repo is used. + // DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.ref instead. + // +optional + Ref *gittypes.GitRef `json:"ref,omitempty"` + + // Path specifies the sub-directory to be used as project directory + // DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.path instead. + // +optional + Path string `json:"path,omitempty"` + + // SecretRef specifies the Secret containing authentication credentials for + // See ProjectSourceCredentials.SecretRef for details + // DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.git + // instead. + // WARNING using this field causes the controller to pass http basic auth credentials to ALL repositories involved. + // Use spec.credentials.git with a proper Host field instead. + SecretRef *LocalObjectReference `json:"secretRef,omitempty"` + + // Credentials specifies a list of secrets with credentials + // DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.git instead. + // +optional + Credentials []ProjectCredentialsGitDeprecated `json:"credentials,omitempty"` +} + +type ProjectSourceGit struct { + // URL specifies the Git url where the project source is located. If the given Git repository needs authentication, + // use spec.credentials.git to specify those. + // +required + URL string `json:"url"` + + // Ref specifies the branch, tag or commit that should be used. If omitted, the default branch of the repo is used. + // +optional + Ref *gittypes.GitRef `json:"ref,omitempty"` + + // Path specifies the sub-directory to be used as project directory + // +optional + Path string `json:"path,omitempty"` +} + +type ProjectSourceOci struct { + // Url specifies the Git url where the project source is located. If the given OCI repository needs authentication, + // use spec.credentials.oci to specify those. + // +required + URL string `json:"url"` + + // Ref specifies the tag to be used. If omitted, the "latest" tag is used. + // +optional + Ref *types.OciRef `json:"ref,omitempty"` + + // Path specifies the sub-directory to be used as project directory + // +optional + Path string `json:"path,omitempty"` +} + +type SourceOverride struct { + // +required + RepoKey gittypes.RepoKey `json:"repoKey"` + // +required + Url string `json:"url"` + // +optional + IsGroup bool `json:"isGroup,omitempty"` +} + +type ProjectCredentials struct { + // Git specifies a list of git credentials + // +optional + Git []ProjectCredentialsGit `json:"git,omitempty"` + + // Oci specifies a list of OCI credentials + // +optional + Oci []ProjectCredentialsOci `json:"oci,omitempty"` + + // Helm specifies a list of Helm credentials + // +optional + Helm []ProjectCredentialsHelm `json:"helm,omitempty"` +} + +type ProjectCredentialsGit struct { + // Host specifies the hostname that this secret applies to. If set to '*', this set of credentials + // applies to all hosts. + // Using '*' for http(s) based repositories is not supported, meaning that such credentials sets will be ignored. + // You must always set a proper hostname in that case. + // +required + Host string `json:"host,omitempty"` + + // Path specifies the path to be used to filter Git repositories. The path can contain wildcards. These credentials + // will only be used for matching Git URLs. If omitted, all repositories are considered to match. + // +optional + Path string `json:"path,omitempty"` + + // SecretRef specifies the Secret containing authentication credentials for + // the git repository. + // For HTTPS git repositories the Secret must contain 'username' and 'password' + // fields. + // For SSH git repositories the Secret must contain 'identity' + // and 'known_hosts' fields. + // +required + SecretRef LocalObjectReference `json:"secretRef"` +} + +type ProjectCredentialsGitDeprecated struct { + // Host specifies the hostname that this secret applies to. If set to '*', this set of credentials + // applies to all hosts. + // Using '*' for http(s) based repositories is not supported, meaning that such credentials sets will be ignored. + // You must always set a proper hostname in that case. + // +required + Host string `json:"host,omitempty"` + + // PathPrefix specifies the path prefix to be used to filter source urls. Only urls that have this prefix will use + // this set of credentials. + // +optional + PathPrefix string `json:"pathPrefix,omitempty"` + + // SecretRef specifies the Secret containing authentication credentials for + // the git repository. + // For HTTPS git repositories the Secret must contain 'username' and 'password' + // fields. + // For SSH git repositories the Secret must contain 'identity' + // and 'known_hosts' fields. + // +required + SecretRef LocalObjectReference `json:"secretRef"` +} + +type ProjectCredentialsOci struct { + // Registry specifies the hostname that this secret applies to. + // +required + Registry string `json:"registry,omitempty"` + + // Repository specifies the org and repo name in the format 'org-name/repo-name'. + // Both 'org-name' and 'repo-name' can be specified as '*', meaning that all names are matched. + // +optional + Repository string `json:"repository,omitempty"` + + // SecretRef specifies the Secret containing authentication credentials for + // the oci repository. + // The secret must contain 'username' and 'password'. + // +required + SecretRef LocalObjectReference `json:"secretRef"` +} + +type ProjectCredentialsHelm struct { + // Host specifies the hostname that this secret applies to. + // +required + Host string `json:"host"` + + // Path specifies the path to be used to filter Helm urls. The path can contain wildcards. These credentials + // will only be used for matching URLs. If omitted, all URLs are considered to match. + // +optional + Path string `json:"path,omitempty"` + + // SecretRef specifies the Secret containing authentication credentials for + // the Helm repository. + // The secret can either container basic authentication credentials via `username` and `password` or + // TLS authentication via `certFile` and `keyFile`. `caFile` can be specified to override the CA to use while + // contacting the repository. + // The secret can also contain `insecureSkipTlsVerify: "true"`, which will disable TLS verification. + // `passCredentialsAll: "true"` can be specified to make the controller pass credentials to all requests, even if + // the hostname changes in-between. + // +required + SecretRef LocalObjectReference `json:"secretRef"` +} + +// Decryption defines how decryption is handled for Kubernetes manifests. +type Decryption struct { + // Provider is the name of the decryption engine. + // +kubebuilder:validation:Enum=sops + // +required + Provider string `json:"provider"` + + // The secret name containing the private OpenPGP keys used for decryption. + // +optional + SecretRef *LocalObjectReference `json:"secretRef,omitempty"` + + // ServiceAccount specifies the service account used to authenticate against cloud providers. + // This is currently only usable for AWS KMS keys. The specified service account will be used to authenticate to AWS + // by signing a token in an IRSA compliant way. + // +optional + ServiceAccount string `json:"serviceAccount,omitempty"` +} + +type HelmCredentials struct { + // SecretRef holds the name of a secret that contains the Helm credentials. + // The secret must either contain the fields `credentialsId` which refers to the credentialsId + // found in https://kluctl.io/docs/kluctl/reference/deployments/helm/#private-repositories or an `url` used + // to match the credentials found in Kluctl projects helm-chart.yaml files. + // The secret can either container basic authentication credentials via `username` and `password` or + // TLS authentication via `certFile` and `keyFile`. `caFile` can be specified to override the CA to use while + // contacting the repository. + // The secret can also contain `insecureSkipTlsVerify: "true"`, which will disable TLS verification. + // `passCredentialsAll: "true"` can be specified to make the controller pass credentials to all requests, even if + // the hostname changes in-between. + // +required + SecretRef LocalObjectReference `json:"secretRef,omitempty"` +} + +// KubeConfig references a Kubernetes secret that contains a kubeconfig file. +type KubeConfig struct { + // SecretRef holds the 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'. The secret must be in the same namespace as + // the Kustomization. + // 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 + // the KluctlDeployment. + // +required + SecretRef SecretKeyReference `json:"secretRef,omitempty"` +} + +// KluctlDeploymentStatus defines the observed state of KluctlDeployment +type KluctlDeploymentStatus struct { + // +optional + ReconcileRequestResult *ManualRequestResult `json:"reconcileRequestResult,omitempty"` + + // +optional + DiffRequestResult *ManualRequestResult `json:"diffRequestResult,omitempty"` + + // +optional + DeployRequestResult *ManualRequestResult `json:"deployRequestResult,omitempty"` + + // +optional + PruneRequestResult *ManualRequestResult `json:"pruneRequestResult,omitempty"` + + // +optional + ValidateRequestResult *ManualRequestResult `json:"validateRequestResult,omitempty"` + + // ObservedGeneration is the last reconciled generation. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // ObservedCommit is the last commit observed + ObservedCommit string `json:"observedCommit,omitempty"` + + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // +optional + ProjectKey *gittypes.ProjectKey `json:"projectKey,omitempty"` + + // +optional + TargetKey *result.TargetKey `json:"targetKey,omitempty"` + + // +optional + LastObjectsHash string `json:"lastObjectsHash,omitempty"` + + // +optional + LastManualObjectsHash *string `json:"lastManualObjectsHash,omitempty"` + + // +optional + LastPrepareError string `json:"lastPrepareError,omitempty"` + + // LastDiffResult is the result summary of the last diff command + // +optional + // +kubebuilder:pruning:PreserveUnknownFields + LastDiffResult *runtime.RawExtension `json:"lastDiffResult,omitempty"` + + // LastDeployResult is the result summary of the last deploy command + // +optional + // +kubebuilder:pruning:PreserveUnknownFields + LastDeployResult *runtime.RawExtension `json:"lastDeployResult,omitempty"` + + // LastValidateResult is the result summary of the last validate command + // +optional + LastValidateResult *runtime.RawExtension `json:"lastValidateResult,omitempty"` + + // LastDriftDetectionResult is the result of the last drift detection command + // optional + LastDriftDetectionResult *runtime.RawExtension `json:"lastDriftDetectionResult,omitempty"` + + // LastDriftDetectionResultMessage contains a short message that describes the drift + // optional + LastDriftDetectionResultMessage string `json:"lastDriftDetectionResultMessage,omitempty"` +} + +func (s *KluctlDeploymentStatus) SetLastDiffResult(crs *result.CommandResultSummary) { + if crs == nil { + s.LastDiffResult = nil + } else { + b := yaml.WriteJsonStringMust(crs) + s.LastDiffResult = &runtime.RawExtension{Raw: []byte(b)} + } +} + +func (s *KluctlDeploymentStatus) SetLastDeployResult(crs *result.CommandResultSummary) { + if crs == nil { + s.LastDeployResult = nil + } else { + b := yaml.WriteJsonStringMust(crs) + s.LastDeployResult = &runtime.RawExtension{Raw: []byte(b)} + } +} + +func (s *KluctlDeploymentStatus) SetLastValidateResult(crs *result.ValidateResult) { + if crs == nil { + s.LastValidateResult = nil + } else { + b := yaml.WriteJsonStringMust(crs) + s.LastValidateResult = &runtime.RawExtension{Raw: []byte(b)} + } +} + +func (s *KluctlDeploymentStatus) SetLastDriftDetectionResult(dr *result.DriftDetectionResult) { + if dr == nil { + s.LastDriftDetectionResult = nil + } else { + b := yaml.WriteJsonStringMust(dr) + s.LastDriftDetectionResult = &runtime.RawExtension{Raw: []byte(b)} + s.LastDriftDetectionResultMessage = dr.BuildShortMessage() + } +} + +func (s *KluctlDeploymentStatus) GetLastDeployResult() (*result.CommandResultSummary, error) { + if s.LastDeployResult == nil { + return nil, nil + } + var ret result.CommandResultSummary + err := yaml.ReadYamlBytes(s.LastDeployResult.Raw, &ret) + if err != nil { + return nil, err + } + return &ret, nil +} + +func (s *KluctlDeploymentStatus) GetLastValidateResult() (*result.ValidateResult, error) { + if s.LastValidateResult == nil { + return nil, nil + } + var ret result.ValidateResult + err := yaml.ReadYamlBytes(s.LastValidateResult.Raw, &ret) + if err != nil { + return nil, err + } + return &ret, nil +} + +func (s *KluctlDeploymentStatus) GetDriftDetectionResult() (*result.DriftDetectionResult, error) { + if s.LastDriftDetectionResult == nil { + return nil, nil + } + var ret result.DriftDetectionResult + err := yaml.ReadYamlBytes(s.LastDriftDetectionResult.Raw, &ret) + if err != nil { + return nil, err + } + return &ret, nil +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Suspend",type="boolean",JSONPath=".spec.suspend",description="" +//+kubebuilder:printcolumn:name="DryRun",type="boolean",JSONPath=".spec.dryRun",description="" +//+kubebuilder:printcolumn:name="Deployed",type="date",JSONPath=".status.lastDeployResult.commandInfo.endTime",description="" +//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" +//+kubebuilder:printcolumn:name="Drift",type="string",JSONPath=".status.lastDriftDetectionResultMessage",description="" +//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" + +// KluctlDeployment is the Schema for the kluctldeployments API +type KluctlDeployment struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec KluctlDeploymentSpec `json:"spec,omitempty"` + Status KluctlDeploymentStatus `json:"status,omitempty"` +} + +// GetConditions returns the status conditions of the object. +func (in *KluctlDeployment) GetConditions() []metav1.Condition { + return in.Status.Conditions +} + +// SetConditions sets the status conditions on the object. +func (in *KluctlDeployment) SetConditions(conditions []metav1.Condition) { + in.Status.Conditions = conditions +} + +//+kubebuilder:object:root=true + +// KluctlDeploymentList contains a list of KluctlDeployment +type KluctlDeploymentList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []KluctlDeployment `json:"items"` +} + +func init() { + SchemeBuilder.Register(&KluctlDeployment{}, &KluctlDeploymentList{}) +} diff --git a/api/v1beta1/manual_requests.go b/api/v1beta1/manual_requests.go new file mode 100644 index 000000000..b88391b89 --- /dev/null +++ b/api/v1beta1/manual_requests.go @@ -0,0 +1,35 @@ +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// ManualRequest is used in json form inside the manual request annotations +type ManualRequest struct { + // +required + RequestValue string `json:"requestValue"` + + // +optional + OverridesPatch *runtime.RawExtension `json:"overridesPatch,omitempty"` +} + +type ManualRequestResult struct { + // +required + Request ManualRequest `json:"request"` + + // +required + StartTime metav1.Time `json:"startTime"` + + // +optional + EndTime *metav1.Time `json:"endTime,omitempty"` + + // +required + ReconcileId string `json:"reconcileId"` + + // +optional + ResultId string `json:"resultId,omitempty"` + + // +optional + CommandError string `json:"commandError,omitempty"` +} diff --git a/api/v1beta1/util_types.go b/api/v1beta1/util_types.go new file mode 100644 index 000000000..1236e80e6 --- /dev/null +++ b/api/v1beta1/util_types.go @@ -0,0 +1,31 @@ +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LocalObjectReference struct { + // Name of the referent. + // +required + Name string `json:"name"` +} + +// SecretKeyReference contains enough information to locate the referenced Kubernetes Secret object in the same +// namespace. Optionally a key can be specified. +// Use this type instead of core/v1 SecretKeySelector when the Key is optional and the Optional field is not +// applicable. +type SecretKeyReference struct { + // Name of the Secret. + // +required + Name string `json:"name"` + + // Key in the Secret, when not specified an implementation-specific default key is used. + // +optional + Key string `json:"key,omitempty"` +} + +// +kubebuilder:validation:Type=string +// +kubebuilder:validation:Pattern="^(([0-9]+(\\.[0-9]+)?(ms|s|m|h))+)" +type SafeDuration struct { + metav1.Duration +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 000000000..c96524e19 --- /dev/null +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,614 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + gittypes "github.com/kluctl/kluctl/lib/git/types" + "github.com/kluctl/kluctl/v2/pkg/types" + "github.com/kluctl/kluctl/v2/pkg/types/result" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Decryption) DeepCopyInto(out *Decryption) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Decryption. +func (in *Decryption) DeepCopy() *Decryption { + if in == nil { + return nil + } + out := new(Decryption) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmCredentials) DeepCopyInto(out *HelmCredentials) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmCredentials. +func (in *HelmCredentials) DeepCopy() *HelmCredentials { + if in == nil { + return nil + } + out := new(HelmCredentials) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KluctlDeployment) DeepCopyInto(out *KluctlDeployment) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KluctlDeployment. +func (in *KluctlDeployment) DeepCopy() *KluctlDeployment { + if in == nil { + return nil + } + out := new(KluctlDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *KluctlDeployment) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KluctlDeploymentList) DeepCopyInto(out *KluctlDeploymentList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]KluctlDeployment, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KluctlDeploymentList. +func (in *KluctlDeploymentList) DeepCopy() *KluctlDeploymentList { + if in == nil { + return nil + } + out := new(KluctlDeploymentList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *KluctlDeploymentList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KluctlDeploymentSpec) DeepCopyInto(out *KluctlDeploymentSpec) { + *out = *in + in.Source.DeepCopyInto(&out.Source) + if in.SourceOverrides != nil { + in, out := &in.SourceOverrides, &out.SourceOverrides + *out = make([]SourceOverride, len(*in)) + copy(*out, *in) + } + in.Credentials.DeepCopyInto(&out.Credentials) + if in.Decryption != nil { + in, out := &in.Decryption, &out.Decryption + *out = new(Decryption) + (*in).DeepCopyInto(*out) + } + out.Interval = in.Interval + if in.RetryInterval != nil { + in, out := &in.RetryInterval, &out.RetryInterval + *out = new(v1.Duration) + **out = **in + } + if in.DeployInterval != nil { + in, out := &in.DeployInterval, &out.DeployInterval + *out = new(SafeDuration) + **out = **in + } + if in.ValidateInterval != nil { + in, out := &in.ValidateInterval, &out.ValidateInterval + *out = new(SafeDuration) + **out = **in + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(v1.Duration) + **out = **in + } + if in.HelmCredentials != nil { + in, out := &in.HelmCredentials, &out.HelmCredentials + *out = make([]HelmCredentials, len(*in)) + copy(*out, *in) + } + if in.KubeConfig != nil { + in, out := &in.KubeConfig, &out.KubeConfig + *out = new(KubeConfig) + **out = **in + } + if in.Target != nil { + in, out := &in.Target, &out.Target + *out = new(string) + **out = **in + } + if in.TargetNameOverride != nil { + in, out := &in.TargetNameOverride, &out.TargetNameOverride + *out = new(string) + **out = **in + } + if in.Context != nil { + in, out := &in.Context, &out.Context + *out = new(string) + **out = **in + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Images != nil { + in, out := &in.Images, &out.Images + *out = make([]types.FixedImage, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.IncludeTags != nil { + in, out := &in.IncludeTags, &out.IncludeTags + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExcludeTags != nil { + in, out := &in.ExcludeTags, &out.ExcludeTags + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.IncludeDeploymentDirs != nil { + in, out := &in.IncludeDeploymentDirs, &out.IncludeDeploymentDirs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExcludeDeploymentDirs != nil { + in, out := &in.ExcludeDeploymentDirs, &out.ExcludeDeploymentDirs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ManualObjectsHash != nil { + in, out := &in.ManualObjectsHash, &out.ManualObjectsHash + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KluctlDeploymentSpec. +func (in *KluctlDeploymentSpec) DeepCopy() *KluctlDeploymentSpec { + if in == nil { + return nil + } + out := new(KluctlDeploymentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KluctlDeploymentStatus) DeepCopyInto(out *KluctlDeploymentStatus) { + *out = *in + if in.ReconcileRequestResult != nil { + in, out := &in.ReconcileRequestResult, &out.ReconcileRequestResult + *out = new(ManualRequestResult) + (*in).DeepCopyInto(*out) + } + if in.DiffRequestResult != nil { + in, out := &in.DiffRequestResult, &out.DiffRequestResult + *out = new(ManualRequestResult) + (*in).DeepCopyInto(*out) + } + if in.DeployRequestResult != nil { + in, out := &in.DeployRequestResult, &out.DeployRequestResult + *out = new(ManualRequestResult) + (*in).DeepCopyInto(*out) + } + if in.PruneRequestResult != nil { + in, out := &in.PruneRequestResult, &out.PruneRequestResult + *out = new(ManualRequestResult) + (*in).DeepCopyInto(*out) + } + if in.ValidateRequestResult != nil { + in, out := &in.ValidateRequestResult, &out.ValidateRequestResult + *out = new(ManualRequestResult) + (*in).DeepCopyInto(*out) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ProjectKey != nil { + in, out := &in.ProjectKey, &out.ProjectKey + *out = new(gittypes.ProjectKey) + **out = **in + } + if in.TargetKey != nil { + in, out := &in.TargetKey, &out.TargetKey + *out = new(result.TargetKey) + **out = **in + } + if in.LastManualObjectsHash != nil { + in, out := &in.LastManualObjectsHash, &out.LastManualObjectsHash + *out = new(string) + **out = **in + } + if in.LastDiffResult != nil { + in, out := &in.LastDiffResult, &out.LastDiffResult + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.LastDeployResult != nil { + in, out := &in.LastDeployResult, &out.LastDeployResult + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.LastValidateResult != nil { + in, out := &in.LastValidateResult, &out.LastValidateResult + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.LastDriftDetectionResult != nil { + in, out := &in.LastDriftDetectionResult, &out.LastDriftDetectionResult + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KluctlDeploymentStatus. +func (in *KluctlDeploymentStatus) DeepCopy() *KluctlDeploymentStatus { + if in == nil { + return nil + } + out := new(KluctlDeploymentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeConfig) DeepCopyInto(out *KubeConfig) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeConfig. +func (in *KubeConfig) DeepCopy() *KubeConfig { + if in == nil { + return nil + } + out := new(KubeConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalObjectReference) DeepCopyInto(out *LocalObjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalObjectReference. +func (in *LocalObjectReference) DeepCopy() *LocalObjectReference { + if in == nil { + return nil + } + out := new(LocalObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManualRequest) DeepCopyInto(out *ManualRequest) { + *out = *in + if in.OverridesPatch != nil { + in, out := &in.OverridesPatch, &out.OverridesPatch + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManualRequest. +func (in *ManualRequest) DeepCopy() *ManualRequest { + if in == nil { + return nil + } + out := new(ManualRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManualRequestResult) DeepCopyInto(out *ManualRequestResult) { + *out = *in + in.Request.DeepCopyInto(&out.Request) + in.StartTime.DeepCopyInto(&out.StartTime) + if in.EndTime != nil { + in, out := &in.EndTime, &out.EndTime + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManualRequestResult. +func (in *ManualRequestResult) DeepCopy() *ManualRequestResult { + if in == nil { + return nil + } + out := new(ManualRequestResult) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectCredentials) DeepCopyInto(out *ProjectCredentials) { + *out = *in + if in.Git != nil { + in, out := &in.Git, &out.Git + *out = make([]ProjectCredentialsGit, len(*in)) + copy(*out, *in) + } + if in.Oci != nil { + in, out := &in.Oci, &out.Oci + *out = make([]ProjectCredentialsOci, len(*in)) + copy(*out, *in) + } + if in.Helm != nil { + in, out := &in.Helm, &out.Helm + *out = make([]ProjectCredentialsHelm, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectCredentials. +func (in *ProjectCredentials) DeepCopy() *ProjectCredentials { + if in == nil { + return nil + } + out := new(ProjectCredentials) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectCredentialsGit) DeepCopyInto(out *ProjectCredentialsGit) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectCredentialsGit. +func (in *ProjectCredentialsGit) DeepCopy() *ProjectCredentialsGit { + if in == nil { + return nil + } + out := new(ProjectCredentialsGit) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectCredentialsGitDeprecated) DeepCopyInto(out *ProjectCredentialsGitDeprecated) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectCredentialsGitDeprecated. +func (in *ProjectCredentialsGitDeprecated) DeepCopy() *ProjectCredentialsGitDeprecated { + if in == nil { + return nil + } + out := new(ProjectCredentialsGitDeprecated) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectCredentialsHelm) DeepCopyInto(out *ProjectCredentialsHelm) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectCredentialsHelm. +func (in *ProjectCredentialsHelm) DeepCopy() *ProjectCredentialsHelm { + if in == nil { + return nil + } + out := new(ProjectCredentialsHelm) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectCredentialsOci) DeepCopyInto(out *ProjectCredentialsOci) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectCredentialsOci. +func (in *ProjectCredentialsOci) DeepCopy() *ProjectCredentialsOci { + if in == nil { + return nil + } + out := new(ProjectCredentialsOci) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectSource) DeepCopyInto(out *ProjectSource) { + *out = *in + if in.Git != nil { + in, out := &in.Git, &out.Git + *out = new(ProjectSourceGit) + (*in).DeepCopyInto(*out) + } + if in.Oci != nil { + in, out := &in.Oci, &out.Oci + *out = new(ProjectSourceOci) + (*in).DeepCopyInto(*out) + } + if in.URL != nil { + in, out := &in.URL, &out.URL + *out = new(string) + **out = **in + } + if in.Ref != nil { + in, out := &in.Ref, &out.Ref + *out = new(gittypes.GitRef) + **out = **in + } + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(LocalObjectReference) + **out = **in + } + if in.Credentials != nil { + in, out := &in.Credentials, &out.Credentials + *out = make([]ProjectCredentialsGitDeprecated, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectSource. +func (in *ProjectSource) DeepCopy() *ProjectSource { + if in == nil { + return nil + } + out := new(ProjectSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectSourceGit) DeepCopyInto(out *ProjectSourceGit) { + *out = *in + if in.Ref != nil { + in, out := &in.Ref, &out.Ref + *out = new(gittypes.GitRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectSourceGit. +func (in *ProjectSourceGit) DeepCopy() *ProjectSourceGit { + if in == nil { + return nil + } + out := new(ProjectSourceGit) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectSourceOci) DeepCopyInto(out *ProjectSourceOci) { + *out = *in + if in.Ref != nil { + in, out := &in.Ref, &out.Ref + *out = new(types.OciRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectSourceOci. +func (in *ProjectSourceOci) DeepCopy() *ProjectSourceOci { + if in == nil { + return nil + } + out := new(ProjectSourceOci) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SafeDuration) DeepCopyInto(out *SafeDuration) { + *out = *in + out.Duration = in.Duration +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SafeDuration. +func (in *SafeDuration) DeepCopy() *SafeDuration { + if in == nil { + return nil + } + out := new(SafeDuration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretKeyReference) DeepCopyInto(out *SecretKeyReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeyReference. +func (in *SecretKeyReference) DeepCopy() *SecretKeyReference { + if in == nil { + return nil + } + out := new(SecretKeyReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SourceOverride) DeepCopyInto(out *SourceOverride) { + *out = *in + out.RepoKey = in.RepoKey +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceOverride. +func (in *SourceOverride) DeepCopy() *SourceOverride { + if in == nil { + return nil + } + out := new(SourceOverride) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/kluctl/args/cobra_types.go b/cmd/kluctl/args/cobra_types.go index c6907ea51..ade735c6e 100644 --- a/cmd/kluctl/args/cobra_types.go +++ b/cmd/kluctl/args/cobra_types.go @@ -5,27 +5,27 @@ import ( "github.com/kluctl/kluctl/v2/pkg/utils" ) -type existingPathType string +type ExistingPathType string -func (s *existingPathType) Set(val string) error { +func (s *ExistingPathType) Set(val string) error { if val != "-" { val = utils.ExpandPath(val) } if !utils.Exists(val) { return fmt.Errorf("%s does not exist", val) } - *s = existingPathType(val) + *s = ExistingPathType(val) return nil } -func (s *existingPathType) Type() string { +func (s *ExistingPathType) Type() string { return "existingpath" } -func (s *existingPathType) String() string { return string(*s) } +func (s *ExistingPathType) String() string { return string(*s) } -type existingFileType string +type ExistingFileType string -func (s *existingFileType) Set(val string) error { +func (s *ExistingFileType) Set(val string) error { if val != "-" { val = utils.ExpandPath(val) } @@ -35,18 +35,18 @@ func (s *existingFileType) Set(val string) error { if utils.IsDirectory(val) { return fmt.Errorf("%s exists but is a directory", val) } - *s = existingFileType(val) + *s = ExistingFileType(val) return nil } -func (s *existingFileType) Type() string { +func (s *ExistingFileType) Type() string { return "existingfile" } -func (s *existingFileType) String() string { return string(*s) } +func (s *ExistingFileType) String() string { return string(*s) } -type existingDirType string +type ExistingDirType string -func (s *existingDirType) Set(val string) error { +func (s *ExistingDirType) Set(val string) error { if val != "-" { val = utils.ExpandPath(val) } @@ -56,11 +56,11 @@ func (s *existingDirType) Set(val string) error { if !utils.IsDirectory(val) { return fmt.Errorf("%s exists but is not a directory", val) } - *s = existingDirType(val) + *s = ExistingDirType(val) return nil } -func (s *existingDirType) Type() string { +func (s *ExistingDirType) Type() string { return "existingdir" } -func (s *existingDirType) String() string { return string(*s) } +func (s *ExistingDirType) String() string { return string(*s) } diff --git a/cmd/kluctl/args/git_credentials.go b/cmd/kluctl/args/git_credentials.go new file mode 100644 index 000000000..fd59fe006 --- /dev/null +++ b/cmd/kluctl/args/git_credentials.go @@ -0,0 +1,112 @@ +package args + +import ( + "context" + "fmt" + "github.com/gobwas/glob" + git_auth "github.com/kluctl/kluctl/lib/git/auth" + "github.com/kluctl/kluctl/v2/pkg/utils" + "os" + "strings" +) + +type GitCredentials struct { + GitUsername []string `group:"git" skipenv:"true" help:"Specify username to use for Git basic authentication. Must be in the form --git-username=/=."` + GitPassword []string `group:"git" skipenv:"true" help:"Specify password to use for Git basic authentication. Must be in the form --git-password=/=."` + GitSshKeyFile []string `group:"git" skipenv:"true" help:"Specify SSH key to use for Git authentication. Must be in the form --git-ssh-key-file=/=."` + GitSshKnownHostsFile []string `group:"git" skipenv:"true" help:"Specify known_hosts file to use for Git authentication. Must be in the form --git-ssh-known-hosts-file=/=."` + GitCAFile []string `group:"git" skipenv:"true" help:"Specify CA bundle to use for https verification. Must be in the form --git-ca-file=/=."` +} + +func (c *GitCredentials) BuildAuthProvider(ctx context.Context) (git_auth.GitAuthProvider, error) { + la := &git_auth.ListAuthProvider{} + if c == nil { + return la, nil + } + + var byHostPath utils.OrderedMap[string, *git_auth.AuthEntry] + + getEntry := func(s string, expectValue bool) (*git_auth.AuthEntry, string, error) { + x := strings.SplitN(s, "=", 2) + if expectValue && len(x) != 2 { + return nil, "", fmt.Errorf("expected value") + } + k := x[0] + e, ok := byHostPath.Get(k) + if !ok { + x := strings.SplitN(k, "/", 2) + e = &git_auth.AuthEntry{} + if len(x) == 2 { + e.Host = x[0] + g, err := glob.Compile(x[1], '/') + if err != nil { + return nil, "", err + } + e.PathStr = x[1] + e.PathGlob = g + } else { + e.Host = x[0] + } + byHostPath.Set(k, e) + } + + if len(x) == 1 { + return e, "", nil + } + return e, x[1], nil + } + + for _, s := range c.GitUsername { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + e.Username = v + } + for _, s := range c.GitPassword { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + e.Password = v + } + for _, s := range c.GitSshKeyFile { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + b, err := os.ReadFile(v) + if err != nil { + return nil, err + } + e.SshKey = b + } + for _, s := range c.GitSshKnownHostsFile { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + b, err := os.ReadFile(v) + if err != nil { + return nil, err + } + e.KnownHosts = b + } + for _, s := range c.GitCAFile { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + b, err := os.ReadFile(v) + if err != nil { + return nil, err + } + e.CABundle = b + } + + for _, e := range byHostPath.ListValues() { + la.AddEntry(*e) + } + + return la, nil +} diff --git a/cmd/kluctl/args/gitops_args.go b/cmd/kluctl/args/gitops_args.go new file mode 100644 index 000000000..c50c33688 --- /dev/null +++ b/cmd/kluctl/args/gitops_args.go @@ -0,0 +1,40 @@ +package args + +import ( + "time" +) + +type GitOpsArgs struct { + ProjectDir + CommandResultReadOnlyFlags + + Name string `group:"gitops" help:"Specifies the name of the KluctlDeployment."` + Namespace string `group:"gitops" short:"n" help:"Specifies the namespace of the KluctlDeployment. If omitted, the current namespace from your kubeconfig is used."` + LabelSelector string `group:"gitops" short:"l" help:"If specified, KluctlDeployments are searched and filtered by this label selector."` + + Kubeconfig ExistingFileType `group:"gitops" help:"Overrides the kubeconfig to use."` + Context string `group:"gitops" help:"Override the context to use."` + ControllerNamespace string `group:"gitops" help:"The namespace where the controller runs in." default:"kluctl-system"` + + LocalSourceOverridePort int `group:"gitops" help:"Specifies the local port to which the source-override client should connect to when running the controller locally." default:"0"` +} + +type GitOpsLogArgs struct { + LogSince time.Duration `group:"logs" help:"Show logs since this time." default:"60s"` + LogGroupingTime time.Duration `group:"logs" help:"Logs are by default grouped by time passed, meaning that they are printed in batches to make reading them easier. This argument allows to modify the grouping time." default:"1s"` + LogTime bool `group:"logs" help:"If enabled, adds timestamps to log lines"` +} + +type GitOpsOverridableArgs struct { + SourceOverrides + TargetFlagsBase + ArgsFlags + ImageFlags + InclusionFlags + DryRunFlags + ForceApplyFlags + ReplaceOnErrorFlags + AbortOnErrorFlags + + TargetContext string `group:"override" help:"Overrides the context name specified in the target. If the selected target does not specify a context or the no-name target is used, --context will override the currently active context."` +} diff --git a/cmd/kluctl/args/helm_credentials.go b/cmd/kluctl/args/helm_credentials.go index 23db78c6d..58bd54429 100644 --- a/cmd/kluctl/args/helm_credentials.go +++ b/cmd/kluctl/args/helm_credentials.go @@ -1,81 +1,160 @@ package args import ( - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/repo" + "context" + "fmt" + "github.com/gobwas/glob" + "github.com/kluctl/kluctl/lib/status" + helm_auth "github.com/kluctl/kluctl/v2/pkg/helm/auth" + "github.com/kluctl/kluctl/v2/pkg/utils" + "os" "strings" ) type HelmCredentials struct { - Username []string `group:"misc" help:"Specify username to use for Helm Repository authentication. Must be in the form --username=:, where must match the id specified in the helm-chart.yaml."` - Password []string `group:"misc" help:"Specify password to use for Helm Repository authentication. Must be in the form --password=:, where must match the id specified in the helm-chart.yaml."` - KeyFile []string `group:"misc" help:"Specify client certificate to use for Helm Repository authentication. Must be in the form --key-file=:, where must match the id specified in the helm-chart.yaml."` - InsecureSkipTlsVerify []string `group:"misc" help:"Controls skipping of TLS verification. Must be in the form --insecure-skip-tls-verify=, where must match the id specified in the helm-chart.yaml."` + HelmUsername []string `group:"helm" skipenv:"true" help:"Specify username to use for Helm Repository authentication. Must be in the form --helm-username=/= or in the deprecated form --helm-username=:, where must match the id specified in the helm-chart.yaml."` + HelmPassword []string `group:"helm" skipenv:"true" help:"Specify password to use for Helm Repository authentication. Must be in the form --helm-password=/= or in the deprecated form --helm-password=:, where must match the id specified in the helm-chart.yaml."` + HelmKeyFile []string `group:"helm" skipenv:"true" help:"Specify client certificate to use for Helm Repository authentication. Must be in the form --helm-key-file=/= or in the deprecated form --helm-key-file=:, where must match the id specified in the helm-chart.yaml."` + HelmCertFile []string `group:"helm" skipenv:"true" help:"Specify key to use for Helm Repository authentication. Must be in the form --helm-cert-file=/= or in the deprecated form --helm-cert-file=:, where must match the id specified in the helm-chart.yaml."` + HelmCAFile []string `group:"helm" skipenv:"true" help:"Specify ca bundle certificate to use for Helm Repository authentication. Must be in the form --helm-ca-file=/= or in the deprecated form --helm-ca-file=:, where must match the id specified in the helm-chart.yaml."` + HelmInsecureSkipTlsVerify []string `group:"helm" skipenv:"true" help:"Controls skipping of TLS verification. Must be in the form --helm-insecure-skip-tls-verify=/ or in the deprecated form --helm-insecure-skip-tls-verify=, where must match the id specified in the helm-chart.yaml."` + HelmCreds []string `group:"helm" skipenv:"true" help:"This is a shortcut to --helm-username and --helm-password. Must be in the form --helm-creds=/=:, which specifies the username and password for the same repository."` } -func (c *HelmCredentials) FindCredentials(repoUrl string, credentialsId *string) *repo.Entry { - if credentialsId != nil { - splitIdAndValue := func(s string) (string, bool) { - x := strings.SplitN(s, ":", 2) - if len(x) < 0 { - return "", false - } - if x[0] != *credentialsId { - return "", false - } - return x[1], true - } +func (c *HelmCredentials) BuildAuthProvider(ctx context.Context) (helm_auth.HelmAuthProvider, error) { + la := &helm_auth.ListAuthProvider{} + if c == nil { + return la, nil + } - var e repo.Entry - for _, x := range c.Username { - if v, ok := splitIdAndValue(x); ok { - e.Username = v - } + var byCredentialId utils.OrderedMap[string, *helm_auth.AuthEntry] + var byHostPath utils.OrderedMap[string, *helm_auth.AuthEntry] + + getDeprecatedEntry := func(s string) (*helm_auth.AuthEntry, string, bool) { + x := strings.Split(s, ":") + if len(x) != 2 { + return nil, "", false } - for _, x := range c.Password { - if v, ok := splitIdAndValue(x); ok { - e.Password = v + status.Deprecation(ctx, "helm-credential-args-id", "Passing Helm credentials via credentialsId is deprecated and support for it will be removed in a future version of Kluctl. Please switch to using the /=value format.") + k := x[0] + e, ok := byCredentialId.Get(k) + if !ok { + e = &helm_auth.AuthEntry{ + CredentialsId: k, } + byCredentialId.Set(k, e) } - for _, x := range c.KeyFile { - if v, ok := splitIdAndValue(x); ok { - e.KeyFile = v + return e, x[1], true + } + + getEntry := func(s string, expectValue bool) (*helm_auth.AuthEntry, string, error) { + if !strings.Contains(s, "=") { + e, v, ok := getDeprecatedEntry(s) + if ok { + return e, v, nil } } - for _, x := range c.InsecureSkipTlsVerify { - if x == *credentialsId { - e.InsecureSkipTLSverify = true + + x := strings.SplitN(s, "=", 2) + if expectValue && len(x) != 2 { + return nil, "", fmt.Errorf("expected value: %s", s) + } + + k := x[0] + e, ok := byHostPath.Get(k) + if !ok { + x := strings.SplitN(k, "/", 2) + e = &helm_auth.AuthEntry{} + if len(x) == 2 { + e.Host = x[0] + g, err := glob.Compile(x[1], '/') + if err != nil { + return nil, "", err + } + e.PathStr = x[1] + e.PathGlob = g + } else { + e.Host = x[0] } + byHostPath.Set(k, e) } - if e != (repo.Entry{}) { - return &e + if len(x) == 1 { + return e, "", nil } + return e, x[1], nil } - env := cli.New() - - f, err := repo.LoadFile(env.RepositoryConfig) - if err != nil { - return nil + for _, s := range c.HelmUsername { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + e.Username = v } - - removeTrailingSlash := func(s string) string { - if len(s) == 0 { - return s + for _, s := range c.HelmPassword { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err } - if s[len(s)-1] == '/' { - return s[:len(s)-1] + e.Password = v + } + for _, s := range c.HelmKeyFile { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + e.Key, err = os.ReadFile(v) + if err != nil { + return nil, err } - return s } - repoUrl = removeTrailingSlash(repoUrl) - - for _, e := range f.Repositories { - if removeTrailingSlash(e.URL) == repoUrl { - return e + for _, s := range c.HelmCertFile { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + e.Cert, err = os.ReadFile(v) + if err != nil { + return nil, err + } + } + for _, s := range c.HelmCAFile { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + e.CA, err = os.ReadFile(v) + if err != nil { + return nil, err + } + } + for _, s := range c.HelmInsecureSkipTlsVerify { + e, _, err := getEntry(s, false) + if err != nil { + return nil, err + } + e.InsecureSkipTLSverify = true + } + for _, s := range c.HelmCreds { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + x := strings.SplitN(v, ":", 2) + if len(x) != 2 { + return nil, fmt.Errorf("format of --helm-creds values must be /=:") } + e.Username = x[0] + e.Password = x[1] + } + + for _, e := range byCredentialId.ListValues() { + la.AddEntry(*e) + } + for _, e := range byHostPath.ListValues() { + la.AddEntry(*e) } - return nil + return la, nil } diff --git a/cmd/kluctl/args/images.go b/cmd/kluctl/args/images.go index 13aaf244d..287d5f84c 100644 --- a/cmd/kluctl/args/images.go +++ b/cmd/kluctl/args/images.go @@ -2,16 +2,14 @@ package args import ( "fmt" + "github.com/kluctl/kluctl/lib/yaml" "github.com/kluctl/kluctl/v2/pkg/types" - "github.com/kluctl/kluctl/v2/pkg/yaml" "strings" ) type ImageFlags struct { FixedImage []string `group:"images" short:"F" help:"Pin an image to a given version. Expects '--fixed-image=image<:namespace:deployment:container>=result'"` - FixedImagesFile existingFileType `group:"images" help:"Use .yaml file to pin image versions. See output of list-images sub-command or read the documentation for details about the output format" exts:"yml,yaml"` - UpdateImages bool `group:"images" short:"u" help:"This causes kluctl to prefer the latest image found in registries, based on the 'latest_image' filters provided to 'images.get_image(...)' calls. Use this flag if you want to update to the latest versions/tags of all images. '-u' takes precedence over '--fixed-image/--fixed-images-file', meaning that the latest images are used even if an older image is given via fixed images."` - OfflineImages bool `group:"images" help:"Omit contacting image registries and do not query for latest image tags."` + FixedImagesFile ExistingFileType `group:"images" help:"Use .yaml file to pin image versions. See output of list-images sub-command or read the documentation for details about the output format" exts:"yml,yaml"` } func (args *ImageFlags) LoadFixedImagesFromArgs() ([]types.FixedImage, error) { @@ -45,7 +43,7 @@ func buildFixedImageEntryFromArg(arg string) (*types.FixedImage, error) { s = strings.Split(image, ":") e := types.FixedImage{ - Image: s[0], + Image: &s[0], ResultImage: result, } diff --git a/cmd/kluctl/args/misc.go b/cmd/kluctl/args/misc.go index 196f52872..308617930 100644 --- a/cmd/kluctl/args/misc.go +++ b/cmd/kluctl/args/misc.go @@ -8,6 +8,11 @@ type YesFlags struct { Yes bool `group:"misc" short:"y" help:"Suppresses 'Are you sure?' questions and proceeds as if you would answer 'yes'."` } +type OfflineKubernetesFlags struct { + OfflineKubernetes bool `group:"misc" help:"Run command in offline mode, meaning that it will not try to connect the target cluster"` + KubernetesVersion string `group:"misc" help:"Specify the Kubernetes version that will be assumed. This will also override the kubeVersion used when rendering Helm Charts."` +} + type DryRunFlags struct { DryRun bool `group:"misc" help:"Performs all kubernetes API calls in dry-run mode."` } @@ -26,9 +31,10 @@ type HookFlags struct { } type IgnoreFlags struct { - IgnoreTags bool `group:"misc" help:"Ignores changes in tags when diffing"` - IgnoreLabels bool `group:"misc" help:"Ignores changes in labels when diffing"` - IgnoreAnnotations bool `group:"misc" help:"Ignores changes in annotations when diffing"` + IgnoreTags bool `group:"misc" help:"Ignores changes in tags when diffing"` + IgnoreLabels bool `group:"misc" help:"Ignores changes in labels when diffing"` + IgnoreAnnotations bool `group:"misc" help:"Ignores changes in annotations when diffing"` + IgnoreKluctlMetadata bool `group:"misc" help:"Ignores changes in Kluctl related metadata (e.g. tags, discriminators, ...)"` } type AbortOnErrorFlags struct { @@ -37,6 +43,8 @@ type AbortOnErrorFlags struct { type OutputFormatFlags struct { OutputFormat []string `group:"misc" short:"o" help:"Specify output format and target file, in the format 'format=path'. Format can either be 'text' or 'yaml'. Can be specified multiple times. The actual format for yaml is currently not documented and subject to change."` + NoObfuscate bool `group:"misc" help:"Disable obfuscation of sensitive/secret data"` + ShortOutput bool `group:"misc" help:"When using the 'text' output format (which is the default), only names of changes objects are shown instead of showing all changes."` } type OutputFlags struct { diff --git a/cmd/kluctl/args/overrides.go b/cmd/kluctl/args/overrides.go new file mode 100644 index 000000000..52e0f18ee --- /dev/null +++ b/cmd/kluctl/args/overrides.go @@ -0,0 +1,91 @@ +package args + +import ( + "context" + "fmt" + "github.com/kluctl/kluctl/lib/git/types" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/v2/pkg/sourceoverride" + "strings" +) + +type SourceOverrides struct { + LocalGitOverride []string `group:"project" help:"Specify a single repository local git override in the form of 'github.com/my-org/my-repo=/local/path/to/override'. This will cause kluctl to not use git to clone for the specified repository but instead use the local directory. This is useful in case you need to test out changes in external git repositories without pushing them."` + LocalGitGroupOverride []string `group:"project" help:"Same as --local-git-override, but for a whole group prefix instead of a single repository. All repositories that have the given prefix will be overridden with the given local path and the repository suffix appended. For example, 'gitlab.com/some-org/sub-org=/local/path/to/my-forks' will override all repositories below 'gitlab.com/some-org/sub-org/' with the repositories found in '/local/path/to/my-forks'. It will however only perform an override if the given repository actually exists locally and otherwise revert to the actual (non-overridden) repository."` + LocalOciOverride []string `group:"project" help:"Same as --local-git-override, but for OCI repositories."` + LocalOciGroupOverride []string `group:"project" help:"Same as --local-git-group-override, but for OCI repositories."` +} + +func (a *SourceOverrides) ParseOverrides(ctx context.Context) (*sourceoverride.Manager, error) { + var overrides []sourceoverride.RepoOverride + for _, x := range a.LocalGitOverride { + ro, err := a.parseRepoOverride(ctx, x, false, "git", true) + if err != nil { + return nil, fmt.Errorf("invalid --local-git-override: %w", err) + } + overrides = append(overrides, ro) + } + for _, x := range a.LocalGitGroupOverride { + ro, err := a.parseRepoOverride(ctx, x, true, "git", true) + if err != nil { + return nil, fmt.Errorf("invalid --local-git-group-override: %w", err) + } + overrides = append(overrides, ro) + } + for _, x := range a.LocalOciOverride { + ro, err := a.parseRepoOverride(ctx, x, false, "oci", false) + if err != nil { + return nil, fmt.Errorf("invalid --local-oci-override: %w", err) + } + overrides = append(overrides, ro) + } + for _, x := range a.LocalOciGroupOverride { + ro, err := a.parseRepoOverride(ctx, x, true, "oci", false) + if err != nil { + return nil, fmt.Errorf("invalid --local-oci-group-override: %w", err) + } + overrides = append(overrides, ro) + } + m := sourceoverride.NewManager(overrides) + return m, nil +} + +func (a *SourceOverrides) parseRepoOverride(ctx context.Context, s string, isGroup bool, type_ string, allowLegacy bool) (sourceoverride.RepoOverride, error) { + sp := strings.SplitN(s, "=", 2) + if len(sp) != 2 { + return sourceoverride.RepoOverride{}, fmt.Errorf("%s", s) + } + + repoKey, err := types.ParseRepoKey(sp[0], type_) + if err != nil { + if !allowLegacy { + return sourceoverride.RepoOverride{}, err + } + + // try as legacy repo key + u, err2 := types.ParseGitUrl(sp[0]) + if err2 != nil { + // return original error + return sourceoverride.RepoOverride{}, err + } + + x := u.Host + if !strings.HasPrefix(u.Path, "/") { + x += "/" + } + x += u.Path + repoKey, err2 = types.ParseRepoKey(x, type_) + if err2 != nil { + // return original error + return sourceoverride.RepoOverride{}, err + } + + status.Deprecation(ctx, "old-repo-override", "Passing --local-git-override/--local-git-override-group in the example.com:path form is deprecated and will not be supported in future versions of Kluctl. Please use the example.com/path form.") + } + + return sourceoverride.RepoOverride{ + RepoKey: repoKey, + IsGroup: isGroup, + Override: sp[1], + }, nil +} diff --git a/cmd/kluctl/args/project.go b/cmd/kluctl/args/project.go index 2639966fb..4a4d658c7 100644 --- a/cmd/kluctl/args/project.go +++ b/cmd/kluctl/args/project.go @@ -1,26 +1,93 @@ package args -import "time" +import ( + "github.com/kluctl/kluctl/v2/pkg/kluctl_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "os" + "path/filepath" + "time" +) + +type ProjectDir struct { + ProjectDir ExistingDirType `group:"project" help:"Specify the project directory. Defaults to the current working directory."` +} + +func (a ProjectDir) GetProjectDir() (string, error) { + if a.ProjectDir != "" { + return filepath.Abs(a.ProjectDir.String()) + } + cwd, err := os.Getwd() + if err != nil { + return "", err + } + return cwd, nil +} type ProjectFlags struct { - ProjectUrl string `group:"project" short:"p" help:"Git url of the kluctl project. If not specified, the current directory will be used instead of a remote Git project"` - ProjectRef string `group:"project" short:"b" help:"Git ref of the kluctl project. Only used when --project-url was given."` + ProjectDir + SourceOverrides - ProjectConfig existingFileType `group:"project" short:"c" help:"Location of the .kluctl.yaml config file. Defaults to $PROJECT/.kluctl.yaml" exts:"yml,yaml"` - LocalClusters existingDirType `group:"project" help:"DEPRECATED. Local clusters directory. Overrides the project from .kluctl.yaml"` - LocalDeployment existingDirType `group:"project" help:"DEPRECATED. Local deployment directory. Overrides the project from .kluctl.yaml"` - LocalSealedSecrets existingDirType `group:"project" help:"DEPRECATED. Local sealed-secrets directory. Overrides the project from .kluctl.yaml" ` - OutputMetadata string `group:"project" help:"Specify the output path for the project metadata to be written to."` - Cluster string `group:"project" help:"DEPRECATED. Specify/Override cluster"` + ProjectConfig ExistingFileType `group:"project" short:"c" help:"Location of the .kluctl.yaml config file. Defaults to $PROJECT/.kluctl.yaml" exts:"yml,yaml"` Timeout time.Duration `group:"project" help:"Specify timeout for all operations, including loading of the project, all external api calls and waiting for readiness." default:"10m"` GitCacheUpdateInterval time.Duration `group:"project" help:"Specify the time to wait between git cache updates. Defaults to not wait at all and always updating caches."` } type ArgsFlags struct { - Arg []string `group:"project" short:"a" help:"Template argument in the form name=value"` + Arg []string `group:"project" short:"a" help:"Passes a template argument in the form of name=value. Nested args can be set with the '-a my.nested.arg=value' syntax. Values are interpreted as yaml values, meaning that 'true' and 'false' will lead to boolean values and numbers will be treated as numbers. Use quotes if you want these to be treated as strings. If the value starts with @, it is treated as a file, meaning that the contents of the file will be loaded and treated as yaml."` + ArgsFromFile []string `group:"project" help:"Loads a yaml file and makes it available as arguments, meaning that they will be available thought the global 'args' variable."` +} + +func (a *ArgsFlags) LoadArgs() (*uo.UnstructuredObject, error) { + if a == nil { + return uo.New(), nil + } + + var args *uo.UnstructuredObject + optionArgs, err := kluctl_project.ParseArgs(a.Arg) + if err != nil { + return nil, err + } + args, err = kluctl_project.ConvertArgsToVars(optionArgs, true) + if err != nil { + return nil, err + } + for _, a := range a.ArgsFromFile { + optionArgs2, err := uo.FromFile(a) + if err != nil { + return nil, err + } + args.Merge(optionArgs2) + } + return args, nil +} + +type TargetFlagsBase struct { + Target string `group:"project" short:"t" help:"Target name to run command for. Target must exist in .kluctl.yaml."` + TargetNameOverride string `group:"project" short:"T" help:"Overrides the target name. If -t is used at the same time, then the target will be looked up based on -t and then renamed to the value of -T. If no target is specified via -t, then the no-name target is renamed to the value of -T."` } type TargetFlags struct { - Target string `group:"project" short:"t" help:"Target name to run command for. Target must exist in .kluctl.yaml."` + TargetFlagsBase + Context string `group:"project" help:"Overrides the context name specified in the target. If the selected target does not specify a context or the no-name target is used, --context will override the currently active context."` +} + +type KubeconfigFlags struct { + Kubeconfig ExistingFileType `group:"project" help:"Overrides the kubeconfig to use."` +} + +type CommandResultReadOnlyFlags struct { + CommandResultNamespace string `group:"results" help:"Override the namespace to be used when writing command results." default:"kluctl-results"` +} + +type CommandResultWriteFlags struct { + WriteCommandResult bool `group:"results" help:"Enable writing of command results into the cluster. This is enabled by default." default:"true"` + ForceWriteCommandResult bool `group:"results" help:"Force writing of command results, even if the command is run in dry-run mode."` + KeepCommandResultsCount int `group:"results" help:"Configure how many old command results to keep." default:"5"` + KeepValidateResultsCount int `group:"results" help:"Configure how many old validate results to keep." default:"2"` +} + +type CommandResultFlags struct { + CommandResultReadOnlyFlags + CommandResultWriteFlags } diff --git a/cmd/kluctl/args/registry_credentials.go b/cmd/kluctl/args/registry_credentials.go new file mode 100644 index 000000000..7262da506 --- /dev/null +++ b/cmd/kluctl/args/registry_credentials.go @@ -0,0 +1,160 @@ +package args + +import ( + "context" + "fmt" + "github.com/gobwas/glob" + "github.com/kluctl/kluctl/v2/pkg/oci/auth_provider" + "github.com/kluctl/kluctl/v2/pkg/utils" + "os" + "strings" +) + +type RegistryCredentials struct { + RegistryUsername []string `group:"registry" skipenv:"true" help:"Specify username to use for OCI authentication. Must be in the form --registry-username=/=."` + RegistryPassword []string `group:"registry" skipenv:"true" help:"Specify password to use for OCI authentication. Must be in the form --registry-password=/=."` + RegistryIdentityToken []string `group:"registry" skipenv:"true" help:"Specify identity token to use for OCI authentication. Must be in the form --registry-identity-token=/=."` + RegistryToken []string `group:"registry" skipenv:"true" help:"Specify registry token to use for OCI authentication. Must be in the form --registry-token=/=."` + RegistryCreds []string `group:"registry" skipenv:"true" help:"This is a shortcut to --registry-username, --registry-password and --registry-token. It can be specified in two different forms. The first one is --registry-creds=/=:, which specifies the username and password for the same registry. The second form is --registry-creds=/=, which specifies a JWT token for the specified registry."` + + RegistryKeyFile []string `group:"registry" skipenv:"true" help:"Specify key to use for OCI authentication. Must be in the form --registry-key-file=/=."` + RegistryCertFile []string `group:"registry" skipenv:"true" help:"Specify certificate to use for OCI authentication. Must be in the form --registry-cert-file=/=."` + RegistryCAFile []string `group:"registry" skipenv:"true" help:"Specify CA bundle to use for https verification. Must be in the form --registry-ca-file=/=."` + + RegistryPlainHttp []string `group:"registry" skipenv:"true" help:"Forces the use of http (no TLS). Must be in the form --registry-plain-http=/."` + RegistryInsecureSkipTlsVerify []string `group:"registry" skipenv:"true" help:"Controls skipping of TLS verification. Must be in the form --registry-insecure-skip-tls-verify=/."` +} + +func (c *RegistryCredentials) BuildAuthProvider(ctx context.Context) (auth_provider.OciAuthProvider, error) { + la := &auth_provider.ListAuthProvider{} + if c == nil { + return la, nil + } + + var byRegistryAndRepo utils.OrderedMap[string, *auth_provider.AuthEntry] + + getEntry := func(s string, expectValue bool) (*auth_provider.AuthEntry, string, error) { + x := strings.SplitN(s, "=", 2) + if expectValue && len(x) != 2 { + return nil, "", fmt.Errorf("expected value") + } + k := x[0] + e, ok := byRegistryAndRepo.Get(k) + if !ok { + x := strings.SplitN(k, "/", 2) + e = &auth_provider.AuthEntry{} + if len(x) == 2 { + e.Registry = x[0] + g, err := glob.Compile(x[1], '/') + if err != nil { + return nil, "", err + } + e.RepoStr = x[1] + e.RepoGlob = g + } else { + e.Registry = x[0] + } + byRegistryAndRepo.Set(k, e) + } + + if len(x) == 1 { + return e, "", nil + } + return e, x[1], nil + } + + for _, s := range c.RegistryUsername { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + e.AuthConfig.Username = v + } + for _, s := range c.RegistryPassword { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + e.AuthConfig.Password = v + } + for _, s := range c.RegistryIdentityToken { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + e.AuthConfig.IdentityToken = v + } + for _, s := range c.RegistryToken { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + e.AuthConfig.RegistryToken = v + } + for _, s := range c.RegistryKeyFile { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + b, err := os.ReadFile(v) + if err != nil { + return nil, err + } + e.Key = b + } + for _, s := range c.RegistryCertFile { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + b, err := os.ReadFile(v) + if err != nil { + return nil, err + } + e.Cert = b + } + for _, s := range c.RegistryCAFile { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + b, err := os.ReadFile(v) + if err != nil { + return nil, err + } + e.CA = b + } + for _, s := range c.RegistryPlainHttp { + e, _, err := getEntry(s, false) + if err != nil { + return nil, err + } + e.PlainHTTP = true + } + for _, s := range c.RegistryInsecureSkipTlsVerify { + e, _, err := getEntry(s, false) + if err != nil { + return nil, err + } + e.InsecureSkipTlsVerify = true + } + for _, s := range c.RegistryCreds { + e, v, err := getEntry(s, true) + if err != nil { + return nil, err + } + x := strings.SplitN(v, ":", 2) + if len(x) == 1 { + e.AuthConfig.RegistryToken = x[0] + } else { + e.AuthConfig.Username = x[0] + e.AuthConfig.Password = x[1] + } + } + + for _, e := range byRegistryAndRepo.ListValues() { + la.AddEntry(*e) + } + + return la, nil +} diff --git a/cmd/kluctl/commands/cmd_check_image_updates.go b/cmd/kluctl/commands/cmd_check_image_updates.go deleted file mode 100644 index 14be5ede7..000000000 --- a/cmd/kluctl/commands/cmd_check_image_updates.go +++ /dev/null @@ -1,134 +0,0 @@ -package commands - -import ( - "github.com/kluctl/kluctl/v2/cmd/kluctl/args" - "github.com/kluctl/kluctl/v2/pkg/registries" - "github.com/kluctl/kluctl/v2/pkg/status" - "github.com/kluctl/kluctl/v2/pkg/utils" - "github.com/kluctl/kluctl/v2/pkg/utils/versions" - "os" - "regexp" - "sort" - "strings" - "sync" -) - -type checkImageUpdatesCmd struct { - args.ProjectFlags - args.TargetFlags -} - -func (cmd *checkImageUpdatesCmd) Help() string { - return `This is based on a best effort approach and might give many false-positives.` -} - -func (cmd *checkImageUpdatesCmd) Run() error { - ptArgs := projectTargetCommandArgs{ - projectFlags: cmd.ProjectFlags, - targetFlags: cmd.TargetFlags, - } - return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error { - return runCheckImageUpdates(ctx) - }) -} - -func runCheckImageUpdates(ctx *commandCtx) error { - renderedImages := ctx.targetCtx.DeploymentCollection.FindRenderedImages() - - rh := registries.NewRegistryHelper(ctx.ctx) - - imageTags := make(map[string]interface{}) - var mutex sync.Mutex - var wg sync.WaitGroup - - for _, images := range renderedImages { - for _, image := range images { - s := strings.SplitN(image, ":", 2) - if len(s) == 1 { - continue - } - repo := s[0] - if _, ok := imageTags[repo]; !ok { - wg.Add(1) - go func() { - defer wg.Done() - tags, err := rh.ListImageTags(repo) - mutex.Lock() - defer mutex.Unlock() - if err != nil { - imageTags[repo] = err - } else { - imageTags[repo] = tags - } - }() - } - } - } - wg.Wait() - - prefixPattern := regexp.MustCompile("^([a-zA-Z]+[a-zA-Z-_.]*)") - suffixPattern := regexp.MustCompile("([-][a-zA-Z]+[a-zA-Z-_.]*)$") - - var table utils.PrettyTable - table.AddRow("Object", "Image", "Old", "New") - - for ref, images := range renderedImages { - for _, image := range images { - s := strings.SplitN(image, ":", 2) - if len(s) == 1 { - status.Warning(ctx.ctx, "%s: Ignoring image %s as it doesn't specify a tag", ref.String(), image) - continue - } - repo := s[0] - curTag := s[1] - repoTags, _ := imageTags[repo].([]string) - err, _ := imageTags[repo].(error) - if err != nil { - status.Warning(ctx.ctx, "%s: Failed to list tags for %s. %v", ref.String(), repo, err) - continue - } - - prefix := prefixPattern.FindString(curTag) - suffix := suffixPattern.FindString(curTag) - hasDot := strings.Index(curTag, ".") != -1 - - var filteredTags []string - for _, tag := range repoTags { - hasDot2 := strings.Index(tag, ".") != -1 - if hasDot != hasDot2 { - continue - } - if prefix != "" && !strings.HasPrefix(tag, prefix) { - continue - } - if suffix != "" && !strings.HasSuffix(tag, suffix) { - continue - } - filteredTags = append(filteredTags, tag) - } - doKey := func(tag string) versions.LooseVersion { - if prefix != "" { - tag = tag[len(prefix):] - } - if suffix != "" { - tag = tag[:len(tag)-len(suffix)] - } - return versions.LooseVersion(tag) - } - sort.SliceStable(filteredTags, func(i, j int) bool { - a := doKey(filteredTags[i]) - b := doKey(filteredTags[j]) - return a.Less(b, true) - }) - latestTag := filteredTags[len(filteredTags)-1] - - if latestTag != curTag { - table.AddRow(ref.String(), repo, curTag, latestTag) - } - } - } - - table.SortRows(1) - _, _ = os.Stdout.WriteString(table.Render([]int{60})) - return nil -} diff --git a/cmd/kluctl/commands/cmd_controller.go b/cmd/kluctl/commands/cmd_controller.go new file mode 100644 index 000000000..5a49c5187 --- /dev/null +++ b/cmd/kluctl/commands/cmd_controller.go @@ -0,0 +1,6 @@ +package commands + +type controllerCmd struct { + Install controllerInstallCmd `cmd:"" help:"Install the Kluctl controller"` + Run_ controllerRunCmd `cmd:"run" help:"Run the Kluctl controller"` +} diff --git a/cmd/kluctl/commands/cmd_controller_install.go b/cmd/kluctl/commands/cmd_controller_install.go new file mode 100644 index 000000000..947bf59c7 --- /dev/null +++ b/cmd/kluctl/commands/cmd_controller_install.go @@ -0,0 +1,58 @@ +package commands + +import ( + "context" + "fmt" + "github.com/kluctl/go-embed-python/embed_util" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/install/controller" + "time" +) + +type controllerInstallCmd struct { + args.KubeconfigFlags + args.YesFlags + args.DryRunFlags + args.CommandResultFlags + + Context string `group:"misc" help:"Override the context to use."` + KluctlVersion string `group:"misc" help:"Specify the controller version to install."` +} + +func (cmd *controllerInstallCmd) Help() string { + return `This command will install the kluctl-controller to the current Kubernetes clusters.` +} + +func (cmd *controllerInstallCmd) Run(ctx context.Context) error { + src, err := embed_util.NewEmbeddedFiles(controller.Project, "kluctl-controller-deployment") + if err != nil { + return err + } + + var deployArgs []string + if cmd.KluctlVersion != "" { + deployArgs = append(deployArgs, fmt.Sprintf("kluctl_version=%s", cmd.KluctlVersion)) + } + + cmd2 := deployCmd{ + ProjectFlags: args.ProjectFlags{ + ProjectDir: args.ProjectDir{ + ProjectDir: args.ExistingDirType(src.GetExtractedPath()), + }, + Timeout: 10 * time.Minute, + }, + KubeconfigFlags: cmd.KubeconfigFlags, + TargetFlags: args.TargetFlags{ + Context: cmd.Context, + }, + ArgsFlags: args.ArgsFlags{ + Arg: deployArgs, + }, + YesFlags: cmd.YesFlags, + DryRunFlags: cmd.DryRunFlags, + CommandResultFlags: cmd.CommandResultFlags, + Discriminator: "kluctl.io-controller", + internal: true, + } + return cmd2.Run(ctx) +} diff --git a/cmd/kluctl/commands/cmd_controller_run.go b/cmd/kluctl/commands/cmd_controller_run.go new file mode 100644 index 000000000..d5c5e34bd --- /dev/null +++ b/cmd/kluctl/commands/cmd_controller_run.go @@ -0,0 +1,275 @@ +package commands + +import ( + "context" + "fmt" + ssh_pool "github.com/kluctl/kluctl/lib/git/ssh-pool" + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/controllers" + "github.com/kluctl/kluctl/v2/pkg/sourceoverride" + "github.com/kluctl/kluctl/v2/pkg/utils/flux_utils/metrics" + log "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "os" + "os/user" + "path/filepath" + "sigs.k8s.io/cli-utils/pkg/flowcontrol" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + crtlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "testing" +) + +var ( + setupLog = ctrl.Log.WithName("setup") +) + +type controllerRunCmd struct { + scheme *runtime.Scheme + + Kubeconfig string `group:"misc" help:"Override the kubeconfig to use."` + Context string `group:"misc" help:"Override the context to use."` + + ControllerName string `group:"misc" help:"The controller name used for metrics and logs." default:"kluctl-controller"` + ControllerNamespace string `group:"misc" help:"The namespace where the controller runs in." default:"kluctl-system"` + Namespace string `group:"misc" help:"Specify the namespace to watch. If omitted, all namespaces are watched."` + + MetricsBindAddress string `group:"misc" help:"The address the metric endpoint binds to." default:":8080"` + HealthProbeBindAddress string `group:"misc" help:"The address the probe endpoint binds to." default:":8081"` + SourceOverrideBindAddress string `group:"misc" help:"The address the source override manager endpoint binds to." default:":8082"` + + LeaderElect bool `group:"misc" help:"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager."` + Concurrency int `group:"misc" help:"Configures how many KluctlDeployments can be be reconciled concurrently." default:"4"` + + DefaultServiceAccount string `group:"misc" help:"Default service account used for impersonation."` + DryRun bool `group:"misc" help:"Run all deployments in dryRun=true mode."` + + args.CommandResultFlags +} + +func (cmd *controllerRunCmd) Help() string { + return `This command will run the Kluctl Controller. This is usually meant to be run inside a cluster and not from your local machine. +` +} + +func (cmd *controllerRunCmd) initScheme() { + cmd.scheme = runtime.NewScheme() + scheme := cmd.scheme + + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(kluctlv1.AddToScheme(scheme)) + + //+kubebuilder:scaffold:scheme +} + +func (cmd *controllerRunCmd) Run(ctx context.Context) error { + cmd.initScheme() + + globalFlags := getCobraGlobalFlags(ctx) + + metricsRecorder := metrics.NewRecorder() + if cmd.MetricsBindAddress != "0" { + metricsRecorder = metrics.NewRecorder() + crtlmetrics.Registry.MustRegister(metricsRecorder.Collectors()...) + } + + opts := zap.Options{} + if testing.Testing() { + opts.Development = true + } + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + restConfig, err := cmd.loadConfig(cmd.Kubeconfig, cmd.Context) + if err != nil { + setupLog.Error(err, "unable to load kubeconfig") + os.Exit(1) + } + + enabled, err := flowcontrol.IsEnabled(context.Background(), restConfig) + if err == nil && enabled { + // A negative QPS and Burst indicates that the client should not have a rate limiter. + // Ref: https://github.com/kubernetes/kubernetes/blob/v1.24.0/staging/src/k8s.io/client-go/rest/config.go#L354-L364 + restConfig.QPS = -1 + restConfig.Burst = -1 + } + + var cacheNamespaces map[string]cache.Config + if cmd.Namespace != "" { + cacheNamespaces = map[string]cache.Config{ + cmd.ControllerNamespace: {}, + cmd.Namespace: {}, + } + } + + mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ + BaseContext: func() context.Context { + return ctx + }, + Scheme: cmd.scheme, + Metrics: metricsserver.Options{ + BindAddress: cmd.MetricsBindAddress, + }, + HealthProbeBindAddress: cmd.HealthProbeBindAddress, + LeaderElection: cmd.LeaderElect, + LeaderElectionID: "5ab5d0f9.kluctl.io", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + Cache: cache.Options{ + DefaultNamespaces: cacheNamespaces, + }, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + err = mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { + if cmd.SourceOverrideBindAddress == "0" { + return nil + } + setupLog.Info("Initializing source-override server TLS") + return sourceoverride.InitControllerSecret(ctx, mgr.GetClient(), cmd.ControllerNamespace) + })) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + eventRecorder := mgr.GetEventRecorderFor(cmd.ControllerName) + + sshPool := &ssh_pool.SshPool{} + + var soProxyServer *sourceoverride.ProxyServerImpl + if cmd.SourceOverrideBindAddress != "0" { + soProxyServer = sourceoverride.NewProxyServerImpl(ctx, mgr.GetAPIReader(), cmd.ControllerNamespace) + _, err := soProxyServer.Listen(cmd.SourceOverrideBindAddress) + if err != nil { + return err + } + go func() { + _ = soProxyServer.Serve() + }() + defer soProxyServer.Stop() + } + + r := controllers.KluctlDeploymentReconciler{ + ControllerName: cmd.ControllerName, + ControllerNamespace: cmd.ControllerNamespace, + DefaultServiceAccount: cmd.DefaultServiceAccount, + DryRun: cmd.DryRun, + UseSystemPython: globalFlags.UseSystemPython, + RestConfig: restConfig, + ApiReader: mgr.GetAPIReader(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: eventRecorder, + MetricsRecorder: metricsRecorder, + SshPool: sshPool, + } + + r.ResultStore, err = buildResultStoreRW(ctx, restConfig, mgr.GetRESTMapper(), &cmd.CommandResultFlags, true) + if err != nil { + return err + } + + if err = r.SetupWithManager(ctx, mgr, controllers.KluctlDeploymentReconcilerOpts{ + Concurrency: cmd.Concurrency, + }); err != nil { + setupLog.Error(err, "unable to create controller", "controller", kluctlv1.KluctlDeploymentKind) + os.Exit(1) + } + + //+kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctx); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } + + return nil +} + +// taken from clientcmd +func (cmd *controllerRunCmd) loadConfig(kubeconfig string, context string) (config *rest.Config, configErr error) { + // If a flag is specified with the config location, use that + if len(kubeconfig) > 0 { + return cmd.loadConfigWithContext("", &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, context) + } + + // If the recommended kubeconfig env variable is not specified, + // try the in-cluster config. + kubeconfigPath := os.Getenv(clientcmd.RecommendedConfigPathEnvVar) + if len(kubeconfigPath) == 0 { + c, err := rest.InClusterConfig() + if err == nil { + return c, nil + } + + defer func() { + if configErr != nil { + log.Error(err, "unable to load in-cluster config") + } + }() + } + + // If the recommended kubeconfig env variable is set, or there + // is no in-cluster config, try the default recommended locations. + // + // NOTE: For default config file locations, upstream only checks + // $HOME for the user's home directory, but we can also try + // os/user.HomeDir when $HOME is unset. + // + // TODO(jlanford): could this be done upstream? + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if _, ok := os.LookupEnv("HOME"); !ok { + u, err := user.Current() + if err != nil { + return nil, fmt.Errorf("could not get current user: %w", err) + } + loadingRules.Precedence = append(loadingRules.Precedence, filepath.Join(u.HomeDir, clientcmd.RecommendedHomeDir, clientcmd.RecommendedFileName)) + } + + return cmd.loadConfigWithContext("", loadingRules, context) +} + +// taken from clientcmd +func (cmd *controllerRunCmd) loadConfigWithContext(apiServerURL string, loader clientcmd.ClientConfigLoader, context string) (*rest.Config, error) { + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + loader, + &clientcmd.ConfigOverrides{ + ClusterInfo: clientcmdapi.Cluster{ + Server: apiServerURL, + }, + CurrentContext: context, + }).ClientConfig() +} diff --git a/cmd/kluctl/commands/cmd_delete.go b/cmd/kluctl/commands/cmd_delete.go index 368ccd554..463c84a71 100644 --- a/cmd/kluctl/commands/cmd_delete.go +++ b/cmd/kluctl/commands/cmd_delete.go @@ -4,67 +4,63 @@ import ( "context" "fmt" "github.com/kluctl/kluctl/v2/cmd/kluctl/args" - "github.com/kluctl/kluctl/v2/pkg/deployment" "github.com/kluctl/kluctl/v2/pkg/deployment/commands" - "github.com/kluctl/kluctl/v2/pkg/deployment/utils" - "github.com/kluctl/kluctl/v2/pkg/k8s" - "github.com/kluctl/kluctl/v2/pkg/status" - "github.com/kluctl/kluctl/v2/pkg/types" + "github.com/kluctl/kluctl/v2/pkg/prompts" k8s2 "github.com/kluctl/kluctl/v2/pkg/types/k8s" - "os" ) type deleteCmd struct { args.ProjectFlags + args.KubeconfigFlags args.TargetFlags args.ArgsFlags args.ImageFlags args.InclusionFlags + args.GitCredentials + args.HelmCredentials + args.RegistryCredentials args.YesFlags args.DryRunFlags args.OutputFormatFlags args.RenderOutputDirFlags + args.CommandResultFlags - DeleteByLabel []string `group:"misc" short:"l" help:"Override the labels used to find objects for deletion."` + Discriminator string `group:"misc" help:"Override the discriminator used to find objects for deletion."` + + NoWait bool `group:"misc" help:"Don't wait for deletion of objects to finish.'"` } func (cmd *deleteCmd) Help() string { - return `Objects are located based on 'commonLabels', configured in 'deployment.yaml' + return `Objects are located based on the target discriminator. WARNING: This command will also delete objects which are not part of your deployment -project (anymore). It really only decides based on the 'deleteByLabel' labels and does NOT +project (anymore). It really only decides based on the discriminator and does NOT take the local target/state into account!` } -func (cmd *deleteCmd) Run() error { +func (cmd *deleteCmd) Run(ctx context.Context) error { ptArgs := projectTargetCommandArgs{ projectFlags: cmd.ProjectFlags, + kubeconfigFlags: cmd.KubeconfigFlags, targetFlags: cmd.TargetFlags, argsFlags: cmd.ArgsFlags, imageFlags: cmd.ImageFlags, inclusionFlags: cmd.InclusionFlags, + gitCredentials: cmd.GitCredentials, + helmCredentials: cmd.HelmCredentials, + registryCredentials: cmd.RegistryCredentials, dryRunArgs: &cmd.DryRunFlags, renderOutputDirFlags: cmd.RenderOutputDirFlags, + commandResultFlags: &cmd.CommandResultFlags, } - return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error { - cmd2 := commands.NewDeleteCommand(ctx.targetCtx.DeploymentCollection) - - deleteByLabels, err := deployment.ParseArgs(cmd.DeleteByLabel) - if err != nil { - return err - } + return withProjectCommandContext(ctx, ptArgs, func(cmdCtx *commandCtx) error { + cmd2 := commands.NewDeleteCommand(cmd.Discriminator, cmdCtx.targetCtx, nil, !cmd.NoWait) - cmd2.OverrideDeleteByLabels = deleteByLabels + result := cmd2.Run(cmdCtx.targetCtx.SharedContext.Ctx, cmdCtx.targetCtx.SharedContext.K, func(refs []k8s2.ObjectRef) error { + return confirmDeletion(ctx, refs, cmd.DryRun, cmd.Yes) + }) - objects, err := cmd2.Run(ctx.ctx, ctx.targetCtx.SharedContext.K) - if err != nil { - return err - } - result, err := confirmedDeleteObjects(ctx.ctx, ctx.targetCtx.SharedContext.K, objects, cmd.DryRun, cmd.Yes) - if err != nil { - return err - } - err = outputCommandResult(cmd.OutputFormat, result) + err := outputCommandResult(ctx, cmdCtx, cmd.OutputFormatFlags, result, !cmd.DryRun || cmd.ForceWriteCommandResult) if err != nil { return err } @@ -75,18 +71,17 @@ func (cmd *deleteCmd) Run() error { }) } -func confirmedDeleteObjects(ctx context.Context, k *k8s.K8sCluster, refs []k8s2.ObjectRef, dryRun bool, forceYes bool) (*types.CommandResult, error) { +func confirmDeletion(ctx context.Context, refs []k8s2.ObjectRef, dryRun bool, forceYes bool) error { if len(refs) != 0 { - _, _ = os.Stderr.WriteString("The following objects will be deleted:\n") + _, _ = getStderr(ctx).WriteString("The following objects will be deleted:\n") for _, ref := range refs { - _, _ = os.Stderr.WriteString(fmt.Sprintf(" %s\n", ref.String())) + _, _ = getStderr(ctx).WriteString(fmt.Sprintf(" %s\n", ref.String())) } if !forceYes && !dryRun { - if !status.AskForConfirmation(ctx, fmt.Sprintf("Do you really want to delete %d objects?", len(refs))) { - return nil, fmt.Errorf("aborted") + if !prompts.AskForConfirmation(ctx, fmt.Sprintf("Do you really want to delete %d objects?", len(refs))) { + return fmt.Errorf("aborted") } } } - - return utils.DeleteObjects(k, refs, true) + return nil } diff --git a/cmd/kluctl/commands/cmd_deploy.go b/cmd/kluctl/commands/cmd_deploy.go index 0e57f6e75..f0cbc20ef 100644 --- a/cmd/kluctl/commands/cmd_deploy.go +++ b/cmd/kluctl/commands/cmd_deploy.go @@ -1,19 +1,25 @@ package commands import ( + "context" "fmt" + "github.com/kluctl/kluctl/lib/status" "github.com/kluctl/kluctl/v2/cmd/kluctl/args" "github.com/kluctl/kluctl/v2/pkg/deployment/commands" - "github.com/kluctl/kluctl/v2/pkg/status" - "github.com/kluctl/kluctl/v2/pkg/types" + "github.com/kluctl/kluctl/v2/pkg/prompts" + "github.com/kluctl/kluctl/v2/pkg/types/result" ) type deployCmd struct { args.ProjectFlags + args.KubeconfigFlags args.TargetFlags args.ArgsFlags args.ImageFlags args.InclusionFlags + args.GitCredentials + args.HelmCredentials + args.RegistryCredentials args.YesFlags args.DryRunFlags args.ForceApplyFlags @@ -22,8 +28,18 @@ type deployCmd struct { args.HookFlags args.OutputFormatFlags args.RenderOutputDirFlags + args.CommandResultFlags - NoWait bool `group:"misc" help:"Don't wait for objects readiness'"` + DeployExtraFlags + + Discriminator string `group:"misc" help:"Override the target discriminator."` + + internal bool +} + +type DeployExtraFlags struct { + NoWait bool `group:"misc" help:"Don't wait for objects readiness."` + Prune bool `group:"misc" help:"Prune orphaned objects directly after deploying. See the help for the 'prune' sub-command for details."` } func (cmd *deployCmd) Help() string { @@ -33,43 +49,51 @@ It will also output a list of prunable objects (without actually deleting them). ` } -func (cmd *deployCmd) Run() error { +func (cmd *deployCmd) Run(ctx context.Context) error { ptArgs := projectTargetCommandArgs{ projectFlags: cmd.ProjectFlags, + kubeconfigFlags: cmd.KubeconfigFlags, targetFlags: cmd.TargetFlags, argsFlags: cmd.ArgsFlags, imageFlags: cmd.ImageFlags, inclusionFlags: cmd.InclusionFlags, + gitCredentials: cmd.GitCredentials, + helmCredentials: cmd.HelmCredentials, + registryCredentials: cmd.RegistryCredentials, dryRunArgs: &cmd.DryRunFlags, renderOutputDirFlags: cmd.RenderOutputDirFlags, + commandResultFlags: &cmd.CommandResultFlags, + internalDeploy: cmd.internal, + discriminator: cmd.Discriminator, } - return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error { - return cmd.runCmdDeploy(ctx) + return withProjectCommandContext(ctx, ptArgs, func(cmdCtx *commandCtx) error { + return cmd.runCmdDeploy(ctx, cmdCtx) }) } -func (cmd *deployCmd) runCmdDeploy(ctx *commandCtx) error { - status.Trace(ctx.ctx, "enter runCmdDeploy") - defer status.Trace(ctx.ctx, "leave runCmdDeploy") +func (cmd *deployCmd) runCmdDeploy(ctx context.Context, cmdCtx *commandCtx) error { + status.Trace(ctx, "enter runCmdDeploy") + defer status.Trace(ctx, "leave runCmdDeploy") - cmd2 := commands.NewDeployCommand(ctx.targetCtx.DeploymentCollection) + cmd2 := commands.NewDeployCommand(cmdCtx.targetCtx) cmd2.ForceApply = cmd.ForceApply cmd2.ReplaceOnError = cmd.ReplaceOnError cmd2.ForceReplaceOnError = cmd.ForceReplaceOnError cmd2.AbortOnError = cmd.AbortOnError cmd2.ReadinessTimeout = cmd.ReadinessTimeout cmd2.NoWait = cmd.NoWait + cmd2.Prune = cmd.Prune + cmd2.WaitPrune = !cmd.NoWait - cb := cmd.diffResultCb + cb := func(diffResult *result.CommandResult) error { + return cmd.diffResultCb(ctx, cmdCtx, diffResult) + } if cmd.Yes || cmd.DryRun { cb = nil } - result, err := cmd2.Run(ctx.ctx, ctx.targetCtx.SharedContext.K, cb) - if err != nil { - return err - } - err = outputCommandResult(cmd.OutputFormat, result) + result := cmd2.Run(cb) + err := outputCommandResult(ctx, cmdCtx, cmd.OutputFormatFlags, result, !cmd.DryRun || cmd.ForceWriteCommandResult) if err != nil { return err } @@ -79,8 +103,11 @@ func (cmd *deployCmd) runCmdDeploy(ctx *commandCtx) error { return nil } -func (cmd *deployCmd) diffResultCb(diffResult *types.CommandResult) error { - err := outputCommandResult(nil, diffResult) +func (cmd *deployCmd) diffResultCb(ctx context.Context, cmdCtx *commandCtx, diffResult *result.CommandResult) error { + flags := cmd.OutputFormatFlags + flags.OutputFormat = nil // use default output format + + err := outputCommandResult(ctx, cmdCtx, flags, diffResult, false) if err != nil { return err } @@ -88,11 +115,11 @@ func (cmd *deployCmd) diffResultCb(diffResult *types.CommandResult) error { return nil } if len(diffResult.Errors) != 0 { - if !status.AskForConfirmation(cliCtx, "The diff resulted in errors, do you still want to proceed?") { + if !prompts.AskForConfirmation(ctx, "The diff resulted in errors, do you still want to proceed?") { return fmt.Errorf("aborted") } } else { - if !status.AskForConfirmation(cliCtx, "The diff succeeded, do you want to proceed?") { + if !prompts.AskForConfirmation(ctx, "The diff succeeded, do you want to proceed?") { return fmt.Errorf("aborted") } } diff --git a/cmd/kluctl/commands/cmd_diff.go b/cmd/kluctl/commands/cmd_diff.go index bc6fcc211..c2c2d0096 100644 --- a/cmd/kluctl/commands/cmd_diff.go +++ b/cmd/kluctl/commands/cmd_diff.go @@ -1,6 +1,7 @@ package commands import ( + "context" "fmt" "github.com/kluctl/kluctl/v2/cmd/kluctl/args" "github.com/kluctl/kluctl/v2/pkg/deployment/commands" @@ -8,15 +9,21 @@ import ( type diffCmd struct { args.ProjectFlags + args.KubeconfigFlags args.TargetFlags args.ArgsFlags args.InclusionFlags args.ImageFlags + args.GitCredentials + args.HelmCredentials + args.RegistryCredentials args.ForceApplyFlags args.ReplaceOnErrorFlags args.IgnoreFlags args.OutputFormatFlags args.RenderOutputDirFlags + + Discriminator string `group:"misc" help:"Override the target discriminator."` } func (cmd *diffCmd) Help() string { @@ -26,28 +33,31 @@ is currently not documented and prone to changes. After the diff is performed, the command will also search for prunable objects and list them.` } -func (cmd *diffCmd) Run() error { +func (cmd *diffCmd) Run(ctx context.Context) error { ptArgs := projectTargetCommandArgs{ projectFlags: cmd.ProjectFlags, + kubeconfigFlags: cmd.KubeconfigFlags, targetFlags: cmd.TargetFlags, argsFlags: cmd.ArgsFlags, imageFlags: cmd.ImageFlags, inclusionFlags: cmd.InclusionFlags, + gitCredentials: cmd.GitCredentials, + helmCredentials: cmd.HelmCredentials, + registryCredentials: cmd.RegistryCredentials, renderOutputDirFlags: cmd.RenderOutputDirFlags, + discriminator: cmd.Discriminator, } - return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error { - cmd2 := commands.NewDiffCommand(ctx.targetCtx.DeploymentCollection) + return withProjectCommandContext(ctx, ptArgs, func(cmdCtx *commandCtx) error { + cmd2 := commands.NewDiffCommand(cmdCtx.targetCtx) cmd2.ForceApply = cmd.ForceApply cmd2.ReplaceOnError = cmd.ReplaceOnError cmd2.ForceReplaceOnError = cmd.ForceReplaceOnError cmd2.IgnoreTags = cmd.IgnoreTags cmd2.IgnoreLabels = cmd.IgnoreLabels cmd2.IgnoreAnnotations = cmd.IgnoreAnnotations - result, err := cmd2.Run(ctx.ctx, ctx.targetCtx.SharedContext.K) - if err != nil { - return err - } - err = outputCommandResult(cmd.OutputFormat, result) + cmd2.IgnoreKluctlMetadata = cmd.IgnoreKluctlMetadata + result := cmd2.Run() + err := outputCommandResult(ctx, cmdCtx, cmd.OutputFormatFlags, result, false) if err != nil { return err } diff --git a/cmd/kluctl/commands/cmd_downscale.go b/cmd/kluctl/commands/cmd_downscale.go deleted file mode 100644 index ac2d0a610..000000000 --- a/cmd/kluctl/commands/cmd_downscale.go +++ /dev/null @@ -1,60 +0,0 @@ -package commands - -import ( - "fmt" - "github.com/kluctl/kluctl/v2/cmd/kluctl/args" - "github.com/kluctl/kluctl/v2/pkg/deployment/commands" - "github.com/kluctl/kluctl/v2/pkg/status" -) - -type downscaleCmd struct { - args.ProjectFlags - args.TargetFlags - args.ArgsFlags - args.ImageFlags - args.InclusionFlags - args.YesFlags - args.DryRunFlags - args.OutputFormatFlags - args.RenderOutputDirFlags -} - -func (cmd *downscaleCmd) Help() string { - return `This command will downscale all Deployments, StatefulSets and CronJobs. -It is also possible to influence the behaviour with the help of annotations, as described in -the documentation.` -} - -func (cmd *downscaleCmd) Run() error { - ptArgs := projectTargetCommandArgs{ - projectFlags: cmd.ProjectFlags, - targetFlags: cmd.TargetFlags, - argsFlags: cmd.ArgsFlags, - imageFlags: cmd.ImageFlags, - inclusionFlags: cmd.InclusionFlags, - dryRunArgs: &cmd.DryRunFlags, - renderOutputDirFlags: cmd.RenderOutputDirFlags, - } - return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error { - if !cmd.Yes && !cmd.DryRun { - if !status.AskForConfirmation(cliCtx, fmt.Sprintf("Do you really want to downscale on context/cluster %s?", ctx.targetCtx.ClusterContext)) { - return fmt.Errorf("aborted") - } - } - - cmd2 := commands.NewDownscaleCommand(ctx.targetCtx.DeploymentCollection) - - result, err := cmd2.Run(ctx.ctx, ctx.targetCtx.SharedContext.K) - if err != nil { - return err - } - err = outputCommandResult(cmd.OutputFormat, result) - if err != nil { - return err - } - if len(result.Errors) != 0 { - return fmt.Errorf("command failed") - } - return nil - }) -} diff --git a/cmd/kluctl/commands/cmd_gitops.go b/cmd/kluctl/commands/cmd_gitops.go new file mode 100644 index 000000000..a40015a37 --- /dev/null +++ b/cmd/kluctl/commands/cmd_gitops.go @@ -0,0 +1,896 @@ +package commands + +import ( + "bytes" + "context" + "fmt" + json_patch "github.com/evanphx/json-patch/v5" + "github.com/kluctl/kluctl/lib/git" + gittypes "github.com/kluctl/kluctl/lib/git/types" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/lib/yaml" + "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/controllers/logs" + "github.com/kluctl/kluctl/v2/pkg/k8s" + "github.com/kluctl/kluctl/v2/pkg/prompts" + "github.com/kluctl/kluctl/v2/pkg/results" + "github.com/kluctl/kluctl/v2/pkg/sourceoverride" + "github.com/kluctl/kluctl/v2/pkg/utils" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + flag "github.com/spf13/pflag" + v12 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "math/rand" + "net/url" + "sigs.k8s.io/controller-runtime/pkg/client" + "strconv" + "sync" + "time" +) + +type gitopsCmd struct { + Suspend GitopsSuspendCmd `cmd:"" help:"Suspend a GitOps deployment"` + Resume gitopsResumeCmd `cmd:"" help:"Resume a GitOps deployment"` + Reconcile gitopsReconcileCmd `cmd:"" help:"Trigger a GitOps reconciliation"` + Diff gitopsDiffCmd `cmd:"" help:"Trigger a GitOps diff"` + Deploy gitopsDeployCmd `cmd:"" help:"Trigger a GitOps deployment"` + Prune gitopsPruneCmd `cmd:"" help:"Trigger a GitOps prune"` + Validate gitopsValidateCmd `cmd:"" help:"Trigger a GitOps validate"` + Logs gitopsLogsCmd `cmd:"" help:"Show logs from controller"` +} + +type gitopsCmdHelper struct { + args args.GitOpsArgs + logsArgs args.GitOpsLogArgs + overridableArgs args.GitOpsOverridableArgs + + noArgsReact noArgsReact + + projectGitRoot string + projectGitInfo *gittypes.GitInfo + projectKey *gittypes.ProjectKey + + kds []v1beta1.KluctlDeployment + + restConfig *rest.Config + restMapper meta.RESTMapper + client client.Client + corev1Client *v1.CoreV1Client + + soResolver *sourceoverride.Manager + soClient *sourceoverride.ProxyClientCli + + resultStore results.ResultStore + + logsMutex sync.Mutex + logsBufs map[logsKey]*logsBuf + lastFlushedLogsKey *logsKey +} + +type logsKey struct { + objectKey client.ObjectKey + reconcileId string +} + +type logsBuf struct { + lastFlushTime time.Time + lines []logs.LogLine +} + +type noArgsReact int + +const ( + noArgsForbid noArgsReact = iota + noArgsNoDeployments + noArgsAllDeployments + noArgsAutoDetectProject + noArgsAutoDetectProjectAsk +) + +func (g *gitopsCmdHelper) init(ctx context.Context) error { + err := g.collectLocalProjectInfo(ctx) + if err != nil { + status.Warningf(ctx, "Failed to collect local project info: %s", err.Error()) + } + + g.logsBufs = map[logsKey]*logsBuf{} + + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + loadingRules.ExplicitPath = g.args.Kubeconfig.String() + + configOverrides := &clientcmd.ConfigOverrides{ + CurrentContext: g.args.Context, + } + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) + restConfig, err := clientConfig.ClientConfig() + if err != nil { + return err + } + defaultNs, _, err := clientConfig.Namespace() + if err != nil { + return err + } + + _, mapper, err := k8s.CreateDiscoveryAndMapper(ctx, restConfig) + if err != nil { + return err + } + + g.restConfig = restConfig + g.restMapper = mapper + + g.client, err = client.NewWithWatch(restConfig, client.Options{ + Mapper: mapper, + }) + if err != nil { + return err + } + g.corev1Client, err = v1.NewForConfig(restConfig) + if err != nil { + return err + } + + err = g.checkCRDSupport(ctx) + if err != nil { + return err + } + + ns := g.args.Namespace + if ns == "" { + ns = defaultNs + } + + if g.args.Name != "" { + if ns == "" { + return fmt.Errorf("no namespace specified") + } + if g.args.LabelSelector != "" { + return fmt.Errorf("either name or label selector must be set, not both") + } + + var kd v1beta1.KluctlDeployment + err = g.client.Get(ctx, client.ObjectKey{Name: g.args.Name, Namespace: ns}, &kd) + if err != nil { + return err + } + g.kds = append(g.kds, kd) + } else if g.args.LabelSelector != "" { + label, err := metav1.ParseToLabelSelector(g.args.LabelSelector) + if err != nil { + return err + } + sel, err := metav1.LabelSelectorAsSelector(label) + if err != nil { + return err + } + + var opts []client.ListOption + opts = append(opts, client.MatchingLabelsSelector{Selector: sel}) + if ns != "" { + opts = append(opts, client.InNamespace(ns)) + } + + var l v1beta1.KluctlDeploymentList + err = g.client.List(ctx, &l, opts...) + if err != nil { + return err + } + g.kds = append(g.kds, l.Items...) + } else if g.noArgsReact == noArgsNoDeployments { + // do nothing + } else if g.noArgsReact == noArgsAllDeployments { + var l v1beta1.KluctlDeploymentList + err = g.client.List(ctx, &l, client.InNamespace(g.args.Namespace)) + if err != nil { + return err + } + g.kds = append(g.kds, l.Items...) + } else if g.noArgsReact == noArgsAutoDetectProject || g.noArgsReact == noArgsAutoDetectProjectAsk { + err = g.autoDetectDeployment(ctx) + if err != nil { + return err + } + } else { + return fmt.Errorf("either name or label selector must be set") + } + + g.resultStore, err = buildResultStoreRO(ctx, restConfig, mapper, &g.args.CommandResultReadOnlyFlags) + if err != nil { + return err + } + + err = g.initSourceOverrides(ctx) + if err != nil { + status.Warningf(ctx, "Failed to initialize source overrides: %s", err.Error()) + } + + return nil +} + +func (g *gitopsCmdHelper) collectLocalProjectInfo(ctx context.Context) error { + projectDir, err := g.args.ProjectDir.GetProjectDir() + if err != nil { + return err + } + gitRoot, err := git.DetectGitRepositoryRoot(projectDir) + if err != nil { + return err + } + gitInfo, projectKey, err := git.BuildGitInfo(ctx, gitRoot, projectDir) + if err != nil { + return err + } + + if projectKey.RepoKey == (gittypes.RepoKey{}) { + return fmt.Errorf("failed to determine repo key") + } + + g.projectGitRoot = gitRoot + g.projectGitInfo = &gitInfo + g.projectKey = &projectKey + + return nil +} + +func (g *gitopsCmdHelper) autoDetectDeployment(ctx context.Context) error { + if g.projectKey == nil { + return fmt.Errorf("auto-detection of KluctlDeployments only possible if local project is a Git repository") + } + + msg := fmt.Sprintf("Auto-detecting KluctlDeployments via repo key %s", g.projectKey.RepoKey.String()) + if g.projectKey.SubDir != "" { + msg += fmt.Sprintf(" and sub directory %s", g.projectKey.SubDir) + } + + st := status.Start(ctx, msg) + defer st.Failed() + + var l v1beta1.KluctlDeploymentList + err := g.client.List(ctx, &l, client.InNamespace(g.args.Namespace)) + if err != nil { + return err + } + + var matching []v1beta1.KluctlDeployment + for _, kd := range l.Items { + isGit := false + var u, subDir string + if kd.Spec.Source.Git != nil { + isGit = true + u = kd.Spec.Source.Git.URL + subDir = kd.Spec.Source.Git.Path + } else if kd.Spec.Source.Oci != nil { + u = kd.Spec.Source.Oci.URL + subDir = kd.Spec.Source.Oci.Path + } else if kd.Spec.Source.URL != nil { + isGit = true + u = *kd.Spec.Source.URL + subDir = kd.Spec.Source.Path + } + var repoKey gittypes.RepoKey + if isGit { + repoKey, err = gittypes.NewRepoKeyFromGitUrl(u) + } else { + repoKey, err = gittypes.NewRepoKeyFromUrl(u) + } + if err != nil { + status.Warningf(ctx, "Failed to determine repo key for KluctlDeployment %s/%s with source url %s: %s", kd.Namespace, kd.Name, u, err.Error()) + continue + } + + if repoKey != g.projectKey.RepoKey || subDir != g.projectKey.SubDir { + continue + } + + matching = append(matching, kd) + } + + if len(matching) == 0 { + return fmt.Errorf("no matching KluctlDeployments found") + } + + if len(matching) == 1 { + if g.noArgsReact == noArgsAutoDetectProjectAsk { + kd := matching[0] + if !prompts.AskForConfirmation(ctx, fmt.Sprintf("Auto-detected %s/%s, do you want to run the command for this deployment?", kd.Namespace, kd.Name)) { + return fmt.Errorf("aborted") + } + } + g.kds = matching + st.Success() + return nil + } + + if g.noArgsReact == noArgsAutoDetectProjectAsk { + if len(matching) == 1 { + if !prompts.AskForConfirmation(ctx, "Auto-detected %s/%s, do you want to run the command for this deployment?") { + return fmt.Errorf("aborted") + } + } + + var choices utils.OrderedMap[string, string] + for i, kd := range matching { + choices.Set(fmt.Sprintf("%d", i+1), fmt.Sprintf("%s/%s", kd.Namespace, kd.Name)) + } + choices.Set("a", "All") + + response, err := prompts.AskForChoice(ctx, "Auto-detected multiple KluctlDeployments. Which one do you want to run the command for?", &choices) + if err != nil { + return err + } + + if response == "a" { + g.kds = matching + } else { + idx, _ := strconv.ParseInt(response, 10, 32) + g.kds = append(g.kds, matching[idx-1]) + } + } else { + g.kds = matching + } + + st.Success() + + return nil +} + +func (g *gitopsCmdHelper) patchDeployment(ctx context.Context, key client.ObjectKey, cb func(kd *v1beta1.KluctlDeployment) error) (*v1beta1.KluctlDeployment, error) { + s := status.Startf(ctx, "Patching KluctlDeployment %s/%s", key.Namespace, key.Name) + defer s.Failed() + + var kd v1beta1.KluctlDeployment + err := g.client.Get(ctx, key, &kd) + if err != nil { + return nil, err + } + + patch := client.MergeFrom(kd.DeepCopy()) + err = cb(&kd) + if err != nil { + return nil, err + } + + err = g.client.Patch(ctx, &kd, patch, client.FieldOwner("kluctl")) + if err != nil { + return nil, err + } + + s.Success() + + return &kd, nil +} + +func (g *gitopsCmdHelper) updateDeploymentStatus(ctx context.Context, key client.ObjectKey, cb func(kd *v1beta1.KluctlDeployment) error, retries int) error { + s := status.Startf(ctx, "Updating KluctlDeployment %s/%s", key.Namespace, key.Name) + defer s.Failed() + + var lastErr error + for i := 0; i < retries; i++ { + var kd v1beta1.KluctlDeployment + err := g.client.Get(ctx, key, &kd) + if err != nil { + return err + } + + err = cb(&kd) + if err != nil { + return err + } + + err = g.client.Status().Update(ctx, &kd, client.FieldOwner("kluctl")) + lastErr = err + if err != nil { + if !errors.IsConflict(err) { + return err + } + } + s.Success() + return nil + } + + return lastErr +} + +func (g *gitopsCmdHelper) patchDeploymentStatus(ctx context.Context, key client.ObjectKey, cb func(kd *v1beta1.KluctlDeployment) error) error { + var kd v1beta1.KluctlDeployment + err := g.client.Get(ctx, key, &kd) + if err != nil { + return err + } + + patch := client.MergeFrom(kd.DeepCopy()) + err = cb(&kd) + if err != nil { + return err + } + + err = g.client.Status().Patch(ctx, &kd, patch, client.FieldOwner("kluctl")) + if err != nil { + return err + } + + return nil +} + +func (g *gitopsCmdHelper) patchManualRequest(ctx context.Context, key client.ObjectKey, requestAnnotation string, value string) error { + _, err := g.patchDeployment(ctx, key, func(kd *v1beta1.KluctlDeployment) error { + overridePatch, err := g.buildOverridePatch(ctx, kd) + if err != nil { + return err + } + + mr := v1beta1.ManualRequest{ + RequestValue: value, + } + if len(overridePatch) != 0 && !bytes.Equal(overridePatch, []byte("{}")) { + mr.OverridesPatch = &runtime.RawExtension{Raw: overridePatch} + } + + mrJson, err := yaml.WriteJsonString(&mr) + if err != nil { + return err + } + + a := kd.GetAnnotations() + if a == nil { + a = map[string]string{} + } + a[requestAnnotation] = mrJson + + kd.SetAnnotations(a) + return nil + }) + if err != nil { + return err + } + + return nil +} + +func (g *gitopsCmdHelper) buildOverridePatch(ctx context.Context, kdIn *v1beta1.KluctlDeployment) ([]byte, error) { + cobraCmd := getCobraCommand(ctx) + if cobraCmd == nil { + panic("no cobra cmd") + } + + handleFlag := func(name string, cb func(f *flag.Flag)) { + f := cobraCmd.Flag(name) + if f == nil { + return + } + if f.Changed { + cb(f) + } + } + + kd := kdIn.DeepCopy() + + handleFlag("dry-run", func(f *flag.Flag) { + kd.Spec.DryRun = g.overridableArgs.DryRun + }) + handleFlag("force-apply", func(f *flag.Flag) { + kd.Spec.ForceApply = g.overridableArgs.ForceApply + }) + handleFlag("replace-on-error", func(f *flag.Flag) { + kd.Spec.ReplaceOnError = g.overridableArgs.ReplaceOnError + }) + handleFlag("force-replace-on-error", func(f *flag.Flag) { + kd.Spec.ForceReplaceOnError = g.overridableArgs.ForceReplaceOnError + }) + handleFlag("abort-on-error", func(f *flag.Flag) { + kd.Spec.AbortOnError = g.overridableArgs.AbortOnError + }) + handleFlag("no-wait", func(f *flag.Flag) { + kd.Spec.NoWait = utils.ParseBoolOrFalse(f.Value.String()) + }) + handleFlag("prune", func(f *flag.Flag) { + kd.Spec.Prune = utils.ParseBoolOrFalse(f.Value.String()) + }) + + if g.overridableArgs.Target != "" { + kd.Spec.Target = &g.overridableArgs.Target + } + if g.overridableArgs.TargetNameOverride != "" { + kd.Spec.TargetNameOverride = &g.overridableArgs.TargetNameOverride + } + if g.overridableArgs.TargetContext != "" { + kd.Spec.Context = &g.overridableArgs.TargetContext + } + + fis, err := g.overridableArgs.ImageFlags.LoadFixedImagesFromArgs() + if err != nil { + return nil, err + } + kd.Spec.Images = append(kd.Spec.Images, fis...) + + inc, err := g.overridableArgs.InclusionFlags.ParseInclusionFromArgs() + if err != nil { + return nil, err + } + kd.Spec.IncludeTags = append(kd.Spec.IncludeTags, inc.GetIncludes("tag")...) + kd.Spec.ExcludeTags = append(kd.Spec.ExcludeTags, inc.GetExcludes("tag")...) + kd.Spec.IncludeDeploymentDirs = append(kd.Spec.IncludeDeploymentDirs, inc.GetIncludes("deploymentItemDir")...) + kd.Spec.ExcludeDeploymentDirs = append(kd.Spec.ExcludeDeploymentDirs, inc.GetExcludes("deploymentItemDir")...) + + err = g.overrideDeploymentArgs(kd) + if err != nil { + return nil, err + } + + err = g.buildSourceOverrides(ctx, kd) + + kdOrigS, err := yaml.WriteJsonString(kdIn) + if err != nil { + return nil, err + } + kdS, err := yaml.WriteJsonString(kd) + if err != nil { + return nil, err + } + + patchB, err := json_patch.CreateMergePatch([]byte(kdOrigS), []byte(kdS)) + if err != nil { + return nil, err + } + + return patchB, nil +} + +func (g *gitopsCmdHelper) overrideDeploymentArgs(kd *v1beta1.KluctlDeployment) error { + overrideArgs, err := g.overridableArgs.ArgsFlags.LoadArgs() + if err != nil { + return err + } + if overrideArgs.IsZero() { + return nil + } + + newArgs := uo.New() + if kd.Spec.Args != nil { + x, err := uo.FromString(string(kd.Spec.Args.Raw)) + if err != nil { + return err + } + newArgs = x + } + newArgs.Merge(overrideArgs) + b, err := yaml.WriteJsonString(newArgs) + if err != nil { + return err + } + kd.Spec.Args = &runtime.RawExtension{Raw: []byte(b)} + + return nil +} + +func (g *gitopsCmdHelper) buildSourceOverrides(ctx context.Context, kd *v1beta1.KluctlDeployment) error { + var err error + var proxyUrl *url.URL + + if g.soClient != nil { + proxyUrl, err = g.soClient.BuildProxyUrl() + if err != nil { + return err + } + } else if len(g.soResolver.Overrides) != 0 { + return fmt.Errorf("local source overrides present but no connection to source override proxy established") + } else { + status.Warningf(ctx, "Will not try to auto override local project source") + return nil + } + + for _, ro := range g.soResolver.Overrides { + kd.Spec.SourceOverrides = append(kd.Spec.SourceOverrides, v1beta1.SourceOverride{ + RepoKey: ro.RepoKey, + Url: proxyUrl.String(), + IsGroup: ro.IsGroup, + }) + } + + err = g.buildProjectDirSourceOverride(kd, proxyUrl) + if err != nil { + status.Warningf(ctx, "Failed to add source override for local deployment project: %s", err.Error()) + } + + return nil +} + +func (g *gitopsCmdHelper) buildProjectDirSourceOverride(kd *v1beta1.KluctlDeployment, proxyUrl *url.URL) error { + if g.projectKey == nil || proxyUrl == nil { + return nil + } + + g.soResolver.Overrides = append(g.soResolver.Overrides, sourceoverride.RepoOverride{ + RepoKey: g.projectKey.RepoKey, + Override: g.projectGitRoot, + IsGroup: false, + }) + + kd.Spec.SourceOverrides = append(kd.Spec.SourceOverrides, v1beta1.SourceOverride{ + RepoKey: g.projectKey.RepoKey, + Url: proxyUrl.String(), + IsGroup: false, + }) + + return nil +} + +func (g *gitopsCmdHelper) waitForRequestToStart(ctx context.Context, key client.ObjectKey, requestValue string, getRequestResult func(status *v1beta1.KluctlDeploymentStatus) *v1beta1.ManualRequestResult) (*v1beta1.ManualRequestResult, error) { + s := status.Startf(ctx, "Waiting for controller to start processing the request") + defer s.Failed() + + sleep := time.Second * 1 + + var rr *v1beta1.ManualRequestResult + for { + var kd v1beta1.KluctlDeployment + err := g.client.Get(ctx, key, &kd) + if err != nil { + return nil, err + } + + rr = getRequestResult(&kd.Status) + if rr == nil { + time.Sleep(sleep) + continue + } + + if rr.Request.RequestValue != requestValue { + time.Sleep(sleep) + continue + } + break + } + s.Success() + return rr, nil +} + +func (g *gitopsCmdHelper) waitForRequestToFinish(ctx context.Context, key client.ObjectKey, getRequestResult func(status *v1beta1.KluctlDeploymentStatus) *v1beta1.ManualRequestResult) (*v1beta1.ManualRequestResult, error) { + sleep := time.Second * 1 + + var rr *v1beta1.ManualRequestResult + for { + var kd v1beta1.KluctlDeployment + err := g.client.Get(ctx, key, &kd) + if err != nil { + return nil, err + } + + rr = getRequestResult(&kd.Status) + if rr == nil { + time.Sleep(sleep) + continue + } + + if rr.EndTime == nil { + time.Sleep(sleep) + continue + } + break + } + return rr, nil +} + +func (g *gitopsCmdHelper) waitForRequestToStartAndFinish(ctx context.Context, key client.ObjectKey, requestValue string, getRequestResult func(status *v1beta1.KluctlDeploymentStatus) *v1beta1.ManualRequestResult) (*v1beta1.ManualRequestResult, error) { + rrStarted, err := g.waitForRequestToStart(ctx, key, requestValue, getRequestResult) + if err != nil { + return nil, err + } + + stopCh := make(chan struct{}) + + var rrFinished *v1beta1.ManualRequestResult + + gh := utils.NewGoHelper(ctx, 0) + gh.RunE(func() error { + defer func() { + close(stopCh) + }() + var err error + rrFinished, err = g.waitForRequestToFinish(ctx, key, getRequestResult) + return err + }) + gh.RunE(func() error { + return g.watchLogs(ctx, stopCh, key, true, rrStarted.ReconcileId) + }) + gh.Wait() + + if gh.ErrorOrNil() != nil { + return nil, gh.ErrorOrNil() + } + + return rrFinished, nil +} + +func (g *gitopsCmdHelper) watchLogs(ctx context.Context, stopCh chan struct{}, key client.ObjectKey, follow bool, reconcileId string) error { + if key == (client.ObjectKey{}) { + status.Infof(ctx, "Watching logs...") + } else { + status.Infof(ctx, "Watching logs for %s/%s...", key.Namespace, key.Name) + } + + logsCh, err := logs.WatchControllerLogs(ctx, g.corev1Client, g.args.ControllerNamespace, key, reconcileId, g.logsArgs.LogSince, follow) + if err != nil { + return err + } + + timeout := g.logsArgs.LogGroupingTime + timeoutCh := time.After(timeout) + for { + select { + case <-ctx.Done(): + g.flushLogLines(ctx, true) + return ctx.Err() + case <-stopCh: + g.flushLogLines(ctx, true) + return nil + case <-timeoutCh: + g.flushLogLines(ctx, true) + timeoutCh = time.After(timeout) + case l, ok := <-logsCh: + if !ok { + g.flushLogLines(ctx, true) + return nil + } + g.handleLogLine(ctx, l) + } + } +} + +func (g *gitopsCmdHelper) handleLogLine(ctx context.Context, logLine logs.LogLine) { + g.logsMutex.Lock() + defer g.logsMutex.Unlock() + + lk := logsKey{ + objectKey: client.ObjectKey{Name: logLine.Name, Namespace: logLine.Namespace}, + reconcileId: logLine.ReconcileID, + } + + lb := g.logsBufs[lk] + if lb == nil { + lb = &logsBuf{} + g.logsBufs[lk] = lb + } + lb.lines = append(lb.lines, logLine) + + hasOther := false + for k, x := range g.logsBufs { + if k != lk && len(x.lines) != 0 { + hasOther = true + break + } + } + if !hasOther && (g.lastFlushedLogsKey == nil || *g.lastFlushedLogsKey == lk) { + g.flushLogLines(ctx, false) + } +} + +func (g *gitopsCmdHelper) flushLogLines(ctx context.Context, needLock bool) { + if needLock { + g.logsMutex.Lock() + defer g.logsMutex.Unlock() + } + + for lk, lb := range g.logsBufs { + lk := lk + if len(lb.lines) == 0 { + if time.Now().After(lb.lastFlushTime.Add(5 * time.Second)) { + delete(g.logsBufs, lk) + } + continue + } + if g.lastFlushedLogsKey == nil || *g.lastFlushedLogsKey != lk { + g.lastFlushedLogsKey = &lk + lb.lastFlushTime = time.Now() + if lk.reconcileId == "" { + status.Info(ctx, "Showing general controller logs") + } else { + status.Infof(ctx, "Showing logs for %s/%s and reconciliation ID %s", lk.objectKey.Namespace, lk.objectKey.Name, lk.reconcileId) + } + status.Flush(ctx) + } + + for _, l := range lb.lines { + var s string + if g.logsArgs.LogTime { + s = fmt.Sprintf("%s: %s\n", l.Timestamp.Format(time.RFC3339), s) + } else { + s = l.Msg + "\n" + } + _, _ = getStdout(ctx).WriteString(s) + } + lb.lines = lb.lines[0:0] + } +} + +func (g *gitopsCmdHelper) initSourceOverrides(ctx context.Context) error { + var err error + g.soResolver, err = g.overridableArgs.SourceOverrides.ParseOverrides(ctx) + if err != nil { + return err + } + + pods, err := g.corev1Client.Pods(g.args.ControllerNamespace).List(ctx, metav1.ListOptions{ + LabelSelector: "control-plane=kluctl-controller", + }) + if err != nil { + return err + } + var pod *v12.Pod + if len(pods.Items) > 0 { + pod = &pods.Items[rand.Int()%len(pods.Items)] + } + + soClient, err := sourceoverride.NewClientCli(ctx, g.client, g.args.ControllerNamespace, g.soResolver) + if err != nil { + return err + } + + if pod != nil { + err = soClient.ConnectToPod(g.restConfig, *pod) + if err != nil { + return err + } + } else { + if g.args.LocalSourceOverridePort == 0 { + return nil + } + err = soClient.Connect(fmt.Sprintf("localhost:%d", g.args.LocalSourceOverridePort)) + if err != nil { + return err + } + } + g.soClient = soClient + + err = g.soClient.Handshake() + if err != nil { + return err + } + + go func() { + err := soClient.Start() + if err != nil { + status.Error(ctx, err.Error()) + } + }() + + return nil +} + +func (g *gitopsCmdHelper) checkCRDSupport(ctx context.Context) error { + var crd apiextensionsv1.CustomResourceDefinition + err := g.client.Get(ctx, client.ObjectKey{Name: "kluctldeployments.gitops.kluctl.io"}, &crd) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("KluctlDeployment CRD not found, which usually means the kluctl-controller is not installed") + } + return err + } + + for _, version := range crd.Spec.Versions { + if !version.Storage { + continue + } + status := version.Schema.OpenAPIV3Schema.Properties["status"] + if _, ok := status.Properties["reconcileRequestResult"]; ok { + // seems to be recent enough + return nil + } + } + + return fmt.Errorf("the KluctlDeployment CRD on the cluster seems to be outdated. Ensure to install at least kluctl-controller v2.22.0 with its corresponding CRDs") +} + +func init() { + utilruntime.Must(v1beta1.AddToScheme(scheme.Scheme)) +} diff --git a/cmd/kluctl/commands/cmd_gitops_deploy.go b/cmd/kluctl/commands/cmd_gitops_deploy.go new file mode 100644 index 000000000..6dd2cf366 --- /dev/null +++ b/cmd/kluctl/commands/cmd_gitops_deploy.go @@ -0,0 +1,69 @@ +package commands + +import ( + "context" + "fmt" + "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/results" + "sigs.k8s.io/controller-runtime/pkg/client" + "time" +) + +type gitopsDeployCmd struct { + args.GitOpsArgs + args.OutputFormatFlags + args.GitOpsLogArgs + args.GitOpsOverridableArgs `groupOverride:"override"` + + DeployExtraFlags `groupOverride:"override"` +} + +func (cmd *gitopsDeployCmd) Help() string { + return `This command will trigger an existing KluctlDeployment to perform a reconciliation loop with a forced deployment. +It does this by setting the annotation 'kluctl.io/request-deploy' to the current time. + +You can override many deployment relevant fields, see the list of command flags for details.` +} + +func (cmd *gitopsDeployCmd) Run(ctx context.Context) error { + g := gitopsCmdHelper{ + args: cmd.GitOpsArgs, + logsArgs: cmd.GitOpsLogArgs, + overridableArgs: cmd.GitOpsOverridableArgs, + noArgsReact: noArgsAutoDetectProjectAsk, + } + err := g.init(ctx) + if err != nil { + return err + } + for _, kd := range g.kds { + v := time.Now().Format(time.RFC3339Nano) + err := g.patchManualRequest(ctx, client.ObjectKeyFromObject(&kd), v1beta1.KluctlRequestDeployAnnotation, v) + if err != nil { + return err + } + + rr, err := g.waitForRequestToStartAndFinish(ctx, client.ObjectKeyFromObject(&kd), v, func(status *v1beta1.KluctlDeploymentStatus) *v1beta1.ManualRequestResult { + return status.DeployRequestResult + }) + if err != nil { + return err + } + + if g.resultStore != nil && rr != nil && rr.ResultId != "" { + cmdResult, err := g.resultStore.GetCommandResult(results.GetCommandResultOptions{Id: rr.ResultId, Reduced: true}) + if err != nil { + return err + } + err = outputCommandResult2(ctx, cmd.OutputFormatFlags, cmdResult) + if err != nil { + return err + } + } + if rr.CommandError != "" { + return fmt.Errorf("%s", rr.CommandError) + } + } + return nil +} diff --git a/cmd/kluctl/commands/cmd_gitops_diff.go b/cmd/kluctl/commands/cmd_gitops_diff.go new file mode 100644 index 000000000..c785baa57 --- /dev/null +++ b/cmd/kluctl/commands/cmd_gitops_diff.go @@ -0,0 +1,103 @@ +package commands + +import ( + "context" + "fmt" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/results" + "sigs.k8s.io/controller-runtime/pkg/client" + "time" +) + +type gitopsDiffCmd struct { + args.GitOpsArgs + args.OutputFormatFlags + args.GitOpsLogArgs + args.GitOpsOverridableArgs `groupOverride:"override"` +} + +func (cmd *gitopsDiffCmd) Help() string { + return `This command will trigger an existing KluctlDeployment to perform a reconciliation loop with a forced diff. +It does this by setting the annotation 'kluctl.io/request-diff' to the current time. + +You can override many deployment relevant fields, see the list of command flags for details.` +} + +func (cmd *gitopsDiffCmd) Run(ctx context.Context) error { + g := gitopsCmdHelper{ + args: cmd.GitOpsArgs, + logsArgs: cmd.GitOpsLogArgs, + overridableArgs: cmd.GitOpsOverridableArgs, + noArgsReact: noArgsAutoDetectProject, + } + err := g.init(ctx) + if err != nil { + return err + } + for _, kd := range g.kds { + v := time.Now().Format(time.RFC3339Nano) + err := g.patchManualRequest(ctx, client.ObjectKeyFromObject(&kd), v1beta1.KluctlRequestDiffAnnotation, v) + if err != nil { + return err + } + + rr, err := g.waitForRequestToStartAndFinish(ctx, client.ObjectKeyFromObject(&kd), v, func(status *v1beta1.KluctlDeploymentStatus) *v1beta1.ManualRequestResult { + return status.DiffRequestResult + }) + if err != nil { + return err + } + + if g.resultStore != nil && rr != nil && rr.ResultId != "" { + cmdResult, err := g.resultStore.GetCommandResult(results.GetCommandResultOptions{Id: rr.ResultId, Reduced: true}) + if err != nil { + return err + } + err = outputCommandResult2(ctx, cmd.OutputFormatFlags, cmdResult) + if err != nil { + return err + } + + err = cmd.cleanupDiffResult(ctx, &g, &kd, rr) + if err != nil { + status.Warningf(ctx, "Failed to cleanup diff result: %s", err.Error()) + } + } + if rr.CommandError != "" { + return fmt.Errorf("%s", rr.CommandError) + } + } + return nil +} + +func (cmd *gitopsDiffCmd) cleanupDiffResult(ctx context.Context, g *gitopsCmdHelper, kd *v1beta1.KluctlDeployment, rr *v1beta1.ManualRequestResult) error { + flags := args.CommandResultFlags{ + CommandResultReadOnlyFlags: cmd.CommandResultReadOnlyFlags, + CommandResultWriteFlags: args.CommandResultWriteFlags{ + WriteCommandResult: true, + }, + } + rwRS, err := buildResultStoreRW(ctx, g.restConfig, g.restMapper, &flags, false) + if err != nil { + return err + } + + err = rwRS.DeleteCommandResult(rr.ResultId) + if err != nil { + return err + } + + err = g.updateDeploymentStatus(ctx, client.ObjectKeyFromObject(kd), func(kd *v1beta1.KluctlDeployment) error { + if kd.Status.DiffRequestResult == nil || kd.Status.DiffRequestResult.Request.RequestValue != rr.Request.RequestValue { + return nil + } + kd.Status.LastDiffResult = nil + return nil + }, 5) + if err != nil { + return err + } + return nil +} diff --git a/cmd/kluctl/commands/cmd_gitops_logs.go b/cmd/kluctl/commands/cmd_gitops_logs.go new file mode 100644 index 000000000..38a2ff1ac --- /dev/null +++ b/cmd/kluctl/commands/cmd_gitops_logs.go @@ -0,0 +1,58 @@ +package commands + +import ( + "context" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/utils" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type gitopsLogsCmd struct { + args.GitOpsArgs + args.GitOpsLogArgs + + ReconcileId string `group:"misc" help:"If specified, logs are filtered for the given reconcile ID."` + Follow bool `group:"misc" short:"f" help:"Follow logs after printing old logs."` + + All bool `group:"misc" help:"Follow all controller logs, including all deployments and non-deployment related logs."` +} + +func (cmd *gitopsLogsCmd) Help() string { + return `Print and watch logs of specified KluctlDeployments from the kluctl-controller.` +} + +func (cmd *gitopsLogsCmd) Run(ctx context.Context) error { + g := gitopsCmdHelper{ + args: cmd.GitOpsArgs, + logsArgs: cmd.GitOpsLogArgs, + noArgsReact: noArgsAutoDetectProject, + } + + if cmd.All { + g.noArgsReact = noArgsNoDeployments + } + + err := g.init(ctx) + if err != nil { + return err + } + + stopCh := make(chan struct{}) + + gh := utils.NewGoHelper(ctx, 0) + if cmd.All { + gh.RunE(func() error { + return g.watchLogs(ctx, stopCh, client.ObjectKey{}, cmd.Follow, cmd.ReconcileId) + }) + } else { + for _, kd := range g.kds { + key := client.ObjectKeyFromObject(&kd) + gh.RunE(func() error { + return g.watchLogs(ctx, stopCh, key, cmd.Follow, cmd.ReconcileId) + }) + } + } + gh.Wait() + + return gh.ErrorOrNil() +} diff --git a/cmd/kluctl/commands/cmd_gitops_prune.go b/cmd/kluctl/commands/cmd_gitops_prune.go new file mode 100644 index 000000000..bda9a04d2 --- /dev/null +++ b/cmd/kluctl/commands/cmd_gitops_prune.go @@ -0,0 +1,67 @@ +package commands + +import ( + "context" + "fmt" + "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/results" + "sigs.k8s.io/controller-runtime/pkg/client" + "time" +) + +type gitopsPruneCmd struct { + args.GitOpsArgs + args.GitOpsLogArgs + args.OutputFormatFlags + args.GitOpsOverridableArgs +} + +func (cmd *gitopsPruneCmd) Help() string { + return `This command will trigger an existing KluctlDeployment to perform a reconciliation loop with a forced prune. +It does this by setting the annotation 'kluctl.io/request-prune' to the current time. + +You can override many deployment relevant fields, see the list of command flags for details.` +} + +func (cmd *gitopsPruneCmd) Run(ctx context.Context) error { + g := gitopsCmdHelper{ + args: cmd.GitOpsArgs, + logsArgs: cmd.GitOpsLogArgs, + overridableArgs: cmd.GitOpsOverridableArgs, + noArgsReact: noArgsAutoDetectProjectAsk, + } + err := g.init(ctx) + if err != nil { + return err + } + for _, kd := range g.kds { + v := time.Now().Format(time.RFC3339Nano) + err := g.patchManualRequest(ctx, client.ObjectKeyFromObject(&kd), v1beta1.KluctlRequestPruneAnnotation, v) + if err != nil { + return err + } + + rr, err := g.waitForRequestToStartAndFinish(ctx, client.ObjectKeyFromObject(&kd), v, func(status *v1beta1.KluctlDeploymentStatus) *v1beta1.ManualRequestResult { + return status.PruneRequestResult + }) + if err != nil { + return err + } + + if g.resultStore != nil && rr != nil && rr.ResultId != "" { + cmdResult, err := g.resultStore.GetCommandResult(results.GetCommandResultOptions{Id: rr.ResultId, Reduced: true}) + if err != nil { + return err + } + err = outputCommandResult2(ctx, cmd.OutputFormatFlags, cmdResult) + if err != nil { + return err + } + } + if rr.CommandError != "" { + return fmt.Errorf("%s", rr.CommandError) + } + } + return nil +} diff --git a/cmd/kluctl/commands/cmd_gitops_reconcile.go b/cmd/kluctl/commands/cmd_gitops_reconcile.go new file mode 100644 index 000000000..aaecfc5ec --- /dev/null +++ b/cmd/kluctl/commands/cmd_gitops_reconcile.go @@ -0,0 +1,56 @@ +package commands + +import ( + "context" + "fmt" + "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "sigs.k8s.io/controller-runtime/pkg/client" + "time" +) + +type gitopsReconcileCmd struct { + args.GitOpsArgs + args.GitOpsLogArgs + args.GitOpsOverridableArgs + + DeployExtraFlags `groupOverride:"override"` +} + +func (cmd *gitopsReconcileCmd) Help() string { + return `This command will trigger an existing KluctlDeployment to perform a reconciliation loop. +It does this by setting the annotation 'kluctl.io/request-reconcile' to the current time. + +You can override many deployment relevant fields, see the list of command flags for details.` +} + +func (cmd *gitopsReconcileCmd) Run(ctx context.Context) error { + g := gitopsCmdHelper{ + args: cmd.GitOpsArgs, + logsArgs: cmd.GitOpsLogArgs, + overridableArgs: cmd.GitOpsOverridableArgs, + noArgsReact: noArgsAutoDetectProjectAsk, + } + err := g.init(ctx) + if err != nil { + return err + } + for _, kd := range g.kds { + v := time.Now().Format(time.RFC3339Nano) + err := g.patchManualRequest(ctx, client.ObjectKeyFromObject(&kd), v1beta1.KluctlRequestReconcileAnnotation, v) + if err != nil { + return err + } + + rr, err := g.waitForRequestToStartAndFinish(ctx, client.ObjectKeyFromObject(&kd), v, func(status *v1beta1.KluctlDeploymentStatus) *v1beta1.ManualRequestResult { + return status.ReconcileRequestResult + }) + if err != nil { + return err + } + if rr.CommandError != "" { + return fmt.Errorf("%s", rr.CommandError) + } + } + return nil +} diff --git a/cmd/kluctl/commands/cmd_gitops_suspend_resume.go b/cmd/kluctl/commands/cmd_gitops_suspend_resume.go new file mode 100644 index 000000000..a2aba64a6 --- /dev/null +++ b/cmd/kluctl/commands/cmd_gitops_suspend_resume.go @@ -0,0 +1,94 @@ +package commands + +import ( + "context" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "sigs.k8s.io/controller-runtime/pkg/client" + "time" +) + +type GitopsSuspendCmd struct { + args.GitOpsArgs + args.OutputFormatFlags + args.GitOpsLogArgs + + All bool `group:"misc" help:"If enabled, suspend all deployments."` + + // we re-use the same code for "gitops resume" as well :) + forResume bool +} + +type gitopsResumeCmd struct { + GitopsSuspendCmd +} + +func (cmd *GitopsSuspendCmd) Help() string { + if cmd.forResume { + return `This command will resume a GitOps deployment by setting spec.suspend to 'false'.` + } else { + return `This command will suspend a GitOps deployment by setting spec.suspend to 'true'.` + } +} + +func (cmd *GitopsSuspendCmd) Run(ctx context.Context) error { + g := gitopsCmdHelper{ + args: cmd.GitOpsArgs, + logsArgs: cmd.GitOpsLogArgs, + noArgsReact: noArgsAutoDetectProjectAsk, + } + if cmd.All { + g.noArgsReact = noArgsAllDeployments + } + err := g.init(ctx) + if err != nil { + return err + } + for _, kd := range g.kds { + patchedKd, err := g.patchDeployment(ctx, client.ObjectKeyFromObject(&kd), func(kd *v1beta1.KluctlDeployment) error { + if cmd.forResume { + kd.Spec.Suspend = false + } else { + kd.Spec.Suspend = true + } + return nil + }) + if err != nil { + return err + } + err = func() error { + // modifying the spec causes a reconciliation loop and we should really wait for it to finish before we consider + // suspension to be done (otherwise you'd be surprised for some last-second deployments...) + st := status.Startf(ctx, "Waiting for final reconciliation to finish") + defer st.Failed() + + tick := time.NewTicker(time.Second) + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-tick.C: + var kd2 v1beta1.KluctlDeployment + err := g.client.Get(ctx, client.ObjectKeyFromObject(&kd), &kd2) + if err != nil { + return err + } + if kd2.Status.ObservedGeneration >= patchedKd.Generation { + st.Success() + return nil + } + } + } + }() + if err != nil { + return err + } + } + return nil +} + +func (cmd *gitopsResumeCmd) Run(ctx context.Context) error { + cmd.forResume = true + return cmd.GitopsSuspendCmd.Run(ctx) +} diff --git a/cmd/kluctl/commands/cmd_gitops_validate.go b/cmd/kluctl/commands/cmd_gitops_validate.go new file mode 100644 index 000000000..ccc069a1f --- /dev/null +++ b/cmd/kluctl/commands/cmd_gitops_validate.go @@ -0,0 +1,77 @@ +package commands + +import ( + "context" + "fmt" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/results" + "sigs.k8s.io/controller-runtime/pkg/client" + "time" +) + +type gitopsValidateCmd struct { + args.GitOpsArgs + args.GitOpsLogArgs + args.GitOpsOverridableArgs + args.OutputFlags + + WarningsAsErrors bool `group:"misc" help:"Consider warnings as failures"` +} + +func (cmd *gitopsValidateCmd) Help() string { + return `This command will trigger an existing KluctlDeployment to perform a reconciliation loop with a forced validation. +It does this by setting the annotation 'kluctl.io/request-validate' to the current time. + +You can override many deployment relevant fields, see the list of command flags for details.` +} + +func (cmd *gitopsValidateCmd) Run(ctx context.Context) error { + g := gitopsCmdHelper{ + args: cmd.GitOpsArgs, + logsArgs: cmd.GitOpsLogArgs, + overridableArgs: cmd.GitOpsOverridableArgs, + noArgsReact: noArgsAutoDetectProjectAsk, + } + err := g.init(ctx) + if err != nil { + return err + } + for _, kd := range g.kds { + v := time.Now().Format(time.RFC3339Nano) + err := g.patchManualRequest(ctx, client.ObjectKeyFromObject(&kd), v1beta1.KluctlRequestValidateAnnotation, v) + if err != nil { + return err + } + + rr, err := g.waitForRequestToStartAndFinish(ctx, client.ObjectKeyFromObject(&kd), v, func(status *v1beta1.KluctlDeploymentStatus) *v1beta1.ManualRequestResult { + return status.ValidateRequestResult + }) + if err != nil { + return err + } + if g.resultStore != nil && rr != nil && rr.ResultId != "" { + cmdResult, err := g.resultStore.GetValidateResult(results.GetValidateResultOptions{Id: rr.ResultId}) + if err != nil { + return err + } + err = outputValidateResult2(ctx, cmd.Output, cmdResult) + if err != nil { + return err + } + failed := len(cmdResult.Errors) != 0 || (cmd.WarningsAsErrors && len(cmdResult.Warnings) != 0) + if failed { + return fmt.Errorf("Validation failed") + } else { + status.Info(ctx, "Validation succeeded") + } + } else { + status.Info(ctx, "No validation result was returned.") + } + if rr.CommandError != "" { + return fmt.Errorf("%s", rr.CommandError) + } + } + return nil +} diff --git a/cmd/kluctl/commands/cmd_helm_pull.go b/cmd/kluctl/commands/cmd_helm_pull.go index 90d8c9fc8..056271651 100644 --- a/cmd/kluctl/commands/cmd_helm_pull.go +++ b/cmd/kluctl/commands/cmd_helm_pull.go @@ -1,63 +1,233 @@ package commands import ( + "context" "fmt" - "github.com/kluctl/kluctl/v2/cmd/kluctl/args" - "github.com/kluctl/kluctl/v2/pkg/deployment" - "github.com/kluctl/kluctl/v2/pkg/status" + "github.com/kluctl/kluctl/lib/git/types" "io/fs" + "os" "path/filepath" + "time" + + gitauth "github.com/kluctl/kluctl/lib/git/auth" + "github.com/kluctl/kluctl/lib/git/messages" + ssh_pool "github.com/kluctl/kluctl/lib/git/ssh-pool" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/lib/yaml" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/helm" + helmauth "github.com/kluctl/kluctl/v2/pkg/helm/auth" + ociauth "github.com/kluctl/kluctl/v2/pkg/oci/auth_provider" + "github.com/kluctl/kluctl/v2/pkg/prompts" + "github.com/kluctl/kluctl/v2/pkg/repocache" + "github.com/kluctl/kluctl/v2/pkg/utils" ) type helmPullCmd struct { + args.ProjectDir + args.GitCredentials args.HelmCredentials - - LocalDeployment string `group:"project" help:"Local deployment directory. Defaults to current directory"` + args.RegistryCredentials } func (cmd *helmPullCmd) Help() string { - return `The Helm charts are stored under the sub-directory 'charts/' next to the -'helm-chart.yaml'. These Helm charts are meant to be added to version control so that -pulling is only needed when really required (e.g. when the chart version changes).` + return `Kluctl requires Helm Charts to be pre-pulled by default, which is handled by this command. It will collect +all required Charts and versions and pre-pull them into .helm-charts. To disable pre-pulling for individual charts, +set 'skipPrePull: true' in helm-chart.yaml.` } -func (cmd *helmPullCmd) Run() error { - rootPath := "." - if cmd.LocalDeployment != "" { - rootPath = cmd.LocalDeployment +func (cmd *helmPullCmd) Run(ctx context.Context) error { + projectDir, err := cmd.ProjectDir.GetProjectDir() + if err != nil { + return err } - err := filepath.WalkDir(rootPath, func(p string, d fs.DirEntry, err error) error { - fname := filepath.Base(p) - if fname == "helm-chart.yml" || fname == "helm-chart.yaml" { - s := status.Start(cliCtx, "Pulling for %s", p) - chart, err := deployment.NewHelmChart(p) - if err != nil { - s.FailedWithMessage(err.Error()) - return err + if !yaml.Exists(filepath.Join(projectDir, ".kluctl.yaml")) && !yaml.Exists(filepath.Join(projectDir, ".kluctl-library.yaml")) { + return fmt.Errorf("helm-pull can only be used on the root of a Kluctl project that must have a .kluctl.yaml or .kluctl-library.yaml file") + } + sshPool := &ssh_pool.SshPool{} + messageCallbacks := &messages.MessageCallbacks{ + WarningFn: func(s string) { status.Warning(ctx, s) }, + TraceFn: func(s string) { status.Trace(ctx, s) }, + AskForPasswordFn: func(s string) (string, error) { return prompts.AskForPassword(ctx, s) }, + AskForConfirmationFn: func(s string) bool { return prompts.AskForConfirmation(ctx, s) }, + } + gitAuthProvider := gitauth.NewDefaultAuthProviders("KLUCTL_GIT", messageCallbacks) + ociAuthProvider := ociauth.NewDefaultAuthProviders("KLUCTL_REGISTRY") + helmAuthProvider := helmauth.NewDefaultAuthProviders("KLUCTL_HELM") + if x, err := cmd.GitCredentials.BuildAuthProvider(ctx); err != nil { + return err + } else { + gitAuthProvider.RegisterAuthProvider(x, false) + } + if x, err := cmd.HelmCredentials.BuildAuthProvider(ctx); err != nil { + return err + } else { + helmAuthProvider.RegisterAuthProvider(x, false) + } + if x, err := cmd.RegistryCredentials.BuildAuthProvider(ctx); err != nil { + return err + } else { + ociAuthProvider.RegisterAuthProvider(x, false) + } + + gitRp := repocache.NewGitRepoCache(ctx, sshPool, gitAuthProvider, nil, time.Second*60) + defer gitRp.Clear() + + ociRp := repocache.NewOciRepoCache(ctx, ociAuthProvider, nil, time.Second*60) + defer ociRp.Clear() + + _, err = doHelmPull(ctx, projectDir, helmAuthProvider, ociAuthProvider, gitRp, ociRp, false, true) + return err +} + +func cleanupUnusedCharts(ctx context.Context, versionsToPull map[string]helm.ChartVersion, dryRun bool, chartsDir string, isGitHelmChart bool) (int, error) { + if isGitHelmChart { + chartsDir = filepath.Join(chartsDir, "refs", "tags") + } + + actions := 0 + des, err := os.ReadDir(chartsDir) + if err != nil && !os.IsNotExist(err) { + return actions, err + } + for _, de := range des { + if !de.IsDir() { + continue + } + var testVersion helm.ChartVersion + if isGitHelmChart { + testVersion.GitRef = &types.GitRef{ + Tag: de.Name(), + } + } else { + testVersion.Version = utils.Ptr(de.Name()) + } + if _, ok := versionsToPull[testVersion.String()]; !ok { + actions++ + if !dryRun { + status.Infof(ctx, "Removing unused Chart with version %s", testVersion.String()) + err = os.RemoveAll(filepath.Join(chartsDir, de.Name())) + if err != nil { + return actions, err + } } + } + } + return actions, nil +} + +func doHelmPull(ctx context.Context, projectDir string, helmAuthProvider helmauth.HelmAuthProvider, ociAuthProvider ociauth.OciAuthProvider, gitRp *repocache.GitRepoCache, ociRp *repocache.OciRepoCache, dryRun bool, force bool) (int, error) { + actions := 0 + + baseChartsDir := filepath.Join(projectDir, ".helm-charts") + + releases, charts, err := loadHelmReleases(ctx, projectDir, baseChartsDir, helmAuthProvider, ociAuthProvider, gitRp, ociRp) + if err != nil { + return actions, err + } - creds := cmd.HelmCredentials.FindCredentials(*chart.Config.Repo, chart.Config.CredentialsId) - if chart.Config.CredentialsId != nil && creds == nil { - err := fmt.Errorf("no credentials provided for %s", p) - s.FailedWithMessage(err.Error()) - return err + g := utils.NewGoHelper(ctx, 8) + + for _, chart := range charts { + statusPrefix := chart.GetChartName() + versionsToPull := map[string]helm.ChartVersion{} + for _, hr := range releases { + if hr.Config.SkipPrePull { + continue + } + if hr.Chart == chart { + var v helm.ChartVersion + if hr.Chart.IsRegistryChart() { + v.Version = hr.Config.ChartVersion + } + if hr.Chart.IsGitRepositoryChart() { + v.GitRef = hr.Config.Git.Ref + } + versionsToPull[v.String()] = v + } + } + + chartsDir, err := chart.BuildPulledChartDir(baseChartsDir) + if err != nil { + return actions, err + } + cleanupActions, err := cleanupUnusedCharts(ctx, versionsToPull, dryRun, chartsDir, chart.IsGitRepositoryChart()) + actions += cleanupActions + + for _, version := range versionsToPull { + if yaml.Exists(filepath.Join(chartsDir, version.String(), "Chart.yaml")) && !force { + continue } - chart.SetCredentials(creds) - err = chart.Pull(cliCtx) - if err != nil { - s.FailedWithMessage(err.Error()) - return err + actions++ + + if dryRun { + continue } - s.Success() + g.RunE(func() error { + s := status.Startf(ctx, "%s: Pulling Chart with version %s", statusPrefix, version.String()) + defer s.Failed() + + _, err := chart.PullInProject(ctx, baseChartsDir, version) + if err != nil { + s.FailedWithMessagef("%s: %s", statusPrefix, err.Error()) + return err + } + + s.Success() + return nil + }) + } + } + g.Wait() + + if g.ErrorOrNil() != nil { + return actions, fmt.Errorf("command failed") + } + + return actions, nil +} + +func loadHelmReleases(ctx context.Context, projectDir string, baseChartsDir string, helmAuthProvider helmauth.HelmAuthProvider, ociAuthProvider ociauth.OciAuthProvider, gitRp *repocache.GitRepoCache, ociRp *repocache.OciRepoCache) ([]*helm.Release, []*helm.Chart, error) { + var releases []*helm.Release + chartsMap := make(map[string]*helm.Chart) + err := filepath.WalkDir(projectDir, func(p string, d fs.DirEntry, err error) error { + fname := filepath.Base(p) + if fname != "helm-chart.yml" && fname != "helm-chart.yaml" { + return nil + } + + relDir, err := filepath.Rel(projectDir, filepath.Dir(p)) + if err != nil { + return err + } + + hr, err := helm.NewRelease(ctx, projectDir, relDir, p, baseChartsDir, helmAuthProvider, ociAuthProvider, gitRp, ociRp) + if err != nil { + return err + } + + if hr.Chart.IsLocalChart() { + return nil + } + + releases = append(releases, hr) + chart := hr.Chart + key := fmt.Sprintf("%s / %s", chart.GetRepo(), chart.GetChartName()) + if x, ok := chartsMap[key]; !ok { + chartsMap[key] = chart + } else { + hr.Chart = x } return nil }) - if err != nil { - return fmt.Errorf("command failed") + return nil, nil, err } - - return err + charts := make([]*helm.Chart, 0, len(chartsMap)) + for _, chart := range chartsMap { + charts = append(charts, chart) + } + return releases, charts, nil } diff --git a/cmd/kluctl/commands/cmd_helm_update.go b/cmd/kluctl/commands/cmd_helm_update.go index 55c30b8e8..3726e4f9c 100644 --- a/cmd/kluctl/commands/cmd_helm_update.go +++ b/cmd/kluctl/commands/cmd_helm_update.go @@ -1,161 +1,387 @@ package commands import ( + "context" "fmt" - "github.com/go-git/go-git/v5" - "github.com/kluctl/kluctl/v2/cmd/kluctl/args" - "github.com/kluctl/kluctl/v2/pkg/deployment" - git2 "github.com/kluctl/kluctl/v2/pkg/git" - "github.com/kluctl/kluctl/v2/pkg/status" "io/fs" + "os" "path/filepath" + "strings" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/format/index" + git2 "github.com/kluctl/kluctl/lib/git" + gitauth "github.com/kluctl/kluctl/lib/git/auth" + "github.com/kluctl/kluctl/lib/git/messages" + ssh_pool "github.com/kluctl/kluctl/lib/git/ssh-pool" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/lib/yaml" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/helm" + helmauth "github.com/kluctl/kluctl/v2/pkg/helm/auth" + ociauth "github.com/kluctl/kluctl/v2/pkg/oci/auth_provider" + "github.com/kluctl/kluctl/v2/pkg/prompts" + "github.com/kluctl/kluctl/v2/pkg/repocache" + "github.com/kluctl/kluctl/v2/pkg/utils" ) type helmUpdateCmd struct { + args.ProjectDir + args.GitCredentials args.HelmCredentials + args.RegistryCredentials - LocalDeployment string `group:"project" help:"Local deployment directory. Defaults to current directory"` - Upgrade bool `group:"misc" help:"Write new versions into helm-chart.yaml and perform helm-pull afterwards"` - Commit bool `group:"misc" help:"Create a git commit for every updated chart"` + Upgrade bool `group:"misc" help:"Write new versions into helm-chart.yaml and perform helm-pull afterwards"` + Commit bool `group:"misc" help:"Create a git commit for every updated chart"` + + Interactive bool `group:"misc" short:"i" help:"Ask for every Helm Chart if it should be upgraded."` } func (cmd *helmUpdateCmd) Help() string { return `Optionally performs the actual upgrade and/or add a commit to version control.` } -func (cmd *helmUpdateCmd) Run() error { - rootPath := "." - if cmd.LocalDeployment != "" { - rootPath = cmd.LocalDeployment +func (cmd *helmUpdateCmd) Run(ctx context.Context) error { + projectDir, err := cmd.ProjectDir.GetProjectDir() + if err != nil { + return err + } + + if !yaml.Exists(filepath.Join(projectDir, ".kluctl.yaml")) && !yaml.Exists(filepath.Join(projectDir, ".kluctl-library.yaml")) { + return fmt.Errorf("helm-update can only be used on the root of a Kluctl project that must have a .kluctl.yaml or .kluctl-library.yaml file") } - gitRootPath, err := git2.DetectGitRepositoryRoot(rootPath) + + gitRootPath, err := git2.DetectGitRepositoryRoot(projectDir) if err != nil { return err } + sshPool := &ssh_pool.SshPool{} + messageCallbacks := &messages.MessageCallbacks{ + WarningFn: func(s string) { status.Warning(ctx, s) }, + TraceFn: func(s string) { status.Trace(ctx, s) }, + AskForPasswordFn: func(s string) (string, error) { return prompts.AskForPassword(ctx, s) }, + AskForConfirmationFn: func(s string) bool { return prompts.AskForConfirmation(ctx, s) }, + } + gitAuthProvider := gitauth.NewDefaultAuthProviders("KLUCTL_GIT", messageCallbacks) + ociAuthProvider := ociauth.NewDefaultAuthProviders("KLUCTL_REGISTRY") + helmAuthProvider := helmauth.NewDefaultAuthProviders("KLUCTL_HELM") + if x, err := cmd.GitCredentials.BuildAuthProvider(ctx); err != nil { + return err + } else { + gitAuthProvider.RegisterAuthProvider(x, false) + } + if x, err := cmd.HelmCredentials.BuildAuthProvider(ctx); err != nil { + return err + } else { + helmAuthProvider.RegisterAuthProvider(x, false) + } + if x, err := cmd.RegistryCredentials.BuildAuthProvider(ctx); err != nil { + return err + } else { + ociAuthProvider.RegisterAuthProvider(x, false) + } + gitRp := repocache.NewGitRepoCache(ctx, sshPool, gitAuthProvider, nil, time.Second*60) + defer gitRp.Clear() - err = filepath.WalkDir(rootPath, func(p string, d fs.DirEntry, err error) error { - fname := filepath.Base(p) - if fname == "helm-chart.yml" || fname == "helm-chart.yaml" { - statusPrefix := filepath.Base(filepath.Dir(p)) - s := status.Start(cliCtx, "%s: Checking for updates", statusPrefix) - defer s.Failed() + ociRp := repocache.NewOciRepoCache(ctx, ociAuthProvider, nil, time.Second*60) + + defer ociRp.Clear() + if cmd.Commit { + gitStatus, err := git2.GetWorktreeStatus(ctx, gitRootPath) + if err != nil { + return err + } + for pth, s := range gitStatus { + if strings.HasPrefix(pth, ".helm-charts/") { + status.Tracef(ctx, "gitStatus=%s", gitStatus.String()) + return fmt.Errorf("--commit can only be used when .helm-chart directory is clean") + } + if (s.Staging != git.Untracked && s.Staging != git.Unmodified) || (s.Worktree != git.Untracked && s.Worktree != git.Unmodified) { + status.Tracef(ctx, "gitStatus=%s", gitStatus.String()) + return fmt.Errorf("--commit can only be used when the git worktree is unmodified") + } + } + } + + baseChartsDir := filepath.Join(projectDir, ".helm-charts") + + g := utils.NewGoHelper(ctx, 8) + + releases, charts, err := loadHelmReleases(ctx, projectDir, baseChartsDir, helmAuthProvider, ociAuthProvider, gitRp, ociRp) + if err != nil { + return err + } + + if cmd.Commit { + actions, err := doHelmPull(ctx, projectDir, helmAuthProvider, ociAuthProvider, gitRp, ociRp, true, false) + if err != nil { + return err + } + if actions != 0 { + return fmt.Errorf(".helm-charts is not up-to-date. Please run helm-pull before") + } + } - chart, err := deployment.NewHelmChart(p) + for _, chart := range charts { + g.RunE(func() error { + s := status.Startf(ctx, "%s: Querying versions", chart.GetChartName()) + defer s.Failed() + chart.GetLocalChartVersion() + err := chart.QueryVersions(ctx) if err != nil { - s.Update("%s: Error while loading helm-chart.yaml: %v", statusPrefix, err) + s.FailedWithMessagef("%s: %s", chart.GetChartName(), err.Error()) return err } + s.Success() + return nil + }) + } + g.Wait() + if g.ErrorOrNil() != nil { + return g.ErrorOrNil() + } - creds := cmd.HelmCredentials.FindCredentials(*chart.Config.Repo, chart.Config.CredentialsId) - if chart.Config.CredentialsId != nil && creds == nil { - err := fmt.Errorf("%s: No credentials provided", statusPrefix) - s.FailedWithMessage(err.Error()) - return err + for _, chart := range charts { + versionsToPull := map[string]helm.ChartVersion{} + for _, hr := range releases { + if hr.Chart != chart { + continue } - chart.SetCredentials(creds) + version := hr.GetAbstractVersion() + versionsToPull[version.String()] = version + } - newVersion, updated, err := chart.CheckUpdate() - if err != nil { - return err + for _, version := range versionsToPull { + var out string + if chart.IsRegistryChart() { + out = fmt.Sprintf("%s: Downloading Chart with version %s into cache", chart.GetChartName(), version.String()) } - if !updated { - s.Update("%s: Version %s is already up-to-date.", statusPrefix, *chart.Config.ChartVersion) + if chart.IsGitRepositoryChart() { + out = fmt.Sprintf("%s: Downloading Chart with branch, tag or commit %s into cache", chart.GetChartName(), version.String()) + } + g.RunE(func() error { + s := status.Start(ctx, out) + defer s.Failed() + _, err := chart.PullCached(ctx, version) + if err != nil { + s.FailedWithMessagef("%s: %s", chart.GetChartName(), err.Error()) + return err + } s.Success() return nil + }) + } + } + g.Wait() + if g.ErrorOrNil() != nil { + return g.ErrorOrNil() + } + + upgrades := map[helmUpgradeKey][]*helm.Release{} + + for _, hr := range releases { + cd, err := hr.Chart.BuildPulledChartDir(baseChartsDir) + if err != nil { + return err + } + + relDir, err := filepath.Rel(projectDir, filepath.Dir(hr.ConfigFile)) + if err != nil { + return err + } + + latestVersion, err := hr.Chart.GetLatestVersion(hr.Config.UpdateConstraints) + if err != nil { + return err + } + currentVersion := hr.GetAbstractVersion() + + if currentVersion == latestVersion { + continue + } + + if hr.Config.SkipUpdate { + status.Infof(ctx, "%s: Skipped update to version %s", relDir, latestVersion.String()) + continue + } + + status.Infof(ctx, "%s: Chart %s (old version %s) has new version %s available", + relDir, hr.Chart.GetChartName(), currentVersion.String(), latestVersion.String()) + + if !cmd.Upgrade { + continue + } + + if cmd.Interactive { + if !prompts.AskForConfirmation(ctx, fmt.Sprintf("%s: Do you want to upgrade Chart %s to version %s?", + relDir, hr.Chart.GetChartName(), latestVersion.String())) { + continue + } + } + + oldVersion := currentVersion + if hr.Chart.IsGitRepositoryChart() { + if hr.Config.Git.Ref.Tag == "" { + return fmt.Errorf("unexpected error, tag of git chart is empty") } - msg := fmt.Sprintf("%s: Chart has new version %s available. Old version is %s.", statusPrefix, newVersion, *chart.Config.ChartVersion) - if chart.Config.SkipUpdate { - msg += " skipUpdate is set to true." + // git object is shared by multiple releases + hr.Config.Git, err = utils.DeepClone(hr.Config.Git) + if err != nil { + return err } - s.Update(msg) + hr.Config.Git.Ref = latestVersion.GitRef + } else { + hr.Config.ChartVersion = latestVersion.Version + } + err = hr.Save() + if err != nil { + return err + } + status.Infof(ctx, "%s: Updated Chart version to %s", relDir, latestVersion.String()) - if !cmd.Upgrade { - s.Success() - } else { - if chart.Config.SkipUpdate { - s.Update("%s: NOT upgrading chart as skipUpdate was set to true", statusPrefix) - s.Success() - return nil - } + k := helmUpgradeKey{ + chartDir: cd, + oldVersion: oldVersion, + newVersion: latestVersion, + } + upgrades[k] = append(upgrades[k], hr) + } - oldVersion := *chart.Config.ChartVersion - chart.Config.ChartVersion = &newVersion - err = chart.Save() - if err != nil { - return err - } + for k, hrs := range upgrades { + err = cmd.pullAndCommit(ctx, projectDir, baseChartsDir, gitRootPath, hrs, k.oldVersion, helmAuthProvider, ociAuthProvider, gitRp, ociRp) + if err != nil { + return err + } + } - chartsDir := filepath.Join(filepath.Dir(p), "charts") + return nil +} - // we need to list all files contained inside the charts dir BEFORE doing the pull, so that we later - // know what got deleted - gitFiles := make(map[string]bool) - gitFiles[p] = true - err = filepath.WalkDir(chartsDir, func(p string, d fs.DirEntry, err error) error { - if !d.IsDir() { - gitFiles[p] = true - } - return nil - }) - if err != nil { - return err - } +func (cmd *helmUpdateCmd) collectFiles(root string, dir string, m map[string]os.FileInfo) error { + err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error { + if d == nil || d.IsDir() { + return nil + } + relToGit, err := filepath.Rel(root, p) + if err != nil { + return err + } + if _, ok := m[relToGit]; ok { + return nil + } + m[relToGit], _ = d.Info() + return nil + }) + if os.IsNotExist(err) { + err = nil + } + return err +} - s.Update("%s: Pulling new version", statusPrefix) - defer s.Failed() +func (cmd *helmUpdateCmd) pullAndCommit(ctx context.Context, projectDir string, baseChartsDir string, gitRootPath string, hrs []*helm.Release, oldVersion helm.ChartVersion, helmAuthProvider helmauth.HelmAuthProvider, ociAuthProvider *ociauth.OciAuthProviders, gitRp *repocache.GitRepoCache, ociRp *repocache.OciRepoCache) error { + chart := hrs[0].Chart - err = chart.Pull(cliCtx) - if err != nil { - return err - } + newVersion := hrs[0].GetAbstractVersion() - // and now list all files again to catch all new files - err = filepath.WalkDir(chartsDir, func(p string, d fs.DirEntry, err error) error { - if !d.IsDir() { - gitFiles[p] = true - } - return nil - }) - if err != nil { - return err - } + s := status.Startf(ctx, "Upgrading Chart %s from version %s to %s", chart.GetChartName(), oldVersion.String(), newVersion.String()) + defer s.Failed() - if cmd.Commit { - commitMsg := fmt.Sprintf("Updated helm chart %s from %s to %s", filepath.Dir(p), oldVersion, newVersion) + doError := func(err error) error { + s.FailedWithMessage(err.Error()) + return err + } - s.Update(fmt.Sprintf("%s: Updating chart from %s to %s", statusPrefix, oldVersion, newVersion)) + r, err := git.PlainOpen(gitRootPath) + if err != nil { + return doError(err) + } + wt, err := r.Worktree() + if err != nil { + return doError(err) + } - r, err := git.PlainOpen(gitRootPath) - if err != nil { - return err - } - wt, err := r.Worktree() - if err != nil { - return err + if cmd.Commit { + for _, hr := range hrs { + // add helm-chart.yaml + relToGit, err := filepath.Rel(gitRootPath, hr.ConfigFile) + if err != nil { + return doError(err) + } + _, err = wt.Add(relToGit) + if err != nil { + return doError(err) + } + } + } + + // we need to list all files contained inside the charts dir BEFORE doing the pull, so that we later + // know what got deleted + files := map[string]os.FileInfo{} + if cmd.Commit { + err = cmd.collectFiles(gitRootPath, baseChartsDir, files) + if err != nil { + return doError(err) + } + } + + _, err = doHelmPull(ctx, projectDir, helmAuthProvider, ociAuthProvider, gitRp, ociRp, false, false) + if err != nil { + return doError(err) + } + + if cmd.Commit { + files2 := map[string]os.FileInfo{} + err = cmd.collectFiles(gitRootPath, baseChartsDir, files2) + if err != nil { + return doError(err) + } + + for pth, st1 := range files { + st2, ok := files2[pth] + if !ok || st1.Mode() != st2.Mode() || st1.ModTime() != st2.ModTime() || st1.Size() != st2.Size() { + // removed or modified + if ok { + if !st2.IsDir() { + _, err = wt.Add(pth) } - for p := range gitFiles { - absPath, err := filepath.Abs(filepath.Join(rootPath, p)) - if err != nil { - return err - } - relToGit, err := filepath.Rel(gitRootPath, absPath) - if err != nil { - return err - } - _, err = wt.Add(relToGit) - if err != nil { - return err - } + } else { + if !st1.IsDir() { + _, err = wt.Remove(pth) } - _, err = wt.Commit(commitMsg, &git.CommitOptions{}) - if err != nil { - return err + } + if err != nil && err != index.ErrEntryNotFound { + return doError(err) + } + } + } + for pth, st1 := range files2 { + if _, ok := files[pth]; !ok { + if !st1.IsDir() { + // added + _, err = wt.Add(pth) + if err != nil && err != index.ErrEntryNotFound { + return doError(err) } } - s.Success() } } - return nil - }) - return err + + commitMsg := fmt.Sprintf("Updated helm chart %s from version %s to version %s", chart.GetChartName(), oldVersion.String(), newVersion.String()) + _, err = wt.Commit(commitMsg, &git.CommitOptions{}) + if err != nil { + return doError(fmt.Errorf("failed to commit: %w", err)) + } + + s.UpdateAndInfoFallbackf("Committed helm chart %s with version %s", chart.GetChartName(), newVersion.String()) + } + s.Success() + + return nil +} + +type helmUpgradeKey struct { + chartDir string + oldVersion helm.ChartVersion + newVersion helm.ChartVersion } diff --git a/cmd/kluctl/commands/cmd_list_images.go b/cmd/kluctl/commands/cmd_list_images.go index cfd9ef56f..325c5700f 100644 --- a/cmd/kluctl/commands/cmd_list_images.go +++ b/cmd/kluctl/commands/cmd_list_images.go @@ -1,18 +1,24 @@ package commands import ( + "context" "github.com/kluctl/kluctl/v2/cmd/kluctl/args" "github.com/kluctl/kluctl/v2/pkg/types" ) type listImagesCmd struct { args.ProjectFlags + args.KubeconfigFlags args.TargetFlags args.ArgsFlags args.ImageFlags args.InclusionFlags + args.GitCredentials + args.HelmCredentials + args.RegistryCredentials args.OutputFlags args.RenderOutputDirFlags + args.OfflineKubernetesFlags Simple bool `group:"misc" help:"Output a simplified version of the images list"` } @@ -21,22 +27,28 @@ func (cmd *listImagesCmd) Help() string { return `The result is a compatible with yaml files expected by --fixed-images-file. If fixed images ('-f/--fixed-image') are provided, these are also taken into account, -as described in for the deploy command.` +as described in the deploy command.` } -func (cmd *listImagesCmd) Run() error { +func (cmd *listImagesCmd) Run(ctx context.Context) error { ptArgs := projectTargetCommandArgs{ projectFlags: cmd.ProjectFlags, + kubeconfigFlags: cmd.KubeconfigFlags, targetFlags: cmd.TargetFlags, argsFlags: cmd.ArgsFlags, imageFlags: cmd.ImageFlags, inclusionFlags: cmd.InclusionFlags, + gitCredentials: cmd.GitCredentials, + helmCredentials: cmd.HelmCredentials, + registryCredentials: cmd.RegistryCredentials, renderOutputDirFlags: cmd.RenderOutputDirFlags, + offlineKubernetes: cmd.OfflineKubernetes, + kubernetesVersion: cmd.KubernetesVersion, } - return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error { + return withProjectCommandContext(ctx, ptArgs, func(cmdCtx *commandCtx) error { result := types.FixedImagesConfig{ - Images: ctx.images.SeenImages(cmd.Simple), + Images: cmdCtx.images.SeenImages(cmd.Simple), } - return outputYamlResult(cmd.Output, result, false) + return outputYamlResult(ctx, cmd.Output, result, false) }) } diff --git a/cmd/kluctl/commands/cmd_list_targets.go b/cmd/kluctl/commands/cmd_list_targets.go index f8a6b5626..909530679 100644 --- a/cmd/kluctl/commands/cmd_list_targets.go +++ b/cmd/kluctl/commands/cmd_list_targets.go @@ -2,6 +2,8 @@ package commands import ( "context" + "strings" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" "github.com/kluctl/kluctl/v2/pkg/kluctl_project" "github.com/kluctl/kluctl/v2/pkg/types" @@ -10,18 +12,33 @@ import ( type listTargetsCmd struct { args.ProjectFlags args.OutputFlags + + OnlyNames bool `group:"misc" help:"If provided --only-names will only output "` } func (cmd *listTargetsCmd) Help() string { - return `Outputs a yaml list with all target, including dynamic targets` + return `Outputs a yaml list with all targets` } -func (cmd *listTargetsCmd) Run() error { - return withKluctlProjectFromArgs(cmd.ProjectFlags, true, false, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { +func (cmd *listTargetsCmd) Run(ctx context.Context) error { + return withKluctlProjectFromArgs(ctx, nil, cmd.ProjectFlags, nil, nil, nil, nil, false, true, false, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { var result []*types.Target - for _, t := range p.DynamicTargets { - result = append(result, t.Target) + + for _, t := range p.Targets { + result = append(result, t) + } + + if cmd.OnlyNames { + var targetNames []string + for _, target := range result { + targetNames = append(targetNames, target.Name) + } + targetNamesStr := strings.Join(targetNames, "\n") + if targetNamesStr != "" { + targetNamesStr += "\n" + } + return outputResult2(ctx, cmd.Output, targetNamesStr) } - return outputYamlResult(cmd.Output, result, false) + return outputYamlResult(ctx, cmd.Output, result, false) }) } diff --git a/cmd/kluctl/commands/cmd_oci.go b/cmd/kluctl/commands/cmd_oci.go new file mode 100644 index 000000000..869198bc6 --- /dev/null +++ b/cmd/kluctl/commands/cmd_oci.go @@ -0,0 +1,5 @@ +package commands + +type ociCmd struct { + Push ociPushCmd `cmd:"" help:"Push to an oci repository"` +} diff --git a/cmd/kluctl/commands/cmd_oci_push.go b/cmd/kluctl/commands/cmd_oci_push.go new file mode 100644 index 000000000..bcc989bb7 --- /dev/null +++ b/cmd/kluctl/commands/cmd_oci_push.go @@ -0,0 +1,184 @@ +package commands + +import ( + "context" + "encoding/json" + "fmt" + "github.com/kluctl/kluctl/lib/git" + "github.com/kluctl/kluctl/lib/git/sourceignore" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/lib/yaml" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/oci/auth_provider" + "os" + "path/filepath" + "strings" + "time" + + reg "github.com/google/go-containerregistry/pkg/name" + "github.com/kluctl/kluctl/v2/pkg/oci/client" +) + +var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...) + +type ociPushCmd struct { + args.ProjectDir + args.RegistryCredentials + + Url string `group:"misc" help:"Specifies the artifact URL. This argument is required." required:"true"` + Annotation []string `group:"misc" help:"Set custom OCI annotations in the format '='"` + Output string `group:"misc" help:"the format in which the artifact digest should be printed, can be 'json' or 'yaml'"` + + Timeout time.Duration `group:"misc" help:"Specify timeout for all operations, including loading of the project, all external api calls and waiting for readiness." default:"10m"` +} + +func (cmd *ociPushCmd) Help() string { + return `The push command creates a tarball from the current project and uploads the +artifact to an OCI repository.` +} + +func (cmd *ociPushCmd) Run(ctx context.Context) error { + path, err := cmd.ProjectDir.GetProjectDir() + if err != nil { + return err + } + + repoRoot, err := git.DetectGitRepositoryRoot(path) + if err != nil { + return err + } + gitInfo, _, err := git.BuildGitInfo(ctx, repoRoot, path) + if err != nil { + return err + } + + if _, err := os.Stat(path); err != nil { + return fmt.Errorf("invalid path '%s', must point to an existing project: %w", path, err) + } + + absPath, err := filepath.Abs(path) + if err != nil { + return err + } + ignorePatterns, err := git.LoadGitignore(absPath) + if err != nil { + return err + } + + if cmd.Timeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, cmd.Timeout) + defer cancel() + } + + ociAuthProvider := auth_provider.NewDefaultAuthProviders("KLUCTL_REGISTRY") + if x, err := cmd.RegistryCredentials.BuildAuthProvider(ctx); err != nil { + return err + } else { + ociAuthProvider.RegisterAuthProvider(x, false) + } + + url, err := client.ParseArtifactURL(cmd.Url) + if err != nil { + return err + } + + annotations := map[string]string{} + for _, annotation := range cmd.Annotation { + kv := strings.Split(annotation, "=") + if len(kv) != 2 { + return fmt.Errorf("invalid annotation %s, must be in the format key=value", annotation) + } + annotations[kv[0]] = kv[1] + } + + annotations["io.kluctl.image.git_info"], err = yaml.WriteJsonString(&gitInfo) + if err != nil { + return err + } + + meta := client.Metadata{ + Revision: gitInfo.Commit, + Annotations: annotations, + } + if gitInfo.Url != nil { + meta.Source = gitInfo.Url.String() + } + + ctx, cancel := context.WithTimeout(ctx, cmd.Timeout) + defer cancel() + + ae, err := ociAuthProvider.FindAuthEntry(ctx, cmd.Url) + if err != nil { + return err + } + + opts := client.DefaultOptions() + + if ae != nil { + authOpts, err := ae.BuildCraneOptions() + if err != nil { + return err + } + opts = append(opts, authOpts...) + } + + var st *status.StatusContext + if cmd.Output == "" { + st = status.Startf(ctx, "Pushing artifact to %s", url) + defer st.Failed() + } + + ociClient := client.NewClient(opts) + digestURL, err := ociClient.Push(ctx, url, path, client.WithPushIgnorePatterns(ignorePatterns), client.WithPushMetadata(meta)) + if err != nil { + return fmt.Errorf("pushing artifact failed: %w", err) + } + + digest, err := reg.NewDigest(digestURL) + if err != nil { + return fmt.Errorf("artifact digest parsing failed: %w", err) + } + + tag, err := reg.NewTag(url) + if err != nil { + return fmt.Errorf("artifact tag parsing failed: %w", err) + } + + info := struct { + URL string `json:"url"` + Repository string `json:"repository"` + Tag string `json:"tag"` + Digest string `json:"digest"` + }{ + URL: fmt.Sprintf("oci://%s", digestURL), + Repository: digest.Repository.Name(), + Tag: tag.TagStr(), + Digest: digest.DigestStr(), + } + + if cmd.Output == "" { + st.UpdateAndInfoFallbackf("Artifact successfully pushed to %s", digestURL) + } + + st.Success() + status.Flush(ctx) + + switch cmd.Output { + case "json": + marshalled, err := json.MarshalIndent(&info, "", " ") + if err != nil { + return fmt.Errorf("artifact digest JSON conversion failed: %w", err) + } + marshalled = append(marshalled, "\n"...) + _, _ = getStdout(ctx).Write(marshalled) + case "yaml": + marshalled, err := yaml.WriteYamlBytes(&info) + if err != nil { + return fmt.Errorf("artifact digest YAML conversion failed: %w", err) + } + _, _ = getStdout(ctx).Write(marshalled) + } + + return nil +} diff --git a/cmd/kluctl/commands/cmd_poke_images.go b/cmd/kluctl/commands/cmd_poke_images.go index e08e152c8..5ce9eb5d2 100644 --- a/cmd/kluctl/commands/cmd_poke_images.go +++ b/cmd/kluctl/commands/cmd_poke_images.go @@ -1,22 +1,28 @@ package commands import ( + "context" "fmt" "github.com/kluctl/kluctl/v2/cmd/kluctl/args" "github.com/kluctl/kluctl/v2/pkg/deployment/commands" - "github.com/kluctl/kluctl/v2/pkg/status" + "github.com/kluctl/kluctl/v2/pkg/prompts" ) type pokeImagesCmd struct { args.ProjectFlags + args.KubeconfigFlags args.TargetFlags args.ArgsFlags args.ImageFlags args.InclusionFlags + args.GitCredentials + args.HelmCredentials + args.RegistryCredentials args.YesFlags args.DryRunFlags args.OutputFormatFlags args.RenderOutputDirFlags + args.CommandResultFlags } func (cmd *pokeImagesCmd) Help() string { @@ -25,30 +31,32 @@ deploying the target. Only images used in combination with 'images.get_image(... replaced` } -func (cmd *pokeImagesCmd) Run() error { +func (cmd *pokeImagesCmd) Run(ctx context.Context) error { ptArgs := projectTargetCommandArgs{ projectFlags: cmd.ProjectFlags, + kubeconfigFlags: cmd.KubeconfigFlags, targetFlags: cmd.TargetFlags, argsFlags: cmd.ArgsFlags, imageFlags: cmd.ImageFlags, inclusionFlags: cmd.InclusionFlags, + gitCredentials: cmd.GitCredentials, + helmCredentials: cmd.HelmCredentials, + registryCredentials: cmd.RegistryCredentials, dryRunArgs: &cmd.DryRunFlags, renderOutputDirFlags: cmd.RenderOutputDirFlags, + commandResultFlags: &cmd.CommandResultFlags, } - return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error { + return withProjectCommandContext(ctx, ptArgs, func(cmdCtx *commandCtx) error { if !cmd.Yes && !cmd.DryRun { - if !status.AskForConfirmation(cliCtx, fmt.Sprintf("Do you really want to poke images to the context/cluster %s?", ctx.targetCtx.ClusterContext)) { + if !prompts.AskForConfirmation(ctx, fmt.Sprintf("Do you really want to poke images to the context/cluster %s?", cmdCtx.targetCtx.ClusterContext)) { return fmt.Errorf("aborted") } } - cmd2 := commands.NewPokeImagesCommand(ctx.targetCtx.DeploymentCollection) + cmd2 := commands.NewPokeImagesCommand(cmdCtx.targetCtx) - result, err := cmd2.Run(ctx.ctx, ctx.targetCtx.SharedContext.K) - if err != nil { - return err - } - err = outputCommandResult(cmd.OutputFormat, result) + result := cmd2.Run() + err := outputCommandResult(ctx, cmdCtx, cmd.OutputFormatFlags, result, !cmd.DryRun || cmd.ForceWriteCommandResult) if err != nil { return err } diff --git a/cmd/kluctl/commands/cmd_prune.go b/cmd/kluctl/commands/cmd_prune.go index 3610aed88..ac35825e9 100644 --- a/cmd/kluctl/commands/cmd_prune.go +++ b/cmd/kluctl/commands/cmd_prune.go @@ -1,21 +1,30 @@ package commands import ( + "context" "fmt" "github.com/kluctl/kluctl/v2/cmd/kluctl/args" "github.com/kluctl/kluctl/v2/pkg/deployment/commands" + k8s2 "github.com/kluctl/kluctl/v2/pkg/types/k8s" ) type pruneCmd struct { args.ProjectFlags + args.KubeconfigFlags args.TargetFlags args.ArgsFlags args.ImageFlags args.InclusionFlags + args.GitCredentials + args.HelmCredentials + args.RegistryCredentials args.YesFlags args.DryRunFlags args.OutputFormatFlags args.RenderOutputDirFlags + args.CommandResultFlags + + Discriminator string `group:"misc" help:"Override the target discriminator."` } func (cmd *pruneCmd) Help() string { @@ -26,32 +35,33 @@ func (cmd *pruneCmd) Help() string { 3. Remove all objects from the list of 1. that are part of the list in 2.` } -func (cmd *pruneCmd) Run() error { +func (cmd *pruneCmd) Run(ctx context.Context) error { ptArgs := projectTargetCommandArgs{ projectFlags: cmd.ProjectFlags, + kubeconfigFlags: cmd.KubeconfigFlags, targetFlags: cmd.TargetFlags, argsFlags: cmd.ArgsFlags, imageFlags: cmd.ImageFlags, inclusionFlags: cmd.InclusionFlags, + gitCredentials: cmd.GitCredentials, + helmCredentials: cmd.HelmCredentials, + registryCredentials: cmd.RegistryCredentials, dryRunArgs: &cmd.DryRunFlags, renderOutputDirFlags: cmd.RenderOutputDirFlags, + commandResultFlags: &cmd.CommandResultFlags, + discriminator: cmd.Discriminator, } - return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error { - return cmd.runCmdPrune(ctx) + return withProjectCommandContext(ctx, ptArgs, func(cmdCtx *commandCtx) error { + return cmd.runCmdPrune(ctx, cmdCtx) }) } -func (cmd *pruneCmd) runCmdPrune(ctx *commandCtx) error { - cmd2 := commands.NewPruneCommand(ctx.targetCtx.DeploymentCollection) - objects, err := cmd2.Run(ctx.ctx, ctx.targetCtx.SharedContext.K) - if err != nil { - return err - } - result, err := confirmedDeleteObjects(ctx.ctx, ctx.targetCtx.SharedContext.K, objects, cmd.DryRun, cmd.Yes) - if err != nil { - return err - } - err = outputCommandResult(cmd.OutputFormat, result) +func (cmd *pruneCmd) runCmdPrune(ctx context.Context, cmdCtx *commandCtx) error { + cmd2 := commands.NewPruneCommand(cmdCtx.targetCtx.Target.Discriminator, cmdCtx.targetCtx, true) + result := cmd2.Run(func(refs []k8s2.ObjectRef) error { + return confirmDeletion(ctx, refs, cmd.DryRun, cmd.Yes) + }) + err := outputCommandResult(ctx, cmdCtx, cmd.OutputFormatFlags, result, !cmd.DryRun || cmd.ForceWriteCommandResult) if err != nil { return err } diff --git a/cmd/kluctl/commands/cmd_render.go b/cmd/kluctl/commands/cmd_render.go index 190760e8d..5dbc57ff3 100644 --- a/cmd/kluctl/commands/cmd_render.go +++ b/cmd/kluctl/commands/cmd_render.go @@ -1,18 +1,29 @@ package commands import ( + "context" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/lib/yaml" "github.com/kluctl/kluctl/v2/cmd/kluctl/args" - "github.com/kluctl/kluctl/v2/pkg/status" "github.com/kluctl/kluctl/v2/pkg/utils" "io/ioutil" + "os" ) type renderCmd struct { args.ProjectFlags + args.KubeconfigFlags args.TargetFlags args.ArgsFlags args.ImageFlags + args.InclusionFlags + args.GitCredentials + args.HelmCredentials + args.RegistryCredentials args.RenderOutputDirFlags + args.OfflineKubernetesFlags + + PrintAll bool `group:"misc" help:"Write all rendered manifests to stdout"` } func (cmd *renderCmd) Help() string { @@ -20,24 +31,47 @@ func (cmd *renderCmd) Help() string { a temporary directory or a specified directory.` } -func (cmd *renderCmd) Run() error { +func (cmd *renderCmd) Run(ctx context.Context) error { + isTmp := false if cmd.RenderOutputDir == "" { - p, err := ioutil.TempDir(utils.GetTmpBaseDir(), "rendered-") + p, err := ioutil.TempDir(utils.GetTmpBaseDir(ctx), "rendered-") if err != nil { return err } cmd.RenderOutputDir = p + isTmp = true } ptArgs := projectTargetCommandArgs{ projectFlags: cmd.ProjectFlags, + kubeconfigFlags: cmd.KubeconfigFlags, targetFlags: cmd.TargetFlags, argsFlags: cmd.ArgsFlags, imageFlags: cmd.ImageFlags, + inclusionFlags: cmd.InclusionFlags, + gitCredentials: cmd.GitCredentials, + helmCredentials: cmd.HelmCredentials, + registryCredentials: cmd.RegistryCredentials, renderOutputDirFlags: cmd.RenderOutputDirFlags, + offlineKubernetes: cmd.OfflineKubernetes, + kubernetesVersion: cmd.KubernetesVersion, } - return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error { - status.Info(ctx.ctx, "Rendered into %s", ctx.targetCtx.SharedContext.RenderDir) + return withProjectCommandContext(ctx, ptArgs, func(cmdCtx *commandCtx) error { + if cmd.PrintAll { + var all []any + for _, d := range cmdCtx.targetCtx.DeploymentCollection.Deployments { + for _, o := range d.Objects { + all = append(all, o) + } + } + if isTmp { + defer os.RemoveAll(cmd.RenderOutputDir) + } + status.Flush(ctx) + return yaml.WriteYamlAllStream(getStdout(ctx), all) + } else { + status.Infof(ctx, "Rendered into %s", cmdCtx.targetCtx.SharedContext.RenderDir) + } return nil }) } diff --git a/cmd/kluctl/commands/cmd_seal.go b/cmd/kluctl/commands/cmd_seal.go deleted file mode 100644 index 65fe92997..000000000 --- a/cmd/kluctl/commands/cmd_seal.go +++ /dev/null @@ -1,141 +0,0 @@ -package commands - -import ( - "context" - "fmt" - "github.com/kluctl/kluctl/v2/cmd/kluctl/args" - "github.com/kluctl/kluctl/v2/pkg/deployment/commands" - "github.com/kluctl/kluctl/v2/pkg/kluctl_project" - "github.com/kluctl/kluctl/v2/pkg/seal" - "github.com/kluctl/kluctl/v2/pkg/status" -) - -type sealCmd struct { - args.ProjectFlags - args.TargetFlags - - ForceReseal bool `group:"misc" help:"Lets kluctl ignore secret hashes found in already sealed secrets and thus forces resealing of those."` -} - -func (cmd *sealCmd) Help() string { - return `Loads all secrets from the specified secrets sets from the target's sealingConfig and -then renders the target, including all files with the '.sealme' extension. Then runs -kubeseal on each '.sealme' file and stores secrets in the directory specified by -'--local-sealed-secrets', using the outputPattern from your deployment project. - -If no '--target' is specified, sealing is performed for all targets.` -} - -func (cmd *sealCmd) runCmdSealForTarget(ctx context.Context, p *kluctl_project.LoadedKluctlProject, targetName string) error { - s := status.Start(ctx, "%s: Sealing for target", targetName) - defer s.FailedWithMessage("%s: Sealing failed", targetName) - - doFail := func(err error) error { - s.FailedWithMessage(fmt.Sprintf("Sealing failed: %v", err)) - return err - } - - ptArgs := projectTargetCommandArgs{ - projectFlags: cmd.ProjectFlags, - targetFlags: cmd.TargetFlags, - forSeal: true, - } - ptArgs.targetFlags.Target = targetName - - // pass forSeal=True so that .sealme files are rendered as well - return withProjectTargetCommandContext(ctx, ptArgs, p, func(ctx *commandCtx) error { - err := ctx.targetCtx.DeploymentCollection.RenderDeployments() - if err != nil { - return doFail(err) - } - - sealedSecretsNamespace := "kube-system" - sealedSecretsControllerName := "sealed-secrets-controller" - if p.Config.SecretsConfig != nil && p.Config.SecretsConfig.SealedSecrets != nil { - if p.Config.SecretsConfig.SealedSecrets.Namespace != nil { - sealedSecretsNamespace = *p.Config.SecretsConfig.SealedSecrets.Namespace - } - if p.Config.SecretsConfig.SealedSecrets.ControllerName != nil { - sealedSecretsControllerName = *p.Config.SecretsConfig.SealedSecrets.ControllerName - } - } - if p.Config.SecretsConfig == nil || p.Config.SecretsConfig.SealedSecrets == nil || p.Config.SecretsConfig.SealedSecrets.Bootstrap == nil || *p.Config.SecretsConfig.SealedSecrets.Bootstrap { - err = seal.BootstrapSealedSecrets(ctx.ctx, ctx.targetCtx.SharedContext.K, sealedSecretsNamespace) - if err != nil { - return doFail(err) - } - } - - sealer, err := seal.NewSealer(ctx.ctx, ctx.targetCtx.SharedContext.K, sealedSecretsNamespace, sealedSecretsControllerName, cmd.ForceReseal) - if err != nil { - return doFail(err) - } - - outputPattern := targetName - if ctx.targetCtx.DeploymentProject.Config.SealedSecrets != nil && ctx.targetCtx.DeploymentProject.Config.SealedSecrets.OutputPattern != nil { - // the outputPattern is rendered already at this point, meaning that for example - // '{{ cluster.name }}/{{ target.name }}' will already be rendered to 'my-cluster/my-target' - outputPattern = *ctx.targetCtx.DeploymentProject.Config.SealedSecrets.OutputPattern - } - - cmd2 := commands.NewSealCommand(ctx.targetCtx.DeploymentCollection, outputPattern, ctx.targetCtx.SharedContext.RenderDir, ctx.targetCtx.SharedContext.SealedSecretsDir) - err = cmd2.Run(sealer) - - if err != nil { - return doFail(err) - } - s.Success() - return nil - }) -} - -func (cmd *sealCmd) Run() error { - return withKluctlProjectFromArgs(cmd.ProjectFlags, true, false, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { - hadError := false - - baseTargets := make(map[string]bool) - noTargetMatch := true - for _, target := range p.DynamicTargets { - if cmd.Target != "" && cmd.Target != target.Target.Name { - continue - } - if cmd.Cluster != "" && target.Target.Cluster != nil && cmd.Cluster != *target.Target.Cluster { - continue - } - if target.Target.SealingConfig == nil { - status.Info(ctx, "Target %s has no sealingConfig", target.Target.Name) - continue - } - noTargetMatch = false - - sealTarget := target.Target - dynamicSealing := target.Target.SealingConfig.DynamicSealing == nil || *target.Target.SealingConfig.DynamicSealing - isDynamicTarget := target.BaseTargetName != target.Target.Name - if !dynamicSealing && isDynamicTarget { - baseTarget, err := p.FindBaseTarget(target.BaseTargetName) - if err != nil { - return err - } - if baseTargets[target.BaseTargetName] { - // Skip this target as it was already sealed - continue - } - baseTargets[target.BaseTargetName] = true - sealTarget = baseTarget - } - - err := cmd.runCmdSealForTarget(ctx, p, sealTarget.Name) - if err != nil { - hadError = true - status.Error(ctx, err.Error()) - } - } - if hadError { - return fmt.Errorf("sealing for at least one target failed") - } - if noTargetMatch { - return fmt.Errorf("no target matched the given target and/or cluster name") - } - return nil - }) -} diff --git a/cmd/kluctl/commands/cmd_validate.go b/cmd/kluctl/commands/cmd_validate.go index b34d90a0d..dfb7351ee 100644 --- a/cmd/kluctl/commands/cmd_validate.go +++ b/cmd/kluctl/commands/cmd_validate.go @@ -1,18 +1,23 @@ package commands import ( + "context" "fmt" + "github.com/kluctl/kluctl/lib/status" "github.com/kluctl/kluctl/v2/cmd/kluctl/args" "github.com/kluctl/kluctl/v2/pkg/deployment/commands" - "os" "time" ) type validateCmd struct { args.ProjectFlags + args.KubeconfigFlags args.TargetFlags args.ArgsFlags args.InclusionFlags + args.GitCredentials + args.HelmCredentials + args.RegistryCredentials args.OutputFlags args.RenderOutputDirFlags @@ -27,51 +32,57 @@ func (cmd *validateCmd) Help() string { TODO: This needs to be better documented!` } -func (cmd *validateCmd) Run() error { +func (cmd *validateCmd) Run(ctx context.Context) error { ptArgs := projectTargetCommandArgs{ projectFlags: cmd.ProjectFlags, + kubeconfigFlags: cmd.KubeconfigFlags, targetFlags: cmd.TargetFlags, argsFlags: cmd.ArgsFlags, inclusionFlags: cmd.InclusionFlags, + gitCredentials: cmd.GitCredentials, + helmCredentials: cmd.HelmCredentials, + registryCredentials: cmd.RegistryCredentials, renderOutputDirFlags: cmd.RenderOutputDirFlags, } - return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error { - startTime := time.Now() - cmd2 := commands.NewValidateCommand(ctx.ctx, ctx.targetCtx.DeploymentCollection) - for true { - result, err := cmd2.Run(ctx.ctx, ctx.targetCtx.SharedContext.K) - if err != nil { - return err - } - failed := len(result.Errors) != 0 || (cmd.WarningsAsErrors && len(result.Warnings) != 0) - err = outputValidateResult(cmd.Output, result) - if err != nil { - return err - } + return withProjectCommandContext(ctx, ptArgs, func(cmdCtx *commandCtx) error { + cmd2 := commands.NewValidateCommand("", cmdCtx.targetCtx) + return cmd.doValidate(ctx, cmdCtx, cmd2) + }) +} - if !failed { - _, _ = os.Stderr.WriteString("Validation succeeded\n") - return nil - } +func (cmd *validateCmd) doValidate(ctx context.Context, cmdCtx *commandCtx, cmd2 *commands.ValidateCommand) error { + startTime := time.Now() + for true { + result := cmd2.Run(ctx) + failed := len(result.Errors) != 0 || (cmd.WarningsAsErrors && len(result.Warnings) != 0) - if cmd.Wait <= 0 || time.Now().Sub(startTime) > cmd.Wait { - return fmt.Errorf("Validation failed") - } + err := outputValidateResult(ctx, cmdCtx, cmd.Output, result) + if err != nil { + return err + } - time.Sleep(cmd.Sleep) + if !failed { + status.Info(ctx, "Validation succeeded") + return nil + } - // Need to force re-requesting these objects - for _, e := range result.Results { - cmd2.ForgetRemoteObject(e.Ref) - } - for _, e := range result.Warnings { - cmd2.ForgetRemoteObject(e.Ref) - } - for _, e := range result.Errors { - cmd2.ForgetRemoteObject(e.Ref) - } + if cmd.Wait <= 0 || time.Now().Sub(startTime) > cmd.Wait { + return fmt.Errorf("Validation failed") } - return nil - }) + + time.Sleep(cmd.Sleep) + + // Need to force re-requesting these objects + for _, e := range result.Results { + cmd2.ForgetRemoteObject(e.Ref) + } + for _, e := range result.Warnings { + cmd2.ForgetRemoteObject(e.Ref) + } + for _, e := range result.Errors { + cmd2.ForgetRemoteObject(e.Ref) + } + } + return nil } diff --git a/cmd/kluctl/commands/cmd_version.go b/cmd/kluctl/commands/cmd_version.go index 98da1b398..c8f3edf60 100644 --- a/cmd/kluctl/commands/cmd_version.go +++ b/cmd/kluctl/commands/cmd_version.go @@ -1,14 +1,16 @@ package commands import ( + "context" + "github.com/kluctl/kluctl/lib/status" "github.com/kluctl/kluctl/v2/pkg/version" - "os" ) type versionCmd struct { } -func (cmd *versionCmd) Run() error { - _, err := os.Stdout.WriteString(version.GetVersion() + "\n") +func (cmd *versionCmd) Run(ctx context.Context) error { + status.Flush(ctx) + _, err := getStdout(ctx).WriteString(version.GetVersion() + "\n") return err } diff --git a/cmd/kluctl/commands/cmd_webui.go b/cmd/kluctl/commands/cmd_webui.go new file mode 100644 index 000000000..2bf8bb9c2 --- /dev/null +++ b/cmd/kluctl/commands/cmd_webui.go @@ -0,0 +1,91 @@ +package commands + +import ( + "context" + "fmt" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/k8s" + "github.com/kluctl/kluctl/v2/pkg/results" + "github.com/kluctl/kluctl/v2/pkg/utils" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +type webuiCmd struct { + Run_ webuiRunCmd `cmd:"run" help:"Run the Kluctl Webui"` + Build webuiBuildCmd `cmd:"build" help:"Build the static Kluctl Webui"` +} + +func createResultStores(ctx context.Context, kubeconfigOverride string, k8sContexts []string, allContexts bool, inCluster bool) ([]results.ResultStore, []*rest.Config, error) { + r := clientcmd.NewDefaultClientConfigLoadingRules() + r.ExplicitPath = kubeconfigOverride + + kcfg, err := r.Load() + if err != nil { + return nil, nil, err + } + + var contexts []string + if allContexts { + for name, _ := range kcfg.Contexts { + contexts = append(contexts, name) + } + } else if inCluster { + // placeholder for current context + contexts = append(contexts, "") + } else { + if len(k8sContexts) == 0 { + // placeholder for current context + contexts = append(contexts, "") + } + for _, c := range k8sContexts { + found := false + for name, _ := range kcfg.Contexts { + if c == name { + contexts = append(contexts, name) + found = true + break + } + } + if !found { + return nil, nil, fmt.Errorf("context '%s' not found in kubeconfig", c) + } + } + } + + gh := utils.NewGoHelper(ctx, 4) + stores := make([]results.ResultStore, len(contexts)) + configs := make([]*rest.Config, len(contexts)) + for i, c := range contexts { + i := i + c := c + gh.RunE(func() error { + configOverrides := &clientcmd.ConfigOverrides{ + CurrentContext: c, + } + config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(r, configOverrides).ClientConfig() + if err != nil { + return err + } + + _, mapper, err := k8s.CreateDiscoveryAndMapper(ctx, config) + if err != nil { + return err + } + + store, err := buildResultStoreRO(ctx, config, mapper, &args.CommandResultReadOnlyFlags{}) + if err != nil { + return err + } + stores[i] = store + configs[i] = config + return nil + }) + } + gh.Wait() + if gh.ErrorOrNil() != nil { + return nil, nil, gh.ErrorOrNil() + } + + return stores, configs, nil +} diff --git a/cmd/kluctl/commands/cmd_webui_build.go b/cmd/kluctl/commands/cmd_webui_build.go new file mode 100644 index 000000000..d45714b60 --- /dev/null +++ b/cmd/kluctl/commands/cmd_webui_build.go @@ -0,0 +1,47 @@ +package commands + +import ( + "context" + "fmt" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/v2/pkg/results" + "github.com/kluctl/kluctl/v2/pkg/webui" + "time" +) + +type webuiBuildCmd struct { + Path string `group:"misc" help:"Output path." required:"true"` + Context []string `group:"misc" help:"List of kubernetes contexts to use. Defaults to the current context."` + AllContexts bool `group:"misc" help:"Use all Kubernetes contexts found in the kubeconfig."` + MaxResults int `group:"misc" help:"Specify the maximum number of results per target." default:"1"` +} + +func (cmd *webuiBuildCmd) Help() string { + return `This command will build the static Kluctl Webui. +` +} + +func (cmd *webuiBuildCmd) Run(ctx context.Context) error { + if !webui.IsWebUiBuildIncluded() { + return fmt.Errorf("this build of Kluctl does not have the webui embedded") + } + + stores, _, err := createResultStores(ctx, "", cmd.Context, cmd.AllContexts, false) + if err != nil { + return err + } + + collector := results.NewResultsCollector(ctx, stores) + collector.Start() + + st := status.Start(ctx, "Collecting summaries") + defer st.Failed() + err = collector.WaitForResults(time.Second, time.Second*30) + if err != nil { + return err + } + st.Success() + + sbw := webui.NewStaticWebuiBuilder(collector, cmd.MaxResults) + return sbw.Build(ctx, cmd.Path) +} diff --git a/cmd/kluctl/commands/cmd_webui_run.go b/cmd/kluctl/commands/cmd_webui_run.go new file mode 100644 index 000000000..0c6da2981 --- /dev/null +++ b/cmd/kluctl/commands/cmd_webui_run.go @@ -0,0 +1,163 @@ +package commands + +import ( + "context" + "fmt" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/results" + "github.com/kluctl/kluctl/v2/pkg/webui" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "net/netip" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type webuiRunCmd struct { + Host string `group:"misc" help:"Host to bind to. Pass an empty string to bind to all addresses. Defaults to 'localhost' when run locally and to all hosts when run in-cluster."` + Port int `group:"misc" help:"Port to bind to." default:"8080"` + PathPrefix string `group:"misc" help:"Specify the prefix of the path to serve the webui on. This is required when using a reverse proxy, ingress or gateway that serves the webui on another path than /." default:"/"` + + Kubeconfig args.ExistingFileType `group:"misc" help:"Overrides the kubeconfig to use."` + Context []string `group:"misc" help:"List of kubernetes contexts to use."` + AllContexts bool `group:"misc" help:"Use all Kubernetes contexts found in the kubeconfig."` + + InCluster bool `group:"misc" help:"This enables in-cluster functionality. This also enforces authentication."` + InClusterContext string `group:"misc" help:"The context to use fo in-cluster functionality."` + ControllerNamespace string `group:"misc" help:"The namespace where the controller runs in." default:"kluctl-system"` + + OnlyApi bool `group:"misc" help:"Only serve API without the actual UI."` + + AuthSecretName string `group:"auth" help:"Specify the secret name for the secret used for internal encryption of tokens and cookies." default:"webui-secret"` + AuthSecretKey string `group:"auth" help:"Specify the secret key for the secret used for internal encryption of tokens and cookies." default:"auth-secret"` + + AuthStaticLoginEnabled bool `group:"auth" help:"Enable the admin user." default:"true"` + AuthStaticLoginSecretName string `group:"auth" help:"Specify the secret name for the admin and viewer passwords." default:"webui-secret"` + AuthStaticAdminSecretKey string `group:"auth" help:"Specify the secret key for the admin password." default:"admin-password"` + AuthStaticViewerSecretKey string `group:"auth" help:"Specify the secret key for the viewer password." default:"viewer-password"` + + AuthAdminRbacUser string `group:"auth" help:"Specify the RBAC user to use for admin access." default:"kluctl-webui-admin"` + AuthViewerRbacUser string `group:"auth" help:"Specify the RBAC user to use for viewer access." default:"kluctl-webui-viewer"` + + AuthOidcIssuerUrl string `group:"auth" help:"Specify the OIDC provider's issuer URL."` + AuthOidcDisplayName string `group:"auth" help:"Specify the name of the OIDC provider to be displayed on the login page." default:"OpenID Connect"` + AuthOidcClientID string `group:"auth" help:"Specify the ClientID."` + AuthOidcClientSecretName string `group:"auth" help:"Specify the secret name for the ClientSecret." default:"webui-secret"` + AuthOidcClientSecretKey string `group:"auth" help:"Specify the secret name for the ClientSecret." default:"oidc-client-secret"` + AuthOidcRedirectURL string `group:"auth" help:"Specify the redirect URL."` + AuthOidcScope []string `group:"auth" help:"Specify the scopes."` + AuthOidcParam []string `group:"auth" help:"Specify additional parameters to be passed to the authorize endpoint."` + AuthOidcUserClaim string `group:"auth" help:"Specify claim for the username.'" default:"email"` + AuthOidcGroupClaim string `group:"auth" help:"Specify claim for the groups.'" default:"groups"` + AuthOidcAdminsGroup []string `group:"auth" help:"Specify admins group names.'"` + AuthOidcViewersGroup []string `group:"auth" help:"Specify viewers group names.'"` + + AuthLogoutURL string `group:"auth" help:"Specify the logout URL, to which the user should be redirected after clearing the Kluctl Webui session."` + AuthLogoutReturnParam string `group:"auth" help:"Specify the parameter name to pass to the logout redirect url, containing the return URL to redirect back."` +} + +func (cmd *webuiRunCmd) Help() string { + return `This command will run the Kluctl Webui. It can be run in two modes: +1. Locally, simply by invoking 'kluctl webui', which will run the Webui against the current Kubernetes context. +2. On a Kubernetes Cluster. See https://kluctl.io/docs/kluctl/installation/#installing-the-kluctl-webui for details and documentation. +` +} + +func (cmd *webuiRunCmd) buildAuthConfig(ctx context.Context, c client.Client) (webui.AuthConfig, error) { + var authConfig webui.AuthConfig + authConfig.AuthEnabled = cmd.InCluster + + authConfig.AuthSecretName = cmd.AuthSecretName + authConfig.AuthSecretKey = cmd.AuthSecretKey + + authConfig.StaticLoginEnabled = cmd.AuthStaticLoginEnabled + authConfig.StaticLoginSecretName = cmd.AuthStaticLoginSecretName + authConfig.StaticAdminSecretKey = cmd.AuthStaticAdminSecretKey + authConfig.StaticViewerSecretKey = cmd.AuthStaticViewerSecretKey + + authConfig.AdminRbacUser = cmd.AuthAdminRbacUser + authConfig.ViewerRbacUser = cmd.AuthViewerRbacUser + + authConfig.OidcIssuerUrl = cmd.AuthOidcIssuerUrl + authConfig.OidcDisplayName = cmd.AuthOidcDisplayName + if cmd.AuthOidcIssuerUrl != "" { + authConfig.OidcClientId = cmd.AuthOidcClientID + authConfig.OidcClientSecretName = cmd.AuthOidcClientSecretName + authConfig.OidcClientSecretKey = cmd.AuthOidcClientSecretKey + authConfig.OidcRedirectUrl = cmd.AuthOidcRedirectURL + authConfig.OidcScopes = cmd.AuthOidcScope + authConfig.OidcParams = cmd.AuthOidcParam + authConfig.OidcUserClaim = cmd.AuthOidcUserClaim + authConfig.OidcGroupClaim = cmd.AuthOidcGroupClaim + authConfig.OidcAdminsGroups = cmd.AuthOidcAdminsGroup + authConfig.OidcViewersGroups = cmd.AuthOidcViewersGroup + authConfig.LogoutUrl = cmd.AuthLogoutURL + authConfig.LogoutReturnParam = cmd.AuthLogoutReturnParam + } + + return authConfig, nil +} + +func (cmd *webuiRunCmd) Run(ctx context.Context) error { + if !cmd.OnlyApi && !webui.IsWebUiBuildIncluded() { + return fmt.Errorf("this build of Kluctl does not have the webui embedded") + } + + if !cmd.InCluster { // no authentication? + if cmd.Host == "" { + // we only use "localhost" as default if not running inside a cluster + cmd.Host = "localhost" + } + if cmd.Host != "localhost" { + isNonLocal := cmd.Host == "" + if a, err := netip.ParseAddr(cmd.Host); err == nil { // on error, we assume it's a hostname + if !a.IsLoopback() { + isNonLocal = true + } + } + if isNonLocal { + status.Warning(ctx, "When running the webui without authentication enabled, it is extremely dangerous to expose it to non-localhost addresses, as the webui is running with admin privileges.") + } + } + } + + var authConfig webui.AuthConfig + + var inClusterConfig *rest.Config + var inClusterClient client.Client + if cmd.InCluster { + configOverrides := &clientcmd.ConfigOverrides{ + CurrentContext: cmd.InClusterContext, + } + var err error + inClusterConfig, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), + configOverrides).ClientConfig() + if err != nil { + return err + } + inClusterClient, err = client.NewWithWatch(inClusterConfig, client.Options{}) + if err != nil { + return err + } + + authConfig, err = cmd.buildAuthConfig(ctx, inClusterClient) + if err != nil { + return err + } + } + + stores, configs, err := createResultStores(ctx, cmd.Kubeconfig.String(), cmd.Context, cmd.AllContexts, cmd.InCluster) + if err != nil { + return err + } + + collector := results.NewResultsCollector(ctx, stores) + collector.Start() + + server, err := webui.NewCommandResultsServer(ctx, collector, configs, cmd.ControllerNamespace, inClusterConfig, inClusterClient, authConfig, cmd.PathPrefix, cmd.OnlyApi) + if err != nil { + return err + } + return server.Run(cmd.Host, cmd.Port, isTerminal) +} diff --git a/cmd/kluctl/commands/cobra_utils.go b/cmd/kluctl/commands/cobra_utils.go index b8fae0c44..a09ec2f3c 100644 --- a/cmd/kluctl/commands/cobra_utils.go +++ b/cmd/kluctl/commands/cobra_utils.go @@ -1,8 +1,11 @@ package commands import ( + "context" "fmt" - "github.com/kluctl/kluctl/v2/pkg/utils" + "github.com/hashicorp/go-multierror" + "github.com/kluctl/kluctl/lib/envutils" + "github.com/kluctl/kluctl/lib/term" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -19,7 +22,7 @@ type helpProvider interface { } type runProvider interface { - Run() error + Run(ctx context.Context) error } type rootCommand struct { @@ -65,18 +68,17 @@ func (c *rootCommand) buildCobraCmd(parent *commandAndGroups, cmdStruct interfac } runP, ok := cmdStruct.(runProvider) - if !ok { - panic(fmt.Sprintf("%s does not implement runProvider", name)) - } - cg.cmd.RunE = func(cmd *cobra.Command, args []string) error { - return runP.Run() + if ok { + cg.cmd.RunE = func(cmd *cobra.Command, args []string) error { + return runP.Run(cmd.Context()) + } } err := c.buildCobraSubCommands(cg, cmdStruct) if err != nil { return nil, err } - err = c.buildCobraArgs(cg, cmdStruct) + err = c.buildCobraArgs(cg, cmdStruct, "") if err != nil { return nil, err } @@ -87,7 +89,7 @@ func (c *rootCommand) buildCobraCmd(parent *commandAndGroups, cmdStruct interfac } cg.cmd.SetHelpFunc(func(command *cobra.Command, i []string) { - c.helpFunc(cg) + c.helpFunc(cg, command) }) return cg, nil @@ -98,10 +100,16 @@ func (c *rootCommand) buildCobraSubCommands(cg *commandAndGroups, cmdStruct inte t := v.Type() for i := 0; i < t.NumField(); i++ { f := t.Field(i) + if !f.IsExported() { + continue + } v2 := v.Field(i).Addr().Interface() name := buildCobraName(f.Name) - if _, ok := f.Tag.Lookup("cmd"); ok { + if cmd, ok := f.Tag.Lookup("cmd"); ok { + if cmd != "" { + name = cmd + } // subcommand shortHelp := f.Tag.Get("help") longHelp := "" @@ -119,16 +127,24 @@ func (c *rootCommand) buildCobraSubCommands(cg *commandAndGroups, cmdStruct inte return nil } -func (c *rootCommand) buildCobraArgs(cg *commandAndGroups, cmdStruct interface{}) error { +func (c *rootCommand) buildCobraArgs(cg *commandAndGroups, cmdStruct interface{}, groupOverride string) error { v := reflect.ValueOf(cmdStruct).Elem() t := v.Type() for i := 0; i < t.NumField(); i++ { f := t.Field(i) + if !f.IsExported() { + continue + } if _, ok := f.Tag.Lookup("cmd"); ok { continue } - err := c.buildCobraArg(cg, f, v.Field(i)) + groupOverride2, _ := f.Tag.Lookup("groupOverride") + if groupOverride2 == "" { + groupOverride2 = groupOverride + } + + err := c.buildCobraArg(cg, f, v.Field(i), groupOverride2) if err != nil { return err } @@ -136,7 +152,7 @@ func (c *rootCommand) buildCobraArgs(cg *commandAndGroups, cmdStruct interface{} return nil } -func (c *rootCommand) buildCobraArg(cg *commandAndGroups, f reflect.StructField, v reflect.Value) error { +func (c *rootCommand) buildCobraArg(cg *commandAndGroups, f reflect.StructField, v reflect.Value, groupOverride string) error { v2 := v.Addr().Interface() name := buildCobraName(f.Name) @@ -144,8 +160,12 @@ func (c *rootCommand) buildCobraArg(cg *commandAndGroups, f reflect.StructField, shortFlag := f.Tag.Get("short") defaultValue := f.Tag.Get("default") required := f.Tag.Get("required") == "true" + skipEnv := f.Tag.Get("skipenv") - group := f.Tag.Get("group") + group := groupOverride + if group == "" { + group = f.Tag.Get("group") + } if group != "" { cg.groups[name] = group } @@ -171,7 +191,7 @@ func (c *rootCommand) buildCobraArg(cg *commandAndGroups, f reflect.StructField, if defaultValue != "" { return fmt.Errorf("default not supported for slices") } - cg.cmd.PersistentFlags().StringSliceVarP(v2.(*[]string), name, shortFlag, nil, help) + cg.cmd.PersistentFlags().StringArrayVarP(v2.(*[]string), name, shortFlag, nil, help) case *bool: parsedDefault := false if defaultValue != "" { @@ -204,7 +224,7 @@ func (c *rootCommand) buildCobraArg(cg *commandAndGroups, f reflect.StructField, cg.cmd.PersistentFlags().DurationVarP(v2.(*time.Duration), name, shortFlag, parsedDefault, help) default: if f.Anonymous { - return c.buildCobraArgs(cg, v2) + return c.buildCobraArgs(cg, v2, groupOverride) } return fmt.Errorf("unknown type %s", f.Type.Name()) } @@ -213,6 +233,8 @@ func (c *rootCommand) buildCobraArg(cg *commandAndGroups, f reflect.StructField, _ = cg.cmd.MarkPersistentFlagRequired(name) } + _ = cg.cmd.PersistentFlags().SetAnnotation(name, "skipenv", []string{skipEnv}) + return nil } @@ -228,31 +250,70 @@ func copyViperValuesToCobraCmd(cmd *cobra.Command) error { } func copyViperValuesToCobraFlags(flags *pflag.FlagSet) error { - var retErr []error + var errs *multierror.Error flags.VisitAll(func(flag *pflag.Flag) { - if flag.Changed { + if a := flag.Annotations["skipenv"]; len(a) != 0 && a[0] == "true" { + return + } + + sliceValue, _ := flag.Value.(pflag.SliceValue) + if flag.Changed && sliceValue == nil { return } v := viper.Get(flag.Name) + + var a []string if v != nil { - s, ok := v.(string) - if !ok { - retErr = append(retErr, fmt.Errorf("viper flag %s is not a string", flag.Name)) + if x, ok := v.(string); ok { + a = []string{x} + } else if x, ok := v.(bool); ok { + a = []string{strconv.FormatBool(x)} + } else if x, ok := v.(int); ok { + a = []string{strconv.FormatInt(int64(x), 10)} + } else if x, ok := v.([]any); ok { + for _, y := range x { + s, ok := y.(string) + if !ok { + errs = multierror.Append(errs, fmt.Errorf("viper flag %s has unexpected type", flag.Name)) + return + } + a = append(a, s) + } + } else { + errs = multierror.Append(errs, fmt.Errorf("viper flag %s has unexpected type", flag.Name)) return } - err := flag.Value.Set(s) + } + + envName := strings.ReplaceAll(flag.Name, "-", "_") + envName = strings.ToUpper(envName) + envName = fmt.Sprintf("KLUCTL_%s", envName) + + for _, v := range envutils.ParseEnvConfigList(envName) { + a = append(a, v) + } + + if sliceValue != nil { + // we must ensure that values passed via CLI are at the end of the slice + a = append(a, sliceValue.GetSlice()...) + err := sliceValue.Replace(a) if err != nil { - retErr = append(retErr, err) + errs = multierror.Append(errs, err) + } + } else { + for _, x := range a { + err := flag.Value.Set(x) + if err != nil { + errs = multierror.Append(errs, err) + } } } }) - return utils.NewErrorListOrNil(retErr) + return errs.ErrorOrNil() } -func (c *rootCommand) helpFunc(cg *commandAndGroups) { - cmd := cg.cmd - - termWidth := utils.GetTermWidth() +func (c *rootCommand) helpFunc(cg *commandAndGroups, cmd *cobra.Command) { + termWidth := term.GetWidth() h := "Usage: " if cmd.Runnable() { diff --git a/cmd/kluctl/commands/command_result.go b/cmd/kluctl/commands/command_result.go index 668e21ec2..debd70dc4 100644 --- a/cmd/kluctl/commands/command_result.go +++ b/cmd/kluctl/commands/command_result.go @@ -2,63 +2,86 @@ package commands import ( "bytes" + "context" "fmt" - "github.com/kluctl/kluctl/v2/pkg/status" - "github.com/kluctl/kluctl/v2/pkg/types" + "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/lib/yaml" + "github.com/kluctl/kluctl/v2/cmd/kluctl/args" + "github.com/kluctl/kluctl/v2/pkg/diff" "github.com/kluctl/kluctl/v2/pkg/types/k8s" + "github.com/kluctl/kluctl/v2/pkg/types/result" "github.com/kluctl/kluctl/v2/pkg/utils" - "github.com/kluctl/kluctl/v2/pkg/yaml" "io" "os" "strings" ) -func formatCommandResultText(cr *types.CommandResult) string { +func formatCommandResultText(cr *result.CommandResult, short bool) string { buf := bytes.NewBuffer(nil) - if len(cr.Warnings) != 0 { - buf.WriteString("\nWarnings:\n") - prettyErrors(buf, cr.Warnings) + var newObjects []k8s.ObjectRef + var changedObjects []k8s.ObjectRef + var deletedObjects []k8s.ObjectRef + var orphanObjects []k8s.ObjectRef + var appliedHookObjects []k8s.ObjectRef + + for _, o := range cr.Objects { + if o.New { + newObjects = append(newObjects, o.Ref) + } + if len(o.Changes) != 0 { + changedObjects = append(changedObjects, o.Ref) + } + if o.Deleted { + deletedObjects = append(deletedObjects, o.Ref) + } + if o.Orphan { + orphanObjects = append(orphanObjects, o.Ref) + } + if o.Hook { + appliedHookObjects = append(appliedHookObjects, o.Ref) + } } - if len(cr.NewObjects) != 0 { + if len(newObjects) != 0 { buf.WriteString("\nNew objects:\n") - var refs []k8s.ObjectRef - for _, o := range cr.NewObjects { - refs = append(refs, o.Ref) - } - prettyObjectRefs(buf, refs) + prettyObjectRefs(buf, newObjects) } - if len(cr.ChangedObjects) != 0 { + if len(changedObjects) != 0 { buf.WriteString("\nChanged objects:\n") - var refs []k8s.ObjectRef - for _, co := range cr.ChangedObjects { - refs = append(refs, co.Ref) - } - prettyObjectRefs(buf, refs) + prettyObjectRefs(buf, changedObjects) - buf.WriteString("\n") - for _, co := range cr.ChangedObjects { - prettyChanges(buf, co.Ref, co.Changes) + if !short { + buf.WriteString("\n") + for i, o := range cr.Objects { + if len(o.Changes) == 0 { + continue + } + if i != 0 { + buf.WriteString("\n") + } + prettyChanges(buf, o.Ref, o.Changes) + } } } - if len(cr.DeletedObjects) != 0 { + if len(deletedObjects) != 0 { buf.WriteString("\nDeleted objects:\n") - prettyObjectRefs(buf, cr.DeletedObjects) + prettyObjectRefs(buf, deletedObjects) } - if len(cr.HookObjects) != 0 { + if len(appliedHookObjects) != 0 { buf.WriteString("\nApplied hooks:\n") - var refs []k8s.ObjectRef - for _, o := range cr.HookObjects { - refs = append(refs, o.Ref) - } - prettyObjectRefs(buf, refs) + prettyObjectRefs(buf, appliedHookObjects) } - if len(cr.OrphanObjects) != 0 { + if len(orphanObjects) != 0 { buf.WriteString("\nOrphan objects:\n") - prettyObjectRefs(buf, cr.OrphanObjects) + prettyObjectRefs(buf, orphanObjects) + } + + if len(cr.Warnings) != 0 { + buf.WriteString("\nWarnings:\n") + prettyErrors(buf, cr.Warnings) } if len(cr.Errors) != 0 { @@ -75,13 +98,17 @@ func prettyObjectRefs(buf io.StringWriter, refs []k8s.ObjectRef) { } } -func prettyErrors(buf io.StringWriter, errors []types.DeploymentError) { +func prettyErrors(buf io.StringWriter, errors []result.DeploymentError) { for _, e := range errors { - _, _ = buf.WriteString(fmt.Sprintf(" %s: %s\n", e.Ref.String(), e.Error)) + prefix := "" + if s := e.Ref.String(); s != "" { + prefix = fmt.Sprintf("%s: ", s) + } + _, _ = buf.WriteString(fmt.Sprintf(" %s%s\n", prefix, e.Message)) } } -func prettyChanges(buf io.StringWriter, ref k8s.ObjectRef, changes []types.Change) { +func prettyChanges(buf io.StringWriter, ref k8s.ObjectRef, changes []result.Change) { _, _ = buf.WriteString(fmt.Sprintf("Diff for object %s\n", ref.String())) var t utils.PrettyTable @@ -94,18 +121,18 @@ func prettyChanges(buf io.StringWriter, ref k8s.ObjectRef, changes []types.Chang _, _ = buf.WriteString(s) } -func formatCommandResultYaml(cr *types.CommandResult) (string, error) { - b, err := yaml.WriteYamlString(cr) +func formatCommandResultYaml(cr *result.CommandResult) (string, error) { + b, err := yaml.WriteYamlString(cr.ToCompacted()) if err != nil { return "", err } return b, nil } -func formatCommandResult(cr *types.CommandResult, format string) (string, error) { +func formatCommandResult(cr *result.CommandResult, format string, short bool) (string, error) { switch format { case "text": - return formatCommandResultText(cr), nil + return formatCommandResultText(cr, short), nil case "yaml": return formatCommandResultYaml(cr) default: @@ -113,7 +140,7 @@ func formatCommandResult(cr *types.CommandResult, format string) (string, error) } } -func prettyValidationResults(buf io.StringWriter, results []types.ValidateResultEntry) { +func prettyValidationResults(buf io.StringWriter, results []result.ValidateResultEntry) { var t utils.PrettyTable t.AddRow("Object", "Message") @@ -124,7 +151,7 @@ func prettyValidationResults(buf io.StringWriter, results []types.ValidateResult _, _ = buf.WriteString(s) } -func formatValidateResultText(vr *types.ValidateResult) string { +func formatValidateResultText(vr *result.ValidateResult) string { buf := bytes.NewBuffer(nil) if len(vr.Warnings) != 0 { @@ -147,10 +174,11 @@ func formatValidateResultText(vr *types.ValidateResult) string { buf.WriteString("Results:\n") prettyValidationResults(buf, vr.Results) } + return buf.String() } -func formatValidateResultYaml(vr *types.ValidateResult) (string, error) { +func formatValidateResultYaml(vr *result.ValidateResult) (string, error) { b, err := yaml.WriteYamlString(vr) if err != nil { return "", err @@ -158,7 +186,7 @@ func formatValidateResultYaml(vr *types.ValidateResult) (string, error) { return string(b), nil } -func formatValidateResult(vr *types.ValidateResult, format string) (string, error) { +func formatValidateResult(vr *result.ValidateResult, format string) (string, error) { switch format { case "text": return formatValidateResultText(vr), nil @@ -169,7 +197,7 @@ func formatValidateResult(vr *types.ValidateResult, format string) (string, erro } } -func outputHelper(output []string, cb func(format string) (string, error)) error { +func outputHelper(ctx context.Context, output []string, cb func(format string) (string, error)) error { if len(output) == 0 { output = []string{"text"} } @@ -185,7 +213,7 @@ func outputHelper(output []string, cb func(format string) (string, error)) error return err } - err = outputResult(path, r) + err = outputResult(ctx, path, r) if err != nil { return err } @@ -193,28 +221,79 @@ func outputHelper(output []string, cb func(format string) (string, error)) error return nil } -func outputCommandResult(output []string, cr *types.CommandResult) error { - status.Flush(cliCtx) +func outputCommandResult(ctx context.Context, cmdCtx *commandCtx, flags args.OutputFormatFlags, cr *result.CommandResult, writeToResultStore bool) error { + cr.Id = cmdCtx.resultId + cr.Command.Initiator = result.CommandInititiator_CommandLine + + if !flags.NoObfuscate { + var obfuscator diff.Obfuscator + err := obfuscator.ObfuscateResult(cr) + if err != nil { + return err + } + } + + var resultStoreErr error + if writeToResultStore && cmdCtx.resultStore != nil { + s := status.Start(ctx, "Writing command result") + defer s.Failed() + + didWarn := false + if cr.ClusterInfo.ClusterId == "" { + warning := "failed to determine cluster ID due to missing get/list permissions for the kube-system namespace. This might result in follow up issues in regard to cluster differentiation stored command results" + cr.Warnings = append(cr.Warnings, result.DeploymentError{ + Message: warning, + }) + status.Warning(ctx, warning) + didWarn = true + } + + resultStoreErr = cmdCtx.resultStore.WriteCommandResult(cr) + if resultStoreErr != nil { + s.FailedWithMessagef("Failed to write result to result store: %s", resultStoreErr.Error()) + } else { + if didWarn { + s.Warning() + } else { + s.Success() + } + } + } + err := outputCommandResult2(ctx, flags, cr) + if err == nil && resultStoreErr != nil { + return resultStoreErr + } + return err +} - return outputHelper(output, func(format string) (string, error) { - return formatCommandResult(cr, format) +func outputCommandResult2(ctx context.Context, flags args.OutputFormatFlags, cr *result.CommandResult) error { + status.Flush(ctx) + err := outputHelper(ctx, flags.OutputFormat, func(format string) (string, error) { + return formatCommandResult(cr, format, flags.ShortOutput) }) + status.Flush(ctx) + return err +} + +func outputValidateResult(ctx context.Context, cmdCtx *commandCtx, output []string, vr *result.ValidateResult) error { + vr.Id = cmdCtx.resultId + + return outputValidateResult2(ctx, output, vr) } -func outputValidateResult(output []string, vr *types.ValidateResult) error { - status.Flush(cliCtx) +func outputValidateResult2(ctx context.Context, output []string, vr *result.ValidateResult) error { + status.Flush(ctx) - return outputHelper(output, func(format string) (string, error) { + err := outputHelper(ctx, output, func(format string) (string, error) { return formatValidateResult(vr, format) }) + status.Flush(ctx) + return err } -func outputYamlResult(output []string, result interface{}, multiDoc bool) error { - status.Flush(cliCtx) +func outputYamlResult(ctx context.Context, output []string, result interface{}, multiDoc bool) error { + status.Flush(ctx) - if len(output) == 0 { - output = []string{"-"} - } var s string if multiDoc { l, ok := result.([]interface{}) @@ -233,17 +312,15 @@ func outputYamlResult(output []string, result interface{}, multiDoc bool) error } s = x } - for _, path := range output { - err := outputResult(&path, s) - if err != nil { - return err - } - } - return nil + return outputResult2(ctx, output, s) } -func outputResult(f *string, result string) error { - w := os.Stdout +func outputResult(ctx context.Context, f *string, result string) error { + // make sure there is no pending render of a status line + status.Flush(ctx) + + var w io.Writer + w = getStdout(ctx) if f != nil && *f != "-" { f, err := os.Create(*f) if err != nil { @@ -255,3 +332,16 @@ func outputResult(f *string, result string) error { _, err := w.Write([]byte(result)) return err } + +func outputResult2(ctx context.Context, output []string, result string) error { + if len(output) == 0 { + output = []string{"-"} + } + for _, o := range output { + err := outputResult(ctx, &o, result) + if err != nil { + return err + } + } + return nil +} diff --git a/cmd/kluctl/commands/completion.go b/cmd/kluctl/commands/completion.go index 3c8ae9e37..b6cca1405 100644 --- a/cmd/kluctl/commands/completion.go +++ b/cmd/kluctl/commands/completion.go @@ -2,15 +2,19 @@ package commands import ( "context" + "github.com/kluctl/kluctl/lib/status" + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" "github.com/kluctl/kluctl/v2/cmd/kluctl/args" "github.com/kluctl/kluctl/v2/pkg/kluctl_project" - "github.com/kluctl/kluctl/v2/pkg/status" - "github.com/kluctl/kluctl/v2/pkg/types" "github.com/kluctl/kluctl/v2/pkg/utils" - "github.com/kluctl/kluctl/v2/pkg/yaml" "github.com/spf13/cobra" - "os" - "path/filepath" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" "reflect" "strings" "sync" @@ -20,21 +24,25 @@ import ( func RegisterFlagCompletionFuncs(cmdStruct interface{}, ccmd *cobra.Command) error { v := reflect.ValueOf(cmdStruct).Elem() projectFlags := v.FieldByName("ProjectFlags") + argsFlags := v.FieldByName("ArgsFlags") targetFlags := v.FieldByName("TargetFlags") inclusionFlags := v.FieldByName("InclusionFlags") imageFlags := v.FieldByName("ImageFlags") + gitopsFlags := v.FieldByName("GitOpsArgs") - if projectFlags.IsValid() { - _ = ccmd.RegisterFlagCompletionFunc("cluster", buildClusterCompletionFunc(projectFlags.Addr().Interface().(*args.ProjectFlags))) - } + ctx := context.Background() if projectFlags.IsValid() && targetFlags.IsValid() { - _ = ccmd.RegisterFlagCompletionFunc("target", buildTargetCompletionFunc(projectFlags.Addr().Interface().(*args.ProjectFlags))) + var argsFlag2 *args.ArgsFlags + if argsFlags.IsValid() { + argsFlag2 = argsFlags.Addr().Interface().(*args.ArgsFlags) + } + _ = ccmd.RegisterFlagCompletionFunc("target", buildTargetCompletionFunc(ctx, projectFlags.Addr().Interface().(*args.ProjectFlags), argsFlag2)) } if projectFlags.IsValid() && inclusionFlags.IsValid() { - tagsFunc := buildInclusionCompletionFunc(cmdStruct, false) - dirsFunc := buildInclusionCompletionFunc(cmdStruct, true) + tagsFunc := buildInclusionCompletionFunc(ctx, cmdStruct, false) + dirsFunc := buildInclusionCompletionFunc(ctx, cmdStruct, true) _ = ccmd.RegisterFlagCompletionFunc("include-tag", tagsFunc) _ = ccmd.RegisterFlagCompletionFunc("exclude-tag", tagsFunc) _ = ccmd.RegisterFlagCompletionFunc("include-deployment-dir", dirsFunc) @@ -42,59 +50,41 @@ func RegisterFlagCompletionFuncs(cmdStruct interface{}, ccmd *cobra.Command) err } if imageFlags.IsValid() { - _ = ccmd.RegisterFlagCompletionFunc("fixed-image", buildImagesCompletionFunc(cmdStruct)) + _ = ccmd.RegisterFlagCompletionFunc("fixed-image", buildImagesCompletionFunc(ctx, cmdStruct)) + } + + if gitopsFlags.IsValid() { + _ = ccmd.RegisterFlagCompletionFunc("context", buildContextCompletionFunc(ctx, cmdStruct)) + _ = ccmd.RegisterFlagCompletionFunc("namespace", buildObjectNamespaceCompletionFunc(ctx, cmdStruct)) + _ = ccmd.RegisterFlagCompletionFunc("name", buildObjectNameCompletionFunc(ctx, cmdStruct, schema.GroupVersionResource{ + Group: kluctlv1.GroupVersion.Group, + Version: kluctlv1.GroupVersion.Version, + Resource: "kluctldeployments", + })) } return nil } -func withProjectForCompletion(projectArgs *args.ProjectFlags, cb func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error) error { +func withProjectForCompletion(ctx context.Context, projectArgs *args.ProjectFlags, argsFlags *args.ArgsFlags, cb func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error) error { // let's not update git caches too often projectArgs.GitCacheUpdateInterval = time.Second * 60 - return withKluctlProjectFromArgs(*projectArgs, false, true, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { + return withKluctlProjectFromArgs(ctx, nil, *projectArgs, argsFlags, nil, nil, nil, false, false, true, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { return cb(ctx, p) }) } -func buildClusterCompletionFunc(projectArgs *args.ProjectFlags) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func buildTargetCompletionFunc(ctx context.Context, projectArgs *args.ProjectFlags, argsFlags *args.ArgsFlags) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var ret []string - err := withProjectForCompletion(projectArgs, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { - dents, err := os.ReadDir(p.ClustersDir) - if err != nil { - return err - } - for _, de := range dents { - var config types.ClusterConfig - err = yaml.ReadYamlFile(filepath.Join(p.ClustersDir, de.Name()), &config) - if err != nil { - continue - } - if config.Cluster.Name != "" { - ret = append(ret, config.Cluster.Name) - } + err := withProjectForCompletion(ctx, projectArgs, argsFlags, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { + for _, t := range p.Targets { + ret = append(ret, t.Name) } return nil }) if err != nil { - status.Error(cliCtx, err.Error()) - return nil, cobra.ShellCompDirectiveError - } - return ret, cobra.ShellCompDirectiveDefault - } -} - -func buildTargetCompletionFunc(projectArgs *args.ProjectFlags) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - var ret []string - err := withProjectForCompletion(projectArgs, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { - for _, t := range p.DynamicTargets { - ret = append(ret, t.Target.Name) - } - return nil - }) - if err != nil { - status.Error(cliCtx, err.Error()) + status.Error(ctx, err.Error()) return nil, cobra.ShellCompDirectiveError } return ret, cobra.ShellCompDirectiveDefault @@ -125,19 +115,19 @@ func buildAutocompleteProjectTargetCommandArgs(cmdStruct interface{}) projectTar return ptArgs } -func buildInclusionCompletionFunc(cmdStruct interface{}, forDirs bool) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func buildInclusionCompletionFunc(ctx context.Context, cmdStruct interface{}, forDirs bool) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { ptArgs := buildAutocompleteProjectTargetCommandArgs(cmdStruct) - var tags utils.OrderedMap - var deploymentItemDirs utils.OrderedMap + var tags utils.OrderedMap[string, bool] + var deploymentItemDirs utils.OrderedMap[string, bool] var mutex sync.Mutex - err := withProjectForCompletion(&ptArgs.projectFlags, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { + err := withProjectForCompletion(ctx, &ptArgs.projectFlags, &ptArgs.argsFlags, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { var targets []string if ptArgs.targetFlags.Target == "" { - for _, t := range p.DynamicTargets { - targets = append(targets, t.Target.Name) + for _, t := range p.Targets { + targets = append(targets, t.Name) } } else { targets = append(targets, ptArgs.targetFlags.Target) @@ -149,10 +139,10 @@ func buildInclusionCompletionFunc(cmdStruct interface{}, forDirs bool) func(cmd ptArgs.targetFlags.Target = t wg.Add(1) go func() { - _ = withProjectTargetCommandContext(ctx, ptArgs, p, func(ctx *commandCtx) error { + _ = withProjectTargetCommandContext(ctx, ptArgs, p, func(cmdCtx *commandCtx) error { mutex.Lock() defer mutex.Unlock() - for _, di := range ctx.targetCtx.DeploymentCollection.Deployments { + for _, di := range cmdCtx.targetCtx.DeploymentCollection.Deployments { tags.Merge(di.Tags) deploymentItemDirs.Set(di.RelToSourceItemDir, true) } @@ -165,7 +155,7 @@ func buildInclusionCompletionFunc(cmdStruct interface{}, forDirs bool) func(cmd return nil }) if err != nil { - status.Error(cliCtx, err.Error()) + status.Error(ctx, err.Error()) return nil, cobra.ShellCompDirectiveError } if forDirs { @@ -176,7 +166,7 @@ func buildInclusionCompletionFunc(cmdStruct interface{}, forDirs bool) func(cmd } } -func buildImagesCompletionFunc(cmdStruct interface{}) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func buildImagesCompletionFunc(ctx context.Context, cmdStruct interface{}) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { ptArgs := buildAutocompleteProjectTargetCommandArgs(cmdStruct) @@ -184,14 +174,14 @@ func buildImagesCompletionFunc(cmdStruct interface{}) func(cmd *cobra.Command, a return nil, cobra.ShellCompDirectiveDefault } - var images utils.OrderedMap + var images utils.OrderedMap[string, bool] var mutex sync.Mutex - err := withProjectForCompletion(&ptArgs.projectFlags, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { + err := withProjectForCompletion(ctx, &ptArgs.projectFlags, &ptArgs.argsFlags, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { var targets []string if ptArgs.targetFlags.Target == "" { - for _, t := range p.DynamicTargets { - targets = append(targets, t.Target.Name) + for _, t := range p.Targets { + targets = append(targets, t.Name) } } else { targets = append(targets, ptArgs.targetFlags.Target) @@ -203,16 +193,16 @@ func buildImagesCompletionFunc(cmdStruct interface{}) func(cmd *cobra.Command, a ptArgs.targetFlags.Target = t wg.Add(1) go func() { - _ = withProjectTargetCommandContext(ctx, ptArgs, p, func(ctx *commandCtx) error { - err := ctx.targetCtx.DeploymentCollection.Prepare() + _ = withProjectTargetCommandContext(ctx, ptArgs, p, func(cmdCtx *commandCtx) error { + err := cmdCtx.targetCtx.DeploymentCollection.Prepare() if err != nil { - status.Error(cliCtx, err.Error()) + status.Error(ctx, err.Error()) } mutex.Lock() defer mutex.Unlock() - for _, si := range ctx.images.SeenImages(false) { - str := si.Image + for _, si := range cmdCtx.images.SeenImages(false) { + str := *si.Image if si.Namespace != nil { str += ":" + *si.Namespace } @@ -233,9 +223,129 @@ func buildImagesCompletionFunc(cmdStruct interface{}) func(cmd *cobra.Command, a return nil }) if err != nil { - status.Error(cliCtx, err.Error()) + status.Error(ctx, err.Error()) return nil, cobra.ShellCompDirectiveError } return images.ListKeys(), cobra.ShellCompDirectiveNoSpace } } + +func loadKubeconfig(ctx context.Context, cmdStruct interface{}) (api.Config, *rest.Config, error) { + var kubeconfigPath string + var kubeContext string + cmdV := reflect.ValueOf(cmdStruct).Elem() + if cmdV.FieldByName("Kubeconfig").IsValid() { + kubeconfigPath = cmdV.FieldByName("Kubeconfig").Interface().(string) + } + if cmdV.FieldByName("Context").IsValid() { + kubeContext = cmdV.FieldByName("Context").Interface().(string) + } + + rules := clientcmd.NewDefaultClientConfigLoadingRules() + if kubeconfigPath != "" { + rules.ExplicitPath = kubeconfigPath + } + + configOverrides := &clientcmd.ConfigOverrides{ + CurrentContext: kubeContext, + } + + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + rules, configOverrides) + + rawConfig, err := clientConfig.RawConfig() + if err != nil { + return api.Config{}, nil, err + } + + restConfig, err := clientConfig.ClientConfig() + if err != nil { + return api.Config{}, nil, err + } + + return rawConfig, restConfig, nil +} + +func buildContextCompletionFunc(ctx context.Context, cmdStruct interface{}) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if strings.Index(toComplete, "=") != -1 { + return nil, cobra.ShellCompDirectiveDefault + } + + rawConfig, _, err := loadKubeconfig(ctx, cmdStruct) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var contextNames []string + for n, _ := range rawConfig.Contexts { + contextNames = append(contextNames, n) + } + return contextNames, cobra.ShellCompDirectiveNoSpace + } +} + +func buildObjectNamespaceCompletionFunc(ctx context.Context, cmdStruct interface{}) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if strings.Index(toComplete, "=") != -1 { + return nil, cobra.ShellCompDirectiveDefault + } + + _, restConfig, err := loadKubeconfig(ctx, cmdStruct) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + c, err := v1.NewForConfig(restConfig) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + l, err := c.Namespaces().List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var names []string + for _, x := range l.Items { + names = append(names, x.Name) + } + return names, cobra.ShellCompDirectiveNoSpace + } +} + +func buildObjectNameCompletionFunc(ctx context.Context, cmdStruct interface{}, gvr schema.GroupVersionResource) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if strings.Index(toComplete, "=") != -1 { + return nil, cobra.ShellCompDirectiveDefault + } + + var namespace string + + cmdV := reflect.ValueOf(cmdStruct).Elem() + if cmdV.FieldByName("Namespace").IsValid() { + namespace = cmdV.FieldByName("Namespace").Interface().(string) + } + + _, restConfig, err := loadKubeconfig(ctx, cmdStruct) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + c, err := dynamic.NewForConfig(restConfig) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + l, err := c.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var names []string + for _, x := range l.Items { + names = append(names, x.GetName()) + } + return names, cobra.ShellCompDirectiveNoSpace + } +} diff --git a/cmd/kluctl/commands/override_std_streams.go b/cmd/kluctl/commands/override_std_streams.go new file mode 100644 index 000000000..ab2573f72 --- /dev/null +++ b/cmd/kluctl/commands/override_std_streams.go @@ -0,0 +1,53 @@ +package commands + +import ( + "context" + "io" + "os" +) + +type overrideStdStreamsKey int +type overrideStdStreamsValue struct { + stdout io.Writer + stderr io.Writer +} + +var overrideStdStreamsKeyInst overrideStdStreamsKey + +func WithStdStreams(ctx context.Context, stdout io.Writer, stderr io.Writer) context.Context { + return context.WithValue(ctx, overrideStdStreamsKeyInst, &overrideStdStreamsValue{ + stdout: stdout, + stderr: stderr, + }) +} + +func getStdStreams(ctx context.Context) (io.Writer, io.Writer) { + v := ctx.Value(overrideStdStreamsKeyInst) + if v == nil { + return os.Stdout, os.Stderr + } + v2 := v.(*overrideStdStreamsValue) + return v2.stdout, v2.stderr +} + +type stringWriter struct { + w io.Writer +} + +func (w *stringWriter) WriteString(s string) (n int, err error) { + return w.w.Write([]byte(s)) +} + +func (w *stringWriter) Write(b []byte) (n int, err error) { + return w.w.Write(b) +} + +func getStdout(ctx context.Context) *stringWriter { + stdout, _ := getStdStreams(ctx) + return &stringWriter{stdout} +} + +func getStderr(ctx context.Context) *stringWriter { + _, stderr := getStdStreams(ctx) + return &stringWriter{stderr} +} diff --git a/cmd/kluctl/commands/root.go b/cmd/kluctl/commands/root.go index 7e3928ac8..9462aaa2c 100644 --- a/cmd/kluctl/commands/root.go +++ b/cmd/kluctl/commands/root.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18,48 +18,65 @@ package commands import ( "context" "fmt" - "github.com/kluctl/kluctl/v2/pkg/status" - "github.com/kluctl/kluctl/v2/pkg/utils" - "github.com/kluctl/kluctl/v2/pkg/utils/uo" - "github.com/kluctl/kluctl/v2/pkg/utils/versions" - "github.com/kluctl/kluctl/v2/pkg/version" - "github.com/kluctl/kluctl/v2/pkg/yaml" - "github.com/mattn/go-isatty" - "github.com/spf13/cobra" - "github.com/spf13/viper" + go_container_logs "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/gops/agent" + status2 "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/lib/yaml" + "github.com/kluctl/kluctl/v2/pkg/prompts" + flag "github.com/spf13/pflag" "io" - "k8s.io/klog/v2" "log" "net/http" "os" "path/filepath" "runtime/pprof" + ctrl "sigs.k8s.io/controller-runtime" "strings" "time" + + "github.com/Masterminds/semver/v3" + "github.com/kluctl/kluctl/v2/pkg/utils" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/kluctl/kluctl/v2/pkg/version" + "github.com/mattn/go-colorable" + "github.com/mattn/go-isatty" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "k8s.io/klog/v2" ) const latestReleaseUrl = "https://api.github.com/repos/kluctl/kluctl/releases/latest" -type cli struct { +type GlobalFlags struct { Debug bool `group:"global" help:"Enable debug logging"` NoUpdateCheck bool `group:"global" help:"Disable update check on startup"` + NoColor bool `group:"global" help:"Disable colored output"` - CpuProfile string `group:"global" help:"Enable CPU profiling and write the result to the given path"` - - CheckImageUpdates checkImageUpdatesCmd `cmd:"" help:"Render deployment and check if any images have new tags available"` - Delete deleteCmd `cmd:"" help:"Delete a target (or parts of it) from the corresponding cluster"` - Deploy deployCmd `cmd:"" help:"Deploys a target to the corresponding cluster"` - Diff diffCmd `cmd:"" help:"Perform a diff between the locally rendered target and the already deployed target"` - Downscale downscaleCmd `cmd:"" help:"Downscale all deployments"` - HelmPull helmPullCmd `cmd:"" help:"Recursively searches for 'helm-chart.yaml' files and pulls the specified Helm charts"` - HelmUpdate helmUpdateCmd `cmd:"" help:"Recursively searches for 'helm-chart.yaml' files and checks for new available versions"` - ListImages listImagesCmd `cmd:"" help:"Renders the target and outputs all images used via 'images.get_image(...)"` - ListTargets listTargetsCmd `cmd:"" help:"Outputs a yaml list with all target, including dynamic targets"` - PokeImages pokeImagesCmd `cmd:"" help:"Replace all images in target"` - Prune pruneCmd `cmd:"" help:"Searches the target cluster for prunable objects and deletes them"` - Render renderCmd `cmd:"" help:"Renders all resources and configuration files"` - Seal sealCmd `cmd:"" help:"Seal secrets based on target's sealingConfig"` - Validate validateCmd `cmd:"" help:"Validates the already deployed deployment"` + CpuProfile string `group:"global" help:"Enable CPU profiling and write the result to the given path"` + GopsAgent bool `group:"global" help:"Start gops agent in the background"` + GopsAgentAddr string `group:"global" help:"Specify the address:port to use for the gops agent" default:"127.0.0.1:0"` + + UseSystemPython bool `group:"global" help:"Use the system Python instead of the embedded Python."` +} + +type cli struct { + GlobalFlags + + Delete deleteCmd `cmd:"" help:"Delete a target (or parts of it) from the corresponding cluster"` + Deploy deployCmd `cmd:"" help:"Deploys a target to the corresponding cluster"` + Diff diffCmd `cmd:"" help:"Perform a diff between the locally rendered target and the already deployed target"` + HelmPull helmPullCmd `cmd:"" help:"Recursively searches for 'helm-chart.yaml' files and pre-pulls the specified Helm charts"` + HelmUpdate helmUpdateCmd `cmd:"" help:"Recursively searches for 'helm-chart.yaml' files and checks for new available versions"` + ListImages listImagesCmd `cmd:"" help:"Renders the target and outputs all images used via 'images.get_image(...)"` + ListTargets listTargetsCmd `cmd:"" help:"Outputs a yaml list with all targets"` + PokeImages pokeImagesCmd `cmd:"" help:"Replace all images in target"` + Prune pruneCmd `cmd:"" help:"Searches the target cluster for prunable objects and deletes them"` + Render renderCmd `cmd:"" help:"Renders all resources and configuration files"` + Validate validateCmd `cmd:"" help:"Validates the already deployed deployment"` + Controller controllerCmd `cmd:"" help:"Kluctl controller sub-commands"` + Gitops gitopsCmd `cmd:"" help:"GitOps sub-commands"` + Webui webuiCmd `cmd:"" help:"Kluctl Webui sub-commands"` + Oci ociCmd `cmd:"" help:"Oci sub-commands"` Version versionCmd `cmd:"" help:"Print kluctl version"` } @@ -69,33 +86,56 @@ var flagGroups = []groupInfo{ {group: "project", title: "Project arguments:", description: "Define where and how to load the kluctl project and its components from."}, {group: "images", title: "Image arguments:", description: "Control fixed images and update behaviour."}, {group: "inclusion", title: "Inclusion/Exclusion arguments:", description: "Control inclusion/exclusion."}, + {group: "gitops", title: "GitOps arguments:", description: "Specify gitops flags."}, {group: "misc", title: "Misc arguments:", description: "Command specific arguments."}, + {group: "results", title: "Command Results:", description: "Configure how command results are stored."}, + {group: "logs", title: "Log arguments:", description: "Configure logging."}, + {group: "override", title: "GitOps overrides:", description: "Override settings for GitOps deployments."}, + {group: "git", title: "Git arguments:", description: "Configure Git authentication."}, + {group: "helm", title: "Helm arguments:", description: "Configure Helm authentication."}, + {group: "registry", title: "Registry arguments:", description: "Configure OCI registry authentication."}, + {group: "auth", title: "Auth arguments:", description: "Configure authentication."}, } -var cliCtx = context.Background() -var didSetupStatusHandler bool - -func setupStatusHandler(debug bool) { - didSetupStatusHandler = true +var origStderr = os.Stderr - origStderr := os.Stderr +// we must determine isTerminal before we override os.Stderr +var isTerminal = isatty.IsTerminal(os.Stderr.Fd()) - // we must determine isTerminal before we override os.Stderr - isTerminal := isatty.IsTerminal(os.Stderr.Fd()) - var sh status.StatusHandler - if !debug && isatty.IsTerminal(os.Stderr.Fd()) { - sh = status.NewMultiLineStatusHandler(cliCtx, os.Stderr, isTerminal, false) +func initStatusHandlerAndPrompts(ctx context.Context, debug bool, noColor bool) context.Context { + var sh status2.StatusHandler + var pp prompts.PromptProvider + if !debug && isTerminal { + sh = status2.NewMultiLineStatusHandler(ctx, origStderr, isTerminal && !noColor, false) + pp = &prompts.StatusAndStdinPromptProvider{} } else { - sh = status.NewSimpleStatusHandler(func(message string) { + sh = status2.NewSimpleStatusHandler(func(level status2.Level, message string) { _, _ = fmt.Fprintf(origStderr, "%s\n", message) - }, isTerminal, false) + }, debug) + pp = &prompts.SimplePromptProvider{Out: origStderr} } - sh.SetTrace(debug) - cliCtx = status.NewContext(cliCtx, sh) + ctx = status2.NewContext(ctx, sh) + ctx = prompts.NewContext(ctx, pp) + + return ctx +} + +func redirectLogsAndStderr(ctx context.Context) { + f := func(line string) { + status2.Info(ctx, line) + } + + lr1 := status2.NewLineRedirector(f) + lr2 := status2.NewLineRedirector(f) klog.LogToStderr(false) - klog.SetOutput(status.NewLineRedirector(sh.Info)) - log.SetOutput(status.NewLineRedirector(sh.Info)) + klog.SetOutput(lr1) + log.SetOutput(lr2) + ctrl.SetLogger(klog.NewKlogr()) + + go_container_logs.Warn.SetOutput(status2.NewLineRedirector(func(line string) { + status2.Warning(ctx, line) + })) pr, pw, err := os.Pipe() if err != nil { @@ -103,13 +143,26 @@ func setupStatusHandler(debug bool) { } go func() { - x := status.NewLineRedirector(sh.Info) + x := status2.NewLineRedirector(f) _, _ = io.Copy(x, pr) }() os.Stderr = pw } +func setupGops(flags *GlobalFlags) error { + if !flags.GopsAgent { + return nil + } + + if err := agent.Listen(agent.Options{ + Addr: flags.GopsAgentAddr, + }); err != nil { + return err + } + return nil +} + var cpuProfileFile *os.File func setupProfiling(cpuProfile string) error { @@ -128,18 +181,15 @@ func setupProfiling(cpuProfile string) error { } type VersionCheckState struct { - LastVersionCheck time.Time `yaml:"lastVersionCheck"` + LastVersionCheck time.Time `json:"lastVersionCheck"` } -func (c *cli) checkNewVersion() { - if c.NoUpdateCheck { - return - } +func checkNewVersion(ctx context.Context) { if version.GetVersion() == "0.0.0" { return } - versionCheckPath := filepath.Join(utils.GetTmpBaseDir(), "version_check.yaml") + versionCheckPath := filepath.Join(utils.GetCacheDir(ctx), "version_check.yaml") var versionCheckState VersionCheckState err := yaml.ReadYamlFile(versionCheckPath, &versionCheckState) if err == nil { @@ -151,7 +201,7 @@ func (c *cli) checkNewVersion() { versionCheckState.LastVersionCheck = time.Now() _ = yaml.WriteYamlFile(versionCheckPath, &versionCheckState) - s := status.Start(cliCtx, "Checking for new kluctl version") + s := status2.Start(ctx, "Checking for new kluctl version") defer s.Failed() r, err := http.Get(latestReleaseUrl) @@ -173,28 +223,26 @@ func (c *cli) checkNewVersion() { if strings.HasPrefix(latestVersionStr, "v") { latestVersionStr = latestVersionStr[1:] } - latestVersion := versions.LooseVersion(latestVersionStr) - localVersion := versions.LooseVersion(version.GetVersion()) - if localVersion.Less(latestVersion, true) { - s.Update(fmt.Sprintf("You are using an outdated version (%v) of kluctl. You should update soon to version %v", localVersion, latestVersion)) + latestVersion, err := semver.NewVersion(latestVersionStr) + if err != nil { + s.FailedWithMessagef("Failed to parse latest version: %v", err) + return + } + localVersion, err := semver.NewVersion(version.GetVersion()) + if err != nil { + s.FailedWithMessagef("Failed to parse local version: %v", err) + return + } + if localVersion.LessThan(latestVersion) { + s.Updatef("You are using an outdated version (%v) of kluctl. You should update soon to version %v", localVersion.String(), latestVersion.String()) } else { s.Update("Your kluctl version is up-to-date") } s.Success() } -func (c *cli) preRun() error { - err := setupProfiling(c.CpuProfile) - if err != nil { - return err - } - setupStatusHandler(c.Debug) - c.checkNewVersion() - return nil -} - -func (c *cli) Run() error { - return nil +func (c *cli) Run(ctx context.Context) error { + return flag.ErrHelp } func initViper() { @@ -204,17 +252,90 @@ func initViper() { viper.AddConfigPath("$HOME/.kluctl") if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { - status.Error(cliCtx, err.Error()) + _, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } } +} + +func Main() { + colorable.EnableColorsStdout(nil) + ctx := context.Background() - viper.SetEnvPrefix("kluctl") - viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - viper.AutomaticEnv() + didSetupStatusHandler := false + + err := Execute(ctx, os.Args[1:], func(ctxIn context.Context) (context.Context, error) { + cmd := getCobraCommand(ctxIn) + flags := getCobraGlobalFlags(ctxIn) + + err := setupGops(flags) + if err != nil { + return ctx, err + } + err = setupProfiling(flags.CpuProfile) + if err != nil { + return ctx, err + } + + ctx = initStatusHandlerAndPrompts(ctxIn, flags.Debug, flags.NoColor) + didSetupStatusHandler = true + + if cmd.Parent() == nil || (cmd.Name() != "run" && cmd.Parent().Name() != "controller") { + redirectLogsAndStderr(ctx) + } + + if !flags.NoUpdateCheck { + if len(os.Args) < 2 || (os.Args[1] != "completion" && os.Args[1] != "__complete") { + checkNewVersion(ctx) + } + } + return ctx, nil + }) + + if cpuProfileFile != nil { + pprof.StopCPUProfile() + _ = cpuProfileFile.Close() + cpuProfileFile = nil + } + + if err != nil { + if didSetupStatusHandler { + status2.Error(ctx, err.Error()) + } else { + _, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + } + } + + sh := status2.FromContext(ctx) + if sh != nil { + sh.Stop() + } + + if err != nil { + os.Exit(1) + } } -func Execute() { +type cobraCmdContextKey struct{} +type cobraGlobalFlagsKey struct{} + +func getCobraCommand(ctx context.Context) *cobra.Command { + v := ctx.Value(cobraCmdContextKey{}) + if x, ok := v.(*cobra.Command); ok { + return x + } + return nil +} + +func getCobraGlobalFlags(ctx context.Context) *GlobalFlags { + v := ctx.Value(cobraGlobalFlagsKey{}) + if x, ok := v.(*GlobalFlags); ok { + return x + } + panic("missing global flags") +} + +func Execute(ctx context.Context, args []string, preRun func(ctx context.Context) (context.Context, error)) error { root := cli{} rootCmd, err := buildRootCobraCmd(&root, "kluctl", "Deploy and manage complex deployments on Kubernetes", @@ -223,40 +344,48 @@ composed of multiple smaller parts (Helm/Kustomize/...) in a manageable and unif flagGroups) if err != nil { - panic(err) + return err } + rootCmd.SetContext(ctx) + rootCmd.SetArgs(args) rootCmd.Version = version.GetVersion() rootCmd.SilenceUsage = true rootCmd.SilenceErrors = true rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { - err := copyViperValuesToCobraCmd(cmd) + ctx = context.WithValue(ctx, cobraCmdContextKey{}, cmd) + for c := cmd; c != nil; c = c.Parent() { + c.SetContext(ctx) + } + + err = copyViperValuesToCobraCmd(cmd) if err != nil { return err } - return root.preRun() - } - - initViper() - err = rootCmd.ExecuteContext(cliCtx) - if !didSetupStatusHandler { - setupStatusHandler(false) - } + ctx = context.WithValue(ctx, cobraGlobalFlagsKey{}, &root.GlobalFlags) + for c := cmd; c != nil; c = c.Parent() { + c.SetContext(ctx) + } - if cpuProfileFile != nil { - pprof.StopCPUProfile() - _ = cpuProfileFile.Close() - cpuProfileFile = nil + if preRun != nil { + ctx, err = preRun(ctx) + if ctx != nil { + for c := cmd; c != nil; c = c.Parent() { + c.SetContext(ctx) + } + } + if err != nil { + return err + } + } + return nil } - sh := status.FromContext(cliCtx) + return rootCmd.Execute() +} - if err != nil { - status.Error(cliCtx, err.Error()) - sh.Stop() - os.Exit(1) - } - sh.Stop() +func init() { + initViper() } diff --git a/cmd/kluctl/commands/utils.go b/cmd/kluctl/commands/utils.go index 3ab20e9be..0576759d1 100644 --- a/cmd/kluctl/commands/utils.go +++ b/cmd/kluctl/commands/utils.go @@ -3,132 +3,174 @@ package commands import ( "context" "fmt" + "github.com/google/uuid" + "github.com/kluctl/kluctl/lib/git" + "github.com/kluctl/kluctl/lib/git/auth" + "github.com/kluctl/kluctl/lib/git/messages" + ssh_pool "github.com/kluctl/kluctl/lib/git/ssh-pool" + "github.com/kluctl/kluctl/lib/status" "github.com/kluctl/kluctl/v2/cmd/kluctl/args" "github.com/kluctl/kluctl/v2/pkg/deployment" - "github.com/kluctl/kluctl/v2/pkg/git" - "github.com/kluctl/kluctl/v2/pkg/git/auth" - git_url "github.com/kluctl/kluctl/v2/pkg/git/git-url" - "github.com/kluctl/kluctl/v2/pkg/git/repocache" - ssh_pool "github.com/kluctl/kluctl/v2/pkg/git/ssh-pool" - "github.com/kluctl/kluctl/v2/pkg/jinja2" + helm_auth "github.com/kluctl/kluctl/v2/pkg/helm/auth" + "github.com/kluctl/kluctl/v2/pkg/k8s" + "github.com/kluctl/kluctl/v2/pkg/kluctl_jinja2" "github.com/kluctl/kluctl/v2/pkg/kluctl_project" - "github.com/kluctl/kluctl/v2/pkg/registries" - "github.com/kluctl/kluctl/v2/pkg/status" + "github.com/kluctl/kluctl/v2/pkg/kluctl_project/target-context" + "github.com/kluctl/kluctl/v2/pkg/oci/auth_provider" + "github.com/kluctl/kluctl/v2/pkg/prompts" + "github.com/kluctl/kluctl/v2/pkg/repocache" + "github.com/kluctl/kluctl/v2/pkg/results" "github.com/kluctl/kluctl/v2/pkg/utils" - "github.com/kluctl/kluctl/v2/pkg/yaml" - "io/ioutil" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" "os" + client2 "sigs.k8s.io/controller-runtime/pkg/client" ) -func withKluctlProjectFromArgs(projectFlags args.ProjectFlags, strictTemplates bool, forCompletion bool, cb func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error) error { - var url *git_url.GitUrl - if projectFlags.ProjectUrl != "" { - var err error - url, err = git_url.Parse(projectFlags.ProjectUrl) - if err != nil { - return err - } - } +func withKluctlProjectFromArgs(ctx context.Context, kubeconfigFlags *args.KubeconfigFlags, projectFlags args.ProjectFlags, + argsFlags *args.ArgsFlags, + gitCredentials *args.GitCredentials, + helmCredentials *args.HelmCredentials, + registryCredentials *args.RegistryCredentials, + internalDeploy bool, strictTemplates bool, forCompletion bool, cb func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error) error { + globalFlags := getCobraGlobalFlags(ctx) - tmpDir, err := ioutil.TempDir(utils.GetTmpBaseDir(), "project-") + j2, err := kluctl_jinja2.NewKluctlJinja2(ctx, strictTemplates, globalFlags.UseSystemPython) if err != nil { - return fmt.Errorf("creating temporary project directory failed: %w", err) + return err } - defer os.RemoveAll(tmpDir) + defer j2.Close() - j2, err := jinja2.NewJinja2() + projectDir, err := projectFlags.ProjectDir.GetProjectDir() if err != nil { return err } - defer j2.Close() - j2.SetStrict(strictTemplates) + var repoRoot string + if !internalDeploy { + repoRoot, err = git.DetectGitRepositoryRoot(projectDir) + if err != nil { + status.Warning(ctx, "Failed to detect git project root. This might cause follow-up errors") + } + } - cwd, err := os.Getwd() + if repoRoot == "" { + repoRoot = projectDir + } + + ctx, cancel := context.WithTimeout(ctx, projectFlags.Timeout) + defer cancel() + + sshPool := &ssh_pool.SshPool{} + + sourceOverrides, err := projectFlags.SourceOverrides.ParseOverrides(ctx) if err != nil { return err } - repoRoot, err := git.DetectGitRepositoryRoot(cwd) - if err != nil { - status.Warning(cliCtx, "Failed to detect git project root. This might cause follow-up errors") + messageCallbacks := &messages.MessageCallbacks{ + WarningFn: func(s string) { status.Warning(ctx, s) }, + TraceFn: func(s string) { status.Trace(ctx, s) }, + AskForPasswordFn: func(s string) (string, error) { return prompts.AskForPassword(ctx, s) }, + AskForConfirmationFn: func(s string) bool { return prompts.AskForConfirmation(ctx, s) }, + } + gitAuth := auth.NewDefaultAuthProviders("KLUCTL_GIT", messageCallbacks) + ociAuth := auth_provider.NewDefaultAuthProviders("KLUCTL_REGISTRY") + helmAuth := helm_auth.NewDefaultAuthProviders("KLUCTL_HELM") + if x, err := gitCredentials.BuildAuthProvider(ctx); err != nil { + return err + } else { + gitAuth.RegisterAuthProvider(x, false) + } + if x, err := helmCredentials.BuildAuthProvider(ctx); err != nil { + return err + } else { + helmAuth.RegisterAuthProvider(x, false) + } + if x, err := registryCredentials.BuildAuthProvider(ctx); err != nil { + return err + } else { + ociAuth.RegisterAuthProvider(x, false) } - ctx, cancel := context.WithTimeout(cliCtx, projectFlags.Timeout) - defer cancel() + gitRp := repocache.NewGitRepoCache(ctx, sshPool, gitAuth, sourceOverrides, projectFlags.GitCacheUpdateInterval) + defer gitRp.Clear() - sshPool := &ssh_pool.SshPool{} + ociRp := repocache.NewOciRepoCache(ctx, ociAuth, sourceOverrides, projectFlags.GitCacheUpdateInterval) + defer gitRp.Clear() - rp := repocache.NewGitRepoCache(ctx, sshPool, auth.NewDefaultAuthProviders(), projectFlags.GitCacheUpdateInterval) - defer rp.Clear() + externalArgs, err := argsFlags.LoadArgs() + if err != nil { + return err + } loadArgs := kluctl_project.LoadKluctlProjectArgs{ RepoRoot: repoRoot, - ProjectDir: cwd, - ProjectUrl: url, - ProjectRef: projectFlags.ProjectRef, + ProjectDir: projectDir, ProjectConfig: projectFlags.ProjectConfig.String(), - LocalClusters: projectFlags.LocalClusters.String(), - LocalDeployment: projectFlags.LocalDeployment.String(), - LocalSealedSecrets: projectFlags.LocalSealedSecrets.String(), - RP: rp, - ClientConfigGetter: clientConfigGetter(forCompletion), + ExternalArgs: externalArgs, + GitRP: gitRp, + OciRP: ociRp, + OciAuthProvider: ociAuth, + HelmAuthProvider: helmAuth, + ClientConfigGetter: clientConfigGetter(kubeconfigFlags, forCompletion), } - p, err := kluctl_project.LoadKluctlProject(ctx, loadArgs, tmpDir, j2) + p, err := kluctl_project.LoadKluctlProject(ctx, loadArgs, j2) if err != nil { return err } - if projectFlags.OutputMetadata != "" { - md := p.GetMetadata() - b, err := yaml.WriteYamlBytes(md) - if err != nil { - return err - } - err = ioutil.WriteFile(projectFlags.OutputMetadata, b, 0o640) - if err != nil { - return err - } - } return cb(ctx, p) } type projectTargetCommandArgs struct { projectFlags args.ProjectFlags + kubeconfigFlags args.KubeconfigFlags targetFlags args.TargetFlags argsFlags args.ArgsFlags imageFlags args.ImageFlags inclusionFlags args.InclusionFlags + gitCredentials args.GitCredentials + helmCredentials args.HelmCredentials + registryCredentials args.RegistryCredentials dryRunArgs *args.DryRunFlags renderOutputDirFlags args.RenderOutputDirFlags + commandResultFlags *args.CommandResultFlags + + discriminator string - forSeal bool - forCompletion bool + internalDeploy bool + forCompletion bool + offlineKubernetes bool + kubernetesVersion string } type commandCtx struct { - ctx context.Context - targetCtx *kluctl_project.TargetContext + targetCtx *target_context.TargetContext images *deployment.Images + + resultId string + resultStore results.ResultStore } -func withProjectCommandContext(args projectTargetCommandArgs, cb func(ctx *commandCtx) error) error { - return withKluctlProjectFromArgs(args.projectFlags, true, false, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { +func withProjectCommandContext(ctx context.Context, args projectTargetCommandArgs, cb func(cmdCtx *commandCtx) error) error { + return withKluctlProjectFromArgs(ctx, &args.kubeconfigFlags, args.projectFlags, &args.argsFlags, &args.gitCredentials, &args.helmCredentials, &args.registryCredentials, args.internalDeploy, true, false, func(ctx context.Context, p *kluctl_project.LoadedKluctlProject) error { return withProjectTargetCommandContext(ctx, args, p, cb) }) } -func withProjectTargetCommandContext(ctx context.Context, args projectTargetCommandArgs, p *kluctl_project.LoadedKluctlProject, cb func(ctx *commandCtx) error) error { - rh := registries.NewRegistryHelper(ctx) - err := rh.ParseAuthEntriesFromEnv() +func withProjectTargetCommandContext(ctx context.Context, args projectTargetCommandArgs, p *kluctl_project.LoadedKluctlProject, cb func(cmdCtx *commandCtx) error) error { + tmpDir, err := os.MkdirTemp(utils.GetTmpBaseDir(ctx), "project-") if err != nil { - return fmt.Errorf("failed to parse registry auth from environment: %w", err) + return fmt.Errorf("creating temporary project directory failed: %w", err) } - images, err := deployment.NewImages(rh, args.imageFlags.UpdateImages, args.imageFlags.OfflineImages || args.forCompletion) + defer os.RemoveAll(tmpDir) + + images, err := deployment.NewImages() if err != nil { return err } @@ -136,23 +178,16 @@ func withProjectTargetCommandContext(ctx context.Context, args projectTargetComm if err != nil { return err } - for _, fi := range fixedImages { - images.AddFixedImage(fi) - } + images.PrependFixedImages(fixedImages) inclusion, err := args.inclusionFlags.ParseInclusionFromArgs() if err != nil { return err } - optionArgs, err := deployment.ParseArgs(args.argsFlags.Arg) - if err != nil { - return err - } - renderOutputDir := args.renderOutputDirFlags.RenderOutputDir if renderOutputDir == "" { - tmpDir, err := ioutil.TempDir(p.TmpDir, "rendered") + tmpDir, err := os.MkdirTemp(tmpDir, "rendered") if err != nil { return err } @@ -160,42 +195,84 @@ func withProjectTargetCommandContext(ctx context.Context, args projectTargetComm renderOutputDir = tmpDir } - var clusterName *string - if args.projectFlags.Cluster != "" { - clusterName = &args.projectFlags.Cluster + targetParams := target_context.TargetContextParams{ + TargetName: args.targetFlags.Target, + TargetNameOverride: args.targetFlags.TargetNameOverride, + ContextOverride: args.targetFlags.Context, + Discriminator: args.discriminator, + OfflineK8s: args.offlineKubernetes, + K8sVersion: args.kubernetesVersion, + DryRun: args.dryRunArgs == nil || args.dryRunArgs.DryRun || args.forCompletion, + Images: images, + Inclusion: inclusion, + OciAuthProvider: p.LoadArgs.OciAuthProvider, + HelmAuthProvider: p.LoadArgs.HelmAuthProvider, + RenderOutputDir: renderOutputDir, } - targetCtx, err := p.NewTargetContext(ctx, args.targetFlags.Target, clusterName, - args.dryRunArgs == nil || args.dryRunArgs.DryRun || args.forCompletion, - optionArgs, args.forSeal, images, inclusion, - renderOutputDir) + commandResultId := uuid.NewString() + + clientConfig, contextName, err := p.LoadK8sConfig(ctx, targetParams.TargetName, targetParams.ContextOverride, targetParams.OfflineK8s) if err != nil { return err } - if !args.forSeal && !args.forCompletion { - err = targetCtx.DeploymentCollection.Prepare() + var k *k8s.K8sCluster + var resultStore results.ResultStore + if clientConfig != nil { + discovery, mapper, err := k8s.CreateDiscoveryAndMapper(ctx, clientConfig) + if err != nil { + return err + } + + s := status.Start(ctx, fmt.Sprintf("Initializing k8s client")) + k, err = k8s.NewK8sCluster(ctx, clientConfig, discovery, mapper, targetParams.DryRun) if err != nil { + s.Failed() return err } + s.Success() + + resultStore, err = buildResultStoreRW(ctx, clientConfig, mapper, args.commandResultFlags, false) + if err != nil { + if !errors.IsForbidden(err) { + return err + } + status.Warningf(ctx, "Not enough permissions to write to the result store.") + } } + targetCtx, err := target_context.NewTargetContext(ctx, p, contextName, k, targetParams) + if err != nil { + return err + } + + if !args.forCompletion { + err = targetCtx.DeploymentCollection.Prepare() + if err != nil { + return err + } + } cmdCtx := &commandCtx{ - ctx: ctx, - targetCtx: targetCtx, - images: images, + targetCtx: targetCtx, + images: images, + resultId: commandResultId, + resultStore: resultStore, } return cb(cmdCtx) } -func clientConfigGetter(forCompletion bool) func(context *string) (*rest.Config, *api.Config, error) { +func clientConfigGetter(kubeconfigFlags *args.KubeconfigFlags, forCompletion bool) func(context *string) (*rest.Config, *api.Config, error) { return func(context *string) (*rest.Config, *api.Config, error) { if forCompletion { return nil, nil, nil } configLoadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if kubeconfigFlags != nil { + configLoadingRules.ExplicitPath = kubeconfigFlags.Kubeconfig.String() + } configOverrides := &clientcmd.ConfigOverrides{} if context != nil { configOverrides.CurrentContext = *context @@ -215,3 +292,50 @@ func clientConfigGetter(forCompletion bool) func(context *string) (*rest.Config, return restConfig, &rawConfig, nil } } + +func buildResultStoreRO(ctx context.Context, restConfig *rest.Config, mapper meta.RESTMapper, flags *args.CommandResultReadOnlyFlags) (results.ResultStore, error) { + if flags == nil { + return nil, nil + } + + c, err := client2.NewWithWatch(restConfig, client2.Options{ + Mapper: mapper, + }) + if err != nil { + return nil, err + } + + resultStore, err := results.NewResultStoreSecrets(ctx, restConfig, c, false, flags.CommandResultNamespace, 0, 0) + if err != nil { + return nil, err + } + + return resultStore, nil +} + +func buildResultStoreRW(ctx context.Context, restConfig *rest.Config, mapper meta.RESTMapper, flags *args.CommandResultFlags, startCleanup bool) (results.ResultStore, error) { + if flags == nil || !flags.WriteCommandResult { + return nil, nil + } + + c, err := client2.NewWithWatch(restConfig, client2.Options{ + Mapper: mapper, + }) + if err != nil { + return nil, err + } + + resultStore, err := results.NewResultStoreSecrets(ctx, restConfig, c, true, flags.CommandResultNamespace, flags.KeepCommandResultsCount, flags.KeepValidateResultsCount) + if err != nil { + return nil, err + } + + if startCleanup { + err = resultStore.StartCleanupOrphans() + if err != nil { + return nil, err + } + } + + return resultStore, nil +} diff --git a/main.go b/cmd/main.go similarity index 91% rename from main.go rename to cmd/main.go index fb256f4c4..d7b055670 100644 --- a/main.go +++ b/cmd/main.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -24,5 +24,5 @@ var version = "0.0.0" func main() { version2.SetVersion(version) - commands.Execute() + commands.Main() } diff --git a/config/crd/bases/gitops.kluctl.io_kluctldeployments.yaml b/config/crd/bases/gitops.kluctl.io_kluctldeployments.yaml new file mode 100644 index 000000000..6ae4812f8 --- /dev/null +++ b/config/crd/bases/gitops.kluctl.io_kluctldeployments.yaml @@ -0,0 +1,914 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: kluctldeployments.gitops.kluctl.io +spec: + group: gitops.kluctl.io + names: + kind: KluctlDeployment + listKind: KluctlDeploymentList + plural: kluctldeployments + singular: kluctldeployment + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.suspend + name: Suspend + type: boolean + - jsonPath: .spec.dryRun + name: DryRun + type: boolean + - jsonPath: .status.lastDeployResult.commandInfo.endTime + name: Deployed + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.lastDriftDetectionResultMessage + name: Drift + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: KluctlDeployment is the Schema for the kluctldeployments 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: + properties: + abortOnError: + default: false + description: |- + ForceReplaceOnError instructs kluctl to abort deployments immediately when something fails. + Equivalent to using '--abort-on-error' when calling kluctl. + type: boolean + args: + description: Args specifies dynamic target args. + type: object + x-kubernetes-preserve-unknown-fields: true + context: + description: |- + If specified, overrides the context to be used. This will effectively make kluctl ignore the context specified + in the target. + type: string + credentials: + description: Credentials specifies the credentials used when pulling + sources + properties: + git: + description: Git specifies a list of git credentials + items: + properties: + host: + description: |- + Host specifies the hostname that this secret applies to. If set to '*', this set of credentials + applies to all hosts. + Using '*' for http(s) based repositories is not supported, meaning that such credentials sets will be ignored. + You must always set a proper hostname in that case. + type: string + path: + description: |- + Path specifies the path to be used to filter Git repositories. The path can contain wildcards. These credentials + will only be used for matching Git URLs. If omitted, all repositories are considered to match. + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + the git repository. + For HTTPS git repositories the Secret must contain 'username' and 'password' + fields. + For SSH git repositories the Secret must contain 'identity' + and 'known_hosts' fields. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - secretRef + type: object + type: array + helm: + description: Helm specifies a list of Helm credentials + items: + properties: + host: + description: Host specifies the hostname that this secret + applies to. + type: string + path: + description: |- + Path specifies the path to be used to filter Helm urls. The path can contain wildcards. These credentials + will only be used for matching URLs. If omitted, all URLs are considered to match. + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + the Helm repository. + The secret can either container basic authentication credentials via `username` and `password` or + TLS authentication via `certFile` and `keyFile`. `caFile` can be specified to override the CA to use while + contacting the repository. + The secret can also contain `insecureSkipTlsVerify: "true"`, which will disable TLS verification. + `passCredentialsAll: "true"` can be specified to make the controller pass credentials to all requests, even if + the hostname changes in-between. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - host + - secretRef + type: object + type: array + oci: + description: Oci specifies a list of OCI credentials + items: + properties: + registry: + description: Registry specifies the hostname that this secret + applies to. + type: string + repository: + description: |- + Repository specifies the org and repo name in the format 'org-name/repo-name'. + Both 'org-name' and 'repo-name' can be specified as '*', meaning that all names are matched. + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + the oci repository. + The secret must contain 'username' and 'password'. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - secretRef + type: object + type: array + type: object + decryption: + description: Decrypt Kubernetes secrets before applying them on the + cluster. + properties: + provider: + description: Provider is the name of the decryption engine. + enum: + - sops + type: string + secretRef: + description: The secret name containing the private OpenPGP keys + used for decryption. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + serviceAccount: + description: |- + ServiceAccount specifies the service account used to authenticate against cloud providers. + This is currently only usable for AWS KMS keys. The specified service account will be used to authenticate to AWS + by signing a token in an IRSA compliant way. + type: string + required: + - provider + type: object + delete: + default: false + description: Delete enables deletion of the specified target when + the KluctlDeployment object gets deleted. + type: boolean + deployInterval: + description: |- + DeployInterval specifies the interval at which to deploy the KluctlDeployment, even in cases the rendered + result does not change. + pattern: ^(([0-9]+(\.[0-9]+)?(ms|s|m|h))+) + type: string + deployMode: + default: full-deploy + description: |- + DeployMode specifies what deploy mode should be used. + The options 'full-deploy' and 'poke-images' are supported. + With the 'poke-images' option, only images are patched into the target without performing a full deployment. + enum: + - full-deploy + - poke-images + type: string + dryRun: + default: false + description: |- + DryRun instructs kluctl to run everything in dry-run mode. + Equivalent to using '--dry-run' when calling kluctl. + type: boolean + excludeDeploymentDirs: + description: |- + ExcludeDeploymentDirs instructs kluctl to exclude deployments with the given dir. + Equivalent to using '--exclude-deployment-dir' when calling kluctl. + items: + type: string + type: array + excludeTags: + description: |- + ExcludeTags instructs kluctl to exclude deployments with given tags. + Equivalent to using '--exclude-tag' when calling kluctl. + items: + type: string + type: array + forceApply: + default: false + description: |- + ForceApply instructs kluctl to force-apply in case of SSA conflicts. + Equivalent to using '--force-apply' when calling kluctl. + type: boolean + forceReplaceOnError: + default: false + description: |- + ForceReplaceOnError instructs kluctl to force-replace resources in case a normal replace fails. + Equivalent to using '--force-replace-on-error' when calling kluctl. + type: boolean + helmCredentials: + description: |- + HelmCredentials is a list of Helm credentials used when non pre-pulled Helm Charts are used inside a + Kluctl deployment. + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.helm instead. + items: + properties: + secretRef: + description: |- + SecretRef holds the name of a secret that contains the Helm credentials. + The secret must either contain the fields `credentialsId` which refers to the credentialsId + found in https://kluctl.io/docs/kluctl/reference/deployments/helm/#private-repositories or an `url` used + to match the credentials found in Kluctl projects helm-chart.yaml files. + The secret can either container basic authentication credentials via `username` and `password` or + TLS authentication via `certFile` and `keyFile`. `caFile` can be specified to override the CA to use while + contacting the repository. + The secret can also contain `insecureSkipTlsVerify: "true"`, which will disable TLS verification. + `passCredentialsAll: "true"` can be specified to make the controller pass credentials to all requests, even if + the hostname changes in-between. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + type: object + type: array + images: + description: |- + Images contains a list of fixed image overrides. + Equivalent to using '--fixed-images-file' when calling kluctl. + items: + properties: + container: + type: string + deployTags: + items: + type: string + type: array + deployedImage: + type: string + deployment: + type: string + deploymentDir: + type: string + image: + type: string + imageRegex: + type: string + namespace: + type: string + object: + properties: + group: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + version: + type: string + required: + - kind + - name + type: object + resultImage: + type: string + required: + - resultImage + type: object + type: array + includeDeploymentDirs: + description: |- + IncludeDeploymentDirs instructs kluctl to only include deployments with the given dir. + Equivalent to using '--include-deployment-dir' when calling kluctl. + items: + type: string + type: array + includeTags: + description: |- + IncludeTags instructs kluctl to only include deployments with given tags. + Equivalent to using '--include-tag' when calling kluctl. + items: + type: string + type: array + interval: + description: |- + The interval at which to reconcile the KluctlDeployment. + Reconciliation means that the deployment is fully rendered and only deployed when the result changes compared + to the last deployment. + To override this behavior, set the DeployInterval value. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + kubeConfig: + description: |- + The KubeConfig for deploying to the target cluster. + Specifies the kubeconfig to be used when invoking kluctl. Contexts in this kubeconfig must match + the context found in the kluctl target. As an alternative, specify the context to be used via 'context' + properties: + secretRef: + description: |- + SecretRef holds the 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'. The secret must be in the same namespace as + the Kustomization. + 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 + the KluctlDeployment. + 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 + manual: + description: |- + Manual enables manual deployments, meaning that the deployment will initially start as a dry run deployment + and only after manual approval cause a real deployment + type: boolean + manualObjectsHash: + description: |- + ManualObjectsHash specifies the rendered objects hash that is approved for manual deployment. + If Manual is set to true, the controller will skip deployments when the current reconciliation loops calculated + objects hash does not match this value. + There are two ways to use this value properly. + 1. Set it manually to the value found in status.lastObjectsHash. + 2. Use the Kluctl Webui to manually approve a deployment, which will set this field appropriately. + type: string + noWait: + default: false + description: |- + NoWait instructs kluctl to not wait for any resources to become ready, including hooks. + Equivalent to using '--no-wait' when calling kluctl. + type: boolean + prune: + default: false + description: Prune enables pruning after deploying. + type: boolean + replaceOnError: + default: false + description: |- + ReplaceOnError instructs kluctl to replace resources on error. + Equivalent to using '--replace-on-error' when calling kluctl. + type: boolean + retryInterval: + description: |- + The interval at which to retry a previously failed reconciliation. + When not specified, the controller uses the Interval + value to retry failures. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + serviceAccountName: + description: |- + The name of the Kubernetes service account to use while deploying. + If not specified, the default service account is used. + type: string + source: + description: Specifies the project source location + properties: + credentials: + description: |- + Credentials specifies a list of secrets with credentials + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.git instead. + items: + properties: + host: + description: |- + Host specifies the hostname that this secret applies to. If set to '*', this set of credentials + applies to all hosts. + Using '*' for http(s) based repositories is not supported, meaning that such credentials sets will be ignored. + You must always set a proper hostname in that case. + type: string + pathPrefix: + description: |- + PathPrefix specifies the path prefix to be used to filter source urls. Only urls that have this prefix will use + this set of credentials. + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + the git repository. + For HTTPS git repositories the Secret must contain 'username' and 'password' + fields. + For SSH git repositories the Secret must contain 'identity' + and 'known_hosts' fields. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - secretRef + type: object + type: array + git: + description: Git specifies a git repository as project source + properties: + path: + description: Path specifies the sub-directory to be used as + project directory + type: string + ref: + description: Ref specifies the branch, tag or commit that + should be used. If omitted, the default branch of the repo + is used. + properties: + branch: + description: Branch to use. + type: string + commit: + description: Commit SHA to use. + type: string + tag: + description: Tag to use. + type: string + type: object + url: + description: |- + URL specifies the Git url where the project source is located. If the given Git repository needs authentication, + use spec.credentials.git to specify those. + type: string + required: + - url + type: object + oci: + description: Oci specifies an OCI repository as project source + properties: + path: + description: Path specifies the sub-directory to be used as + project directory + type: string + ref: + description: Ref specifies the tag to be used. If omitted, + the "latest" tag is used. + properties: + digest: + description: |- + Digest is the image digest to pull, takes precedence over SemVer. + The value should be in the format 'sha256:'. + type: string + tag: + description: Tag is the image tag to pull, defaults to + latest. + type: string + type: object + url: + description: |- + Url specifies the Git url where the project source is located. If the given OCI repository needs authentication, + use spec.credentials.oci to specify those. + type: string + required: + - url + type: object + path: + description: |- + Path specifies the sub-directory to be used as project directory + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.path instead. + type: string + ref: + description: |- + Ref specifies the branch, tag or commit that should be used. If omitted, the default branch of the repo is used. + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.ref instead. + properties: + branch: + description: Branch to use. + type: string + commit: + description: Commit SHA to use. + type: string + tag: + description: Tag to use. + type: string + type: object + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + See ProjectSourceCredentials.SecretRef for details + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.git + instead. + WARNING using this field causes the controller to pass http basic auth credentials to ALL repositories involved. + Use spec.credentials.git with a proper Host field instead. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + url: + description: |- + Url specifies the Git url where the project source is located + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.url instead. + type: string + type: object + sourceOverrides: + description: Specifies source overrides + items: + properties: + isGroup: + type: boolean + repoKey: + type: string + url: + type: string + required: + - repoKey + - url + type: object + type: array + suspend: + description: |- + This flag tells the controller to suspend subsequent kluctl executions, + it does not apply to already started executions. Defaults to false. + type: boolean + target: + description: |- + Target specifies the kluctl target to deploy. If not specified, an empty target is used that has no name and no + context. Use 'TargetName' and 'Context' to specify the name and context in that case. + maxLength: 63 + minLength: 1 + type: string + targetNameOverride: + description: TargetNameOverride sets or overrides the target name. + This is especially useful when deployment without a target. + maxLength: 63 + minLength: 1 + type: string + timeout: + description: |- + Timeout for all operations. + Defaults to 'Interval' duration. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + validate: + default: true + description: Validate enables validation after deploying + type: boolean + validateInterval: + description: |- + ValidateInterval specifies the interval at which to validate the KluctlDeployment. + Validation is performed the same way as with 'kluctl validate -t '. + Defaults to the same value as specified in Interval. + Validate is also performed whenever a deployment is performed, independent of the value of ValidateInterval + pattern: ^(([0-9]+(\.[0-9]+)?(ms|s|m|h))+) + type: string + required: + - interval + - source + type: object + status: + description: KluctlDeploymentStatus defines the observed state of KluctlDeployment + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + 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 + deployRequestResult: + properties: + commandError: + type: string + endTime: + format: date-time + type: string + reconcileId: + type: string + request: + description: ManualRequest is used in json form inside the manual + request annotations + properties: + overridesPatch: + type: object + x-kubernetes-preserve-unknown-fields: true + requestValue: + type: string + required: + - requestValue + type: object + resultId: + type: string + startTime: + format: date-time + type: string + required: + - reconcileId + - request + - startTime + type: object + diffRequestResult: + properties: + commandError: + type: string + endTime: + format: date-time + type: string + reconcileId: + type: string + request: + description: ManualRequest is used in json form inside the manual + request annotations + properties: + overridesPatch: + type: object + x-kubernetes-preserve-unknown-fields: true + requestValue: + type: string + required: + - requestValue + type: object + resultId: + type: string + startTime: + format: date-time + type: string + required: + - reconcileId + - request + - startTime + type: object + lastDeployResult: + description: LastDeployResult is the result summary of the last deploy + command + type: object + x-kubernetes-preserve-unknown-fields: true + lastDiffResult: + description: LastDiffResult is the result summary of the last diff + command + type: object + x-kubernetes-preserve-unknown-fields: true + lastDriftDetectionResult: + description: |- + LastDriftDetectionResult is the result of the last drift detection command + optional + type: object + x-kubernetes-preserve-unknown-fields: true + lastDriftDetectionResultMessage: + description: |- + LastDriftDetectionResultMessage contains a short message that describes the drift + optional + type: string + lastManualObjectsHash: + type: string + lastObjectsHash: + type: string + lastPrepareError: + type: string + lastValidateResult: + description: LastValidateResult is the result summary of the last + validate command + type: object + x-kubernetes-preserve-unknown-fields: true + observedCommit: + description: ObservedCommit is the last commit observed + type: string + observedGeneration: + description: ObservedGeneration is the last reconciled generation. + format: int64 + type: integer + projectKey: + properties: + repoKey: + type: string + subDir: + type: string + type: object + pruneRequestResult: + properties: + commandError: + type: string + endTime: + format: date-time + type: string + reconcileId: + type: string + request: + description: ManualRequest is used in json form inside the manual + request annotations + properties: + overridesPatch: + type: object + x-kubernetes-preserve-unknown-fields: true + requestValue: + type: string + required: + - requestValue + type: object + resultId: + type: string + startTime: + format: date-time + type: string + required: + - reconcileId + - request + - startTime + type: object + reconcileRequestResult: + properties: + commandError: + type: string + endTime: + format: date-time + type: string + reconcileId: + type: string + request: + description: ManualRequest is used in json form inside the manual + request annotations + properties: + overridesPatch: + type: object + x-kubernetes-preserve-unknown-fields: true + requestValue: + type: string + required: + - requestValue + type: object + resultId: + type: string + startTime: + format: date-time + type: string + required: + - reconcileId + - request + - startTime + type: object + targetKey: + properties: + clusterId: + type: string + discriminator: + type: string + targetName: + type: string + required: + - clusterId + type: object + validateRequestResult: + properties: + commandError: + type: string + endTime: + format: date-time + type: string + reconcileId: + type: string + request: + description: ManualRequest is used in json form inside the manual + request annotations + properties: + overridesPatch: + type: object + x-kubernetes-preserve-unknown-fields: true + requestValue: + type: string + required: + - requestValue + type: object + resultId: + type: string + startTime: + format: date-time + type: string + required: + - reconcileId + - request + - startTime + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 000000000..d5f5b903f --- /dev/null +++ b/config/crd/kustomization.yaml @@ -0,0 +1,21 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/gitops.kluctl.io_kluctldeployments.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_kluctldeployments.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_kluctldeployments.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000..ec5c150a9 --- /dev/null +++ b/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/config/crd/patches/cainjection_in_kluctldeployments.yaml b/config/crd/patches/cainjection_in_kluctldeployments.yaml new file mode 100644 index 000000000..4adca8433 --- /dev/null +++ b/config/crd/patches/cainjection_in_kluctldeployments.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: kluctldeployments.gitops.kluctl.io diff --git a/config/crd/patches/webhook_in_kluctldeployments.yaml b/config/crd/patches/webhook_in_kluctldeployments.yaml new file mode 100644 index 000000000..07d04c73e --- /dev/null +++ b/config/crd/patches/webhook_in_kluctldeployments.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kluctldeployments.gitops.kluctl.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: kluctl-system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml new file mode 100644 index 000000000..98deb64cd --- /dev/null +++ b/config/default/kustomization.yaml @@ -0,0 +1,5 @@ + +resources: +- ../crd +- ../rbac +- ../manager diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml new file mode 100644 index 000000000..5c5f0b84c --- /dev/null +++ b/config/manager/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- manager.yaml diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 000000000..82cab85df --- /dev/null +++ b/config/manager/manager.yaml @@ -0,0 +1,106 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: namespace + app.kubernetes.io/instance: system + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: controller + app.kubernetes.io/part-of: controller + app.kubernetes.io/managed-by: kluctl + name: kluctl-system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kluctl-controller + namespace: kluctl-system + labels: + control-plane: kluctl-controller + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: kluctl-controller + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: controller + app.kubernetes.io/part-of: controller + app.kubernetes.io/managed-by: kluctl +spec: + selector: + matchLabels: + control-plane: kluctl-controller + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: kluctl-controller + spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: controller + image: ghcr.io/kluctl/kluctl:latest + imagePullPolicy: IfNotPresent + command: + - kluctl + - controller + - run + args: + - --leader-elect + env: [] + ports: + - containerPort: 8080 + name: metrics + - containerPort: 8082 + name: source-override + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 500m + memory: 512Mi + serviceAccountName: kluctl-controller + terminationGracePeriodSeconds: 10 diff --git a/config/rbac/kluctldeployment_editor_role.yaml b/config/rbac/kluctldeployment_editor_role.yaml new file mode 100644 index 000000000..bdd62da70 --- /dev/null +++ b/config/rbac/kluctldeployment_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit kluctldeployments. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: kluctldeployment-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/part-of: controller + app.kubernetes.io/managed-by: kluctl + name: kluctldeployment-editor-role +rules: +- apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments/status + verbs: + - get diff --git a/config/rbac/kluctldeployment_viewer_role.yaml b/config/rbac/kluctldeployment_viewer_role.yaml new file mode 100644 index 000000000..bee8b81ea --- /dev/null +++ b/config/rbac/kluctldeployment_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view kluctldeployments. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: kluctldeployment-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/part-of: controller + app.kubernetes.io/managed-by: kluctl + name: kluctldeployment-viewer-role +rules: +- apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments + verbs: + - get + - list + - watch +- apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml new file mode 100644 index 000000000..588dbee4b --- /dev/null +++ b/config/rbac/kustomization.yaml @@ -0,0 +1,12 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +- reconciler_binding.yaml diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000..4d4c486d0 --- /dev/null +++ b/config/rbac/leader_election_role.yaml @@ -0,0 +1,45 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/part-of: controller + app.kubernetes.io/managed-by: kluctl + name: kluctl-controller-leader-election-role + namespace: kluctl-system +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000..1ddbed5d3 --- /dev/null +++ b/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,20 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/part-of: controller + app.kubernetes.io/managed-by: kluctl + name: kluctl-controller-leader-election-rolebinding + namespace: kluctl-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kluctl-controller-leader-election-role +subjects: +- kind: ServiceAccount + name: kluctl-controller + namespace: kluctl-system diff --git a/config/rbac/reconciler_binding.yaml b/config/rbac/reconciler_binding.yaml new file mode 100644 index 000000000..f85f09807 --- /dev/null +++ b/config/rbac/reconciler_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kluctl-controller-cluster-admin +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: kluctl-controller + namespace: kluctl-system diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 000000000..03ed9ed6f --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kluctl-controller-role +rules: +- apiGroups: + - "" + resources: + - configmaps + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments/status + verbs: + - get + - patch + - update diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml new file mode 100644 index 000000000..7381f094e --- /dev/null +++ b/config/rbac/role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: kluctl-controller-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/part-of: controller + app.kubernetes.io/managed-by: kluctl + name: kluctl-controller-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kluctl-controller-role +subjects: +- kind: ServiceAccount + name: kluctl-controller + namespace: kluctl-system diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml new file mode 100644 index 000000000..b5f5dd8fc --- /dev/null +++ b/config/rbac/service_account.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: kluctl-controller-sa + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/part-of: controller + app.kubernetes.io/managed-by: kluctl + name: kluctl-controller + namespace: kluctl-system diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..0e06dbae8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,16 @@ + + +# Table of Contents + +1. [Kluctl](./kluctl) +2. [Kluctl GitOps](./gitops) +3. [Kluctl Webui](./webui) diff --git a/docs/gitops/README.md b/docs/gitops/README.md new file mode 100644 index 000000000..a3aa71116 --- /dev/null +++ b/docs/gitops/README.md @@ -0,0 +1,125 @@ + + +# Kluctl GitOps + +GitOps in Kluctl is implemented through the Kluctl Controller, which must be [installed](./installation.md) +to your target cluster. + +The Kluctl Controller is a Kubernetes operator which implements the [`KluctlDeployment`](./spec/v1beta1/kluctldeployment.md#kluctldeployment) +custom resource. This resource allows to define a Kluctl deployment that should be constantly reconciled (re-deployed) +whenever the deployment changes. + +It is suggested to read through the [GitOps Recipe](https://kluctl.io/docs/recipes/gitops/) to get a basic +understanding of how to use it. + +## Motivation and Philosophy + +Kluctl tries its best to implement all its features via [Kluctl projects](../kluctl/kluctl-project/README.md), meaning that +the deployments are, at least theoretically, deployable from the CLI at all times. The Kluctl Controller does not +add functionality on top of that and thus does not couple your deployments to a running controller. + +Instead, the `KluctlDeployment` custom resource acts as an interface to the deployment. It tries to offer the same +functionality and options as offered by the CLI, but through a custom resource instead of a CLI invocation. + +As an example, arguments passed via `-a arg=value` can be passed to the custom resource via the `spec.args` field. +The same applies to options like `--dry-run`, which equals to `spec.dryRun: true` in the custom resource. Check the +documentation of [`KluctlDeployment`](./spec/v1beta1/kluctldeployment.md#spec-fields) for more such options. + +## GitOps Commands + +Kluctl GitOps deployments can be controlled via the Kluctl CLI interface, e.g. with +`kluctl gitops deploy --namespace my-ns --name my-deployment`, which will trigger a deployment and wait for it to finish. + +See [commands](../kluctl/commands/README.md) and the +[GitOps recipe](https://kluctl.io/docs/recipes/gitops/#gitops-commands) for more details. + +## Kluctl Webui + +The same deployments can also be controlled and monitored via the [Kluctl Webui](../webui/README.md). + +## Installation + +Installation instructions can be found [here](./installation.md) + +## Design + +The reconciliation process consists of multiple steps which are constantly repeated: + +- **clone** the root Kluctl project via Git +- **prepare** the Kluctl deployment by rendering the whole deployment +- **deploy** the specified target via [kluctl deploy](../kluctl/commands/deploy.md) if the rendered resources changed +- **prune** orphaned objects via [kluctl prune](../kluctl/commands/prune.md) +- **validate** the deployment status via [kluctl validate](../kluctl/commands/validate.md) +- **drift-detection** is performed to allow the [Kluctl Webui](../webui/README.md) to show drift. + +Reconciliation is performed on a configurable [interval](./spec/v1beta1/kluctldeployment.md#interval). A single +reconciliation iteration will first clone and prepare the project. Only when the rendered resources indicate a change +(by using a hash internally), the controller will initiate a deployment. After the deployment, the controller will +also perform pruning (only if [prune: true](./spec/v1beta1/kluctldeployment.md#prune) is set). + +When the `KluctlDeployment` is removed from the cluster, the controller cal also delete all resources belonging to +that deployment. This will only happen if [delete: true](./spec/v1beta1/kluctldeployment.md#delete) is set. + +Deletion and pruning is based on the [discriminator](../kluctl/kluctl-project/README.md#discriminator) of the given target. + +A `KluctlDeployment` can be [suspended](./spec/v1beta1/kluctldeployment.md#suspend). While suspended, the controller +will skip reconciliation, including deletion and pruning. + +The API design of the controller can be found at [kluctldeployment.gitops.kluctl.io/v1beta1](./spec/v1beta1/README.md). + +## Example + +After installing the Kluctl Controller, we can create a `KluctlDeployment` that automatically deploys the +[Microservices Demo](https://kluctl.io/docs/tutorials/microservices-demo/3-templating-and-multi-env/). + +Create a KluctlDeployment that uses the demo project source to deploy the `test` target to the same cluster that the +controller runs on. + +```yaml +apiVersion: gitops.kluctl.io/v1beta1 +kind: KluctlDeployment +metadata: + name: microservices-demo-test + namespace: kluctl-system +spec: + interval: 10m + source: + git: + url: https://github.com/kluctl/kluctl-examples.git + path: "./microservices-demo/3-templating-and-multi-env/" + timeout: 2m + target: test + context: default + prune: true +``` + +This example will deploy a fully-fledged microservices application with multiple backend services, frontends and +databases, all via one single `KluctlDeployment`. + +To deploy the same Kluctl project to another target (e.g. prod), simply create the following resource. + +```yaml +apiVersion: gitops.kluctl.io/v1beta1 +kind: KluctlDeployment +metadata: + name: microservices-demo-prod + namespace: kluctl-system +spec: + interval: 10m + source: + git: + url: https://github.com/kluctl/kluctl-examples.git + path: "./microservices-demo/3-templating-and-multi-env/" + timeout: 2m + target: prod + context: default + prune: true +``` diff --git a/docs/gitops/api/kluctl-controller.front-matter.yaml b/docs/gitops/api/kluctl-controller.front-matter.yaml new file mode 100644 index 000000000..be96cbbcb --- /dev/null +++ b/docs/gitops/api/kluctl-controller.front-matter.yaml @@ -0,0 +1,4 @@ +title: Kluctl Controller API reference +linkTitle: Kluctl Controller API +description: Kluctl Controller API reference +weight: 300 diff --git a/docs/gitops/api/kluctl-controller.md b/docs/gitops/api/kluctl-controller.md new file mode 100644 index 000000000..b162b22ec --- /dev/null +++ b/docs/gitops/api/kluctl-controller.md @@ -0,0 +1,2249 @@ +

Kluctl Controller API reference

+

Packages:

+ +

gitops.kluctl.io/v1beta1

+

Package v1beta1 contains API Schema definitions for the gitops.kluctl.io v1beta1 API group.

+Resource Types: +
    +

    Decryption +

    +

    +(Appears on: +KluctlDeploymentSpec) +

    +

    Decryption defines how decryption is handled for Kubernetes manifests.

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +provider
    + +string + +
    +

    Provider is the name of the decryption engine.

    +
    +secretRef
    + + +LocalObjectReference + + +
    +(Optional) +

    The secret name containing the private OpenPGP keys used for decryption.

    +
    +serviceAccount
    + +string + +
    +(Optional) +

    ServiceAccount specifies the service account used to authenticate against cloud providers. +This is currently only usable for AWS KMS keys. The specified service account will be used to authenticate to AWS +by signing a token in an IRSA compliant way.

    +
    +
    +
    +

    HelmCredentials +

    +

    +(Appears on: +KluctlDeploymentSpec) +

    +
    +
    + + + + + + + + + + + + + +
    FieldDescription
    +secretRef
    + + +LocalObjectReference + + +
    +

    SecretRef holds the name of a secret that contains the Helm credentials. +The secret must either contain the fields credentialsId which refers to the credentialsId +found in https://kluctl.io/docs/kluctl/reference/deployments/helm/#private-repositories or an url used +to match the credentials found in Kluctl projects helm-chart.yaml files. +The secret can either container basic authentication credentials via username and password or +TLS authentication via certFile and keyFile. caFile can be specified to override the CA to use while +contacting the repository. +The secret can also contain insecureSkipTlsVerify: "true", which will disable TLS verification. +passCredentialsAll: "true" can be specified to make the controller pass credentials to all requests, even if +the hostname changes in-between.

    +
    +
    +
    +

    KluctlDeployment +

    +

    KluctlDeployment is the Schema for the kluctldeployments API

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +metadata
    + + +Kubernetes meta/v1.ObjectMeta + + +
    +Refer to the Kubernetes API documentation for the fields of the +metadata field. +
    +spec
    + + +KluctlDeploymentSpec + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +source
    + + +ProjectSource + + +
    +

    Specifies the project source location

    +
    +sourceOverrides
    + + +[]SourceOverride + + +
    +(Optional) +

    Specifies source overrides

    +
    +credentials
    + + +ProjectCredentials + + +
    +(Optional) +

    Credentials specifies the credentials used when pulling sources

    +
    +decryption
    + + +Decryption + + +
    +(Optional) +

    Decrypt Kubernetes secrets before applying them on the cluster.

    +
    +interval
    + + +Kubernetes meta/v1.Duration + + +
    +

    The interval at which to reconcile the KluctlDeployment. +Reconciliation means that the deployment is fully rendered and only deployed when the result changes compared +to the last deployment. +To override this behavior, set the DeployInterval value.

    +
    +retryInterval
    + + +Kubernetes meta/v1.Duration + + +
    +(Optional) +

    The interval at which to retry a previously failed reconciliation. +When not specified, the controller uses the Interval +value to retry failures.

    +
    +deployInterval
    + + +SafeDuration + + +
    +(Optional) +

    DeployInterval specifies the interval at which to deploy the KluctlDeployment, even in cases the rendered +result does not change.

    +
    +validateInterval
    + + +SafeDuration + + +
    +(Optional) +

    ValidateInterval specifies the interval at which to validate the KluctlDeployment. +Validation is performed the same way as with ‘kluctl validate -t ’. +Defaults to the same value as specified in Interval. +Validate is also performed whenever a deployment is performed, independent of the value of ValidateInterval

    +
    +timeout
    + + +Kubernetes meta/v1.Duration + + +
    +(Optional) +

    Timeout for all operations. +Defaults to ‘Interval’ duration.

    +
    +suspend
    + +bool + +
    +(Optional) +

    This flag tells the controller to suspend subsequent kluctl executions, +it does not apply to already started executions. Defaults to false.

    +
    +helmCredentials
    + + +[]HelmCredentials + + +
    +(Optional) +

    HelmCredentials is a list of Helm credentials used when non pre-pulled Helm Charts are used inside a +Kluctl deployment. +DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.helm instead.

    +
    +serviceAccountName
    + +string + +
    +(Optional) +

    The name of the Kubernetes service account to use while deploying. +If not specified, the default service account is used.

    +
    +kubeConfig
    + + +KubeConfig + + +
    +(Optional) +

    The KubeConfig for deploying to the target cluster. +Specifies the kubeconfig to be used when invoking kluctl. Contexts in this kubeconfig must match +the context found in the kluctl target. As an alternative, specify the context to be used via ‘context’

    +
    +target
    + +string + +
    +(Optional) +

    Target specifies the kluctl target to deploy. If not specified, an empty target is used that has no name and no +context. Use ‘TargetName’ and ‘Context’ to specify the name and context in that case.

    +
    +targetNameOverride
    + +string + +
    +(Optional) +

    TargetNameOverride sets or overrides the target name. This is especially useful when deployment without a target.

    +
    +context
    + +string + +
    +(Optional) +

    If specified, overrides the context to be used. This will effectively make kluctl ignore the context specified +in the target.

    +
    +args
    + +k8s.io/apimachinery/pkg/runtime.RawExtension + +
    +(Optional) +

    Args specifies dynamic target args.

    +
    +images
    + +[]github.com/kluctl/kluctl/v2/pkg/types.FixedImage + +
    +(Optional) +

    Images contains a list of fixed image overrides. +Equivalent to using ‘–fixed-images-file’ when calling kluctl.

    +
    +dryRun
    + +bool + +
    +(Optional) +

    DryRun instructs kluctl to run everything in dry-run mode. +Equivalent to using ‘–dry-run’ when calling kluctl.

    +
    +noWait
    + +bool + +
    +(Optional) +

    NoWait instructs kluctl to not wait for any resources to become ready, including hooks. +Equivalent to using ‘–no-wait’ when calling kluctl.

    +
    +forceApply
    + +bool + +
    +(Optional) +

    ForceApply instructs kluctl to force-apply in case of SSA conflicts. +Equivalent to using ‘–force-apply’ when calling kluctl.

    +
    +replaceOnError
    + +bool + +
    +(Optional) +

    ReplaceOnError instructs kluctl to replace resources on error. +Equivalent to using ‘–replace-on-error’ when calling kluctl.

    +
    +forceReplaceOnError
    + +bool + +
    +(Optional) +

    ForceReplaceOnError instructs kluctl to force-replace resources in case a normal replace fails. +Equivalent to using ‘–force-replace-on-error’ when calling kluctl.

    +
    +abortOnError
    + +bool + +
    +(Optional) +

    ForceReplaceOnError instructs kluctl to abort deployments immediately when something fails. +Equivalent to using ‘–abort-on-error’ when calling kluctl.

    +
    +includeTags
    + +[]string + +
    +(Optional) +

    IncludeTags instructs kluctl to only include deployments with given tags. +Equivalent to using ‘–include-tag’ when calling kluctl.

    +
    +excludeTags
    + +[]string + +
    +(Optional) +

    ExcludeTags instructs kluctl to exclude deployments with given tags. +Equivalent to using ‘–exclude-tag’ when calling kluctl.

    +
    +includeDeploymentDirs
    + +[]string + +
    +(Optional) +

    IncludeDeploymentDirs instructs kluctl to only include deployments with the given dir. +Equivalent to using ‘–include-deployment-dir’ when calling kluctl.

    +
    +excludeDeploymentDirs
    + +[]string + +
    +(Optional) +

    ExcludeDeploymentDirs instructs kluctl to exclude deployments with the given dir. +Equivalent to using ‘–exclude-deployment-dir’ when calling kluctl.

    +
    +deployMode
    + +string + +
    +(Optional) +

    DeployMode specifies what deploy mode should be used. +The options ‘full-deploy’ and ‘poke-images’ are supported. +With the ‘poke-images’ option, only images are patched into the target without performing a full deployment.

    +
    +validate
    + +bool + +
    +(Optional) +

    Validate enables validation after deploying

    +
    +prune
    + +bool + +
    +(Optional) +

    Prune enables pruning after deploying.

    +
    +delete
    + +bool + +
    +(Optional) +

    Delete enables deletion of the specified target when the KluctlDeployment object gets deleted.

    +
    +manual
    + +bool + +
    +(Optional) +

    Manual enables manual deployments, meaning that the deployment will initially start as a dry run deployment +and only after manual approval cause a real deployment

    +
    +manualObjectsHash
    + +string + +
    +(Optional) +

    ManualObjectsHash specifies the rendered objects hash that is approved for manual deployment. +If Manual is set to true, the controller will skip deployments when the current reconciliation loops calculated +objects hash does not match this value. +There are two ways to use this value properly. +1. Set it manually to the value found in status.lastObjectsHash. +2. Use the Kluctl Webui to manually approve a deployment, which will set this field appropriately.

    +
    +
    +status
    + + +KluctlDeploymentStatus + + +
    +
    +
    +
    +

    KluctlDeploymentSpec +

    +

    +(Appears on: +KluctlDeployment) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +source
    + + +ProjectSource + + +
    +

    Specifies the project source location

    +
    +sourceOverrides
    + + +[]SourceOverride + + +
    +(Optional) +

    Specifies source overrides

    +
    +credentials
    + + +ProjectCredentials + + +
    +(Optional) +

    Credentials specifies the credentials used when pulling sources

    +
    +decryption
    + + +Decryption + + +
    +(Optional) +

    Decrypt Kubernetes secrets before applying them on the cluster.

    +
    +interval
    + + +Kubernetes meta/v1.Duration + + +
    +

    The interval at which to reconcile the KluctlDeployment. +Reconciliation means that the deployment is fully rendered and only deployed when the result changes compared +to the last deployment. +To override this behavior, set the DeployInterval value.

    +
    +retryInterval
    + + +Kubernetes meta/v1.Duration + + +
    +(Optional) +

    The interval at which to retry a previously failed reconciliation. +When not specified, the controller uses the Interval +value to retry failures.

    +
    +deployInterval
    + + +SafeDuration + + +
    +(Optional) +

    DeployInterval specifies the interval at which to deploy the KluctlDeployment, even in cases the rendered +result does not change.

    +
    +validateInterval
    + + +SafeDuration + + +
    +(Optional) +

    ValidateInterval specifies the interval at which to validate the KluctlDeployment. +Validation is performed the same way as with ‘kluctl validate -t ’. +Defaults to the same value as specified in Interval. +Validate is also performed whenever a deployment is performed, independent of the value of ValidateInterval

    +
    +timeout
    + + +Kubernetes meta/v1.Duration + + +
    +(Optional) +

    Timeout for all operations. +Defaults to ‘Interval’ duration.

    +
    +suspend
    + +bool + +
    +(Optional) +

    This flag tells the controller to suspend subsequent kluctl executions, +it does not apply to already started executions. Defaults to false.

    +
    +helmCredentials
    + + +[]HelmCredentials + + +
    +(Optional) +

    HelmCredentials is a list of Helm credentials used when non pre-pulled Helm Charts are used inside a +Kluctl deployment. +DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.helm instead.

    +
    +serviceAccountName
    + +string + +
    +(Optional) +

    The name of the Kubernetes service account to use while deploying. +If not specified, the default service account is used.

    +
    +kubeConfig
    + + +KubeConfig + + +
    +(Optional) +

    The KubeConfig for deploying to the target cluster. +Specifies the kubeconfig to be used when invoking kluctl. Contexts in this kubeconfig must match +the context found in the kluctl target. As an alternative, specify the context to be used via ‘context’

    +
    +target
    + +string + +
    +(Optional) +

    Target specifies the kluctl target to deploy. If not specified, an empty target is used that has no name and no +context. Use ‘TargetName’ and ‘Context’ to specify the name and context in that case.

    +
    +targetNameOverride
    + +string + +
    +(Optional) +

    TargetNameOverride sets or overrides the target name. This is especially useful when deployment without a target.

    +
    +context
    + +string + +
    +(Optional) +

    If specified, overrides the context to be used. This will effectively make kluctl ignore the context specified +in the target.

    +
    +args
    + +k8s.io/apimachinery/pkg/runtime.RawExtension + +
    +(Optional) +

    Args specifies dynamic target args.

    +
    +images
    + +[]github.com/kluctl/kluctl/v2/pkg/types.FixedImage + +
    +(Optional) +

    Images contains a list of fixed image overrides. +Equivalent to using ‘–fixed-images-file’ when calling kluctl.

    +
    +dryRun
    + +bool + +
    +(Optional) +

    DryRun instructs kluctl to run everything in dry-run mode. +Equivalent to using ‘–dry-run’ when calling kluctl.

    +
    +noWait
    + +bool + +
    +(Optional) +

    NoWait instructs kluctl to not wait for any resources to become ready, including hooks. +Equivalent to using ‘–no-wait’ when calling kluctl.

    +
    +forceApply
    + +bool + +
    +(Optional) +

    ForceApply instructs kluctl to force-apply in case of SSA conflicts. +Equivalent to using ‘–force-apply’ when calling kluctl.

    +
    +replaceOnError
    + +bool + +
    +(Optional) +

    ReplaceOnError instructs kluctl to replace resources on error. +Equivalent to using ‘–replace-on-error’ when calling kluctl.

    +
    +forceReplaceOnError
    + +bool + +
    +(Optional) +

    ForceReplaceOnError instructs kluctl to force-replace resources in case a normal replace fails. +Equivalent to using ‘–force-replace-on-error’ when calling kluctl.

    +
    +abortOnError
    + +bool + +
    +(Optional) +

    ForceReplaceOnError instructs kluctl to abort deployments immediately when something fails. +Equivalent to using ‘–abort-on-error’ when calling kluctl.

    +
    +includeTags
    + +[]string + +
    +(Optional) +

    IncludeTags instructs kluctl to only include deployments with given tags. +Equivalent to using ‘–include-tag’ when calling kluctl.

    +
    +excludeTags
    + +[]string + +
    +(Optional) +

    ExcludeTags instructs kluctl to exclude deployments with given tags. +Equivalent to using ‘–exclude-tag’ when calling kluctl.

    +
    +includeDeploymentDirs
    + +[]string + +
    +(Optional) +

    IncludeDeploymentDirs instructs kluctl to only include deployments with the given dir. +Equivalent to using ‘–include-deployment-dir’ when calling kluctl.

    +
    +excludeDeploymentDirs
    + +[]string + +
    +(Optional) +

    ExcludeDeploymentDirs instructs kluctl to exclude deployments with the given dir. +Equivalent to using ‘–exclude-deployment-dir’ when calling kluctl.

    +
    +deployMode
    + +string + +
    +(Optional) +

    DeployMode specifies what deploy mode should be used. +The options ‘full-deploy’ and ‘poke-images’ are supported. +With the ‘poke-images’ option, only images are patched into the target without performing a full deployment.

    +
    +validate
    + +bool + +
    +(Optional) +

    Validate enables validation after deploying

    +
    +prune
    + +bool + +
    +(Optional) +

    Prune enables pruning after deploying.

    +
    +delete
    + +bool + +
    +(Optional) +

    Delete enables deletion of the specified target when the KluctlDeployment object gets deleted.

    +
    +manual
    + +bool + +
    +(Optional) +

    Manual enables manual deployments, meaning that the deployment will initially start as a dry run deployment +and only after manual approval cause a real deployment

    +
    +manualObjectsHash
    + +string + +
    +(Optional) +

    ManualObjectsHash specifies the rendered objects hash that is approved for manual deployment. +If Manual is set to true, the controller will skip deployments when the current reconciliation loops calculated +objects hash does not match this value. +There are two ways to use this value properly. +1. Set it manually to the value found in status.lastObjectsHash. +2. Use the Kluctl Webui to manually approve a deployment, which will set this field appropriately.

    +
    +
    +
    +

    KluctlDeploymentStatus +

    +

    +(Appears on: +KluctlDeployment) +

    +

    KluctlDeploymentStatus defines the observed state of KluctlDeployment

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +reconcileRequestResult
    + + +ManualRequestResult + + +
    +(Optional) +
    +diffRequestResult
    + + +ManualRequestResult + + +
    +(Optional) +
    +deployRequestResult
    + + +ManualRequestResult + + +
    +(Optional) +
    +pruneRequestResult
    + + +ManualRequestResult + + +
    +(Optional) +
    +validateRequestResult
    + + +ManualRequestResult + + +
    +(Optional) +
    +observedGeneration
    + +int64 + +
    +(Optional) +

    ObservedGeneration is the last reconciled generation.

    +
    +observedCommit
    + +string + +
    +

    ObservedCommit is the last commit observed

    +
    +conditions
    + + +[]Kubernetes meta/v1.Condition + + +
    +(Optional) +
    +projectKey
    + +github.com/kluctl/kluctl/lib/git/types.ProjectKey + +
    +(Optional) +
    +targetKey
    + +github.com/kluctl/kluctl/v2/pkg/types/result.TargetKey + +
    +(Optional) +
    +lastObjectsHash
    + +string + +
    +(Optional) +
    +lastManualObjectsHash
    + +string + +
    +(Optional) +
    +lastPrepareError
    + +string + +
    +(Optional) +
    +lastDiffResult
    + +k8s.io/apimachinery/pkg/runtime.RawExtension + +
    +(Optional) +

    LastDiffResult is the result summary of the last diff command

    +
    +lastDeployResult
    + +k8s.io/apimachinery/pkg/runtime.RawExtension + +
    +(Optional) +

    LastDeployResult is the result summary of the last deploy command

    +
    +lastValidateResult
    + +k8s.io/apimachinery/pkg/runtime.RawExtension + +
    +(Optional) +

    LastValidateResult is the result summary of the last validate command

    +
    +lastDriftDetectionResult
    + +k8s.io/apimachinery/pkg/runtime.RawExtension + +
    +

    LastDriftDetectionResult is the result of the last drift detection command +optional

    +
    +lastDriftDetectionResultMessage
    + +string + +
    +

    LastDriftDetectionResultMessage contains a short message that describes the drift +optional

    +
    +
    +
    +

    KubeConfig +

    +

    +(Appears on: +KluctlDeploymentSpec) +

    +

    KubeConfig references a Kubernetes secret that contains a kubeconfig file.

    +
    +
    + + + + + + + + + + + + + +
    FieldDescription
    +secretRef
    + + +SecretKeyReference + + +
    +

    SecretRef holds the 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’. The secret must be in the same namespace as +the Kustomization. +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 +the KluctlDeployment.

    +
    +
    +
    +

    LocalObjectReference +

    +

    +(Appears on: +Decryption, +HelmCredentials, +ProjectCredentialsGit, +ProjectCredentialsGitDeprecated, +ProjectCredentialsHelm, +ProjectCredentialsOci, +ProjectSource) +

    +
    +
    + + + + + + + + + + + + + +
    FieldDescription
    +name
    + +string + +
    +

    Name of the referent.

    +
    +
    +
    +

    ManualRequest +

    +

    +(Appears on: +ManualRequestResult) +

    +

    ManualRequest is used in json form inside the manual request annotations

    +
    +
    + + + + + + + + + + + + + + + + + +
    FieldDescription
    +requestValue
    + +string + +
    +
    +overridesPatch
    + +k8s.io/apimachinery/pkg/runtime.RawExtension + +
    +(Optional) +
    +
    +
    +

    ManualRequestResult +

    +

    +(Appears on: +KluctlDeploymentStatus) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +request
    + + +ManualRequest + + +
    +
    +startTime
    + + +Kubernetes meta/v1.Time + + +
    +
    +endTime
    + + +Kubernetes meta/v1.Time + + +
    +(Optional) +
    +reconcileId
    + +string + +
    +
    +resultId
    + +string + +
    +(Optional) +
    +commandError
    + +string + +
    +(Optional) +
    +
    +
    +

    ProjectCredentials +

    +

    +(Appears on: +KluctlDeploymentSpec) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +git
    + + +[]ProjectCredentialsGit + + +
    +(Optional) +

    Git specifies a list of git credentials

    +
    +oci
    + + +[]ProjectCredentialsOci + + +
    +(Optional) +

    Oci specifies a list of OCI credentials

    +
    +helm
    + + +[]ProjectCredentialsHelm + + +
    +(Optional) +

    Helm specifies a list of Helm credentials

    +
    +
    +
    +

    ProjectCredentialsGit +

    +

    +(Appears on: +ProjectCredentials) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +host
    + +string + +
    +

    Host specifies the hostname that this secret applies to. If set to ‘’, this set of credentials +applies to all hosts. +Using ‘’ for http(s) based repositories is not supported, meaning that such credentials sets will be ignored. +You must always set a proper hostname in that case.

    +
    +path
    + +string + +
    +(Optional) +

    Path specifies the path to be used to filter Git repositories. The path can contain wildcards. These credentials +will only be used for matching Git URLs. If omitted, all repositories are considered to match.

    +
    +secretRef
    + + +LocalObjectReference + + +
    +

    SecretRef specifies the Secret containing authentication credentials for +the git repository. +For HTTPS git repositories the Secret must contain ‘username’ and ‘password’ +fields. +For SSH git repositories the Secret must contain ‘identity’ +and ‘known_hosts’ fields.

    +
    +
    +
    +

    ProjectCredentialsGitDeprecated +

    +

    +(Appears on: +ProjectSource) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +host
    + +string + +
    +

    Host specifies the hostname that this secret applies to. If set to ‘’, this set of credentials +applies to all hosts. +Using ‘’ for http(s) based repositories is not supported, meaning that such credentials sets will be ignored. +You must always set a proper hostname in that case.

    +
    +pathPrefix
    + +string + +
    +(Optional) +

    PathPrefix specifies the path prefix to be used to filter source urls. Only urls that have this prefix will use +this set of credentials.

    +
    +secretRef
    + + +LocalObjectReference + + +
    +

    SecretRef specifies the Secret containing authentication credentials for +the git repository. +For HTTPS git repositories the Secret must contain ‘username’ and ‘password’ +fields. +For SSH git repositories the Secret must contain ‘identity’ +and ‘known_hosts’ fields.

    +
    +
    +
    +

    ProjectCredentialsHelm +

    +

    +(Appears on: +ProjectCredentials) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +host
    + +string + +
    +

    Host specifies the hostname that this secret applies to.

    +
    +path
    + +string + +
    +(Optional) +

    Path specifies the path to be used to filter Helm urls. The path can contain wildcards. These credentials +will only be used for matching URLs. If omitted, all URLs are considered to match.

    +
    +secretRef
    + + +LocalObjectReference + + +
    +

    SecretRef specifies the Secret containing authentication credentials for +the Helm repository. +The secret can either container basic authentication credentials via username and password or +TLS authentication via certFile and keyFile. caFile can be specified to override the CA to use while +contacting the repository. +The secret can also contain insecureSkipTlsVerify: "true", which will disable TLS verification. +passCredentialsAll: "true" can be specified to make the controller pass credentials to all requests, even if +the hostname changes in-between.

    +
    +
    +
    +

    ProjectCredentialsOci +

    +

    +(Appears on: +ProjectCredentials) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +registry
    + +string + +
    +

    Registry specifies the hostname that this secret applies to.

    +
    +repository
    + +string + +
    +(Optional) +

    Repository specifies the org and repo name in the format ‘org-name/repo-name’. +Both ‘org-name’ and ‘repo-name’ can be specified as ‘*’, meaning that all names are matched.

    +
    +secretRef
    + + +LocalObjectReference + + +
    +

    SecretRef specifies the Secret containing authentication credentials for +the oci repository. +The secret must contain ‘username’ and ‘password’.

    +
    +
    +
    +

    ProjectSource +

    +

    +(Appears on: +KluctlDeploymentSpec) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +git
    + + +ProjectSourceGit + + +
    +(Optional) +

    Git specifies a git repository as project source

    +
    +oci
    + + +ProjectSourceOci + + +
    +(Optional) +

    Oci specifies an OCI repository as project source

    +
    +url
    + +string + +
    +(Optional) +

    Url specifies the Git url where the project source is located +DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.url instead.

    +
    +ref
    + +github.com/kluctl/kluctl/lib/git/types.GitRef + +
    +(Optional) +

    Ref specifies the branch, tag or commit that should be used. If omitted, the default branch of the repo is used. +DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.ref instead.

    +
    +path
    + +string + +
    +(Optional) +

    Path specifies the sub-directory to be used as project directory +DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.path instead.

    +
    +secretRef
    + + +LocalObjectReference + + +
    +

    SecretRef specifies the Secret containing authentication credentials for +See ProjectSourceCredentials.SecretRef for details +DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.git +instead. +WARNING using this field causes the controller to pass http basic auth credentials to ALL repositories involved. +Use spec.credentials.git with a proper Host field instead.

    +
    +credentials
    + + +[]ProjectCredentialsGitDeprecated + + +
    +(Optional) +

    Credentials specifies a list of secrets with credentials +DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.git instead.

    +
    +
    +
    +

    ProjectSourceGit +

    +

    +(Appears on: +ProjectSource) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +url
    + +string + +
    +

    URL specifies the Git url where the project source is located. If the given Git repository needs authentication, +use spec.credentials.git to specify those.

    +
    +ref
    + +github.com/kluctl/kluctl/lib/git/types.GitRef + +
    +(Optional) +

    Ref specifies the branch, tag or commit that should be used. If omitted, the default branch of the repo is used.

    +
    +path
    + +string + +
    +(Optional) +

    Path specifies the sub-directory to be used as project directory

    +
    +
    +
    +

    ProjectSourceOci +

    +

    +(Appears on: +ProjectSource) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +url
    + +string + +
    +

    Url specifies the Git url where the project source is located. If the given OCI repository needs authentication, +use spec.credentials.oci to specify those.

    +
    +ref
    + +github.com/kluctl/kluctl/v2/pkg/types.OciRef + +
    +(Optional) +

    Ref specifies the tag to be used. If omitted, the “latest” tag is used.

    +
    +path
    + +string + +
    +(Optional) +

    Path specifies the sub-directory to be used as project directory

    +
    +
    +
    +

    SafeDuration +

    +

    +(Appears on: +KluctlDeploymentSpec) +

    +
    +
    + + + + + + + + + + + + + +
    FieldDescription
    +Duration
    + + +Kubernetes meta/v1.Duration + + +
    +
    +
    +
    +

    SecretKeyReference +

    +

    +(Appears on: +KubeConfig) +

    +

    SecretKeyReference contains enough information to locate the referenced Kubernetes Secret object in the same +namespace. Optionally a key can be specified. +Use this type instead of core/v1 SecretKeySelector when the Key is optional and the Optional field is not +applicable.

    +
    +
    + + + + + + + + + + + + + + + + + +
    FieldDescription
    +name
    + +string + +
    +

    Name of the Secret.

    +
    +key
    + +string + +
    +(Optional) +

    Key in the Secret, when not specified an implementation-specific default key is used.

    +
    +
    +
    +

    SourceOverride +

    +

    +(Appears on: +KluctlDeploymentSpec) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +repoKey
    + +github.com/kluctl/kluctl/lib/git/types.RepoKey + +
    +
    +url
    + +string + +
    +
    +isGroup
    + +bool + +
    +(Optional) +
    +
    +
    +
    +

    This page was automatically generated with gen-crd-api-reference-docs

    +
    diff --git a/docs/gitops/installation.md b/docs/gitops/installation.md new file mode 100644 index 000000000..c37dd400f --- /dev/null +++ b/docs/gitops/installation.md @@ -0,0 +1,32 @@ + + +# Installation + +The controller can be installed via two available options. + +## Using the "install" sub-command + +The [`kluctl controller install`](../kluctl/commands/controller-install.md) command can be used to install the +controller. It will use an embedded version of the Controller Kluctl deployment project +found [here](https://github.com/kluctl/kluctl/tree/main/install/controller). + +## Using a Kluctl deployment + +To manage and install the controller via Kluctl, you can use a Git include in your own deployment: + +```yaml +deployments: + - git: + url: https://github.com/kluctl/kluctl.git + subDir: install/controller + ref: + tag: v2.26.0 +``` diff --git a/docs/gitops/metrics/README.md b/docs/gitops/metrics/README.md new file mode 100644 index 000000000..a580dc43b --- /dev/null +++ b/docs/gitops/metrics/README.md @@ -0,0 +1,9 @@ + diff --git a/docs/gitops/metrics/v1beta1/README.md b/docs/gitops/metrics/v1beta1/README.md new file mode 100644 index 000000000..a9d0a3e03 --- /dev/null +++ b/docs/gitops/metrics/v1beta1/README.md @@ -0,0 +1,19 @@ + + +# Prometheus Metrics + +The controller exports several metrics in the [OpenMetrics compatible format](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md). +They can be scraped by all sorts of monitoring solutions (e.g. Prometheus) or stored in a database. Because the +controller is based on [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime), all +the [default metrics](https://book.kubebuilder.io/reference/metrics-reference.html) as well as the +following controller-specific custom metrics are exported: + +- [kluctldeployment_controller](./kluctldeployment_controller.md) diff --git a/docs/gitops/metrics/v1beta1/kluctldeployment_controller.md b/docs/gitops/metrics/v1beta1/kluctldeployment_controller.md new file mode 100644 index 000000000..a54633b3b --- /dev/null +++ b/docs/gitops/metrics/v1beta1/kluctldeployment_controller.md @@ -0,0 +1,30 @@ + + +# Exported Metrics References + +| Metrics name | Type | Description | +|--------------------------------------|-----------|--------------------------------------------------------------------------------------| +| deployment_duration_seconds | Histogram | How long a single deployment takes in seconds. | +| number_of_changed_objects | Gauge | How many objects have been changed by a single deployment. | +| number_of_deleted_objects | Gauge | How many objects have been deleted by a single deployment. | +| number_of_errors | Gauge | How many errors are related to a single deployment. | +| number_of_images | Gauge | Number of images of a single deployment. | +| number_of_orphan_objects | Gauge | How many orphans are related to a single deployment. | +| number_of_warnings | Gauge | How many warnings are related to a single deployment. | +| prune_duration_seconds | Histogram | How long a single prune takes in seconds. | +| validate_duration_seconds | Histogram | How long a single validate takes in seconds. | +| deployment_interval_seconds | Gauge | The configured deployment interval of a single deployment. | +| dry_run_enabled | Gauge | Is dry-run enabled for a single deployment. | +| last_object_status | Gauge | Last object status of a single deployment. Zero means failure and one means success. | +| last_deploy_start_timestamp_seconds | Gauge | Start time of the last deployment. | +| prune_enabled | Gauge | Is pruning enabled for a single deployment. | +| delete_enabled | Gauge | Is deletion enabled for a single deployment. | +| source_spec | Gauge | The configured source spec of a single deployment exported via labels. | diff --git a/docs/gitops/spec/README.md b/docs/gitops/spec/README.md new file mode 100644 index 000000000..b770afede --- /dev/null +++ b/docs/gitops/spec/README.md @@ -0,0 +1,9 @@ + diff --git a/docs/gitops/spec/v1beta1/README.md b/docs/gitops/spec/v1beta1/README.md new file mode 100644 index 000000000..2a48a5a86 --- /dev/null +++ b/docs/gitops/spec/v1beta1/README.md @@ -0,0 +1,24 @@ + + +# gitops.kluctl.io/v1beta1 + +This is the v1beta1 API specification for defining continuous delivery pipelines +of Kluctl Deployments. + +## Specification + +- [KluctlDeployment CRD](./kluctldeployment.md) + + [Spec fields](./kluctldeployment.md#spec-fields) + + [Reconciliation](./kluctldeployment.md#reconciliation) + + [Kubeconfigs and RBAC](./kluctldeployment.md#kubeconfigs-and-rbac) + + [Credentilas](kluctldeployment.md#credentials) + + [Secrets Decryption](./kluctldeployment.md#secrets-decryption) + + [Status](./kluctldeployment.md#status) diff --git a/docs/gitops/spec/v1beta1/kluctldeployment.md b/docs/gitops/spec/v1beta1/kluctldeployment.md new file mode 100644 index 000000000..7a2d74110 --- /dev/null +++ b/docs/gitops/spec/v1beta1/kluctldeployment.md @@ -0,0 +1,788 @@ + + +# KluctlDeployment + +The `KluctlDeployment` API defines a deployment of a [target](../../../kluctl/kluctl-project/targets) +from a [Kluctl Project](../../../kluctl/kluctl-project). + +## Example + +```yaml +apiVersion: gitops.kluctl.io/v1beta1 +kind: KluctlDeployment +metadata: + name: microservices-demo-prod +spec: + interval: 5m + source: + git: + url: https://github.com/kluctl/kluctl-examples.git + path: "./microservices-demo/3-templating-and-multi-env/" + timeout: 2m + target: prod + context: default + prune: true + delete: true + manual: true +``` + +In the above example a KluctlDeployment is being created that defines the deployment based on the Kluctl project. + +The deployment is performed every 5 minutes. It will deploy the `prod` +[target](../../../kluctl/kluctl-project/targets) and then prune orphaned objects afterward. + +When the KluctlDeployment gets deleted, `delete: true` will cause the controller to actually delete the target +resources. + +It uses the `default` context provided by the default service account and thus overrides the context specified in the +target definition. + +## Spec fields + +### source + +The KluctlDeployment `spec.source` specifies the source repository to be used. Example: + +Multiple source types are supported, as described in the following subsections. + +#### Git source + +Specifies a Git repository to load the project source from. + +Example: + +```yaml +apiVersion: gitops.kluctl.io/v1beta1 +kind: KluctlDeployment +metadata: + name: example +spec: + source: + git: + url: https://github.com/kluctl/kluctl-examples.git + path: path/to/project + ref: + branch: my-branch + credentials: + git: + - host: github.com + path: kluctl/* + secretRef: + name: git-credentials + ... +``` + +The `url` specifies the git clone url. It can either be a https or a git/ssh url. Git/Ssh url will require a secret +to be provided with credentials. + +The `path` specifies the subdirectory where the Kluctl project is located. + +The `ref` provides the Git reference to be used. The `ref` field has the same format as in +[git includes](../../../kluctl/deployments/deployment-yml.md#git-includes). + +See [Git authentication](#git-authentication) for details on authentication via the `spec.credentials.git` field. + +#### OCI source + +Specifies a OCI artifact to load the project source from. The artifact must have been pushed via the +[kluctl oci push](../../../kluctl/commands/oci-push.md) command. + +Example: + +```yaml +apiVersion: gitops.kluctl.io/v1beta1 +kind: KluctlDeployment +metadata: + name: example +spec: + source: + oci: + url: oci://ghcr.io/kluctl/kluctl-examples/simple + path: my-subdir + ref: + tag: latest + credentials: + oci: + - registry: ghcr.io + repository: kluctl/** + secretRef: + name: oci-credentials + ... +``` + +The `url` specifies the OCI repository url. It must use the `oci://` scheme. It is not allowed to add tags or digests to +the url. Instead, use the dedicated `ref` field. + +The `path` specifies the subdirectory where the Kluctl project is located. + +The `ref` provides the Git reference to be used. The `ref` field has the same format as in +[oci includes](../../../kluctl/deployments/deployment-yml.md#oci-includes). + +See [OCI authentication](#oci-registry-authentication) for details on authentication via the `spec.credentials.oci` field. + +### interval +See [Reconciliation](#reconciliation). + +### deployInterval +If set, the controller will periodically force a deployment, even if the rendered manifests have not changed. +See [Reconciliation](#reconciliation) for more details. + +### timeout +The maximum time granted for the all the work needed to perform in a reconciliation. If `timeout` is not specified, +then the value of `deployInterval` is used as the default value, but only if it is set. If `deployInterval` is not set, +then the value of `interval` is used as default. + +When a reconciliation takes longer then the determined timeout value, then the reconciliation is aborted with an error. + +### suspend +See [Reconciliation](#reconciliation). + +### target +`spec.target` specifies the target to be deployed. It must exist in the Kluctl projects +[kluctl.yaml targets](../../../kluctl/kluctl-project/targets) list. + +This field is optional and can be omitted if the referenced Kluctl project allows deployments without targets. + +### targetNameOverride +`spec.targetNameOverride` will set or override the name of the target. This is equivalent to passing +`--target-name-override` to `kluctl deploy`. + +### context +`spec.context` will override the context used while deploying. This is equivalent to passing `--context` to +`kluctl deploy`. + +### deployMode +By default, the operator will perform a full deployment, which is equivalent to using the `kluctl deploy` command. +As an alternative, the controller can be instructed to only perform a `kluctl poke-images` command. Please +see [poke-images](../../../kluctl/commands/poke-images.md) for details on the command. To do so, set `spec.deployMode` +field to `poke-images`. + +Example: +```yaml +apiVersion: gitops.kluctl.io/v1beta1 +kind: KluctlDeployment +metadata: + name: microservices-demo-prod +spec: + interval: 5m + source: + git: + url: https://github.com/kluctl/kluctl-examples.git + path: "./microservices-demo/3-templating-and-multi-env/" + timeout: 2m + target: prod + context: default + deployMode: poke-images +``` + +### prune + +To enable pruning, set `spec.prune` to `true`. This will cause the controller to run `kluctl prune` after each +successful deployment. + +### delete + +To enable deletion, set `spec.delete` to `true`. This will cause the controller to run `kluctl delete` when the +KluctlDeployment gets deleted. + +### manual + +`spec.manual` enables manually approved/triggered deployments. This means, that deployments are performed in dry-run +mode until the most recent deployment is approved. + +This feature is most useful in combination with the Kluctl Webui, which offers a visualisation and proper actions +for this feature. + +Internally, approval happens by setting `spec.manualObjectsHash` to the objects hash of the approved command result. + +### args +`spec.args` is an object representing [arguments](../../../kluctl/kluctl-project/README.md#args) +passed to the deployment. Example: + +```yaml +apiVersion: gitops.kluctl.io/v1beta1 +kind: KluctlDeployment +metadata: + name: example +spec: + interval: 5m + source: + git: + url: https://github.com/kluctl/kluctl-examples.git + path: "./microservices-demo/3-templating-and-multi-env/" + timeout: 2m + target: prod + context: default + args: + arg1: value1 + arg2: value2 + arg3: + k1: v1 + k2: v2 +``` + +The above example is equivalent to calling `kluctl deploy -t prod -a arg1=value1 -a arg2=value2`. + +### images +`spec.images` specifies a list of fixed images to be used by +[`image.get_image(...)`](../../../kluctl/deployments/images.md#imagesget_image). Example: + +```yaml +apiVersion: gitops.kluctl.io/v1beta1 +kind: KluctlDeployment +metadata: + name: example +spec: + interval: 5m + source: + git: + url: https://example.com + timeout: 2m + target: prod + images: + - image: nginx + resultImage: nginx:1.21.6 + namespace: example-namespace + deployment: Deployment/example + - image: registry.gitlab.com/my-org/my-repo/image + resultImage: registry.gitlab.com/my-org/my-repo/image:1.2.3 +``` + +The above example will cause the `images.get_image("nginx")` invocations of the `example` Deployment to return +`nginx:1.21.6`. It will also cause all `images.get_image("registry.gitlab.com/my-org/my-repo/image")` invocations +to return `registry.gitlab.com/my-org/my-repo/image:1.2.3`. + +The fixed images provided here take precedence over the ones provided in the +[target definition](../../../kluctl/kluctl-project/targets#images). + +`spec.images` is equivalent to calling `kluctl deploy -t prod --fixed-image=nginx:example-namespace:Deployment/example=nginx:1.21.6 ...` +and to `kluctl deploy -t prod --fixed-images-file=fixed-images.yaml` with `fixed-images.yaml` containing: + +```yaml +images: +- image: nginx + resultImage: nginx:1.21.6 + namespace: example-namespace + deployment: Deployment/example +- image: registry.gitlab.com/my-org/my-repo/image + resultImage: registry.gitlab.com/my-org/my-repo/image:1.2.3 +``` + +### dryRun +`spec.dryRun` is a boolean value that turns the deployment into a dry-run deployment. This is equivalent to calling +`kluctl deploy -t prod --dry-run`. + +### noWait +`spec.noWait` is a boolean value that disables all internal waiting (hooks and readiness). This is equivalent to calling +`kluctl deploy -t prod --no-wait`. + +### forceApply +`spec.forceApply` is a boolean value that causes kluctl to solve conflicts via force apply. This is equivalent to calling +`kluctl deploy -t prod --force-apply`. + +### replaceOnError and forceReplaceOnError +`spec.replaceOnError` and `spec.forceReplaceOnError` are both boolean values that cause kluctl to perform a replace +after a failed apply. `forceReplaceOnError` goes a step further and deletes and recreates the object in question. +These are equivalent to calling `kluctl deploy -t prod --replace-on-error` and `kluctl deploy -t prod --force-replace-on-error`. + +### abortOnError +`spec.abortOnError` is a boolean value that causes kluctl to abort as fast as possible in case of errors. This is equivalent to calling +`kluctl deploy -t prod --abort-on-error`. + +### includeTags, excludeTags, includeDeploymentDirs and excludeDeploymentDirs +`spec.includeTags` and `spec.excludeTags` are lists of tags to be used in inclusion/exclusion logic while deploying. +These are equivalent to calling `kluctl deploy -t prod --include-tag ` and `kluctl deploy -t prod --exclude-tag `. + +`spec.includeDeploymentDirs` and `spec.excludeDeploymentDirs` are lists of relative deployment directories to be used in +inclusion/exclusion logic while deploying. These are equivalent to calling `kluctl deploy -t prod --include-tag ` +and `kluctl deploy -t prod --exclude-tag `. + +## Reconciliation + +The KluctlDeployment `spec.interval` tells the controller at which interval to try reconciliations. +The interval time units are `s`, `m` and `h` e.g. `interval: 5m`, the minimum value should be over 60 seconds. + +At each reconciliation run, the controller will check if any rendered objects have been changes since the last +deployment and then perform a new deployment if changes are detected. Changes are tracked via a hash consisting of +all rendered objects. + +To enforce periodic full deployments even if nothing has changed, `spec.deployInterval` can be used to specify an +interval at which forced deployments must be performed by the controller. + +The KluctlDeployment reconciliation can be suspended by setting `spec.suspend` to `true`. Suspension will however not +prevent manual reconciliation requests via the `kluctl gitops` sub-commands. + +## Manual requests/reconciliation + +The controller can be told to reconcile the KluctlDeployment outside of the specified interval +by using the [`kluctl gitops`](../../../kluctl/commands/README.md) sub-commands. + +On-demand reconciliation example: + +```bash +$ kluctl gitops deploy --namespace my-namespace --name my-deployment +``` + +You can also perform manual requests while temporarily overriding deployment configurations, e.g.: + +```bash +$ kluctl gitops deploy --namespace my-namespace --name my-deployment --force-apply +``` + +Local source overrides are also possible, allowing you to test changes before pushing them: + +```bash +$ kluctl gitops diff --namespace my-namespace --name my-deployment --local-git-override=github.com/exaple-org/example-project=/local/path/to/modified/repo +``` + +When `--namespace` and `--name` are omitted, the CLI will try to auto-detect the deployment on the current cluster +and suggest the auto-detected deployment to you. + +## Kubeconfigs and RBAC + +As Kluctl is meant to be a CLI-first tool, it expects a kubeconfig to be present while deployments are +performed. The controller will generate such kubeconfigs on-the-fly before performing the actual deployment. + +The kubeconfig can be generated from 3 different sources: +1. The default impersonation service account specified at controller startup (via `--default-service-account`) +2. The service account specified via `spec.serviceAccountName` in the KluctlDeployment +3. The secret specified via `spec.kubeConfig` in the KluctlDeployment. + +The behavior/functionality of 1. and 2. is comparable to how the [kustomize-controller](https://fluxcd.io/docs/components/kustomize/kustomization/#role-based-access-control) +handles impersonation, with the difference that a kubeconfig with a "default" context is created in-between. + +`spec.kubeConfig` will simply load the kubeconfig from `data.value` of the specified secret. + +Kluctl [targets](../../../kluctl/kluctl-project/targets) specify a context name that is expected to +be present in the kubeconfig while deploying. As the context found in the generated kubeconfig does not necessarily +have the correct name, `spec.context` can be used to while deploying. This is especially useful +when using service account based kubeconfigs, as these always have the same context with the name "default". + +Here is an example of a deployment that uses the service account "prod-service-account" and overrides the context +appropriately (assuming the Kluctl cluster config for the given target expects a "prod" context): + +```yaml +apiVersion: gitops.kluctl.io/v1beta1 +kind: KluctlDeployment +metadata: + name: example + namespace: kluctl-system +spec: + interval: 10m + source: + git: + url: https://github.com/kluctl/kluctl-examples.git + path: "./microservices-demo/3-templating-and-multi-env/" + target: prod + serviceAccountName: prod-service-account + context: default +``` + +## Credentials +A `KluctlDeployment` can specify multiple sets of credentials for different kind of repositories and registries. These +are specified through the `spec.credentials` field, which specifies multiple list of credentials. + +### Git authentication +Git authentication can be specified via `spec.credentials.git`, which is a list of credential configs. Each entry +specifies information to match Git repositories and a reference to a Kubernetes secret. + +Each time the controller needs to access a git repository, it will iterate through this list and pick the first one +matching. + +Example: + +```yaml +... +spec: + source: + git: + url: https://github.com/my-org/my-repo.git + credentials: + git: + - host: github.com + path: my-org/* + secretRef: + name: my-git-secrets +... +``` + +Each entry has the following fields: + +`host` is required and specifies the hostname to apply this set of credentials. It can also be set to `*`, meaning that +it will match all git hosts. `*` will however be ignored for https based urls to avoid leaking credentials. + +`path` is optional and allows to filter for different paths on the same host. This is for example useful +when public Git providers are used, for example github.com. For these, you can for example use `my-org/*` as pattern to +tell the controller that it should use this set of credentials only for projects below the `my-org` GitHub organisation. + +`secretRef` is required and specifies the name of the secret that contains the actual credentials. + +The following authentication types are supported through the referenced secret. + +#### Basic access authentication + +To authenticate towards a Git repository over HTTPS using basic access +authentication (in other words: using a username and password), the referenced +Secret is expected to contain `.data.username` and `.data.password` values. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: basic-access-auth +type: Opaque +data: + username: + password: +``` + +#### HTTPS Certificate Authority + +To provide a Certificate Authority to trust while connecting with a Git +repository over HTTPS, the referenced Secret can contain a `.data.caFile` +value. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: https-ca-credentials + namespace: default +type: Opaque +data: + caFile: +``` + +#### SSH authentication + +To authenticate towards a Git repository over SSH, the referenced Secret is +expected to contain `identity` and `known_hosts` fields. With the respective +private key of the SSH key pair, and the host keys of the Git repository. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: ssh-credentials +type: Opaque +stringData: + identity: | + -----BEGIN OPENSSH PRIVATE KEY----- + ... + -----END OPENSSH PRIVATE KEY----- + known_hosts: | + github.com ecdsa-sha2-nistp256 AAAA... +``` + +### Helm Repository authentication +Kluctl allows to [integrate Helm Charts](../../../kluctl/deployments/helm.md) in two different ways. +One is to [pre-pull charts](../../../kluctl/commands/helm-pull.md) and put them into version control, +making it unnecessary to pull them at deploy time. This option also means that you don't have to take any special care +on the controller side. + +The other way is to let Kluctl pull Helm Charts at deploy time. In that case, you have to ensure that the controller +has the necessary access to the Helm repositories. + +Helm Repository authentication can be specified via `spec.credentials.helm`, which is a list of credential configs. Each entry +specifies information to match Helm repositories and a reference to a Kubernetes secret. + +Each time the controller needs to access a Helm repository, it will iterate through this list and pick the first one +matching. + +Example: + +```yaml +... +spec: + source: + git: + url: https://github.com/my-org/my-repo.git + credentials: + helm: + - host: my-repo.com + path: some-path/* + secretRef: + name: my-helm-secrets +... +``` + +Each entry has the following fields: + +`host` is required and specifies the hostname to apply this set of credentials. + +`path` is optional and allows to filter for different paths on the same host. The behavior is identical to how +Git credentials handle it. + +`secretRef` is required and specifies the name of the secret that contains the actual credentials. + +The following authentication types are supported through the referenced secret. + +#### Basic access authentication + +To authenticate towards a Helm repository over HTTP/HTTPS using basic access +authentication (in other words: using a username and password), the referenced +Secret is expected to contain `.data.username` and `.data.password` values. + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-helm-creds + namespace: kluctl-system +stringData: + username: my-user + password: my-password +``` + +#### TLS authentication + +For TLS authentication, see the following example secret: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-helm-creds + namespace: kluctl-system +data: + certFile: + keyFile: + # NOTE: The following values can be supplied without the above values and for all other (e.g. basic) authentication types as well + caFile: + insecureSkipTlsVerify: "true" # this field is optional + passCredentialsAll: "true" # this field is optional +``` + +`certFile` and `keyFile` optionally specify a client certificate and key pair to use for client certificate based +authentication. `caFile` specifies a CA bundle to use when TSL/https verification is performed. + +If `insecureSkipTlsVerify` is set to `true`, TLS verification is skipped. + +If `passCredentialsAll` is set to `true`, Kluctl will pass credentials to all domains. See https://helm.sh/docs/helm/helm_repo_add/ for details. + +### OCI registry authentication +OCI registry authentication can be specified via `spec.credentials.oci`, which is a list of credential configs. Each entry +specifies information to match OCI registries and a reference to a Kubernetes secret. + +Each time the controller needs to access an OCI registry, it will iterate through this list and pick the first one +matching. This also includes OCI registry usages via the [Helm integration](../../../kluctl/deployments/helm.md). + +Example: + +```yaml +... +spec: + source: + git: + url: https://github.com/my-org/my-repo.git + credentials: + oci: + - registry: docker.com + repository: my-org/* + secretRef: + name: my-oci-secrets +... +``` + +Each entry has the following fields: + +`registry` is required and specifies the registry name to apply this set of credentials. + +`repository` is optional and allows to filter for different repositories in the same registry. Wildcards can also be used. +If omitted, all repositories on the specified registry will match. + +`secretRef` is required and specifies the name of the secret that contains the actual credentials. + +The following authentication types are supported through the referenced secret. + +#### Basic access authentication +To authenticate towards an OCI registry over HTTP/HTTPS using basic access +authentication (in other words: using a username and password), the referenced +Secret is expected to contain `.data.username` and `.data.password` values. + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-oci-secrets + namespace: kluctl-system +stringData: + username: my-user + password: my-password +``` + +#### Token based authentication +To authenticate via a bearer token, use specify `.data.token` in the referenced secret. + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-oci-secrets + namespace: kluctl-system +stringData: + token: my-token +``` + +#### TLS authentication + +For TLS authentication, see the following example secret: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-oci-creds + namespace: kluctl-system +data: + certFile: + keyFile: + # NOTE: The following values can be supplied without the above values and for all other (e.g. basic) authentication types as well + caFile: + insecureSkipTlsVerify: "true" # this field is optional + plainHttp: "true" # this field is optional +``` + +`certFile` and `keyFile` optionally specify a client certificate and key pair to use for client certificate based +authentication. `caFile` specifies a CA bundle to use when TSL/https verification is performed. + +If `insecureSkipTlsVerify` is set to `true`, TLS verification is skipped. + +If `plainHttp` if set to `true`, HTTPS is disabled and HTTP is used instead. + +### Deprecated ways of credentials configurations +Kluctl still supports the deprecated `spec.source.credentials`, `spec.source.secretRef` and `spec.helmCredentials` fields +in the `v1beta1` api version. These fields are however deprecated and will be removed in the next version bump. + +## Secrets Decryption + +Kluctl offers a [SOPS Integration](../../../kluctl/deployments/sops.md) that allows to use encrypted +manifests and variable sources in Kluctl deployments. Decryption by the controller is also supported and currently +mirrors how the [Secrets Decryption configuration](https://fluxcd.io/flux/components/kustomize/kustomization/#secrets-decryption) +of the Flux Kustomize Controller. To configure it in the `KluctlDeployment`, simply set the `decryption` field in the +spec: + +```yaml +apiVersion: gitops.kluctl.io/v1beta1 +kind: KluctlDeployment +metadata: + name: example + namespace: kluctl-system +spec: + decryption: + provider: sops + secretRef: + name: sops-keys + ... +``` + +The `sops-keys` Secret has the same format as in the +[Flux Kustomize Controller](https://fluxcd.io/flux/components/kustomize/kustomization/#decryption-secret-reference). + +### AWS KMS with IRSA + +In addition to the [AWS KMS Secret Entry](https://fluxcd.io/flux/components/kustomize/kustomization/#aws-kms-secret-entry) +in the secret and the [global AWS KMS](https://fluxcd.io/flux/components/kustomize/kustomization/#aws-kms) +authentication via the controller's service account, the Kluctl controller also supports using the IRSA role of the +impersonated service account of the `KluctlDeployment` (specified via `serviceAccountName` in the spec or +`--default-service-account`): + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kluctl-deployment + namespace: kluctl-system + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::123456:role/my-irsa-enabled-role +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: kluctl-deployment + namespace: kluctl-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + # watch out, don't use cluster-admin if you don't trust the deployment + name: cluster-admin +subjects: + - kind: ServiceAccount + name: kluctl-deployment + namespace: kluctl-system +--- +apiVersion: gitops.kluctl.io/v1beta1 +kind: KluctlDeployment +metadata: + name: example + namespace: kluctl-system +spec: + serviceAccountName: kluctl-deployment + decryption: + provider: sops + # you can also leave out the secretRef if you don't provide addinional keys + secretRef: + name: sops-keys + ... +``` + +## Status + +When the controller completes a deployments, it reports the result in the `status` sub-resource. + +A successful reconciliation sets the ready condition to `true`. + +```yaml +... +status: + conditions: + - lastTransitionTime: "2022-07-07T11:48:14Z" + message: "deploy: ok" + reason: ReconciliationSucceeded + status: "True" + type: Ready + lastDeployResult: + ... + lastPruneResult: + ... + lastValidateResult: + ... +``` + +You can wait for the controller to complete a reconciliation with: + +```bash +$ kubectl wait kluctldeployment/backend --for=condition=ready +``` + +A failed reconciliation sets the ready condition to `false`: + +```yaml +... +status: + conditions: + - lastTransitionTime: "2022-05-04T10:18:11Z" + message: target invalid-name not found in kluctl project + reason: PrepareFailed + status: "False" + type: Ready + lastDeployResult: + ... + lastPruneResult: + ... + lastValidateResult: + ... +``` + +> **Note** that the lastDeployResult, lastPruneResult and lastValidateResult are only updated on a successful reconciliation. diff --git a/docs/kluctl/README.md b/docs/kluctl/README.md new file mode 100644 index 000000000..496cf4196 --- /dev/null +++ b/docs/kluctl/README.md @@ -0,0 +1,86 @@ + + +# Kluctl + +## What is Kluctl? + +Kluctl is the missing glue that puts together your (and any third-party) deployments into one large declarative +Kubernetes deployment, while making it fully manageable (deploy, diff, prune, delete, ...) via one unified command +line interface. + +## Core Concepts + +These are some core concepts in Kluctl. + +### Kluctl project +The kluctl project defines targets. +It is defined via the [.kluctl.yaml](../kluctl/kluctl-project) configuration file. + +### Targets +A target defines a target cluster and a set of deployment arguments. Multiple targets can use the same cluster. Targets +allow implementing multi-cluster, multi-environment, multi-customer, ... deployments. + +### Deployments +A [deployment](../kluctl/deployments) defines which Kustomize deployments and which sub-deployments +to deploy. It also controls the order of deployments. + +Deployments may be configured through deployment arguments, which are typically provided via the targets but might also +be provided through the CLI. + +### Variables +[Variables](../kluctl/templating) are the main source of configuration. They are either loaded yaml +files or directly defined inside deployments. Each variables file that is loaded has access to all the variables which +were defined before, allowing complex composition of configuration. + +After being loaded, variables are usable through the templating engine at all nearly all places. + +### Templating +All configuration files (including .kluctl.yaml and deployment.yaml) and all Kubernetes manifests involved are processed +through a templating engine. +The [templating engine](../kluctl/templating) allows simple variable substitution and also complex +control structures (if/else, for loops, ...). + +### Unified CLI +The CLI of kluctl is designed to be unified/consistent as much as possible. Most commands are centered around targets +and thus require you to specify the target name (via `-t `). If you remember how one command works, it's easy +to figure out how the others work. Output from all targets based commands is also unified, allowing you to easily see +what will and what did happen. + +## History + +Kluctl was created after multiple incarnations of complex multi-environment (e.g. dev, test, prod) deployments, including everything +from monitoring, persistency and the actual custom services. The philosophy of these deployments was always +"what belongs together, should be put together", meaning that only as much Git repositories were involved as necessary. + +The problems to solve turned out to be always the same: +* Dozens of Helm Charts, kustomize deployments and standalone Kubernetes manifests needed to be orchestrated in a way + that they work together (services need to connect to the correct databases, and so on) +* (Encrypted) Secrets needed to be managed and orchestrated for multiple environments and clusters +* Updates of components was always risky and required keeping track of what actually changed since the last deployment +* Available tools (Helm, Kustomize) were not suitable to solve this on its own in an easy/natural way +* A lot of bash scripting was required to put things together + +When this got more and more complex, and the bash scripts started to become a mess (as "simple" Bash scripts always tend to become), +kluctl was started from scratch. It now tries to solve the mentioned problems and provide a useful set of features (commands) +in a sane and unified way. + +The first versions of kluctl were written in Python, hence the use of Jinja2 templating in kluctl. With version 2.0.0, +kluctl was rewritten in Go. + +## Table of Contents + +1. [Get Started](./get-started.md) +2. [Installation](./installation.md) +3. [Kluctl Commands](./commands) +4. [Kluctl Projects](./kluctl-project) +5. [Kluctl Libraries](./kluctl-libraries) +6. [Deployments](./deployments) diff --git a/docs/kluctl/commands/README.md b/docs/kluctl/commands/README.md new file mode 100644 index 000000000..48a579247 --- /dev/null +++ b/docs/kluctl/commands/README.md @@ -0,0 +1,46 @@ + + +# Commands + +kluctl offers a unified command line interface that allows to standardize all your deployments. Every project, +no matter how different it is from other projects, is managed the same way. + +You can always call `kluctl --help` or `kluctl --help` for a help prompt. + +Individual commands are documented in sub-sections. + +## Table of Contents + +1. [Common Arguments](./common-arguments.md) +2. [Environment Variables](./environment-variables.md) +3. [delete](./delete.md) +4. [deploy](./deploy.md) +5. [diff](./diff.md) +6. [helm-pull](./helm-pull.md) +7. [helm-update](./helm-update.md) +8. [list-images](./list-images.md) +9. [list-targets](./list-targets.md) +10. [poke-images](./poke-images.md) +11. [prune](./prune.md) +12. [render](./render.md) +13. [validate](./validate.md) +14. [gitops deploy](./gitops-deploy.md) +15. [gitops logs](./gitops-logs.md) +16. [gitops prune](./gitops-prune.md) +17. [gitops reconcile](./gitops-reconcile.md) +18. [gitops validate](./gitops-validate.md) +19. [gitops resume](./gitops-resume.md) +20. [gitops suspend](./gitops-suspend.md) +21. [controller run](./controller-run.md) +22. [controller install](./controller-install.md) +23. [webui run](./webui-run.md) +24. [webui build](./webui-build.md) diff --git a/docs/kluctl/commands/common-arguments.md b/docs/kluctl/commands/common-arguments.md new file mode 100644 index 000000000..7c0f1b1a9 --- /dev/null +++ b/docs/kluctl/commands/common-arguments.md @@ -0,0 +1,309 @@ + + +# Common Arguments + +A few sets of arguments are common between multiple commands. These arguments are still part of the command itself and +must be placed *after* the command name. + +## Global arguments + +These arguments are available for all commands. + + +``` +Global arguments: + --cpu-profile string Enable CPU profiling and write the result to the given path + --debug Enable debug logging + --gops-agent Start gops agent in the background + --gops-agent-addr string Specify the address:port to use for the gops agent (default "127.0.0.1:0") + --no-color Disable colored output + --no-update-check Disable update check on startup + --use-system-python Use the system Python instead of the embedded Python. + +``` + + +## Project arguments + +These arguments are available for all commands that are based on a Kluctl project. +They control where and how to load the kluctl project and deployment project. + + +``` +Project arguments: + Define where and how to load the kluctl project and its components from. + + -a, --arg stringArray Passes a template argument in the form of name=value. Nested args + can be set with the '-a my.nested.arg=value' syntax. Values are + interpreted as yaml values, meaning that 'true' and 'false' will + lead to boolean values and numbers will be treated as numbers. Use + quotes if you want these to be treated as strings. If the value + starts with @, it is treated as a file, meaning that the contents + of the file will be loaded and treated as yaml. + --args-from-file stringArray Loads a yaml file and makes it available as arguments, meaning that + they will be available thought the global 'args' variable. + --context string Overrides the context name specified in the target. If the selected + target does not specify a context or the no-name target is used, + --context will override the currently active context. + --git-cache-update-interval duration Specify the time to wait between git cache updates. Defaults to not + wait at all and always updating caches. + --kubeconfig existingfile Overrides the kubeconfig to use. + --local-git-group-override stringArray Same as --local-git-override, but for a whole group prefix instead + of a single repository. All repositories that have the given prefix + will be overridden with the given local path and the repository + suffix appended. For example, + 'gitlab.com/some-org/sub-org=/local/path/to/my-forks' will override + all repositories below 'gitlab.com/some-org/sub-org/' with the + repositories found in '/local/path/to/my-forks'. It will however + only perform an override if the given repository actually exists + locally and otherwise revert to the actual (non-overridden) repository. + --local-git-override stringArray Specify a single repository local git override in the form of + 'github.com/my-org/my-repo=/local/path/to/override'. This will + cause kluctl to not use git to clone for the specified repository + but instead use the local directory. This is useful in case you + need to test out changes in external git repositories without + pushing them. + --local-oci-group-override stringArray Same as --local-git-group-override, but for OCI repositories. + --local-oci-override stringArray Same as --local-git-override, but for OCI repositories. + -c, --project-config existingfile Location of the .kluctl.yaml config file. Defaults to + $PROJECT/.kluctl.yaml + --project-dir existingdir Specify the project directory. Defaults to the current working + directory. + -t, --target string Target name to run command for. Target must exist in .kluctl.yaml. + -T, --target-name-override string Overrides the target name. If -t is used at the same time, then the + target will be looked up based on -t and then renamed to the + value of -T. If no target is specified via -t, then the no-name + target is renamed to the value of -T. + --timeout duration Specify timeout for all operations, including loading of the + project, all external api calls and waiting for readiness. (default + 10m0s) + +``` + + +## Image arguments + +These arguments are available on some target based commands. +They control image versions requested by `images.get_image(...)` [calls](../deployments/images.md#imagesget_image). + + +``` +Image arguments: + Control fixed images and update behaviour. + + -F, --fixed-image stringArray Pin an image to a given version. Expects + '--fixed-image=image<:namespace:deployment:container>=result' + --fixed-images-file existingfile Use .yaml file to pin image versions. See output of list-images + sub-command or read the documentation for details about the output format + +``` + + +## Inclusion/Exclusion arguments + +These arguments are available for some target based commands. +They control inclusion/exclusion based on tags and deployment item pathes. + + +``` +Inclusion/Exclusion arguments: + Control inclusion/exclusion. + + --exclude-deployment-dir stringArray Exclude deployment dir. The path must be relative to the root + deployment project. Exclusion has precedence over inclusion, same as + in --exclude-tag + -E, --exclude-tag stringArray Exclude deployments with given tag. Exclusion has precedence over + inclusion, meaning that explicitly excluded deployments will always + be excluded even if an inclusion rule would match the same deployment. + --include-deployment-dir stringArray Include deployment dir. The path must be relative to the root + deployment project. + -I, --include-tag stringArray Include deployments with given tag. + +``` + + +## Command Results arguments + +These arguments control how command results are stored. + + +``` +Command Results: + Configure how command results are stored. + + --command-result-namespace string Override the namespace to be used when writing command results. (default + "kluctl-results") + --force-write-command-result Force writing of command results, even if the command is run in dry-run mode. + --keep-command-results-count int Configure how many old command results to keep. (default 5) + --keep-validate-results-count int Configure how many old validate results to keep. (default 2) + --write-command-result Enable writing of command results into the cluster. This is enabled by + default. (default true) + +``` + + +## Git arguments + +These arguments mainly control authentication to Git repositories. + + +``` +Git arguments: + Configure Git authentication. + + --git-ca-file stringArray Specify CA bundle to use for https verification. Must be in the + form --git-ca-file=/=. + --git-password stringArray Specify password to use for Git basic authentication. Must be in + the form --git-password=/=. + --git-ssh-key-file stringArray Specify SSH key to use for Git authentication. Must be in the form + --git-ssh-key-file=/=. + --git-ssh-known-hosts-file stringArray Specify known_hosts file to use for Git authentication. Must be in + the form --git-ssh-known-hosts-file=/=. + --git-username stringArray Specify username to use for Git basic authentication. Must be in + the form --git-username=/=. + +``` + + +All the arguments from above can also be passed via [environment variables](./environment-variables.md). For example: + +```shell +# for http(s) username/password auth +export KLUCTL_GIT_0_HOST="github.com" +export KLUCTL_GIT_0_USERNAME="my-user" +export KLUCTL_GIT_0_PASSWORD="my-password" +# in case you have self-signed certs or other non-standard CAs +export KLUCTL_GIT_0_CA_BUNDLE="/path/to/ca/bundle" + +# for ssh auth +export KLUCTL_GIT_1_HOST="gitlab.com" +export KLUCTL_GIT_1_SSH_KEY="/path/to/ssh/key" +# optionally specify path glob to limit this credentials set to only a defined set of repos (can also be used with http auth) +export KLUCTL_GIT_1_PATH="my-org/*" +``` + +In addition to the provided credentials, Kluctl will also try to use default Git authentication mechanisms like git +credentials helpers, default SSH keys and SSH agents. + +## Helm arguments + +These arguments mainly control authentication to Helm repositories. + + +``` +Helm arguments: + Configure Helm authentication. + + --helm-ca-file stringArray Specify ca bundle certificate to use for Helm Repository + authentication. Must be in the form + --helm-ca-file=/= or in the deprecated + form --helm-ca-file=:, where + must match the id specified in the helm-chart.yaml. + --helm-cert-file stringArray Specify key to use for Helm Repository authentication. Must be + in the form --helm-cert-file=/= or in + the deprecated form + --helm-cert-file=:, where + must match the id specified in the helm-chart.yaml. + --helm-creds stringArray This is a shortcut to --helm-username and --helm-password. + Must be in the form + --helm-creds=/=:, which + specifies the username and password for the same repository. + --helm-insecure-skip-tls-verify stringArray Controls skipping of TLS verification. Must be in the form + --helm-insecure-skip-tls-verify=/ or in the + deprecated form + --helm-insecure-skip-tls-verify=, where + must match the id specified in the helm-chart.yaml. + --helm-key-file stringArray Specify client certificate to use for Helm Repository + authentication. Must be in the form + --helm-key-file=/= or in the deprecated + form --helm-key-file=:, where + must match the id specified in the helm-chart.yaml. + --helm-password stringArray Specify password to use for Helm Repository authentication. + Must be in the form --helm-password=/= + or in the deprecated form + --helm-password=:, where + must match the id specified in the helm-chart.yaml. + --helm-username stringArray Specify username to use for Helm Repository authentication. + Must be in the form --helm-username=/= + or in the deprecated form + --helm-username=:, where + must match the id specified in the helm-chart.yaml. + +``` + + +All the arguments from above can also be passed via [environment variables](./environment-variables.md). For example: + +```shell +# for http(s) username/password auth +export KLUCTL_HELM_0_HOST="gitlab.com" +export KLUCTL_HELM_0_USERNAME="my-user" +export KLUCTL_HELM_0_PASSWORD="my-password" +export KLUCTL_HELM_0_PATH="api/v4/projects/1111111/packages/helm/stable" +# in case you have self-signed certs or other non-standard CAs +export KLUCTL_HELM_0_CA_FILE="/path/to/ca/bundle" +... +``` + +## Registry arguments + +These arguments mainly control authentication to OCI based registries. This is used by the Helm integration and +by the OCI includes integration. + + +``` +Registry arguments: + Configure OCI registry authentication. + + --registry-ca-file stringArray Specify CA bundle to use for https verification. Must be + in the form --registry-ca-file=/=. + --registry-cert-file stringArray Specify certificate to use for OCI authentication. Must be + in the form --registry-cert-file=/=. + --registry-creds stringArray This is a shortcut to --registry-username, + --registry-password and --registry-token. It can be + specified in two different forms. The first one is + --registry-creds=/=:, + which specifies the username and password for the same + registry. The second form is + --registry-creds=/=, which + specifies a JWT token for the specified registry. + --registry-identity-token stringArray Specify identity token to use for OCI authentication. Must + be in the form + --registry-identity-token=/=. + --registry-insecure-skip-tls-verify stringArray Controls skipping of TLS verification. Must be in the form + --registry-insecure-skip-tls-verify=/. + --registry-key-file stringArray Specify key to use for OCI authentication. Must be in the + form --registry-key-file=/=. + --registry-password stringArray Specify password to use for OCI authentication. Must be in + the form --registry-password=/=. + --registry-plain-http stringArray Forces the use of http (no TLS). Must be in the form + --registry-plain-http=/. + --registry-token stringArray Specify registry token to use for OCI authentication. Must + be in the form --registry-token=/=. + --registry-username stringArray Specify username to use for OCI authentication. Must be in + the form --registry-username=/=. + +``` + + +All the arguments from above can also be passed via [environment variables](./environment-variables.md). For example: + +```shell +# for http(s) username/password auth +export KLUCTL_REGISTRY_0_HOST="docker.io" +export KLUCTL_REGISTRY_0_USERNAME="my-user" +export KLUCTL_REGISTRY_0_PASSWORD="my-password" +export KLUCTL_REGISTRY_0_PATH="my-org/*" +# in case you have self-signed certs or other non-standard CAs +export KLUCTL_REGISTRY_0_CA_FILE="/path/to/ca/bundle" +... +``` diff --git a/docs/kluctl/commands/controller-install.md b/docs/kluctl/commands/controller-install.md new file mode 100644 index 000000000..fcaabfd83 --- /dev/null +++ b/docs/kluctl/commands/controller-install.md @@ -0,0 +1,37 @@ + + +## Command + +Usage: kluctl controller install [flags] + +Install the Kluctl controller +This command will install the kluctl-controller to the current Kubernetes clusters. + + + +## Arguments +The following sets of arguments are available: +1. [command results arguments](./common-arguments.md#command-results-arguments) + +In addition, the following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --context string Override the context to use. + --dry-run Performs all kubernetes API calls in dry-run mode. + --kluctl-version string Specify the controller version to install. + -y, --yes Suppresses 'Are you sure?' questions and proceeds as if you would answer 'yes'. + +``` + diff --git a/docs/kluctl/commands/controller-run.md b/docs/kluctl/commands/controller-run.md new file mode 100644 index 000000000..9f49c4a4e --- /dev/null +++ b/docs/kluctl/commands/controller-run.md @@ -0,0 +1,47 @@ + + +## Command + +Usage: kluctl controller run [flags] + +Run the Kluctl controller +This command will run the Kluctl Controller. This is usually meant to be run inside a cluster and not from your local machine. + + + +## Arguments + +The following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --concurrency int Configures how many KluctlDeployments can be be reconciled + concurrently. (default 4) + --context string Override the context to use. + --controller-name string The controller name used for metrics and logs. (default + "kluctl-controller") + --controller-namespace string The namespace where the controller runs in. (default "kluctl-system") + --default-service-account string Default service account used for impersonation. + --dry-run Run all deployments in dryRun=true mode. + --health-probe-bind-address string The address the probe endpoint binds to. (default ":8081") + --kubeconfig string Override the kubeconfig to use. + --leader-elect Enable leader election for controller manager. Enabling this will + ensure there is only one active controller manager. + --metrics-bind-address string The address the metric endpoint binds to. (default ":8080") + --namespace string Specify the namespace to watch. If omitted, all namespaces are watched. + --source-override-bind-address string The address the source override manager endpoint binds to. (default + ":8082") + +``` + diff --git a/docs/kluctl/commands/delete.md b/docs/kluctl/commands/delete.md new file mode 100644 index 000000000..66c2defe6 --- /dev/null +++ b/docs/kluctl/commands/delete.md @@ -0,0 +1,56 @@ + + +## Command + +Usage: kluctl delete [flags] + +Delete a target (or parts of it) from the corresponding cluster +Objects are located based on the target discriminator. + +WARNING: This command will also delete objects which are not part of your deployment +project (anymore). It really only decides based on the discriminator and does NOT +take the local target/state into account! + + + +## Arguments +The following sets of arguments are available: +1. [project arguments](./common-arguments.md#project-arguments) +1. [image arguments](./common-arguments.md#image-arguments) +1. [inclusion/exclusion arguments](./common-arguments.md#inclusionexclusion-arguments) +1. [command results arguments](./common-arguments.md#command-results-arguments) +1. [helm arguments](./common-arguments.md#helm-arguments) +1. [registry arguments](./common-arguments.md#registry-arguments) + +In addition, the following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --discriminator string Override the discriminator used to find objects for deletion. + --dry-run Performs all kubernetes API calls in dry-run mode. + --no-obfuscate Disable obfuscation of sensitive/secret data + --no-wait Don't wait for deletion of objects to finish.' + -o, --output-format stringArray Specify output format and target file, in the format 'format=path'. Format can + either be 'text' or 'yaml'. Can be specified multiple times. The actual format + for yaml is currently not documented and subject to change. + --render-output-dir string Specifies the target directory to render the project into. If omitted, a + temporary directory is used. + --short-output When using the 'text' output format (which is the default), only names of + changes objects are shown instead of showing all changes. + -y, --yes Suppresses 'Are you sure?' questions and proceeds as if you would answer 'yes'. + +``` + + +They have the same meaning as described in [deploy](./deploy.md). diff --git a/docs/kluctl/commands/deploy.md b/docs/kluctl/commands/deploy.md new file mode 100644 index 000000000..07073b5e4 --- /dev/null +++ b/docs/kluctl/commands/deploy.md @@ -0,0 +1,90 @@ + + +## Command + +Usage: kluctl deploy [flags] + +Deploys a target to the corresponding cluster +This command will also output a diff between the initial state and the state after +deployment. The format of this diff is the same as for the 'diff' command. +It will also output a list of prunable objects (without actually deleting them). + + + +## Arguments +The following sets of arguments are available: +1. [project arguments](./common-arguments.md#project-arguments) +1. [image arguments](./common-arguments.md#image-arguments) +1. [inclusion/exclusion arguments](./common-arguments.md#inclusionexclusion-arguments) +1. [command results arguments](./common-arguments.md#command-results-arguments) +1. [helm arguments](./common-arguments.md#helm-arguments) +1. [registry arguments](./common-arguments.md#registry-arguments) + +In addition, the following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --abort-on-error Abort deploying when an error occurs instead of trying the remaining deployments + --discriminator string Override the target discriminator. + --dry-run Performs all kubernetes API calls in dry-run mode. + --force-apply Force conflict resolution when applying. See documentation for details + --force-replace-on-error Same as --replace-on-error, but also try to delete and re-create objects. See + documentation for more details. + --no-obfuscate Disable obfuscation of sensitive/secret data + --no-wait Don't wait for objects readiness. + -o, --output-format stringArray Specify output format and target file, in the format 'format=path'. Format + can either be 'text' or 'yaml'. Can be specified multiple times. The actual + format for yaml is currently not documented and subject to change. + --prune Prune orphaned objects directly after deploying. See the help for the 'prune' + sub-command for details. + --readiness-timeout duration Maximum time to wait for object readiness. The timeout is meant per-object. + Timeouts are in the duration format (1s, 1m, 1h, ...). If not specified, a + default timeout of 5m is used. (default 5m0s) + --render-output-dir string Specifies the target directory to render the project into. If omitted, a + temporary directory is used. + --replace-on-error When patching an object fails, try to replace it. See documentation for more + details. + --short-output When using the 'text' output format (which is the default), only names of + changes objects are shown instead of showing all changes. + -y, --yes Suppresses 'Are you sure?' questions and proceeds as if you would answer 'yes'. + +``` + + +### --force-apply +kluctl implements deployments via [server-side apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) +and a custom automatic conflict resolution algorithm. This algurithm is an automatic implementation of the +"[Don't overwrite value, give up management claim](https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts)" +method. It should work in most cases, but might still fail. In case of such failure, you can use `--force-apply` to +use the "Overwrite value, become sole manager" strategy instead. + +Please note that this is a risky operation which might overwrite fields which were initially managed by kluctl but were +then overtaken by other managers (e.g. by operators). Always use this option with caution and perform a dry-run +before to ensure nothing unexpected gets overwritten. + +### --replace-on-error +In some situations, patching Kubernetes objects might fail for different reasons. In such cases, you can try +`--replace-on-error` to instruct kluctl to retry with an update operation. + +Please note that this will cause all fields to be overwritten, even if owned by other field managers. + +### --force-replace-on-error +This flag will cause the same replacement attempt on failure as with `--replace-on-error`. In addition, it will fallback +to a delete+recreate operation in case the replace also fails. + +Please note that this is a potentially risky operation, especially when an object carries some kind of important state. + +### --abort-on-error +kluctl does not abort a command when an individual object fails can not be updated. It collects all errors and warnings +and outputs them instead. This option modifies the behaviour to immediately abort the command. diff --git a/docs/kluctl/commands/diff.md b/docs/kluctl/commands/diff.md new file mode 100644 index 000000000..393fbbb90 --- /dev/null +++ b/docs/kluctl/commands/diff.md @@ -0,0 +1,60 @@ + + +## Command + +Usage: kluctl diff [flags] + +Perform a diff between the locally rendered target and the already deployed target +The output is by default in human readable form (a table combined with unified diffs). +The output can also be changed to output a yaml file. Please note however that the format +is currently not documented and prone to changes. +After the diff is performed, the command will also search for prunable objects and list them. + + + +## Arguments +The following sets of arguments are available: +1. [project arguments](./common-arguments.md#project-arguments) +1. [image arguments](./common-arguments.md#image-arguments) +1. [inclusion/exclusion arguments](./common-arguments.md#inclusionexclusion-arguments) +1. [helm arguments](./common-arguments.md#helm-arguments) +1. [registry arguments](./common-arguments.md#registry-arguments) + +In addition, the following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --discriminator string Override the target discriminator. + --force-apply Force conflict resolution when applying. See documentation for details + --force-replace-on-error Same as --replace-on-error, but also try to delete and re-create objects. See + documentation for more details. + --ignore-annotations Ignores changes in annotations when diffing + --ignore-kluctl-metadata Ignores changes in Kluctl related metadata (e.g. tags, discriminators, ...) + --ignore-labels Ignores changes in labels when diffing + --ignore-tags Ignores changes in tags when diffing + --no-obfuscate Disable obfuscation of sensitive/secret data + -o, --output-format stringArray Specify output format and target file, in the format 'format=path'. Format can + either be 'text' or 'yaml'. Can be specified multiple times. The actual format + for yaml is currently not documented and subject to change. + --render-output-dir string Specifies the target directory to render the project into. If omitted, a + temporary directory is used. + --replace-on-error When patching an object fails, try to replace it. See documentation for more + details. + --short-output When using the 'text' output format (which is the default), only names of + changes objects are shown instead of showing all changes. + +``` + + +`--force-apply` and `--replace-on-error` have the same meaning as in [deploy](./deploy.md). diff --git a/docs/kluctl/commands/environment-variables.md b/docs/kluctl/commands/environment-variables.md new file mode 100644 index 000000000..4b8db5184 --- /dev/null +++ b/docs/kluctl/commands/environment-variables.md @@ -0,0 +1,26 @@ + + +In addition to arguments, Kluctl can be controlled via a set of environment variables. + +## Environment variables as arguments +All options/arguments accepted by kluctl can also be specified via environment variables. The name of the environment +variables always start with `KLUCTL_` and end with the option/argument in uppercase and dashes replaced with +underscores. As an example, `--dry-run` can also be specified with the environment variable +`KLUCTL_DRY_RUN=true`. + +If an argument needs to be specified multiple times through environment variables, indexed can be appended to the +names of the environment variables, e.g. `KLUCTL_ARG_0=name1=value1` and `KLUCTL_ARG_1=name2=value2`. + +## Additional environment variables +A few additional environment variables are supported which do not belong to an option/argument. These are: + +1. `KLUCTL_SSH_DISABLE_STRICT_HOST_KEY_CHECKING`. Disable ssh host key checking when accessing git repositories. diff --git a/docs/kluctl/commands/gitops-deploy.md b/docs/kluctl/commands/gitops-deploy.md new file mode 100644 index 000000000..306daf2cb --- /dev/null +++ b/docs/kluctl/commands/gitops-deploy.md @@ -0,0 +1,148 @@ + + +## Command + +Usage: kluctl gitops deploy [flags] + +Trigger a GitOps deployment +This command will trigger an existing KluctlDeployment to perform a reconciliation loop with a forced deployment. +It does this by setting the annotation 'kluctl.io/request-deploy' to the current time. + +You can override many deployment relevant fields, see the list of command flags for details. + + + +## Arguments + +The following arguments are available: + +``` +GitOps arguments: + Specify gitops flags. + + --context string Override the context to use. + --controller-namespace string The namespace where the controller runs in. (default "kluctl-system") + --kubeconfig existingfile Overrides the kubeconfig to use. + -l, --label-selector string If specified, KluctlDeployments are searched and filtered by this label + selector. + --local-source-override-port int Specifies the local port to which the source-override client should + connect to when running the controller locally. + --name string Specifies the name of the KluctlDeployment. + -n, --namespace string Specifies the namespace of the KluctlDeployment. If omitted, the current + namespace from your kubeconfig is used. + +``` + + +``` +Misc arguments: + Command specific arguments. + + --no-obfuscate Disable obfuscation of sensitive/secret data + -o, --output-format stringArray Specify output format and target file, in the format 'format=path'. Format can + either be 'text' or 'yaml'. Can be specified multiple times. The actual format + for yaml is currently not documented and subject to change. + --short-output When using the 'text' output format (which is the default), only names of + changes objects are shown instead of showing all changes. + +``` + + +``` +Command Results: + Configure how command results are stored. + + --command-result-namespace string Override the namespace to be used when writing command results. (default + "kluctl-results") + +``` + + +``` +Log arguments: + Configure logging. + + --log-grouping-time duration Logs are by default grouped by time passed, meaning that they are printed in + batches to make reading them easier. This argument allows to modify the + grouping time. (default 1s) + --log-since duration Show logs since this time. (default 1m0s) + --log-time If enabled, adds timestamps to log lines + +``` + + +``` +GitOps overrides: + Override settings for GitOps deployments. + + --abort-on-error Abort deploying when an error occurs instead of trying the + remaining deployments + -a, --arg stringArray Passes a template argument in the form of name=value. Nested args + can be set with the '-a my.nested.arg=value' syntax. Values are + interpreted as yaml values, meaning that 'true' and 'false' will + lead to boolean values and numbers will be treated as numbers. Use + quotes if you want these to be treated as strings. If the value + starts with @, it is treated as a file, meaning that the contents + of the file will be loaded and treated as yaml. + --args-from-file stringArray Loads a yaml file and makes it available as arguments, meaning that + they will be available thought the global 'args' variable. + --dry-run Performs all kubernetes API calls in dry-run mode. + --exclude-deployment-dir stringArray Exclude deployment dir. The path must be relative to the root + deployment project. Exclusion has precedence over inclusion, same + as in --exclude-tag + -E, --exclude-tag stringArray Exclude deployments with given tag. Exclusion has precedence over + inclusion, meaning that explicitly excluded deployments will always + be excluded even if an inclusion rule would match the same deployment. + -F, --fixed-image stringArray Pin an image to a given version. Expects + '--fixed-image=image<:namespace:deployment:container>=result' + --fixed-images-file existingfile Use .yaml file to pin image versions. See output of list-images + sub-command or read the documentation for details about the output + format + --force-apply Force conflict resolution when applying. See documentation for details + --force-replace-on-error Same as --replace-on-error, but also try to delete and re-create + objects. See documentation for more details. + --include-deployment-dir stringArray Include deployment dir. The path must be relative to the root + deployment project. + -I, --include-tag stringArray Include deployments with given tag. + --local-git-group-override stringArray Same as --local-git-override, but for a whole group prefix instead + of a single repository. All repositories that have the given prefix + will be overridden with the given local path and the repository + suffix appended. For example, + 'gitlab.com/some-org/sub-org=/local/path/to/my-forks' will override + all repositories below 'gitlab.com/some-org/sub-org/' with the + repositories found in '/local/path/to/my-forks'. It will however + only perform an override if the given repository actually exists + locally and otherwise revert to the actual (non-overridden) repository. + --local-git-override stringArray Specify a single repository local git override in the form of + 'github.com/my-org/my-repo=/local/path/to/override'. This will + cause kluctl to not use git to clone for the specified repository + but instead use the local directory. This is useful in case you + need to test out changes in external git repositories without + pushing them. + --local-oci-group-override stringArray Same as --local-git-group-override, but for OCI repositories. + --local-oci-override stringArray Same as --local-git-override, but for OCI repositories. + --no-wait Don't wait for objects readiness. + --prune Prune orphaned objects directly after deploying. See the help for + the 'prune' sub-command for details. + --replace-on-error When patching an object fails, try to replace it. See documentation + for more details. + -t, --target string Target name to run command for. Target must exist in .kluctl.yaml. + --target-context string Overrides the context name specified in the target. If the selected + target does not specify a context or the no-name target is used, + --context will override the currently active context. + -T, --target-name-override string Overrides the target name. If -t is used at the same time, then the + target will be looked up based on -t and then renamed to the + value of -T. If no target is specified via -t, then the no-name + target is renamed to the value of -T. + +``` + \ No newline at end of file diff --git a/docs/kluctl/commands/gitops-diff.md b/docs/kluctl/commands/gitops-diff.md new file mode 100644 index 000000000..feb18fb2e --- /dev/null +++ b/docs/kluctl/commands/gitops-diff.md @@ -0,0 +1,145 @@ + + +## Command + +Usage: kluctl gitops diff [flags] + +Trigger a GitOps diff +This command will trigger an existing KluctlDeployment to perform a reconciliation loop with a forced diff. +It does this by setting the annotation 'kluctl.io/request-diff' to the current time. + +You can override many deployment relevant fields, see the list of command flags for details. + + + +## Arguments + +The following arguments are available: + +``` +GitOps arguments: + Specify gitops flags. + + --context string Override the context to use. + --controller-namespace string The namespace where the controller runs in. (default "kluctl-system") + --kubeconfig existingfile Overrides the kubeconfig to use. + -l, --label-selector string If specified, KluctlDeployments are searched and filtered by this label + selector. + --local-source-override-port int Specifies the local port to which the source-override client should + connect to when running the controller locally. + --name string Specifies the name of the KluctlDeployment. + -n, --namespace string Specifies the namespace of the KluctlDeployment. If omitted, the current + namespace from your kubeconfig is used. + +``` + + +``` +Misc arguments: + Command specific arguments. + + --no-obfuscate Disable obfuscation of sensitive/secret data + -o, --output-format stringArray Specify output format and target file, in the format 'format=path'. Format can + either be 'text' or 'yaml'. Can be specified multiple times. The actual format + for yaml is currently not documented and subject to change. + --short-output When using the 'text' output format (which is the default), only names of + changes objects are shown instead of showing all changes. + +``` + + +``` +Command Results: + Configure how command results are stored. + + --command-result-namespace string Override the namespace to be used when writing command results. (default + "kluctl-results") + +``` + + +``` +Log arguments: + Configure logging. + + --log-grouping-time duration Logs are by default grouped by time passed, meaning that they are printed in + batches to make reading them easier. This argument allows to modify the + grouping time. (default 1s) + --log-since duration Show logs since this time. (default 1m0s) + --log-time If enabled, adds timestamps to log lines + +``` + + +``` +GitOps overrides: + Override settings for GitOps deployments. + + --abort-on-error Abort deploying when an error occurs instead of trying the + remaining deployments + -a, --arg stringArray Passes a template argument in the form of name=value. Nested args + can be set with the '-a my.nested.arg=value' syntax. Values are + interpreted as yaml values, meaning that 'true' and 'false' will + lead to boolean values and numbers will be treated as numbers. Use + quotes if you want these to be treated as strings. If the value + starts with @, it is treated as a file, meaning that the contents + of the file will be loaded and treated as yaml. + --args-from-file stringArray Loads a yaml file and makes it available as arguments, meaning that + they will be available thought the global 'args' variable. + --dry-run Performs all kubernetes API calls in dry-run mode. + --exclude-deployment-dir stringArray Exclude deployment dir. The path must be relative to the root + deployment project. Exclusion has precedence over inclusion, same + as in --exclude-tag + -E, --exclude-tag stringArray Exclude deployments with given tag. Exclusion has precedence over + inclusion, meaning that explicitly excluded deployments will always + be excluded even if an inclusion rule would match the same deployment. + -F, --fixed-image stringArray Pin an image to a given version. Expects + '--fixed-image=image<:namespace:deployment:container>=result' + --fixed-images-file existingfile Use .yaml file to pin image versions. See output of list-images + sub-command or read the documentation for details about the output + format + --force-apply Force conflict resolution when applying. See documentation for details + --force-replace-on-error Same as --replace-on-error, but also try to delete and re-create + objects. See documentation for more details. + --include-deployment-dir stringArray Include deployment dir. The path must be relative to the root + deployment project. + -I, --include-tag stringArray Include deployments with given tag. + --local-git-group-override stringArray Same as --local-git-override, but for a whole group prefix instead + of a single repository. All repositories that have the given prefix + will be overridden with the given local path and the repository + suffix appended. For example, + 'gitlab.com/some-org/sub-org=/local/path/to/my-forks' will override + all repositories below 'gitlab.com/some-org/sub-org/' with the + repositories found in '/local/path/to/my-forks'. It will however + only perform an override if the given repository actually exists + locally and otherwise revert to the actual (non-overridden) repository. + --local-git-override stringArray Specify a single repository local git override in the form of + 'github.com/my-org/my-repo=/local/path/to/override'. This will + cause kluctl to not use git to clone for the specified repository + but instead use the local directory. This is useful in case you + need to test out changes in external git repositories without + pushing them. + --local-oci-group-override stringArray Same as --local-git-group-override, but for OCI repositories. + --local-oci-override stringArray Same as --local-git-override, but for OCI repositories. + --replace-on-error When patching an object fails, try to replace it. See documentation + for more details. + -t, --target string Target name to run command for. Target must exist in .kluctl.yaml. + --target-context string Overrides the context name specified in the target. If the selected + target does not specify a context or the no-name target is used, + --context will override the currently active context. + -T, --target-name-override string Overrides the target name. If -t is used at the same time, then the + target will be looked up based on -t and then renamed to the + value of -T. If no target is specified via -t, then the no-name + target is renamed to the value of -T. + +``` + \ No newline at end of file diff --git a/docs/kluctl/commands/gitops-logs.md b/docs/kluctl/commands/gitops-logs.md new file mode 100644 index 000000000..ea867b338 --- /dev/null +++ b/docs/kluctl/commands/gitops-logs.md @@ -0,0 +1,75 @@ + + +## Command + +Usage: kluctl gitops logs [flags] + +Show logs from controller +Print and watch logs of specified KluctlDeployments from the kluctl-controller. + + + +## Arguments + +The following arguments are available: + +``` +GitOps arguments: + Specify gitops flags. + + --context string Override the context to use. + --controller-namespace string The namespace where the controller runs in. (default "kluctl-system") + --kubeconfig existingfile Overrides the kubeconfig to use. + -l, --label-selector string If specified, KluctlDeployments are searched and filtered by this label + selector. + --local-source-override-port int Specifies the local port to which the source-override client should + connect to when running the controller locally. + --name string Specifies the name of the KluctlDeployment. + -n, --namespace string Specifies the namespace of the KluctlDeployment. If omitted, the current + namespace from your kubeconfig is used. + +``` + + +``` +Misc arguments: + Command specific arguments. + + --all Follow all controller logs, including all deployments and non-deployment related logs. + -f, --follow Follow logs after printing old logs. + --reconcile-id string If specified, logs are filtered for the given reconcile ID. + +``` + + +``` +Command Results: + Configure how command results are stored. + + --command-result-namespace string Override the namespace to be used when writing command results. (default + "kluctl-results") + +``` + + +``` +Log arguments: + Configure logging. + + --log-grouping-time duration Logs are by default grouped by time passed, meaning that they are printed in + batches to make reading them easier. This argument allows to modify the + grouping time. (default 1s) + --log-since duration Show logs since this time. (default 1m0s) + --log-time If enabled, adds timestamps to log lines + +``` + diff --git a/docs/kluctl/commands/gitops-prune.md b/docs/kluctl/commands/gitops-prune.md new file mode 100644 index 000000000..e9ea342a8 --- /dev/null +++ b/docs/kluctl/commands/gitops-prune.md @@ -0,0 +1,99 @@ + + +## Command + +Usage: kluctl gitops prune [flags] + +Trigger a GitOps prune +This command will trigger an existing KluctlDeployment to perform a reconciliation loop with a forced prune. +It does this by setting the annotation 'kluctl.io/request-prune' to the current time. + +You can override many deployment relevant fields, see the list of command flags for details. + + + +## Arguments + +The following arguments are available: + +``` +GitOps arguments: + Specify gitops flags. + + --context string Override the context to use. + --controller-namespace string The namespace where the controller runs in. (default "kluctl-system") + --kubeconfig existingfile Overrides the kubeconfig to use. + -l, --label-selector string If specified, KluctlDeployments are searched and filtered by this label + selector. + --local-source-override-port int Specifies the local port to which the source-override client should + connect to when running the controller locally. + --name string Specifies the name of the KluctlDeployment. + -n, --namespace string Specifies the namespace of the KluctlDeployment. If omitted, the current + namespace from your kubeconfig is used. + +``` + + +``` +Misc arguments: + Command specific arguments. + + --abort-on-error Abort deploying when an error occurs instead of trying the remaining deployments + --dry-run Performs all kubernetes API calls in dry-run mode. + --force-apply Force conflict resolution when applying. See documentation for details + --force-replace-on-error Same as --replace-on-error, but also try to delete and re-create objects. See + documentation for more details. + --no-obfuscate Disable obfuscation of sensitive/secret data + -o, --output-format stringArray Specify output format and target file, in the format 'format=path'. Format can + either be 'text' or 'yaml'. Can be specified multiple times. The actual format + for yaml is currently not documented and subject to change. + --replace-on-error When patching an object fails, try to replace it. See documentation for more + details. + --short-output When using the 'text' output format (which is the default), only names of + changes objects are shown instead of showing all changes. + +``` + + +``` +Command Results: + Configure how command results are stored. + + --command-result-namespace string Override the namespace to be used when writing command results. (default + "kluctl-results") + +``` + + +``` +Log arguments: + Configure logging. + + --log-grouping-time duration Logs are by default grouped by time passed, meaning that they are printed in + batches to make reading them easier. This argument allows to modify the + grouping time. (default 1s) + --log-since duration Show logs since this time. (default 1m0s) + --log-time If enabled, adds timestamps to log lines + +``` + + +``` +GitOps overrides: + Override settings for GitOps deployments. + + --target-context string Overrides the context name specified in the target. If the selected target does + not specify a context or the no-name target is used, --context will override the + currently active context. + +``` + \ No newline at end of file diff --git a/docs/kluctl/commands/gitops-reconcile.md b/docs/kluctl/commands/gitops-reconcile.md new file mode 100644 index 000000000..fdb3033be --- /dev/null +++ b/docs/kluctl/commands/gitops-reconcile.md @@ -0,0 +1,95 @@ + + +## Command + +Usage: kluctl gitops reconcile [flags] + +Trigger a GitOps reconciliation +This command will trigger an existing KluctlDeployment to perform a reconciliation loop. +It does this by setting the annotation 'kluctl.io/request-reconcile' to the current time. + +You can override many deployment relevant fields, see the list of command flags for details. + + + +## Arguments + +The following arguments are available: + +``` +GitOps arguments: + Specify gitops flags. + + --context string Override the context to use. + --controller-namespace string The namespace where the controller runs in. (default "kluctl-system") + --kubeconfig existingfile Overrides the kubeconfig to use. + -l, --label-selector string If specified, KluctlDeployments are searched and filtered by this label + selector. + --local-source-override-port int Specifies the local port to which the source-override client should + connect to when running the controller locally. + --name string Specifies the name of the KluctlDeployment. + -n, --namespace string Specifies the namespace of the KluctlDeployment. If omitted, the current + namespace from your kubeconfig is used. + +``` + + +``` +Misc arguments: + Command specific arguments. + + --abort-on-error Abort deploying when an error occurs instead of trying the remaining deployments + --dry-run Performs all kubernetes API calls in dry-run mode. + --force-apply Force conflict resolution when applying. See documentation for details + --force-replace-on-error Same as --replace-on-error, but also try to delete and re-create objects. See + documentation for more details. + --replace-on-error When patching an object fails, try to replace it. See documentation for more details. + +``` + + +``` +Command Results: + Configure how command results are stored. + + --command-result-namespace string Override the namespace to be used when writing command results. (default + "kluctl-results") + +``` + + +``` +Log arguments: + Configure logging. + + --log-grouping-time duration Logs are by default grouped by time passed, meaning that they are printed in + batches to make reading them easier. This argument allows to modify the + grouping time. (default 1s) + --log-since duration Show logs since this time. (default 1m0s) + --log-time If enabled, adds timestamps to log lines + +``` + + +``` +GitOps overrides: + Override settings for GitOps deployments. + + --no-wait Don't wait for objects readiness. + --prune Prune orphaned objects directly after deploying. See the help for the 'prune' + sub-command for details. + --target-context string Overrides the context name specified in the target. If the selected target does + not specify a context or the no-name target is used, --context will override the + currently active context. + +``` + \ No newline at end of file diff --git a/docs/kluctl/commands/gitops-resume.md b/docs/kluctl/commands/gitops-resume.md new file mode 100644 index 000000000..cdf2f1be2 --- /dev/null +++ b/docs/kluctl/commands/gitops-resume.md @@ -0,0 +1,79 @@ + + +## Command + +Usage: kluctl gitops resume [flags] + +Resume a GitOps deployment +This command will suspend a GitOps deployment by setting spec.suspend to 'true'. + + + +## Arguments + +The following arguments are available: + +``` +GitOps arguments: + Specify gitops flags. + + --context string Override the context to use. + --controller-namespace string The namespace where the controller runs in. (default "kluctl-system") + --kubeconfig existingfile Overrides the kubeconfig to use. + -l, --label-selector string If specified, KluctlDeployments are searched and filtered by this label + selector. + --local-source-override-port int Specifies the local port to which the source-override client should + connect to when running the controller locally. + --name string Specifies the name of the KluctlDeployment. + -n, --namespace string Specifies the namespace of the KluctlDeployment. If omitted, the current + namespace from your kubeconfig is used. + +``` + + +``` +Misc arguments: + Command specific arguments. + + --all If enabled, suspend all deployments. + --no-obfuscate Disable obfuscation of sensitive/secret data + -o, --output-format stringArray Specify output format and target file, in the format 'format=path'. Format can + either be 'text' or 'yaml'. Can be specified multiple times. The actual format + for yaml is currently not documented and subject to change. + --short-output When using the 'text' output format (which is the default), only names of + changes objects are shown instead of showing all changes. + +``` + + +``` +Command Results: + Configure how command results are stored. + + --command-result-namespace string Override the namespace to be used when writing command results. (default + "kluctl-results") + +``` + + +``` +Log arguments: + Configure logging. + + --log-grouping-time duration Logs are by default grouped by time passed, meaning that they are printed in + batches to make reading them easier. This argument allows to modify the + grouping time. (default 1s) + --log-since duration Show logs since this time. (default 1m0s) + --log-time If enabled, adds timestamps to log lines + +``` + diff --git a/docs/kluctl/commands/gitops-suspend.md b/docs/kluctl/commands/gitops-suspend.md new file mode 100644 index 000000000..7a701581c --- /dev/null +++ b/docs/kluctl/commands/gitops-suspend.md @@ -0,0 +1,79 @@ + + +## Command + +Usage: kluctl gitops suspend [flags] + +Suspend a GitOps deployment +This command will suspend a GitOps deployment by setting spec.suspend to 'true'. + + + +## Arguments + +The following arguments are available: + +``` +GitOps arguments: + Specify gitops flags. + + --context string Override the context to use. + --controller-namespace string The namespace where the controller runs in. (default "kluctl-system") + --kubeconfig existingfile Overrides the kubeconfig to use. + -l, --label-selector string If specified, KluctlDeployments are searched and filtered by this label + selector. + --local-source-override-port int Specifies the local port to which the source-override client should + connect to when running the controller locally. + --name string Specifies the name of the KluctlDeployment. + -n, --namespace string Specifies the namespace of the KluctlDeployment. If omitted, the current + namespace from your kubeconfig is used. + +``` + + +``` +Misc arguments: + Command specific arguments. + + --all If enabled, suspend all deployments. + --no-obfuscate Disable obfuscation of sensitive/secret data + -o, --output-format stringArray Specify output format and target file, in the format 'format=path'. Format can + either be 'text' or 'yaml'. Can be specified multiple times. The actual format + for yaml is currently not documented and subject to change. + --short-output When using the 'text' output format (which is the default), only names of + changes objects are shown instead of showing all changes. + +``` + + +``` +Command Results: + Configure how command results are stored. + + --command-result-namespace string Override the namespace to be used when writing command results. (default + "kluctl-results") + +``` + + +``` +Log arguments: + Configure logging. + + --log-grouping-time duration Logs are by default grouped by time passed, meaning that they are printed in + batches to make reading them easier. This argument allows to modify the + grouping time. (default 1s) + --log-since duration Show logs since this time. (default 1m0s) + --log-time If enabled, adds timestamps to log lines + +``` + diff --git a/docs/kluctl/commands/gitops-validate.md b/docs/kluctl/commands/gitops-validate.md new file mode 100644 index 000000000..f1ac80bff --- /dev/null +++ b/docs/kluctl/commands/gitops-validate.md @@ -0,0 +1,94 @@ + + +## Command + +Usage: kluctl gitops validate [flags] + +Trigger a GitOps validate +This command will trigger an existing KluctlDeployment to perform a reconciliation loop with a forced validation. +It does this by setting the annotation 'kluctl.io/request-validate' to the current time. + +You can override many deployment relevant fields, see the list of command flags for details. + + + +## Arguments + +The following arguments are available: + +``` +GitOps arguments: + Specify gitops flags. + + --context string Override the context to use. + --controller-namespace string The namespace where the controller runs in. (default "kluctl-system") + --kubeconfig existingfile Overrides the kubeconfig to use. + -l, --label-selector string If specified, KluctlDeployments are searched and filtered by this label + selector. + --local-source-override-port int Specifies the local port to which the source-override client should + connect to when running the controller locally. + --name string Specifies the name of the KluctlDeployment. + -n, --namespace string Specifies the namespace of the KluctlDeployment. If omitted, the current + namespace from your kubeconfig is used. + +``` + + +``` +Misc arguments: + Command specific arguments. + + --abort-on-error Abort deploying when an error occurs instead of trying the remaining deployments + --dry-run Performs all kubernetes API calls in dry-run mode. + --force-apply Force conflict resolution when applying. See documentation for details + --force-replace-on-error Same as --replace-on-error, but also try to delete and re-create objects. See + documentation for more details. + -o, --output stringArray Specify output target file. Can be specified multiple times + --replace-on-error When patching an object fails, try to replace it. See documentation for more details. + --warnings-as-errors Consider warnings as failures + +``` + + +``` +Command Results: + Configure how command results are stored. + + --command-result-namespace string Override the namespace to be used when writing command results. (default + "kluctl-results") + +``` + + +``` +Log arguments: + Configure logging. + + --log-grouping-time duration Logs are by default grouped by time passed, meaning that they are printed in + batches to make reading them easier. This argument allows to modify the + grouping time. (default 1s) + --log-since duration Show logs since this time. (default 1m0s) + --log-time If enabled, adds timestamps to log lines + +``` + + +``` +GitOps overrides: + Override settings for GitOps deployments. + + --target-context string Overrides the context name specified in the target. If the selected target does + not specify a context or the no-name target is used, --context will override the + currently active context. + +``` + \ No newline at end of file diff --git a/docs/kluctl/commands/helm-pull.md b/docs/kluctl/commands/helm-pull.md new file mode 100644 index 000000000..47f6d75f7 --- /dev/null +++ b/docs/kluctl/commands/helm-pull.md @@ -0,0 +1,29 @@ + + +## Command + +Usage: kluctl helm-pull [flags] + +Recursively searches for 'helm-chart.yaml' files and pre-pulls the specified Helm charts +Kluctl requires Helm Charts to be pre-pulled by default, which is handled by this command. It will collect +all required Charts and versions and pre-pull them into .helm-charts. To disable pre-pulling for individual charts, +set 'skipPrePull: true' in helm-chart.yaml. + + + +See [helm-integration](../deployments/helm.md) for more details. + +## Arguments +The following sets of arguments are available: +1. [project arguments](./common-arguments.md#project-arguments) (except `-a`) +1. [helm arguments](./common-arguments.md#helm-arguments) +1. [registry arguments](./common-arguments.md#registry-arguments) diff --git a/docs/kluctl/commands/helm-update.md b/docs/kluctl/commands/helm-update.md new file mode 100644 index 000000000..ca25c8a4b --- /dev/null +++ b/docs/kluctl/commands/helm-update.md @@ -0,0 +1,38 @@ + + +## Command + +Usage: kluctl helm-update [flags] + +Recursively searches for 'helm-chart.yaml' files and checks for new available versions +Optionally performs the actual upgrade and/or add a commit to version control. + + + +## Arguments +The following sets of arguments are available: +1. [project arguments](./common-arguments.md#project-arguments) (except `-a`) +1. [helm arguments](./common-arguments.md#helm-arguments) +1. [registry arguments](./common-arguments.md#registry-arguments) + +In addition, the following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --commit Create a git commit for every updated chart + -i, --interactive Ask for every Helm Chart if it should be upgraded. + --upgrade Write new versions into helm-chart.yaml and perform helm-pull afterwards + +``` + \ No newline at end of file diff --git a/docs/kluctl/commands/list-images.md b/docs/kluctl/commands/list-images.md new file mode 100644 index 000000000..2838b4515 --- /dev/null +++ b/docs/kluctl/commands/list-images.md @@ -0,0 +1,48 @@ + + +## Command + +Usage: kluctl list-images [flags] + +Renders the target and outputs all images used via 'images.get_image(...) +The result is a compatible with yaml files expected by --fixed-images-file. + +If fixed images ('-f/--fixed-image') are provided, these are also taken into account, +as described in the deploy command. + + + +## Arguments +The following sets of arguments are available: +1. [project arguments](./common-arguments.md#project-arguments) +1. [image arguments](./common-arguments.md#image-arguments) +1. [inclusion/exclusion arguments](./common-arguments.md#inclusionexclusion-arguments) +1. [helm arguments](./common-arguments.md#helm-arguments) +1. [registry arguments](./common-arguments.md#registry-arguments) + +In addition, the following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --kubernetes-version string Specify the Kubernetes version that will be assumed. This will also override + the kubeVersion used when rendering Helm Charts. + --offline-kubernetes Run command in offline mode, meaning that it will not try to connect the + target cluster + -o, --output stringArray Specify output target file. Can be specified multiple times + --render-output-dir string Specifies the target directory to render the project into. If omitted, a + temporary directory is used. + --simple Output a simplified version of the images list + +``` + diff --git a/docs/kluctl/commands/list-targets.md b/docs/kluctl/commands/list-targets.md new file mode 100644 index 000000000..551d483c8 --- /dev/null +++ b/docs/kluctl/commands/list-targets.md @@ -0,0 +1,32 @@ + + +## Command + +Usage: kluctl list-targets [flags] + +Outputs a yaml list with all targets +Outputs a yaml list with all targets + + + +## Arguments +The following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --only-names If provided --only-names will only output + -o, --output stringArray Specify output target file. Can be specified multiple times + +``` + \ No newline at end of file diff --git a/docs/kluctl/commands/oci-push.md b/docs/kluctl/commands/oci-push.md new file mode 100644 index 000000000..7921e1d68 --- /dev/null +++ b/docs/kluctl/commands/oci-push.md @@ -0,0 +1,41 @@ + + +## Command + +Usage: kluctl oci push [flags] + +Push to an oci repository +The push command creates a tarball from the current project and uploads the +artifact to an OCI repository. + + + +## Arguments +The following sets of arguments are available: +1. [registry arguments](./common-arguments.md#registry-arguments) + +In addition, the following arguments are available: + + +``` +Misc arguments: + Command specific arguments. + + --annotation stringArray Set custom OCI annotations in the format '=' + --output string the format in which the artifact digest should be printed, can be 'json' or 'yaml' + --timeout duration Specify timeout for all operations, including loading of the project, all + external api calls and waiting for readiness. (default 10m0s) + --url string Specifies the artifact URL. This argument is required. + +``` + + diff --git a/docs/kluctl/commands/poke-images.md b/docs/kluctl/commands/poke-images.md new file mode 100644 index 000000000..49fef9659 --- /dev/null +++ b/docs/kluctl/commands/poke-images.md @@ -0,0 +1,50 @@ + + +## Command + +Usage: kluctl poke-images [flags] + +Replace all images in target +This command will fully render the target and then only replace images instead of fully +deploying the target. Only images used in combination with 'images.get_image(...)' are +replaced + + + +## Arguments +The following sets of arguments are available: +1. [project arguments](./common-arguments.md#project-arguments) +1. [image arguments](./common-arguments.md#image-arguments) +1. [inclusion/exclusion arguments](./common-arguments.md#inclusionexclusion-arguments) +1. [command results arguments](./common-arguments.md#command-results-arguments) +1. [helm arguments](./common-arguments.md#helm-arguments) +1. [registry arguments](./common-arguments.md#registry-arguments) + +In addition, the following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --dry-run Performs all kubernetes API calls in dry-run mode. + --no-obfuscate Disable obfuscation of sensitive/secret data + -o, --output-format stringArray Specify output format and target file, in the format 'format=path'. Format can + either be 'text' or 'yaml'. Can be specified multiple times. The actual format + for yaml is currently not documented and subject to change. + --render-output-dir string Specifies the target directory to render the project into. If omitted, a + temporary directory is used. + --short-output When using the 'text' output format (which is the default), only names of + changes objects are shown instead of showing all changes. + -y, --yes Suppresses 'Are you sure?' questions and proceeds as if you would answer 'yes'. + +``` + \ No newline at end of file diff --git a/docs/kluctl/commands/prune.md b/docs/kluctl/commands/prune.md new file mode 100644 index 000000000..5c3c82305 --- /dev/null +++ b/docs/kluctl/commands/prune.md @@ -0,0 +1,49 @@ + + +## Command + +Usage: kluctl prune [flags] + +Searches the target cluster for prunable objects and deletes them + + +## Arguments +The following sets of arguments are available: +1. [project arguments](./common-arguments.md#project-arguments) +1. [image arguments](./common-arguments.md#image-arguments) +1. [inclusion/exclusion arguments](./common-arguments.md#inclusionexclusion-arguments) +1. [command results arguments](./common-arguments.md#command-results-arguments) +1. [helm arguments](./common-arguments.md#helm-arguments) +1. [registry arguments](./common-arguments.md#registry-arguments) + +In addition, the following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --discriminator string Override the target discriminator. + --dry-run Performs all kubernetes API calls in dry-run mode. + --no-obfuscate Disable obfuscation of sensitive/secret data + -o, --output-format stringArray Specify output format and target file, in the format 'format=path'. Format can + either be 'text' or 'yaml'. Can be specified multiple times. The actual format + for yaml is currently not documented and subject to change. + --render-output-dir string Specifies the target directory to render the project into. If omitted, a + temporary directory is used. + --short-output When using the 'text' output format (which is the default), only names of + changes objects are shown instead of showing all changes. + -y, --yes Suppresses 'Are you sure?' questions and proceeds as if you would answer 'yes'. + +``` + + +They have the same meaning as described in [deploy](./prune.md). diff --git a/docs/kluctl/commands/render.md b/docs/kluctl/commands/render.md new file mode 100644 index 000000000..cc6aac4d5 --- /dev/null +++ b/docs/kluctl/commands/render.md @@ -0,0 +1,45 @@ + + +## Command + +Usage: kluctl render [flags] + +Renders all resources and configuration files +Renders all resources and configuration files and stores the result in either +a temporary directory or a specified directory. + + + +## Arguments +The following sets of arguments are available: +1. [project arguments](./common-arguments.md#project-arguments) +1. [image arguments](./common-arguments.md#image-arguments) +1. [inclusion/exclusion arguments](./common-arguments.md#inclusionexclusion-arguments) +1. [helm arguments](./common-arguments.md#helm-arguments) +1. [registry arguments](./common-arguments.md#registry-arguments) + +In addition, the following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --kubernetes-version string Specify the Kubernetes version that will be assumed. This will also override + the kubeVersion used when rendering Helm Charts. + --offline-kubernetes Run command in offline mode, meaning that it will not try to connect the + target cluster + --print-all Write all rendered manifests to stdout + --render-output-dir string Specifies the target directory to render the project into. If omitted, a + temporary directory is used. + +``` + \ No newline at end of file diff --git a/docs/kluctl/commands/validate.md b/docs/kluctl/commands/validate.md new file mode 100644 index 000000000..352f365f8 --- /dev/null +++ b/docs/kluctl/commands/validate.md @@ -0,0 +1,44 @@ + + +## Command + +Usage: kluctl validate [flags] + +Validates the already deployed deployment +This means that all objects are retrieved from the cluster and checked for readiness. + +TODO: This needs to be better documented! + + + +## Arguments +The following sets of arguments are available: +1. [project arguments](./common-arguments.md#project-arguments) +1. [image arguments](./common-arguments.md#image-arguments) +1. [helm arguments](./common-arguments.md#helm-arguments) +1. [registry arguments](./common-arguments.md#registry-arguments) + +In addition, the following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + -o, --output stringArray Specify output target file. Can be specified multiple times + --render-output-dir string Specifies the target directory to render the project into. If omitted, a + temporary directory is used. + --sleep duration Sleep duration between validation attempts (default 5s) + --wait duration Wait for the given amount of time until the deployment validates + --warnings-as-errors Consider warnings as failures + +``` + diff --git a/docs/kluctl/commands/webui-build.md b/docs/kluctl/commands/webui-build.md new file mode 100644 index 000000000..bae3e34d7 --- /dev/null +++ b/docs/kluctl/commands/webui-build.md @@ -0,0 +1,36 @@ + + +## Command + +Usage: kluctl webui build [flags] + +Build the static Kluctl Webui +This command will build the static Kluctl Webui. + + + +## Arguments + +The following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --all-contexts Use all Kubernetes contexts found in the kubeconfig. + --context stringArray List of kubernetes contexts to use. Defaults to the current context. + --max-results int Specify the maximum number of results per target. (default 1) + --path string Output path. + +``` + + diff --git a/docs/kluctl/commands/webui-run.md b/docs/kluctl/commands/webui-run.md new file mode 100644 index 000000000..a3dc0dd15 --- /dev/null +++ b/docs/kluctl/commands/webui-run.md @@ -0,0 +1,84 @@ + + +## Command + +Usage: kluctl webui run [flags] + +Run the Kluctl Webui + + +## Arguments + +The following arguments are available: + +``` +Misc arguments: + Command specific arguments. + + --all-contexts Use all Kubernetes contexts found in the kubeconfig. + --context stringArray List of kubernetes contexts to use. + --controller-namespace string The namespace where the controller runs in. (default "kluctl-system") + --host string Host to bind to. Pass an empty string to bind to all addresses. Defaults to + 'localhost' when run locally and to all hosts when run in-cluster. + --in-cluster This enables in-cluster functionality. This also enforces authentication. + --in-cluster-context string The context to use fo in-cluster functionality. + --kubeconfig existingfile Overrides the kubeconfig to use. + --only-api Only serve API without the actual UI. + --path-prefix string Specify the prefix of the path to serve the webui on. This is required when + using a reverse proxy, ingress or gateway that serves the webui on another + path than /. (default "/") + --port int Port to bind to. (default 8080) + +``` + + + +``` +Auth arguments: + Configure authentication. + + --auth-admin-rbac-user string Specify the RBAC user to use for admin access. (default + "kluctl-webui-admin") + --auth-logout-return-param string Specify the parameter name to pass to the logout redirect url, + containing the return URL to redirect back. + --auth-logout-url string Specify the logout URL, to which the user should be redirected + after clearing the Kluctl Webui session. + --auth-oidc-admins-group stringArray Specify admins group names.' + --auth-oidc-client-id string Specify the ClientID. + --auth-oidc-client-secret-key string Specify the secret name for the ClientSecret. (default + "oidc-client-secret") + --auth-oidc-client-secret-name string Specify the secret name for the ClientSecret. (default "webui-secret") + --auth-oidc-display-name string Specify the name of the OIDC provider to be displayed on the login + page. (default "OpenID Connect") + --auth-oidc-group-claim string Specify claim for the groups.' (default "groups") + --auth-oidc-issuer-url string Specify the OIDC provider's issuer URL. + --auth-oidc-param stringArray Specify additional parameters to be passed to the authorize endpoint. + --auth-oidc-redirect-url string Specify the redirect URL. + --auth-oidc-scope stringArray Specify the scopes. + --auth-oidc-user-claim string Specify claim for the username.' (default "email") + --auth-oidc-viewers-group stringArray Specify viewers group names.' + --auth-secret-key string Specify the secret key for the secret used for internal encryption + of tokens and cookies. (default "auth-secret") + --auth-secret-name string Specify the secret name for the secret used for internal encryption + of tokens and cookies. (default "webui-secret") + --auth-static-admin-secret-key string Specify the secret key for the admin password. (default + "admin-password") + --auth-static-login-enabled Enable the admin user. (default true) + --auth-static-login-secret-name string Specify the secret name for the admin and viewer passwords. + (default "webui-secret") + --auth-static-viewer-secret-key string Specify the secret key for the viewer password. (default + "viewer-password") + --auth-viewer-rbac-user string Specify the RBAC user to use for viewer access. (default + "kluctl-webui-viewer") + +``` + diff --git a/docs/kluctl/deployments/README.md b/docs/kluctl/deployments/README.md new file mode 100644 index 000000000..56f7d7532 --- /dev/null +++ b/docs/kluctl/deployments/README.md @@ -0,0 +1,82 @@ + + +## Table of Contents + +1. [Deployments](./deployment-yml.md) +2. [Kustomize Integration](./kustomize.md) +3. [Container Images](./images.md) +4. [Helm Integration](./helm.md) +5. [OCI Integration](./oci.md) +6. [SOPS Integration](./sops.md) +7. [Hooks](./hooks.md) +8. [Readiness](./readiness.md) +9. [Tags](./tags.md) +10. [Annotations](./annotations) + +A deployment project is a collection of deployment items and sub-deployments. Deployment items are usually +[Kustomize](./kustomize.md) deployments, but can also integrate [Helm Charts](./helm.md). + +## Basic structure + +The following visualization shows the basic structure of a deployment project. The entry point of every deployment +project is the `deployment.yaml` file, which then includes further sub-deployments and kustomize deployments. It also +provides some additional configuration required for multiple kluctl features to work as expected. + +As can be seen, sub-deployments can include other sub-deployments, allowing you to structure the deployment project +as you need. + +Each level in this structure recursively adds [tags](./tags.md) to each deployed resources, allowing you to control +precisely what is deployed in the future. + +``` +-- project-dir/ + |-- deployment.yaml + |-- .gitignore + |-- kustomize-deployment1/ + | |-- kustomization.yaml + | `-- resource.yaml + |-- sub-deployment/ + | |-- deployment.yaml + | |-- kustomize-deployment2/ + | | |-- resource1.yaml + | | `-- ... + | |-- kustomize-deployment3/ + | | |-- kustomization.yaml + | | |-- resource1.yaml + | | |-- resource2.yaml + | | |-- patch1.yaml + | | `-- ... + | |-- kustomize-with-helm-deployment + | | |-- charts/ + | | | `-- ... + | | |-- kustomization.yaml + | | |-- helm-chart.yaml + | | `-- helm-values.yaml + | `-- subsub-deployment/ + | |-- deployment.yaml + | |-- ... kustomize deployments + | `-- ... subsubsub deployments + `-- sub-deployment/ + `-- ... +``` + +## Order of deployments +Deployments are done in parallel, meaning that there are usually no order guarantees. The only way to somehow control +order, is by placing [barriers](./deployment-yml.md#barriers) between kustomize deployments. +You should however not overuse barriers, as they negatively impact the speed of kluctl. + +## Plain Kustomize + +It's also possible to use Kluctl on plain Kustomize deployments. Simply run `kluctl deploy` from inside the +folder of your `kustomization.yaml`. If you also don't have a `.kluctl.yaml`, you can also work without targets. + +Please note that pruning and deletion is not supported in this mode. \ No newline at end of file diff --git a/docs/kluctl/deployments/annotations/README.md b/docs/kluctl/deployments/annotations/README.md new file mode 100644 index 000000000..3c38ab58b --- /dev/null +++ b/docs/kluctl/deployments/annotations/README.md @@ -0,0 +1,17 @@ + + +## Table of Contents + +1. [All Resources](./all-resources.md) +2. [Hooks](./hooks.md) +3. [Validation](./validation.md) +4. [Kustomize](./kustomization.md) diff --git a/docs/kluctl/deployments/annotations/all-resources.md b/docs/kluctl/deployments/annotations/all-resources.md new file mode 100644 index 000000000..168c97b03 --- /dev/null +++ b/docs/kluctl/deployments/annotations/all-resources.md @@ -0,0 +1,153 @@ + + +# All resources + +The following annotations control the behavior of the `deploy` and related commands. + +## Control deploy behavior + +The following annotations control deploy behavior, especially in regard to conflict resolution. + +### kluctl.io/delete +If set to "true", the resource will be deleted at deployment time. Kluctl will not emit an error in case the resource +does not exist. A resource with this annotation does not have to be complete/valid as it is never sent to the Kubernetes +api server. + +### kluctl.io/force-apply +If set to "true", the whole resource will be force-applied, meaning that all fields will be overwritten in case of +field manager conflicts. + +As an alternative, conflict resolution can be controlled via [conflictResolution](../deployment-yml.md#conflictresolution). + +### kluctl.io/force-apply-field +Specifies a [JSON Path](https://goessner.net/articles/JsonPath/) for fields that should be force-applied. Matching +fields will be overwritten in case of field manager conflicts. + +If more than one field needs to be specified, add `-xxx` to the annotation key, where `xxx` is an arbitrary number. + +As an alternative, conflict resolution can be controlled via [conflictResolution](../deployment-yml.md#conflictresolution). + +### kluctl.io/force-apply-manager +Specifies a regex for managers that should be force-applied. Fields with matching managers will be overwritten in +case of field manager conflicts. + +If more than one field needs to be specified, add `-xxx` to the annotation key, where `xxx` is an arbitrary number. + +As an alternative, conflict resolution can be controlled via [conflictResolution](../deployment-yml.md#conflictresolution). + +### kluctl.io/ignore-conflicts +If set to "true", the whole all fields of the object are going to be ignored when conflicts arise. +This effectively disables the warnings that are shown when field ownership is lost. + +As an alternative, conflict resolution can be controlled via [conflictResolution](../deployment-yml.md#conflictresolution). + +### kluctl.io/ignore-conflicts-field +Specifies a [JSON Path](https://goessner.net/articles/JsonPath/) for fields that should be ignored when conflicts arise. +This effectively disables the warnings that are shown when field ownership is lost. + +If more than one field needs to be specified, add `-xxx` to the annotation key, where `xxx` is an arbitrary number. + +As an alternative, conflict resolution can be controlled via [conflictResolution](../deployment-yml.md#conflictresolution). + +### kluctl.io/ignore-conflicts-manager +Specifies a regex for field managers that should be ignored when conflicts arise. +This effectively disables the warnings that are shown when field ownership is lost. + +If more than one manager needs to be specified, add `-xxx` to the annotation key, where `xxx` is an arbitrary number. + +As an alternative, conflict resolution can be controlled via [conflictResolution](../deployment-yml.md#conflictresolution). + +### kluctl.io/wait-readiness +If set to `true`, kluctl will wait for readiness of this object. Readiness is defined +the same as in [hook readiness](../../deployments/readiness.md). Waiting happens after all resources from the parent +deployment item have been applied. + +### kluctl.io/is-ready +If set to `true`, kluctl will always consider this object as [ready](../../deployments/readiness.md). If set to `false`, +kluctl will always consider this object as not ready. If omitted, kluctl will perform normal readiness checks. + +This annotation is useful if you need to introduce externalized readiness determination, e.g. inside a non-hook `Pod` +that can annotate an object that something got ready. + +## Control deletion/pruning + +The following annotations control how delete/prune is behaving. + +### kluctl.io/skip-delete +If set to "true", the annotated resource will not be deleted when [delete](../../commands/delete.md) or +[prune](../../commands/prune.md) is called. + +### kluctl.io/skip-delete-if-tags +If set to "true", the annotated resource will not be deleted when [delete](../../commands/delete.md) or +[prune](../../commands/prune.md) is called and inclusion/exclusion tags are used at the same time. + +This tag is especially useful and required on resources that would otherwise cause cascaded deletions of resources that +do not match the specified inclusion/exclusion tags. Namespaces are the most prominent example of such resources, as +they most likely don't match exclusion tags, but cascaded deletion would still cause deletion of the excluded resources. + +### kluctl.io/force-managed +If set to "true", Kluctl will always treat the annotated resource as being managed by Kluctl, meaning that it will +consider it for deletion and pruning even if a foreign field manager resets/removes the Kluctl field manager or if +foreign controllers add `ownerReferences` even though they do not really own the resources. + +## Control diff behavior + +The following annotations control how diffs are performed. + +### kluctl.io/diff-name +This annotation will override the name of the object when looking for the in-cluster version of an object used for +diffs. This is useful when you are forced to use new names for the same objects whenever the content changes, e.g. +for all kinds of immutable resource types. + +Example (filename job.yaml): +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: myjob-{{ load_sha256("job.yaml", 6) }} + annotations: + kluctl.io/diff-name: myjob +spec: + template: + spec: + containers: + - name: hello + image: busybox + command: ["sh", "-c", "echo hello"] + restartPolicy: Never +``` + +Without the `kluctl.io/diff-name` annotation, any change to the `job.yaml` would be treated as a new object in resulting +diffs from various commands. This is due to the inclusion of the file hash in the job name. This would make it very hard +to figure out what exactly changed in an object. + +With the `kluctl.io/diff-name` annotation, kluctl will pick an existing job from the cluster with the same diff-name +and use it for the diff, making it a lot easier to analyze changes. If multiple objects match, the one with the youngest +`creationTimestamp` is chosen. + +Please note that this will not cause old objects (with the same diff-name) to be prunes. You still have to regularely +prune the deployment. + +### kluctl.io/ignore-diff +If set to "true", the whole resource will be ignored while calculating diffs. + +### kluctl.io/ignore-diff-field +Specifies a [JSON Path](https://goessner.net/articles/JsonPath/) for fields that should be ignored while calculating +diffs. + +If more than one field needs to be specified, add `-xxx` to the annotation key, where `xxx` is an arbitrary number. + +### kluctl.io/ignore-diff-field-regex +Same as [kluctl.io/ignore-diff-field](#kluctlioignore-diff-field) but specifying a regular expressions instead of a +JSON Path. + +If more than one field needs to be specified, add `-xxx` to the annotation key, where `xxx` is an arbitrary number. diff --git a/docs/kluctl/deployments/annotations/hooks.md b/docs/kluctl/deployments/annotations/hooks.md new file mode 100644 index 000000000..a250f2676 --- /dev/null +++ b/docs/kluctl/deployments/annotations/hooks.md @@ -0,0 +1,29 @@ + + +# Hooks + +The following annotations control hook execution + +See [hooks](../../deployments/hooks.md) for more details. + +### kluctl.io/hook +Declares a resource to be a hook, which is deployed/executed as described in [hooks](../../deployments/hooks.md). The value of the +annotation determines when the hook is deployed/executed. + +### kluctl.io/hook-weight +Specifies a weight for the hook, used to determine deployment/execution order. For resources with the same `kluctl.io/hook` annotation, hooks are executed in ascending order based on hook-weight. + +### kluctl.io/hook-delete-policy +Defines when to delete the hook resource. + +### kluctl.io/hook-wait +Defines whether kluctl should wait for hook-completion. It defaults to `true` and can be manually set to `false`. diff --git a/docs/kluctl/deployments/annotations/kustomization.md b/docs/kluctl/deployments/annotations/kustomization.md new file mode 100644 index 000000000..e2d88c27d --- /dev/null +++ b/docs/kluctl/deployments/annotations/kustomization.md @@ -0,0 +1,39 @@ + + +# Kustomize + +Even though the `kustomization.yaml` from Kustomize deployments are not really Kubernetes resources (as they are not +really deployed), they have the same structure as Kubernetes resources. This also means that the `kustomization.yaml` +can define metadata and annotations. Through these annotations, additional behavior on the deployment can be controlled. + +Example: +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +metadata: + annotations: + kluctl.io/barrier: "true" + kluctl.io/wait-readiness: "true" + +resources: + - deployment.yaml +``` + +### kluctl.io/barrier +If set to `true`, kluctl will wait for all previous objects to be applied (but not necessarily ready). This has the +same effect as [barrier](../../deployments/deployment-yml.md#barriers) from deployment projects. + +### kluctl.io/wait-readiness +If set to `true`, kluctl will wait for readiness of all objects from this kustomization project. Readiness is defined +the same as in [hook readiness](../../deployments/readiness.md). Waiting happens after all resources from the current +deployment item have been applied. diff --git a/docs/kluctl/deployments/annotations/validation.md b/docs/kluctl/deployments/annotations/validation.md new file mode 100644 index 000000000..a6c35372c --- /dev/null +++ b/docs/kluctl/deployments/annotations/validation.md @@ -0,0 +1,24 @@ + + +# Validation + +The following annotations influence the [validate](../../commands/validate.md) command. + +### validate-result.kluctl.io/xxx +If this annotation is found on a resource that is checked while validation, the key and the value of the annotation +are added to the validation result, which is then returned by the validate command. + +The annotation key is dynamic, meaning that all annotations that begin with `validate-result.kluctl.io/` are taken +into account. + +### kluctl.io/validate-ignore +If this annotation is set to `true`, the object will be ignored while `kluctl validate` is run. \ No newline at end of file diff --git a/docs/kluctl/deployments/deployment-yml.md b/docs/kluctl/deployments/deployment-yml.md new file mode 100644 index 000000000..a7459655a --- /dev/null +++ b/docs/kluctl/deployments/deployment-yml.md @@ -0,0 +1,574 @@ + + +# Deployments + +The `deployment.yaml` file is the entrypoint for the deployment project. Included sub-deployments also provide a +`deployment.yaml` file with the same structure as the initial one. + +An example `deployment.yaml` looks like this: +```yaml +deployments: +- path: nginx +- path: my-app +- include: monitoring +- git: + url: git@github.com:example/example.git +- oci: + url: oci://ghcr.io/kluctl/kluctl-examples/simple + +commonLabels: + my.prefix/target: "{{ target.name }}" + my.prefix/deployment-project: my-deployment-project +``` + +The following sub-chapters describe the available fields in the `deployment.yaml` + +## deployments + +`deployments` is a list of deployment items. Multiple deployment types are supported, which is documented further down. +Individual deployments are performed in parallel, unless a [barrier](#barriers) is encountered which causes kluctl to +wait for all previous deployments to finish. + +Deployments can also be conditional by using the [when](#when) field. + +### Simple deployments + +Simple deployments are specified via `path` and are expected to be directories with Kubernetes manifests inside. +Kluctl will internally generate a kustomization.yaml from these manifests and treat the deployment item the same way +as it would treat a [Kustomize deployment](#kustomize-deployments). + +Example: +```yaml +deployments: +- path: path/to/manifests +``` + +### Kustomize deployments + +When the deployment item directory specified via `path` contains a `kustomization.yaml`, Kluctl will use this file +instead of generating one. + +Please see [Kustomize integration](./kustomize.md) for more details. + +Example: +```yaml +deployments: +- path: path/to/deployment1 +- path: path/to/deployment2 +``` + +The `path` must point to a directory relative to the directory containing the `deployment.yaml`. Only directories +that are part of the kluctl project are allowed. The directory must contain a valid `kustomization.yaml`. + +### Includes + +Specifies a sub-deployment project to be included. The included sub-deployment project will inherit many properties +of the parent project, e.g. tags, commonLabels and so on. + +Example: +```yaml +deployments: +- include: path/to/sub-deployment +``` + +The `path` must point to a directory relative to the directory containing the `deployment.yaml`. Only directories +that are part of the kluctl project are allowed. The directory must contain a valid `deployment.yaml`. + +### Git includes + +Specifies an external git project to be included. The project is included the same way with regular includes, except +that the included project can not use/load templates from the parent project. An included project might also include +further git projects. + +If the included project is a [Kluctl Library Project](../kluctl-libraries/README.md), current variables are NOT passed +automatically into the included project. Only when [passVars](#passvars) is set to true, all current variables are passed. +For library projects, [args](#args) is the preferred way to pass configuration. + +Simple example: +```yaml +deployments: +- git: git@github.com:example/example.git +``` + +This will clone the git repository at `git@github.com:example/example.git`, checkout the default branch and include it +into the current project. + +Advanced Example: +```yaml +deployments: +- git: + url: git@github.com:example/example.git + ref: + branch: my-branch + subDir: some/sub/dir +``` + +The url specifies the Git url to be cloned and checked out. + +`ref` is optional and specifies the branch or tag to be used. To specify a branch, set the sub-field `branch` as seen +in the above example. To pass a tag, set the `tag` field instead. To pass a commit, set the `commit` field instead. + +If `ref` is omitted, the default branch will be checked out. + +`subDir` is optional and specifies the sub directory inside the git repository to include. + +### OCI includes + +Specifies an OCI based artifact to include. The artifact must be pushed to your OCI repository via the +[`kluctl oci push`](../commands/oci-push.md) command. The artifact is extracted and then included the same way a +[git include](#git-includes) is included. + +If the included project is a [Kluctl Library Project](../kluctl-libraries/README.md), current variables are NOT passed +automatically into the included project. Only when [passVars](#passvars) is set to true, all current variables are passed. +For library projects, [args](#args) is the preferred way to pass configuration. + +Simple example: +```yaml +deployments: +- oci: + url: oci://ghcr.io/kluctl/kluctl-examples/simple +``` + +The `url` specifies the OCI repository url. It must use the `oci://` scheme. It is not allowed to add tags or digests to +the url. Instead, use the dedicated `ref` field: + +```yaml +deployments: +- oci: + url: oci://ghcr.io/kluctl/kluctl-examples/simple + ref: + tag: latest +``` + +For digests, use: + +```yaml +deployments: +- oci: + url: oci://ghcr.io/kluctl/kluctl-examples/simple + ref: + digest: sha256:9ac3ba762c373ebccecb9dd3ac1d8ca091e4bd4a101701ce99e6058c0c74eedc +``` + +Subdirectories of the pushed artifact can be specified via `subDir`: + +```yaml +deployments: +- oci: + url: oci://ghcr.io/kluctl/kluctl-examples/simple + subDir: my-subdir +``` + +See [OCI support](./oci.md) for more details, especially in regard to authentication for private registries. + +### Barriers +Causes kluctl to wait until all previous kustomize deployments have been applied. This is useful when +upcoming deployments need the current or previous deployments to be finished beforehand. Previous deployments also +include all sub-deployments from included deployments. + +Please note that barriers do not wait for readiness of individual resources. This means that it will not wait for +readiness of services, deployments, daemon sets, and so on. To actually wait for readiness, use `waitReadiness: true` or +`waitReadinessObjects`. + +Example: +```yaml +deployments: +- path: kustomizeDeployment1 +- path: kustomizeDeployment2 +- include: subDeployment1 +- barrier: true +# At this point, it's ensured that kustomizeDeployment1, kustomizeDeployment2 and all sub-deployments from +# subDeployment1 are fully deployed. +- path: kustomizeDeployment3 +``` + +To create a barrier with a custom message, include the message parameter when creating the barrier. The message parameter accepts a string value that represents the custom message. + +Example: +```yaml +deployments: +- path: kustomizeDeployment1 +- path: kustomizeDeployment2 +- include: subDeployment1 +- barrier: true + message: "Waiting for subDeployment1 to be finished" +# At this point, it's ensured that kustomizeDeployment1, kustomizeDeployment2 and all sub-deployments from +# subDeployment1 are fully applied. +- path: kustomizeDeployment3 +``` +If no custom message is provided, the barrier will be created without a specific message, and the default behavior will be applied. + +When viewing the `kluctl deploy` status, the custom message, if provided, will be displayed along with default barrier information. + +### waitReadiness +`waitReadiness` can be set on all deployment items. If set to `true`, Kluctl will wait for readiness of each individual object +of the current deployment item. Readiness is defined in [readiness](./readiness.md). + +Please note that Kluctl will not wait for readiness of previous deployment items. + +This can also be combined with [barriers](#barriers), which will instruct Kluctl to stop processing the next deployment +items until everything before the barrier is applied and the current deployment item's objects are all ready. + +Examples: +```yaml +deployments: +- path: kustomizeDeployment1 + waitReadiness: true +- path: kustomizeDeployment2 + # this will wait for kustomizeDeployment1 to be applied+ready and kustomizeDeployment2 to be applied + # kustomizeDeployment2 is not guaranteed to be ready at this point, but might be due to the parallel nature of Kluctl +- barrier: true +- path: kustomizeDeployment3 +``` + +### waitReadinessObjects +This is comparable to `waitReadiness`, but instead of waiting for all objects of the current deployment item, it allows +to explicitly specify objects which are not necessarily part of the current (or any) deployment item. + +This is for example useful if you used an external Helm Chart and want to wait for readiness of some individual objects, +e.g. CRDs that are being deployment by some in-cluster operator instead of the Helm chart itself. + +Examples: +```yaml +deployments: +# The cilium Helm chart does not deploy CRDs anymore. Instead, the cilium-operator does this on startup. This means, +# we can't apply CiliumNetworkPolicies before the CRDs get applied by the operator. +- path: cilium +- barrier: true + waitReadinessObjects: + - kind: Deployment + name: cilium-operator + namespace: kube-system + - kind: CustomResourceDefinition + name: ciliumnetworkpolicies.cilium.io +# This deployment can now safely use the CRDs applied by the operator +- path: kustomizeDeployment1 +``` + +### deleteObjects +Causes kluctl to delete matching objects, specified by a list of group/kind/name/namespace dictionaries. +The order/parallelization of deletion is identical to the order and parallelization of normal deployment items, +meaning that it happens in parallel by default until a barrier is encountered. + +Example: +```yaml +deployments: + - deleteObjects: + - group: apps + kind: DaemonSet + namespace: kube-system + name: kube-proxy + - barrier: true + - path: my-cni +``` + +The above example shows how to delete the kube-proxy DaemonSet before installing a CNI (e.g. Cilium in +proxy-replacement mode). + +## deployments common properties +All entries in `deployments` can have the following common properties: + +### vars (deployment item) +A list of variable sets to be loaded into the templating context, which is then available in all [deployment items](#deployments) +and [sub-deployments](#includes). + +See [templating](../templating/variable-sources.md) for more details. + +Example: +```yaml +deployments: +- path: kustomizeDeployment1 + vars: + - file: vars1.yaml + - values: + var1: value1 +- path: kustomizeDeployment2 +# all sub-deployments of this include will have the given variables available in their Jinj2 context. +- include: subDeployment1 + vars: + - file: vars2.yaml +``` + +### passVars +Can only be used on [include](#includes), [git include](#git-includes) and [oci include](#oci-includes). If set to `true`, +all variables will be passed down to the included project even if the project is an explicitly marked +[Kluctl Library Project](../kluctl-libraries/README.md). + +If the included project is not a library project, variables are always fully passed into the included deployment. + +### args +Can only be used on [include](#includes), [git include](#git-includes) and [oci include](#oci-includes). Passes the given +arguments into [Kluctl Library Projects](../kluctl-libraries/README.md). + +### when +Each deployment item can be conditional with the help of the `when` field. It must be set to a +[Jinja2 based expression](https://jinja.palletsprojects.com/en/3.1.x/templates/#expressions) +that evaluates to a boolean. + +Example: +```yaml +deployments: +- path: item1 +- path: item2 + when: my.var == "my-value" +``` + +### tags (deployment item) +A list of tags the deployment should have. See [tags](./tags.md) for more details. For includes, this means that all +sub-deployments will get these tags applied to. If not specified, the default tags logic as described in [tags](./tags.md) +is applied. + +Example: + +```yaml +deployments: +- path: kustomizeDeployment1 + tags: + - tag1 + - tag2 +- path: kustomizeDeployment2 + tags: + - tag3 +# all sub-deployments of this include will get tag4 applied +- include: subDeployment1 + tags: + - tag4 +``` + +### alwaysDeploy +Forces a deployment to be included everytime, ignoring inclusion/exclusion sets from the command line. +See [Deploying with tag inclusion/exclusion](./tags.md#deploying-with-tag-inclusionexclusion) for details. + +```yaml +deployments: +- path: kustomizeDeployment1 + alwaysDeploy: true +- path: kustomizeDeployment2 +``` + +Please note that `alwaysDeploy` will also cause [kluctl render](../commands/render.md) to always render the resources. + +### skipDeleteIfTags +Forces exclusion of a deployment whenever inclusion/exclusion tags are specified via command line. +See [Deleting with tag inclusion/exclusion](./tags.md#deploying-with-tag-inclusionexclusion) for details. + +```yaml +deployments: +- path: kustomizeDeployment1 + skipDeleteIfTags: true +- path: kustomizeDeployment2 +``` + +### onlyRender +Causes a path to be rendered only but not treated as a deployment item. This can be useful if you for example want to +use Kustomize components which you'd refer from other deployment items. + +```yaml +deployments: +- path: component + onlyRender: true +- path: kustomizeDeployment2 +``` + +## vars (deployment project) +A list of variable sets to be loaded into the templating context, which is then available in all [deployment items](#deployments) +and [sub-deployments](#includes). + +See [templating](../templating/variable-sources.md) for more details. + +## commonLabels +A dictionary of [labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) and values to be +added to all resources deployed by any of the deployment items in this deployment project. + +Consider the following example `deployment.yaml`: +```yaml +deployments: + - path: nginx + - include: sub-deployment1 + +commonLabels: + my.prefix/target: {{ target.name }} + my.prefix/deployment-name: my-deployment-project-name + my.prefix/label-1: value-1 + my.prefix/label-2: value-2 +``` + +Every resource deployed by the kustomize deployment `nginx` will now get the four provided labels attached. All included +sub-deployment projects (e.g. `sub-deployment1`) will also recursively inherit these labels and pass them further +down. + +In case an included sub-deployment project also contains `commonLabels`, both dictionaries of commonLabels are merged +inside the included sub-deployment project. In case of conflicts, the included common labels override the inherited. + +Please note that these `commonLabels` are not related to `commonLabels` supported in `kustomization.yaml` files. It was +decided to not rely on this feature but instead attach labels manually to resources right before sending them to +kubernetes. This is due to an [implementation detail](https://github.com/kubernetes-sigs/kustomize/issues/1009) in +kustomize which causes `commonLabels` to also be applied to label selectors, which makes otherwise editable resources +read-only when it comes to `commonLabels`. + +## commonAnnotations +A dictionary of [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) and values to be +added to all resources deployed by any of the deployment items in this deployment project. + +`commonAnnotations` are handled the same as [commonLabels](#commonlabels) in regard to inheriting, merging and overriding. + +## overrideNamespace +A string that is used as the default namespace for all kustomize deployments which don't have a `namespace` set in their +`kustomization.yaml`. + +## tags (deployment project) +A list of common tags which are applied to all kustomize deployments and sub-deployment includes. + +See [tags](./tags.md) for more details. + +## ignoreForDiff + +A list of rules used to determine which differences should be ignored in diff outputs. + +As an alternative, [annotations](./annotations/all-resources.md#control-diff-behavior) can be used to control +diff behavior of individual resources. + +Consider the following example: + +```yaml +deployments: + - ... + +ignoreForDiff: + - kind: Deployment + name: my-deployment + fieldPath: spec.replicas +``` + +This will ignore differences for the `spec.replicas` field in the `Deployment` with the name `my-deployment`. + +Using regex expressions instead of JSON Pathes is also supported: + +```yaml +deployments: + - ... + +ignoreForDiff: + - kind: Deployment + name: my-deployment + fieldPathRegex: metadata.labels.my-label-.* +``` + +The following properties are supported in `ignoreForDiff` items. + +### fieldPath +If specified, must be a valid [JSON Path](https://goessner.net/articles/JsonPath/). Kluctl will ignore differences for +all matching fields of all matching objects (see the other properties). + +Either `fieldPath` or `fieldPathRegex` must be provided. + +### fieldPathRegex +If specified, must be a valid regex. Kluctl will ignore differences for all matching fields of all matching objects +(see the other properties). + +Either `fieldPath` or `fieldPathRegex` must be provided. + +### group +This property is optional. If specified, only objects with a matching api group will be considered. Please note that this +field should NOT include the version of the api group. + +### kind +This property is optional. If specified, only objects with a matching `kind` will be considered. + +### namespace +This property is optional. If specified, only objects with a matching `namespace` will be considered. + +### name +This property is optional. If specified, only objects with a matching `name` will be considered. + +## conflictResolution + +A list of rules used to determine how to handle conflict resolution. + +As an alternative, [annotations](./annotations/all-resources.md#control-deploy-behavior) can be used to control +conflict resolution of individual resources. + +Consider the following example: + +```yaml +deployments: + - ... + +conflictResolution: + - kind: ValidatingWebhookConfiguration + fieldPath: webhooks.*.* + action: ignore +``` + +This will cause Kluctl to ignore conflicts on all matching fields of all `ValidatingWebhookConfiguration` objects. + +Using regex expressions instead of JSON Pathes is also supported: + +```yaml +deployments: + - ... + +conflictResolution: + - kind: ValidatingWebhookConfiguration + fieldPathRegex: webhooks\.. + action: ignore +``` + +In some cases, it's easier to match fields by manager name: + +```yaml +deployments: + - ... + +conflictResolution: + - manager: clusterrole-aggregation-controller + action: ignore + - manager: cert-manager-cainjector + action: ignore +``` + +The following properties are supported in `conflictResolution` items. + +### fieldPath +If specified, must be a valid [JSON Path](https://goessner.net/articles/JsonPath/). Kluctl will ignore conflicts for +all matching fields of all matching objects (see the other properties). + +Either `fieldPath`, `fieldPathRegex` or `manager` must be provided. + +### fieldPathRegex +If specified, must be a valid regex. Kluctl will ignore conflicts for all matching fields of all matching objects +(see the other properties). + +Either `fieldPath`, `fieldPathRegex` or `manager` must be provided. + +### manager +If specified, must be a valid regex. Kluctl will ignore conflicts for all fields that currently have a matching field +manager assigned. This is useful if a mutating webhook or controller is known to modify fields after they have been +applied. + +Either `fieldPath`, `fieldPathRegex` or `manager` must be provided. + +### action +This field is required and must be either `ignore` or `force-apply`. + +### group +This property is optional. If specified, only objects with a matching api group will be considered. Please note that this +field should NOT include the version of the api group. + +### kind +This property is optional. If specified, only objects with a matching `kind` will be considered. + +### namespace +This property is optional. If specified, only objects with a matching `namespace` will be considered. + +### name +This property is optional. If specified, only objects with a matching `name` will be considered. diff --git a/docs/kluctl/deployments/helm.md b/docs/kluctl/deployments/helm.md new file mode 100644 index 000000000..c4d9c8f82 --- /dev/null +++ b/docs/kluctl/deployments/helm.md @@ -0,0 +1,244 @@ + + +# Helm Integration + +kluctl offers a simple-to-use Helm integration, which allows you to reuse many common third-party Helm Charts. + +The integration is split into 2 parts/steps/layers. The first is the management and pulling of the Helm Charts, while +the second part handles configuration/customization and deployment of the chart. + +It is recommended to pre-pull Helm Charts with [`kluctl helm-pull`](../commands/helm-pull.md), which will store the +pulled charts inside `.helm-charts` of the project directory. It is however also possible (but not +recommended) to skip the pre-pulling phase and let kluctl pull Charts on-demand. + +When pre-pulling Helm Charts, you can also add the resulting Chart contents into version control. This is actually +recommended as it ensures that the deployment will always behave the same. It also allows pull-request based reviews +on third-party Helm Charts. + +## How it works + +Helm charts are not directly installed via Helm. Instead, kluctl renders the Helm Chart into a single file and then +hands over the rendered yaml to [kustomize](https://kustomize.io/). Rendering is done in combination with a provided +`helm-values.yaml`, which contains the necessary values to configure the Helm Chart. + +The resulting rendered yaml is then referred by your `kustomization.yaml`, from which point on the +[kustomize integration](./kustomize.md) +takes over. This means, that you can perform all desired customization (patches, namespace override, ...) as if you +provided your own resources via yaml files. + +### Helm hooks + +[Helm Hooks](https://helm.sh/docs/topics/charts_hooks/) are implemented by mapping them +to [kluctl hooks](./hooks.md), based on the following mapping table: + +| Helm hook | kluctl hook | +|---------------|---------------------| +| pre-install | pre-deploy-initial | +| post-install | post-deploy-initial | +| pre-delete | Not supported | +| post-delete | Not supported | +| pre-upgrade | pre-deploy-upgrade | +| post-upgrade | post-deploy-upgrade | +| pre-rollback | Not supported | +| post-rollback | Not supported | +| test | Not supported | + +Please note that this is a best effort approach and not 100% compatible to how Helm would run hooks. + +## helm-chart.yaml + +The `helm-chart.yaml` defines where to get the chart from, which version should be pulled, the rendered output file name, +and a few more Helm options. After this file is added to your project, you need to invoke the `helm-pull` command +to pull the Helm Chart into your local project. It is advised to put the pulled Helm Chart into version control, so +that deployments will always be based on the exact same Chart (Helm does not guarantee this when pulling). + +Example `helm-chart.yaml`: + +```yaml +helmChart: + repo: https://charts.bitnami.com/bitnami + chartName: redis + chartVersion: 12.1.1 + updateConstraints: ~12.1.0 + skipUpdate: false + skipPrePull: false + releaseName: redis-cache + namespace: "{{ my.jinja2.var }}" + output: helm-rendered.yaml # this is optional +``` + +When running the `helm-pull` command, it will search for all `helm-chart.yaml` files in your project and then pull the +chart from the specified repository with the specified version. The pull chart will then be located in the sub-directory +`charts` below the same directory as the `helm-chart.yaml` + +The same filename that was specified in `output` must then be referred in a `kustomization.yaml` as a normal local +resource. If `output` is omitted, the default value `helm-rendered.yaml` is used and must also be referenced in +`kustomization.yaml`. + +`helmChart` inside `helm-chart.yaml` supports the following fields: + +### repo +The url to the Helm repository where the Helm Chart is located. You can use hub.helm.sh to search for repositories and +charts and then use the repos found there. + +OCI based repositories are also supported, for example: +```yaml +helmChart: + repo: oci://r.myreg.io/mycharts/pepper + chartVersion: 1.2.3 + releaseName: pepper + namespace: pepper +``` + +### path +As alternative to `repo`, you can also specify `path`. The path must point to a local Helm Chart that is relative to the +`helm-chart.yaml`. The local Chart must reside in your Kluctl project. + +When `path` is specified, `repo`, `chartName`, `chartVersion` and `updateContrainsts` are not allowed. + +### chartName +The name of the chart that can be found in the repository. + +### chartVersion +The version of the chart. Must be a valid semantic version. + +### git +Instead of using `repo` for OCI/Helm registries or `path` for local charts, you can also pull Charts from Git repositories. +This is helpful in cases where you don't want to publish a chart to a registry or as page, e.g. because of the overhead or other internal restrictions. +You have to set the `url` as well as the `branch`, `tag` or `commit`. If the chart itself is in a sub directory, you can also specify a `subDir`: + +```yaml +helmChart: + git: + url: https://github.com/mycharts/salt + ref: + branch: main + #tag: v1.0.0 -- branch, tag and commit are mutually exclusive + #commit: 015244630b53eb69d77858e5587641b741e91706 -- branch, tag and commit are mutually exclusive + subDir: charts/path/to/chart + releaseName: salt + namespace: salt +``` +In order to be able to use the `helm-update` command, the branch or tag has to be semantic. If this is not the case, the update is skipped. + +### updateConstraints +Specifies version constraints to be used when running [helm-update](../commands/helm-update.md). See +[Checking Version Constraints](https://github.com/Masterminds/semver#checking-version-constraints) for details on the +supported syntax. + +If omitted, Kluctl will filter out pre-releases by default. Use a `updateConstraints` like `~1.2.3-0` to enable +pre-releases. + +### skipUpdate +If set to `true`, skip this Helm Chart when the [helm-update](../commands/helm-update.md) command is called. +If omitted, defaults to `false`. + +### skipPrePull +If set to `true`, skip pre-pulling of this Helm Chart when running [helm-pull](../commands/helm-pull.md). This will +also enable pulling on-demand when the deployment project is rendered/deployed. + +### releaseName +The name of the Helm Release. + +### namespace +The namespace that this Helm Chart is going to be deployed to. Please note that this should match the namespace +that you're actually deploying the kustomize deployment to. This means, that either `namespace` in `kustomization.yaml` +or `overrideNamespace` in `deployment.yaml` should match the namespace given here. The namespace should also be existing +already at the point in time when the kustomize deployment is deployed. + +### output +This is the file name into which the Helm Chart is rendered into. Your `kustomization.yaml` should include this same +file. The file should not be existing in your project, as it is created on-the-fly while deploying. + +### skipCRDs +If set to `true`, kluctl will pass `--skip-crds` to Helm when rendering the deployment. If set to `false` (which is +the default), kluctl will pass `--include-crds` to Helm. + +## helm-values.yaml +This file should be present when you need to pass custom Helm Value to Helm while rendering the deployment. Please +read the documentation of the used Helm Charts for details on what is supported. + +## Updates to helm-charts +In case a Helm Chart needs to be updated, you can either do this manually by replacing the [chartVersion](#chartversion) +value in `helm-chart.yaml` and the calling the [helm-pull](../commands/helm-pull.md) command or by simply invoking +[helm-update](../commands/helm-update.md) with `--upgrade` and/or `--commit` being set. + +## Private Repositories +It is also possible to use private chart repositories and private OCI registries. There are multiple options to +provide credentials to Kluctl. + +### Use `helm repo add --username xxx --password xxx` before +Kluctl will try to find known repositories that are managed by the Helm CLI and then try to reuse the credentials of +these. The repositories are identified by the URL of the repository, so it doesn't matter what name you used when you +added the repository to Helm. The same method can be used for client certificate based authentication (`--key-file` +in `helm repo add`). + +### Use `helm registry login --username xxx --password xxx` for OCI registries +The same as for `helm repo add` applies here, except that authentication entries are matched by hostname. + + +### Use `docker login` for OCI registries +Kluctl tries to use credentials stored in `$HOME/.docker/config.json` as well, so +[`docker login`](https://docs.docker.com/engine/reference/commandline/login/) will also allow Kluctl to authenticate +against OCI registries. + +### Use the --helm-xxx and --registry-xxx arguments of Kluctl sub-commands +All [commands](../commands/README.md) that interact with Helm Chart repositories and OCI registries support the +[helm arguments](../commands/common-arguments.md#helm-arguments) and [registry arguments](../commands/common-arguments.md#registry-arguments) +to specify authentication per repository and/or OCI registry. + +⚠️DEPRECATION WARNING ⚠️ +Previous versions (prior to v2.22.0) of Kluctl supported managing Helm credentials via `credentialsId` in `helm-chart.yaml`. +This is deprecated now and will be removed in the future. Please switch to hostname/registry-name based authentication +instead. See [helm arguments](../commands/common-arguments.md#helm-arguments) for details. + +### Use environment variables to specify authentication +You can also use environment variables to specify Helm Chart repository authentication. For OCI based registries, see +[OCI authentication](./oci.md#authentication) for details. + +The following environment variables are supported: + +1. `KLUCTL_HELM_HOST`: Specifies the host name of the repository to match before the specified credentials are considered. +2. `KLUCTL_HELM_PATH`: Specifies the path to match before the specified credentials are considered. If omitted, credentials are applied to all matching hosts. Can contain wildcards. +3. `KLUCTL_HELM_USERNAME`: Specifies the username. +4. `KLUCTL_HELM_PASSWORD`: Specifies the password. +5. `KLUCTL_HELM_INSECURE_SKIP_TLS_VERIFY`: If set to `true`, Kluctl will skip TLS verification for matching repositories. +6. `KLUCTL_HELM_PASS_CREDENTIALS_ALL`: If set to `true`, Kluctl will instruct Helm to pass credentials to all domains. See https://helm.sh/docs/helm/helm_repo_add/ for details. +7. `KLUCTL_HELM_CERT_FILE`: Specifies the client certificate to use while connecting to the matching repository. +8. `KLUCTL_HELM_KEY_FILE`: Specifies the client key to use while connecting to the matching repository. +9. `KLUCTL_HELM_CA_FILE`: Specifies CA bundle to use for TLS/https verification. + +Multiple credential sets can be specified by including an index in the environment variable names, e.g. +`KLUCTL_HELM_1_HOST=host.org`, `KLUCTL_HELM_1_USERNAME=my-user` and `KLUCTL_HELM_1_PASSWORD=my-password` will apply +the given credential to all repositories with the host `host.org`, while `KLUCTL_HELM_2_HOST=other.org`, +`KLUCTL_HELM_2_USERNAME=my-other-user` and `KLUCTL_HELM_2_PASSWORD=my-other-password` will apply the other credentials +to the `other.org` repository. + +### Credentials when using the kluctl-controller +In case you want to use the same Kluctl deployment via the [kluctl-controller](../../gitops/README.md), you have to +configure Helm and OCI credentials via [`spec.credentials`](../../gitops/spec/v1beta1/kluctldeployment.md#credentials). + +## Templating + +Both `helm-chart.yaml` and `helm-values.yaml` are rendered by the [templating engine](../templating) before they +are actually used. This means, that you can use all available Jinja2 variables at that point, which can for example be +seen in the above `helm-chart.yaml` example for the namespace. + +There is however one exception that leads to a small limitation. When `helm-pull` reads the `helm-chart.yaml`, it does +NOT render the file via the templating engine. This is because it can not know how to properly render the template as it +does have no information about targets (there are no `-t` arguments set) at that point. + +This exception leads to the limitation that the `helm-chart.yaml` MUST be valid yaml even in case it is not rendered +via the templating engine. This makes using control statements (if/for/...) impossible in this file. It also makes it +a requirement to use quotes around values that contain templates (e.g. the namespace in the above example). + +`helm-values.yaml` is not subject to these limitations as it is only interpreted while deploying. diff --git a/docs/kluctl/deployments/hooks.md b/docs/kluctl/deployments/hooks.md new file mode 100644 index 000000000..39167d212 --- /dev/null +++ b/docs/kluctl/deployments/hooks.md @@ -0,0 +1,57 @@ + + +# Hooks + +Kluctl supports hooks in a similar fashion as known from Helm Charts. Hooks are executed/deployed before and/or after the +actual deployment of a kustomize deployment. + +To mark a resource as a hook, add the `kluctl.io/hook` annotation to a resource. The value of the annotation must be +a comma separated list of hook names. Possible value are described in the next chapter. + +## Hook types + +| Hook Type | Description | +|---|---| +| pre-deploy-initial | Executed right before the initial deployment is performed. | +| post-deploy-initial | Executed right after the initial deployment is performed. | +| pre-deploy-upgrade | Executed right before a non-initial deployment is performed. | +| post-deploy-upgrade | Executed right after a non-initial deployment is performed. | +| pre-deploy | Executed right before any (initial and non-initial) deployment is performed.| +| post-deploy | Executed right after any (initial and non-initial) deployment is performed. | + +A deployment is considered to be an "initial" deployment if none of the resources related to the current kustomize +deployment are found on the cluster at the time of deployment. + +If you need to execute hooks for every deployment, independent of its "initial" state, use +`pre-deploy-initial,pre-deploy` to indicate that it should be executed all the time. + +## Hook deletion + +Hook resources are by default deleted right before creation (if they already existed before). This behavior can be +changed by setting the `kluctl.io/hook-delete-policy` to a comma separated list of the following values: + +| Policy | Description | +|---|---| +| before-hook-creation | The default behavior, which means that the hook resource is deleted right before (re-)creation. | +| hook-succeeded | Delete the hook resource directly after it got "ready" | +| hook-failed | Delete the hook resource when it failed to get "ready" | + +## Hook readiness + +After each deployment/execution of the hooks that belong to a deployment stage (before/after deployment), kluctl +waits for the hook resources to become "ready". Readiness is defined [here](./readiness.md). + +It is possible to disable waiting for hook readiness by setting the annotation `kluctl.io/hook-wait` to "false". + +## Hook Annotations + +More control over hook behavior can be configured using additional annotations as described in [annotations/hooks](./annotations/hooks.md) \ No newline at end of file diff --git a/docs/kluctl/deployments/images.md b/docs/kluctl/deployments/images.md new file mode 100644 index 000000000..bb8d58fa0 --- /dev/null +++ b/docs/kluctl/deployments/images.md @@ -0,0 +1,118 @@ + + +# Container Images + +There are usually 2 different scenarios where Container Images need to be specified: +1. When deploying third party applications like nginx, redis, ... (e.g. via the [Helm integration](./helm.md)).
    + * In this case, image versions/tags rarely change, and if they do, this is an explicit change to the deployment. This + means it's fine to have the image versions/tags directly in the deployment manifests. +2. When deploying your own applications.
    + * In this case, image versions/tags might change very rapidly, sometimes multiple times per hour. Having these + versions/tags directly in the deployment manifests can easily lead to commit spam and hard to manage + multi-environment deployments. + +kluctl offers a better solution for the second case. + +## images.get_image() + +This is solved via a templating function that is available in all templates/resources. The function is part of the global +`images` object and expects the following arguments: + +`images.get_image(image)` + +* image + * The image name/repository. It is looked up the list of fixed images. + +The function will lookup the given image in the list of fixed images and return the last match. + +Example deployment: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-deployment +spec: + template: + spec: + containers: + - name: c1 + image: "{{ images.get_image('registry.gitlab.com/my-group/my-project') }}" +``` + +## Fixed images + +Fixed images can be configured multiple methods: +1. Command line argument `--fixed-image` +2. Command line argument `--fixed-images-file` +3. Target definition +4. Global 'images' variable + +## Command line argument `--fixed-image` + +You can pass fixed images configuration via the `--fixed-image` [argument](../commands/common-arguments.md#image-arguments). +Due to [environment variables support](../commands/environment-variables.md) in the CLI, you can also use the +environment variable `KLUCTL_FIXED_IMAGE_XXX` to configure fixed images. + +The format of the `--fixed-image` argument is `--fixed-image image<:namespace:deployment:container>=result`. The simplest +example is `--fixed-image registry.gitlab.com/my-group/my-project=registry.gitlab.com/my-group/my-project:1.1.2`. + +## Command line argument `--fixed-images-file` + +You can also configure fixed images via a yaml file by using `--fixed-images-file /path/to/fixed-images.yaml`. +file: + +```yaml +images: + - image: registry.gitlab.com/my-group/my-project + resultImage: registry.gitlab.com/my-group/my-project:1.1.2 +``` + +The file must contain a single root list named `images` with each entry having the following form: + +```yaml +images: + - image: + resultImage: + # optional fields + namespace: + deployment: / + container: +``` + +`image` (or `imageRegex`) and `resultImage` are required. All the other fields are optional and allow to specify in detail for which +object the fixed is specified. + +You can also specify a regex for the image name: + +```yaml +images: + - imageRegex: registry\.gitlab\.com/my-group/.* + resultImage: + # optional fields + namespace: + deployment: / + container: +``` + +## Target definition + +The [target](../kluctl-project/targets/README.md#targets) definition can optionally specify an `images` field that can +contain the same fixed images configuration as found in the `--fixed-images-file` file. + +## Global 'images' variable + +You can also define a global variable named `images` via one of the [variable sources](../templating/variable-sources.md). +This variable must be a list of the same format as the images list in the `--fixed-images-file` file. + +This option allows to externalize fixed images configuration, meaning that you can maintain image versions outside +the deployment project, e.g. in another [Git repository](../templating/variable-sources.md#git). diff --git a/docs/kluctl/deployments/kustomize.md b/docs/kluctl/deployments/kustomize.md new file mode 100644 index 000000000..8ac96432b --- /dev/null +++ b/docs/kluctl/deployments/kustomize.md @@ -0,0 +1,26 @@ + + +# Kustomize Integration + +kluctl uses [kustomize](https://kustomize.io/) to render final resources. This means, that the finest/lowest +level in kluctl is represented with kustomize deployments. These kustomize deployments can then perform further +customization, e.g. patching and more. You can also use kustomize to easily generate ConfigMaps or secrets from files. + +Generally, everything is possible via `kustomization.yaml`, is thus possible in kluctl. + +We advise to read the kustomize +[reference](https://kubectl.docs.kubernetes.io/references/kustomize/). You can also look into the official kustomize +[example](https://github.com/kubernetes-sigs/kustomize/tree/master/examples). + +# Using the Kustomize Integration + +Please refer to the [Kustomize Deployment Item](./deployment-yml.md#kustomize-deployments) documentation for details. diff --git a/docs/kluctl/deployments/oci.md b/docs/kluctl/deployments/oci.md new file mode 100644 index 000000000..ed2ee4697 --- /dev/null +++ b/docs/kluctl/deployments/oci.md @@ -0,0 +1,61 @@ + + +# OCI Support +Kluctl provides OCI support in multiple places. See the following sections for details. + +## Helm OCI based registries +Kluctl fully supports [OCI based Helm registries](https://helm.sh/docs/topics/registries/) in +the [Helm integration](./helm.md). + +## OCI includes +Kluctl can include sub-deployments from OCI artifacts via [OCI includes](./deployment-yml.md#oci-includes). + +These artifacts can be pushed via the [kluctl oci push](../commands/oci-push.md) sub-command. + +## Authentication +Private registries are supported as well. To authenticate to these, use one of the following methods. + +### Authenticate via `--registry-xxx` arguments +All [commands](../commands/README.md) that interact with OCI registries support the +[registry arguments](../commands/common-arguments.md#registry-arguments) to specify authentication per OCI registry. + +### Authenticate via `docker login` +Kluctl tries to use credentials stored in `$HOME/.docker/config.json` as well, so +[`docker login`](https://docs.docker.com/engine/reference/commandline/login/) will also allow Kluctl to authenticate +against OCI registries. + +### Use environment variables to specify authentication +You can also use environment variables to specify OCI authentication. + +The following environment variables are supported: + +1. `KLUCTL_REGISTRY_HOST`: Specifies the registry host name to match before the specified credentials are considered. +2. `KLUCTL_REGISTRY_REPOSITORY`: Specifies the repository name to match before the specified credentials are considered. The repository name can contain the organization name, which default to `library` is omitted. Can contain wildcards. +3. `KLUCTL_REGISTRY_USERNAME`: Specifies the username. +4. `KLUCTL_REGISTRY_PASSWORD`: Specifies the password. +5. `KLUCTL_REGISTRY_IDENTITY_TOKEN`: Specifies the identity token used for authentication. +6. `KLUCTL_REGISTRY_TOKEN`: Specifies the bearer token used for authentication. +7. `KLUCTL_REGISTRY_INSECURE_SKIP_TLS_VERIFY`: If set to `true`, Kluctl will skip TLS verification for matching registries. +8. `KLUCTL_REGISTRY_PLAIN_HTTP`: If set to `true`, forces the use of http (no TLS). +9. `KLUCTL_REGISTRY_CERT_FILE`: Specifies the client certificate to use while connecting to the matching repository. +10. `KLUCTL_REGISTRY_KEY_FILE`: Specifies the client key to use while connecting to the matching repository. +11. `KLUCTL_REGISTRY_CA_FILE`: Specifies CA bundle to use for TLS/https verification. + +Multiple credential sets can be specified by including an index in the environment variable names, e.g. +`KLUCTL_REGISTRY_1_HOST=host.org`, `KLUCTL_REGISTRY_1_USERNAME=my-user` and `KLUCTL_REGISTRY_1_PASSWORD=my-password` will apply +the given credential to all registries with the host `host.org`, while `KLUCTL_REGISTRY_2_HOST=other.org`, +`KLUCTL_REGISTRY_2_USERNAME=my-other-user` and `KLUCTL_REGISTRY_2_PASSWORD=my-other-password` will apply the other credentials +to the `other.org` registry. + +### Credentials when using the kluctl-controller +In case you want to use the same Kluctl deployment via the [kluctl-controller](../../gitops/README.md), you have to +configure OCI credentials via [`spec.credentials`](../../gitops/spec/v1beta1/kluctldeployment.md#oci-registry-authentication). diff --git a/docs/kluctl/deployments/readiness.md b/docs/kluctl/deployments/readiness.md new file mode 100644 index 000000000..dc2e9e64d --- /dev/null +++ b/docs/kluctl/deployments/readiness.md @@ -0,0 +1,26 @@ + + +# Readiness + +There are multiple places where kluctl can wait for "readiness" of resources, e.g. for hooks or when `waitReadiness` is +specified on a deployment item. Readiness depends on the resource kind, e.g. for a Job, kluctl would wait until it +finishes successfully. + +## Control via Annotations + +Multiple [annotations](./annotations/README.md) control the behaviour when waiting for readiness of resources. These are +the following annoations: + +- [kluctl.io/wait-readiness in resources](./annotations/all-resources.md#kluctliowait-readiness) +- [kluctl.io/wait-readiness in kustomization.yaml](./annotations/kustomization.md#kluctliowait-readiness) +- [kluctl.io/is-ready](./annotations/all-resources.md#kluctliois-ready) +- [kluctl.io/hook-wait](./annotations/hooks.md#kluctliohook-wait) diff --git a/docs/kluctl/deployments/sops.md b/docs/kluctl/deployments/sops.md new file mode 100644 index 000000000..52398060e --- /dev/null +++ b/docs/kluctl/deployments/sops.md @@ -0,0 +1,70 @@ + + +# SOPS Integration + +Kluctl integrates natively with [SOPS](https://github.com/getsops/sops). Kluctl is able to decrypt all resources +referenced by [Kustomize](./kustomize.md) deployment items (including [simple deployments](./deployment-yml.md#simple-deployments)). +In addition, Kluctl will also decrypt all variable sources of the types [file](../templating/variable-sources.md#file) +and [git](../templating/variable-sources.md#git). + +Kluctl assumes that you have setup sops as usual so that it knows how to decrypt these files. + +## Only encrypting Secrets's data + +To only encrypt the `data` and `stringData` fields of Kubernetes secrets, use a `.sops.yaml` configuration file that +`encrypted_regex` to filter encrypted fields: + +``` +creation_rules: + - path_regex: .*.yaml + encrypted_regex: ^(data|stringData)$ +``` + +## Combining templating and SOPS + +As an alternative, you can split secret values and the resulting Kubernetes resources into two different places and then +use templating to use the secret values wherever needed. Example: + +Write the following content into `secrets/my-secrets.yaml`: + +```yaml +secrets: + mySecret: secret-value +``` + +And encrypt it with SOPS: + +```shell +$ sops -e -i secrets/my-secrets.yaml +``` + +Add this [variables source](../templating/variable-sources.md) to one of your [deployments](./deployment-yml.md): + +```yaml +vars: + - file: secrets/my-secrets.yaml + +deployments: +- ... +``` + +Then, in one of your deployment items define the following `Secret`: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-secret + namespace: default +stringData: + secret: "{{ secrets.mySecret }}" +``` diff --git a/docs/kluctl/deployments/tags.md b/docs/kluctl/deployments/tags.md new file mode 100644 index 000000000..b1aa08afe --- /dev/null +++ b/docs/kluctl/deployments/tags.md @@ -0,0 +1,88 @@ + + +# Tags + +Every kustomize deployment has a set of tags assigned to it. These tags are defined in multiple places, which is +documented in [deployment.yaml](./deployment-yml.md). Look for the `tags` field, which is available in multiple places per +deployment project. + +Tags are useful when only one or more specific kustomize deployments need to be deployed or deleted. + +## Default tags + +[deployment items](./deployment-yml.md#deployments) in deployment projects can have an optional list of tags assigned. + +If this list is completely omitted, one single entry is added by default. This single entry equals to the last element +of the `path` in the `deployments` entry. + +Consider the following example: + +```yaml +deployments: + - path: nginx + - path: some/subdir +``` + +In this example, two kustomize deployments are defined. The first would get the tag `nginx` while the second +would get the tag `subdir`. + +In most cases this heuristic is enough to get proper tags with which you can work. It might however lead to strange +or even conflicting tags (e.g. `subdir` is really a bad tag), in which case you'd have to explicitly set tags. + +## Tag inheritance + +Deployment projects and deployments items inherit the tags of their parents. For example, if a deployment project +has a [tags](./deployment-yml.md#tags-deployment-project) property defined, all `deployments` entries would +inherit all these tags. Also, the sub-deployment projects included via deployment items of type +[include](./deployment-yml.md#includes) inherit the tags of the deployment project. These included sub-deployments also +inherit the [tags](./deployment-yml.md#tags-deployment-item) specified by the deployment item itself. + +Consider the following example `deployment.yaml`: + +```yaml +deployments: + - include: sub-deployment1 + tags: + - tag1 + - tag2 + - include: sub-deployment2 + tags: + - tag3 + - tag4 + - include: subdir/subsub +``` + +Any kustomize deployment found in `sub-deployment1` would now inherit `tag1` and `tag2`. If `sub-deployment1` performs +any further includes, these would also inherit these two tags. Inheriting is additive and recursive. + +The last sub-deployment project in the example is subject to the same default-tags logic as described +in [Default tags](#default-tags), meaning that it will get the default tag `subsub`. + +## Deploying with tag inclusion/exclusion + +Special care needs to be taken when trying to deploy only a specific part of your deployment which requires some base +resources to be deployed as well. + +Imagine a large deployment is able to deploy 10 applications, but you only want to deploy one of them. When using tags +to achieve this, there might be some base resources (e.g. Namespaces) which are needed no matter if everything or just +this single application is deployed. In that case, you'd need to set [alwaysDeploy](./deployment-yml.md#deployments) +to `true`. + +## Deleting with tag inclusion/exclusion + +Also, in most cases, even more special care has to be taken for the same types of resources as decribed before. + +Imagine a kustomize deployment being responsible for namespaces deployments. If you now want to delete everything except +deployments that have the `persistency` tag assigned, the exclusion logic would NOT exclude deletion of the namespace. +This would ultimately lead to everything being deleted, and the exclusion tag having no effect. + +In such a case, you'd need to set [skipDeleteIfTags](./deployment-yml.md#skipdeleteiftags) to `true` as well. + +In most cases, setting `alwaysDeploy` to `true` also requires setting `skipDeleteIfTags` to `true`. diff --git a/docs/kluctl/get-started.md b/docs/kluctl/get-started.md new file mode 100644 index 000000000..3c381e32e --- /dev/null +++ b/docs/kluctl/get-started.md @@ -0,0 +1,131 @@ + + +# Get Started + +This tutorial shows you how to start using kluctl. + +## Before you begin + +A few things must be prepared before you actually begin. + +### Get a Kubernetes cluster + +The first step is of course: You need a kubernetes cluster. It doesn't really matter where this cluster is hosted, if +it's a local (e.g. [kind](https://kind.sigs.k8s.io/docs/user/quick-start/)) cluster, managed cluster, or a self-hosted +cluster, kops or kubespray based, AWS, GCE, Azure, ... and so on. Kluctl +is completely independent of how Kubernetes is deployed and where it is hosted. + +There is however a minimum Kubernetes version that must be met: 1.20.0. This is due to the heavy use of server-side apply +which was not stable enough in older versions of Kubernetes. + +### Prepare your kubeconfig + +Your local kubeconfig should be configured to have access to the target Kubernetes cluster via a dedicated context. The context +name should match with the name that you want to use for the cluster from now on. Let's assume the name is `test.example.com`, +then you'd have to ensure that the kubeconfig context `test.example.com` is correctly pointing and authorized for this +cluster. + +See [Configure Access to Multiple Clusters](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/) for documentation +on how to manage multiple clusters with a single kubeconfig. Depending on the Kubernetes provisioning/deployment tooling +you used, you might also be able to directly export the context into your local kubeconfig. For example, +[kops](https://github.com/kubernetes/kops/blob/master/docs/cli/kops_export.md) is able to export and merge the kubeconfig +for a given cluster. + +## Objectives + +- Checkout one of the example Kluctl projects +- Deploy to your local cluster +- Change something and re-deploy + +## Install Kluctl + +The `kluctl` command-line interface (CLI) is required to perform deployments. Read the [installation instructions](./installation.md) +to figure out how to install it. + +## Use Kluctl with a plain Kustomize deployment + +The simplest way to test out Kluctl is to use an existing Kustomize deployment and just test out the CLI. For example, +try it with the [podtato-head project](https://github.com/podtato-head/podtato-head): + +```shell +$ git clone https://github.com/podtato-head/podtato-head.git +$ cd podtato-head/delivery/kustomize/base +$ kluctl deploy +``` + +Then try to modify something inside the Kustomize deployment and retry the `kluctl deploy` call. + +## Try out the Kluctl examples + +For more advanced examples, check out the Kluctl example projects. +Clone the example project found at https://github.com/kluctl/kluctl-examples + +```shell +$ git clone https://github.com/kluctl/kluctl-examples.git +``` + +## Choose one of the examples + +You can choose whatever example you like from the cloned repository. We will however continue this guide by referring +to the `simple-helm` example found in that repository. Change the current directory: + +```shell +$ cd kluctl-examples/simple-helm +``` + +## Create your local cluster + +Create a local cluster with [kind](https://kind.sigs.k8s.io): + +```shell +$ kind create cluster +``` + +This will update your kubeconfig to contain a context with the name `kind-kind`. By default, all examples will use +the currently active context. + +## Deploy the example + +Now run the following command to deploy the example: + +```shell +$ kluctl deploy -t simple-helm +``` + +Kluctl will perform a diff first and then ask for your confirmation to deploy it. In this case, you should only see +some objects being newly deployed. + +```shell +$ kubectl -n simple-helm get pod +``` + +## Change something and re-deploy + +Now change something inside the deployment project. You could for example add `replicaCount: 2` to `deployment/nginx/helm-values.yml`. +After you have saved your changes, run the deploy command again: + +```shell +$ kluctl deploy -t simple-helm +``` + +This time it should show your modifications in the diff. Confirm that you want to perform the deployment and then verify +it: + +```shell +$ kubectl -n simple-helm get pod +``` + +You should need 2 instances of the nginx POD running now. + +## Where to continue? + +Continue by reading through the [recipes](https://kluctl.io/docs/recipes/) and [tutorials](https://kluctl.io/docs/tutorials/). +Also, consult the [reference documentation](./README.md) for details about specifics. diff --git a/docs/kluctl/installation.md b/docs/kluctl/installation.md new file mode 100644 index 000000000..b57b75fcc --- /dev/null +++ b/docs/kluctl/installation.md @@ -0,0 +1,122 @@ + + +# Installation + +Kluctl is available as a CLI and as a GitOps controller. + +## Installing the CLI + +### Binaries + +The kluctl CLI is available as a binary executable for all major platforms, +the binaries can be downloaded form GitHub +[releases page](https://github.com/kluctl/kluctl/releases). + +### Installation with Homebrew + +With [Homebrew](https://brew.sh) for macOS and Linux: + +```shell +$ brew install kluctl/tap/kluctl +``` + +### Installation with Bash + +With [Bash](https://www.gnu.org/software/bash/) for macOS and Linux: + +```shell +$ curl -s https://kluctl.io/install.sh | bash +``` + +The install script does the following: +* attempts to detect your OS +* downloads and unpacks the release tar file in a temporary directory +* copies the kluctl binary to `/usr/local/bin` +* removes the temporary directory + +### Build from source + +Clone the repository: + +```bash +$ git clone https://github.com/kluctl/kluctl +$ cd kluctl +``` + +Build the `kluctl` binary (requires go >= 1.19): + +```bash +$ make build +``` + +Run the binary: + +```bash +$ ./bin/kluctl -h +``` + + + + + + +### Container images + +A container image with `kluctl` is available on GitHub: + +* `ghcr.io/kluctl/kluctl:` + +## Installing the GitOps Controller + +The controller can be installed via two available options. + +### Using the "install" sub-command + +The [`kluctl controller install`](../kluctl/commands/controller-install.md) command can be used to install the +controller. It will use an embedded version of the Controller Kluctl deployment project +found [here](https://github.com/kluctl/kluctl/tree/main/install/controller). + +### Using a Kluctl deployment + +To manage and install the controller via Kluctl, you can use a Git include in your own deployment: + +```yaml +deployments: + - git: + url: https://github.com/kluctl/kluctl.git + subDir: install/controller + ref: + tag: v2.26.0 +``` + +## Installing the Kluctl Webui + +See [Installing the Kluctl Webui](../webui/installation.md) for details. diff --git a/docs/kluctl/kluctl-libraries/README.md b/docs/kluctl/kluctl-libraries/README.md new file mode 100644 index 000000000..9d2b79f5a --- /dev/null +++ b/docs/kluctl/kluctl-libraries/README.md @@ -0,0 +1,72 @@ + + +# Kluctl Library Projects + +A library project is a Kluctl deployment that is meant to be included by other projects. It can be provided with +configuration either via [args](#args) or via [vars in the include](../deployments/deployment-yml.md#vars-deployment-item). + +Kluctl [deployment projects](../deployments/README.md) can include these library projects via +[local include](../deployments/deployment-yml.md#includes), [Git include](../deployments/deployment-yml.md#git-includes) +or [Oci includes](../deployments/deployment-yml.md#oci-includes). +artifacts. + +The `.kluctl-library.yaml` marks a deployment project as a library project and provides some configuration. + +## Example + +Consider the following root `deployment.yaml` inside your root project: + +```yaml +deployments: + - git: + url: git@github.com:example/example-library.git + args: + arg1: value1 +``` + +And the following `.kluctl-library.yaml` inside the included `example-library` git project: + +```yaml +args: + - name: arg1 + - name: arg2 + default: value2 +``` + +This will include the given git repository and make `args.arg1` and `args.arg2` available via [templating](../templating/README.md). + +## Allowed fields + +### args + +A list of arguments that can or must be passed when including the library project. Each of these arguments is then available +in templating via the global `args` object. + +An example looks like this: +```yaml +args: + - name: environment + - name: enable_debug + default: false + - name: complex_arg + default: + my: + nested1: arg1 + nested2: arg2 +``` + +The meaning and function of these arguements is identical to the [args in .kluctl.yaml](../kluctl-project/README.md#args). + +## Using Kluctl Libraries without .kluctl-library.yaml + +Includes can also be done on projects that do not have a `.kluctl-library.yaml` configuration. In that case, all +currently available variables are passed into the include project, including the `args` from the root deployment project. diff --git a/docs/kluctl/kluctl-project/README.md b/docs/kluctl/kluctl-project/README.md new file mode 100644 index 000000000..5f4fa88fc --- /dev/null +++ b/docs/kluctl/kluctl-project/README.md @@ -0,0 +1,136 @@ + + +# Kluctl Projects + +The `.kluctl.yaml` is the central configuration and entry point for your deployments. It defines which targets are +available to invoke [commands](../commands) on. + +## Example + +An example .kluctl.yaml looks like this: + +```yaml +discriminator: "my-project-{{ target.name }}" + +targets: + # test cluster, dev env + - name: dev + context: dev.example.com + args: + environment: dev + # test cluster, test env + - name: test + context: test.example.com + args: + environment: test + # prod cluster, prod env + - name: prod + context: prod.example.com + args: + environment: prod + +args: + - name: environment +``` + +## Allowed fields + +### discriminator + +Specifies a default discriminator template to be used for targets that don't have +their own discriminator specified. + +See [target discriminator](./targets/#discriminator) for details. + +### targets + +Please check the [targets](./targets) sub-section for details. + +### args + +A list of arguments that can or must be passed to most kluctl operations. Each of these arguments is then available +in templating via the global `args` object. + +An example looks like this: +```yaml +targets: +... + +args: + - name: environment + - name: enable_debug + default: false + - name: complex_arg + default: + my: + nested1: arg1 + nested2: arg2 +``` + +These arguments can then be used in templating, e.g. by using `{{ args.environment }}`. + +When calling kluctl, most of the commands will then require you to specify at least `-a environment=xxx` and optionally +`-a enable_debug=true` + +The following sub chapters describe the fields for argument entries. + +#### name +The name of the argument. + +#### default +If specified, the argument becomes optional and will use the given value as default when not specified. + +The default value can be an arbitrary yaml value, meaning that it can also be a nested dictionary. In that case, passing +args in nested form will only set the nested value. With the above example of `complex_arg`, running: + +``` +kluctl deploy -t my-target -a my.nested1=override` +``` + +will only modify the value below `my.nested1` and keep the value of `my.nested2`. + +### aws +If specified, configures the default AWS configuration to use for +[awsSecretsManager](../templating/variable-sources.md#awssecretsmanager) vars sources and KMS based +[SOPS descryption](../deployments/sops.md). + +Example: + +```yaml +aws: + profile: my-local-aws-profile + serviceAccount: + name: service-account-name + namespace: service-account-namespace +``` + +If any of the environment variables `AWS_PROFILE`, `AWS_ACCESS_KEY_ID`, `AWS_ACCESS_KEY` or `AWS_WEB_IDENTITY_TOKEN_FILE` +is set, Kluctl will ignore this AWS configuration and revert to using the environment variables based credentials. + +#### profile +If specified, Kluctl will use this [AWS config profile](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-using-profiles) +when found locally. If it is not found in your local AWS config, Kluctl will not try to use the specified profile. + +#### serviceAccount +Optionally specifies the name and namespace of a service account to use for [IRSA based authentication](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html). +The specified service accounts needs to have the `eks.amazonaws.com/role-arn` annotation set to an existing IAM role +with a proper trust policy that allows this service account to assume that role. Please read the AWS documentation +for details. + +The service account is only used when [profile](#profile) was not specified or when it is not present locally. +If a service account is specified and accessible (you need proper RBAC access), Kluctl will not try to perform default +AWS config loading. + +## Using Kluctl without .kluctl.yaml + +It's possible to use Kluctl without any `.kluctl.yaml`. In that case, all commands must be used without specifying the +target. diff --git a/docs/kluctl/kluctl-project/targets/README.md b/docs/kluctl/kluctl-project/targets/README.md new file mode 100644 index 000000000..4e7ebd50d --- /dev/null +++ b/docs/kluctl/kluctl-project/targets/README.md @@ -0,0 +1,88 @@ + + +# targets + +Specifies a list of targets for which commands can be invoked. A target puts together environment/target specific +configuration and the target cluster. Multiple targets can exist which target the same cluster but with differing +configuration (via `args`). + +Each value found in the target definition is rendered with a simple Jinja2 context that only contains the target and args. +The rendering process is retried 10 times until it finally succeeds, allowing you to reference +the target itself in complex ways. + +Target entries have the following form: +```yaml +targets: +... + - name: + context: + args: + arg1: + arg2: + ... + images: + - image: my-image + resultImage: my-image:1.2.3 + aws: + profile: my-local-aws-profile + serviceAccount: + name: service-account-name + namespace: service-account-namespace + discriminator: "my-project-{{ target.name }}" +... +``` + +The following fields are allowed per target: + +## name +This field specifies the name of the target. The name must be unique. It is referred in all commands via the +[-t](../../commands/common-arguments.md) option. + +## context +This field specifies the kubectl context of the target cluster. The context must exist in the currently active kubeconfig. +If this field is omitted, Kluctl will always use the currently active context. + +## args +This fields specifies a map of arguments to be passed to the deployment project when it is rendered. Allowed argument names +are configured via [deployment args](../../deployments/deployment-yml.md#args). + +## images +This field specifies a list of fixed images to be used by [`images.get_image(...)`](../../deployments/images.md#imagesget_image). +The format is identical to the [fixed images file](../../deployments/images.md#command-line-argument---fixed-images-file). + +## aws +This field specifies target specific AWS configuration, which overrides what was optionally specified via the +[global AWS configuration](../README.md#aws). + +## discriminator + +Specifies a discriminator which is used to uniquely identify all deployed objects on the cluster. It is added to all +objects as the value of the `kluctl.io/discriminator` label. This label is then later used to identify all objects +belonging to the deployment project and target, so that Kluctl can determine which objects got orphaned and need to +be pruned. The discriminator is also used to identify all objects that need to be deleted when +[kluctl delete](../../commands/delete.md) is called. + +If no discriminator is set for a target, [kluctl prune](../../commands/prune.md) and +[kluctl delete](../../commands/delete.md) are not supported. + +The discriminator can be a [template](../../templating/README.md) which is rendered at project loading time. While +rendering, only the `target` and `args` are available as global variables in the templating context. + +The rendered discriminator should be unique on the target cluster to avoid mis-identification of objects from other +deployments or targets. It's good practice to prefix the discriminator with a project name and at least use the target +name to make it unique. Example discriminator to achieve this: `my-project-name-{{ target.name }}`. + +If a target is meant to be deployed multiple times, e.g. by using external [arguments](../README.md#args), the external +arguments should be taken into account as well. Example: `my-project-name-{{ target.name }}-{{ args.environment_name }}`. + +A [default discriminator](../../kluctl-project/README.md#discriminator) can also be specified which is used whenever +a target has no discriminator configured. diff --git a/docs/kluctl/templating/README.md b/docs/kluctl/templating/README.md new file mode 100644 index 000000000..27f9e193f --- /dev/null +++ b/docs/kluctl/templating/README.md @@ -0,0 +1,65 @@ + + +## Table of Contents + +1. [Predefined Variables](./predefined-variables.md) +2. [Variable Sources](./variable-sources.md) +3. [Filters](./filters.md) +4. [Functions](./functions.md) + +# Templating + +kluctl uses a Jinja2 Templating engine to pre-process/render every involved configuration file and resource before +actually interpreting it. Only files that are explicitly excluded via [.templateignore files](#templateignore) +are not rendered via Jinja2. + +Generally, everything that is possible with Jinja2 is possible in kluctl configuration/resources. Please +read into the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/templates/) to understand what exactly +is possible and how to use it. + +## .templateignore +In some cases it is required to exclude specific files from templating, for example when the contents conflict with +the used template engine (e.g. Go templates conflict with Jinja2 and cause errors). In such cases, you can place +a `.templateignore` beside the excluded files or into a parent folder of it. The contents/format of the `.templateignore` +file is the same as you would use in a `.gitignore` file. + +## Includes and imports +Standard Jinja2 [includes](https://jinja.palletsprojects.com/en/3.1.x/templates/#include) and +[imports](https://jinja.palletsprojects.com/en/3.1.x/templates/#import) can be used in all templates. + +The path given to include/import is searched in the directory of the root template and all it's parent directories up +until the project root. Please note that the search path is not altered in included templates, meaning that it will +always search in the same directories even if an include happens inside a file that was included as well. + +To include/import a file relative to the currently rendered file (which is not necessarily the root template), prefix +the path with `./`, e.g. use `{% include "./my-relative-file.j2" %}"`. + +## Macros + +[Jinja2 macros](https://jinja.palletsprojects.com/en/3.1.x/templates/#macros) are fully supported. When writing +macros that produce yaml resources, you must use the `---` yaml separator in case you want to produce multiple resources +in one go. + +## Why no Go Templating + +kluctl started as a python project and was then migrated to be a Go project. In the python world, Jinja2 is the obvious +choice when it comes to templating. In the Go world, of course Go Templates would be the first choice. + +When the migration to Go was performed, it was a conscious and opinionated decision to stick with Jinja2 templating. +The reason is that I (@codablock) believe that Go Templates are hard to read and write and at the same time quite limited +in their features (without extensive work). It never felt natural to write Go Templates. + +This "feeling" was confirmed by multiple users of kluctl when it started and users described as "relieving" to not +be forced to use Go Templates. + +The above is my personal experience and opinion. I'm still quite open for contributions in regard to Go Templating +support, as long as Jinja2 support is kept. diff --git a/docs/kluctl/templating/filters.md b/docs/kluctl/templating/filters.md new file mode 100644 index 000000000..cedf900b0 --- /dev/null +++ b/docs/kluctl/templating/filters.md @@ -0,0 +1,96 @@ + + +# Filters + +In addition to the [builtin Jinja2 filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-builtin-filters), +kluctl provides a few additional filters: + +### b64encode +Encodes the input value as base64. Example: `{{ "test" | b64encode }}` will result in `dGVzdA==`. + +### b64decode +Decodes an input base64 encoded string. Example `{{ my.source.var | b64decode }}`. + +### from_yaml +Parses a yaml string and returns an object. Please note that json is valid yaml, meaning that you can also use this +filter to parse json. + +### to_yaml +Converts a variable/object into its yaml representation. Please note that in most cases the resulting string will not +be properly indented, which will require you to also use the `indent` filter. Example: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-config +data: + config.yaml: | + {{ my_config | to_yaml | indent(4) }} +``` + +### to_json +Same as `to_yaml`, but with json as output. Please note that json is always valid yaml, meaning that you can also use +`to_json` in yaml files. Consider the following example: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-deployment +spec: + template: + spec: + containers: + - name: c1 + image: my-image + env: {{ my_list_of_env_entries | to_json }} +``` + +This would render json into a yaml file, which is still a valid yaml file. Compare this to how this would have to be +solved with `to_yaml`: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-deployment +spec: + template: + spec: + containers: + - name: c1 + image: my-image + env: + {{ my_list_of_env_entries | to_yaml | indent(10) }} +``` + +The required indention filter is the part that makes this error-prone and hard to maintain. Consider using `to_json` +whenever you can. + +### render +Same as the global [render function](./functions.md#rendertemplate), but deprecated now. `render` being a filter turned out to +not work well with local variables, as these are not accessible in filters. Please only use the global function. + +### sha256(digest_len) +Calculates the sha256 digest of the input string. Example: +``` +{{ "some-string" | sha256 }} +``` + +`digest_len` is an optional parameter that allows to limit the length of the returned hex digest. Example: +``` +{{ "some-string" | sha256(6) }} +``` + +### slugify +Slugify a string based on [python-slugify](https://github.com/un33k/python-slugify). diff --git a/docs/kluctl/templating/functions.md b/docs/kluctl/templating/functions.md new file mode 100644 index 000000000..c7af65827 --- /dev/null +++ b/docs/kluctl/templating/functions.md @@ -0,0 +1,157 @@ + + +# Functions + +In addition to the provided +[builtin global functions](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-global-functions), +kluctl also provides a few global functions: + +### load_template(file) +Loads the given file into memory, renders it with the current Jinja2 context and then returns it as a string. Example: +``` +{% set a=load_template('file.yaml') %} +{{ a }} +``` + +`load_template` uses the same path searching rules as described in [includes/imports](../templating#includes-and-imports). + +Please note that there is a limitation in this (and other) functions in regard to loop variables. You can currently not +use loop variables directly as they are not accessible inside Jinja2 extensions/filters. There is an open issue in +that regard [here](https://github.com/pallets/jinja/issues/1478). For a workaround, perform the same as in +[get_var](#getvarfieldpath-default). + +### load_sha256(file, digest_len) +Loads the given file into memory, renders it and calculates the sha256 hash of the result. + +The filename given to `load_sha256` is treated the same as in `load_template`. Recursive loading/calculating of hashes +is allowed and is solved by replacing `load_sha256` invocations with currently loaded templates with dummy strings. +This also allows to calculate the hash of the currently rendered template, for example: + +``` +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-config-{{ load_sha256("configmap.yaml") }} +data: +``` + +`digest_len` is an optional parameter that allows to limit the length of the returned hex digest. + +### load_base64(file, width) +Loads the given file into memory and returns the base64 representation of the binary data. +The width parameter is optional and causes `load_base64` to wrap the base64 string into a multiline string. + +The filename given to `load_base64` is treated the same as in `load_template`. + +This function is useful if you need to include binary data in your deployment. For example: + +``` +apiVersion: v1 +kind: Secret +metadata: + name: my-secret +data: + binarySecret: "{{ load_base64("secret.bin") }}" +``` + +To use wrapped base64, use: + +``` +apiVersion: v1 +kind: Secret +metadata: + name: my-secret +data: + binarySecret: | + "{{ load_base64("large-secret.bin") | indent(4) }}" +``` + +### get_var(field_path, default) +Convenience method to navigate through the current context variables via a +[JSON Path](https://goessner.net/articles/JsonPath/). Let's assume you currently have these variables defined +(e.g. via [vars](../deployments/deployment-yml.md#vars-deployment-project)): +```yaml +my: + deep: + var: value +``` +Then `{{ get_var('my.deep.var', 'my-default') }}` would return `value`. +When any of the elements inside the field path are non-existent, the given default value is returned instead. + +The `field_path` parameter can also be a list of pathes, which are then tried one after the another, returning the first +result that gives a value that is not None. For example, `{{ get_var(['non.existing.var', my.deep.var'], 'my-default') }}` +would also return `value`. + +Please note that there is a limitation in this (and other) functions in regard to loop variables. You can currently not +use loop variables directly as they are not accessible inside Jinja2 global functions or filters. There is an open issue in +that regard [here](https://github.com/pallets/jinja/issues/1478). For a workaround, assign the loop variable to a local variable: + +``` +{% set list=[{x: "a"}, {x: "b"}, {x: "c"}] %} +{% for e in list %} +{% set e=e %} <-- this is the workaround +{{ get_var('e.x') }} +{% endfor %} +``` + +### merge_dict(d1, d2) +Clones d1 and then recursively merges d2 into it and returns the result. Values inside d2 will override values in d1. + +### update_dict(d1, d2) +Same as `merge_dict`, but merging is performed in-place into d1. + +### raise(msg) +Raises a python exception with the given message. This causes the current command to abort. + +### render(template) +Renders the input string with the current Jinja2 context. Example: +``` +{% set a="{{ my_var }}" %} +{{ render(a) }} +``` + +Please note that there is a limitation in this (and other) functions in regard to loop variables. You can currently not +use loop variables directly as they are not accessible inside Jinja2 global functions or filters. There is an open issue in +that regard [here](https://github.com/pallets/jinja/issues/1478). For a workaround, perform the same as in +[get_var](#getvarfieldpath-default). + +### debug_print(msg) +Prints a line to stderr. + +### time.now() +Returns the current time. The returned object has the following members: + +| member | description | +|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| t.as_timezone(tz) | Converts and returns the time `t` in the given timezone. Example:
    `{{ time.now().as_timezone("Europe/Berlin") }}` | +| t.weekday() | Returns the time's weekday. 0 means Monday and 6 means Sunday. | +| t.hour() | Returns the time's hour from 0-23. | +| t.minute() | Returns the time's minute from 0-59. | +| t.second() | Returns the time's second from 0-59. | +| t.nanosecond() | Returns the time's nanosecond from 0-999999999. | +| t + delta | Adds a delta to `t`. Example: `{{ time.now() + time.second * 10 }}` | +| t - delta | Subtracts a delta from `t`. Example: `{{ time.now() - time.second * 10 }}` | +| t1 < t2
    t1 >= t2
    ... | Time objects can be compared to other time objects. Example:
    `{% if time.now() < time.parse_iso("2022-10-01T10:00") %}...{% endif %}`
    All logical operators are supported. | + +### time.utcnow() +Returns the current time in UTC. +The object has the same members as described in [time.now()](#timenow). + +### time.parse_iso(iso_time_str) +Parse the given string and return a time object. The string must be in ISO time. +The object has the same members as described in [time.now()](#timenow). + +### time.second, time.minute, time.hour +Represents a time delta to be used with `t + delta` and `t - delta`. Example +``` +{{ time.now() + time.minute * 10 }} +``` diff --git a/docs/kluctl/templating/predefined-variables.md b/docs/kluctl/templating/predefined-variables.md new file mode 100644 index 000000000..bb1fe4d53 --- /dev/null +++ b/docs/kluctl/templating/predefined-variables.md @@ -0,0 +1,25 @@ + + +# Predefined Variables + +There are multiple variables available which are pre-defined by kluctl. These are: + +### args +This is a dictionary of arguments given via command line. It contains every argument defined in +[deployment args](../deployments/deployment-yml.md#args). + +### target +This is the target definition of the currently processed target. It contains all values found in the +[target definition](../kluctl-project/targets), for example `target.name`. + +### images +This global object provides the dynamic images features described in [images](../deployments/images.md). diff --git a/docs/kluctl/templating/variable-sources.md b/docs/kluctl/templating/variable-sources.md new file mode 100644 index 000000000..7a77645fb --- /dev/null +++ b/docs/kluctl/templating/variable-sources.md @@ -0,0 +1,579 @@ + + +# Variable Sources + +There are multiple places in deployment projects (deployment.yaml) where additional variables can be loaded into +future Jinja2 contexts. + +The first place where vars can be specified is the deployment root, as documented [here](../deployments/deployment-yml.md#vars-deployment-project). +These vars are visible for all deployments inside the deployment project, including sub-deployments from includes. + +The second place to specify variables is in the deployment items, as documented [here](../deployments/deployment-yml.md#vars-deployment-item). + +The variables loaded for each entry in `vars` are not available inside the `deployment.yaml` file itself. +However, each entry in `vars` can use all variables defined before that specific entry is processed. Consider the +following example. + +```yaml +vars: +- file: vars1.yaml +- file: vars2.yaml +- file: optional-vars.yaml + ignoreMissing: true +- file: default-vars.yaml + noOverride: true +- file: vars3.yaml + when: some.var == "value" +- file: vars3.yaml + sensitive: true +- file: vars4.yaml + targetPath: my.target.path +``` + +`vars2.yaml` can now use variables that are defined in `vars1.yaml`. A special case is the use of previously defined +variables inside [values](#values) vars sources. Please see the documentation of [values](#values) for details. + +At all times, variables defined by +parents of the current sub-deployment project can be used in the current vars source. + +The following properties can be set on all variable sources: + +##### ignoreMissing +Each variable source can have the optional field `ignoreMissing` set to `true`, causing Kluctl to ignore if the source +can not be found. + +##### noOverride +When specifying `noOverride: true`, Kluctl will not override variables from the previously loaded variables. This is +useful if you want to load default values for variables. + +##### when +Variables can also be loaded conditionally by specifying a condition via `when: `. The condition must be in +the same format as described in [conditional deployment items](../deployments/deployment-yml.md#when) + +##### sensitive +Specifying `sensitive: true` causes the Webui to redact the underlying variables for non-admin users. This will be set +to `true` by default for all variable sources that usually load sensitive data, including sops encrypted files and +Kubernetes secrets. + +##### targetPath +Specifies a [JSON path](https://goessner.net/articles/JsonPath/) to be used as the target path in the new templating +context. + +Only simple pathes are supported that do not contain wildcards or lists. + +For some variable sources, `targetPath` will become mandatory when the resulting variable is not a dictionary. + +## Variable source types +Different types of vars entries are possible: + +### file +This loads variables from a yaml file. Assume the following yaml file with the name `vars1.yaml`: +```yaml +my_vars: + a: 1 + b: "b" + c: + - l1 + - l2 +``` + +This file can be loaded via: + +```yaml +vars: + - file: vars1.yaml +``` + +After which all included deployments and sub-deployments can use the jinja2 variables from `vars1.yaml`. + +Kluctl also supports variable files encrypted with [SOPS](https://github.com/getsops/sops). See the +[sops integration](../deployments/sops.md) integration for more details. + +### values +An inline definition of variables. Example: + +```yaml +vars: + - values: + a: 1 + b: c +``` + +These variables can then be used in all deployments and sub-deployments. + +In case you need to use variables defined in previous vars sources, the `values` var source needs some special handling +in regard to templating. It's important to understand that the deployment project is rendered BEFORE any vars source +processing is performed, which means that it will fail to render when you use previously defined variables in a `values` +vars source. To still use previously defined variables, surround the `values` vars source with `{% raw %}` and `{% endraw %}`. +In addition, the template expressions must be wrapped with `"`, as otherwise the loading of the deployment project +will fail shortly after rendering due to YAML parsing errors. + +```yaml +vars: + - values: + a: 1 + b: c +{% raw %} + - values: + c: "{{ a }}" +{% endraw %} +``` + +An alternative syntax is to use a template expression that itself outputs a template expression: + +```yaml +vars: + - values: + a: 1 + b: c + - values: + c: {{ '{{ a }}' }} +``` + +The advantage of the second method is that the type (number) of `a` is preserved, while the first method would convert +it into a string. + +### git +This loads variables from a file inside a git repository. Example: + +```yaml +vars: + - git: + url: ssh://git@github.com/example/repo.git + ref: + branch: my-branch + path: path/to/vars.yaml +``` + +The ref field has the same format at found in [Git includes](../deployments/deployment-yml.md#git-includes) + +Kluctl also supports variable files encrypted with [SOPS](https://github.com/getsops/sops). See the +[sops integration](../deployments/sops.md) integration for more details. + +### gitFiles +This loads multiple branches/tags and its contents from a git repository. The branches/tags can be filtered via regex +and the files to load can be filtered via globs. Files can also be parsed and interpreted as yaml. Providing +[`targrtPath`](#targetpath) is mandatory for this variables source. + +Example: + +```yaml +vars: + - gitFiles: + url: ssh://git@github.com/example/repo.git + ref: + branch: preview-env-.* + files: + - glob: preview-info.yaml + parseYaml: true + targetPath: previewEnvs +``` + +The following fields are supported for `gitFiles`. + +##### url +Specified the Git url. + +##### ref +Specifies the ref to match. The ref field has the same format at found in [Git includes](../deployments/deployment-yml.md#git-includes), +with the addition that branches and tags can specify regular expressions. + +##### files +Specifies a list of file filters. Each entry can have the following fields: + +| field | required | description | +|--------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| glob | yes | Specifies the globbing pattern to test files against. `/` must be used as separator, even on Windows. | +| render | no | If set to `true`, Kluctl will render the content of matching files with the current context (excluding the currently loaded `gitFiles`. | +| parseYaml | no | If set to `true`, Kluctl will parse and interpret the content of matching files as YAML.
    The result is stored in the `parsed` field of the resulting file dict.
    Parsing happend after rendering (if `render: true` is used). | +| yamlMultiDoc | no | If set to `true`, Kluctl will treat the content of matching files as multi-document YAML file. | + + +#### gitFiles result +The above example will put the result into the variable `previewEnvs`. The result is a list of matching branches/tags with each entry +having the following form: + +```yaml +previewEnvs: +- ref: + branch: preview-env-1 + refStr: refs/heads/preview-env-1 + files: + - path: preview-info.yaml + size: 1234 + content: | + some: + arbitrary: + yamlContent: 42 + parsed: + some: + arbitrary: + yamlContent: 42 + # this is a copy of the original `gitFiles.files` entry that caused this match + file: + glob: preview-info.yaml + parseYaml: true + # this is a flat dict with each entry being a copy of what is found in `files` for that same entry + # it is indexed by the relative path of each file + filesByPath: + preview-info.yaml: + path: preview-info.yaml + content: ... + dir1/sub-dir/file.yaml: + path: dir1/sub-dir/file.yaml + content: ... + # this is a nested dict that follows the directory structure + filesTree: + preview-info.yaml: + path: preview-info.yaml + content: ... + dir1: + sub-dir: + file.yaml: + path: dir1/sub-dir/file.yaml + content: ... +- ref: + branch: preview-env-2 + ... +``` + +Each file entry, as found in `files`, `filesByPath` and `filesTree` has the following fields: + +| field | description | +|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| file | This is a copy of the `files` entry from `gitFiles` that caused the match. | +| path | The relative path inside the git repository. | +| size | The size of the file. If the file is encrypted, this specifies the size of the unencrypted content. | +| content | The content of the file. If the original file is encrypted, the `content` will contain the unencrypted content. If `render: true` was specified, the `content` will be the rendered content. | +| parsed | If `parsed: true` was specified, this field will contain the parsed content of the file. | + + +### clusterConfigMap +Loads a configmap from the target's cluster and loads the specified key's value into the templating context. The value +is treated and loaded as YAML and thus can either be a simple value or a complex nested structure. In case of a simple +value (e.g. a number), you must also specify `targetPath`. + +The referred ConfigMap must already exist while the Kluctl project is loaded, meaning that it is not possible to use +a ConfigMap that is deployed as part of the Kluctl project itself. + +Assume the following ConfigMap to be already deployed to the target cluster: +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-vars + namespace: my-namespace +data: + vars: | + a: 1 + b: "b" + c: + - l1 + - l2 +``` + +This ConfigMap can be loaded via: + +```yaml +vars: + - clusterConfigMap: + name: my-vars + namespace: my-namespace + key: vars +``` + +The following example uses a simple value: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-vars + namespace: my-namespace +data: + value: 123 +``` + +This ConfigMap can be loaded via: + +```yaml +vars: + - clusterConfigMap: + name: my-vars + namespace: my-namespace + key: value + targetPath: deep.nested.path +``` + +### clusterSecret +Same as clusterConfigMap, but for secrets. + +### clusterObject +Retrieves an arbitrary Kubernetes object from the target's cluster and loads the specified content under `path` into the +templating context. The content can either be interpreted as is or interpreted and loaded as yaml text. In both cases, +rendering with the current context (without the newly introduced variables) can also be enabled. + +`targetPath` must also be specified to configure under which sub-keys the new variables should be loaded. + +The referred Kubernetes object must already exist while the Kluctl project is loaded, meaning that it is not possible to use +an object that is deployed as part of the Kluctl project itself. The exception to this is when you use `ignoreMissing: true` +and properly handle the missing case inside your templating (an example can be found further down). + +Objects can either be referred to by `name` or by `labels`. In case of `labels`, Kluctl assumes that only a single object +matches. If multiple object are expected to match, `list: true` must also be passed, in which case the result loaded +into `targetPath` will be a list of objects instead of a single object. + +Assume the following object to be already deployed to the target cluster: +```yaml +apiVersion: some.group/v1 +kind: SomeObject +metadata: + name: my-object + namespace: my-namespace +spec: + ... +status: + my-status: all-good +``` + +This object can be loaded via: + +```yaml +vars: + - clusterObject: + kind: SomeObject + name: my-object + namespace: my-namespace + path: status + targetPath: my.custom.object.status +``` + +The following properties are supported for clusterObject sources: + +##### kind (required) +The object kind. Kluctl will try to find the matching Kubernetes resource for this kind, which might either be a native +API resource or a custom resource. If multiple resources match, `apiVersion` must also be specified. + +##### apiVersion (optional) +The apiVersion of the object. This field is only required if `kind` is not enough to identify the underlying API resource. + +##### namespace (required) +The namespace from which to load the object. + +##### name (optional) +The name of the object. If specified, the object with the given name must exist (`ignoreMissing: true` can override this). + +Can be omitted when `labels` is specified. + +##### labels (optional) +Specifies one or multiple labels to match. If specified, `name` is not allowed. + +By default, assumes and requires (unless `ignoreMissing: true` is set) that only one object matches. If multiple objects +are assumed to match, set `list: true` as well, in which case the result will be a list as well. + +##### list (optional) +If set to `true`, the result will be a list with one or more elements. + +##### path (required) +Specifies a [JSON path](https://goessner.net/articles/JsonPath/) to be used to load a sub-key from the matching object(s). +Use `$` to load the whole object. To load a single field, use something like `status.my.field`. To load a whole +sub-dict/sub-object or sub-list, use something like `status.conditions`. + +The specified JSON path is only allowed to result in a single match. + +##### render (optional) +If set to `true`, Kluctl will render the resulting object(s) with the current templating context (excluding the newly +loaded variables). Rendering happens on the values of individual fields of the resulting object(s). When `parseYaml: true` +is specified as well, rendering happens before parsing the YAML string. + +##### parseYaml (optional) +Instructs Kluctl to treat the value found at `path` as a YAML string. The value must be of type string. Kluctl will parse +the string as YAML and use the resulting YAML value (which can be a simple int/float/bool or a complex list/dict) as the +result and store it in `targetPath`. When `render: true` is specified as well, the YAML string is rendered before parsing +happens. + +### http +The http variables source allows to load variables from an arbitrary HTTP resource by performing a GET (or any other +configured HTTP method) on the URL. Example: + +```yaml +vars: + - http: + url: https://example.com/path/to/my/vars +``` + +The above source will load a variables file from the given URL. The file is expected to be in yaml or json format. + +The following additional properties are supported for http sources: + +##### method +Specifies the HTTP method to be used when requesting the given resource. Defaults to `GET`. + +##### body +The body to send along with the request. If not specified, nothing is sent. + +#### headers +A map of key/values pairs representing the header entries to be added to the request. If not specified, nothing is added. + +##### jsonPath +Can be used to select a nested element from the yaml/json document returned by the HTTP request. This is useful in case +some REST api is used which does not directly return the variables file. Example: + +```yaml +vars: + - http: + url: https://example.com/path/to/my/vars + jsonPath: $[0].data +``` + +The above example would successfully use the following json document as variables source: + +```json +[{"data": {"vars": {"var1": "value1"}}}] +``` + +#### Authentication + +Kluctl currently supports BASIC and NTLM authentication. It will prompt for credentials when needed. + +### awsSecretsManager +[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) integration. Loads a variables YAML from an AWS Secrets +Manager secret. The secret can either be specified via an ARN or via a secretName and region combination. An existing AWS +config profile can also be specified. + +The secrets stored in AWS Secrets manager must contain a valid yaml or json file. + +Example using an ARN: +```yaml +vars: + - awsSecretsManager: + secretName: arn:aws:secretsmanager:eu-central-1:12345678:secret:secret-name-XYZ + profile: my-prod-profile +``` + +Example using a secret name and region: +```yaml +vars: + - awsSecretsManager: + secretName: secret-name + region: eu-central-1 + profile: my-prod-profile +``` + +The advantage of the latter is that the auto-generated suffix in the ARN (which might not be known at the time of +writing the configuration) doesn't have to be specified. + +### gcpSecretManager +[Google Secret Manager](https://cloud.google.com/secret-manager) integration. Loads a variables YAML from a Google Secrets +Manager secret. The secret name should be specified in `projects/*/secrets/*/versions/*` [format](https://cloud.google.com/secret-manager/docs/reference/rest/v1/projects.secrets.versions/get#path-parameters). + +The secrets stored in Google Secrets manager must contain a valid yaml or json file. + +Example: +```yaml +vars: + - gcpSecretManager: + secretName: "projects/my-project/secrets/secret/versions/latest" +``` + +It is recommended to use [workload identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) when you are using kluctl controller. You will need to annotate kluctl controller service account with service account name created in your google project: + +``` + args: + controller_service_account_annotations: + iam.gke.io/gcp-service-account: kluctl-controller@PROJECT-NAME.iam.gserviceaccount.com +``` +substitute PROJECT-NAME with your real project name in google. Service account in your google project should have role `roles/secretmanager.secretAccessor` to access secrets. + +To run kluctl locally with gcpSecretManager enabled refer to [setting local development environment](https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev) article. + +### azureKeyVault +[Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault/) integration. +Loads a variables YAML from an Azure Key Vault. + +Example +```yaml +vars: + - azureKeyVault: + vaultUri: "https://example.vault.azure.net/" + secretName: kluctl +``` + +SDK [azure-sdk-for-go](https://github.com/Azure/azure-sdk-for-go) supports `az login` +or Environment Variables +```bash +$ export AZURE_CLIENT_ID="__CLIENT_ID__" +$ export AZURE_CLIENT_SECRET="__CLIENT_SECRET__" +$ export AZURE_TENANT_ID="__TENANT_ID__" +$ export AZURE_SUBSCRIPTION_ID="__SUBSCRIPTION_ID__" +``` + +### vault + +[Vault by HashiCorp](https://www.vaultproject.io/) with [Tokens](https://www.vaultproject.io/docs/concepts/tokens) +authentication integration. The address and the path to the secret can be configured. +The implementation was tested with KV Secrets Engine. + +Example using vault: +```yaml +vars: + - vault: + address: http://localhost:8200 + path: secret/data/simple +``` + +Before deploying please make sure that you have access to vault. You can do this for example by setting +the environment variable `VAULT_TOKEN`. + +### systemEnvVars +Load variables from environment variables. Children of `systemEnvVars` can be arbitrary yaml, e.g. dictionaries or lists. +The leaf values are used to get a value from the system environment. + +Example: +```yaml +vars: +- systemEnvVars: + var1: ENV_VAR_NAME1 + someDict: + var2: ENV_VAR_NAME2 + someList: + - var3: ENV_VAR_NAME3 +``` + +The above example will make 3 variables available: `var1`, `someDict.var2` and +`someList[0].var3`, each having the values of the environment variables specified by the leaf values. + +All specified environment variables must be set before calling kluctl unless a default value is set. Default values +can be set by using the `ENV_VAR_NAME:default-value` form. + +Example: +```yaml +vars: +- systemEnvVars: + var1: ENV_VAR_NAME4:defaultValue +``` + +The above example will set the variable `var1` to `defaultValue` in case ENV_VAR_NAME4 is not set. + +All values retrieved from environment variables (or specified as default values) will be treated as YAML, meaning that +integers and booleans will be treated as integers/booleans. If you want to enforce strings, encapsulate the values in +quotes. + +Example: +```yaml +vars: +- systemEnvVars: + var1: ENV_VAR_NAME5:'true' +``` + +The above example will treat `true` as a string instead of a boolean. When the environment variable is set outside +kluctl, it should also contain the quotes. Please note that your shell might require escaping to properly pass quotes. diff --git a/docs/webui/README.md b/docs/webui/README.md new file mode 100644 index 000000000..c66b671da --- /dev/null +++ b/docs/webui/README.md @@ -0,0 +1,29 @@ + + +# Kluctl Webui + +The Kluctl Webui is a powerful UI which allows you to monitor and control your Kluctl GitOps deployments. + +You can [run it locally](./running-locally.md) or [install](./installation.md) it to your Kubernetes cluster. + +## State of the Webui + +Please note that the Kluctl Webui is still in early stage of development, missing many planned features. It might +also contain bugs and be unstable in some situations. If you encounter any such problems, please report these +to https://github.com/kluctl/kluctl/issues. + +## Screenshots + +### Targets Overview +![targets.png](https://kluctl.io/images/webui/targets.png) + +### Command Result +![targets-result.png](https://kluctl.io/images/webui/targets-result.png) diff --git a/docs/webui/installation.md b/docs/webui/installation.md new file mode 100644 index 000000000..8c5c0085a --- /dev/null +++ b/docs/webui/installation.md @@ -0,0 +1,103 @@ + + +# Installation + +The Kluctl Webui can be installed by using a [Git Include](../kluctl/deployments/deployment-yml.md#git-includes) that refers +to the [webui deployment project](https://github.com/kluctl/kluctl/tree/main/install/webui). Example: + +```yaml +deployments: + - git: + url: https://github.com/kluctl/kluctl.git + subDir: install/webui + ref: + tag: v2.26.0 +``` + +## Login + +### Static Users + +By default, the Webui will automatically generate an static credentials for an admin and for a viewer user. These +credentials can be extracted from the `kluctl-system/webui-secret` Secret after the Webui has started up for the first +time. To get the admin password, invoke: + +```shell +$ kubectl -n kluctl-system get secret webui-secret -o jsonpath='{.data.admin-password}' | base64 -d +``` + +For the viewer password, invoke: + +```shell +$ kubectl -n kluctl-system get secret webui-secret -o jsonpath='{.data.viewer-password}' | base64 -d +``` + +If you do not want to rely on the Webui to generate those secrets, simply use your typical means of creating/updating +the `webui-secret` Secret. The secret must contain values for `admin-password`, `viewer-password`. + +### OIDC Integration + +The Webui offers an OIDC integration, which can be configured via [CLI arguments](#passing-arguments). + +For an example of an OIDC provider configurations, see [Azure AD Integration](./oidc-azure-ad.md). + +## Customization + +### Serving under a different path + +By default, the webui is served under the `/`path. To change the path, pass the `--prefix-path` argument to the webui: + +```yaml +deployments: + - git: + url: https://github.com/kluctl/kluctl.git + subDir: install/webui + ref: + tag: v2.26.0 + vars: + - values: + webui_args: + - --path-prefix=/my-custom-prefix +``` + +### Overriding the version + +The image version of the Webui can be overriden with the `kluctl_version` arg: + +```yaml +deployments: + - git: + url: https://github.com/kluctl/kluctl.git + subDir: install/webui + ref: + tag: main + vars: + - values: + args: + kluctl_version: v2.26.0 +``` + +### Passing arguments + +You can pass arbitrary command line arguments to the webui by providing the `webui_args` arg: + +```yaml +deployments: + - git: + url: https://github.com/kluctl/kluctl.git + subDir: install/webui + ref: + tag: v2.26.0 + vars: + - values: + webui_args: + - --gops-agent +``` diff --git a/docs/webui/oidc-azure-ad.md b/docs/webui/oidc-azure-ad.md new file mode 100644 index 000000000..ecad76f2b --- /dev/null +++ b/docs/webui/oidc-azure-ad.md @@ -0,0 +1,90 @@ + + +# Azure AD Integration + +Azure AD can be integrated via the [OIDC integration](./installation.md#oidc-integration). + +## Configure a new Azure AD App registration +### Add a new Azure AD App registration + +1. From the `Azure Active Directory` > `App registrations` menu, choose `+ New registration` +2. Enter a `Name` for the application (e.g. `Kluctl Webui`). +3. Specify who can use the application (e.g. `Accounts in this organizational directory only`). +4. Enter Redirect URI (optional) as follows (replacing `my-kluctl-webui-url` with your Kluctl Webui URL), then choose `Add`. + - **Platform:** `Web` + - **Redirect URI:** https://``/auth/callback +5. When registration finishes, the Azure portal displays the app registration's Overview pane. You see the Application (client) ID. + ![Azure App registration's Overview](https://kluctl.io/images/webui/azure-app-registration-overview.png "Azure App registration's Overview") + +### Add credentials a new Azure AD App registration + +1. From the `Certificates & secrets` menu, choose `+ New client secret` +2. Enter a `Name` for the secret (e.g. `Kluctl Webui SSO`). + - Make sure to copy and save generated value. This is a value for the `oidc-client-secret`. + ![Azure App registration's Secret](https://kluctl.io/images/webui/azure-app-registration-secret.png "Azure App registration's Secret") + +### Setup permissions for Azure AD Application + +1. From the `API permissions` menu, choose `+ Add a permission` +2. Find `User.Read` permission (under `Microsoft Graph`) and grant it to the created application: + ![Azure AD API permissions](https://kluctl.io/images/webui/azure-api-permissions.png "Azure AD API permissions") +3. From the `Token Configuration` menu, choose `+ Add groups claim` + ![Azure AD token configuration](https://kluctl.io/images/webui/azure-token-configuration.png "Azure AD token configuration") + +## Associate an Azure AD group to your Azure AD App registration + +1. From the `Azure Active Directory` > `Enterprise applications` menu, search the App that you created (e.g. `Kluctl Webui`). + - An Enterprise application with the same name of the Azure AD App registration is created when you add a new Azure AD App registration. +2. From the `Users and groups` menu of the app, add any users or groups requiring access to the service. + ![Azure Enterprise SAML Users](https://kluctl.io/images/webui/azure-enterprise-users.png "Azure Enterprise SAML Users") + +## Configure the Kluctl Webui to use the new Azure AD App registration + +Use the following configuration when [installing](./installation.md) the Webui. Replace occurrences of +``, ``, `` and `` with the appropriate values from +above. + +```yaml +deployments: + - path: secrets + - git: + url: https://github.com/kluctl/kluctl.git + subDir: install/webui + ref: + tag: v2.26.0 + vars: + - values: + args: + webui_args: + - --auth-oidc-issuer-url=https://login.microsoftonline.com//v2.0 + - --auth-oidc-client-id= + - --auth-oidc-scope=openid + - --auth-oidc-scope=profile + - --auth-oidc-scope=email + - --auth-oidc-redirect-url=https:///auth/callback + - --auth-oidc-group-claim=groups + - --auth-oidc-admins-group= +``` + +Also, add `webui-secrets.yaml` inside the `secrets` subdirectory: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: webui-secret + namespace: kluctl-system +stringData: + oidc-client-secret: "" +``` + +Please note that the client secret is sensitive data and should not be added unencrypted to you git repository. +Consider encrypting it via [SOPS](../kluctl/deployments/sops.md). diff --git a/docs/webui/running-locally.md b/docs/webui/running-locally.md new file mode 100644 index 000000000..b4c21bd47 --- /dev/null +++ b/docs/webui/running-locally.md @@ -0,0 +1,23 @@ + + +# Running locally + +The Kluctl Webui can be run locally by simply invoking [`kluctl webui run`](../kluctl/commands/webui-run.md). +It will by default connect to your local Kubeconfig Context and expose the Webui on `localhost`. It will also open +the browser for you. + +## Multiple Clusters + +The Webui can already handle multiple clusters. Simply pass `--context ` multiple times to `kluctl webui run`. +This will cause the Webui to listen for status updates on all passed clusters. + +As noted in [State of the Webui](./README.md#state-of-the-webui), the Webui is still in early stage and thus currently +lacks sorting and filtering for clusters. This will be implemented in future releases. diff --git a/e2e/args_test.go b/e2e/args_test.go new file mode 100644 index 000000000..1c117bdc7 --- /dev/null +++ b/e2e/args_test.go @@ -0,0 +1,259 @@ +package e2e + +import ( + "fmt" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "os" + "testing" +) + +func TestArgs(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + args := []any{ + map[string]any{ + "name": "a", + }, + map[string]any{ + "name": "b", + "default": "default", + }, + map[string]any{ + "name": "d", + "default": map[string]any{ + "nested": "default", + }, + }, + map[string]any{ + "name": "e", + "default": 42, + }, + } + + p.UpdateKluctlYaml(func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(args, "args") + return nil + }) + + addConfigMapDeployment(p, "cm", map[string]string{ + "a": `{{ args.a | default("na") }}`, + "b": `{{ args.b | default("na") }}`, + "c": `{{ args.c | default("na") }}`, + "d": "{{ args.d | to_json }}", + "e": "{{ args.e + 1 }}", + }, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-aa=a") + cm := k.MustGetCoreV1(t, "configmaps", p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, "a", "data", "a") + assertNestedFieldEquals(t, cm, "default", "data", "b") + assertNestedFieldEquals(t, cm, "na", "data", "c") + assertNestedFieldEquals(t, cm, `{"nested": "default"}`, "data", "d") + assertNestedFieldEquals(t, cm, `43`, "data", "e") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-aa=a", "-ab=b") + cm = k.MustGetCoreV1(t, "configmaps", p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, "a", "data", "a") + assertNestedFieldEquals(t, cm, "b", "data", "b") + assertNestedFieldEquals(t, cm, "na", "data", "c") + assertNestedFieldEquals(t, cm, `{"nested": "default"}`, "data", "d") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-aa=a", "-ab=b", "-ac=c") + cm = k.MustGetCoreV1(t, "configmaps", p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, "a", "data", "a") + assertNestedFieldEquals(t, cm, "b", "data", "b") + assertNestedFieldEquals(t, cm, "c", "data", "c") + assertNestedFieldEquals(t, cm, `{"nested": "default"}`, "data", "d") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-aa=a", "-ab=b", "-ac=c", "-ad.nested=d") + cm = k.MustGetCoreV1(t, "configmaps", p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, `{"nested": "d"}`, "data", "d") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-aa=a", "-ab=b", "-ac=c", `-ad={"nested": "d2"}`) + cm = k.MustGetCoreV1(t, "configmaps", p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, `{"nested": "d2"}`, "data", "d") + + tmpFile, err := os.CreateTemp("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpFile.Name()) + _, _ = tmpFile.WriteString(` +nested: + nested2: d3 +`) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-aa=a", "-ab=b", "-ac=c", fmt.Sprintf(`-ad=@%s`, tmpFile.Name())) + cm = k.MustGetCoreV1(t, "configmaps", p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, `{"nested": {"nested2": "d3"}}`, "data", "d") + + _ = tmpFile.Truncate(0) + _, _ = tmpFile.Seek(0, 0) + _, _ = tmpFile.WriteString(` +a: a2 +c: c2 +d: + nested: + nested2: d4 +`) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", fmt.Sprintf(`--args-from-file=%s`, tmpFile.Name())) + cm = k.MustGetCoreV1(t, "configmaps", p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, "a2", "data", "a") + assertNestedFieldEquals(t, cm, "default", "data", "b") + assertNestedFieldEquals(t, cm, "c2", "data", "c") + assertNestedFieldEquals(t, cm, `{"nested": {"nested2": "d4"}}`, "data", "d") +} + +func TestArgsFromEnv(t *testing.T) { + k := defaultCluster1 + + p := test_project.NewTestProject(t, test_project.WithUseProcess(true)) + p.SetEnv("KLUCTL_ARG", "a=a") + p.SetEnv("KLUCTL_ARG_1", "b=b") + p.SetEnv("KLUCTL_ARG_2", `c={"nested":{"nested2":"c"}}`) + p.SetEnv("KLUCTL_ARG_3", "d=true") + p.SetEnv("KLUCTL_ARG_4", "e='true'") + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + addConfigMapDeployment(p, "cm", map[string]string{ + "a": `{{ args.a }}`, + "b": `{{ args.b }}`, + "c": `{{ args.c | to_json }}`, + "d": `{{ args.d }}`, + "e": `{{ args.e }}`, + }, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm := k.MustGetCoreV1(t, "configmaps", p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, "a", "data", "a") + assertNestedFieldEquals(t, cm, "b", "data", "b") + assertNestedFieldEquals(t, cm, `{"nested": {"nested2": "c"}}`, "data", "c") + assertNestedFieldEquals(t, cm, "True", "data", "d") + assertNestedFieldEquals(t, cm, "true", "data", "e") +} + +func TestArgsFromEnvAndCli(t *testing.T) { + k := defaultCluster1 + + p := test_project.NewTestProject(t, test_project.WithUseProcess(true)) + p.SetEnv("KLUCTL_ARG_1", "a=a") + p.SetEnv("KLUCTL_ARG_2", "c=c") + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + addConfigMapDeployment(p, "cm", map[string]string{ + "a": `{{ args.a }}`, + "b": `{{ args.b }}`, + "c": `{{ args.c }}`, + }, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-a", "b=b") + cm := k.MustGetCoreV1(t, "configmaps", p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, "a", "data", "a") + assertNestedFieldEquals(t, cm, "b", "data", "b") + assertNestedFieldEquals(t, cm, "c", "data", "c") + + // make sure the CLI overrides values from env + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-a", "b=b", "-a", "c=c2") + cm = k.MustGetCoreV1(t, "configmaps", p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, "a", "data", "a") + assertNestedFieldEquals(t, cm, "b", "data", "b") + assertNestedFieldEquals(t, cm, "c2", "data", "c") +} + +func testArgsInDiscriminator(t *testing.T, inDefaultDiscriminator bool) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + if !inDefaultDiscriminator { + _ = target.SetNestedField("discriminator-{{ args.a }}", "discriminator") + } + }) + + args := []any{ + map[string]any{ + "name": "a", + "default": "default", + }, + } + + p.UpdateKluctlYaml(func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(args, "args") + if inDefaultDiscriminator { + _ = o.SetNestedField("discriminator-{{ args.a }}", "discriminator") + } else { + _ = o.RemoveNestedField("discriminator") + } + return nil + }) + + addConfigMapDeployment(p, "cm", nil, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm := assertConfigMapExists(t, k, p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, "discriminator-default", "metadata", "labels", "kluctl.io/discriminator") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-aa=a") + cm = assertConfigMapExists(t, k, p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, "discriminator-a", "metadata", "labels", "kluctl.io/discriminator") + + if inDefaultDiscriminator { + // now without targets + p.UpdateKluctlYaml(func(o *uo.UnstructuredObject) error { + _ = o.RemoveNestedField("targets") + return nil + }) + + p.KluctlMust(t, "deploy", "--yes") + cm = assertConfigMapExists(t, k, p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, "discriminator-default", "metadata", "labels", "kluctl.io/discriminator") + + p.KluctlMust(t, "deploy", "--yes", "-aa=a") + cm = assertConfigMapExists(t, k, p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, "discriminator-a", "metadata", "labels", "kluctl.io/discriminator") + } +} + +func TestArgsInDefaultDiscriminator(t *testing.T) { + testArgsInDiscriminator(t, true) +} + +func TestArgsInTargetDiscriminator(t *testing.T) { + testArgsInDiscriminator(t, false) +} diff --git a/e2e/contexts_test.go b/e2e/contexts_test.go index b55c5ab6d..b93ce968c 100644 --- a/e2e/contexts_test.go +++ b/e2e/contexts_test.go @@ -1,128 +1,124 @@ package e2e import ( + "github.com/kluctl/kluctl/v2/e2e/test_project" "github.com/kluctl/kluctl/v2/pkg/utils/uo" - "k8s.io/client-go/tools/clientcmd/api" - "sync" "testing" ) -func prepareContextTest(t *testing.T, name string) *testProject { - p := &testProject{} - p.init(t, defaultKindCluster1, name) - p.mergeKubeconfig(defaultKindCluster2) - - var wg sync.WaitGroup - wg.Add(2) - go func() { - defer wg.Done() - recreateNamespace(t, defaultKindCluster1, p.projectName) - }() - go func() { - defer wg.Done() - recreateNamespace(t, defaultKindCluster2, p.projectName) - }() - wg.Wait() +func prepareContextTest(t *testing.T) *test_project.TestProject { + p := test_project.NewTestProject(t) + + createNamespace(t, defaultCluster1, p.TestSlug()) + createNamespace(t, defaultCluster2, p.TestSlug()) addConfigMapDeployment(p, "cm", nil, resourceOpts{ name: "cm", - namespace: p.projectName, + namespace: p.TestSlug(), }) return p } func TestContextCurrent(t *testing.T) { - t.Parallel() - - p := prepareContextTest(t, "context-current") - defer p.cleanup() + p := prepareContextTest(t) - p.updateTarget("test1", func(target *uo.UnstructuredObject) { + p.UpdateTarget("test1", func(target *uo.UnstructuredObject) { // no context set, assume the current one is used }) - p.KluctlMust("deploy", "--yes", "-t", "test1") - assertResourceExists(t, defaultKindCluster1, p.projectName, "ConfigMap/cm") - assertResourceNotExists(t, defaultKindCluster2, p.projectName, "ConfigMap/cm") + p.KluctlMust(t, "deploy", "--yes", "-t", "test1") + assertConfigMapExists(t, defaultCluster1, p.TestSlug(), "cm") + assertConfigMapNotExists(t, defaultCluster2, p.TestSlug(), "cm") - p.updateMergedKubeconfig(func(config *api.Config) { - config.CurrentContext = defaultKindCluster2.Context - }) + setMergedKubeconfigContext(t, defaultCluster2.Context) - p.KluctlMust("deploy", "--yes", "-t", "test1") - assertResourceExists(t, defaultKindCluster2, p.projectName, "ConfigMap/cm") + p.KluctlMust(t, "deploy", "--yes", "-t", "test1") + assertConfigMapExists(t, defaultCluster2, p.TestSlug(), "cm") } func TestContext1(t *testing.T) { t.Parallel() - p := prepareContextTest(t, "context-1") - defer p.cleanup() + p := prepareContextTest(t) - p.updateTarget("test1", func(target *uo.UnstructuredObject) { - _ = target.SetNestedField(defaultKindCluster1.Context, "context") + p.UpdateTarget("test1", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField(defaultCluster1.Context, "context") }) - p.KluctlMust("deploy", "--yes", "-t", "test1") - assertResourceExists(t, defaultKindCluster1, p.projectName, "ConfigMap/cm") - assertResourceNotExists(t, defaultKindCluster2, p.projectName, "ConfigMap/cm") + p.KluctlMust(t, "deploy", "--yes", "-t", "test1") + assertConfigMapExists(t, defaultCluster1, p.TestSlug(), "cm") + assertConfigMapNotExists(t, defaultCluster2, p.TestSlug(), "cm") } func TestContext2(t *testing.T) { t.Parallel() - p := prepareContextTest(t, "context-2") - defer p.cleanup() + p := prepareContextTest(t) - p.updateTarget("test1", func(target *uo.UnstructuredObject) { - _ = target.SetNestedField(defaultKindCluster2.Context, "context") + p.UpdateTarget("test1", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField(defaultCluster2.Context, "context") }) - p.KluctlMust("deploy", "--yes", "-t", "test1") - assertResourceExists(t, defaultKindCluster2, p.projectName, "ConfigMap/cm") - assertResourceNotExists(t, defaultKindCluster1, p.projectName, "ConfigMap/cm") + p.KluctlMust(t, "deploy", "--yes", "-t", "test1") + assertConfigMapExists(t, defaultCluster2, p.TestSlug(), "cm") + assertConfigMapNotExists(t, defaultCluster1, p.TestSlug(), "cm") } func TestContext1And2(t *testing.T) { t.Parallel() - p := prepareContextTest(t, "context-1-and-2") - defer p.cleanup() + p := prepareContextTest(t) - p.updateTarget("test1", func(target *uo.UnstructuredObject) { - _ = target.SetNestedField(defaultKindCluster1.Context, "context") + p.UpdateTarget("test1", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField(defaultCluster1.Context, "context") }) - p.updateTarget("test2", func(target *uo.UnstructuredObject) { - _ = target.SetNestedField(defaultKindCluster2.Context, "context") + p.UpdateTarget("test2", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField(defaultCluster2.Context, "context") }) - p.KluctlMust("deploy", "--yes", "-t", "test1") - assertResourceExists(t, defaultKindCluster1, p.projectName, "ConfigMap/cm") - assertResourceNotExists(t, defaultKindCluster2, p.projectName, "ConfigMap/cm") + p.KluctlMust(t, "deploy", "--yes", "-t", "test1") + assertConfigMapExists(t, defaultCluster1, p.TestSlug(), "cm") + assertConfigMapNotExists(t, defaultCluster2, p.TestSlug(), "cm") - p.KluctlMust("deploy", "--yes", "-t", "test2") - assertResourceExists(t, defaultKindCluster2, p.projectName, "ConfigMap/cm") + p.KluctlMust(t, "deploy", "--yes", "-t", "test2") + assertConfigMapExists(t, defaultCluster2, p.TestSlug(), "cm") } func TestContextSwitch(t *testing.T) { t.Parallel() - p := prepareContextTest(t, "context-switch") - defer p.cleanup() + p := prepareContextTest(t) + + p.UpdateTarget("test1", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField(defaultCluster1.Context, "context") + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test1") + assertConfigMapExists(t, defaultCluster1, p.TestSlug(), "cm") + assertConfigMapNotExists(t, defaultCluster2, p.TestSlug(), "cm") - p.updateTarget("test1", func(target *uo.UnstructuredObject) { - _ = target.SetNestedField(defaultKindCluster1.Context, "context") + p.UpdateTarget("test1", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField(defaultCluster2.Context, "context") }) - p.KluctlMust("deploy", "--yes", "-t", "test1") - assertResourceExists(t, defaultKindCluster1, p.projectName, "ConfigMap/cm") - assertResourceNotExists(t, defaultKindCluster2, p.projectName, "ConfigMap/cm") + p.KluctlMust(t, "deploy", "--yes", "-t", "test1") + assertConfigMapExists(t, defaultCluster2, p.TestSlug(), "cm") +} + +func TestContextOverride(t *testing.T) { + t.Parallel() + + p := prepareContextTest(t) - p.updateTarget("test1", func(target *uo.UnstructuredObject) { - _ = target.SetNestedField(defaultKindCluster2.Context, "context") + p.UpdateTarget("test1", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField(defaultCluster1.Context, "context") }) - p.KluctlMust("deploy", "--yes", "-t", "test1") - assertResourceExists(t, defaultKindCluster2, p.projectName, "ConfigMap/cm") + p.KluctlMust(t, "deploy", "--yes", "-t", "test1") + assertConfigMapExists(t, defaultCluster1, p.TestSlug(), "cm") + assertConfigMapNotExists(t, defaultCluster2, p.TestSlug(), "cm") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test1", "--context", defaultCluster2.Context) + assertConfigMapExists(t, defaultCluster2, p.TestSlug(), "cm") } diff --git a/e2e/controller_install_test.go b/e2e/controller_install_test.go new file mode 100644 index 000000000..81a187213 --- /dev/null +++ b/e2e/controller_install_test.go @@ -0,0 +1,32 @@ +package e2e + +import ( + "context" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/runtime/schema" + "testing" +) + +func TestControllerInstall(t *testing.T) { + k := createTestCluster(t, "cluster1") + kubeconfig := getKubeconfigTmpFile(t, k.Kubeconfig) + + _, _, err := test_project.KluctlExecute(t, context.TODO(), func(args ...any) { + t.Log(args...) + }, "controller", "install", "--yes", "--kubeconfig", kubeconfig) + assert.NoError(t, err) + + x := assertObjectExists(t, k, schema.GroupVersionResource{ + Group: "apps", + Version: "v1", + Resource: "deployments", + }, "kluctl-system", "kluctl-controller") + assertNestedFieldEquals(t, x, "kluctl.io-controller", "metadata", "labels", "kluctl.io/discriminator") + + stdout, _, err := test_project.KluctlExecute(t, context.TODO(), func(args ...any) { + t.Log(args...) + }, "controller", "install", "--yes", "--kubeconfig", kubeconfig, "--kluctl-version", "v2.99999") + assert.NoError(t, err) + assert.Contains(t, stdout, "v2.99999") +} diff --git a/e2e/crds_test.go b/e2e/crds_test.go new file mode 100644 index 000000000..854048fe1 --- /dev/null +++ b/e2e/crds_test.go @@ -0,0 +1,125 @@ +package e2e + +import ( + "fmt" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/e2e/test_resources" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "strings" + "testing" + "time" +) + +func createExampleCR(name string, namespace string) string { + return fmt.Sprintf(` +apiVersion: "stable.example.com/v1" +kind: CronTab +metadata: + name: %s + namespace: %s +spec: + cronSpec: "* * * * */5" + image: my-awesome-cron-image + +`, name, namespace) +} + +func prepareCRDsTest(t *testing.T, k *test_utils.EnvTestCluster, crds bool, barrier bool) *test_project.TestProject { + p := test_project.NewTestProject(t) + p.AddExtraArgs("--kubeconfig", getKubeconfigTmpFile(t, k.Kubeconfig)) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test1", func(target *uo.UnstructuredObject) { + }) + + if crds { + p.AddKustomizeDeployment("crds", []test_project.KustomizeResource{ + {Name: "crds.yaml", Content: test_resources.GetYamlDocs(t, "example-crds.yaml")}, + }, nil) + } + if barrier { + p.AddDeploymentItem(".", uo.FromMap(map[string]interface{}{ + "barrier": true, + })) + } + p.AddKustomizeDeployment("crs", []test_project.KustomizeResource{ + {Name: "crs.yaml", Content: createExampleCR("test", p.TestSlug())}, + }, nil) + + return p +} + +func TestDeployCRDUnordered(t *testing.T) { + t.Parallel() + + for i := 0; i < 100; i++ { + success := func() bool { + k := createTestCluster(t, "cluster1") + defer k.Stop() + + p := prepareCRDsTest(t, k, true, false) + + stdout, _, err := p.Kluctl(t, "deploy", "--yes", "-t", "test1") + if err != nil && strings.Contains(err.Error(), "command failed") && strings.Contains(stdout, `no matches for kind "CronTab" in version`) { + // success + return true + } + return false + }() + if success { + return + } + } + + t.Errorf("could not cause missing CRD error") +} + +func TestDiffCRDSimulated(t *testing.T) { + t.Parallel() + + k := createTestCluster(t, "cluster1") + + p := prepareCRDsTest(t, k, true, true) + + stdout, _ := p.KluctlMust(t, "diff", "-t", "test1") + assert.Contains(t, stdout, fmt.Sprintf("the underyling custom resource definition for %s/CronTab/test has not been applied yet", p.TestSlug())) +} + +func TestDeployCRDBarrier(t *testing.T) { + t.Parallel() + + k := createTestCluster(t, "cluster1") + + p := prepareCRDsTest(t, k, true, true) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test1") +} + +func TestDeployCRDByController(t *testing.T) { + t.Parallel() + + k := createTestCluster(t, "cluster1") + + p := prepareCRDsTest(t, k, false, true) + p.UpdateDeploymentItems(".", func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + barrierItem := items[0] + barrierItem.SetNestedField([]map[string]any{ + { + "kind": "CustomResourceDefinition", + "name": "crontabs.stable.example.com", + }, + }, "waitReadinessObjects") + return items + }) + + // we simulate a controller being spun up that needs a few seconds to get ready and then apply the CRDs + go func() { + time.Sleep(5 * time.Second) + test_resources.ApplyYaml(t, "example-crds.yaml", k) + }() + + p.KluctlMust(t, "deploy", "--yes", "-t", "test1") +} diff --git a/e2e/default_clusters.go b/e2e/default_clusters.go deleted file mode 100644 index 466df5b4f..000000000 --- a/e2e/default_clusters.go +++ /dev/null @@ -1,76 +0,0 @@ -package e2e - -import ( - "fmt" - test_utils "github.com/kluctl/kluctl/v2/internal/test-utils" - "github.com/kluctl/kluctl/v2/pkg/utils" - "os" - "path/filepath" - "strconv" - "sync" -) - -func createDefaultKindCluster(num int) (*test_utils.KindCluster, int) { - kindClusterName := os.Getenv(fmt.Sprintf("KIND_CLUSTER_NAME%d", num)) - kindApiHost := os.Getenv(fmt.Sprintf("KIND_API_HOST%d", num)) - kindApiPort := os.Getenv(fmt.Sprintf("KIND_API_PORT%d", num)) - kindExtraPortsOffset := os.Getenv(fmt.Sprintf("KIND_EXTRA_PORTS_OFFSET%d", num)) - kindKubeconfig := os.Getenv(fmt.Sprintf("KIND_KUBECONFIG%d", num)) - if kindClusterName == "" { - kindClusterName = fmt.Sprintf("kluctl-e2e-%d", num) - } - if kindApiHost == "" { - kindApiHost = "localhost" - } - if kindExtraPortsOffset == "" { - kindExtraPortsOffset = fmt.Sprintf("%d", 30000+num*1000) - } - if kindKubeconfig == "" { - kindKubeconfig = filepath.Join(utils.GetTmpBaseDir(), fmt.Sprintf("kluctl-e2e-kubeconfig-%d.yml", num)) - } - - var err error - var kindApiPortInt, kindExtraPortsOffsetInt int64 - - if kindApiPort != "" { - kindApiPortInt, err = strconv.ParseInt(kindApiPort, 0, 32) - if err != nil { - panic(err) - } - } - kindExtraPortsOffsetInt, err = strconv.ParseInt(kindExtraPortsOffset, 0, 32) - if err != nil { - panic(err) - } - - vaultPort := int(kindExtraPortsOffsetInt) + 0 - - kindExtraPorts := map[int]int{ - vaultPort: 30000, - } - - k, err := test_utils.CreateKindCluster(kindClusterName, kindApiHost, int(kindApiPortInt), kindExtraPorts, kindKubeconfig) - if err != nil { - panic(err) - } - return k, vaultPort -} - -var defaultKindCluster1, defaultKindCluster2 *test_utils.KindCluster -var defaultKindCluster1VaultPort, defaultKindCluster2VaultPort int - -func init() { - var wg sync.WaitGroup - wg.Add(2) - go func() { - defer wg.Done() - defaultKindCluster1, defaultKindCluster1VaultPort = createDefaultKindCluster(1) - deleteTestNamespaces(defaultKindCluster1) - }() - go func() { - defer wg.Done() - defaultKindCluster2, defaultKindCluster2VaultPort = createDefaultKindCluster(2) - deleteTestNamespaces(defaultKindCluster2) - }() - wg.Wait() -} diff --git a/e2e/default_clusters_test.go b/e2e/default_clusters_test.go new file mode 100644 index 000000000..53ad6bff7 --- /dev/null +++ b/e2e/default_clusters_test.go @@ -0,0 +1,127 @@ +package e2e + +import ( + "dario.cat/mergo" + "fmt" + "github.com/kluctl/kluctl/v2/e2e/test-utils" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + "os" + "path/filepath" + "runtime" + "sync" + "testing" +) + +var defaultCluster1 = test_utils.CreateEnvTestCluster("cluster1") +var defaultCluster2 = test_utils.CreateEnvTestCluster("cluster2") +var gitopsCluster = test_utils.CreateEnvTestCluster("gitops") +var mergedKubeconfig string + +func init() { + if isCallKluctl() { + return + } + + var wg sync.WaitGroup + wg.Add(3) + go func() { + defer wg.Done() + err := defaultCluster1.Start() + if err != nil { + panic(err) + } + }() + go func() { + defer wg.Done() + defaultCluster2.InitWebhookCallback(schema.GroupVersionResource{ + Version: "v1", Resource: "configmaps", + }, true) + err := defaultCluster2.Start() + if err != nil { + panic(err) + } + }() + go func() { + defer wg.Done() + gitopsCluster.CRDDirectoryPaths = []string{"../config/crd/bases"} + err := gitopsCluster.Start() + if err != nil { + panic(err) + } + }() + wg.Wait() + + tmpKubeconfig, err := os.CreateTemp("", "") + if err != nil { + panic(err) + } + _ = tmpKubeconfig.Close() + runtime.SetFinalizer(defaultCluster1, func(_ *test_utils.EnvTestCluster) { + _ = os.Remove(tmpKubeconfig.Name()) + }) + + mergeKubeconfig(tmpKubeconfig.Name(), defaultCluster1.Kubeconfig) + mergeKubeconfig(tmpKubeconfig.Name(), defaultCluster2.Kubeconfig) + mergeKubeconfig(tmpKubeconfig.Name(), gitopsCluster.Kubeconfig) + + mergedKubeconfig = tmpKubeconfig.Name() + _ = os.Setenv("KUBECONFIG", mergedKubeconfig) + + _, _ = fmt.Fprintf(os.Stderr, "KUBECONFIG=%s\n", mergedKubeconfig) +} + +func mergeKubeconfig(path string, kubeconfig []byte) { + mkcfg, err := clientcmd.LoadFromFile(path) + if err != nil { + panic(err) + } + + nkcfg, err := clientcmd.Load(kubeconfig) + if err != nil { + panic(err) + } + + err = mergo.Merge(mkcfg, nkcfg) + if err != nil { + panic(err) + } + + err = clientcmd.WriteToFile(*mkcfg, path) + if err != nil { + panic(err) + } +} + +func getKubeconfigTmpFile(t *testing.T, content []byte) string { + fname := filepath.Join(t.TempDir(), "kubeconfig") + err := os.WriteFile(fname, content, 0600) + if err != nil { + t.Fatal(err) + } + return fname +} + +func setKubeconfigString(t *testing.T, content []byte) { + tmpKubeconfig := getKubeconfigTmpFile(t, content) + t.Logf("set KUBECONFIG=%s\n", tmpKubeconfig) + t.Setenv("KUBECONFIG", tmpKubeconfig) +} + +func setKubeconfig(t *testing.T, config api.Config) { + content, err := clientcmd.Write(config) + if err != nil { + t.Fatal(err) + } + setKubeconfigString(t, content) +} + +func setMergedKubeconfigContext(t *testing.T, newContext string) { + kcfg, err := clientcmd.LoadFromFile(mergedKubeconfig) + if err != nil { + t.Fatal(err) + } + kcfg.CurrentContext = newContext + setKubeconfig(t, *kcfg) +} diff --git a/e2e/deploy_test.go b/e2e/deploy_test.go deleted file mode 100644 index f8d451032..000000000 --- a/e2e/deploy_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package e2e - -import ( - "testing" -) - -func TestCommandDeploySimple(t *testing.T) { - t.Parallel() - - k := defaultKindCluster1 - - p := &testProject{} - p.init(t, k, "simple") - defer p.cleanup() - - recreateNamespace(t, k, p.projectName) - - p.updateTarget("test", nil) - - addConfigMapDeployment(p, "cm", nil, resourceOpts{ - name: "cm", - namespace: p.projectName, - }) - p.KluctlMust("deploy", "--yes", "-t", "test") - assertResourceExists(t, k, p.projectName, "ConfigMap/cm") - - addConfigMapDeployment(p, "cm2", nil, resourceOpts{ - name: "cm2", - namespace: p.projectName, - }) - p.KluctlMust("deploy", "--yes", "-t", "test", "--dry-run") - assertResourceNotExists(t, k, p.projectName, "ConfigMap/cm2") - p.KluctlMust("deploy", "--yes", "-t", "test") - assertResourceExists(t, k, p.projectName, "ConfigMap/cm2") -} diff --git a/e2e/deployment_items_test.go b/e2e/deployment_items_test.go new file mode 100644 index 000000000..2fa21b818 --- /dev/null +++ b/e2e/deployment_items_test.go @@ -0,0 +1,289 @@ +package e2e + +import ( + "fmt" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "path/filepath" + "testing" +) + +func TestKustomize(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm", nil, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm") + + addConfigMapDeployment(p, "cm2", nil, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "--dry-run") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm2") + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm2") +} + +func TestGeneratedKustomize(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + p.UpdateDeploymentYaml("", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField([]any{ + map[string]any{ + "path": "generated-kustomize", + }, + }, "deployments") + return nil + }) + p.UpdateYaml("generated-kustomize/cm1.yaml", func(o *uo.UnstructuredObject) error { + *o = *createConfigMapObject(nil, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + return nil + }, "") + p.UpdateYaml("generated-kustomize/cm2.yaml", func(o *uo.UnstructuredObject) error { + *o = *createConfigMapObject(nil, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + return nil + }, "") + p.UpdateYaml("generated-kustomize/cm3._yaml", func(o *uo.UnstructuredObject) error { + *o = *createConfigMapObject(nil, resourceOpts{ + name: "cm3", + namespace: p.TestSlug(), + }) + return nil + }, "") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertConfigMapExists(t, k, p.TestSlug(), "cm2") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm3") +} + +func TestOnlyRender(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "path": "only-render", + "onlyRender": true, + })) + p.UpdateFile("only-render/value.txt", func(f string) (string, error) { + return "{{ args.a }}\n", nil + }, "") + p.UpdateFile("only-render/kustomization.yaml", func(f string) (string, error) { + return fmt.Sprintf(` +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: +- name: %s-cm + files: + - value.txt +`, p.TestSlug()), nil + }, "") + + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "path": "d", + })) + p.UpdateFile("d/kustomization.yaml", func(f string) (string, error) { + return fmt.Sprintf(` +components: +- ../only-render +namespace: %s +`, p.TestSlug()), nil + }, "") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-a", "a=v1") + // it should not appear in the default namespace as that would indicate that the component was treated as a deployment item + assertConfigMapNotExists(t, k, "default", p.TestSlug()+"-cm") + s := assertConfigMapExists(t, k, p.TestSlug(), p.TestSlug()+"-cm") + assert.Equal(t, s.Object["data"], map[string]any{ + "value.txt": "v1", + }) +} + +func TestKustomizeBase(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "base", map[string]string{}, resourceOpts{ + name: "base-cm", + namespace: p.TestSlug(), + }) + p.UpdateDeploymentItems("", func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + _ = items[0].SetNestedField(true, "onlyRender") + return items + }) + + p.AddKustomizeDeployment("k1", []test_project.KustomizeResource{{ + Name: "../base", + }}, nil) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "base-cm") +} + +func TestTemplateIgnore(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm1", map[string]string{ + "k1": `{{ "a" }}`, + }, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + addConfigMapDeployment(p, "cm2", map[string]string{ + "k1": `{{ "a" }}`, + }, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + addConfigMapDeployment(p, "cm3", map[string]string{ + "k1": `{{ "a" }}`, + }, resourceOpts{ + name: "cm3", + namespace: p.TestSlug(), + }) + + // .templateignore outside of deployment item + p.UpdateFile(".templateignore", func(f string) (string, error) { + return `cm2/configmap-cm2.yml`, nil + }, "") + // .templateignore inside of deployment item + p.UpdateFile("cm3/.templateignore", func(f string) (string, error) { + return `/configmap-cm3.yml`, nil + }, "") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm1 := assertConfigMapExists(t, k, p.TestSlug(), "cm1") + cm2 := assertConfigMapExists(t, k, p.TestSlug(), "cm2") + cm3 := assertConfigMapExists(t, k, p.TestSlug(), "cm3") + + assert.Equal(t, map[string]any{ + "k1": "a", + }, cm1.Object["data"]) + assert.Equal(t, map[string]any{ + "k1": `{{ "a" }}`, + }, cm2.Object["data"]) + assert.Equal(t, map[string]any{ + "k1": `{{ "a" }}`, + }, cm3.Object["data"]) +} + +func testLocalIncludes(t *testing.T, projectDir string) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t, test_project.WithBareProject()) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateDeploymentYaml("base", func(o *uo.UnstructuredObject) error { + *o = *uo.FromMap(map[string]interface{}{ + "deployments": []map[string]any{ + {"path": "cm"}, + }, + }) + return nil + }) + p.UpdateYaml("base/cm/cm.yaml", func(o *uo.UnstructuredObject) error { + *o = *createConfigMapObject(map[string]string{ + "d1": "v1", + }, resourceOpts{name: "{{ name }}", namespace: p.TestSlug()}) + return nil + }, "") + + baseDir, _ := filepath.Rel(filepath.Join(p.LocalProjectDir(), projectDir), filepath.Join(p.LocalWorkDir(), "base")) + baseDir = filepath.ToSlash(baseDir) + + p.UpdateDeploymentYaml(projectDir, func(o *uo.UnstructuredObject) error { + *o = *uo.FromMap(map[string]interface{}{ + "deployments": []map[string]any{ + { + "include": baseDir, + "vars": []map[string]any{ + { + "values": map[string]any{ + "name": "cm-inc1", + }, + }, + }, + }, + { + "include": baseDir, + "vars": []map[string]any{ + { + "values": map[string]any{ + "name": "cm-inc2", + }, + }, + }, + }, + }, + }) + return nil + }) + + p.KluctlMust(t, "deploy", "--yes", "--project-dir", filepath.Join(p.LocalProjectDir(), projectDir)) + assertConfigMapExists(t, k, p.TestSlug(), "cm-inc1") + assertConfigMapExists(t, k, p.TestSlug(), "cm-inc2") +} + +func TestIncludeLocalFromRoot(t *testing.T) { + testLocalIncludes(t, ".") +} + +func TestIncludeLocalFromSubdir(t *testing.T) { + testLocalIncludes(t, "foo") +} diff --git a/e2e/diff_name_test.go b/e2e/diff_name_test.go new file mode 100644 index 000000000..f19f3d7f1 --- /dev/null +++ b/e2e/diff_name_test.go @@ -0,0 +1,77 @@ +package e2e + +import ( + test_utils "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/types/k8s" + "github.com/kluctl/kluctl/v2/pkg/types/result" + "github.com/stretchr/testify/assert" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "sort" + "testing" +) + +func TestDiffName(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_utils.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm", map[string]string{ + "d1": "{{ args.v }}", + }, resourceOpts{ + name: "{{ args.cm_name }}", + fname: "cm.yaml", + namespace: p.TestSlug(), + annotations: map[string]string{ + "kluctl.io/diff-name": "cm", + }, + }) + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-a", "cm_name=cm-1", "-a", "v=a") + assertConfigMapExists(t, k, p.TestSlug(), "cm-1") + + r, _ := p.KluctlMustCommandResult(t, "deploy", "--yes", "-t", "test", "-a", "cm_name=cm-2", "-a", "v=b", "-oyaml") + assertConfigMapExists(t, k, p.TestSlug(), "cm-2") + + sort.Slice(r.Objects, func(i, j int) bool { + return r.Objects[i].Ref.String() < r.Objects[j].Ref.String() + }) + + assert.Len(t, r.Objects, 2) + assert.Equal(t, result.BaseObject{ + Ref: k8s.ObjectRef{Version: "v1", Kind: "ConfigMap", Name: "cm", Namespace: p.TestSlug()}, + Changes: []result.Change{ + {Type: "update", JsonPath: "data.d1", OldValue: &v1.JSON{Raw: []byte("\"a\"")}, NewValue: &v1.JSON{Raw: []byte("\"b\"")}, UnifiedDiff: "-a\n+b"}, + {Type: "update", JsonPath: "metadata.name", OldValue: &v1.JSON{Raw: []byte("\"cm-1\"")}, NewValue: &v1.JSON{Raw: []byte("\"cm-2\"")}, UnifiedDiff: "-cm-1\n+cm-2"}}, + }, r.Objects[0].BaseObject) + assert.Equal(t, result.BaseObject{ + Ref: k8s.ObjectRef{Version: "v1", Kind: "ConfigMap", Name: "cm-1", Namespace: p.TestSlug()}, + Orphan: true, + }, r.Objects[1].BaseObject) + + r, _ = p.KluctlMustCommandResult(t, "deploy", "--yes", "-t", "test", "-a", "cm_name=cm-3", "-a", "v=c", "-oyaml") + assertConfigMapExists(t, k, p.TestSlug(), "cm-2") + sort.Slice(r.Objects, func(i, j int) bool { + return r.Objects[i].Ref.String() < r.Objects[j].Ref.String() + }) + + assert.Len(t, r.Objects, 3) + assert.Equal(t, result.BaseObject{ + Ref: k8s.ObjectRef{Version: "v1", Kind: "ConfigMap", Name: "cm", Namespace: p.TestSlug()}, + Changes: []result.Change{ + {Type: "update", JsonPath: "data.d1", OldValue: &v1.JSON{Raw: []byte("\"b\"")}, NewValue: &v1.JSON{Raw: []byte("\"c\"")}, UnifiedDiff: "-b\n+c"}, + {Type: "update", JsonPath: "metadata.name", OldValue: &v1.JSON{Raw: []byte("\"cm-2\"")}, NewValue: &v1.JSON{Raw: []byte("\"cm-3\"")}, UnifiedDiff: "-cm-2\n+cm-3"}}, + }, r.Objects[0].BaseObject) + assert.Equal(t, result.BaseObject{ + Ref: k8s.ObjectRef{Version: "v1", Kind: "ConfigMap", Name: "cm-1", Namespace: p.TestSlug()}, + Orphan: true, + }, r.Objects[1].BaseObject) + assert.Equal(t, result.BaseObject{ + Ref: k8s.ObjectRef{Version: "v1", Kind: "ConfigMap", Name: "cm-2", Namespace: p.TestSlug()}, + Orphan: true, + }, r.Objects[2].BaseObject) +} diff --git a/e2e/discriminator_test.go b/e2e/discriminator_test.go new file mode 100644 index 000000000..e530a8b7e --- /dev/null +++ b/e2e/discriminator_test.go @@ -0,0 +1,108 @@ +package e2e + +import ( + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "testing" +) + +func TestDiscriminator(t *testing.T) { + t.Parallel() + + p := test_project.NewTestProject(t) + k := defaultCluster1 + + addConfigMapDeployment(p, "cm1", nil, resourceOpts{name: "cm1", namespace: p.TestSlug()}) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateKluctlYaml(func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("from-kluctl-yaml-{{ args.a }}", "discriminator") + return nil + }) + + p.KluctlMust(t, "deploy", "--yes", "-a", "a=x") + cm := assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertNestedFieldEquals(t, cm, "from-kluctl-yaml-x", "metadata", "labels", "kluctl.io/discriminator") + + // add a target without a discriminator + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-a", "a=x") + cm = assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertNestedFieldEquals(t, cm, "from-kluctl-yaml-x", "metadata", "labels", "kluctl.io/discriminator") + + // modify target to contain a discriminator + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField("from-target-{{ target.name }}", "discriminator") + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-a", "a=x") + cm = assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertNestedFieldEquals(t, cm, "from-target-test", "metadata", "labels", "kluctl.io/discriminator") +} + +func TestDiscriminatorArgWithoutTarget(t *testing.T) { + t.Parallel() + + p := test_project.NewTestProject(t) + k := defaultCluster1 + + addConfigMapDeployment(p, "cm1", nil, resourceOpts{name: "cm1", namespace: p.TestSlug()}) + addConfigMapDeployment(p, "cm2", nil, resourceOpts{name: "cm2", namespace: p.TestSlug()}) + + createNamespace(t, k, p.TestSlug()) + + p.KluctlMust(t, "deploy", "--yes", "--discriminator", "test-discriminator") + + cm := assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertNestedFieldEquals(t, cm, "test-discriminator", "metadata", "labels", "kluctl.io/discriminator") + cm = assertConfigMapExists(t, k, p.TestSlug(), "cm2") + assertNestedFieldEquals(t, cm, "test-discriminator", "metadata", "labels", "kluctl.io/discriminator") + + p.KluctlMust(t, "deploy", "--yes", "--discriminator", "test-discriminator-x") + cm = assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertNestedFieldEquals(t, cm, "test-discriminator-x", "metadata", "labels", "kluctl.io/discriminator") + + p.DeleteKustomizeDeployment("cm2") + p.KluctlMust(t, "prune", "--yes", "--discriminator", "test-discriminator-x") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm2") +} + +func TestDiscriminatorArgWithTarget(t *testing.T) { + t.Parallel() + + p := test_project.NewTestProject(t) + k := defaultCluster1 + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField("from-target-{{ target.name }}", "discriminator") + }) + + addConfigMapDeployment(p, "cm1", nil, resourceOpts{name: "cm1", namespace: p.TestSlug()}) + addConfigMapDeployment(p, "cm2", nil, resourceOpts{name: "cm2", namespace: p.TestSlug()}) + + createNamespace(t, k, p.TestSlug()) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm := assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertNestedFieldEquals(t, cm, "from-target-test", "metadata", "labels", "kluctl.io/discriminator") + cm = assertConfigMapExists(t, k, p.TestSlug(), "cm2") + assertNestedFieldEquals(t, cm, "from-target-test", "metadata", "labels", "kluctl.io/discriminator") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "--discriminator", "test-discriminator") + + cm = assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertNestedFieldEquals(t, cm, "test-discriminator", "metadata", "labels", "kluctl.io/discriminator") + cm = assertConfigMapExists(t, k, p.TestSlug(), "cm2") + assertNestedFieldEquals(t, cm, "test-discriminator", "metadata", "labels", "kluctl.io/discriminator") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "--discriminator", "test-discriminator-test-x", "-a", "a=x") + cm = assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertNestedFieldEquals(t, cm, "test-discriminator-test-x", "metadata", "labels", "kluctl.io/discriminator") + + p.DeleteKustomizeDeployment("cm3") + p.KluctlMust(t, "prune", "--yes", "-t", "test", "--discriminator", "test-discriminator-test-x") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm3") +} diff --git a/e2e/embedded_python_test.go b/e2e/embedded_python_test.go new file mode 100644 index 000000000..a0cedf7ce --- /dev/null +++ b/e2e/embedded_python_test.go @@ -0,0 +1,67 @@ +package e2e + +import ( + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestEmbeddedPython(t *testing.T) { + cacheDir := t.TempDir() + + p := test_project.NewTestProject(t, test_project.WithCacheDir(cacheDir)) + p.SetEnv("KLUCTL_TEST_JINJA2_CACHE", "1") + + addConfigMapDeployment(p, "cm", map[string]string{ + "k": `{{ "magic" }}{{ 40 + 2 }}`, + }, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + + stdout, _ := p.KluctlProcessMust(t, "render", "--print-all") + assert.Contains(t, stdout, "magic42") + + des, err := os.ReadDir(filepath.Join(cacheDir, "go-embed-jinja2")) + assert.NoError(t, err) + + found := false + for _, de := range des { + if strings.HasPrefix(de.Name(), "kluctl-python-") { + found = true + } + } + assert.Truef(t, found, "extracted python distribution not found") +} + +func TestSystemPython(t *testing.T) { + cacheDir := t.TempDir() + + p := test_project.NewTestProject(t, test_project.WithCacheDir(cacheDir)) + p.SetEnv("KLUCTL_TEST_JINJA2_CACHE", "1") + p.SetEnv("KLUCTL_USE_SYSTEM_PYTHON", "1") + + addConfigMapDeployment(p, "cm", map[string]string{ + "k": `{{ "magic" }}{{ 40 + 2 }}`, + }, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + + stdout, _ := p.KluctlProcessMust(t, "render", "--print-all") + assert.Contains(t, stdout, "magic42") + + des, err := os.ReadDir(filepath.Join(cacheDir, "go-embed-jinja2")) + assert.NoError(t, err) + + found := false + for _, de := range des { + if strings.HasPrefix(de.Name(), "kluctl-python-") { + found = true + } + } + assert.Falsef(t, found, "extracted python distribution found even though we should have used the system distribution") +} diff --git a/e2e/external_projects_test.go b/e2e/external_projects_test.go deleted file mode 100644 index 18dbe95ca..000000000 --- a/e2e/external_projects_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package e2e - -import ( - "fmt" - "github.com/kluctl/kluctl/v2/pkg/utils/uo" - "testing" -) - -func doTestProject(t *testing.T, namespace string, p *testProject) { - k := defaultKindCluster1 - - p.init(t, k, fmt.Sprintf("project-%s", namespace)) - defer p.cleanup() - - recreateNamespace(t, k, namespace) - - p.updateKindCluster(k, uo.FromMap(map[string]interface{}{ - "cluster_var": "cluster_value1", - })) - p.updateTargetDeprecated("test", k.Name, uo.FromMap(map[string]interface{}{ - "target_var": "target_value1", - })) - addConfigMapDeployment(p, "cm1", map[string]string{}, resourceOpts{name: "cm1", namespace: namespace}) - - p.KluctlMust("deploy", "--yes", "-t", "test") - assertResourceExists(t, k, namespace, "ConfigMap/cm1") - - cmData := map[string]string{ - "cluster_var": "{{ cluster.cluster_var }}", - "target_var": "{{ args.target_var }}", - } - - assertResourceNotExists(t, k, namespace, "ConfigMap/cm2") - addConfigMapDeployment(p, "cm2", cmData, resourceOpts{name: "cm2", namespace: namespace}) - p.KluctlMust("deploy", "--yes", "-t", "test") - - o := assertResourceExists(t, k, namespace, "ConfigMap/cm2") - assertNestedFieldEquals(t, o, "cluster_value1", "data", "cluster_var") - assertNestedFieldEquals(t, o, "target_value1", "data", "target_var") - - p.updateKindCluster(k, uo.FromMap(map[string]interface{}{ - "cluster_var": "cluster_value2", - })) - p.KluctlMust("deploy", "--yes", "-t", "test") - o = assertResourceExists(t, k, namespace, "ConfigMap/cm2") - assertNestedFieldEquals(t, o, "cluster_value2", "data", "cluster_var") - assertNestedFieldEquals(t, o, "target_value1", "data", "target_var") - - p.updateTargetDeprecated("test", k.Name, uo.FromMap(map[string]interface{}{ - "target_var": "target_value2", - })) - p.KluctlMust("deploy", "--yes", "-t", "test") - o = assertResourceExists(t, k, namespace, "ConfigMap/cm2") - assertNestedFieldEquals(t, o, "cluster_value2", "data", "cluster_var") - assertNestedFieldEquals(t, o, "target_value2", "data", "target_var") -} - -func TestExternalProjects(t *testing.T) { - testCases := []struct { - name string - p testProject - }{ - {name: "external-kluctl-project", p: testProject{kluctlProjectExternal: true}}, - {name: "external-clusters-project", p: testProject{clustersExternal: true}}, - {name: "external-deployment-project", p: testProject{deploymentExternal: true}}, - {name: "external-sealed-secrets-project", p: testProject{sealedSecretsExternal: true}}, - {name: "external-all-projects", p: testProject{kluctlProjectExternal: true, clustersExternal: true, deploymentExternal: true, sealedSecretsExternal: true}}, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - doTestProject(t, tc.name, &tc.p) - }) - } -} diff --git a/e2e/git_include_test.go b/e2e/git_include_test.go new file mode 100644 index 000000000..9fb8dfa3d --- /dev/null +++ b/e2e/git_include_test.go @@ -0,0 +1,221 @@ +package e2e + +import ( + "fmt" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "testing" +) + +func prepareIncludeProject(t *testing.T, prefix string, subDir string, gitServer *test_utils.TestGitServer) *test_project.TestProject { + p := test_project.NewTestProject(t, + test_project.WithGitSubDir(subDir), + test_project.WithGitServer(gitServer), + test_project.WithRepoName(fmt.Sprintf("repos/%s", prefix)), + ) + addConfigMapDeployment(p, "cm", map[string]string{"a": "v"}, resourceOpts{ + name: fmt.Sprintf("%s-cm", prefix), + namespace: p.TestSlug(), + }) + return p +} + +func prepareGitIncludeTest(t *testing.T, k *test_utils.EnvTestCluster, mainGs *test_utils.TestGitServer, gs1 *test_utils.TestGitServer, gs2 *test_utils.TestGitServer) (*test_project.TestProject, *test_project.TestProject, *test_project.TestProject) { + p := test_project.NewTestProject(t, test_project.WithGitServer(mainGs)) + ip1 := prepareIncludeProject(t, "include1", "", gs1) + ip2 := prepareIncludeProject(t, "include2", "subDir", gs2) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) {}) + + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip1.GitUrl(), + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip2.GitUrl(), + "subDir": "subDir", + }, + })) + + return p, ip1, ip2 +} + +func TestGitInclude(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p, _, _ := prepareGitIncludeTest(t, k, nil, nil, nil) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "include1-cm") + assertConfigMapExists(t, k, p.TestSlug(), "include2-cm") +} + +func createBranchAndTag(t *testing.T, p *test_project.TestProject, branchName string, tagName string, tagMessage string, update func()) string { + err := p.GetGitWorktree().Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName("master"), + }) + assert.NoError(t, err) + err = p.GetGitWorktree().Checkout(&git.CheckoutOptions{ + Create: true, + Branch: plumbing.NewBranchReferenceName(branchName), + }) + assert.NoError(t, err) + + update() + + h, err := p.GetGitRepo().ResolveRevision(plumbing.Revision(branchName)) + assert.NoError(t, err) + + if tagName != "" { + var to *git.CreateTagOptions + if tagMessage != "" { + to = &git.CreateTagOptions{ + Message: tagMessage, + } + } + _, err = p.GetGitRepo().CreateTag(tagName, *h, to) + assert.NoError(t, err) + } + + err = p.GetGitWorktree().Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName("master"), + }) + assert.NoError(t, err) + + return h.String() +} + +func TestGitIncludeRef(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + ip1 := test_project.NewTestProject(t) + + addConfigMapDeployment(p, "cm", map[string]string{"a": "a"}, resourceOpts{ + name: "parent", + namespace: p.TestSlug(), + }) + + createBranchAndTag(t, ip1, "branch1", "tag1", "", func() { + addConfigMapDeployment(ip1, "cm", map[string]string{"a": "branch1"}, resourceOpts{ + name: "branch1", + namespace: p.TestSlug(), + }) + }) + + createBranchAndTag(t, ip1, "branch2", "tag2", "", func() { + addConfigMapDeployment(ip1, "cm", map[string]string{"a": "tag2"}, resourceOpts{ + name: "tag2", + namespace: p.TestSlug(), + }) + }) + + createBranchAndTag(t, ip1, "branch3", "tag3", "", func() { + addConfigMapDeployment(ip1, "cm", map[string]string{"a": "branch3"}, resourceOpts{ + name: "branch3", + namespace: p.TestSlug(), + }) + }) + + createBranchAndTag(t, ip1, "branch4", "tag4", "", func() { + addConfigMapDeployment(ip1, "cm", map[string]string{"a": "tag4"}, resourceOpts{ + name: "tag4", + namespace: p.TestSlug(), + }) + }) + + createBranchAndTag(t, ip1, "branch5", "tag5", "tag5", func() { + addConfigMapDeployment(ip1, "cm", map[string]string{"a": "tag5"}, resourceOpts{ + name: "tag5", + namespace: p.TestSlug(), + }) + }) + + commit6 := createBranchAndTag(t, ip1, "branch6", "tag6", "", func() { + addConfigMapDeployment(ip1, "cm", map[string]string{"a": "tag5"}, resourceOpts{ + name: "commit6", + namespace: p.TestSlug(), + }) + }) + + addConfigMapDeployment(ip1, "cm", map[string]string{"a": "HEAD"}, resourceOpts{ + name: "head", + namespace: p.TestSlug(), + }) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) {}) + + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip1.GitUrl(), + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip1.GitUrl(), + "ref": map[string]any{ + "branch": "branch1", + }, + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip1.GitUrl(), + "ref": map[string]any{ + "tag": "tag2", + }, + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip1.GitUrl(), + "ref": "branch3", + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip1.GitUrl(), + "ref": "tag4", + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip1.GitUrl(), + "ref": map[string]any{ + "tag": "tag5", + }, + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip1.GitUrl(), + "ref": map[string]any{ + "commit": commit6, + }, + }, + })) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "parent") + assertConfigMapExists(t, k, p.TestSlug(), "head") + assertConfigMapExists(t, k, p.TestSlug(), "branch1") + assertConfigMapExists(t, k, p.TestSlug(), "tag2") + assertConfigMapExists(t, k, p.TestSlug(), "branch3") + assertConfigMapExists(t, k, p.TestSlug(), "tag4") + assertConfigMapExists(t, k, p.TestSlug(), "tag5") + assertConfigMapExists(t, k, p.TestSlug(), "commit6") +} diff --git a/e2e/git_info_test.go b/e2e/git_info_test.go new file mode 100644 index 000000000..9889241a0 --- /dev/null +++ b/e2e/git_info_test.go @@ -0,0 +1,119 @@ +package e2e + +import ( + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + gittypes "github.com/kluctl/kluctl/lib/git/types" + "github.com/kluctl/kluctl/v2/e2e/test_project" + copy2 "github.com/otiai10/copy" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func copyProject(t *testing.T, p *test_project.TestProject) string { + tmp := t.TempDir() + err := copy2.Copy(p.LocalWorkDir(), tmp, copy2.Options{ + Skip: func(srcinfo os.FileInfo, src, dest string) (bool, error) { + if srcinfo.Name() == ".git" { + return true, nil + } + return false, nil + }, + }) + assert.NoError(t, err) + + return tmp +} + +func prepareGitRepoTest(t *testing.T) (*test_project.TestProject, string) { + k := defaultCluster1 + + p := test_project.NewTestProject(t) + createNamespace(t, k, p.TestSlug()) + + addConfigMapDeployment(p, "cm", nil, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + + tmp := copyProject(t, p) + + return p, tmp +} + +func TestNGitNoRepo(t *testing.T) { + t.Parallel() + + p, tmp := prepareGitRepoTest(t) + + // no repo at all + r, _ := p.KluctlMustCommandResult(t, "deploy", "--project-dir", tmp, "--yes", "-oyaml") + assert.Equal(t, gittypes.GitInfo{}, r.GitInfo) +} + +func TestGitNoCommit(t *testing.T) { + t.Parallel() + + p, tmp := prepareGitRepoTest(t) + + // empty repo (no commit) + _, err := git.PlainInit(tmp, false) + assert.NoError(t, err) + r, _ := p.KluctlMustCommandResult(t, "deploy", "--project-dir", tmp, "--yes", "-oyaml") + assert.Equal(t, gittypes.GitInfo{ + Dirty: true, + }, r.GitInfo) +} + +func TestGitSingleCommit(t *testing.T) { + t.Parallel() + + p, tmp := prepareGitRepoTest(t) + + // empty repo (no commit) + gr, err := git.PlainInit(tmp, false) + assert.NoError(t, err) + + cfg, err := gr.Config() + assert.NoError(t, err) + cfg.User.Name = "Test User" + cfg.User.Email = "no@mail.com" + cfg.Author = cfg.User + cfg.Committer = cfg.User + err = gr.SetConfig(cfg) + assert.NoError(t, err) + + w, err := gr.Worktree() + assert.NoError(t, err) + + _, err = w.Add(".") + assert.NoError(t, err) + c, err := w.Commit("initial", &git.CommitOptions{}) + assert.NoError(t, err) + + r, _ := p.KluctlMustCommandResult(t, "deploy", "--project-dir", tmp, "--yes", "-oyaml") + assert.Equal(t, gittypes.GitInfo{ + Ref: &gittypes.GitRef{ + Branch: "master", + }, + Commit: c.String(), + Dirty: false, + }, r.GitInfo) + + _, err = gr.CreateRemote(&config.RemoteConfig{ + Name: "origin", + URLs: []string{"https://example.com"}, + }) + assert.NoError(t, err) + + r, _ = p.KluctlMustCommandResult(t, "deploy", "--project-dir", tmp, "--yes", "-oyaml") + assert.Equal(t, gittypes.GitInfo{ + Url: gittypes.ParseGitUrlMust("https://example.com"), + Ref: &gittypes.GitRef{ + Branch: "master", + }, + Commit: c.String(), + Dirty: false, + }, r.GitInfo) +} diff --git a/e2e/gitops_approval_test.go b/e2e/gitops_approval_test.go new file mode 100644 index 000000000..89371049a --- /dev/null +++ b/e2e/gitops_approval_test.go @@ -0,0 +1,59 @@ +package e2e + +import ( + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/stretchr/testify/suite" + "testing" +) + +type GitOpsApprovalTestSuite struct { + GitopsTestSuite +} + +func TestGitOpsApproval(t *testing.T) { + t.Parallel() + suite.Run(t, new(GitOpsApprovalTestSuite)) +} + +func (suite *GitOpsApprovalTestSuite) TestGitOpsManualDeployment() { + p := test_project.NewTestProject(suite.T()) + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("target1", nil) + addConfigMapDeployment(p, "d1", nil, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + + key := suite.createKluctlDeployment(p, "target1", nil) + + suite.Run("initial deployment", func() { + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + }) + + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Manual = true + }) + suite.waitForReconcile(key) + + addConfigMapDeployment(p, "d2", nil, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + + suite.Run("manual deployment not triggered", func() { + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + assertConfigMapNotExists(suite.T(), suite.k, p.TestSlug(), "cm2") + }) + + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.ManualObjectsHash = &kd.Status.LastObjectsHash + }) + + suite.Run("manual deployment triggered", func() { + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm2") + }) +} diff --git a/e2e/gitops_errors_test.go b/e2e/gitops_errors_test.go new file mode 100644 index 000000000..e8bb0615a --- /dev/null +++ b/e2e/gitops_errors_test.go @@ -0,0 +1,239 @@ +package e2e + +import ( + gittypes "github.com/kluctl/kluctl/lib/git/types" + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + test_utils "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/types/k8s" + "github.com/kluctl/kluctl/v2/pkg/types/result" + "github.com/kluctl/kluctl/v2/pkg/utils" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/suite" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" +) + +type GitOpsErrorsSuite struct { + GitopsTestSuite +} + +func TestGitOpsErrors(t *testing.T) { + t.Parallel() + suite.Run(t, new(GitOpsErrorsSuite)) +} + +func (suite *GitOpsErrorsSuite) assertErrors(kd *kluctlv1.KluctlDeployment, rstatus metav1.ConditionStatus, rreason string, rmessage string, prepareError string, expectedErrors []result.DeploymentError, expectedWarnings []result.DeploymentError) { + g := NewWithT(suite.T()) + + g.Expect(kd.Status.LastDeployResult).ToNot(BeNil()) + + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + + g.Expect(readinessCondition.Status).To(Equal(rstatus)) + g.Expect(readinessCondition.Reason).To(Equal(rreason)) + g.Expect(readinessCondition.Message).To(ContainSubstring(rmessage)) + + g.Expect(kd.Status.LastPrepareError, prepareError) + + lastDeployResult, err := kd.Status.GetLastDeployResult() + g.Expect(err).To(Succeed()) + + g.Expect(err).To(Succeed()) + if len(expectedErrors) != 0 || len(expectedWarnings) != 0 { + cr := suite.getCommandResult(lastDeployResult.Id) + g.Expect(cr).ToNot(BeNil()) + g.Expect(err).To(Succeed()) + g.Expect(cr.Errors).To(ConsistOf(expectedErrors)) + g.Expect(cr.Warnings).To(ConsistOf(expectedWarnings)) + } + + g.Expect(lastDeployResult.Errors).To(ConsistOf(expectedErrors)) + g.Expect(lastDeployResult.Warnings).To(ConsistOf(expectedWarnings)) +} + +func (suite *GitOpsErrorsSuite) TestGitOpsErrors() { + g := NewWithT(suite.T()) + _ = g + + p := test_utils.NewTestProject(suite.T()) + createNamespace(suite.T(), suite.k, p.TestSlug()) + + goodCm1 := `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 + namespace: "{{ args.namespace }}" +data: + k1: v1 +` + badCm1_1 := `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 + namespace: "{{ args.namespace }}" +data_error: + k1: v1 +` + badCm1_2 := `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 + namespace: "{{ args.namespace }}" +data: + k1: { +` + + p.UpdateTarget("target1", nil) + p.AddKustomizeDeployment("d1", []test_utils.KustomizeResource{ + {Name: "cm1.yaml", Content: uo.FromStringMust(goodCm1)}, + }, nil) + + key := suite.createKluctlDeployment(p, "target1", map[string]any{ + "namespace": p.TestSlug(), + }) + + suite.Run("initial deployment", func() { + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + }) + + cm1Ref := k8s.NewObjectRef("", "v1", "ConfigMap", "cm1", p.TestSlug()) + + suite.Run("cm1 causes error while applying", func() { + p.UpdateFile("d1/cm1.yaml", func(f string) (string, error) { + return badCm1_1, nil + }, "") + kd := suite.waitForReconcile(key) + suite.assertErrors(kd, metav1.ConditionFalse, kluctlv1.DeployFailedReason, "deploy failed with 1 errors", "", []result.DeploymentError{ + { + Ref: cm1Ref, + Message: "failed to patch git-ops-errors-git-ops-errors/ConfigMap/cm1: failed to create typed patch object (git-ops-errors-git-ops-errors/cm1; /v1, Kind=ConfigMap): .data_error: field not declared in schema", + }, + }, nil) + p.UpdateFile("d1/cm1.yaml", func(f string) (string, error) { + return goodCm1, nil + }, "") + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + }) + + suite.Run("cm1 causes error while loading", func() { + p.UpdateFile("d1/cm1.yaml", func(f string) (string, error) { + return badCm1_2, nil + }, "") + kd := suite.waitForReconcile(key) + suite.assertErrors(kd, metav1.ConditionFalse, kluctlv1.PrepareFailedReason, "prepare failed with 1 errors. Check status.lastPrepareError for details", "MalformedYAMLError: yaml: line 7: did not find expected node content in File: cm1.yaml", nil, nil) + p.UpdateFile("d1/cm1.yaml", func(f string) (string, error) { + return goodCm1, nil + }, "") + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + }) + + suite.Run("project can't be loaded", func() { + kluctlBackup := "" + p.UpdateFile(".kluctl.yml", func(f string) (string, error) { + kluctlBackup = f + return "a: b", nil + }, "") + kd := suite.waitForReconcile(key) + suite.assertErrors(kd, metav1.ConditionFalse, kluctlv1.PrepareFailedReason, "prepare failed. Check status.lastPrepareError for details", ".kluctl.yml failed: error unmarshaling JSON: while decoding JSON: json: unknown field \"a\"", nil, nil) + p.UpdateFile(".kluctl.yml", func(f string) (string, error) { + return kluctlBackup, nil + }, "") + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + }) + + suite.Run("deployment can't be loaded", func() { + deploymentBackup := "" + p.UpdateFile("deployment.yml", func(f string) (string, error) { + deploymentBackup = f + return "a: b", nil + }, "") + kd := suite.waitForReconcile(key) + suite.assertErrors(kd, metav1.ConditionFalse, kluctlv1.PrepareFailedReason, "prepare failed. Check status.lastPrepareError for details", "failed to load deployment.yml: error unmarshaling JSON: while decoding JSON: json: unknown field \"a\"", nil, nil) + p.UpdateFile("deployment.yml", func(f string) (string, error) { + return deploymentBackup, nil + }, "") + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + }) + + suite.Run("invalid target", func() { + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Target = utils.Ptr("invalid") + }) + kd := suite.waitForReconcile(key) + suite.assertErrors(kd, metav1.ConditionFalse, kluctlv1.PrepareFailedReason, "prepare failed. Check status.lastPrepareError for details", "target invalid not existent in kluctl project config", nil, nil) + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Target = utils.Ptr("target1") + }) + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + }) + + suite.Run("invalid context", func() { + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Context = utils.Ptr("invalid") + }) + kd := suite.waitForReconcile(key) + suite.assertErrors(kd, metav1.ConditionFalse, kluctlv1.PrepareFailedReason, "prepare failed. Check status.lastPrepareError for details", "context \"invalid\" does not exist", nil, nil) + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Context = utils.Ptr("default") + }) + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + }) + + suite.Run("non existing git repo", func() { + var backup string + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + backup = kd.Spec.Source.Git.URL + kd.Spec.Source.Git.URL = backup + "/invalid" + }) + kd := suite.waitForReconcile(key) + suite.assertErrors(kd, metav1.ConditionFalse, kluctlv1.PrepareFailedReason, "prepare failed. Check status.lastPrepareError for details", "failed to clone git source: repository not found", nil, nil) + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Source.Git.URL = backup + }) + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + }) + suite.Run("non existing git branch", func() { + var backup *gittypes.GitRef + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + backup = kd.Spec.Source.Git.Ref + kd.Spec.Source.Git.Ref = &gittypes.GitRef{ + Branch: "invalid", + } + }) + kd := suite.waitForReconcile(key) + suite.assertErrors(kd, metav1.ConditionFalse, kluctlv1.PrepareFailedReason, "prepare failed. Check status.lastPrepareError for details", "ref refs/heads/invalid not found", nil, nil) + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Source.Git.Ref = backup + }) + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + }) + + suite.Run("prune without discriminator", func() { + var backup any + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Prune = true + }) + p.UpdateKluctlYaml(func(o *uo.UnstructuredObject) error { + backup, _, _ = o.GetNestedField("discriminator") + _ = o.RemoveNestedField("discriminator") + return nil + }) + kd := suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + suite.assertErrors(kd, metav1.ConditionFalse, kluctlv1.DeployFailedReason, "deploy failed with 1 errors", "", []result.DeploymentError{ + {Message: "pruning without a discriminator is not supported"}, + }, []result.DeploymentError{ + {Message: "no discriminator configured. Orphan object detection will not work"}, + }) + p.UpdateKluctlYaml(func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(backup, "discriminator") + return nil + }) + kd = suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + suite.assertErrors(kd, metav1.ConditionTrue, kluctlv1.ReconciliationSucceededReason, "deploy succeeded", "", nil, nil) + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Prune = false + }) + }) +} diff --git a/e2e/gitops_fieldmanager_test.go b/e2e/gitops_fieldmanager_test.go new file mode 100644 index 000000000..f79be2267 --- /dev/null +++ b/e2e/gitops_fieldmanager_test.go @@ -0,0 +1,143 @@ +package e2e + +import ( + "context" + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/suite" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "testing" + "time" + + . "github.com/onsi/gomega" +) + +type GitOpsFieldManagerTestSuite struct { + GitopsTestSuite +} + +func TestGitOpsFieldManager(t *testing.T) { + t.Parallel() + suite.Run(t, new(GitOpsFieldManagerTestSuite)) +} + +func (suite *GitOpsFieldManagerTestSuite) TestFieldManager() { + g := NewWithT(suite.T()) + + p := test_project.NewTestProject(suite.T()) + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("target1", nil) + p.AddKustomizeDeployment("d1", []test_project.KustomizeResource{ + {Name: "cm1.yaml", Content: uo.FromStringMust(`apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 + namespace: "{{ args.namespace }}" +data: + k1: v1 + k2: "{{ args.k2 + 1 }}" +`)}, + }, nil) + + key := suite.createKluctlDeployment(p, "target1", map[string]any{ + "namespace": p.TestSlug(), + "k2": 42, + }) + + suite.Run("initial deployment", func() { + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + }) + + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.DeployInterval = &kluctlv1.SafeDuration{Duration: metav1.Duration{Duration: interval}} + }) + + cm := &corev1.ConfigMap{} + + suite.Run("cm1 is deployed", func() { + err := suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm1", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + g.Expect(cm.Data).To(HaveKeyWithValue("k1", "v1")) + g.Expect(cm.Data).To(HaveKeyWithValue("k2", "43")) + }) + + suite.Run("cm1 is modified and restored", func() { + cm.Data["k1"] = "v2" + err := suite.k.Client.Update(context.TODO(), cm, client.FieldOwner("kubectl")) + g.Expect(err).To(Succeed()) + + g.Eventually(func() bool { + err := suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm1", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + return cm.Data["k1"] == "v1" + }, timeout, time.Second).Should(BeTrue()) + }) + + suite.Run("cm1 gets a key added which is not modified by the controller", func() { + cm.Data["k1"] = "v2" + cm.Data["k3"] = "v3" + err := suite.k.Client.Update(context.TODO(), cm, client.FieldOwner("kubectl")) + g.Expect(err).To(Succeed()) + + g.Eventually(func() bool { + err := suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm1", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + return cm.Data["k1"] == "v1" + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(cm.Data).To(HaveKeyWithValue("k3", "v3")) + }) + + suite.Run("cm1 gets modified with another field manager", func() { + patch := client.MergeFrom(cm.DeepCopy()) + cm.Data["k1"] = "v2" + + err := suite.k.Client.Patch(context.TODO(), cm, patch, client.FieldOwner("test-field-manager")) + g.Expect(err).To(Succeed()) + + for i := 0; i < 2; i++ { + suite.waitForReconcile(key) + } + + err = suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm1", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + g.Expect(cm.Data).To(HaveKeyWithValue("k1", "v2")) + }) + + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.ForceApply = true + }) + + suite.Run("forceApply is true and cm1 gets restored even with another field manager", func() { + patch := client.MergeFrom(cm.DeepCopy()) + cm.Data["k1"] = "v2" + + err := suite.k.Client.Patch(context.TODO(), cm, patch, client.FieldOwner("test-field-manager")) + g.Expect(err).To(Succeed()) + + g.Eventually(func() bool { + err := suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm1", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + return cm.Data["k1"] == "v1" + }, timeout, time.Second).Should(BeTrue()) + }) +} diff --git a/e2e/gitops_git_include_test.go b/e2e/gitops_git_include_test.go new file mode 100644 index 000000000..905b2f23d --- /dev/null +++ b/e2e/gitops_git_include_test.go @@ -0,0 +1,171 @@ +package e2e + +import ( + "context" + "github.com/kluctl/kluctl/v2/api/v1beta1" + git2 "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/pkg/utils" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/suite" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "testing" +) + +type GitOpsIncludesSuite struct { + GitopsTestSuite +} + +func TestGitOpsIncludes(t *testing.T) { + t.Parallel() + suite.Run(t, new(GitOpsIncludesSuite)) +} + +func (suite *GitOpsIncludesSuite) TestGitOpsGitIncludeDeprecatedSecret() { + g := NewWithT(suite.T()) + _ = g + + mainGs := git2.NewTestGitServer(suite.T(), git2.WithTestGitServerAuth("user1", "password1")) + gs1 := git2.NewTestGitServer(suite.T(), git2.WithTestGitServerAuth("user1", "password1")) + gs2 := git2.NewTestGitServer(suite.T()) + + p, _, _ := prepareGitIncludeTest(suite.T(), suite.k, mainGs, gs1, gs2) + + key := suite.createKluctlDeployment2(p, "test", nil, func(kd *v1beta1.KluctlDeployment) { + kd.Spec.Source.URL = utils.Ptr(p.GitUrl()) + }) + + suite.Run("fail without authentication", func() { + suite.waitForCommit(key, "") + + kd := suite.getKluctlDeployment(key) + + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + g.Expect(readinessCondition.Status).To(Equal(v1.ConditionFalse)) + g.Expect(readinessCondition.Reason).To(Equal("PrepareFailed")) + g.Expect(kd.Status.LastPrepareError).To(Equal("failed to clone git source: authentication required: invalid credentials")) + }) + + secret := corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "git-secrets", + Namespace: key.Namespace, + }, + Data: map[string][]byte{ + "username": []byte("user1"), + "password": []byte("password1"), + }, + } + err := suite.k.Client.Create(context.Background(), &secret) + g.Expect(err).To(Succeed()) + + suite.updateKluctlDeployment(key, func(kd *v1beta1.KluctlDeployment) { + kd.Spec.Source.SecretRef = &v1beta1.LocalObjectReference{Name: "git-secrets"} + }) + + suite.Run("retry with authentication", func() { + kd := suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + g.Expect(readinessCondition.Status).To(Equal(v1.ConditionTrue)) + }) +} + +func (suite *GitOpsIncludesSuite) testGitOpsGitIncludeCredentials(legacyGitSource bool) { + g := NewWithT(suite.T()) + _ = g + + mainGs := git2.NewTestGitServer(suite.T(), git2.WithTestGitServerAuth("user1", "password1")) + gs1 := git2.NewTestGitServer(suite.T(), git2.WithTestGitServerAuth("user2", "password2")) + gs2 := git2.NewTestGitServer(suite.T(), git2.WithTestGitServerFailWhenAuth(true)) + + p, ip1, _ := prepareGitIncludeTest(suite.T(), suite.k, mainGs, gs1, gs2) + + var key client.ObjectKey + if legacyGitSource { + key = suite.createKluctlDeployment2(p, "test", nil, func(kd *v1beta1.KluctlDeployment) { + kd.Spec.Source.URL = utils.Ptr(p.GitUrl()) + }) + } else { + key = suite.createKluctlDeployment(p, "test", nil) + } + + suite.Run("fail without authentication", func() { + suite.waitForCommit(key, "") + + kd := suite.getKluctlDeployment(key) + + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + g.Expect(readinessCondition.Status).To(Equal(v1.ConditionFalse)) + g.Expect(readinessCondition.Reason).To(Equal("PrepareFailed")) + g.Expect(kd.Status.LastPrepareError).To(Equal("failed to clone git source: authentication required: invalid credentials")) + }) + + createSecret := func(username string, password string) string { + return suite.createGitopsSecret(map[string]string{ + "username": username, + "password": password, + }) + } + + secret1 := createSecret("user1", "password1") + secret2 := createSecret("user2", "password2") + + suite.updateKluctlDeployment(key, func(kd *v1beta1.KluctlDeployment) { + if legacyGitSource { + var credentials []v1beta1.ProjectCredentialsGitDeprecated + credentials = append(credentials, v1beta1.ProjectCredentialsGitDeprecated{ + Host: gs1.GitHost(), + PathPrefix: ip1.GitUrlPath(), + SecretRef: v1beta1.LocalObjectReference{Name: secret2}, + }) + credentials = append(credentials, v1beta1.ProjectCredentialsGitDeprecated{ + Host: mainGs.GitHost(), + SecretRef: v1beta1.LocalObjectReference{Name: secret1}, + }) + // make sure this one is ignored for http based urls + credentials = append(credentials, v1beta1.ProjectCredentialsGitDeprecated{ + Host: "*", + SecretRef: v1beta1.LocalObjectReference{Name: secret1}, + }) + kd.Spec.Source.Credentials = credentials + } else { + var credentials []v1beta1.ProjectCredentialsGit + credentials = append(credentials, v1beta1.ProjectCredentialsGit{ + Host: gs1.GitHost(), + Path: ip1.GitUrlPath(), + SecretRef: v1beta1.LocalObjectReference{Name: secret2}, + }) + credentials = append(credentials, v1beta1.ProjectCredentialsGit{ + Host: mainGs.GitHost(), + SecretRef: v1beta1.LocalObjectReference{Name: secret1}, + }) + // make sure this one is ignored for http based urls + credentials = append(credentials, v1beta1.ProjectCredentialsGit{ + Host: "*", + SecretRef: v1beta1.LocalObjectReference{Name: secret1}, + }) + kd.Spec.Credentials.Git = credentials + } + }) + + suite.Run("retry with authentication", func() { + kd := suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + g.Expect(readinessCondition.Status).To(Equal(v1.ConditionTrue)) + }) +} + +func (suite *GitOpsIncludesSuite) TestGitOpsGitIncludeCredentials() { + suite.testGitOpsGitIncludeCredentials(false) +} + +func (suite *GitOpsIncludesSuite) TestGitOpsGitIncludeCredentialsLegacy() { + suite.testGitOpsGitIncludeCredentials(true) +} diff --git a/e2e/gitops_helm_test.go b/e2e/gitops_helm_test.go new file mode 100644 index 000000000..567baf431 --- /dev/null +++ b/e2e/gitops_helm_test.go @@ -0,0 +1,176 @@ +package e2e + +import ( + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "strings" + "testing" +) + +type GitOpsHelmSuite struct { + GitopsTestSuite +} + +func TestGitOpsHelm(t *testing.T) { + t.Parallel() + suite.Run(t, new(GitOpsHelmSuite)) +} + +func (suite *GitOpsHelmSuite) testHelmPull(tc helmTestCase, prePull bool) { + g := NewWithT(suite.T()) + + p, repo, err := prepareHelmTestCase(suite.T(), suite.k, tc, prePull, false, noLibrary) + if err != nil { + if tc.expectedPrepareError == "" { + assert.Fail(suite.T(), "did not expect error") + } + return + } + + var legacyHelmCreds []kluctlv1.HelmCredentials + var projectCreds kluctlv1.ProjectCredentials + + if tc.argCredsId != "" { + name := suite.createGitopsSecret(map[string]string{ + "credentialsId": tc.argCredsId, + "username": tc.argUsername, + "password": tc.argPassword, + }) + legacyHelmCreds = append(legacyHelmCreds, kluctlv1.HelmCredentials{ + SecretRef: kluctlv1.LocalObjectReference{Name: name}, + }) + } else if tc.argCredsHost != "" { + host := strings.ReplaceAll(tc.argCredsHost, "", repo.URL.Host) + if tc.helmType == test_utils.TestHelmRepo_Oci { + m := map[string]string{ + "username": tc.argUsername, + "password": tc.argPassword, + } + if !repo.HttpServer.TLSEnabled { + m["plainHttp"] = "true" + } + if tc.argPassCA { + m["ca"] = string(repo.HttpServer.ServerCAs) + } + if tc.argPassClientCert { + m["cert"] = string(repo.HttpServer.ClientCert) + } + if tc.argPassClientCert { + m["key"] = string(repo.HttpServer.ClientKey) + } + name := suite.createGitopsSecret(m) + projectCreds.Oci = append(projectCreds.Oci, kluctlv1.ProjectCredentialsOci{ + Registry: host, + Repository: tc.argCredsPath, + SecretRef: kluctlv1.LocalObjectReference{Name: name}, + }) + } else if tc.helmType == test_utils.TestHelmRepo_Helm { + m := map[string]string{ + "username": tc.argUsername, + "password": tc.argPassword, + } + if tc.argPassCA { + m["ca"] = string(repo.HttpServer.ServerCAs) + } + if tc.argPassClientCert { + m["cert"] = string(repo.HttpServer.ClientCert) + } + if tc.argPassClientCert { + m["key"] = string(repo.HttpServer.ClientKey) + } + name := suite.createGitopsSecret(m) + projectCreds.Helm = append(projectCreds.Helm, kluctlv1.ProjectCredentialsHelm{ + Host: host, + Path: tc.argCredsPath, + SecretRef: kluctlv1.LocalObjectReference{Name: name}, + }) + } else if tc.helmType == test_utils.TestHelmRepo_Git { + m := map[string]string{ + "username": tc.argUsername, + "password": tc.argPassword, + } + name := suite.createGitopsSecret(m) + projectCreds.Git = append(projectCreds.Git, kluctlv1.ProjectCredentialsGit{ + Host: host, + Path: tc.argCredsPath, + SecretRef: kluctlv1.LocalObjectReference{Name: name}, + }) + } + } + + // add a fallback secret that enables plainHttp in case we have no matching creds + if tc.helmType == test_utils.TestHelmRepo_Oci && !repo.HttpServer.TLSEnabled { + m := map[string]string{ + "plainHttp": "true", + } + name := suite.createGitopsSecret(m) + projectCreds.Oci = append(projectCreds.Oci, kluctlv1.ProjectCredentialsOci{ + Registry: repo.URL.Host, + SecretRef: kluctlv1.LocalObjectReference{Name: name}, + }) + } + + key := suite.createKluctlDeployment2(p, "", map[string]any{ + "namespace": p.TestSlug(), + }, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Source = kluctlv1.ProjectSource{ + Git: &kluctlv1.ProjectSourceGit{ + URL: p.GitUrl(), + }, + } + kd.Spec.HelmCredentials = legacyHelmCreds + kd.Spec.Credentials = projectCreds + }) + + kd := suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + + if tc.expectedReadyError == "" { + g.Expect(readinessCondition.Status).ToNot(Equal(metav1.ConditionFalse)) + } else { + g.Expect(readinessCondition.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(readinessCondition.Message).To(ContainSubstring(tc.expectedReadyError)) + } + + if tc.expectedPrepareError == "" { + g.Expect(kd.Status.LastDeployResult).ToNot(BeNil()) + g.Expect(readinessCondition.Status).ToNot(Equal(metav1.ConditionFalse)) + assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "test-helm1-test-chart1") + } else { + g.Expect(kd.Status.LastDeployResult).To(BeNil()) + + g.Expect(readinessCondition.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(readinessCondition.Reason).To(Equal(kluctlv1.PrepareFailedReason)) + g.Expect(kd.Status.LastPrepareError).To(ContainSubstring(tc.expectedPrepareError)) + } +} + +func (suite *GitOpsHelmSuite) TestHelm() { + for _, tc := range helmTests { + tc := tc + if tc.name == "dep-oci-creds-fail" { + continue + } + suite.Run(tc.name, func() { + suite.testHelmPull(tc, false) + }) + } +} + +func (suite *GitOpsHelmSuite) TestHelmPrePull() { + for _, tc := range helmTests { + tc := tc + if tc.name == "dep-oci-creds-fail" { + continue + } + suite.Run(tc.name, func() { + suite.testHelmPull(tc, true) + }) + } +} diff --git a/e2e/gitops_manual_requests_test.go b/e2e/gitops_manual_requests_test.go new file mode 100644 index 000000000..3dc8b4cc7 --- /dev/null +++ b/e2e/gitops_manual_requests_test.go @@ -0,0 +1,426 @@ +package e2e + +import ( + "context" + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/types/k8s" + "github.com/kluctl/kluctl/v2/pkg/types/result" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "testing" + "time" + + . "github.com/onsi/gomega" +) + +type GitOpsManualRequestsSuite struct { + GitopsTestSuite +} + +func TestGitOpsManualRequests(t *testing.T) { + t.Parallel() + suite.Run(t, new(GitOpsManualRequestsSuite)) +} + +func (suite *GitOpsManualRequestsSuite) TestManualRequests() { + g := NewWithT(suite.T()) + + p := test_project.NewTestProject(suite.T()) + p.AddExtraArgs("--controller-namespace", suite.gitopsNamespace+"-system") + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("target1", nil) + addConfigMapDeployment(p, "d1", nil, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + p.UpdateYaml("d1/configmap-cm1.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("v1", "data", "k") + return nil + }, "") + + key := suite.createKluctlDeployment(p, "target1", nil) + + suite.Run("initial deployment", func() { + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + }) + + suite.Run("suspending the deployment", func() { + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Suspend = true + // this will get important later when we test suspend/resume + kd.Spec.DeployInterval = &kluctlv1.SafeDuration{Duration: metav1.Duration{Duration: time.Second * 2}} + }) + suite.waitForReconcile(key) + }) + + suite.Run("run manual diff (with no changes)", func() { + p.KluctlMust(suite.T(), "gitops", "diff", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DiffRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DiffRequestResult.ResultId) + + suite.assertNoChanges(kd.Status.DiffRequestResult.ResultId) + }) + + p.UpdateYaml("d1/configmap-cm1.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("v2", "data", "k") + return nil + }, "") + addConfigMapDeployment(p, "d2", nil, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + + suite.Run("run manual diff (with changes)", func() { + p.KluctlMust(suite.T(), "gitops", "diff", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DiffRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DiffRequestResult.ResultId) + + suite.assertChanges(kd.Status.DiffRequestResult.ResultId, 1, 1, 0, 0) + + // assert nothing actually changed + cm1 := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + assertNestedFieldEquals(suite.T(), cm1, "v1", "data", "k") + assertConfigMapNotExists(suite.T(), suite.k, p.TestSlug(), "cm2") + }) + + suite.Run("run manual deploy (with changes)", func() { + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DeployRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DeployRequestResult.ResultId) + + suite.assertChanges(kd.Status.DeployRequestResult.ResultId, 1, 1, 0, 0) + + // assert it actually changed + cm1 := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + assertNestedFieldEquals(suite.T(), cm1, "v2", "data", "k") + assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm2") + }) + + suite.Run("run manual deploy (with no changes)", func() { + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DeployRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DeployRequestResult.ResultId) + + suite.assertNoChanges(kd.Status.DeployRequestResult.ResultId) + + // assert nothing actually changed + cm1 := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + assertNestedFieldEquals(suite.T(), cm1, "v2", "data", "k") + assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm2") + }) + + suite.Run("run manual prune (with no changes)", func() { + p.KluctlMust(suite.T(), "gitops", "prune", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.PruneRequestResult) + assert.NotEmpty(suite.T(), kd.Status.PruneRequestResult.ResultId) + + suite.assertNoChanges(kd.Status.PruneRequestResult.ResultId) + }) + + p.DeleteKustomizeDeployment("d2") + + suite.Run("run manual prune (with changes)", func() { + p.KluctlMust(suite.T(), "gitops", "prune", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.PruneRequestResult) + assert.NotEmpty(suite.T(), kd.Status.PruneRequestResult.ResultId) + + suite.assertChanges(kd.Status.PruneRequestResult.ResultId, 0, 0, 0, 1) + assertConfigMapNotExists(suite.T(), suite.k, p.TestSlug(), "cm2") + }) + + suite.Run("run manual validate (with no errors)", func() { + p.KluctlMust(suite.T(), "gitops", "validate", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.ValidateRequestResult) + assert.NotEmpty(suite.T(), kd.Status.ValidateRequestResult.ResultId) + + vr := suite.getValidateResult(kd.Status.ValidateRequestResult.ResultId) + assert.Empty(suite.T(), vr.Errors) + assert.Empty(suite.T(), vr.Warnings) + }) + + cm1 := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + err := suite.k.Client.Delete(context.Background(), cm1.ToUnstructured()) + assert.NoError(suite.T(), err) + + suite.Run("run manual validate (with errors)", func() { + _, _, err := p.Kluctl(suite.T(), "gitops", "validate", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + assert.ErrorContains(suite.T(), err, "Validation failed") + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.ValidateRequestResult) + assert.NotEmpty(suite.T(), kd.Status.ValidateRequestResult.ResultId) + + vr := suite.getValidateResult(kd.Status.ValidateRequestResult.ResultId) + assert.Equal(suite.T(), []result.DeploymentError{{Ref: k8s.ObjectRef{Group: "", Version: "v1", Kind: "ConfigMap", Name: "cm1", Namespace: p.TestSlug()}, Message: "object not found"}}, vr.Errors) + assert.Empty(suite.T(), vr.Warnings) + }) + + suite.Run("resume and wait for reconcile", func() { + p.KluctlMust(suite.T(), "gitops", "resume", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + g.Eventually(func() bool { + var cm1 corev1.ConfigMap + err := suite.k.Client.Get(context.Background(), client.ObjectKey{Name: "cm1", Namespace: p.TestSlug()}, &cm1) + return err == nil + }, timeout, time.Second).Should(BeTrue()) + + // delete it again and ensure it re-appears (we have deployInterval=2s) + err := suite.k.Client.Delete(context.Background(), cm1.ToUnstructured()) + assert.NoError(suite.T(), err) + + g.Eventually(func() bool { + var cm1 corev1.ConfigMap + err := suite.k.Client.Get(context.Background(), client.ObjectKey{Name: "cm1", Namespace: p.TestSlug()}, &cm1) + return err == nil + }, timeout, time.Second).Should(BeTrue()) + }) + + suite.Run("suspend and ensure reconcile does not happen", func() { + p.KluctlMust(suite.T(), "gitops", "suspend", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + + err := suite.k.Client.Delete(context.Background(), cm1.ToUnstructured()) + assert.NoError(suite.T(), err) + + g.Consistently(func() bool { + var cm1 corev1.ConfigMap + err := suite.k.Client.Get(context.Background(), client.ObjectKey{Name: "cm1", Namespace: p.TestSlug()}, &cm1) + return errors.IsNotFound(err) + }, 5*time.Second, time.Second).Should(BeTrue()) + + // ensure source is changed + p.UpdateYaml("d1/configmap-cm1.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("v3", "data", "k") + return nil + }, "") + + // and it should still not reconcile + g.Consistently(func() bool { + var cm1 corev1.ConfigMap + err := suite.k.Client.Get(context.Background(), client.ObjectKey{Name: "cm1", Namespace: p.TestSlug()}, &cm1) + return errors.IsNotFound(err) + }, 5*time.Second, time.Second).Should(BeTrue()) + }) + + suite.Run("run manual reconcile", func() { + assertConfigMapNotExists(suite.T(), suite.k, p.TestSlug(), "cm1") + + // this should run even though suspend=true + p.KluctlMust(suite.T(), "gitops", "reconcile", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.ReconcileRequestResult) + + assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + }) +} + +func (suite *GitOpsManualRequestsSuite) TestOverrides() { + p := test_project.NewTestProject(suite.T()) + p.AddExtraArgs("--controller-namespace", suite.gitopsNamespace+"-system") + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("target1", nil) + addConfigMapDeployment(p, "d1", nil, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + p.UpdateYaml("d1/configmap-cm1.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("v1", "data", "k1") + _ = o.SetNestedField("{{ args.a }}", "data", "k2") + _ = o.SetNestedField(`{{ get_var("args.b", "na") }}`, "data", "k3") + return nil + }, "") + + key := suite.createKluctlDeployment(p, "target1", map[string]any{ + "a": "v1", + }) + + suite.Run("initial deployment", func() { + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + cm1 := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + assertNestedFieldEquals(suite.T(), cm1, "v1", "data", "k1") + assertNestedFieldEquals(suite.T(), cm1, "v1", "data", "k2") + assertNestedFieldEquals(suite.T(), cm1, "na", "data", "k3") + }) + + suite.Run("suspending the deployment", func() { + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Suspend = true + }) + suite.waitForReconcile(key) + }) + + p.UpdateYaml("d1/configmap-cm1.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("v2", "data", "k1") + return nil + }, "") + addConfigMapDeployment(p, "d2", nil, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + + suite.Run("deploy with dry-run", func() { + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, "--dry-run") + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DeployRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DeployRequestResult.ResultId) + + // assert that the result pretends that it was changed + suite.assertChanges(kd.Status.DeployRequestResult.ResultId, 1, 1, 0, 0) + + // assert that in reality nothing was changed + cm1 := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + assertNestedFieldEquals(suite.T(), cm1, "v1", "data", "k1") + assertConfigMapNotExists(suite.T(), suite.k, p.TestSlug(), "cm2") + + // now re-deploy with dry-run=false + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.DryRun = true + }) + suite.waitForReconcile(key) + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, "--dry-run=false") + + kd = suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DeployRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DeployRequestResult.ResultId) + + suite.assertChanges(kd.Status.DeployRequestResult.ResultId, 1, 1, 0, 0) + + // assert it actually changed this time + cm1 = assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + assertNestedFieldEquals(suite.T(), cm1, "v2", "data", "k1") + assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm2") + }) + + // we've set dryRun=true in the previous test, let's undo this + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.DryRun = false + }) + suite.waitForReconcile(key) + + suite.Run("deploy with overridden args", func() { + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, "-a", "a=via_arg") + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DeployRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DeployRequestResult.ResultId) + + suite.assertChanges(kd.Status.DeployRequestResult.ResultId, 0, 1, 0, 0) + + // assert it actually changed this time + cm1 := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + assertNestedFieldEquals(suite.T(), cm1, "via_arg", "data", "k2") + + // undo it + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + }) + + p.DeleteKustomizeDeployment("d2") + + suite.Run("deploy with overridden prune", func() { + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Prune = true + }) + + p.KluctlMust(suite.T(), "gitops", "reconcile", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, "--prune=false") + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, "--prune=false") + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DeployRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DeployRequestResult.ResultId) + + suite.assertChanges(kd.Status.DeployRequestResult.ResultId, 0, 0, 1, 0) + assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm2") + + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Prune = false + }) + p.KluctlMust(suite.T(), "gitops", "reconcile", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, "--prune") + + kd = suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DeployRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DeployRequestResult.ResultId) + + suite.assertChanges(kd.Status.DeployRequestResult.ResultId, 0, 0, 0, 1) + assertConfigMapNotExists(suite.T(), suite.k, p.TestSlug(), "cm2") + }) + + p.UpdateYaml("d1/configmap-cm1.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("v1", "data", "k4") + return nil + }, "") + addConfigMapDeployment(p, "d3", nil, resourceOpts{ + name: "cm3", + namespace: p.TestSlug(), + }) + + suite.Run("deploy with overridden inclusion", func() { + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, "-I", "d1") + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DeployRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DeployRequestResult.ResultId) + + suite.assertChanges(kd.Status.DeployRequestResult.ResultId, 0, 1, 0, 0) + assertConfigMapNotExists(suite.T(), suite.k, p.TestSlug(), "cm3") + }) + + suite.Run("deploy with overridden exclusion", func() { + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, "-E", "d1") + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DeployRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DeployRequestResult.ResultId) + + suite.assertChanges(kd.Status.DeployRequestResult.ResultId, 1, 0, 0, 0) + assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm3") + }) + + p.UpdateTarget("target2", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField("via_target", "args", "b") + }) + + suite.Run("deploy with overridden target", func() { + // first, deploy without overrides + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + cm1 := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + assertNestedFieldEquals(suite.T(), cm1, "na", "data", "k3") + + // now with override + p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, "--target", "target2") + + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DeployRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DeployRequestResult.ResultId) + + suite.assertChanges(kd.Status.DeployRequestResult.ResultId, 0, 2, 0, 0) + cm1 = assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm1") + cm3 := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm3") + assertNestedFieldEquals(suite.T(), cm1, p.TestSlug()+"-target2", "metadata", "labels", "kluctl.io/discriminator") + assertNestedFieldEquals(suite.T(), cm3, p.TestSlug()+"-target2", "metadata", "labels", "kluctl.io/discriminator") + assertNestedFieldEquals(suite.T(), cm1, "via_target", "data", "k3") + }) +} diff --git a/e2e/gitops_misc_test.go b/e2e/gitops_misc_test.go new file mode 100644 index 000000000..0c6328e6d --- /dev/null +++ b/e2e/gitops_misc_test.go @@ -0,0 +1,136 @@ +package e2e + +import ( + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" + + . "github.com/onsi/gomega" +) + +type GitOpsMiscSuite struct { + GitopsTestSuite +} + +func TestGitOpsMisc(t *testing.T) { + t.Parallel() + suite.Run(t, new(GitOpsMiscSuite)) +} + +func (suite *GitOpsMiscSuite) TestGitSourceWithPath() { + p := test_project.NewTestProject(suite.T(), test_project.WithGitSubDir("subDir")) + p.AddExtraArgs("--controller-namespace", suite.gitopsNamespace+"-system") + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("target1", nil) + addConfigMapDeployment(p, "d1", nil, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + + key := suite.createKluctlDeployment(p, "target1", nil) + + suite.Run("initial deployment fails", func() { + kd := suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + status := suite.getReadiness(kd) + assert.Equal(suite.T(), metav1.ConditionFalse, status.Status) + assert.Equal(suite.T(), "target target1 not existent in kluctl project config", kd.Status.LastPrepareError) + }) + + suite.Run("deployment with path succeeds", func() { + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Source.Git.Path = "subDir" + }) + + kd := suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + status := suite.getReadiness(kd) + assert.Equal(suite.T(), metav1.ConditionTrue, status.Status) + }) +} + +func (suite *GitOpsMiscSuite) TestOciSourceWithPath() { + p := test_project.NewTestProject(suite.T(), test_project.WithGitSubDir("subDir")) + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("target1", nil) + addConfigMapDeployment(p, "d1", nil, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Oci, "", nil) + repo.Start(suite.T()) + + repoUrl := repo.URL.String() + "/org/repo" + + p.KluctlMust(suite.T(), "oci", "push", "--url", repoUrl, "--project-dir", p.LocalWorkDir()) + + p.AddExtraArgs("--controller-namespace", suite.gitopsNamespace+"-system") + + key := suite.createKluctlDeployment2(p, "target1", nil, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Source.Oci = &kluctlv1.ProjectSourceOci{ + URL: repoUrl, + } + }) + + suite.Run("initial deployment fails", func() { + kd := suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + status := suite.getReadiness(kd) + assert.Equal(suite.T(), metav1.ConditionFalse, status.Status) + assert.Equal(suite.T(), "target target1 not existent in kluctl project config", kd.Status.LastPrepareError) + }) + + suite.Run("deployment with path succeeds", func() { + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Source.Oci.Path = "subDir" + }) + + kd := suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + status := suite.getReadiness(kd) + assert.Equal(suite.T(), metav1.ConditionTrue, status.Status) + }) +} + +func (suite *GitOpsMiscSuite) TestNoTarget() { + p := prepareNoTargetTest(suite.T(), true) + createNamespace(suite.T(), suite.k, p.TestSlug()) + + key := suite.createKluctlDeployment(p, "", nil) + + suite.Run("deployment with no target", func() { + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + cm := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm") + assert.Equal(suite.T(), map[string]any{ + "targetName": "", + "targetContext": "default", + }, cm.Object["data"]) + }) +} + +func (suite *GitOpsMiscSuite) TestNoTargetError() { + g := NewWithT(suite.T()) + + p := prepareNoTargetTest(suite.T(), true) + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("target1", func(target *uo.UnstructuredObject) { + }) + + key := suite.createKluctlDeployment(p, "", nil) + + suite.Run("deployment with no target", func() { + kd := suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + + g.Expect(readinessCondition.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(readinessCondition.Reason).To(Equal(kluctlv1.PrepareFailedReason)) + g.Expect(kd.Status.LastPrepareError).To(ContainSubstring("a target must be explicitly selected when targets are defined in the Kluctl project")) + }) +} diff --git a/e2e/gitops_oci_include_test.go b/e2e/gitops_oci_include_test.go new file mode 100644 index 000000000..2631c5bfd --- /dev/null +++ b/e2e/gitops_oci_include_test.go @@ -0,0 +1,146 @@ +package e2e + +import ( + "context" + "fmt" + "github.com/kluctl/kluctl/v2/api/v1beta1" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/suite" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" +) + +type GitOpsOciIncludeSuite struct { + GitopsTestSuite +} + +func TestGitOpsOciInclude(t *testing.T) { + t.Parallel() + suite.Run(t, new(GitOpsOciIncludeSuite)) +} + +func (suite *GitOpsOciIncludeSuite) TestGitOpsOciIncludeCredentials() { + g := NewWithT(suite.T()) + _ = g + + ip1 := prepareIncludeProject(suite.T(), "include1", "", nil) + ip2 := prepareIncludeProject(suite.T(), "include2", "subDir", nil) + ip3 := prepareIncludeProject(suite.T(), "include3", "", nil) + + createRepo := func(user, pass string) *test_utils.TestHelmRepo { + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Oci, "", nil) + repo.HttpServer.Username = user + repo.HttpServer.Password = pass + repo.Start(suite.T()) + return repo + } + + repo0 := createRepo("user0", "pass0") + repo1 := createRepo("user1", "pass1") + repo2 := createRepo("user2", "pass2") + repo3 := createRepo("user3", "pass3") + + repoUrl0 := repo0.URL.String() + "/org0/repo0" + repoUrl1 := repo1.URL.String() + "/org1/repo1" + repoUrl2 := repo2.URL.String() + "/org2/repo2" + repoUrl3 := repo3.URL.String() + "/org3/repo3" + + ip1.KluctlMust(suite.T(), "oci", "push", "--url", repoUrl1, "--registry-creds", fmt.Sprintf("%s=user1:pass1", repo1.URL.Host)) + ip2.KluctlMust(suite.T(), "oci", "push", "--url", repoUrl2, "--project-dir", ip2.LocalWorkDir(), "--registry-creds", fmt.Sprintf("%s=user2:pass2", repo2.URL.Host)) + ip3.KluctlMust(suite.T(), "oci", "push", "--url", repoUrl3, "--registry-creds", fmt.Sprintf("%s=user3:pass3", repo3.URL.Host)) + + p := test_project.NewTestProject(suite.T()) + + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) {}) + + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "oci": map[string]any{ + "url": repoUrl1, + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "oci": map[string]any{ + "url": repoUrl2, + "subDir": "subDir", + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "oci": map[string]any{ + "url": repoUrl3, + }, + })) + p.KluctlMust(suite.T(), "oci", "push", "--url", repoUrl0, "--registry-creds", fmt.Sprintf("%s=user0:pass0", repo0.URL.Host)) + + var key = suite.createKluctlDeployment2(p, "test", nil, func(kd *v1beta1.KluctlDeployment) { + kd.Spec.Source.Oci = &v1beta1.ProjectSourceOci{ + URL: repoUrl0, + } + }) + suite.Run("fail without authentication", func() { + kd := suite.waitForCommit(key, "") + + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + g.Expect(readinessCondition.Status).To(Equal(v1.ConditionFalse)) + g.Expect(readinessCondition.Reason).To(Equal("PrepareFailed")) + g.Expect(kd.Status.LastPrepareError).To(ContainSubstring("401 Unauthorized")) + }) + + createSecret := func(secretName string, username string, password string) { + secret := corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: secretName, + Namespace: key.Namespace, + }, + Data: map[string][]byte{ + "username": []byte(username), + "password": []byte(password), + "plain_http": []byte("true"), + }, + } + err := suite.k.Client.Create(context.Background(), &secret) + g.Expect(err).To(Succeed()) + } + + createSecret("secret0", "user0", "pass0") + createSecret("secret1", "user1", "pass1") + createSecret("secret2", "user2", "pass2") + createSecret("secret3", "user3", "pass3") + + suite.updateKluctlDeployment(key, func(kd *v1beta1.KluctlDeployment) { + kd.Spec.Credentials.Oci = append(kd.Spec.Credentials.Oci, v1beta1.ProjectCredentialsOci{ + Registry: repo0.URL.Host, + Repository: "org0/repo0", + SecretRef: v1beta1.LocalObjectReference{Name: "secret0"}, + }) + kd.Spec.Credentials.Oci = append(kd.Spec.Credentials.Oci, v1beta1.ProjectCredentialsOci{ + Registry: repo1.URL.Host, + Repository: "org1/repo1", + SecretRef: v1beta1.LocalObjectReference{Name: "secret1"}, + }) + kd.Spec.Credentials.Oci = append(kd.Spec.Credentials.Oci, v1beta1.ProjectCredentialsOci{ + Registry: repo2.URL.Host, + Repository: "*/repo2", + SecretRef: v1beta1.LocalObjectReference{Name: "secret2"}, + }) + kd.Spec.Credentials.Oci = append(kd.Spec.Credentials.Oci, v1beta1.ProjectCredentialsOci{ + Registry: repo3.URL.Host, + Repository: "org3/*", + SecretRef: v1beta1.LocalObjectReference{Name: "secret3"}, + }) + }) + + suite.Run("retry with authentication", func() { + kd := suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + g.Expect(readinessCondition.Status).To(Equal(v1.ConditionTrue)) + }) +} diff --git a/e2e/gitops_prune_delete_test.go b/e2e/gitops_prune_delete_test.go new file mode 100644 index 000000000..6e751976f --- /dev/null +++ b/e2e/gitops_prune_delete_test.go @@ -0,0 +1,194 @@ +package e2e + +import ( + "context" + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/suite" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "testing" + "time" + + . "github.com/onsi/gomega" +) + +type GitOpsPruneDeleteSuite struct { + GitopsTestSuite +} + +func TestGitOpsPruneDelete(t *testing.T) { + t.Parallel() + suite.Run(t, new(GitOpsPruneDeleteSuite)) +} + +func (suite *GitOpsPruneDeleteSuite) TestKluctlDeploymentReconciler_Prune() { + g := NewWithT(suite.T()) + + p := test_project.NewTestProject(suite.T()) + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("target1", nil) + + p.AddKustomizeDeployment("d1", []test_project.KustomizeResource{ + {Name: "cm1.yaml", Content: uo.FromStringMust(`apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 + namespace: "{{ args.namespace }}" +data: + k1: v1 +`)}, + }, nil) + p.AddKustomizeDeployment("d2", []test_project.KustomizeResource{ + {Name: "cm2.yaml", Content: uo.FromStringMust(`apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 + namespace: "{{ args.namespace }}" +data: + k1: v1 +`)}, + }, nil) + + key := suite.createKluctlDeployment(p, "target1", map[string]any{ + "namespace": p.TestSlug(), + }) + + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + + cm := &corev1.ConfigMap{} + + suite.Run("cm1 and cm2 got deployed", func() { + err := suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm1", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + err = suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm2", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + }) + + p.UpdateDeploymentYaml("", func(o *uo.UnstructuredObject) error { + _ = o.RemoveNestedField("deployments", 1) + return nil + }) + + g.Eventually(func() bool { + kd := suite.getKluctlDeploymentAllowNil(key) + if kd.Status.LastDeployResult == nil { + return false + } + ldr, err := kd.Status.GetLastDeployResult() + g.Expect(err).To(Succeed()) + return ldr.GitInfo.Commit == getHeadRevision(suite.T(), p) + }, timeout, time.Second).Should(BeTrue()) + + suite.Run("cm1 and cm2 were not deleted", func() { + err := suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm1", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + err = suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm2", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + }) + + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Prune = true + }) + + suite.waitForReconcile(key) + + suite.Run("cm1 did not get deleted and cm2 got deleted", func() { + err := suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm1", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + err = suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm2", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(MatchError("configmaps \"cm2\" not found")) + }) +} + +func (suite *GitOpsPruneDeleteSuite) doTestDelete(delete bool) { + g := NewWithT(suite.T()) + + p := test_project.NewTestProject(suite.T()) + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("target1", nil) + + p.AddKustomizeDeployment("d1", []test_project.KustomizeResource{ + {Name: "cm1.yaml", Content: uo.FromStringMust(`apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 + namespace: "{{ args.namespace }}" +data: + k1: v1 +`)}, + }, nil) + + key := suite.createKluctlDeployment(p, "target1", map[string]any{ + "namespace": p.TestSlug(), + }) + + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Delete = delete + }) + + suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + + cm := &corev1.ConfigMap{} + + suite.Run("cm1 got deployed", func() { + err := suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm1", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + }) + + suite.deleteKluctlDeployment(key) + + g.Eventually(func() bool { + return suite.getKluctlDeploymentAllowNil(key) == nil + }, timeout, time.Second).Should(BeTrue()) + + if delete { + suite.Run("cm1 was deleted", func() { + err := suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm1", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(MatchError("configmaps \"cm1\" not found")) + }) + } else { + suite.Run("cm1 was not deleted", func() { + err := suite.k.Client.Get(context.TODO(), client.ObjectKey{ + Name: "cm1", + Namespace: p.TestSlug(), + }, cm) + g.Expect(err).To(Succeed()) + }) + } +} + +func (suite *GitOpsPruneDeleteSuite) Test_Delete_True() { + suite.doTestDelete(true) +} + +func (suite *GitOpsPruneDeleteSuite) Test_Delete_False() { + suite.doTestDelete(false) +} diff --git a/e2e/gitops_sops_test.go b/e2e/gitops_sops_test.go new file mode 100644 index 000000000..e584ceb3e --- /dev/null +++ b/e2e/gitops_sops_test.go @@ -0,0 +1,190 @@ +package e2e + +import ( + "context" + "fmt" + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/process" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/kluctl/kluctl/v2/pkg/vars/sops_test_resources" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/suite" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "os" + "path" + "strings" + "testing" +) + +type GitOpsSopsSuite struct { + GitopsTestSuite +} + +func TestGitOpsSops(t *testing.T) { + suite.Run(t, new(GitOpsSopsSuite)) +} + +func (suite *GitOpsSopsSuite) TestEncryptedVars() { + g := NewWithT(suite.T()) + + p := test_project.NewTestProject(suite.T()) + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm", map[string]string{ + "v1": "{{ test1.test2 }}", + }, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + p.UpdateDeploymentYaml("", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField([]map[string]any{ + { + "file": "encrypted-vars.yaml", + }, + }, "vars") + return nil + }) + + p.UpdateFile("encrypted-vars.yaml", func(f string) (string, error) { + b, _ := sops_test_resources.TestResources.ReadFile("test.yaml") + return string(b), nil + }, "") + + key := suite.createKluctlDeployment(p, "test", nil) + + suite.Run("deployment with missing sops key fails", func() { + kd := suite.waitForCommit(key, getHeadRevision(suite.T(), p)) + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + g.Expect(readinessCondition.Status).To(Equal(v1.ConditionFalse)) + + g.Expect(kd.Status.ReconcileRequestResult.CommandError).To(ContainSubstring("cannot get sops data key")) + assertConfigMapNotExists(suite.T(), suite.k, p.TestSlug(), "cm") + }) + + sopsKey, _ := sops_test_resources.TestResources.ReadFile("test-key.txt") + secret := corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "sops-secrets-age", + Namespace: key.Namespace, + }, + Data: map[string][]byte{ + "identity.agekey": sopsKey, + }, + } + err := suite.k.Client.Create(context.Background(), &secret) + g.Expect(err).To(Succeed()) + + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Decryption = &kluctlv1.Decryption{ + Provider: "sops", + SecretRef: &kluctlv1.LocalObjectReference{ + Name: secret.Name, + }, + } + }) + + suite.Run("deployment with existing sops key", func() { + kd := suite.waitForReconcile(key) + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + g.Expect(readinessCondition.Status).To(Equal(v1.ConditionTrue)) + + cm := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm") + assertNestedFieldEquals(suite.T(), cm, map[string]any{ + "v1": "42", + }, "data") + }) +} + +func (suite *GitOpsSopsSuite) TestGpgAgentKilled() { + g := NewWithT(suite.T()) + + p := test_project.NewTestProject(suite.T()) + createNamespace(suite.T(), suite.k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm", map[string]string{ + "v1": "{{ test1.test2 }}", + }, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + p.UpdateDeploymentYaml("", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField([]map[string]any{ + { + "file": "encrypted-vars.yaml", + }, + }, "vars") + return nil + }) + + p.UpdateFile("encrypted-vars.yaml", func(f string) (string, error) { + b, _ := sops_test_resources.TestResources.ReadFile("test-gpg.yaml") + return string(b), nil + }, "") + + countGpgAgents := func() int { + pss, err := process.ListProcesses(context.Background()) + g.Expect(err).To(Succeed()) + count := 0 + suite.T().Logf("counting gpg-agents") + for _, ps := range pss { + homeDirPrefix := path.Join(os.TempDir(), "gpg-") + homeDirArg := fmt.Sprintf("--homedir %s", homeDirPrefix) + if strings.Index(ps.Command, "gpg-agent") != -1 && strings.Index(ps.Command, homeDirArg) != -1 { + suite.T().Logf("gpg-agent: pid=%d, cmd=%s", ps.Pid, ps.Command) + count++ + } + } + return count + } + + gpgAgentCount := countGpgAgents() + + key := suite.createKluctlDeployment(p, "test", nil) + + gpgKey, _ := sops_test_resources.TestResources.ReadFile("private.gpg") + secret := corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "sops-secrets-gpg", + Namespace: key.Namespace, + }, + Data: map[string][]byte{ + "identity.asc": gpgKey, + }, + } + err := suite.k.Client.Create(context.Background(), &secret) + g.Expect(err).To(Succeed()) + + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Decryption = &kluctlv1.Decryption{ + Provider: "sops", + SecretRef: &kluctlv1.LocalObjectReference{ + Name: secret.Name, + }, + } + }) + + suite.Run("deployment with existing sops key", func() { + kd := suite.waitForReconcile(key) + readinessCondition := suite.getReadiness(kd) + g.Expect(readinessCondition).ToNot(BeNil()) + g.Expect(readinessCondition.Status).To(Equal(v1.ConditionTrue)) + + cm := assertConfigMapExists(suite.T(), suite.k, p.TestSlug(), "cm") + assertNestedFieldEquals(suite.T(), cm, map[string]any{ + "v1": "43", + }, "data") + }) + + suite.Run("verify gpg-agents got killed", func() { + newGpgAgentCount := countGpgAgents() + g.Expect(gpgAgentCount).To(Equal(newGpgAgentCount)) + }) +} diff --git a/e2e/gitops_source_override_test.go b/e2e/gitops_source_override_test.go new file mode 100644 index 000000000..1f9b0df81 --- /dev/null +++ b/e2e/gitops_source_override_test.go @@ -0,0 +1,147 @@ +package e2e + +import ( + "fmt" + gittypes "github.com/kluctl/kluctl/lib/git/types" + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "sigs.k8s.io/controller-runtime/pkg/client" + "strings" + "testing" +) + +type GitOpsLocalSourceOverrideSuite struct { + GitopsTestSuite +} + +func TestGitOpsLocalSourceOverride(t *testing.T) { + t.Parallel() + suite.Run(t, new(GitOpsLocalSourceOverrideSuite)) +} + +func (suite *GitOpsLocalSourceOverrideSuite) assertOverridesDidNotHappen(key client.ObjectKey, pt *preparedSourceOverrideTest) { + cm := assertConfigMapExists(suite.T(), suite.k, pt.p.TestSlug(), "include1-cm") + assertNestedFieldEquals(suite.T(), cm, "v", "data", "a") + cm = assertConfigMapExists(suite.T(), suite.k, pt.p.TestSlug(), "include2-cm") + assertNestedFieldEquals(suite.T(), cm, "v", "data", "a") +} + +func (suite *GitOpsLocalSourceOverrideSuite) assertOverridesDidHappen(key client.ObjectKey, pt *preparedSourceOverrideTest) { + kd := suite.getKluctlDeployment(key) + assert.NotNil(suite.T(), kd.Status.DeployRequestResult) + assert.NotEmpty(suite.T(), kd.Status.DeployRequestResult.ResultId) + + suite.assertChanges(kd.Status.DeployRequestResult.ResultId, 0, 2, 0, 0) + cm := assertConfigMapExists(suite.T(), suite.k, pt.p.TestSlug(), "include1-cm") + assertNestedFieldEquals(suite.T(), cm, "o1", "data", "a") + cm = assertConfigMapExists(suite.T(), suite.k, pt.p.TestSlug(), "include2-cm") + assertNestedFieldEquals(suite.T(), cm, "o2", "data", "a") +} + +func (suite *GitOpsLocalSourceOverrideSuite) TestLocalGitOverrides() { + gs := test_utils.NewTestGitServer(suite.T()) + pt := prepareLocalSourceOverrideTest(suite.T(), suite.k, gs, false) + pt.p.AddExtraArgs("--controller-namespace", suite.gitopsNamespace+"-system") + + key := suite.createKluctlDeployment(pt.p, "test", nil) + + suite.Run("initial deployment", func() { + pt.p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + suite.assertOverridesDidNotHappen(key, &pt) + }) + + suite.Run("suspending the deployment", func() { + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Suspend = true + }) + suite.waitForReconcile(key) + }) + + suite.Run("deploy with overridden git source", func() { + u1, _ := gittypes.ParseGitUrl(pt.ip1.GitUrl()) + u2, _ := gittypes.ParseGitUrl(pt.ip2.GitUrl()) + k1 := u1.RepoKey().String() + k2 := u2.RepoKey().String() + + suite.assertOverridesDidNotHappen(key, &pt) + + pt.p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, + "--local-git-override", fmt.Sprintf("%s=%s", k1, pt.override1), + "--local-git-override", fmt.Sprintf("%s=%s", k2, pt.override2), + "--local-source-override-port", fmt.Sprintf("%d", suite.sourceOverridePort)) + + suite.assertOverridesDidHappen(key, &pt) + + // undo everything + pt.p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + }) + + suite.Run("deploy with overridden git group source", func() { + u1, _ := gittypes.ParseGitUrl(pt.p.GitServer().GitUrl() + "/repos") + k1 := u1.RepoKey().String() + + suite.assertOverridesDidNotHappen(key, &pt) + + pt.p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, + "--local-git-group-override", fmt.Sprintf("%s=%s", k1, pt.overrideGroupDir), + "--local-source-override-port", fmt.Sprintf("%d", suite.sourceOverridePort)) + + suite.assertOverridesDidHappen(key, &pt) + + // undo everything + pt.p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + }) +} + +func (suite *GitOpsLocalSourceOverrideSuite) TestLocalOciOverrides() { + pt := prepareLocalSourceOverrideTest(suite.T(), suite.k, nil, true) + pt.p.AddExtraArgs("--controller-namespace", suite.gitopsNamespace+"-system") + + key := suite.createKluctlDeployment(pt.p, "test", nil) + + suite.Run("initial deployment", func() { + pt.p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + suite.assertOverridesDidNotHappen(key, &pt) + }) + + suite.Run("suspending the deployment", func() { + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Suspend = true + }) + suite.waitForReconcile(key) + }) + + suite.Run("deploy with overridden oci source", func() { + k1 := strings.TrimPrefix(pt.repoUrl1, "oci://") + k2 := strings.TrimPrefix(pt.repoUrl2, "oci://") + + suite.assertOverridesDidNotHappen(key, &pt) + + pt.p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, + "--local-oci-override", fmt.Sprintf("%s=%s", k1, pt.override1), + "--local-oci-override", fmt.Sprintf("%s=%s", k2, pt.override2), + "--local-source-override-port", fmt.Sprintf("%d", suite.sourceOverridePort)) + + suite.assertOverridesDidHappen(key, &pt) + + // undo everything + pt.p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + }) + + suite.Run("deploy with overridden oci group source", func() { + k1 := strings.TrimPrefix(pt.repo.URL.String(), "oci://") + "/org1" + + suite.assertOverridesDidNotHappen(key, &pt) + + pt.p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name, + "--local-oci-group-override", fmt.Sprintf("%s=%s", k1, pt.overrideGroupDir), + "--local-source-override-port", fmt.Sprintf("%d", suite.sourceOverridePort)) + + suite.assertOverridesDidHappen(key, &pt) + + // undo everything + pt.p.KluctlMust(suite.T(), "gitops", "deploy", "--context", suite.k.Context, "--namespace", key.Namespace, "--name", key.Name) + }) +} diff --git a/e2e/gitops_suite_test.go b/e2e/gitops_suite_test.go new file mode 100644 index 000000000..dff784b77 --- /dev/null +++ b/e2e/gitops_suite_test.go @@ -0,0 +1,411 @@ +package e2e + +import ( + "context" + "encoding/json" + "fmt" + "github.com/huandu/xstrings" + "github.com/kluctl/kluctl/lib/yaml" + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + port_tool "github.com/kluctl/kluctl/v2/e2e/test-utils/port-tool" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/results" + "github.com/kluctl/kluctl/v2/pkg/sourceoverride" + "github.com/kluctl/kluctl/v2/pkg/types/result" + "github.com/kluctl/kluctl/v2/pkg/utils/flux_utils/meta" + "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/klog/v2" + "math/rand" + "os" + "path/filepath" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "strings" + "time" + + . "github.com/onsi/gomega" +) + +const ( + timeout = time.Second * 300 + interval = time.Second * 5 +) + +func init() { + // this must be called in the first 30 seconds of startup, so we have to do it here at init() time + ctrl.SetLogger(klog.NewKlogr()) +} + +type GitopsTestSuite struct { + suite.Suite + + k *test_utils.EnvTestCluster + + sourceOverridePort int + cancelController context.CancelFunc + + gitopsNamespace string + gitopsResultsNamespace string + gitopsSecretIdx int +} + +func (suite *GitopsTestSuite) SetupSuite() { + suite.k = gitopsCluster + + n := suite.T().Name() + n = xstrings.ToKebabCase(n) + n = strings.ReplaceAll(n, "/", "-") + n += "-gitops" + suite.gitopsNamespace = n + suite.gitopsResultsNamespace = n + "-r" + + createNamespace(suite.T(), suite.k, suite.gitopsNamespace) + + suite.startController() +} + +func (suite *GitopsTestSuite) TearDownSuite() { + if suite.cancelController != nil { + suite.cancelController() + } +} + +func (suite *GitopsTestSuite) TearDownTest() { +} + +func (suite *GitopsTestSuite) startController() { + tmpKubeconfig := filepath.Join(suite.T().TempDir(), "kubeconfig") + err := os.WriteFile(tmpKubeconfig, suite.k.Kubeconfig, 0o600) + if err != nil { + suite.T().Fatal(err) + } + + controllerNamespace := suite.gitopsNamespace + "-system" + createNamespace(suite.T(), suite.k, controllerNamespace) + + suite.sourceOverridePort = port_tool.NextFreePort("127.0.0.1") + + ctx, ctxCancel := context.WithCancel(context.Background()) + args := []string{ + "controller", + "run", + "--kubeconfig", + tmpKubeconfig, + "--context", + "gitops", + "--namespace", + suite.gitopsNamespace, + "--command-result-namespace", + suite.gitopsNamespace + "-results", + "--controller-name", fmt.Sprintf("kluctl-controller-%s", suite.gitopsNamespace), + "--controller-namespace", + controllerNamespace, + "--metrics-bind-address=0", + "--health-probe-bind-address=0", + fmt.Sprintf("--source-override-bind-address=localhost:%d", suite.sourceOverridePort), + } + done := make(chan struct{}) + go func() { + logFn := func(args ...any) { + suite.T().Log(args...) + } + _, _, err := test_project.KluctlExecute(suite.T(), ctx, logFn, args...) + if err != nil { + suite.T().Error(err) + } + close(done) + }() + + _, _, _, err = sourceoverride.WaitAndLoadSecret(ctx, suite.k.Client, controllerNamespace) + if err != nil { + suite.T().Fatal(err) + } + + cancel := func() { + ctxCancel() + <-done + } + suite.cancelController = cancel +} + +func (suite *GitopsTestSuite) triggerReconcile(key client.ObjectKey) string { + reconcileId := fmt.Sprintf("%d", rand.Int63()) + + suite.T().Logf("%s: triggerReconcile", reconcileId) + + suite.updateKluctlDeployment(key, func(kd *kluctlv1.KluctlDeployment) { + a := kd.GetAnnotations() + if a == nil { + a = map[string]string{} + } + mr := kluctlv1.ManualRequest{ + RequestValue: reconcileId, + } + a[kluctlv1.KluctlRequestReconcileAnnotation] = yaml.WriteJsonStringMust(&mr) + kd.SetAnnotations(a) + }) + return reconcileId +} + +func (suite *GitopsTestSuite) waitForReconcile(key client.ObjectKey) *kluctlv1.KluctlDeployment { + g := gomega.NewWithT(suite.T()) + + reconcileId := suite.triggerReconcile(key) + + suite.T().Logf("%s: waiting for reconcile to finish", reconcileId) + + var kd *kluctlv1.KluctlDeployment + g.Eventually(func() bool { + kd = suite.getKluctlDeployment(key) + if kd.Status.ReconcileRequestResult == nil || kd.Status.ReconcileRequestResult.Request.RequestValue != reconcileId { + suite.T().Logf("%s: request processing not started yet", reconcileId) + return false + } + if kd.Status.ReconcileRequestResult.EndTime == nil { + suite.T().Logf("%s: request processing not finished yet", reconcileId) + return false + } + readinessCondition := suite.getReadiness(kd) + if readinessCondition == nil || readinessCondition.Status == metav1.ConditionUnknown { + suite.T().Logf("%s: readiness status == unknown", reconcileId) + return false + } + suite.T().Logf("%s: finished waiting", reconcileId) + return true + }, timeout, time.Second).Should(BeTrue()) + return kd +} + +func (suite *GitopsTestSuite) waitForCommit(key client.ObjectKey, commit string) *kluctlv1.KluctlDeployment { + g := gomega.NewWithT(suite.T()) + + reconcileId := suite.triggerReconcile(key) + + suite.T().Logf("%s: waiting for commit %s", reconcileId, commit) + + var kd *kluctlv1.KluctlDeployment + g.Eventually(func() bool { + kd = suite.getKluctlDeployment(key) + if kd.Status.ReconcileRequestResult == nil || kd.Status.ReconcileRequestResult.Request.RequestValue != reconcileId { + suite.T().Logf("%s: request processing not started yet", reconcileId) + return false + } + if kd.Status.ReconcileRequestResult.EndTime == nil { + suite.T().Logf("%s: request processing not finished yet", reconcileId) + return false + } + if kd.Status.ObservedCommit != commit { + suite.T().Logf("%s: commit %s does mot match %s", reconcileId, kd.Status.ObservedCommit, commit) + return false + } + readinessCondition := suite.getReadiness(kd) + if readinessCondition == nil || readinessCondition.Status == metav1.ConditionUnknown { + suite.T().Logf("%s: readiness status == unknown", reconcileId) + return false + } + suite.T().Logf("%s: finished waiting", reconcileId) + return true + }, timeout, time.Second).Should(BeTrue()) + return kd +} + +func (suite *GitopsTestSuite) createKluctlDeployment(p *test_project.TestProject, target string, args map[string]any) client.ObjectKey { + return suite.createKluctlDeployment2(p, target, args, func(kd *kluctlv1.KluctlDeployment) { + kd.Spec.Source.Git = &kluctlv1.ProjectSourceGit{ + URL: p.GitUrl(), + } + }) +} + +func (suite *GitopsTestSuite) createKluctlDeployment2(p *test_project.TestProject, target string, args map[string]any, modify ...func(kd *kluctlv1.KluctlDeployment)) client.ObjectKey { + createNamespace(suite.T(), suite.k, suite.gitopsNamespace) + + jargs, err := json.Marshal(args) + if err != nil { + suite.T().Fatal(err) + } + + var targetPtr *string + if target != "" { + targetPtr = &target + } + + kluctlDeployment := &kluctlv1.KluctlDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: p.TestSlug(), + Namespace: suite.gitopsNamespace, + }, + Spec: kluctlv1.KluctlDeploymentSpec{ + Interval: metav1.Duration{Duration: interval}, + Timeout: &metav1.Duration{Duration: timeout}, + Target: targetPtr, + Args: &runtime.RawExtension{ + Raw: jargs, + }, + }, + } + + for _, m := range modify { + if m != nil { + m(kluctlDeployment) + } + } + + err = suite.k.Client.Create(context.Background(), kluctlDeployment) + if err != nil { + suite.T().Fatal(err) + } + + key := client.ObjectKeyFromObject(kluctlDeployment) + suite.T().Cleanup(func() { + suite.deleteKluctlDeployment(key) + }) + return key +} + +func (suite *GitopsTestSuite) getKluctlDeploymentAllowNil(key client.ObjectKey) *kluctlv1.KluctlDeployment { + var kd kluctlv1.KluctlDeployment + err := suite.k.Client.Get(context.TODO(), key, &kd) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + suite.T().Fatal(err) + } + return &kd +} + +func (suite *GitopsTestSuite) getKluctlDeployment(key client.ObjectKey) *kluctlv1.KluctlDeployment { + kd := suite.getKluctlDeploymentAllowNil(key) + if kd == nil { + suite.T().Fatal(fmt.Sprintf("KluctlDeployment %s not found", key.String())) + } + return kd +} + +func (suite *GitopsTestSuite) updateKluctlDeployment(key client.ObjectKey, update func(kd *kluctlv1.KluctlDeployment)) *kluctlv1.KluctlDeployment { + g := NewWithT(suite.T()) + + kd := suite.getKluctlDeployment(key) + + patch := client.MergeFrom(kd.DeepCopy()) + + update(kd) + + err := suite.k.Client.Patch(context.TODO(), kd, patch, client.FieldOwner("kubectl")) + g.Expect(err).To(Succeed()) + + return kd +} + +func (suite *GitopsTestSuite) deleteKluctlDeployment(key client.ObjectKey) { + g := NewWithT(suite.T()) + + suite.T().Logf("Deleting KluctlDeployment %s", key.String()) + + var kd kluctlv1.KluctlDeployment + kd.Name = key.Name + kd.Namespace = key.Namespace + err := suite.k.Client.Delete(context.Background(), &kd) + if err != nil && !errors.IsNotFound(err) { + g.Expect(err).To(Succeed()) + } + + g.Eventually(func() bool { + if suite.getKluctlDeploymentAllowNil(key) != nil { + return false + } + return true + }, timeout, time.Second).Should(BeTrue()) + + suite.T().Logf("KluctlDeployment %s has vanished", key.String()) +} + +func (suite *GitopsTestSuite) createGitopsSecret(m map[string]string) string { + g := NewWithT(suite.T()) + + name := fmt.Sprintf("gitops-secret-%d", suite.gitopsSecretIdx) + suite.gitopsSecretIdx++ + + mb := map[string][]byte{} + for k, v := range m { + mb[k] = []byte(v) + } + credsSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: suite.gitopsNamespace, + Name: name, + }, + Data: mb, + } + err := suite.k.Client.Create(context.TODO(), credsSecret) + g.Expect(err).To(Succeed()) + + return name +} + +func (suite *GitopsTestSuite) getReadiness(obj *kluctlv1.KluctlDeployment) *metav1.Condition { + for _, c := range obj.Status.Conditions { + if c.Type == meta.ReadyCondition { + return &c + } + } + return nil +} + +func (suite *GitopsTestSuite) getCommandResult(id string) *result.CommandResult { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + rs, err := results.NewResultStoreSecrets(ctx, suite.k.RESTConfig(), suite.k.Client, false, "", 0, 0) + assert.NoError(suite.T(), err) + + cr, err := rs.GetCommandResult(results.GetCommandResultOptions{Id: id}) + if err != nil { + return nil + } + return cr +} + +func (suite *GitopsTestSuite) getValidateResult(id string) *result.ValidateResult { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + rs, err := results.NewResultStoreSecrets(ctx, suite.k.RESTConfig(), suite.k.Client, false, "", 0, 0) + assert.NoError(suite.T(), err) + + vr, err := rs.GetValidateResult(results.GetValidateResultOptions{Id: id}) + if err != nil { + return nil + } + return vr +} + +func (suite *GitopsTestSuite) assertNoChanges(resultId string) { + cr := suite.getCommandResult(resultId) + assert.NotNil(suite.T(), cr) + + sum := cr.BuildSummary() + assert.Zero(suite.T(), sum.NewObjects) + assert.Zero(suite.T(), sum.ChangedObjects) + assert.Zero(suite.T(), sum.OrphanObjects) + assert.Zero(suite.T(), sum.DeletedObjects) +} + +func (suite *GitopsTestSuite) assertChanges(resultId string, new int, changed int, orphan int, deleted int) { + cr := suite.getCommandResult(resultId) + assert.NotNil(suite.T(), cr) + + sum := cr.BuildSummary() + assert.Equal(suite.T(), new, sum.NewObjects) + assert.Equal(suite.T(), changed, sum.ChangedObjects) + assert.Equal(suite.T(), orphan, sum.OrphanObjects) + assert.Equal(suite.T(), deleted, sum.DeletedObjects) +} diff --git a/e2e/helm_test.go b/e2e/helm_test.go new file mode 100644 index 000000000..49add5530 --- /dev/null +++ b/e2e/helm_test.go @@ -0,0 +1,1246 @@ +package e2e + +import ( + "context" + "fmt" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "math/rand" + "os" + "path/filepath" + "sort" + "strings" + "testing" +) + +type helmTestCase struct { + path string + name string + helmType test_utils.TestHelmRepoType + testAuth bool + testTLS bool + testTLSClientCert bool + credsId string + + argCredsId string + argCredsHost string + argCredsPath string + argUsername string + argPassword string + + argPassCA bool + argPassClientCert bool + + expectedReadyError string + expectedPrepareError string +} + +var helmTests = []helmTestCase{ + {name: "helm-no-creds"}, + {name: "oci-no-creds", helmType: test_utils.TestHelmRepo_Oci}, + {name: "git-no-creds", helmType: test_utils.TestHelmRepo_Git}, + + // tls tests + { + name: "helm-tls-missing-ca", testTLS: true, + argPassCA: false, + argCredsHost: "", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "failed to verify certificate", + }, + { + name: "helm-tls-valid-ca", testTLS: true, + argPassCA: true, + argCredsHost: "", + }, + { + name: "helm-tls-missing-cert", testTLS: true, testTLSClientCert: true, + argPassCA: true, + argPassClientCert: false, + argCredsHost: "", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "certificate required", + }, + { + name: "helm-tls-valid-cert", testTLS: true, testTLSClientCert: true, + argPassCA: true, + argPassClientCert: true, + argCredsHost: "", + }, + + { + name: "oci-tls-missing-ca", helmType: test_utils.TestHelmRepo_Oci, testTLS: true, + argPassCA: false, + argCredsHost: "", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "failed to verify certificate", + }, + { + name: "oci-tls-valid-ca", helmType: test_utils.TestHelmRepo_Oci, testTLS: true, + argPassCA: true, + argCredsHost: "", + }, + { + name: "oci-tls-missing-cert", helmType: test_utils.TestHelmRepo_Oci, testTLS: true, testTLSClientCert: true, + argPassCA: true, + argPassClientCert: false, + argCredsHost: "", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "certificate required", + }, + { + name: "oci-tls-valid-cert", helmType: test_utils.TestHelmRepo_Oci, testTLS: true, testTLSClientCert: true, + argPassCA: true, + argPassClientCert: true, + argCredsHost: "", + }, + + // deprecated helm credentials flags + { + name: "dep-helm-creds-missing", helmType: test_utils.TestHelmRepo_Helm, testAuth: true, credsId: "test-creds", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "401 Unauthorized", + }, + { + name: "dep-helm-creds-invalid", helmType: test_utils.TestHelmRepo_Helm, testAuth: true, credsId: "test-creds", + argCredsId: "test-creds", argUsername: "test-user", argPassword: "invalid", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "401 Unauthorized", + }, + { + name: "dep-helm-creds-valid", helmType: test_utils.TestHelmRepo_Helm, testAuth: true, credsId: "test-creds", + argCredsId: "test-creds", argUsername: "test-user", argPassword: "secret-password", + }, + { + name: "dep-oci-creds-fail", helmType: test_utils.TestHelmRepo_Oci, testAuth: true, credsId: "test-creds", + argCredsId: "test-creds", argUsername: "test-user", argPassword: "secret-password", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "OCI charts can currently only be authenticated via registry login and environment variables but not via cli arguments", + }, + + // new helm credentials flags + { + name: "helm-creds-missing", helmType: test_utils.TestHelmRepo_Helm, testAuth: true, + argCredsHost: "-invalid", argUsername: "test-user", argPassword: "secret-password", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "401 Unauthorized", + }, + { + name: "helm-creds-invalid", helmType: test_utils.TestHelmRepo_Helm, testAuth: true, + argCredsHost: "", argUsername: "test-user", argPassword: "invalid", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "401 Unauthorized", + }, + { + name: "helm-creds-valid", helmType: test_utils.TestHelmRepo_Helm, testAuth: true, + argCredsHost: "", argUsername: "test-user", argPassword: "secret-password", + }, + { + name: "helm-creds-missing-path", helmType: test_utils.TestHelmRepo_Helm, testAuth: true, path: "path1", + argCredsHost: "", argCredsPath: "path2", argUsername: "test-user", argPassword: "secret-password", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "401 Unauthorized", + }, + { + name: "helm-creds-invalid-path", helmType: test_utils.TestHelmRepo_Helm, testAuth: true, path: "path1", + argCredsHost: "", argCredsPath: "path1", argUsername: "test-user", argPassword: "invalid", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "401 Unauthorized", + }, + { + name: "helm-creds-valid-path", helmType: test_utils.TestHelmRepo_Helm, testAuth: true, path: "path1", + argCredsHost: "", argCredsPath: "path1", argUsername: "test-user", argPassword: "secret-password", + }, + + // oci creds + { + name: "oci-creds-missing", helmType: test_utils.TestHelmRepo_Oci, testAuth: true, + argCredsHost: "-invalid", argUsername: "test-user", argPassword: "secret-password", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "no basic auth credentials", + }, + { + name: "oci-creds-invalid", helmType: test_utils.TestHelmRepo_Oci, testAuth: true, + argCredsHost: "", argUsername: "test-user", argPassword: "invalid", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "401 Unauthorized", + }, + { + name: "oci-creds-valid", helmType: test_utils.TestHelmRepo_Oci, testAuth: true, + argCredsHost: "", argUsername: "test-user", argPassword: "secret-password", + }, + { + name: "oci-creds-missing-path", helmType: test_utils.TestHelmRepo_Oci, testAuth: true, + argCredsHost: "", argCredsPath: "test-chart2", argUsername: "test-user", argPassword: "secret-password", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "no basic auth credentials", + }, + { + name: "oci-creds-invalid-path", helmType: test_utils.TestHelmRepo_Oci, testAuth: true, + argCredsHost: "", argCredsPath: "test-chart1", argUsername: "test-user", argPassword: "invalid", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "401 Unauthorized", + }, + { + name: "oci-creds-valid-path", helmType: test_utils.TestHelmRepo_Oci, testAuth: true, + argCredsHost: "", argCredsPath: "test-chart1", argUsername: "test-user", argPassword: "secret-password", + }, + + // git creds + { + name: "git-creds-missing", helmType: test_utils.TestHelmRepo_Git, testAuth: true, + argCredsHost: "-invalid", argUsername: "test-user", argPassword: "secret-password", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "authentication required", + }, + { + name: "git-creds-invalid", helmType: test_utils.TestHelmRepo_Git, testAuth: true, + argCredsHost: "", argUsername: "test-user", argPassword: "invalid", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "authentication required", + }, + { + name: "git-creds-valid", helmType: test_utils.TestHelmRepo_Git, testAuth: true, + argCredsHost: "", argUsername: "test-user", argPassword: "secret-password", + }, + { + name: "git-creds-missing-path", helmType: test_utils.TestHelmRepo_Git, testAuth: true, + argCredsHost: "", argCredsPath: "*/helm-repo2", argUsername: "test-user", argPassword: "secret-password", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "authentication required", + }, + { + name: "git-creds-invalid-path", helmType: test_utils.TestHelmRepo_Git, testAuth: true, + argCredsHost: "", argCredsPath: "*/helm-repo", argUsername: "test-user", argPassword: "invalid", + expectedReadyError: "prepare failed with 1 errors. Check status.lastPrepareError for details", + expectedPrepareError: "authentication required", + }, + { + name: "git-creds-valid-path", helmType: test_utils.TestHelmRepo_Git, testAuth: true, + argCredsHost: "", argCredsPath: "*/helm-repo", argUsername: "test-user", argPassword: "secret-password", + }, +} + +func newTmpFile(t *testing.T, b []byte) string { + tmp, _ := os.Create(filepath.Join(t.TempDir(), "x.pem")) + defer tmp.Close() + tmp.Write(b) + return tmp.Name() +} + +func buildHelmTestExtraArgs(t *testing.T, tc helmTestCase, repo *test_utils.TestHelmRepo) []string { + var ret []string + if tc.helmType == test_utils.TestHelmRepo_Oci { + if tc.argCredsHost != "" { + r := strings.ReplaceAll(tc.argCredsHost, "", repo.URL.Host) + if tc.argCredsPath != "" { + r += "/" + tc.argCredsPath + } + if (rand.Int() & 1) == 0 { + ret = append(ret, fmt.Sprintf("--registry-username=%s=%s", r, tc.argUsername)) + ret = append(ret, fmt.Sprintf("--registry-password=%s=%s", r, tc.argPassword)) + } else { + ret = append(ret, fmt.Sprintf("--registry-creds=%s=%s:%s", r, tc.argUsername, tc.argPassword)) + } + if !repo.HttpServer.TLSEnabled { + ret = append(ret, fmt.Sprintf("--registry-plain-http=%s", r)) + } + } + if !repo.HttpServer.TLSEnabled { + r := repo.URL.Host + if tc.argCredsPath != "" { + r += "/" + tc.argCredsPath + } + ret = append(ret, fmt.Sprintf("--registry-plain-http=%s", r)) + } + if tc.testTLS { + if tc.argPassCA { + ret = append(ret, fmt.Sprintf("--registry-ca-file=%s=%s", repo.URL.Host, newTmpFile(t, repo.HttpServer.ServerCAs))) + } + if tc.argPassClientCert { + ret = append(ret, fmt.Sprintf("--registry-cert-file=%s=%s", repo.URL.Host, newTmpFile(t, repo.HttpServer.ClientCert))) + ret = append(ret, fmt.Sprintf("--registry-key-file=%s=%s", repo.URL.Host, newTmpFile(t, repo.HttpServer.ClientKey))) + } + } + } else if tc.helmType == test_utils.TestHelmRepo_Helm { + if tc.argCredsId != "" { + ret = append(ret, fmt.Sprintf("--helm-username=%s:%s", tc.argCredsId, tc.argUsername)) + ret = append(ret, fmt.Sprintf("--helm-password=%s:%s", tc.argCredsId, tc.argPassword)) + } else if tc.argCredsHost != "" { + r := strings.ReplaceAll(tc.argCredsHost, "", repo.URL.Host) + if tc.argCredsPath != "" { + r += "/" + tc.argCredsPath + } + if (rand.Int() & 1) == 0 { + ret = append(ret, fmt.Sprintf("--helm-username=%s=%s", r, tc.argUsername)) + ret = append(ret, fmt.Sprintf("--helm-password=%s=%s", r, tc.argPassword)) + } else { + ret = append(ret, fmt.Sprintf("--helm-creds=%s=%s:%s", r, tc.argUsername, tc.argPassword)) + } + } + if tc.testTLS { + if tc.argPassCA { + ret = append(ret, fmt.Sprintf("--helm-ca-file=%s=%s", repo.URL.Host, newTmpFile(t, repo.HttpServer.ServerCAs))) + } + if tc.argPassClientCert { + ret = append(ret, fmt.Sprintf("--helm-cert-file=%s=%s", repo.URL.Host, newTmpFile(t, repo.HttpServer.ClientCert))) + ret = append(ret, fmt.Sprintf("--helm-key-file=%s=%s", repo.URL.Host, newTmpFile(t, repo.HttpServer.ClientKey))) + } + } + } else if tc.helmType == test_utils.TestHelmRepo_Git { + if tc.argCredsHost != "" { + r := strings.ReplaceAll(tc.argCredsHost, "", repo.URL.Host) + if tc.argCredsPath != "" { + r += "/" + tc.argCredsPath + } + ret = append(ret, fmt.Sprintf("--git-username=%s=%s", r, tc.argUsername)) + ret = append(ret, fmt.Sprintf("--git-password=%s=%s", r, tc.argPassword)) + } + if tc.argPassCA { + ret = append(ret, fmt.Sprintf("--git-ca-file=%s=%s", repo.URL.Host, newTmpFile(t, repo.HttpServer.ServerCAs))) + } + } + // add a fallback that enables plain_http in case we have no matching creds + if tc.helmType == test_utils.TestHelmRepo_Oci && !repo.HttpServer.TLSEnabled { + ret = append(ret, fmt.Sprintf("--registry-plain-http=%s", repo.URL.Host)) + } + return ret +} + +func buildHelmTestEnvVars(t *testing.T, tc helmTestCase, p *test_project.TestProject, repo *test_utils.TestHelmRepo) { + setEnv := func(k string, v string) { + if p.IsUseProcess() { + p.SetEnv(k, v) + } else { + t.Setenv(k, v) + } + } + + if tc.helmType == test_utils.TestHelmRepo_Oci { + if tc.argCredsHost != "" { + setEnv("KLUCTL_REGISTRY_HOST", strings.ReplaceAll(tc.argCredsHost, "", repo.URL.Host)) + } + if tc.argCredsPath != "" { + setEnv("KLUCTL_REGISTRY_REPOSITORY", tc.argCredsPath) + } + if tc.argUsername != "" { + setEnv("KLUCTL_REGISTRY_USERNAME", tc.argUsername) + } + if tc.argPassword != "" { + setEnv("KLUCTL_REGISTRY_PASSWORD", tc.argPassword) + } + if !repo.HttpServer.TLSEnabled { + setEnv("KLUCTL_REGISTRY_PLAIN_HTTP", "true") + } + if tc.argPassCA { + setEnv("KLUCTL_REGISTRY_CA_FILE", newTmpFile(t, repo.HttpServer.ServerCAs)) + } + if tc.argPassClientCert { + setEnv("KLUCTL_REGISTRY_CERT_FILE", newTmpFile(t, repo.HttpServer.ClientCert)) + setEnv("KLUCTL_REGISTRY_KEY_FILE", newTmpFile(t, repo.HttpServer.ClientKey)) + } + } else if tc.helmType == test_utils.TestHelmRepo_Helm { + if tc.argCredsId != "" { + setEnv("KLUCTL_HELM_CREDENTIALS_ID", tc.argCredsId) + } + if tc.argCredsHost != "" { + setEnv("KLUCTL_HELM_HOST", strings.ReplaceAll(tc.argCredsHost, "", repo.URL.Host)) + } + if tc.argCredsPath != "" { + setEnv("KLUCTL_HELM_PATH", tc.argCredsPath) + } + if tc.argUsername != "" { + setEnv("KLUCTL_HELM_USERNAME", tc.argUsername) + } + if tc.argPassword != "" { + setEnv("KLUCTL_HELM_PASSWORD", tc.argPassword) + } + if tc.argPassCA { + setEnv("KLUCTL_HELM_CA_FILE", newTmpFile(t, repo.HttpServer.ServerCAs)) + } + if tc.argPassClientCert { + setEnv("KLUCTL_HELM_CERT_FILE", newTmpFile(t, repo.HttpServer.ClientCert)) + setEnv("KLUCTL_HELM_KEY_FILE", newTmpFile(t, repo.HttpServer.ClientKey)) + } + } else if tc.helmType == test_utils.TestHelmRepo_Git { + if tc.argCredsHost != "" { + setEnv("KLUCTL_GIT_HOST", strings.ReplaceAll(tc.argCredsHost, "", repo.URL.Host)) + } + if tc.argCredsPath != "" { + setEnv("KLUCTL_GIT_PATH", tc.argCredsPath) + } + if tc.argUsername != "" { + setEnv("KLUCTL_GIT_USERNAME", tc.argUsername) + } + if tc.argPassword != "" { + setEnv("KLUCTL_GIT_PASSWORD", tc.argPassword) + } + } + // add a fallback that enables plain_http in case we have no matching creds + if tc.helmType == test_utils.TestHelmRepo_Oci && !repo.HttpServer.TLSEnabled { + setEnv("KLUCTL_REGISTRY_1_HOST", repo.URL.Host) + setEnv("KLUCTL_REGISTRY_1_PLAIN_HTTP", "true") + } +} + +func prepareHelmTestCase(t *testing.T, k *test_utils.EnvTestCluster, tc helmTestCase, prePull bool, useProcess bool, libraryMode libraryTestMode) (*test_project.TestProject, *test_utils.TestHelmRepo, error) { + gitServer := test_utils.NewTestGitServer(t) + gitSubDir := "" + + if libraryMode == includeLibrary { + gitSubDir = "include" + } + + p := test_project.NewTestProject(t, + test_project.WithUseProcess(useProcess), + test_project.WithBareProject(), + test_project.WithGitServer(gitServer), + test_project.WithGitSubDir(gitSubDir), + ) + + if libraryMode != noLibrary { + p.UpdateYaml(".kluctl-library.yaml", func(o *uo.UnstructuredObject) error { + return nil + }, "") + } else { + p.UpdateKluctlYaml(func(o *uo.UnstructuredObject) error { + return nil + }) + } + + createNamespace(t, k, p.TestSlug()) + + user := "" + password := "" + if tc.testAuth { + user = "test-user" + password = "secret-password" + } + + charts := []test_utils.RepoChart{ + {ChartName: "test-chart1", Version: "0.1.0"}, + } + + var repo *test_utils.TestHelmRepo + if tc.helmType == test_utils.TestHelmRepo_Helm || tc.helmType == test_utils.TestHelmRepo_Oci { + repo = test_utils.NewHelmTestRepo(tc.helmType, tc.path, charts) + repo.HttpServer.Username = user + repo.HttpServer.Password = password + repo.HttpServer.TLSEnabled = tc.testTLS + repo.HttpServer.TLSClientCertEnabled = tc.testTLSClientCert + repo.HttpServer.NoLoopbackProxyEnabled = true + } else if tc.helmType == test_utils.TestHelmRepo_Git { + repo = test_utils.NewHelmTestRepoGit(t, tc.path, charts, user, password) + } + + repo.Start(t) + + extraArgs := buildHelmTestExtraArgs(t, tc, repo) + + p.AddHelmDeployment("helm1", repo, "test-chart1", "0.1.0", "test-helm1", p.TestSlug(), nil) + + if tc.testAuth { + if tc.credsId != "" { + p.UpdateYaml("helm1/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(tc.credsId, "helmChart", "credentialsId") + return nil + }, "") + } + } + + if prePull { + args := []string{"helm-pull"} + args = append(args, extraArgs...) + + _, stderr, err := p.Kluctl(t, args...) + if tc.expectedPrepareError != "" { + assert.Error(t, err) + assert.Contains(t, stderr, tc.expectedPrepareError) + return p, repo, err + } else { + assert.NoError(t, err) + assert.FileExists(t, getChartFile(t, p, repo, "test-chart1", "0.1.0")) + + p.GitServer().CommitFiles(p.GitRepoName(), []string{filepath.Join(gitSubDir, ".helm-charts")}, true, "helm-pull") + } + } else { + p.UpdateYaml("helm1/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(true, "helmChart", "skipPrePull") + return nil + }, "") + } + + if libraryMode == gitLibrary { + p2 := test_project.NewTestProject(t, + test_project.WithUseProcess(useProcess), + test_project.WithBareProject(), + ) + p2.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": p.GitUrl(), + }, + })) + return p2, repo, nil + } else if libraryMode == includeLibrary { + p2 := test_project.NewTestProject(t, + test_project.WithUseProcess(useProcess), + test_project.WithBareProject(), + test_project.WithGitServer(gitServer), + test_project.WithGitSubDir("project"), + ) + p2.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "include": "../include", + })) + return p2, repo, nil + } + + return p, repo, nil +} + +type libraryTestMode int + +const ( + noLibrary = iota + gitLibrary + includeLibrary +) + +func testHelmPull(t *testing.T, tc helmTestCase, prePull bool, credsViaEnv bool, libraryMode libraryTestMode) { + useProcess := credsViaEnv + + // uncomment this if you want to debug this when credsViaEnv==true + // useProcess = false + + if !credsViaEnv || useProcess { + t.Parallel() + } + + k := defaultCluster1 + p, repo, err := prepareHelmTestCase(t, k, tc, prePull, useProcess, libraryMode) + if err != nil { + if tc.expectedPrepareError == "" { + assert.Fail(t, "did not expect error") + } + return + } + + args := []string{"deploy", "--yes"} + if credsViaEnv { + buildHelmTestEnvVars(t, tc, p, repo) + } else { + args = append(args, buildHelmTestExtraArgs(t, tc, repo)...) + } + + _, stderr, err := p.Kluctl(t, args...) + pullMessage := "Pulling Helm Chart test-chart1 with version 0.1.0" + if tc.helmType == test_utils.TestHelmRepo_Git { + pullMessage = "Pulling Helm Chart test-chart1 with version refs/tags/0.1.0" + } + if prePull { + assert.NotContains(t, stderr, pullMessage) + } else { + assert.Contains(t, stderr, pullMessage) + } + if tc.expectedPrepareError != "" { + if useProcess { + assert.Contains(t, stderr, tc.expectedPrepareError) + } else { + assert.ErrorContains(t, err, tc.expectedPrepareError) + } + } else { + assert.NoError(t, err) + assertConfigMapExists(t, k, p.TestSlug(), "test-helm1-test-chart1") + } +} + +func TestHelmPull(t *testing.T) { + for _, tc := range helmTests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + testHelmPull(t, tc, false, false, noLibrary) + }) + } +} + +func TestHelmPrePull(t *testing.T) { + for _, tc := range helmTests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + testHelmPull(t, tc, true, false, noLibrary) + }) + } +} + +func TestHelmPullCredsViaEnv(t *testing.T) { + for _, tc := range helmTests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + testHelmPull(t, tc, false, true, noLibrary) + }) + } +} + +func TestHelmInGitLibrary(t *testing.T) { + for _, tc := range helmTests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + testHelmPull(t, tc, true, false, gitLibrary) + }) + } +} + +func TestHelmInIncludeLibrary(t *testing.T) { + for _, tc := range helmTests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + testHelmPull(t, tc, true, false, includeLibrary) + }) + } +} + +func testHelmManualUpgrade(t *testing.T, helmType test_utils.TestHelmRepoType) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + charts := []test_utils.RepoChart{ + {ChartName: "test-chart1", Version: "0.1.0"}, + {ChartName: "test-chart1", Version: "0.2.0"}, + } + + var repo *test_utils.TestHelmRepo + switch helmType { + case test_utils.TestHelmRepo_Helm, test_utils.TestHelmRepo_Oci: + repo = test_utils.NewHelmTestRepo(helmType, "", charts) + case test_utils.TestHelmRepo_Git: + repo = test_utils.NewHelmTestRepoGit(t, "", charts, "", "") + } + repo.Start(t) + + p.AddHelmDeployment("helm1", repo, "test-chart1", "0.1.0", "test-helm1", p.TestSlug(), nil) + + p.KluctlMust(t, "helm-pull") + assert.FileExists(t, getChartFile(t, p, repo, "test-chart1", "0.1.0")) + p.KluctlMust(t, "deploy", "--yes") + cm := assertConfigMapExists(t, k, p.TestSlug(), "test-helm1-test-chart1") + v, _, _ := cm.GetNestedString("data", "version") + assert.Equal(t, "0.1.0", v) + + p.UpdateYaml("helm1/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + if helmType == test_utils.TestHelmRepo_Git { + _ = o.SetNestedField("0.2.0", "helmChart", "git", "ref", "tag") + } else { + _ = o.SetNestedField("0.2.0", "helmChart", "chartVersion") + } + return nil + }, "") + + p.KluctlMust(t, "helm-pull") + assert.NoFileExists(t, getChartFile(t, p, repo, "test-chart1", "0.1.0")) + assert.FileExists(t, getChartFile(t, p, repo, "test-chart1", "0.2.0")) + p.KluctlMust(t, "deploy", "--yes") + cm = assertConfigMapExists(t, k, p.TestSlug(), "test-helm1-test-chart1") + v, _, _ = cm.GetNestedString("data", "version") + assert.Equal(t, "0.2.0", v) +} + +func TestHelmManualUpgrade(t *testing.T) { + testHelmManualUpgrade(t, test_utils.TestHelmRepo_Helm) +} + +func TestHelmManualUpgradeOci(t *testing.T) { + testHelmManualUpgrade(t, test_utils.TestHelmRepo_Oci) +} + +func TestHelmManualUpgradeGit(t *testing.T) { + testHelmManualUpgrade(t, test_utils.TestHelmRepo_Git) +} + +func testHelmUpdate(t *testing.T, helmType test_utils.TestHelmRepoType, upgrade bool, commit bool) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + charts1 := []test_utils.RepoChart{ + {ChartName: "test-chart1", Version: "0.1.0"}, + {ChartName: "test-chart1", Version: "0.2.0"}, + } + charts2 := []test_utils.RepoChart{ + {ChartName: "test-chart2", Version: "0.1.0"}, + {ChartName: "test-chart2", Version: "0.3.0"}, + } + var repo1, repo2 *test_utils.TestHelmRepo + switch helmType { + case test_utils.TestHelmRepo_Helm, test_utils.TestHelmRepo_Oci: + repo1 = test_utils.NewHelmTestRepo(helmType, "", charts1) + repo2 = test_utils.NewHelmTestRepo(helmType, "", charts2) + case test_utils.TestHelmRepo_Git: + repo1 = test_utils.NewHelmTestRepoGit(t, "", charts1, "", "") + repo2 = test_utils.NewHelmTestRepoGit(t, "", charts2, "", "") + } + repo1.Start(t) + repo2.Start(t) + + p.AddHelmDeployment("helm1", repo1, "test-chart1", "0.1.0", "test-helm1", p.TestSlug(), nil) + p.AddHelmDeployment("helm2", repo2, "test-chart2", "0.1.0", "test-helm2", p.TestSlug(), nil) + p.AddHelmDeployment("helm3", repo1, "test-chart1", "0.1.0", "test-helm3", p.TestSlug(), nil) + + p.UpdateYaml("helm3/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(true, "helmChart", "skipUpdate") + return nil + }, "") + + p.KluctlMust(t, "helm-pull") + assert.FileExists(t, getChartFile(t, p, repo1, "test-chart1", "0.1.0")) + assert.FileExists(t, getChartFile(t, p, repo2, "test-chart2", "0.1.0")) + + if commit { + wt, _ := p.GetGitRepo().Worktree() + _, _ = wt.Add(".helm-charts") + _, _ = wt.Commit(".helm-charts", &git.CommitOptions{}) + } + + args := []string{"helm-update"} + if upgrade { + args = append(args, "--upgrade") + } + if commit { + args = append(args, "--commit") + } + + versionPrefix := "" + if helmType == test_utils.TestHelmRepo_Git { + versionPrefix = "refs/tags/" + } + + _, stderr := p.KluctlMust(t, args...) + assert.Contains(t, stderr, fmt.Sprintf("helm1: Chart test-chart1 (old version %s0.1.0) has new version %s0.2.0 available", versionPrefix, versionPrefix)) + assert.Contains(t, stderr, fmt.Sprintf("helm2: Chart test-chart2 (old version %s0.1.0) has new version %s0.3.0 available", versionPrefix, versionPrefix)) + assert.Contains(t, stderr, fmt.Sprintf("helm3: Skipped update to version %s0.2.0", versionPrefix)) + + if upgrade { + assert.Contains(t, stderr, fmt.Sprintf("Upgrading Chart test-chart1 from version %s0.1.0 to %s0.2.0", versionPrefix, versionPrefix)) + assert.Contains(t, stderr, fmt.Sprintf("Upgrading Chart test-chart2 from version %s0.1.0 to %s0.3.0", versionPrefix, versionPrefix)) + } + if commit { + assert.Contains(t, stderr, fmt.Sprintf("Committed helm chart test-chart1 with version %s0.2.0", versionPrefix)) + assert.Contains(t, stderr, fmt.Sprintf("Committed helm chart test-chart2 with version %s0.3.0", versionPrefix)) + } + + pulledVersions1 := listChartVersions(t, p, repo1, "test-chart1") + pulledVersions2 := listChartVersions(t, p, repo2, "test-chart2") + + if upgrade { + assert.Equal(t, []string{"0.1.0", "0.2.0"}, pulledVersions1) + assert.Equal(t, []string{"0.3.0"}, pulledVersions2) + } else { + assert.Equal(t, []string{"0.1.0"}, pulledVersions1) + assert.Equal(t, []string{"0.1.0"}, pulledVersions2) + } + + if commit { + r := p.GetGitRepo() + + commits, err := r.Log(&git.LogOptions{}) + assert.NoError(t, err) + var commitList []object.Commit + err = commits.ForEach(func(commit *object.Commit) error { + commitList = append(commitList, *commit) + return nil + }) + assert.NoError(t, err) + + commitList = commitList[0:2] + sort.Slice(commitList, func(i, j int) bool { + return commitList[i].Message < commitList[j].Message + }) + + assert.Equal(t, fmt.Sprintf("Updated helm chart test-chart1 from version %s0.1.0 to version %s0.2.0", versionPrefix, versionPrefix), commitList[0].Message) + assert.Equal(t, fmt.Sprintf("Updated helm chart test-chart2 from version %s0.1.0 to version %s0.3.0", versionPrefix, versionPrefix), commitList[1].Message) + } +} + +func TestHelmUpdate(t *testing.T) { + testHelmUpdate(t, test_utils.TestHelmRepo_Helm, false, false) +} + +func TestHelmUpdateOci(t *testing.T) { + testHelmUpdate(t, test_utils.TestHelmRepo_Oci, false, false) +} + +func TestHelmUpdateGit(t *testing.T) { + testHelmUpdate(t, test_utils.TestHelmRepo_Git, false, false) +} + +func TestHelmUpdateAndUpgrade(t *testing.T) { + testHelmUpdate(t, test_utils.TestHelmRepo_Helm, true, false) +} + +func TestHelmUpdateAndUpgradeOci(t *testing.T) { + testHelmUpdate(t, test_utils.TestHelmRepo_Oci, true, false) +} + +func TestHelmUpdateAndUpgradeGit(t *testing.T) { + testHelmUpdate(t, test_utils.TestHelmRepo_Git, true, false) +} + +func TestHelmUpdateAndUpgradeAndCommit(t *testing.T) { + testHelmUpdate(t, test_utils.TestHelmRepo_Helm, true, true) +} + +func TestHelmUpdateAndUpgradeAndCommitOci(t *testing.T) { + testHelmUpdate(t, test_utils.TestHelmRepo_Oci, true, true) +} + +func TestHelmUpdateAndUpgradeAndCommitGit(t *testing.T) { + testHelmUpdate(t, test_utils.TestHelmRepo_Git, true, true) +} + +func testHelmUpdateConstraints(t *testing.T, helmType test_utils.TestHelmRepoType) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + charts := []test_utils.RepoChart{ + {ChartName: "test-chart1", Version: "0.1.0"}, + {ChartName: "test-chart1", Version: "0.1.1"}, + {ChartName: "test-chart1", Version: "0.2.0"}, + {ChartName: "test-chart1", Version: "1.1.0"}, + {ChartName: "test-chart1", Version: "1.1.1"}, + {ChartName: "test-chart1", Version: "1.2.1"}, + } + repo := test_utils.NewHelmTestRepo(helmType, "", charts) + repo.Start(t) + + p.AddHelmDeployment("helm1", repo, "test-chart1", "0.1.0", "test-helm1", p.TestSlug(), nil) + p.AddHelmDeployment("helm2", repo, "test-chart1", "0.1.0", "test-helm2", p.TestSlug(), nil) + p.AddHelmDeployment("helm3", repo, "test-chart1", "0.1.0", "test-helm3", p.TestSlug(), nil) + + p.UpdateYaml("helm1/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("~0.1.0", "helmChart", "updateConstraints") + return nil + }, "") + p.UpdateYaml("helm2/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("~0.2.0", "helmChart", "updateConstraints") + return nil + }, "") + p.UpdateYaml("helm3/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("~1.2.0", "helmChart", "updateConstraints") + return nil + }, "") + + args := []string{"helm-update", "--upgrade"} + + _, stderr := p.KluctlMust(t, args...) + assert.Contains(t, stderr, "helm1: Chart test-chart1 (old version 0.1.0) has new version 0.1.1 available") + assert.Contains(t, stderr, "helm2: Chart test-chart1 (old version 0.1.0) has new version 0.2.0 available") + assert.Contains(t, stderr, "helm3: Chart test-chart1 (old version 0.1.0) has new version 1.2.1 available") + + c1 := p.GetYaml("helm1/helm-chart.yaml") + c2 := p.GetYaml("helm2/helm-chart.yaml") + c3 := p.GetYaml("helm3/helm-chart.yaml") + + v1, _, _ := c1.GetNestedString("helmChart", "chartVersion") + v2, _, _ := c2.GetNestedString("helmChart", "chartVersion") + v3, _, _ := c3.GetNestedString("helmChart", "chartVersion") + assert.Equal(t, "0.1.1", v1) + assert.Equal(t, "0.2.0", v2) + assert.Equal(t, "1.2.1", v3) +} + +func TestHelmUpdateConstraints(t *testing.T) { + testHelmUpdateConstraints(t, test_utils.TestHelmRepo_Helm) +} + +func TestHelmUpdateConstraintsOci(t *testing.T) { + testHelmUpdateConstraints(t, test_utils.TestHelmRepo_Oci) +} + +func TestHelmValues(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + charts := []test_utils.RepoChart{ + {ChartName: "test-chart1", Version: "0.1.0"}, + {ChartName: "test-chart2", Version: "0.1.0"}, + } + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Helm, "", charts) + repo.Start(t) + + values1 := map[string]any{ + "data": map[string]any{ + "a": "x1", + "b": "y1", + }, + } + values2 := map[string]any{ + "data": map[string]any{ + "a": "x2", + "b": "y2", + }, + } + values3 := map[string]any{ + "data": map[string]any{ + "a": "{{ args.a }}", + "b": "{{ args.b }}", + }, + } + + p.AddHelmDeployment("helm1", repo, "test-chart1", "0.1.0", "test-helm1", p.TestSlug(), values1) + p.AddHelmDeployment("helm2", repo, "test-chart2", "0.1.0", "test-helm2", p.TestSlug(), values2) + p.AddHelmDeployment("helm3", repo, "test-chart1", "0.1.0", "test-helm3", p.TestSlug(), values3) + + p.KluctlMust(t, "helm-pull") + p.KluctlMust(t, "deploy", "--yes", "-aa=a", "-ab=b") + + cm1 := assertConfigMapExists(t, k, p.TestSlug(), "test-helm1-test-chart1") + cm2 := assertConfigMapExists(t, k, p.TestSlug(), "test-helm2-test-chart2") + cm3 := assertConfigMapExists(t, k, p.TestSlug(), "test-helm3-test-chart1") + + assert.Equal(t, map[string]any{ + "a": "x1", + "b": "y1", + "version": "0.1.0", + "kubeVersion": k.ServerVersion.String(), + }, cm1.Object["data"]) + assert.Equal(t, map[string]any{ + "a": "x2", + "b": "y2", + "version": "0.1.0", + "kubeVersion": k.ServerVersion.String(), + }, cm2.Object["data"]) + assert.Equal(t, map[string]any{ + "a": "a", + "b": "b", + "version": "0.1.0", + "kubeVersion": k.ServerVersion.String(), + }, cm3.Object["data"]) +} + +func TestHelmTemplateChartYaml(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + createNamespace(t, k, p.TestSlug()+"-a") + createNamespace(t, k, p.TestSlug()+"-b") + + charts := []test_utils.RepoChart{ + {ChartName: "test-chart1", Version: "0.1.0"}, + {ChartName: "test-chart2", Version: "0.1.0"}, + } + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Helm, "", charts) + repo.Start(t) + + p.AddHelmDeployment("helm1", repo, "test-chart1", "0.1.0", "test-helm-{{ args.a }}", p.TestSlug(), nil) + p.AddHelmDeployment("helm2", repo, "test-chart2", "0.1.0", "test-helm-{{ args.b }}", p.TestSlug(), nil) + p.AddHelmDeployment("helm3", repo, "test-chart1", "0.1.0", "test-helm-ns", p.TestSlug()+"-{{ args.a }}", nil) + p.AddHelmDeployment("helm4", repo, "test-chart1", "0.1.0", "test-helm-ns", p.TestSlug()+"-{{ args.b }}", nil) + + p.KluctlMust(t, "helm-pull") + p.KluctlMust(t, "deploy", "--yes", "-aa=a", "-ab=b") + + assertConfigMapExists(t, k, p.TestSlug(), "test-helm-a-test-chart1") + assertConfigMapExists(t, k, p.TestSlug(), "test-helm-b-test-chart2") + assertConfigMapExists(t, k, p.TestSlug()+"-a", "test-helm-ns-test-chart1") + assertConfigMapExists(t, k, p.TestSlug()+"-b", "test-helm-ns-test-chart1") +} + +func TestHelmRenderOfflineKubernetes(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + charts := []test_utils.RepoChart{ + {ChartName: "test-chart1", Version: "0.1.0"}, + } + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Helm, "", charts) + repo.Start(t) + + p.AddHelmDeployment("helm1", repo, "test-chart1", "0.1.0", "test-helm1", p.TestSlug(), nil) + p.UpdateYaml("helm1/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(true, "helmChart", "skipPrePull") + return nil + }, "") + + stdout, _ := p.KluctlMust(t, "render", "--print-all", "--offline-kubernetes") + cm1 := uo.FromStringMust(stdout) + + assert.Equal(t, map[string]any{ + "a": "v1", + "b": "v2", + "version": "0.1.0", + "kubeVersion": "v1.20.0", + }, cm1.Object["data"]) + + stdout, _ = p.KluctlMust(t, "render", "--print-all", "--offline-kubernetes", "--kubernetes-version", "1.22.1") + cm1 = uo.FromStringMust(stdout) + + assert.Equal(t, map[string]any{ + "a": "v1", + "b": "v2", + "version": "0.1.0", + "kubeVersion": "v1.22.1", + }, cm1.Object["data"]) +} + +func TestHelmLocalChart(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.AddHelmDeployment("helm1", test_utils.NewHelmTestRepoLocal("../test-chart1"), "", "", "test-helm-1", p.TestSlug(), nil) + p.AddHelmDeployment("helm2", test_utils.NewHelmTestRepoLocal("test-chart2"), "", "", "test-helm-2", p.TestSlug(), nil) + + test_utils.CreateHelmDir(t, "test-chart1", "0.1.0", filepath.Join(p.LocalProjectDir(), "test-chart1")) + test_utils.CreateHelmDir(t, "test-chart2", "0.1.0", filepath.Join(p.LocalProjectDir(), "helm2/test-chart2")) + + p.KluctlMust(t, "deploy", "--yes") + assertConfigMapExists(t, k, p.TestSlug(), "test-helm-1-test-chart1") + assertConfigMapExists(t, k, p.TestSlug(), "test-helm-2-test-chart2") + + _, stderr := p.KluctlMust(t, "helm-pull") + assert.NotContains(t, stderr, "test-chart1") + assert.NotContains(t, stderr, "test-chart2") +} + +func TestHelmSkipPrePull(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + charts := []test_utils.RepoChart{ + {ChartName: "test-chart1", Version: "0.1.0"}, + {ChartName: "test-chart1", Version: "0.1.1"}, + {ChartName: "test-chart1", Version: "0.2.0"}, + } + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Helm, "", charts) + repo.Start(t) + + p.AddHelmDeployment("helm1", repo, "test-chart1", "0.1.0", "test-helm1", p.TestSlug(), nil) + p.AddHelmDeployment("helm2", repo, "test-chart1", "0.1.1", "test-helm2", p.TestSlug(), nil) + + p.UpdateYaml("helm2/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(true, "helmChart", "skipPrePull") + return nil + }, "") + + args := []string{"helm-pull"} + _, stderr := p.KluctlMust(t, args...) + assert.Contains(t, stderr, "Pulling Chart with version 0.1.0") + assert.NotContains(t, stderr, "version 0.1.1") + assert.DirExists(t, filepath.Join(p.LocalProjectDir(), fmt.Sprintf(".helm-charts/http_%s_127.0.0.1/test-chart1/0.1.0", repo.URL.Port()))) + assert.NoDirExists(t, filepath.Join(p.LocalProjectDir(), fmt.Sprintf(".helm-charts/http_%s_127.0.0.1/test-chart1/0.1.1", repo.URL.Port()))) + + p.UpdateYaml("helm1/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(true, "helmChart", "skipPrePull") + return nil + }, "") + _, stderr = p.KluctlMust(t, args...) + assert.Contains(t, stderr, "Removing unused Chart with version 0.1.0") + assert.NotContains(t, stderr, "version 0.1.1") + assert.NoDirExists(t, filepath.Join(p.LocalProjectDir(), fmt.Sprintf(".helm-charts/http_%s_127.0.0.1/test-chart1/0.1.0", repo.URL.Port()))) + assert.NoDirExists(t, filepath.Join(p.LocalProjectDir(), fmt.Sprintf(".helm-charts/http_%s_127.0.0.1/test-chart1/0.1.1", repo.URL.Port()))) + + p.UpdateYaml("helm2/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(false, "helmChart", "skipPrePull") + return nil + }, "") + _, stderr = p.KluctlMust(t, args...) + assert.Contains(t, stderr, "test-chart1: Pulling Chart with version 0.1.1") + assert.NotContains(t, stderr, "version 0.1.0") + assert.NoDirExists(t, filepath.Join(p.LocalProjectDir(), fmt.Sprintf(".helm-charts/http_%s_127.0.0.1/test-chart1/0.1.0", repo.URL.Port()))) + assert.DirExists(t, filepath.Join(p.LocalProjectDir(), fmt.Sprintf(".helm-charts/http_%s_127.0.0.1/test-chart1/0.1.1", repo.URL.Port()))) + + p.UpdateYaml("helm1/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(false, "helmChart", "skipPrePull") + return nil + }, "") + _, stderr = p.KluctlMust(t, args...) + assert.Contains(t, stderr, "Pulling Chart with version 0.1.0") + assert.Contains(t, stderr, "Pulling Chart with version 0.1.1") + assert.DirExists(t, filepath.Join(p.LocalProjectDir(), fmt.Sprintf(".helm-charts/http_%s_127.0.0.1/test-chart1/0.1.0", repo.URL.Port()))) + assert.DirExists(t, filepath.Join(p.LocalProjectDir(), fmt.Sprintf(".helm-charts/http_%s_127.0.0.1/test-chart1/0.1.1", repo.URL.Port()))) + + // not try to update+pull + p.UpdateYaml("helm1/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(true, "helmChart", "skipPrePull") + return nil + }, "") + _, stderr = p.KluctlMust(t, args...) + p.GitServer().CommitFiles("kluctl-project", []string{".helm-charts"}, false, ".helm-charts") + args = []string{ + "helm-update", + "--upgrade", + "--commit", + } + _, stderr = p.KluctlMust(t, args...) + assert.NotContains(t, stderr, "Pulling Chart with version 0.1.0") + assert.NotContains(t, stderr, "Pulling Chart with version 0.1.1") + assert.Contains(t, stderr, "Pulling Chart with version 0.2.0") + assert.NoDirExists(t, filepath.Join(p.LocalProjectDir(), fmt.Sprintf(".helm-charts/http_%s_127.0.0.1/test-chart1/0.1.0", repo.URL.Port()))) + assert.NoDirExists(t, filepath.Join(p.LocalProjectDir(), fmt.Sprintf(".helm-charts/http_%s_127.0.0.1/test-chart1/0.1.1", repo.URL.Port()))) + assert.DirExists(t, filepath.Join(p.LocalProjectDir(), fmt.Sprintf(".helm-charts/http_%s_127.0.0.1/test-chart1/0.2.0", repo.URL.Port()))) +} + +func getChartDir(t *testing.T, p *test_project.TestProject, repo *test_utils.TestHelmRepo, chartName string, chartVersion string) string { + var dir string + + switch repo.Type { + case test_utils.TestHelmRepo_Helm: + dir = filepath.Join(p.LocalProjectDir(), ".helm-charts", fmt.Sprintf("%s_%s_%s", repo.URL.Scheme, repo.URL.Port(), repo.URL.Hostname()), repo.URL.Path, chartName) + case test_utils.TestHelmRepo_Oci: + dir = filepath.Join(p.LocalProjectDir(), ".helm-charts", fmt.Sprintf("%s_%s_%s", repo.URL.Scheme, repo.URL.Port(), repo.URL.Hostname()), chartName) + case test_utils.TestHelmRepo_Git: + dir = filepath.Join(p.LocalProjectDir(), ".helm-charts", fmt.Sprintf("git_%s_%s_%s", repo.URL.Scheme, repo.URL.Port(), repo.URL.Hostname()), repo.URL.Path, chartName) + } + + if chartVersion != "" { + if repo.Type == test_utils.TestHelmRepo_Git { + dir = filepath.Join(dir, "refs", "tags") + } + dir = filepath.Join(dir, chartVersion) + } + return dir +} + +func getChartFile(t *testing.T, p *test_project.TestProject, repo *test_utils.TestHelmRepo, chartName string, chartVersion string) string { + return filepath.Join(getChartDir(t, p, repo, chartName, chartVersion), "Chart.yaml") +} + +func listChartVersions(t *testing.T, p *test_project.TestProject, repo *test_utils.TestHelmRepo, chartName string) []string { + var versions []string + if repo.Type == test_utils.TestHelmRepo_Git { + dir := filepath.Join(getChartDir(t, p, repo, chartName, ""), "refs", "tags") + des, err := os.ReadDir(dir) + assert.NoError(t, err) + + for _, de := range des { + versions = append(versions, de.Name()) + } + } else { + des, err := os.ReadDir(getChartDir(t, p, repo, chartName, "")) + assert.NoError(t, err) + + for _, de := range des { + versions = append(versions, de.Name()) + } + } + + sort.Strings(versions) + return versions +} + +func TestHelmLookup(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + lookupCm := corev1.ConfigMap{ + ObjectMeta: v1.ObjectMeta{ + Name: "lookup-cm", + Namespace: p.TestSlug(), + }, + Data: map[string]string{ + "a": "lookupValue", + }, + } + err := k.Client.Create(context.Background(), &lookupCm) + assert.NoError(t, err) + + charts := []test_utils.RepoChart{ + {ChartName: "test-chart1", Version: "0.1.0"}, + } + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Helm, "", charts) + repo.Start(t) + + values1 := map[string]any{ + "lookup": true, + "lookupNamespace": lookupCm.Namespace, + "lookupName": lookupCm.Name, + } + + p.AddHelmDeployment("helm1", repo, "test-chart1", "0.1.0", "test-helm1", p.TestSlug(), values1) + + p.KluctlMust(t, "helm-pull") + p.KluctlMust(t, "deploy", "--yes") + + cm1 := assertConfigMapExists(t, k, p.TestSlug(), "test-helm1-test-chart1") + assertNestedFieldEquals(t, cm1, "lookupValue", "data", "lookup") + + s, _ := p.KluctlMust(t, "render", "--print-all") + y, err := uo.FromString(s) + assert.NoError(t, err) + assertNestedFieldEquals(t, y, "lookupValue", "data", "lookup") + + s, _ = p.KluctlMust(t, "render", "--print-all", "--offline-kubernetes") + y, err = uo.FromString(s) + assert.NoError(t, err) + assertNestedFieldEquals(t, y, "lookupReturnedNil", "data", "lookup") +} + +func TestHelmWithTarget(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + p.UpdateTarget("test", nil) + + createNamespace(t, k, p.TestSlug()) + + charts := []test_utils.RepoChart{ + {ChartName: "test-chart1", Version: "0.1.0"}, + } + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Helm, "", charts) + repo.Start(t) + + p.AddHelmDeployment("helm1", repo, "test-chart1", "0.1.0", "test-helm1", p.TestSlug(), nil) + + p.KluctlMust(t, "helm-pull") + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + + assertConfigMapExists(t, k, p.TestSlug(), "test-helm1-test-chart1") +} diff --git a/e2e/hooks_test.go b/e2e/hooks_test.go index 18149608c..50abf60c9 100644 --- a/e2e/hooks_test.go +++ b/e2e/hooks_test.go @@ -1,26 +1,79 @@ package e2e import ( - "encoding/base64" "fmt" - "github.com/kluctl/kluctl/v2/internal/test-utils" - "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sync" "testing" + "time" + + "github.com/kluctl/kluctl/v2/pkg/utils/uo" ) -const hookScript = ` -kubectl get configmap -oyaml > /tmp/result.yml -cat /tmp/result.yml -if ! kubectl get secret {{ .name }}-result; then - name="{{ .name }}-result" -else - name="{{ .name }}-result2" -fi -kubectl create secret generic $name --from-file=result=/tmp/result.yml -kubectl delete configmap cm2 || true -` - -func addHookDeployment(p *testProject, dir string, opts resourceOpts, isHelm bool, hook string, hookDeletionPolicy string) { +type hooksTestContext struct { + t *testing.T + k *test_utils.EnvTestCluster + + p *test_project.TestProject + + m sync.Mutex + seenConfigMaps []string + runCount int + + whh *test_utils.CallbackHandlerEntry +} + +func (s *hooksTestContext) setupWebhook() { + s.whh = s.k.AddWebhookHandler(schema.GroupVersionResource{ + Version: "v1", Resource: "configmaps", + }, s.handleConfigmap) +} + +func (s *hooksTestContext) removeWebhook() { + s.k.RemoveWebhookHandler(s.whh) +} + +func (s *hooksTestContext) handleConfigmap(request admission.Request) { + if s.p.TestSlug() != request.Namespace { + return + } + s.m.Lock() + defer s.m.Unlock() + + x, err := uo.FromString(string(request.Object.Raw)) + if err != nil { + s.t.Fatal(err) + } + generation, _, err := x.GetNestedInt("metadata", "generation") + if err != nil { + s.t.Fatal(err) + } + uid, _, err := x.GetNestedString("metadata", "uid") + if err != nil { + s.t.Fatal(err) + } + runCount := x.GetK8sLabel("tests.kluctl.io/runCount") + if runCount == nil || *runCount != fmt.Sprintf("%d", s.runCount) { + return + } + + s.t.Logf("handleConfigmap: op=%s, name=%s/%s, generation=%d, uid=%s", request.Operation, request.Namespace, request.Name, generation, uid) + + s.seenConfigMaps = append(s.seenConfigMaps, request.Name) +} + +func (s *hooksTestContext) clearSeenConfigmaps() { + s.m.Lock() + defer s.m.Unlock() + s.t.Logf("clearSeenConfigmaps: %v", s.seenConfigMaps) + s.seenConfigMaps = nil +} + +func (s *hooksTestContext) addHookConfigMap(dir string, opts resourceOpts, isHelm bool, hook string, hookDeletionPolicy string) { annotations := make(map[string]string) if isHelm { annotations["helm.sh/hook"] = hook @@ -34,223 +87,236 @@ func addHookDeployment(p *testProject, dir string, opts resourceOpts, isHelm boo annotations["kluctl.io/hook-deletion-policy"] = hookDeletionPolicy } - script := renderTemplateHelper(hookScript, map[string]interface{}{ - "name": opts.name, - "namespace": opts.namespace, - }) - opts.annotations = uo.CopyMergeStrMap(opts.annotations, annotations) - addJobDeployment(p, dir, opts, "bitnami/kubectl:1.21", []string{"sh"}, []string{"-c", script}) + s.addConfigMap(dir, opts) } -func addConfigMap(p *testProject, dir string, opts resourceOpts) { +func (s *hooksTestContext) addConfigMap(dir string, opts resourceOpts) { o := uo.New() o.SetK8sGVKs("", "v1", "ConfigMap") mergeMetadata(o, opts) o.SetNestedField(map[string]interface{}{}, "data") - p.addKustomizeResources(dir, []kustomizeResource{ - {fmt.Sprintf("%s.yml", opts.name), "", o}, + s.p.AddKustomizeResources(dir, []test_project.KustomizeResource{ + {Name: fmt.Sprintf("%s.yml", opts.name), Content: o}, }) } -func getHookResult(t *testing.T, p *testProject, k *test_utils.KindCluster, secretName string) *uo.UnstructuredObject { - o := k.KubectlYamlMust(t, "-n", p.projectName, "get", "secret", secretName) - s, ok, err := o.GetNestedString("data", "result") - if err != nil { - t.Fatal(err) - } - if !ok { - t.Fatalf("result not found") - } - b, err := base64.StdEncoding.DecodeString(s) - if err != nil { - t.Fatal(err) - } - r, err := uo.FromString(string(b)) - if err != nil { - t.Fatal(err) - } - return r -} +func prepareHookTestProject(t *testing.T, hook string, hookDeletionPolicy string, isHelm bool) *hooksTestContext { + s := prepareHookTestProjectBase(t) -func getHookResultCMNames(t *testing.T, p *testProject, k *test_utils.KindCluster, second bool) []string { - secretName := "hook-result" - if second { - secretName = "hook-result2" - } - o := getHookResult(t, p, k, secretName) - items, _, _ := o.GetNestedObjectList("items") - var names []string - for _, x := range items { - names = append(names, x.GetK8sName()) - } - return names -} + s.p.AddKustomizeDeployment("hook", nil, nil) -func assertHookResultCMName(t *testing.T, p *testProject, k *test_utils.KindCluster, second bool, cmName string) { - names := getHookResultCMNames(t, p, k, second) - for _, x := range names { - if x == cmName { - return - } - } - t.Fatalf("%s not found in hook result", cmName) -} + s.addConfigMap("hook", resourceOpts{name: "cm1", namespace: s.p.TestSlug()}) + s.addHookConfigMap("hook", resourceOpts{name: "hook1", namespace: s.p.TestSlug()}, isHelm, hook, hookDeletionPolicy) -func assertHookResultNotCMName(t *testing.T, p *testProject, k *test_utils.KindCluster, second bool, cmName string) { - names := getHookResultCMNames(t, p, k, second) - for _, x := range names { - if x == cmName { - t.Fatalf("%s found in hook result", cmName) - } - } + return s } -func prepareHookTestProject(t *testing.T, name string, hook string, hookDeletionPolicy string) (*testProject, *test_utils.KindCluster) { - isDone := false - namespace := fmt.Sprintf("hook-%s", name) - k := defaultKindCluster1 - p := &testProject{} - p.init(t, k, namespace) - defer func() { - if !isDone { - p.cleanup() - } - }() +func prepareHookTestProjectBase(t *testing.T) *hooksTestContext { + s := &hooksTestContext{ + t: t, + k: defaultCluster2, // use cluster2 as it has webhooks setup + p: test_project.NewTestProject(t), + } + s.setupWebhook() + t.Cleanup(func() { + s.removeWebhook() + }) - recreateNamespace(t, k, namespace) + createNamespace(s.t, s.k, s.p.TestSlug()) - p.updateTarget("test", nil) + s.p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField(s.k.Context, "context") + }) - addHookDeployment(p, "hook", resourceOpts{name: "hook", namespace: namespace}, false, hook, hookDeletionPolicy) - addConfigMap(p, "hook", resourceOpts{name: "cm1", namespace: namespace}) + return s +} - isDone = true - return p, k +func (s *hooksTestContext) incRunCount() { + s.runCount++ + s.t.Logf("incRunCount: %d", s.runCount) + s.p.UpdateDeploymentYaml(".", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(fmt.Sprintf("%d", s.runCount), "commonLabels", "tests.kluctl.io/runCount") + return nil + }) } -func ensureHookExecuted(t *testing.T, p *testProject, k *test_utils.KindCluster) { - _, _ = k.Kubectl("delete", "-n", p.projectName, "secret", "hook-result", "hook-result2") - p.KluctlMust("deploy", "--yes", "-t", "test") - assertResourceExists(t, k, p.projectName, "ConfigMap/cm1") +func (s *hooksTestContext) ensureHookExecuted(t *testing.T, expectedCms ...string) { + _, err := s.ensureHookExecuted2(t, 0, expectedCms...) + assert.NoError(s.t, err) } -func ensureHookNotExecuted(t *testing.T, p *testProject, k *test_utils.KindCluster) { - _, _ = k.Kubectl("delete", "-n", p.projectName, "secret", "hook-result", "hook-result2") - p.KluctlMust("deploy", "--yes", "-t", "test") - assertResourceNotExists(t, k, p.projectName, "Secret/hook-result") +func (s *hooksTestContext) ensureHookExecuted2(t *testing.T, timeout time.Duration, expectedCms ...string) (string, error) { + s.clearSeenConfigmaps() + s.incRunCount() + args := []string{"deploy", "--yes", "-t", "test"} + if timeout != 0 { + args = append(args, "--timeout", timeout.String()) + } + _, stderr, err := s.p.Kluctl(t, args...) + assert.Equal(s.t, expectedCms, s.seenConfigMaps) + return stderr, err } func TestHooksPreDeployInitial(t *testing.T) { t.Parallel() - p, k := prepareHookTestProject(t, "pre-deploy-initial", "pre-deploy-initial", "") - defer p.cleanup() - ensureHookExecuted(t, p, k) - assertHookResultNotCMName(t, p, k, false, "cm1") - ensureHookNotExecuted(t, p, k) + s := prepareHookTestProject(t, "pre-deploy-initial", "", false) + s.ensureHookExecuted(t, "hook1", "cm1") + s.ensureHookExecuted(t, "cm1") } func TestHooksPostDeployInitial(t *testing.T) { t.Parallel() - p, k := prepareHookTestProject(t, "post-deploy-initial", "post-deploy-initial", "") - defer p.cleanup() - ensureHookExecuted(t, p, k) - assertHookResultCMName(t, p, k, false, "cm1") - ensureHookNotExecuted(t, p, k) + s := prepareHookTestProject(t, "post-deploy-initial", "", false) + s.ensureHookExecuted(t, "cm1", "hook1") + s.ensureHookExecuted(t, "cm1") } func TestHooksPreDeployUpgrade(t *testing.T) { t.Parallel() - p, k := prepareHookTestProject(t, "pre-deploy-upgrade", "pre-deploy-upgrade", "") - defer p.cleanup() - addConfigMap(p, "hook", resourceOpts{name: "cm2", namespace: p.projectName}) - ensureHookNotExecuted(t, p, k) - k.KubectlMust(t, "delete", "-n", p.projectName, "configmap", "cm1") - ensureHookExecuted(t, p, k) - assertHookResultNotCMName(t, p, k, false, "cm1") - ensureHookExecuted(t, p, k) - assertHookResultCMName(t, p, k, false, "cm1") + s := prepareHookTestProject(t, "pre-deploy-upgrade", "", false) + s.ensureHookExecuted(t, "cm1") + s.ensureHookExecuted(t, "hook1", "cm1") + s.ensureHookExecuted(t, "hook1", "cm1") } func TestHooksPostDeployUpgrade(t *testing.T) { t.Parallel() - p, k := prepareHookTestProject(t, "post-deploy-upgrade", "post-deploy-upgrade", "") - defer p.cleanup() - addConfigMap(p, "hook", resourceOpts{name: "cm2", namespace: p.projectName}) - ensureHookNotExecuted(t, p, k) - k.KubectlMust(t, "delete", "-n", p.projectName, "configmap", "cm1") - ensureHookExecuted(t, p, k) - assertHookResultCMName(t, p, k, false, "cm1") -} - -func doTestHooksPreDeploy(t *testing.T, name string, hooks string) { - p, k := prepareHookTestProject(t, name, hooks, "") - defer p.cleanup() - addConfigMap(p, "hook", resourceOpts{name: "cm2", namespace: p.projectName}) - ensureHookExecuted(t, p, k) - k.KubectlMust(t, "delete", "-n", p.projectName, "configmap", "cm1") - ensureHookExecuted(t, p, k) - assertHookResultNotCMName(t, p, k, false, "cm1") - ensureHookExecuted(t, p, k) - assertHookResultCMName(t, p, k, false, "cm1") -} - -func doTestHooksPostDeploy(t *testing.T, name string, hooks string) { - p, k := prepareHookTestProject(t, name, hooks, "") - defer p.cleanup() - addConfigMap(p, "hook", resourceOpts{name: "cm2", namespace: p.projectName}) - ensureHookExecuted(t, p, k) - k.KubectlMust(t, "delete", "-n", p.projectName, "configmap", "cm1") - ensureHookExecuted(t, p, k) - assertHookResultCMName(t, p, k, false, "cm1") -} - -func doTestHooksPrePostDeploy(t *testing.T, name string, hooks string) { - p, k := prepareHookTestProject(t, name, hooks, "") - defer p.cleanup() - addConfigMap(p, "hook", resourceOpts{name: "cm2", namespace: p.projectName}) - ensureHookExecuted(t, p, k) - assertHookResultNotCMName(t, p, k, false, "cm1") - assertHookResultNotCMName(t, p, k, false, "cm2") - assertHookResultCMName(t, p, k, true, "cm1") - assertHookResultCMName(t, p, k, true, "cm2") - - ensureHookExecuted(t, p, k) - assertHookResultCMName(t, p, k, false, "cm1") - assertHookResultNotCMName(t, p, k, false, "cm2") - assertHookResultCMName(t, p, k, true, "cm1") - assertHookResultCMName(t, p, k, true, "cm2") + s := prepareHookTestProject(t, "post-deploy-upgrade", "", false) + s.ensureHookExecuted(t, "cm1") + s.ensureHookExecuted(t, "cm1", "hook1") + s.ensureHookExecuted(t, "cm1", "hook1") +} + +func doTestHooksPreDeploy(t *testing.T, hooks string) { + s := prepareHookTestProject(t, hooks, "", false) + s.ensureHookExecuted(t, "hook1", "cm1") + s.ensureHookExecuted(t, "hook1", "cm1") +} + +func doTestHooksPostDeploy(t *testing.T, hooks string) { + s := prepareHookTestProject(t, hooks, "", false) + s.ensureHookExecuted(t, "cm1", "hook1") + s.ensureHookExecuted(t, "cm1", "hook1") +} + +func doTestHooksPrePostDeploy(t *testing.T, hooks string) { + s := prepareHookTestProject(t, hooks, "", false) + s.ensureHookExecuted(t, "hook1", "cm1", "hook1") + s.ensureHookExecuted(t, "hook1", "cm1", "hook1") } func TestHooksPreDeploy(t *testing.T) { t.Parallel() - doTestHooksPreDeploy(t, "pre-deploy", "pre-deploy") + doTestHooksPreDeploy(t, "pre-deploy") } func TestHooksPreDeploy2(t *testing.T) { t.Parallel() // same as pre-deploy - doTestHooksPreDeploy(t, "pre-deploy2", "pre-deploy-initial,pre-deploy-upgrade") + doTestHooksPreDeploy(t, "pre-deploy-initial,pre-deploy-upgrade") } func TestHooksPostDeploy(t *testing.T) { t.Parallel() - doTestHooksPostDeploy(t, "post-deploy", "post-deploy") + doTestHooksPostDeploy(t, "post-deploy") } func TestHooksPostDeploy2(t *testing.T) { t.Parallel() // same as post-deploy - doTestHooksPostDeploy(t, "post-deploy2", "post-deploy-initial,post-deploy-upgrade") + doTestHooksPostDeploy(t, "post-deploy-initial,post-deploy-upgrade") } func TestHooksPrePostDeploy(t *testing.T) { t.Parallel() - doTestHooksPrePostDeploy(t, "pre-post-deploy", "pre-deploy,post-deploy") + doTestHooksPrePostDeploy(t, "pre-deploy,post-deploy") } func TestHooksPrePostDeploy2(t *testing.T) { t.Parallel() - doTestHooksPrePostDeploy(t, "pre-post-deploy2", "pre-deploy-initial,pre-deploy-upgrade,post-deploy-initial,post-deploy-upgrade") + doTestHooksPrePostDeploy(t, "pre-deploy-initial,pre-deploy-upgrade,post-deploy-initial,post-deploy-upgrade") +} + +func TestHooksPreDelete(t *testing.T) { + t.Parallel() + s := prepareHookTestProject(t, "pre-delete", "", true) + s.ensureHookExecuted(t, "cm1") // none is executed actually +} + +func TestHooksPostDelete(t *testing.T) { + t.Parallel() + s := prepareHookTestProject(t, "post-delete", "", true) + s.ensureHookExecuted(t, "cm1") // none is executed actually +} + +func TestHooksDeleteAndDeploy(t *testing.T) { + t.Parallel() + s := prepareHookTestProject(t, "post-delete,post-install", "", true) + s.ensureHookExecuted(t, "cm1", "hook1") +} + +func TestHooksPreRollback(t *testing.T) { + t.Parallel() + s := prepareHookTestProject(t, "pre-rollback", "", true) + s.ensureHookExecuted(t, "cm1") // none is executed actually +} + +func TestHooksPostRollback(t *testing.T) { + t.Parallel() + s := prepareHookTestProject(t, "post-rollback", "", true) + s.ensureHookExecuted(t, "cm1") // none is executed actually +} + +func TestHooksRollbackAndDeploy(t *testing.T) { + t.Parallel() + s := prepareHookTestProject(t, "post-rollback,post-install", "", true) + s.ensureHookExecuted(t, "cm1", "hook1") +} + +func TestHooksWait(t *testing.T) { + t.Parallel() + + s := prepareHookTestProjectBase(t) + + s.p.AddKustomizeDeployment("hook", nil, nil) + + s.addConfigMap("hook", resourceOpts{name: "cm1", namespace: s.p.TestSlug()}) + s.addHookConfigMap("hook", resourceOpts{name: "hook1", namespace: s.p.TestSlug()}, false, "post-deploy", "") + s.addHookConfigMap("hook", resourceOpts{name: "hook2", namespace: s.p.TestSlug(), annotations: map[string]string{ + "kluctl.io/is-ready": "false", + }}, false, "post-deploy", "") + s.addHookConfigMap("hook", resourceOpts{name: "hook3", namespace: s.p.TestSlug()}, false, "post-deploy", "") + + stderr, err := s.ensureHookExecuted2(t, 3*time.Second, "cm1", "hook1", "hook2") + if err == nil { + t.Fatal("err == nil, but it should have timed out") + } + assert.Contains(t, stderr, "context deadline") + + // we run a goroutine in the background that will wait for a few seconds and then annotate kluctl.io/is-ready=true, + // which will then cause the hook to get ready + go func() { + time.Sleep(10 * time.Second) + patchConfigMap(t, s.k, s.p.TestSlug(), "hook2", func(o *uo.UnstructuredObject) { + o.SetK8sAnnotation("kluctl.io/is-ready", "true") + }) + }() + + // hook2 appears twice because the patch from above causes the webhook to fire + stderr, err = s.ensureHookExecuted2(t, 20*time.Second, "cm1", "hook1", "hook2", "hook2", "hook3") + assert.NoError(t, err) + assert.Contains(t, stderr, "Still waiting") + + patchConfigMap(t, s.k, s.p.TestSlug(), "hook2", func(o *uo.UnstructuredObject) { + o.SetK8sAnnotation("kluctl.io/is-ready", "false") + }) + s.p.UpdateYaml("hook/hook2.yml", func(o *uo.UnstructuredObject) error { + o.SetK8sAnnotation("kluctl.io/hook-wait", "false") + return nil + }, "") + + _, err = s.ensureHookExecuted2(t, 5*time.Second, "cm1", "hook1", "hook2", "hook3") + assert.NoError(t, err) } diff --git a/e2e/images_test.go b/e2e/images_test.go new file mode 100644 index 000000000..de791657a --- /dev/null +++ b/e2e/images_test.go @@ -0,0 +1,239 @@ +package e2e + +import ( + "fmt" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/types" + "github.com/kluctl/kluctl/v2/pkg/utils" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/runtime/schema" + "testing" +) + +func addGetImageDeployment(p *test_project.TestProject, name string, containerName string, gi string) { + y := fmt.Sprintf(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: %s + namespace: %s + labels: + app: %s +spec: + replicas: 1 + selector: + matchLabels: + app: %s + template: + metadata: + labels: + app: %s + spec: + containers: + - name: %s + image: '%s' +`, name, p.TestSlug(), name, name, name, containerName, gi) + + p.AddKustomizeDeployment(name, []test_project.KustomizeResource{ + {Name: name, Content: uo.FromStringMust(y)}, + }, nil) +} + +func assertImage(t *testing.T, k *test_utils.EnvTestCluster, p *test_project.TestProject, deploymentName string, containerName string, expectedImage string) { + d := assertObjectExists(t, k, schema.GroupVersionResource{ + Group: "apps", + Version: "v1", + Resource: "deployments", + }, p.TestSlug(), deploymentName) + + l, _, _ := d.GetNestedObjectList("spec", "template", "spec", "containers") + for _, x := range l { + n, _, _ := x.GetNestedString("name") + if n != containerName { + continue + } + image, _, _ := x.GetNestedString("image") + assert.Equal(t, expectedImage, image) + return + } + + assert.Fail(t, fmt.Sprintf("container %s not found", containerName)) +} + +func TestGetImageNotFound(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + addGetImageDeployment(p, "d1", "c1", `{{ images.get_image("i1") }}`) + + _, _, err := p.Kluctl(t, "deploy", "-y", "-t", "test") + assert.ErrorContains(t, err, "failed to find fixed image for i1") +} + +func TestGetImageArg(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + addGetImageDeployment(p, "d1", "c1", `{{ images.get_image("i1") }}`) + + p.KluctlMust(t, "deploy", "-y", "-t", "test", "--fixed-image", "i1=i1:arg") + assertImage(t, k, p, "d1", "c1", "i1:arg") +} + +func setImagesVars(p *test_project.TestProject, fis []types.FixedImage) { + vars := []types.VarsSource{ + { + Values: uo.FromMap(map[string]interface{}{ + "images": fis, + }), + }, + } + + p.UpdateDeploymentYaml(".", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(vars, "vars") + return nil + }) +} + +func TestGetImageVars(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + setImagesVars(p, []types.FixedImage{ + {Image: utils.Ptr("i1"), ResultImage: "i1:vars"}, + }) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + addGetImageDeployment(p, "d1", "c1", `{{ images.get_image("i1") }}`) + + p.KluctlMust(t, "deploy", "-y", "-t", "test") + assertImage(t, k, p, "d1", "c1", "i1:vars") +} + +func TestGetImageMixed(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + setImagesVars(p, []types.FixedImage{ + {Image: utils.Ptr("i2"), ResultImage: "i2:vars"}, + }) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + addGetImageDeployment(p, "d1", "c1", `{{ images.get_image("i1") }}`) + addGetImageDeployment(p, "d2", "c2", `{{ images.get_image("i2") }}`) + + p.KluctlMust(t, "deploy", "-y", "-t", "test", "--fixed-image", "i1=i1:arg") + assertImage(t, k, p, "d1", "c1", "i1:arg") + assertImage(t, k, p, "d2", "c2", "i2:vars") +} + +func TestGetImageByDeployment(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + setImagesVars(p, []types.FixedImage{ + {Image: utils.Ptr("i1"), ResultImage: "i1:vars1", Deployment: utils.Ptr("Deployment/d1")}, + {Image: utils.Ptr("i1"), ResultImage: "i1:vars2", Deployment: utils.Ptr("Deployment/d2")}, + }) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + addGetImageDeployment(p, "d1", "c1", `{{ images.get_image("i1") }}`) + addGetImageDeployment(p, "d2", "c2", `{{ images.get_image("i1") }}`) + + p.KluctlMust(t, "deploy", "-y", "-t", "test") + assertImage(t, k, p, "d1", "c1", "i1:vars1") + assertImage(t, k, p, "d2", "c2", "i1:vars2") +} + +func TestGetImageByContainer(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + setImagesVars(p, []types.FixedImage{ + {Image: utils.Ptr("i1"), ResultImage: "i1:vars1", Container: utils.Ptr("c1")}, + {Image: utils.Ptr("i1"), ResultImage: "i1:vars2", Container: utils.Ptr("c2")}, + }) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + addGetImageDeployment(p, "d1", "c1", `{{ images.get_image("i1") }}`) + addGetImageDeployment(p, "d2", "c2", `{{ images.get_image("i1") }}`) + + p.KluctlMust(t, "deploy", "-y", "-t", "test") + assertImage(t, k, p, "d1", "c1", "i1:vars1") + assertImage(t, k, p, "d2", "c2", "i1:vars2") +} + +func TestGetImageRegex(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + setImagesVars(p, []types.FixedImage{ + {ImageRegex: utils.Ptr("i.*"), ResultImage: "i1:x"}, + {ImageRegex: utils.Ptr("j.*"), ResultImage: "i1:y"}, + }) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + addGetImageDeployment(p, "d1", "c1", `{{ images.get_image("i1") }}`) + addGetImageDeployment(p, "d2", "c2", `{{ images.get_image("i2") }}`) + addGetImageDeployment(p, "d3", "c1", `{{ images.get_image("j1") }}`) + addGetImageDeployment(p, "d4", "c2", `{{ images.get_image("j2") }}`) + + p.KluctlMust(t, "deploy", "-y", "-t", "test") + assertImage(t, k, p, "d1", "c1", "i1:x") + assertImage(t, k, p, "d2", "c2", "i1:x") + assertImage(t, k, p, "d3", "c1", "i1:y") + assertImage(t, k, p, "d4", "c2", "i1:y") +} diff --git a/e2e/inclusion_test.go b/e2e/inclusion_test.go index 0395bff9f..3a9303e34 100644 --- a/e2e/inclusion_test.go +++ b/e2e/inclusion_test.go @@ -1,55 +1,48 @@ package e2e import ( - "fmt" - "github.com/kluctl/kluctl/v2/internal/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + corev1 "k8s.io/api/core/v1" "path/filepath" "reflect" "testing" ) -func prepareInclusionTestProject(t *testing.T, namespace string, withIncludes bool) (*testProject, *test_utils.KindCluster) { - isDone := false +func prepareInclusionTestProject(t *testing.T, withIncludes bool) (*test_project.TestProject, *test_utils.EnvTestCluster) { + k := defaultCluster1 + p := test_project.NewTestProject(t) - k := defaultKindCluster1 - p := &testProject{} - p.init(t, k, namespace) - defer func() { - if !isDone { - p.cleanup() - } - }() - - recreateNamespace(t, k, p.projectName) + createNamespace(t, k, p.TestSlug()) - p.updateTarget("test", nil) + p.UpdateTarget("test", func(target *uo.UnstructuredObject) {}) - addConfigMapDeployment(p, "cm1", nil, resourceOpts{name: "cm1", namespace: p.projectName}) - addConfigMapDeployment(p, "cm2", nil, resourceOpts{name: "cm2", namespace: p.projectName}) - addConfigMapDeployment(p, "cm3", nil, resourceOpts{name: "cm3", namespace: p.projectName, tags: []string{"tag1", "tag2"}}) - addConfigMapDeployment(p, "cm4", nil, resourceOpts{name: "cm4", namespace: p.projectName, tags: []string{"tag1", "tag3"}}) - addConfigMapDeployment(p, "cm5", nil, resourceOpts{name: "cm5", namespace: p.projectName, tags: []string{"tag1", "tag4"}}) - addConfigMapDeployment(p, "cm6", nil, resourceOpts{name: "cm6", namespace: p.projectName, tags: []string{"tag1", "tag5"}}) - addConfigMapDeployment(p, "cm7", nil, resourceOpts{name: "cm7", namespace: p.projectName, tags: []string{"tag1", "tag6"}}) + addConfigMapDeployment(p, "cm1", nil, resourceOpts{name: "cm1", namespace: p.TestSlug()}) + addConfigMapDeployment(p, "cm2", nil, resourceOpts{name: "cm2", namespace: p.TestSlug()}) + addConfigMapDeployment(p, "cm3", nil, resourceOpts{name: "cm3", namespace: p.TestSlug(), tags: []string{"tag1", "tag2"}}) + addConfigMapDeployment(p, "cm4", nil, resourceOpts{name: "cm4", namespace: p.TestSlug(), tags: []string{"tag1", "tag3"}}) + addConfigMapDeployment(p, "cm5", nil, resourceOpts{name: "cm5", namespace: p.TestSlug(), tags: []string{"tag1", "tag4"}}) + addConfigMapDeployment(p, "cm6", nil, resourceOpts{name: "cm6", namespace: p.TestSlug(), tags: []string{"tag1", "tag5"}}) + addConfigMapDeployment(p, "cm7", nil, resourceOpts{name: "cm7", namespace: p.TestSlug(), tags: []string{"tag1", "tag6"}}) if withIncludes { - p.addDeploymentInclude(".", "include1", nil) - addConfigMapDeployment(p, "include1/icm1", nil, resourceOpts{name: "icm1", namespace: p.projectName, tags: []string{"itag1", "itag2"}}) + p.AddDeploymentInclude(".", "include1", nil) + addConfigMapDeployment(p, "include1/icm1", nil, resourceOpts{name: "icm1", namespace: p.TestSlug(), tags: []string{"itag1", "itag2"}}) - p.addDeploymentInclude(".", "include2", nil) - addConfigMapDeployment(p, "include2/icm2", nil, resourceOpts{name: "icm2", namespace: p.projectName}) - addConfigMapDeployment(p, "include2/icm3", nil, resourceOpts{name: "icm3", namespace: p.projectName, tags: []string{"itag3", "itag4"}}) + p.AddDeploymentInclude(".", "include2", nil) + addConfigMapDeployment(p, "include2/icm2", nil, resourceOpts{name: "icm2", namespace: p.TestSlug()}) + addConfigMapDeployment(p, "include2/icm3", nil, resourceOpts{name: "icm3", namespace: p.TestSlug(), tags: []string{"itag3", "itag4"}}) - p.addDeploymentInclude(".", "include3", []string{"itag5"}) - addConfigMapDeployment(p, "include3/icm4", nil, resourceOpts{name: "icm4", namespace: p.projectName}) - addConfigMapDeployment(p, "include3/icm5", nil, resourceOpts{name: "icm5", namespace: p.projectName, tags: []string{"itag5", "itag6"}}) + p.AddDeploymentInclude(".", "include3", []string{"itag5"}) + addConfigMapDeployment(p, "include3/icm4", nil, resourceOpts{name: "icm4", namespace: p.TestSlug()}) + addConfigMapDeployment(p, "include3/icm5", nil, resourceOpts{name: "icm5", namespace: p.TestSlug(), tags: []string{"itag5", "itag6"}}) } - isDone = true return p, k } -func assertExistsHelper(t *testing.T, p *testProject, k *test_utils.KindCluster, shouldExists map[string]bool, add []string, remove []string) { +func assertExistsHelper(t *testing.T, p *test_project.TestProject, k *test_utils.EnvTestCluster, shouldExists map[string]bool, add []string, remove []string) { for _, x := range add { shouldExists[x] = true } @@ -58,8 +51,10 @@ func assertExistsHelper(t *testing.T, p *testProject, k *test_utils.KindCluster, delete(shouldExists, x) } } - exists := k.KubectlYamlMust(t, "-n", p.projectName, "get", "configmaps", "-l", fmt.Sprintf("project_name=%s", p.projectName)) - items, _, _ := exists.GetNestedObjectList("items") + items, err := k.List(corev1.SchemeGroupVersion.WithResource("configmaps"), p.TestSlug(), map[string]string{"project_name": p.TestSlug()}) + if err != nil { + t.Fatal(err) + } found := make(map[string]bool) for _, x := range items { found[x.GetK8sName()] = true @@ -71,8 +66,7 @@ func assertExistsHelper(t *testing.T, p *testProject, k *test_utils.KindCluster, func TestInclusionTags(t *testing.T) { t.Parallel() - p, k := prepareInclusionTestProject(t, "inclusion-tags", false) - defer p.cleanup() + p, k := prepareInclusionTestProject(t, false) shouldExists := make(map[string]bool) doAssertExists := func(add ...string) { @@ -82,32 +76,31 @@ func TestInclusionTags(t *testing.T) { doAssertExists() // test default tags - p.KluctlMust("deploy", "--yes", "-t", "test", "-I", "cm1") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-I", "cm1") doAssertExists("cm1") - p.KluctlMust("deploy", "--yes", "-t", "test", "-I", "cm2") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-I", "cm2") doAssertExists("cm2") // cm3/cm4 don't have default tags, so they should not deploy - p.KluctlMust("deploy", "--yes", "-t", "test", "-I", "cm3") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-I", "cm3") doAssertExists() // but with tag2, at least cm3 should deploy - p.KluctlMust("deploy", "--yes", "-t", "test", "-I", "tag2") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-I", "tag2") doAssertExists("cm3") // let's try 2 tags at once - p.KluctlMust("deploy", "--yes", "-t", "test", "-I", "tag3", "-I", "tag4") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-I", "tag3", "-I", "tag4") doAssertExists("cm4", "cm5") // And now let's try a tag that matches all non-default ones - p.KluctlMust("deploy", "--yes", "-t", "test", "-I", "tag3", "-I", "tag1") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-I", "tag3", "-I", "tag1") doAssertExists("cm6", "cm7") } func TestExclusionTags(t *testing.T) { t.Parallel() - p, k := prepareInclusionTestProject(t, "inclusion-exclusion", false) - defer p.cleanup() + p, k := prepareInclusionTestProject(t, false) shouldExists := make(map[string]bool) doAssertExists := func(add ...string) { @@ -117,22 +110,21 @@ func TestExclusionTags(t *testing.T) { doAssertExists() // Exclude everything except cm1 - p.KluctlMust("deploy", "--yes", "-t", "test", "-E", "cm2", "-E", "tag1") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-E", "cm2", "-E", "tag1") doAssertExists("cm1") // Test that exclusion has precedence over inclusion - p.KluctlMust("deploy", "--yes", "-t", "test", "-E", "cm2", "-E", "tag1", "-I", "cm2") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-E", "cm2", "-E", "tag1", "-I", "cm2") doAssertExists() // Test that exclusion has precedence over inclusion - p.KluctlMust("deploy", "--yes", "-t", "test", "-I", "tag1", "-E", "tag6") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-I", "tag1", "-E", "tag6") doAssertExists("cm3", "cm4", "cm5", "cm6") } func TestInclusionIncludeDirs(t *testing.T) { t.Parallel() - p, k := prepareInclusionTestProject(t, "inclusion-dirs", true) - defer p.cleanup() + p, k := prepareInclusionTestProject(t, true) shouldExists := make(map[string]bool) doAssertExists := func(add ...string) { @@ -141,20 +133,19 @@ func TestInclusionIncludeDirs(t *testing.T) { doAssertExists() - p.KluctlMust("deploy", "--yes", "-t", "test", "-I", "itag1") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-I", "itag1") doAssertExists("icm1") - p.KluctlMust("deploy", "--yes", "-t", "test", "-I", "include2") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-I", "include2") doAssertExists("icm2", "icm3") - p.KluctlMust("deploy", "--yes", "-t", "test", "-I", "itag5") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-I", "itag5") doAssertExists("icm4", "icm5") } func TestInclusionDeploymentDirs(t *testing.T) { t.Parallel() - p, k := prepareInclusionTestProject(t, "inclusion-kustomize-dirs", true) - defer p.cleanup() + p, k := prepareInclusionTestProject(t, true) shouldExists := make(map[string]bool) doAssertExists := func(add ...string) { @@ -163,15 +154,15 @@ func TestInclusionDeploymentDirs(t *testing.T) { doAssertExists() - p.KluctlMust("deploy", "--yes", "-t", "test", "--include-deployment-dir", "include1/icm1") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "--include-deployment-dir", "include1/icm1") doAssertExists("icm1") - p.KluctlMust("deploy", "--yes", "-t", "test", "--include-deployment-dir", "include2/icm3") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "--include-deployment-dir", "include2/icm3") doAssertExists("icm3") - p.KluctlMust("deploy", "--yes", "-t", "test", "--exclude-deployment-dir", "include3/icm5") + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "--exclude-deployment-dir", "include3/icm5") var a []string - for _, x := range p.listDeploymentItemPathes(".", false) { + for _, x := range p.ListDeploymentItemPathes(".", false) { if x != "icm5" { a = append(a, filepath.Base(x)) } @@ -181,8 +172,7 @@ func TestInclusionDeploymentDirs(t *testing.T) { func TestInclusionPrune(t *testing.T) { t.Parallel() - p, k := prepareInclusionTestProject(t, "inclusion-prune", false) - defer p.cleanup() + p, k := prepareInclusionTestProject(t, false) shouldExists := make(map[string]bool) doAssertExists := func(add []string, remove []string) { @@ -191,50 +181,49 @@ func TestInclusionPrune(t *testing.T) { doAssertExists(nil, nil) - p.KluctlMust("deploy", "--yes", "-t", "test") - doAssertExists(p.listDeploymentItemPathes(".", false), nil) + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + doAssertExists(p.ListDeploymentItemPathes(".", false), nil) - p.deleteKustomizeDeployment("cm1") - p.KluctlMust("prune", "--yes", "-t", "test", "-I", "non-existent-tag") + p.DeleteKustomizeDeployment("cm1") + p.KluctlMust(t, "prune", "--yes", "-t", "test", "-I", "non-existent-tag") doAssertExists(nil, nil) - p.KluctlMust("prune", "--yes", "-t", "test", "-I", "cm1") + p.KluctlMust(t, "prune", "--yes", "-t", "test", "-I", "cm1") doAssertExists(nil, []string{"cm1"}) - p.deleteKustomizeDeployment("cm2") - p.KluctlMust("prune", "--yes", "-t", "test", "-E", "cm2") + p.DeleteKustomizeDeployment("cm2") + p.KluctlMust(t, "prune", "--yes", "-t", "test", "-E", "cm2") doAssertExists(nil, nil) - p.deleteKustomizeDeployment("cm3") - p.KluctlMust("prune", "--yes", "-t", "test", "--exclude-deployment-dir", "cm3") + p.DeleteKustomizeDeployment("cm3") + p.KluctlMust(t, "prune", "--yes", "-t", "test", "--exclude-deployment-dir", "cm3") doAssertExists(nil, []string{"cm2"}) - p.KluctlMust("prune", "--yes", "-t", "test") + p.KluctlMust(t, "prune", "--yes", "-t", "test") doAssertExists(nil, []string{"cm3"}) } func TestInclusionDelete(t *testing.T) { t.Parallel() - p, k := prepareInclusionTestProject(t, "inclusion-delete", false) - defer p.cleanup() + p, k := prepareInclusionTestProject(t, false) shouldExists := make(map[string]bool) doAssertExists := func(add []string, remove []string) { assertExistsHelper(t, p, k, shouldExists, add, remove) } - p.KluctlMust("deploy", "--yes", "-t", "test") - doAssertExists(p.listDeploymentItemPathes(".", false), nil) + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + doAssertExists(p.ListDeploymentItemPathes(".", false), nil) - p.KluctlMust("delete", "--yes", "-t", "test", "-I", "non-existent-tag") + p.KluctlMust(t, "delete", "--yes", "-t", "test", "-I", "non-existent-tag") doAssertExists(nil, nil) - p.KluctlMust("delete", "--yes", "-t", "test", "-I", "cm1") + p.KluctlMust(t, "delete", "--yes", "-t", "test", "-I", "cm1") doAssertExists(nil, []string{"cm1"}) - p.KluctlMust("delete", "--yes", "-t", "test", "-E", "cm2") + p.KluctlMust(t, "delete", "--yes", "-t", "test", "-E", "cm2") var a []string - for _, x := range p.listDeploymentItemPathes(".", false) { + for _, x := range p.ListDeploymentItemPathes(".", false) { if x != "cm1" && x != "cm2" { a = append(a, x) } diff --git a/e2e/library_test.go b/e2e/library_test.go new file mode 100644 index 000000000..e0c3bab32 --- /dev/null +++ b/e2e/library_test.go @@ -0,0 +1,142 @@ +package e2e + +import ( + "fmt" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/types" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "testing" +) + +func prepareLibraryProject(t *testing.T, prefix string, subDir string, cmData map[string]string, args []types.DeploymentArg) *test_project.TestProject { + p := test_project.NewTestProject(t, + test_project.WithGitSubDir(subDir), + test_project.WithRepoName(fmt.Sprintf("repos/%s", prefix)), + ) + addConfigMapDeployment(p, "cm", cmData, resourceOpts{ + name: fmt.Sprintf("%s-cm", prefix), + namespace: p.TestSlug(), + }) + p.UpdateYaml(".kluctl-library.yaml", func(o *uo.UnstructuredObject) error { + *o = *uo.FromMap(map[string]interface{}{ + "args": args, + }) + return nil + }, "") + return p +} + +func TestLibraryIncludePassVars(t *testing.T) { + k := defaultCluster1 + + p := test_project.NewTestProject(t) + ip1 := prepareLibraryProject(t, "lib1", "", map[string]string{ + "a": `{{ get_var("k1", "na") }}`, + }, []types.DeploymentArg{}) + ip2 := prepareLibraryProject(t, "lib2", "subDir", map[string]string{ + "a": `{{ get_var("k2", "na") }}`, + }, []types.DeploymentArg{}) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) {}) + + // add vars to deployment.yaml + p.UpdateDeploymentYaml("", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField([]types.VarsSource{ + { + Values: uo.FromMap(map[string]interface{}{ + "k1": "v1", + "k2": "v2", + }), + }, + }, "vars") + return nil + }) + + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip1.GitUrl(), + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip2.GitUrl(), + "subDir": "subDir", + }, + })) + + // ensure vars were not passed + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm1 := assertConfigMapExists(t, k, p.TestSlug(), "lib1-cm") + cm2 := assertConfigMapExists(t, k, p.TestSlug(), "lib2-cm") + assertNestedFieldEquals(t, cm1, "na", "data", "a") + assertNestedFieldEquals(t, cm2, "na", "data", "a") + + // now let it pass vars to lib1 + p.UpdateDeploymentItems("", func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + _ = items[0].SetNestedField(true, "passVars") + return items + }) + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm1 = assertConfigMapExists(t, k, p.TestSlug(), "lib1-cm") + cm2 = assertConfigMapExists(t, k, p.TestSlug(), "lib2-cm") + assertNestedFieldEquals(t, cm1, "v1", "data", "a") + assertNestedFieldEquals(t, cm2, "na", "data", "a") + + // now remove .kluctl-library.yaml and let it auto-pass variables + ip2.DeleteFile(".kluctl-library.yaml", "") + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm1 = assertConfigMapExists(t, k, p.TestSlug(), "lib1-cm") + cm2 = assertConfigMapExists(t, k, p.TestSlug(), "lib2-cm") + assertNestedFieldEquals(t, cm1, "v1", "data", "a") + assertNestedFieldEquals(t, cm2, "v2", "data", "a") +} + +func TestLibraryIncludeArgs(t *testing.T) { + k := defaultCluster1 + + p := test_project.NewTestProject(t) + ip1 := prepareLibraryProject(t, "lib1", "", map[string]string{ + "a": `{{ get_var("args.a", "na") }}`, + "b": `{{ get_var("args.b", "na") }}`, + }, []types.DeploymentArg{ + { + Name: "a", + Default: &apiextensionsv1.JSON{Raw: []byte(`"def"`)}, + }, + { + Name: "b", + }, + }) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) {}) + + // no args passed + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip1.GitUrl(), + }, + })) + + // ensure vars were not passed + _, _, err := p.Kluctl(t, "deploy", "--yes", "-t", "test") + assert.ErrorContains(t, err, "required argument b not set") + + // pass args + p.UpdateDeploymentItems("", func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + _ = items[0].SetNestedField(uo.FromMap(map[string]interface{}{ + "b": "b", + }), "args") + return items + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm1 := assertConfigMapExists(t, k, p.TestSlug(), "lib1-cm") + assertNestedFieldEquals(t, cm1, "def", "data", "a") + assertNestedFieldEquals(t, cm1, "b", "data", "b") +} diff --git a/e2e/main_test.go b/e2e/main_test.go new file mode 100644 index 000000000..a91610b71 --- /dev/null +++ b/e2e/main_test.go @@ -0,0 +1,52 @@ +package e2e + +import ( + "github.com/kluctl/kluctl/v2/cmd/kluctl/commands" + "os" + "testing" +) + +func isCallKluctl() bool { + return os.Getenv("CALL_KLUCTL") == "true" +} + +func TestMain(m *testing.M) { + // We use the TestMail to run kluctl even though the test executable was invoked + // This is clearly a hack, but it avoids the requirement to have a kluctl executable pre-built + if isCallKluctl() { + commands.Main() + os.Exit(0) + } + + tmpFile1, err := os.CreateTemp("", "") + if err != nil { + panic(err) + } + tmpFile2, err := os.CreateTemp("", "") + if err != nil { + panic(err) + } + tmpFile2.Close() + defer func() { + os.Remove(tmpFile2.Name()) + }() + tmpDir1, err := os.MkdirTemp("", "") + if err != nil { + panic(err) + } + defer func() { + os.RemoveAll(tmpDir1) + }() + tmpDir2, err := os.MkdirTemp("", "") + if err != nil { + panic(err) + } + defer func() { + os.RemoveAll(tmpDir2) + }() + os.Setenv("HELM_REGISTRY_CONFIG", tmpFile1.Name()) + os.Setenv("HELM_REPOSITORY_CONFIG", tmpFile2.Name()) + os.Setenv("HELM_REPOSITORY_CACHE", tmpDir1) + os.Setenv("HELM_PLUGINS", tmpDir2) + os.Exit(m.Run()) +} diff --git a/e2e/no_target_test.go b/e2e/no_target_test.go new file mode 100644 index 000000000..17c83f832 --- /dev/null +++ b/e2e/no_target_test.go @@ -0,0 +1,81 @@ +package e2e + +import ( + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +func prepareNoTargetTest(t *testing.T, withDeploymentYaml bool) *test_project.TestProject { + p := test_project.NewTestProject(t) + + cm := createConfigMapObject(map[string]string{ + "targetName": `{{ target.name }}`, + "targetContext": `{{ target.context }}`, + }, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + + if withDeploymentYaml { + p.AddKustomizeDeployment("cm", []test_project.KustomizeResource{{Name: "cm.yaml", Content: cm}}, nil) + } else { + p.AddKustomizeResources("", []test_project.KustomizeResource{{Name: "cm.yaml", Content: cm}}) + err := os.Remove(filepath.Join(p.LocalProjectDir(), "deployment.yml")) + assert.NoError(t, err) + } + + return p +} + +func testNoTarget(t *testing.T, withDeploymentYaml bool) { + t.Parallel() + + p := prepareNoTargetTest(t, withDeploymentYaml) + createNamespace(t, defaultCluster1, p.TestSlug()) + createNamespace(t, defaultCluster2, p.TestSlug()) + + p.KluctlMust(t, "deploy", "--yes") + cm := assertConfigMapExists(t, defaultCluster1, p.TestSlug(), "cm") + assertConfigMapNotExists(t, defaultCluster2, p.TestSlug(), "cm") + assert.Equal(t, map[string]any{ + "targetName": "", + "targetContext": defaultCluster1.Context, + }, cm.Object["data"]) + + p.KluctlMust(t, "deploy", "--yes", "-T", "override-name") + cm = assertConfigMapExists(t, defaultCluster1, p.TestSlug(), "cm") + assert.Equal(t, map[string]any{ + "targetName": "override-name", + "targetContext": defaultCluster1.Context, + }, cm.Object["data"]) + + p.KluctlMust(t, "deploy", "--yes", "-T", "override-name", "--context", defaultCluster2.Context) + cm = assertConfigMapExists(t, defaultCluster2, p.TestSlug(), "cm") + assert.Equal(t, map[string]any{ + "targetName": "override-name", + "targetContext": defaultCluster2.Context, + }, cm.Object["data"]) +} + +func TestNoTarget(t *testing.T) { + testNoTarget(t, true) +} + +func TestNoTargetNoDeployment(t *testing.T) { + testNoTarget(t, false) +} + +func TestNoTargetWithTargetsInProject(t *testing.T) { + t.Parallel() + + p := prepareNoTargetTest(t, true) + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + _, _, err := p.Kluctl(t, "deploy", "--yes") + assert.ErrorContains(t, err, "a target must be explicitly selected when targets are defined in the Kluctl project") +} diff --git a/e2e/obfuscate_test.go b/e2e/obfuscate_test.go new file mode 100644 index 000000000..5c6e73e61 --- /dev/null +++ b/e2e/obfuscate_test.go @@ -0,0 +1,95 @@ +package e2e + +import ( + "encoding/base64" + test_utils "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestObfuscateSecrets(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_utils.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addSecretDeployment(p, "secret", map[string]string{ + "secret": "secret_value_1", + }, resourceOpts{ + name: "secret", + namespace: p.TestSlug(), + }) + addSecretDeployment(p, "secret2", nil, resourceOpts{ + name: "secret2", + namespace: p.TestSlug(), + }) + addSecretDeployment(p, "secret3", nil, resourceOpts{ + name: "secret3", + namespace: p.TestSlug(), + }) + + stdout, _ := p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertSecretExists(t, k, p.TestSlug(), "secret") + assert.NotContains(t, stdout, base64.StdEncoding.EncodeToString([]byte("secret_value"))) + + p.UpdateYaml("secret/secret-secret.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("secret_value_2", "stringData", "secret") + return nil + }, "") + stdout, _ = p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assert.NotContains(t, stdout, base64.StdEncoding.EncodeToString([]byte("secret_value"))) + assert.Contains(t, stdout, "***** (obfuscated)") + + p.UpdateYaml("secret/secret-secret.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("secret_value_3", "stringData", "secret") + return nil + }, "") + stdout, _ = p.KluctlMust(t, "deploy", "--yes", "-t", "test", "--no-obfuscate") + assert.Contains(t, stdout, "-"+base64.StdEncoding.EncodeToString([]byte("secret_value_2"))) + assert.Contains(t, stdout, "+"+base64.StdEncoding.EncodeToString([]byte("secret_value_3"))) + assert.NotContains(t, stdout, "***** (obfuscated)") + + // also test changing from empty data to filled data + p.UpdateYaml("secret2/secret-secret2.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(map[string]any{ + "secret2": "secret_value_2", + }, "stringData") + return nil + }, "") + + stdout, _ = p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assert.NotContains(t, stdout, base64.StdEncoding.EncodeToString([]byte("secret_value_2"))) + assert.Contains(t, stdout, "+secret2: '***** (obfuscated)'") + + p.UpdateYaml("secret3/secret-secret3.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(map[string]any{ + "secret3": "secret_value_3", + "secret4": "secret_value_4", + }, "stringData") + return nil + }, "") + stdout, _ = p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assert.NotContains(t, stdout, base64.StdEncoding.EncodeToString([]byte("secret_value_3"))) + assert.NotContains(t, stdout, base64.StdEncoding.EncodeToString([]byte("secret_value_4"))) + assert.Contains(t, stdout, "+secret3: '***** (obfuscated)'") + assert.Contains(t, stdout, "+secret4: '***** (obfuscated)'") + + p.UpdateYaml("secret3/secret-secret3.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(map[string]any{ + "secret.dot1": "secret_value_5", + "secret.dot2": "secret_value_6", + }, "stringData") + return nil + }, "") + stdout, _ = p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assert.NotContains(t, stdout, base64.StdEncoding.EncodeToString([]byte("secret_value_5"))) + assert.NotContains(t, stdout, base64.StdEncoding.EncodeToString([]byte("secret_value_6"))) + assert.Contains(t, stdout, "data[\"secret.dot1\"] | +***** (obfuscated)") + assert.Contains(t, stdout, "data[\"secret.dot2\"] | +***** (obfuscated)") +} diff --git a/e2e/oci_include_test.go b/e2e/oci_include_test.go new file mode 100644 index 000000000..0b738e02d --- /dev/null +++ b/e2e/oci_include_test.go @@ -0,0 +1,229 @@ +package e2e + +import ( + "fmt" + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/config/types" + "github.com/kluctl/kluctl/lib/yaml" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +func TestOciIncludeMultipleRepos(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + ip1 := prepareIncludeProject(t, "include1", "", nil) + ip2 := prepareIncludeProject(t, "include2", "subDir", nil) + + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Oci, "", nil) + repo.Start(t) + + repo1 := repo.URL.String() + "/org1/repo1" + repo2 := repo.URL.String() + "/org2/repo2" + + ip1.KluctlMust(t, "oci", "push", "--url", repo1) + ip2.KluctlMust(t, "oci", "push", "--url", repo2, "--project-dir", ip2.LocalWorkDir()) + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) {}) + + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "oci": map[string]any{ + "url": repo1, + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "oci": map[string]any{ + "url": repo2, + "subDir": "subDir", + }, + })) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "include1-cm") + assertConfigMapExists(t, k, p.TestSlug(), "include2-cm") +} + +func TestOciIncludeWithCreds(t *testing.T) { + k := defaultCluster1 + + ip1 := prepareIncludeProject(t, "include1", "", nil) + ip2 := prepareIncludeProject(t, "include2", "subDir", nil) + ip3 := prepareIncludeProject(t, "include3", "", nil) + + createRepo := func(user, pass string) *test_utils.TestHelmRepo { + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Oci, "", nil) + repo.HttpServer.Username = user + repo.HttpServer.Password = pass + repo.Start(t) + return repo + } + repo1 := createRepo("user1", "pass1") + repo2 := createRepo("user2", "pass2") + repo3 := createRepo("user3", "pass3") + repoUrl1 := repo1.URL.String() + "/org1/repo1" + repoUrl2 := repo2.URL.String() + "/org2/repo2" + repoUrl3 := repo3.URL.String() + "/org3/repo3" + + // push with no creds + _, _, err := ip1.Kluctl(t, "oci", "push", "--url", repoUrl1) + assert.ErrorContains(t, err, "401 Unauthorized") + + // push with invalid creds + _, _, err = ip1.Kluctl(t, "oci", "push", "--url", repoUrl1, "--registry-creds", fmt.Sprintf("%s=user1:invalid", repo1.URL.Host)) + assert.ErrorContains(t, err, "401 Unauthorized") + + // now with valid creds + ip1.KluctlMust(t, "oci", "push", "--url", repoUrl1, "--registry-creds", fmt.Sprintf("%s=user1:pass1", repo1.URL.Host)) + ip2.KluctlMust(t, "oci", "push", "--url", repoUrl2, "--project-dir", ip2.LocalWorkDir(), "--registry-creds", fmt.Sprintf("%s=user2:pass2", repo2.URL.Host)) + ip3.KluctlMust(t, "oci", "push", "--url", repoUrl3, "--registry-creds", fmt.Sprintf("%s=user3:pass3", repo3.URL.Host)) + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) {}) + + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "oci": map[string]any{ + "url": repoUrl1, + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "oci": map[string]any{ + "url": repoUrl2, + "subDir": "subDir", + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "oci": map[string]any{ + "url": repoUrl3, + }, + })) + + // deploy with no auth + _, _, err = p.Kluctl(t, "deploy", "--yes", "-t", "test") + assert.ErrorContains(t, err, "401 Unauthorized") + + // deploy with some invalid creds + t.Setenv("KLUCTL_REGISTRY_1_HOST", repo1.URL.Host) + t.Setenv("KLUCTL_REGISTRY_1_REPOSITORY", "org1/repo1") + t.Setenv("KLUCTL_REGISTRY_1_USERNAME", "user1") + t.Setenv("KLUCTL_REGISTRY_1_PASSWORD", "pass1") + t.Setenv("KLUCTL_REGISTRY_2_REPOSITORY", fmt.Sprintf("%s/org2/repo2", repo2.URL.Host)) + t.Setenv("KLUCTL_REGISTRY_2_USERNAME", "user2") + t.Setenv("KLUCTL_REGISTRY_2_PASSWORD", "invalid") + _, _, err = p.Kluctl(t, "deploy", "--yes", "-t", "test") + assert.ErrorContains(t, err, "401 Unauthorized") + + // deploy with valid creds + t.Setenv("KLUCTL_REGISTRY_2_PASSWORD", "pass2") + dockerLogin(t, repo3.URL.Host, "user3", "pass3") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "include1-cm") + assertConfigMapExists(t, k, p.TestSlug(), "include2-cm") +} + +func dockerLogin(t *testing.T, host string, username string, password string) { + dir := t.TempDir() + + var cf configfile.ConfigFile + cf.AuthConfigs = map[string]types.AuthConfig{ + host: { + Username: username, + Password: password, + }, + } + + s := yaml.WriteJsonStringMust(&cf) + + err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(s), 0600) + if err != nil { + t.Fatal(err) + } + + t.Setenv("DOCKER_CONFIG", dir) +} + +func TestOciIncludeTagsAndDigests(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + ip1 := prepareIncludeProject(t, "include1", "", nil) + + repo := test_utils.TestHelmRepo{ + Type: test_utils.TestHelmRepo_Oci, + } + repo.Start(t) + + repo1 := repo.URL.String() + "/org1/repo1" + + ip1.KluctlMust(t, "oci", "push", "--url", repo1+":tag1") + + ip1.UpdateYaml("cm/configmap-include1-cm.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("v2", "data", "a") + return nil + }, "") + ip1.KluctlMust(t, "oci", "push", "--url", repo1+":tag2") + + ip1.UpdateYaml("cm/configmap-include1-cm.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("v3", "data", "a") + return nil + }, "") + stdout, _ := ip1.KluctlMust(t, "oci", "push", "--url", repo1+":tag3", "--output", "json") + md := uo.FromStringMust(stdout) + digest, _, _ := md.GetNestedString("digest") + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) {}) + + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "oci": map[string]any{ + "url": repo1, + "ref": map[string]any{ + "tag": "tag1", + }, + }, + })) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm := assertConfigMapExists(t, k, p.TestSlug(), "include1-cm") + assertNestedFieldEquals(t, cm, "v", "data", "a") + + p.UpdateDeploymentItems("", func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + _ = items[0].SetNestedField(map[string]any{ + "tag": "tag2", + }, "oci", "ref") + return items + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm = assertConfigMapExists(t, k, p.TestSlug(), "include1-cm") + assertNestedFieldEquals(t, cm, "v2", "data", "a") + + p.UpdateDeploymentItems("", func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + _ = items[0].SetNestedField(map[string]any{ + "digest": digest, + }, "oci", "ref") + return items + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm = assertConfigMapExists(t, k, p.TestSlug(), "include1-cm") + assertNestedFieldEquals(t, cm, "v3", "data", "a") +} diff --git a/e2e/project.go b/e2e/project.go deleted file mode 100644 index be0bd2823..000000000 --- a/e2e/project.go +++ /dev/null @@ -1,473 +0,0 @@ -package e2e - -import ( - "fmt" - "github.com/go-git/go-git/v5" - "github.com/imdario/mergo" - test_utils "github.com/kluctl/kluctl/v2/internal/test-utils" - "github.com/kluctl/kluctl/v2/pkg/utils/uo" - "github.com/kluctl/kluctl/v2/pkg/yaml" - "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - "os" - "os/exec" - "path/filepath" - "reflect" - "strings" - "testing" -) - -type testProject struct { - t *testing.T - extraEnv []string - projectName string - - kluctlProjectExternal bool - clustersExternal bool - deploymentExternal bool - sealedSecretsExternal bool - - localClusters *string - localDeployment *string - localSealedSecrets *string - - mergedKubeconfig string - - gitServer *test_utils.GitServer -} - -func (p *testProject) init(t *testing.T, k *test_utils.KindCluster, projectName string) { - p.t = t - p.gitServer = test_utils.NewGitServer(t) - p.projectName = projectName - - p.gitServer.GitInit(p.getKluctlProjectRepo()) - if p.clustersExternal { - p.gitServer.GitInit(p.getClustersRepo()) - } - if p.deploymentExternal { - p.gitServer.GitInit(p.getDeploymentRepo()) - } - if p.sealedSecretsExternal { - p.gitServer.GitInit(p.getSealedSecretsRepo()) - } - - _ = os.MkdirAll(filepath.Join(p.gitServer.LocalRepoDir(p.getClustersRepo()), "clusters"), 0o700) - _ = os.MkdirAll(filepath.Join(p.gitServer.LocalRepoDir(p.getSealedSecretsRepo()), ".sealed-secrets"), 0o700) - - p.updateKluctlYaml(func(o *uo.UnstructuredObject) error { - if p.clustersExternal { - o.SetNestedField(p.gitServer.LocalGitUrl(p.getClustersRepo()), "clusters", "project") - } - if p.deploymentExternal { - o.SetNestedField(p.gitServer.LocalGitUrl(p.getDeploymentRepo()), "deployment", "project") - } - if p.sealedSecretsExternal { - o.SetNestedField(p.gitServer.LocalGitUrl(p.getSealedSecretsRepo()), "sealedSecrets", "project") - } - return nil - }) - p.updateDeploymentYaml(".", func(c *uo.UnstructuredObject) error { - return nil - }) - - tmpFile, err := os.CreateTemp("", projectName+"-kubeconfig-") - if err != nil { - t.Fatal(err) - } - _ = tmpFile.Close() - p.mergedKubeconfig = tmpFile.Name() - p.mergeKubeconfig(k) -} - -func (p *testProject) cleanup() { - if p.gitServer != nil { - p.gitServer.Cleanup() - p.gitServer = nil - } - if p.mergedKubeconfig != "" { - _ = os.Remove(p.mergedKubeconfig) - p.mergedKubeconfig = "" - } -} - -func (p *testProject) mergeKubeconfig(k *test_utils.KindCluster) { - p.updateMergedKubeconfig(func(config *clientcmdapi.Config) { - nkcfg, err := clientcmd.LoadFromFile(k.Kubeconfig) - if err != nil { - p.t.Fatal(err) - } - - err = mergo.Merge(config, nkcfg) - if err != nil { - p.t.Fatal(err) - } - }) -} - -func (p *testProject) updateMergedKubeconfig(cb func(config *clientcmdapi.Config)) { - mkcfg, err := clientcmd.LoadFromFile(p.mergedKubeconfig) - if err != nil { - p.t.Fatal(err) - } - - cb(mkcfg) - - err = clientcmd.WriteToFile(*mkcfg, p.mergedKubeconfig) - if err != nil { - p.t.Fatal(err) - } -} - -func (p *testProject) updateKluctlYaml(update func(o *uo.UnstructuredObject) error) { - p.gitServer.UpdateYaml(p.getKluctlProjectRepo(), ".kluctl.yml", update, "") -} - -func (p *testProject) updateDeploymentYaml(dir string, update func(o *uo.UnstructuredObject) error) { - p.gitServer.UpdateYaml(p.getDeploymentRepo(), filepath.Join(dir, "deployment.yml"), func(o *uo.UnstructuredObject) error { - if dir == "." { - o.SetNestedField(p.projectName, "commonLabels", "project_name") - } - return update(o) - }, "") -} - -func (p *testProject) getDeploymentYaml(dir string) *uo.UnstructuredObject { - o, err := uo.FromFile(filepath.Join(p.gitServer.LocalRepoDir(p.getDeploymentRepo()), dir, "deployment.yml")) - if err != nil { - p.t.Fatal(err) - } - return o -} - -func (p *testProject) listDeploymentItemPathes(dir string, fullPath bool) []string { - var ret []string - o := p.getDeploymentYaml(dir) - l, _, err := o.GetNestedObjectList("deployments") - if err != nil { - p.t.Fatal(err) - } - for _, x := range l { - pth, ok, _ := x.GetNestedString("path") - if ok { - x := pth - if fullPath { - x = filepath.Join(dir, pth) - } - ret = append(ret, x) - } - pth, ok, _ = x.GetNestedString("include") - if ok { - ret = append(ret, p.listDeploymentItemPathes(filepath.Join(dir, pth), fullPath)...) - } - } - return ret -} - -func (p *testProject) updateKustomizeDeployment(dir string, update func(o *uo.UnstructuredObject, wt *git.Worktree) error) { - wt := p.gitServer.GetWorktree(p.getDeploymentRepo()) - - pth := filepath.Join(dir, "kustomization.yml") - p.gitServer.UpdateYaml(p.getDeploymentRepo(), pth, func(o *uo.UnstructuredObject) error { - return update(o, wt) - }, fmt.Sprintf("Update kustomization.yml for %s", dir)) -} - -func (p *testProject) updateCluster(name string, context string, vars *uo.UnstructuredObject) { - pth := filepath.Join("clusters", fmt.Sprintf("%s.yml", name)) - p.gitServer.UpdateYaml(p.getClustersRepo(), pth, func(o *uo.UnstructuredObject) error { - o.Clear() - o.SetNestedField(name, "cluster", "name") - o.SetNestedField(context, "cluster", "context") - if vars != nil { - o.MergeChild("cluster", vars) - } - return nil - }, fmt.Sprintf("add/update cluster %s", name)) -} - -func (p *testProject) updateKindCluster(k *test_utils.KindCluster, vars *uo.UnstructuredObject) { - context, err := k.Kubectl("config", "current-context") - if err != nil { - p.t.Fatal(err) - } - context = strings.TrimSpace(context) - p.updateCluster(k.Name, context, vars) -} - -func (p *testProject) updateTargetDeprecated(name string, cluster string, args *uo.UnstructuredObject) { - p.updateTarget(name, func(target *uo.UnstructuredObject) { - if args != nil { - target.MergeChild("args", args) - } - // compatibility - _ = target.SetNestedField(cluster, "cluster") - }) -} - -func (p *testProject) updateTarget(name string, cb func(target *uo.UnstructuredObject)) { - p.updateNamedListItem(uo.KeyPath{"targets"}, name, cb) -} - -func (p *testProject) updateSecretSet(name string, cb func(secretSet *uo.UnstructuredObject)) { - p.updateNamedListItem(uo.KeyPath{"secretsConfig", "secretSets"}, name, cb) -} - -func (p *testProject) updateNamedListItem(path uo.KeyPath, name string, cb func(item *uo.UnstructuredObject)) { - if cb == nil { - cb = func(target *uo.UnstructuredObject) {} - } - - p.updateKluctlYaml(func(o *uo.UnstructuredObject) error { - l, _, _ := o.GetNestedObjectList(path...) - var newList []*uo.UnstructuredObject - found := false - for _, item := range l { - n, _, _ := item.GetNestedString("name") - if n == name { - cb(item) - found = true - } - newList = append(newList, item) - } - if !found { - n := uo.FromMap(map[string]interface{}{ - "name": name, - }) - cb(n) - newList = append(newList, n) - } - - _ = o.SetNestedObjectList(newList, path...) - return nil - }) -} - -func (p *testProject) updateDeploymentItems(dir string, update func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject) { - p.updateDeploymentYaml(dir, func(o *uo.UnstructuredObject) error { - items, _, _ := o.GetNestedObjectList("deployments") - items = update(items) - return o.SetNestedField(items, "deployments") - }) -} - -func (p *testProject) addDeploymentItem(dir string, item *uo.UnstructuredObject) { - p.updateDeploymentItems(dir, func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { - for _, x := range items { - if reflect.DeepEqual(x, item) { - return items - } - } - items = append(items, item) - return items - }) -} - -func (p *testProject) addDeploymentInclude(dir string, includePath string, tags []string) { - n := uo.FromMap(map[string]interface{}{ - "include": includePath, - }) - if len(tags) != 0 { - n.SetNestedField(tags, "tags") - } - p.addDeploymentItem(dir, n) -} - -func (p *testProject) addDeploymentIncludes(dir string) { - var pp []string - for _, x := range strings.Split(dir, "/") { - if x != "." { - p.addDeploymentInclude(filepath.Join(pp...), x, nil) - } - pp = append(pp, x) - } -} - -func (p *testProject) addKustomizeDeployment(dir string, resources []kustomizeResource, tags []string) { - deploymentDir := filepath.Dir(dir) - if deploymentDir != "" { - p.addDeploymentIncludes(deploymentDir) - } - - absKustomizeDir := filepath.Join(p.gitServer.LocalRepoDir(p.getDeploymentRepo()), dir) - - err := os.MkdirAll(absKustomizeDir, 0o700) - if err != nil { - p.t.Fatal(err) - } - - p.updateKustomizeDeployment(dir, func(o *uo.UnstructuredObject, wt *git.Worktree) error { - o.SetNestedField("kustomize.config.k8s.io/v1beta1", "apiVersion") - o.SetNestedField("Kustomization", "kind") - return nil - }) - - p.addKustomizeResources(dir, resources) - p.updateDeploymentYaml(deploymentDir, func(o *uo.UnstructuredObject) error { - d, _, _ := o.GetNestedObjectList("deployments") - n := uo.FromMap(map[string]interface{}{ - "path": filepath.Base(dir), - }) - if len(tags) != 0 { - n.SetNestedField(tags, "tags") - } - d = append(d, n) - _ = o.SetNestedObjectList(d, "deployments") - return nil - }) -} - -func (p *testProject) convertInterfaceToList(x interface{}) []interface{} { - var ret []interface{} - if l, ok := x.([]interface{}); ok { - return l - } - if l, ok := x.([]*uo.UnstructuredObject); ok { - for _, y := range l { - ret = append(ret, y) - } - return ret - } - if l, ok := x.([]map[string]interface{}); ok { - for _, y := range l { - ret = append(ret, y) - } - return ret - } - return []interface{}{x} -} - -type kustomizeResource struct { - name string - fileName string - content interface{} -} - -func (p *testProject) addKustomizeResources(dir string, resources []kustomizeResource) { - p.updateKustomizeDeployment(dir, func(o *uo.UnstructuredObject, wt *git.Worktree) error { - l, _, _ := o.GetNestedList("resources") - for _, r := range resources { - l = append(l, r.name) - x := p.convertInterfaceToList(r.content) - fileName := r.fileName - if fileName == "" { - fileName = r.name - } - err := yaml.WriteYamlAllFile(filepath.Join(p.gitServer.LocalRepoDir(p.getDeploymentRepo()), dir, fileName), x) - if err != nil { - return err - } - _, err = wt.Add(filepath.Join(dir, fileName)) - if err != nil { - return err - } - } - o.SetNestedField(l, "resources") - return nil - }) -} - -func (p *testProject) deleteKustomizeDeployment(dir string) { - deploymentDir := filepath.Dir(dir) - p.updateDeploymentItems(deploymentDir, func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { - var newItems []*uo.UnstructuredObject - for _, item := range items { - pth, _, _ := item.GetNestedString("path") - if pth == filepath.Base(dir) { - continue - } - newItems = append(newItems, item) - } - return newItems - }) -} - -func (p *testProject) getKluctlProjectRepo() string { - return "kluctl-project" -} - -func (p *testProject) getClustersRepo() string { - if p.clustersExternal { - return "external-clusters" - } - return p.getKluctlProjectRepo() -} - -func (p *testProject) getDeploymentRepo() string { - if p.deploymentExternal { - return "external-deployment" - } - return p.getKluctlProjectRepo() -} - -func (p *testProject) getSealedSecretsRepo() string { - if p.sealedSecretsExternal { - return "external-sealed-secrets" - } - return p.getKluctlProjectRepo() -} - -func (p *testProject) Kluctl(argsIn ...string) (string, string, error) { - var args []string - args = append(args, argsIn...) - args = append(args, "--no-update-check") - - cwd := "" - if p.kluctlProjectExternal { - args = append(args, "--project-url", p.gitServer.LocalGitUrl(p.getKluctlProjectRepo())) - } else { - cwd = p.gitServer.LocalRepoDir(p.getKluctlProjectRepo()) - } - - if p.localClusters != nil { - args = append(args, "--local-clusters", *p.localClusters) - } - if p.localDeployment != nil { - args = append(args, "--local-deployment", *p.localDeployment) - } - if p.localSealedSecrets != nil { - args = append(args, "--local-sealed-secrets", *p.localSealedSecrets) - } - - args = append(args, "--debug") - - env := os.Environ() - env = append(env, p.extraEnv...) - env = append(env, fmt.Sprintf("KUBECONFIG=%s", p.mergedKubeconfig)) - - p.t.Logf("Runnning kluctl: %s", strings.Join(args, " ")) - - kluctlExe := os.Getenv("KLUCTL_EXE") - if kluctlExe == "" { - curDir, _ := os.Getwd() - for i, p := range env { - x := strings.SplitN(p, "=", 2) - if x[0] == "PATH" { - env[i] = fmt.Sprintf("PATH=%s%c%s%c%s", curDir, os.PathListSeparator, filepath.Join(curDir, ".."), os.PathListSeparator, x[1]) - } - } - kluctlExe = "kluctl" - } else { - p, err := filepath.Abs(kluctlExe) - if err != nil { - return "", "", err - } - kluctlExe = p - } - - cmd := exec.Command(kluctlExe, args...) - cmd.Dir = cwd - cmd.Env = env - - stdout, stderr, err := runHelper(p.t, cmd) - return stdout, stderr, err -} - -func (p *testProject) KluctlMust(argsIn ...string) (string, string) { - stdout, stderr, err := p.Kluctl(argsIn...) - if err != nil { - p.t.Logf(stderr) - p.t.Fatal(fmt.Errorf("kluctl failed: %w", err)) - } - return stdout, stderr -} diff --git a/e2e/prune_test.go b/e2e/prune_test.go new file mode 100644 index 000000000..f55d91ee5 --- /dev/null +++ b/e2e/prune_test.go @@ -0,0 +1,127 @@ +package e2e + +import ( + test_utils "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "testing" +) + +func TestPrune(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_utils.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm1", map[string]string{}, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + addConfigMapDeployment(p, "cm2", map[string]string{}, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertConfigMapExists(t, k, p.TestSlug(), "cm2") + + p.DeleteKustomizeDeployment("cm2") + + p.KluctlMust(t, "prune", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm2") +} + +func TestDeployWithPrune(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_utils.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm1", map[string]string{}, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + addConfigMapDeployment(p, "cm2", map[string]string{}, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertConfigMapExists(t, k, p.TestSlug(), "cm2") + + p.DeleteKustomizeDeployment("cm2") + addConfigMapDeployment(p, "cm3", map[string]string{}, resourceOpts{ + name: "cm3", + namespace: p.TestSlug(), + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "--prune") + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm2") + assertConfigMapExists(t, k, p.TestSlug(), "cm3") +} + +func TestPruneForceManaged(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_utils.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm1", map[string]string{}, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + addConfigMapDeployment(p, "cm2", map[string]string{}, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm1 := assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertConfigMapExists(t, k, p.TestSlug(), "cm2") + + p.DeleteKustomizeDeployment("cm2") + + patchObject(t, k, v1.SchemeGroupVersion.WithResource("configmaps"), p.TestSlug(), "cm2", func(o *uo.UnstructuredObject) { + // objects with owner references are not considered when pruning... + o.SetK8sOwnerReferences([]*uo.UnstructuredObject{ + uo.FromStructMust(&metav1.OwnerReference{ + APIVersion: "v1", + Kind: "ConfigMap", + Name: "cm1", + UID: types.UID(cm1.GetK8sUid()), + }), + }) + }) + + p.KluctlMust(t, "prune", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertConfigMapExists(t, k, p.TestSlug(), "cm2") // not pruned because of owner ref + + patchObject(t, k, v1.SchemeGroupVersion.WithResource("configmaps"), p.TestSlug(), "cm2", func(o *uo.UnstructuredObject) { + // ... except when force-managed=true + o.SetK8sAnnotation("kluctl.io/force-managed", "true") + }) + p.KluctlMust(t, "prune", "--yes", "-t", "test") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm2") +} diff --git a/e2e/readiness_test.go b/e2e/readiness_test.go new file mode 100644 index 000000000..ce9bd49e2 --- /dev/null +++ b/e2e/readiness_test.go @@ -0,0 +1,126 @@ +package e2e + +import ( + "fmt" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func testWaitReadiness(t *testing.T, fn func(p *test_project.TestProject)) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + addConfigMapDeployment(p, "cm1", nil, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + addConfigMapDeployment(p, "cm2", nil, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + annotations: map[string]string{ + "kluctl.io/is-ready": "false", + }, + }) + p.AddDeploymentItem(".", uo.FromMap(map[string]interface{}{ + "barrier": true, + })) + addConfigMapDeployment(p, "cm3", nil, resourceOpts{ + name: "cm3", + namespace: p.TestSlug(), + }) + + fn(p) + + _, stderr, err := p.Kluctl(t, "deploy", "--yes", "-t", "test", "--timeout", (3 * time.Second).String()) + assert.Error(t, err) + assert.Contains(t, stderr, fmt.Sprintf("context cancelled while waiting for readiness of %s/ConfigMap/cm2", p.TestSlug())) + + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertConfigMapExists(t, k, p.TestSlug(), "cm2") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm3") + + tt := time.Now() + t.Logf("testReadiness: starting background goroutine startTime=%s", tt.String()) + + // we run a goroutine in the background that will wait for a few seconds and then annotate kluctl.io/is-ready=true, + // which will then cause the hook to get ready + go func() { + t.Logf("testReadiness (goroutine): begin elapsed=%s", time.Now().Sub(tt).String()) + time.Sleep(3 * time.Second) + t.Logf("testReadiness (goroutine): patching elapsed=%s", time.Now().Sub(tt).String()) + patchConfigMap(t, k, p.TestSlug(), "cm2", func(o *uo.UnstructuredObject) { + o.SetK8sAnnotation("kluctl.io/is-ready", "true") + }) + t.Logf("testReadiness (goroutine): done elapsed=%s", time.Now().Sub(tt).String()) + }() + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm3") +} + +func TestWaitReadinessViaDeployment(t *testing.T) { + testWaitReadiness(t, func(p *test_project.TestProject) { + p.UpdateDeploymentItems(".", func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + items[1].SetNestedField(true, "waitReadiness") + return items + }) + }) +} + +func TestWaitReadinessViaDeployment2(t *testing.T) { + testWaitReadiness(t, func(p *test_project.TestProject) { + p.UpdateDeploymentItems(".", func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + items[2].SetNestedField([]map[string]any{ + { + "kind": "ConfigMap", + "namespace": p.TestSlug(), + "name": "cm2", + }, + }, "waitReadinessObjects") + return items + }) + }) +} + +func TestWaitReadinessViaAnnotation(t *testing.T) { + testWaitReadiness(t, func(p *test_project.TestProject) { + p.UpdateYaml("cm2/configmap-cm2.yml", func(o *uo.UnstructuredObject) error { + o.SetK8sAnnotation("kluctl.io/wait-readiness", "true") + return nil + }, "") + }) +} + +func TestWaitReadinessViaKustomization(t *testing.T) { + testWaitReadiness(t, func(p *test_project.TestProject) { + p.UpdateYaml("cm2/kustomization.yml", func(o *uo.UnstructuredObject) error { + o.SetK8sAnnotation("kluctl.io/wait-readiness", "true") + return nil + }, "") + }) +} + +func TestWaitReadinessViaKustomizationWithHook(t *testing.T) { + testWaitReadiness(t, func(p *test_project.TestProject) { + // the kustomization.yaml wait-readiness should actually be ignored in this case... + p.UpdateYaml("cm2/kustomization.yml", func(o *uo.UnstructuredObject) error { + o.SetK8sAnnotation("kluctl.io/wait-readiness", "true") + return nil + }, "") + // because the hook object is what is waited for instead + p.UpdateYaml("cm2/configmap-cm2.yml", func(o *uo.UnstructuredObject) error { + o.SetK8sAnnotation("kluctl.io/hook", "post-deploy") + return nil + }, "") + }) +} diff --git a/e2e/remote_objects_permission_test.go b/e2e/remote_objects_permission_test.go new file mode 100644 index 000000000..fab8d61a2 --- /dev/null +++ b/e2e/remote_objects_permission_test.go @@ -0,0 +1,304 @@ +package e2e + +import ( + "fmt" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "testing" +) + +func buildSingleNamespaceRbac(username string, namespace string, globalResources []schema.GroupResource, resources []schema.GroupResource) []*uo.UnstructuredObject { + var ret []*uo.UnstructuredObject + + verbs := []string{"create", "update", "patch", "get", "list", "watch", "delete"} + + var clusterRole v1.ClusterRole + clusterRole.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("ClusterRole")) + clusterRole.Name = username + clusterRole.Rules = append(clusterRole.Rules, v1.PolicyRule{ + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + ResourceNames: []string{namespace}, + Verbs: verbs, + }) + for _, r := range globalResources { + clusterRole.Rules = append(clusterRole.Rules, v1.PolicyRule{ + APIGroups: []string{r.Group}, + Resources: []string{r.Resource}, + Verbs: verbs, + }) + } + ret = append(ret, uo.FromStructMust(clusterRole)) + + var role v1.Role + role.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Role")) + role.Name = username + role.Namespace = namespace + for _, r := range resources { + role.Rules = append(role.Rules, v1.PolicyRule{ + APIGroups: []string{r.Group}, + Resources: []string{r.Resource}, + Verbs: verbs, + }) + } + ret = append(ret, uo.FromStructMust(role)) + + var clusterRoleBinding v1.ClusterRoleBinding + clusterRoleBinding.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("ClusterRoleBinding")) + clusterRoleBinding.Name = username + clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, v1.Subject{ + Kind: "User", + Name: username, + }) + clusterRoleBinding.RoleRef = v1.RoleRef{ + Kind: "ClusterRole", + Name: username, + APIGroup: "rbac.authorization.k8s.io", + } + ret = append(ret, uo.FromStructMust(clusterRoleBinding)) + + var roleBinding v1.ClusterRoleBinding + roleBinding.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("RoleBinding")) + roleBinding.Name = username + roleBinding.Namespace = namespace + roleBinding.Subjects = append(roleBinding.Subjects, v1.Subject{ + Kind: "User", + Name: username, + }) + roleBinding.RoleRef = v1.RoleRef{ + Kind: "Role", + Name: username, + APIGroup: "rbac.authorization.k8s.io", + } + ret = append(ret, uo.FromStructMust(roleBinding)) + + return ret +} + +func TestRemoteObjectUtils_PermissionErrors(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + username := p.TestSlug() + au, err := k.AddUser(envtest.User{Name: username}, nil) + assert.NoError(t, err) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addSecretDeployment(p, "secret", nil, resourceOpts{ + name: "secret", + namespace: p.TestSlug(), + }) + addConfigMapDeployment(p, "cm", nil, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + + rbac := fmt.Sprintf(` +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: %s +subjects: + - kind: User + name: %s +roleRef: + kind: ClusterRole + name: "system:aggregate-to-view" + apiGroup: rbac.authorization.k8s.io +`, username, username) + p.AddKustomizeDeployment("rbac", []test_project.KustomizeResource{ + {Name: "rbac.yaml", Content: rbac}, + }, nil) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm") + assertSecretExists(t, k, p.TestSlug(), "secret") + + kc, err := au.KubeConfig() + assert.NoError(t, err) + + p.AddExtraArgs("--kubeconfig", getKubeconfigTmpFile(t, kc)) + + stdout, _, err := p.Kluctl(t, "deploy", "--yes", "-t", "test", "--write-command-result=false") + assert.Error(t, err) + assert.Contains(t, stdout, "at least one permission error was encountered while gathering objects by discriminator labels") +} + +func TestNoGetKubeSystemPermissions(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + username := p.TestSlug() + au, err := k.AddUser(envtest.User{Name: username}, nil) + assert.NoError(t, err) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm", nil, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + + rbac := fmt.Sprintf(` +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: %s +rules: + - apiGroups: [""] + resources: ["namespaces"] + # only list allowed, get is forbidden. it should still be able to get the cluster ID + verbs: ["list"] + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["create", "update", "patch", "get", "list", "watch"] + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["*"] + verbs: ["create", "update", "patch", "get", "list", "watch"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: %s +subjects: + - kind: User + name: %s +roleRef: + kind: ClusterRole + name: %s + apiGroup: rbac.authorization.k8s.io +`, username, username, username, username) + p.AddKustomizeDeployment("rbac", []test_project.KustomizeResource{ + {Name: "rbac.yaml", Content: rbac}, + }, nil) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + + kc, err := au.KubeConfig() + assert.NoError(t, err) + + p.AddExtraArgs("--kubeconfig", getKubeconfigTmpFile(t, kc)) + + addConfigMapDeployment(p, "cm2", nil, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "--write-command-result=false") + assertConfigMapExists(t, k, p.TestSlug(), "cm2") +} + +func TestOnlyOneNamespacePermissions(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + username := p.TestSlug() + au, err := k.AddUser(envtest.User{Name: username}, nil) + assert.NoError(t, err) + + createNamespace(t, k, p.TestSlug()) + + rbac := buildSingleNamespaceRbac(username, p.TestSlug(), nil, []schema.GroupResource{{Group: "", Resource: "configmaps"}}) + for _, x := range rbac { + k.MustApply(t, x) + } + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm", nil, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + + kc, err := au.KubeConfig() + assert.NoError(t, err) + + p.AddExtraArgs("--kubeconfig", getKubeconfigTmpFile(t, kc)) + + addConfigMapDeployment(p, "cm2", nil, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + + _, stderr, err := p.Kluctl(t, "deploy", "--yes", "-t", "test") + assert.NoError(t, err) + assertConfigMapExists(t, k, p.TestSlug(), "cm2") + assert.Contains(t, stderr, "Not enough permissions to write to the result store.") +} + +func TestOnlyOneNamespacePrune(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t) + + username := p.TestSlug() + au, err := k.AddUser(envtest.User{Name: username}, nil) + assert.NoError(t, err) + + createNamespace(t, k, p.TestSlug()) + + rbac := buildSingleNamespaceRbac(username, p.TestSlug(), nil, []schema.GroupResource{{Group: "", Resource: "configmaps"}}) + for _, x := range rbac { + k.MustApply(t, x) + } + + kc, err := au.KubeConfig() + assert.NoError(t, err) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm1", nil, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + addConfigMapDeployment(p, "cm2", nil, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + addConfigMapDeployment(p, "cm3", nil, resourceOpts{ + name: "cm3", + namespace: p.TestSlug(), + }) + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertConfigMapExists(t, k, p.TestSlug(), "cm2") + assertConfigMapExists(t, k, p.TestSlug(), "cm3") + + // first, with admin privileges + p.DeleteKustomizeDeployment("cm2") + stdout, _ := p.KluctlMust(t, "deploy", "--yes", "-t", "test", "--prune") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm2") + assert.NotContains(t, stdout, "listing objects by discriminator on global level failed due to permission errors, so Kluctl reverted to listing on namespace level") + + // now with single namespace privileges + p.AddExtraArgs("--kubeconfig", getKubeconfigTmpFile(t, kc)) + p.DeleteKustomizeDeployment("cm3") + stdout, _ = p.KluctlMust(t, "deploy", "--yes", "-t", "test", "--prune") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm3") + + assert.Contains(t, stdout, "listing objects by discriminator on global level failed due to permission errors, so Kluctl reverted to listing on namespace level") +} diff --git a/e2e/render_test.go b/e2e/render_test.go new file mode 100644 index 000000000..c2c37afcd --- /dev/null +++ b/e2e/render_test.go @@ -0,0 +1,61 @@ +package e2e + +import ( + "github.com/kluctl/kluctl/lib/yaml" + test_utils "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRenderPrintAll(t *testing.T) { + t.Parallel() + + p := test_utils.NewTestProject(t) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + }) + + addConfigMapDeployment(p, "cm", nil, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + + stdout, _ := p.KluctlMust(t, "render", "-t", "test", "--print-all") + y, err := yaml.ReadYamlAllString(stdout) + assert.NoError(t, err) + assert.Equal(t, []any{map[string]any{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{"kluctl.io/deployment-item-dir": "cm"}, + "labels": map[string]interface{}{ + "kluctl.io/discriminator": p.Discriminator("test"), + "kluctl.io/tag-0": "cm", + "project_name": p.TestSlug(), + }, + "name": "cm", + "namespace": p.TestSlug(), + }}}, y) +} + +func TestRenderNoKubeconfig(t *testing.T) { + t.Setenv("KUBECONFIG", "invalid") + + p := test_utils.NewTestProject(t) + + addConfigMapDeployment(p, "cm", nil, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + + _, stderr := p.KluctlMust(t, "render", "--print-all") + assert.Contains(t, stderr, "No valid KUBECONFIG provided, which means the Kubernetes client is not available") + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) { + _ = target.SetNestedField("context1", "context") + }) + _, stderr, err := p.Kluctl(t, "render", "-t", "test", "--print-all") + assert.ErrorContains(t, err, "context \"context1\" does not exist") + +} diff --git a/e2e/results_test.go b/e2e/results_test.go new file mode 100644 index 000000000..22eae892b --- /dev/null +++ b/e2e/results_test.go @@ -0,0 +1,118 @@ +package e2e + +import ( + "context" + gittypes "github.com/kluctl/kluctl/lib/git/types" + test_utils "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/results" + "github.com/kluctl/kluctl/v2/pkg/types/result" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "testing" +) + +func assertSummary(t *testing.T, expected result.CommandResultSummary, actual result.CommandResultSummary) { + assert.Equal(t, expected.AppliedObjects, actual.AppliedObjects) + assert.Equal(t, expected.NewObjects, actual.NewObjects) + assert.Equal(t, expected.ChangedObjects, actual.ChangedObjects) + assert.Equal(t, expected.OrphanObjects, actual.OrphanObjects) + assert.Equal(t, expected.DeletedObjects, actual.DeletedObjects) + assert.Equal(t, expected.Errors, actual.Errors) + assert.Equal(t, expected.Warnings, actual.Warnings) + assert.Equal(t, expected.TotalChanges, actual.TotalChanges) +} + +func TestWriteResult(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_utils.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm", map[string]string{ + "d1": "v1", + }, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm") + + // we must ensure that at least a second passes between deployments, as otherwise command result sorting becomes + // unstable + b := newSecondPassedBarrier(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + rs, err := results.NewResultStoreSecrets(ctx, k.RESTConfig(), k.Client, false, "kluctl-results", 0, 0) + assert.NoError(t, err) + + opts := results.ListResultSummariesOptions{ + ProjectFilter: &gittypes.ProjectKey{ + RepoKey: gittypes.ParseGitUrlMust(p.GitUrl()).RepoKey(), + }, + } + + summaries, err := rs.ListCommandResultSummaries(opts) + assert.NoError(t, err) + assert.Len(t, summaries, 1) + assertSummary(t, result.CommandResultSummary{ + AppliedObjects: 1, + NewObjects: 1, + }, summaries[0]) + + addConfigMapDeployment(p, "cm2", nil, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + p.UpdateYaml("cm/configmap-cm.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("v2", "data", "d1") + return nil + }, "") + + b.Wait() + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm2") + + summaries, err = rs.ListCommandResultSummaries(opts) + assert.NoError(t, err) + assert.Len(t, summaries, 2) + assertSummary(t, result.CommandResultSummary{ + AppliedObjects: 2, + NewObjects: 1, + ChangedObjects: 1, + TotalChanges: 1, + }, summaries[0]) + + p.UpdateDeploymentYaml("", func(o *uo.UnstructuredObject) error { + _ = o.RemoveNestedField("deployments", 1) + return nil + }) + b.Wait() + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm2") + + summaries, err = rs.ListCommandResultSummaries(opts) + assert.NoError(t, err) + assert.Len(t, summaries, 3) + assertSummary(t, result.CommandResultSummary{ + AppliedObjects: 1, + OrphanObjects: 1, + }, summaries[0]) + + b.Wait() + p.KluctlMust(t, "prune", "--yes", "-t", "test") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm2") + + summaries, err = rs.ListCommandResultSummaries(opts) + assert.NoError(t, err) + assert.Len(t, summaries, 4) + assertSummary(t, result.CommandResultSummary{ + DeletedObjects: 1, + }, summaries[0]) +} diff --git a/e2e/seal_test.go b/e2e/seal_test.go deleted file mode 100644 index 28e6b976f..000000000 --- a/e2e/seal_test.go +++ /dev/null @@ -1,433 +0,0 @@ -package e2e - -import ( - "encoding/base64" - "fmt" - "github.com/kluctl/kluctl/v2/e2e/test_resources" - "github.com/kluctl/kluctl/v2/internal/test-utils" - "github.com/kluctl/kluctl/v2/pkg/utils" - "github.com/kluctl/kluctl/v2/pkg/utils/uo" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "net/url" - "path/filepath" - "strings" - "sync" - "testing" - "time" -) - -func installSealedSecretsOperator(k *test_utils.KindCluster) { - test_resources.ApplyYaml("sealed-secrets.yaml", k) -} - -func waitForSealedSecretsOperator(t *testing.T, k *test_utils.KindCluster) { - waitForReadiness(t, k, "kube-system", "deployment/sealed-secrets-controller", 5*time.Minute) -} - -func deleteSealedSecretsOperator(k *test_utils.KindCluster) { - test_resources.DeleteYaml("sealed-secrets.yaml", k) - _, _ = k.Kubectl("-n", "kube-system", "delete", "secret", "-l", "sealedsecrets.bitnami.com/sealed-secrets-key", "--wait") - _, _ = k.Kubectl("-n", "kube-system", "delete", "configmap", "sealed-secrets-key-kluctl-bootstrap", "--wait") -} - -func installVault(k *test_utils.KindCluster) { - _, _ = k.Kubectl("create", "ns", "vault") - test_resources.ApplyYaml("vault.yaml", k) -} - -func waitForVault(t *testing.T, k *test_utils.KindCluster) { - waitForReadiness(t, k, "vault", "statefulset/vault", 5*time.Minute) -} - -func init() { - var wg sync.WaitGroup - wg.Add(2) - go func() { - defer wg.Done() - installSealedSecretsOperator(defaultKindCluster1) - installVault(defaultKindCluster1) - }() - go func() { - defer wg.Done() - installSealedSecretsOperator(defaultKindCluster2) - }() - wg.Wait() -} - -func prepareSealTest(t *testing.T, k *test_utils.KindCluster, namespace string, secrets map[string]string, varsSources []*uo.UnstructuredObject) *testProject { - p := &testProject{} - p.init(t, k, fmt.Sprintf("seal-%s", namespace)) - - recreateNamespace(t, k, namespace) - - addSecretsSet(p, "test", varsSources) - addSecretsSetToTarget(p, "test-target", "test") - - addSecretDeployment(p, "secret-deployment", secrets, true, resourceOpts{name: "secret", namespace: namespace}) - - return p -} - -func addSecretsSet(p *testProject, name string, varsSources []*uo.UnstructuredObject) { - p.updateSecretSet(name, func(secretSet *uo.UnstructuredObject) { - _ = secretSet.SetNestedField(varsSources, "vars") - }) -} - -func addSecretsSetToTarget(p *testProject, targetName string, secretSetName string) { - p.updateTarget(targetName, func(target *uo.UnstructuredObject) { - l, _, _ := target.GetNestedList("sealingConfig", "secretSets") - l = append(l, secretSetName) - _ = target.SetNestedField(l, "sealingConfig", "secretSets") - }) -} - -func assertDecryptedSecrets(t *testing.T, k *test_utils.KindCluster, namespace string, secretName string, expectedSecrets map[string]string) { - s := k.KubectlYamlMust(t, "-n", namespace, "get", "secret", secretName) - - for key, value := range expectedSecrets { - x, _, _ := s.GetNestedString("data", key) - decoded, _ := base64.StdEncoding.DecodeString(x) - assert.Equal(t, value, string(decoded)) - } -} - -func TestSeal_WithOperator(t *testing.T) { - k := defaultKindCluster1 - namespace := "seal-with-operator" - - waitForSealedSecretsOperator(t, k) - - p := prepareSealTest(t, k, namespace, - map[string]string{ - "s1": "{{ secrets.s1 }}", - "s2": "{{ secrets.s2 }}", - }, - []*uo.UnstructuredObject{ - uo.FromMap(map[string]interface{}{ - "values": map[string]interface{}{ - "s1": "v1", - "s2": "v2", - }, - }), - }, - ) - defer p.cleanup() - - p.KluctlMust("seal", "-t", "test-target") - - sealedSecretsDir := p.gitServer.LocalRepoDir(p.getSealedSecretsRepo()) - assert.FileExists(t, filepath.Join(sealedSecretsDir, ".sealed-secrets/secret-deployment/test-target/secret-secret.yml")) - - p.KluctlMust("deploy", "--yes", "-t", "test-target") - - waitForReadiness(t, k, namespace, "secret/secret", 1*time.Minute) - assertDecryptedSecrets(t, k, namespace, "secret", map[string]string{ - "s1": "v1", - "s2": "v2", - }) -} - -func TestSeal_WithBootstrap(t *testing.T) { - k := defaultKindCluster2 - namespace := "seal-with-bootstrap" - - // we still wait for it to be ready before we then delete it - // this way it's pre-pulled and pre-warmed when we later start it - waitForSealedSecretsOperator(t, k) - deleteSealedSecretsOperator(k) - - p := prepareSealTest(t, k, namespace, - map[string]string{ - "s1": "{{ secrets.s1 }}", - "s2": "{{ secrets.s2 }}", - }, - []*uo.UnstructuredObject{ - uo.FromMap(map[string]interface{}{ - "values": map[string]interface{}{ - "s1": "v1", - "s2": "v2", - }, - }), - }, - ) - defer p.cleanup() - - p.KluctlMust("seal", "-t", "test-target") - - sealedSecretsDir := p.gitServer.LocalRepoDir(p.getSealedSecretsRepo()) - assert.FileExists(t, filepath.Join(sealedSecretsDir, ".sealed-secrets/secret-deployment/test-target/secret-secret.yml")) - - installSealedSecretsOperator(k) - waitForSealedSecretsOperator(t, k) - - p.KluctlMust("deploy", "--yes", "-t", "test-target") - - waitForReadiness(t, k, namespace, "secret/secret", 1*time.Minute) - assertDecryptedSecrets(t, k, namespace, "secret", map[string]string{ - "s1": "v1", - "s2": "v2", - }) -} - -func TestSeal_MultipleVarSources(t *testing.T) { - t.Parallel() - - k := defaultKindCluster1 - namespace := "seal-multiple-vs" - - waitForSealedSecretsOperator(t, k) - - p := prepareSealTest(t, k, namespace, - map[string]string{ - "s1": "{{ secrets.s1 }}", - "s2": "{{ secrets.s2 }}", - }, - []*uo.UnstructuredObject{ - uo.FromMap(map[string]interface{}{ - "values": map[string]interface{}{ - "s1": "v1", - }, - }), - uo.FromMap(map[string]interface{}{ - "values": map[string]interface{}{ - "s2": "v2", - }, - }), - }, - ) - defer p.cleanup() - - p.KluctlMust("seal", "-t", "test-target") - - sealedSecretsDir := p.gitServer.LocalRepoDir(p.getSealedSecretsRepo()) - assert.FileExists(t, filepath.Join(sealedSecretsDir, ".sealed-secrets/secret-deployment/test-target/secret-secret.yml")) - - p.KluctlMust("deploy", "--yes", "-t", "test-target") - - waitForReadiness(t, k, namespace, "secret/secret", 1*time.Minute) - assertDecryptedSecrets(t, k, namespace, "secret", map[string]string{ - "s1": "v1", - "s2": "v2", - }) -} - -func TestSeal_MultipleSecretSets(t *testing.T) { - t.Parallel() - - k := defaultKindCluster1 - namespace := "seal-multiple-ss" - - waitForSealedSecretsOperator(t, k) - - p := prepareSealTest(t, k, namespace, - map[string]string{ - "s1": "{{ secrets.s1 }}", - "s2": "{{ secrets.s2 }}", - }, - []*uo.UnstructuredObject{ - uo.FromMap(map[string]interface{}{ - "values": map[string]interface{}{ - "s1": "v1", - }, - }), - }, - ) - defer p.cleanup() - - addSecretsSet(p, "test2", []*uo.UnstructuredObject{ - uo.FromMap(map[string]interface{}{ - "values": map[string]interface{}{ - "s2": "v2", - }, - }), - }) - addSecretsSetToTarget(p, "test-target", "test2") - - p.KluctlMust("seal", "-t", "test-target") - - sealedSecretsDir := p.gitServer.LocalRepoDir(p.getSealedSecretsRepo()) - assert.FileExists(t, filepath.Join(sealedSecretsDir, ".sealed-secrets/secret-deployment/test-target/secret-secret.yml")) - - p.KluctlMust("deploy", "--yes", "-t", "test-target") - - waitForReadiness(t, k, namespace, "secret/secret", 1*time.Minute) - assertDecryptedSecrets(t, k, namespace, "secret", map[string]string{ - "s1": "v1", - "s2": "v2", - }) -} - -func TestSeal_MultipleTargets(t *testing.T) { - k := defaultKindCluster1 - namespace := "seal-multiple-targets" - - waitForSealedSecretsOperator(t, k) - waitForSealedSecretsOperator(t, defaultKindCluster2) - - p := prepareSealTest(t, k, namespace, - map[string]string{ - "s1": "{{ secrets.s1 }}", - "s2": "{{ secrets.s2 }}", - }, - []*uo.UnstructuredObject{ - uo.FromMap(map[string]interface{}{ - "values": map[string]interface{}{ - "s1": "v1", - "s2": "v2", - }, - }), - }, - ) - defer p.cleanup() - - addSecretsSet(p, "test2", []*uo.UnstructuredObject{ - uo.FromMap(map[string]interface{}{ - "values": map[string]interface{}{ - "s1": "v3", - "s2": "v4", - }, - }), - }) - addSecretsSetToTarget(p, "test-target2", "test2") - - p.mergeKubeconfig(defaultKindCluster2) - recreateNamespace(t, defaultKindCluster2, namespace) - p.updateTarget("test-target", func(target *uo.UnstructuredObject) { - _ = target.SetNestedField(defaultKindCluster1.Context, "context") - }) - p.updateTarget("test-target2", func(target *uo.UnstructuredObject) { - _ = target.SetNestedField(defaultKindCluster2.Context, "context") - }) - - p.KluctlMust("seal", "-t", "test-target") - p.KluctlMust("seal", "-t", "test-target2") - - sealedSecretsDir := p.gitServer.LocalRepoDir(p.getSealedSecretsRepo()) - assert.FileExists(t, filepath.Join(sealedSecretsDir, ".sealed-secrets/secret-deployment/test-target/secret-secret.yml")) - assert.FileExists(t, filepath.Join(sealedSecretsDir, ".sealed-secrets/secret-deployment/test-target2/secret-secret.yml")) - - p.KluctlMust("deploy", "--yes", "-t", "test-target") - p.KluctlMust("deploy", "--yes", "-t", "test-target2") - - waitForReadiness(t, k, namespace, "secret/secret", 1*time.Minute) - assertDecryptedSecrets(t, k, namespace, "secret", map[string]string{ - "s1": "v1", - "s2": "v2", - }) - waitForReadiness(t, defaultKindCluster2, namespace, "secret/secret", 1*time.Minute) - assertDecryptedSecrets(t, defaultKindCluster2, namespace, "secret", map[string]string{ - "s1": "v3", - "s2": "v4", - }) -} - -func TestSeal_File(t *testing.T) { - t.Parallel() - - k := defaultKindCluster1 - namespace := "seal-file" - - waitForSealedSecretsOperator(t, k) - - p := prepareSealTest(t, k, namespace, - map[string]string{ - "s1": "{{ secrets.s1 }}", - "s2": "{{ secrets.s2 }}", - }, - []*uo.UnstructuredObject{ - uo.FromMap(map[string]interface{}{ - "file": utils.StrPtr("secret-values.yaml"), - }), - }, - ) - defer p.cleanup() - - p.gitServer.UpdateYaml(p.getKluctlProjectRepo(), "secret-values.yaml", func(o *uo.UnstructuredObject) error { - *o = *uo.FromMap(map[string]interface{}{ - "secrets": map[string]interface{}{ - "s1": "v1", - "s2": "v2", - }, - }) - return nil - }, "") - - p.KluctlMust("seal", "-t", "test-target") - - sealedSecretsDir := p.gitServer.LocalRepoDir(p.getSealedSecretsRepo()) - assert.FileExists(t, filepath.Join(sealedSecretsDir, ".sealed-secrets/secret-deployment/test-target/secret-secret.yml")) - - p.KluctlMust("deploy", "--yes", "-t", "test-target") - - waitForReadiness(t, k, namespace, "secret/secret", 1*time.Minute) - assertDecryptedSecrets(t, k, namespace, "secret", map[string]string{ - "s1": "v1", - "s2": "v2", - }) -} - -func TestSeal_Vault(t *testing.T) { - t.Parallel() - - k := defaultKindCluster1 - namespace := "seal-vault" - - waitForSealedSecretsOperator(t, k) - waitForVault(t, k) - - u, err := url.Parse(defaultKindCluster1.RESTConfig().Host) - if err != nil { - t.Fatal(err) - } - - vaultUrl := fmt.Sprintf("http://%s:%d", u.Hostname(), defaultKindCluster1VaultPort) - - req, err := http.NewRequest("POST", fmt.Sprintf("%s/v1/secret/data/secret", vaultUrl), strings.NewReader(`{"data": {"secrets":{"s1":"v1","s2":"v2"}}}`)) - if err != nil { - t.Fatal(err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Vault-Token", "root") - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - body, _ := ioutil.ReadAll(resp.Body) - t.Fatalf("vault response status %d, body=%s", resp.StatusCode, string(body)) - } - - p := prepareSealTest(t, k, namespace, - map[string]string{ - "s1": "{{ secrets.s1 }}", - "s2": "{{ secrets.s2 }}", - }, - []*uo.UnstructuredObject{ - uo.FromMap(map[string]interface{}{ - "vault": map[string]interface{}{ - "address": vaultUrl, - "path": "secret/data/secret", - }, - }), - }, - ) - defer p.cleanup() - - p.extraEnv = append(p.extraEnv, "VAULT_TOKEN=root") - p.KluctlMust("seal", "-t", "test-target") - - sealedSecretsDir := p.gitServer.LocalRepoDir(p.getSealedSecretsRepo()) - assert.FileExists(t, filepath.Join(sealedSecretsDir, ".sealed-secrets/secret-deployment/test-target/secret-secret.yml")) - - p.KluctlMust("deploy", "--yes", "-t", "test-target") - - waitForReadiness(t, k, namespace, "secret/secret", 1*time.Minute) - assertDecryptedSecrets(t, k, namespace, "secret", map[string]string{ - "s1": "v1", - "s2": "v2", - }) -} diff --git a/e2e/skip_delete_test.go b/e2e/skip_delete_test.go new file mode 100644 index 000000000..e3ea87606 --- /dev/null +++ b/e2e/skip_delete_test.go @@ -0,0 +1,132 @@ +package e2e + +import ( + test_utils "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func TestSkipDelete(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_utils.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm1", map[string]string{}, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + }) + addConfigMapDeployment(p, "cm2", map[string]string{}, resourceOpts{ + name: "cm2", + namespace: p.TestSlug(), + }) + addConfigMapDeployment(p, "cm3", map[string]string{}, resourceOpts{ + name: "cm3", + namespace: p.TestSlug(), + annotations: map[string]string{ + "kluctl.io/skip-delete": "true", + }, + }) + addConfigMapDeployment(p, "cm4", map[string]string{}, resourceOpts{ + name: "cm4", + namespace: p.TestSlug(), + annotations: map[string]string{ + "helm.sh/resource-policy": "keep", + }, + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + cm2 := assertConfigMapExists(t, k, p.TestSlug(), "cm2") + assertConfigMapExists(t, k, p.TestSlug(), "cm3") + assertConfigMapExists(t, k, p.TestSlug(), "cm4") + + cm2.SetK8sAnnotation("kluctl.io/skip-delete", "true") + updateObject(t, k, cm2) + + p.KluctlMust(t, "delete", "--yes", "-t", "test") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm1") + cm2 = assertConfigMapExists(t, k, p.TestSlug(), "cm2") + assertConfigMapExists(t, k, p.TestSlug(), "cm3") + assertConfigMapExists(t, k, p.TestSlug(), "cm4") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm1 := assertConfigMapExists(t, k, p.TestSlug(), "cm1") + cm1.SetK8sAnnotation("kluctl.io/skip-delete", "true") + cm2.SetK8sAnnotation("kluctl.io/skip-delete", "false") + updateObject(t, k, cm1) + updateObject(t, k, cm2) + p.DeleteKustomizeDeployment("cm1") + p.DeleteKustomizeDeployment("cm2") + p.DeleteKustomizeDeployment("cm3") + p.KluctlMust(t, "prune", "--yes", "-t", "test") + cm1 = assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assertConfigMapNotExists(t, k, p.TestSlug(), "cm2") + assertConfigMapExists(t, k, p.TestSlug(), "cm3") + assertConfigMapExists(t, k, p.TestSlug(), "cm4") +} + +func TestForceReplaceSkipDelete(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_utils.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm1", map[string]string{ + "k1": "v1", + }, resourceOpts{ + name: "cm1", + namespace: p.TestSlug(), + annotations: map[string]string{ + "kluctl.io/skip-delete": "true", + }, + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm1 := assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assert.Equal(t, map[string]any{ + "k1": "v1", + }, cm1.Object["data"]) + + p.UpdateYaml("cm1/configmap-cm1.yml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField("v2", "data", "k1") + return nil + }, "") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + cm1 = assertConfigMapExists(t, k, p.TestSlug(), "cm1") + assert.Equal(t, map[string]any{ + "k1": "v2", + }, cm1.Object["data"]) + + invalidLabel := "invalid_label_" + strings.Repeat("x", 63) + p.UpdateYaml("cm1/configmap-cm1.yml", func(o *uo.UnstructuredObject) error { + o.SetK8sLabel(invalidLabel, "invalid_label") + return nil + }, "") + stdout, _, err := p.Kluctl(t, "deploy", "--yes", "-t", "test") + assert.Error(t, err) + assert.Contains(t, stdout, "invalid: metadata.labels") + // make sure it did not try to replace it + assertConfigMapExists(t, k, p.TestSlug(), "cm1") + + stdout, stderr, err := p.Kluctl(t, "deploy", "--yes", "-t", "test", "--force-replace-on-error") + assert.Error(t, err) + assert.Contains(t, stdout, "retrying with replace instead of patch") + assert.Contains(t, stderr, "skipped forced replace") + assert.Contains(t, stdout, "invalid: metadata.labels") + // make sure it did not try to replace it + assertConfigMapExists(t, k, p.TestSlug(), "cm1") +} diff --git a/e2e/sops_test.go b/e2e/sops_test.go new file mode 100644 index 000000000..7855d0636 --- /dev/null +++ b/e2e/sops_test.go @@ -0,0 +1,130 @@ +package e2e + +import ( + "github.com/getsops/sops/v3/age" + "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/kluctl/kluctl/v2/pkg/vars/sops_test_resources" + "github.com/stretchr/testify/assert" + "testing" +) + +func setSopsKey(p *test_project.TestProject) { + key, _ := sops_test_resources.TestResources.ReadFile("test-key.txt") + p.SetEnv(age.SopsAgeKeyEnv, string(key)) +} + +func TestSopsVars(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t, test_project.WithUseProcess(true)) + setSopsKey(p) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + addConfigMapDeployment(p, "cm", map[string]string{ + "v1": "{{ test1.test2 }}", + }, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + }) + p.UpdateDeploymentYaml("", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField([]map[string]any{ + { + "file": "encrypted-vars.yaml", + }, + }, "vars") + return nil + }) + + p.UpdateFile("encrypted-vars.yaml", func(f string) (string, error) { + b, _ := sops_test_resources.TestResources.ReadFile("test.yaml") + return string(b), nil + }, "") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + + cm := assertConfigMapExists(t, k, p.TestSlug(), "cm") + assertNestedFieldEquals(t, cm, map[string]any{ + "v1": "42", + }, "data") +} + +func TestSopsResources(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t, test_project.WithUseProcess(true)) + setSopsKey(p) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + p.UpdateDeploymentYaml("", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(p.TestSlug(), "overrideNamespace") + return nil + }) + + p.AddKustomizeDeployment("cm", []test_project.KustomizeResource{ + {Name: "encrypted-cm.yaml"}, + }, nil) + + p.UpdateFile("cm/encrypted-cm.yaml", func(f string) (string, error) { + b, _ := sops_test_resources.TestResources.ReadFile("test-configmap.yaml") + return string(b), nil + }, "") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + + cm := assertConfigMapExists(t, k, p.TestSlug(), "encrypted-cm") + assertNestedFieldEquals(t, cm, map[string]any{ + "a": "b", + }, "data") +} + +func TestSopsHelmValues(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := test_project.NewTestProject(t, test_project.WithUseProcess(true)) + setSopsKey(p) + + createNamespace(t, k, p.TestSlug()) + + charts := []test_utils.RepoChart{ + {ChartName: "test-chart1", Version: "0.1.0"}, + } + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Oci, "", charts) + + repo.Start(t) + + valuesBytes, err := sops_test_resources.TestResources.ReadFile("helm-values.yaml") + assert.NoError(t, err) + values1, err := uo.FromString(string(valuesBytes)) + assert.NoError(t, err) + + p.UpdateTarget("test", nil) + p.AddHelmDeployment("helm1", repo, "test-chart1", "0.1.0", "test-helm1", p.TestSlug(), values1.Object) + p.UpdateYaml("helm1/helm-chart.yaml", func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(true, "helmChart", "skipPrePull") + return nil + }, "") + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + + cm1 := assertConfigMapExists(t, k, p.TestSlug(), "test-helm1-test-chart1") + + assert.Equal(t, map[string]any{ + "a": "secret1", + "b": "secret2", + "version": "0.1.0", + "kubeVersion": k.ServerVersion.String(), + }, cm1.Object["data"]) +} diff --git a/e2e/source_override_test.go b/e2e/source_override_test.go new file mode 100644 index 000000000..a1171fd5c --- /dev/null +++ b/e2e/source_override_test.go @@ -0,0 +1,180 @@ +package e2e + +import ( + "fmt" + gittypes "github.com/kluctl/kluctl/lib/git/types" + "github.com/kluctl/kluctl/lib/yaml" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + "path/filepath" + "strings" + "testing" +) + +type preparedSourceOverrideTest struct { + p *test_project.TestProject + ip1 *test_project.TestProject + ip2 *test_project.TestProject + + repo *test_utils.TestHelmRepo + repoUrl1 string + repoUrl2 string + + overrideGroupDir string + override1 string + override2 string +} + +func prepareLocalSourceOverrideTest(t *testing.T, k *test_utils.EnvTestCluster, gs *test_utils.TestGitServer, oci bool) preparedSourceOverrideTest { + p := test_project.NewTestProject(t, test_project.WithGitServer(gs)) + ip1 := prepareIncludeProject(t, "include1", "", gs) + ip2 := prepareIncludeProject(t, "include2", "subDir", gs) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", func(target *uo.UnstructuredObject) {}) + + repo := test_utils.NewHelmTestRepo(test_utils.TestHelmRepo_Oci, "", nil) + if oci { + repo.Start(t) + } + + repo1 := repo.URL.String() + "/org1/include1" + repo2 := repo.URL.String() + "/org1/include2" + + if oci { + ip1.KluctlMust(t, "oci", "push", "--url", repo1) + ip2.KluctlMust(t, "oci", "push", "--url", repo2, "--project-dir", ip2.LocalWorkDir()) + } + + if oci { + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "oci": map[string]any{ + "url": repo1, + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "oci": map[string]any{ + "url": repo2, + "subDir": "subDir", + }, + })) + } else { + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip1.GitUrl(), + }, + })) + p.AddDeploymentItem("", uo.FromMap(map[string]interface{}{ + "git": map[string]any{ + "url": ip2.GitUrl(), + "subDir": "subDir", + }, + })) + } + + overrideGroupDir := t.TempDir() + + override1 := ip1.CopyProjectSourceTo(filepath.Join(overrideGroupDir, "include1")) + override2 := ip2.CopyProjectSourceTo(filepath.Join(overrideGroupDir, "include2")) + + cm, err := uo.FromFile(filepath.Join(override1, "cm", "configmap-include1-cm.yml")) + assert.NoError(t, err) + _ = cm.SetNestedField("o1", "data", "a") + _ = yaml.WriteYamlFile(filepath.Join(override1, "cm", "configmap-include1-cm.yml"), cm) + + cm, err = uo.FromFile(filepath.Join(override2, "subDir", "cm", "configmap-include2-cm.yml")) + assert.NoError(t, err) + _ = cm.SetNestedField("o2", "data", "a") + _ = yaml.WriteYamlFile(filepath.Join(override2, "subDir", "cm", "configmap-include2-cm.yml"), cm) + + return preparedSourceOverrideTest{ + p: p, + ip1: ip1, + ip2: ip2, + repo: repo, + repoUrl1: repo1, + repoUrl2: repo2, + overrideGroupDir: overrideGroupDir, + override1: override1, + override2: override2, + } +} + +func TestLocalGitOverride(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + pt := prepareLocalSourceOverrideTest(t, k, nil, false) + + u1, _ := gittypes.ParseGitUrl(pt.ip1.GitUrl()) + u2, _ := gittypes.ParseGitUrl(pt.ip2.GitUrl()) + k1 := u1.RepoKey().String() + k2 := u2.RepoKey().String() + + pt.p.KluctlMust(t, "deploy", "--yes", "-t", "test", + "--local-git-override", fmt.Sprintf("%s=%s", k1, pt.override1), + "--local-git-override", fmt.Sprintf("%s=%s", k2, pt.override2), + ) + cm := assertConfigMapExists(t, k, pt.p.TestSlug(), "include1-cm") + assertNestedFieldEquals(t, cm, "o1", "data", "a") + cm = assertConfigMapExists(t, k, pt.p.TestSlug(), "include2-cm") + assertNestedFieldEquals(t, cm, "o2", "data", "a") +} + +func TestGitGroup(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + gs := test_utils.NewTestGitServer(t) + pt := prepareLocalSourceOverrideTest(t, k, gs, false) + + u1, _ := gittypes.ParseGitUrl(pt.p.GitServer().GitUrl() + "/repos") + k1 := u1.RepoKey().String() + + pt.p.KluctlMust(t, "deploy", "--yes", "-t", "test", + "--local-git-group-override", fmt.Sprintf("%s=%s", k1, pt.overrideGroupDir), + ) + cm := assertConfigMapExists(t, k, pt.p.TestSlug(), "include1-cm") + assertNestedFieldEquals(t, cm, "o1", "data", "a") + cm = assertConfigMapExists(t, k, pt.p.TestSlug(), "include2-cm") + assertNestedFieldEquals(t, cm, "o2", "data", "a") +} + +func TestLocalOciOverride(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + pt := prepareLocalSourceOverrideTest(t, k, nil, true) + + k1 := strings.TrimPrefix(pt.repoUrl1, "oci://") + k2 := strings.TrimPrefix(pt.repoUrl2, "oci://") + + pt.p.KluctlMust(t, "deploy", "--yes", "-t", "test", + "--local-oci-override", fmt.Sprintf("%s=%s", k1, pt.override1), + "--local-oci-override", fmt.Sprintf("%s=%s", k2, pt.override2), + ) + cm := assertConfigMapExists(t, k, pt.p.TestSlug(), "include1-cm") + assertNestedFieldEquals(t, cm, "o1", "data", "a") + cm = assertConfigMapExists(t, k, pt.p.TestSlug(), "include2-cm") + assertNestedFieldEquals(t, cm, "o2", "data", "a") +} + +func TestLocalOciGroupOverride(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + pt := prepareLocalSourceOverrideTest(t, k, nil, true) + + k1 := strings.TrimPrefix(pt.repo.URL.String(), "oci://") + "/org1" + + pt.p.KluctlMust(t, "deploy", "--yes", "-t", "test", + "--local-oci-group-override", fmt.Sprintf("%s=%s", k1, pt.overrideGroupDir), + ) + cm := assertConfigMapExists(t, k, pt.p.TestSlug(), "include1-cm") + assertNestedFieldEquals(t, cm, "o1", "data", "a") + cm = assertConfigMapExists(t, k, pt.p.TestSlug(), "include2-cm") + assertNestedFieldEquals(t, cm, "o2", "data", "a") +} diff --git a/e2e/test-utils/envtest_cluster.go b/e2e/test-utils/envtest_cluster.go new file mode 100644 index 000000000..c960d2f0b --- /dev/null +++ b/e2e/test-utils/envtest_cluster.go @@ -0,0 +1,235 @@ +package test_utils + +import ( + "bytes" + "context" + "fmt" + kluctlv1 "github.com/kluctl/kluctl/v2/api/v1beta1" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/client-go/util/flowcontrol" + "net/http" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sync" + "testing" +) + +type EnvTestCluster struct { + CRDDirectoryPaths []string + + env envtest.Environment + envMutex sync.Mutex + started bool + + user *envtest.AuthenticatedUser + Kubeconfig []byte + Context string + config *rest.Config + + HttpClient *http.Client + DynamicClient dynamic.Interface + Client client.WithWatch + ServerVersion *version.Info + + callbackServer webhook.Server + callbackServerStop context.CancelFunc + + webhookHandlers []*CallbackHandlerEntry + webhookHandlersMutex sync.Mutex +} + +func CreateEnvTestCluster(context string) *EnvTestCluster { + k := &EnvTestCluster{ + Context: context, + env: envtest.Environment{ + Scheme: runtime.NewScheme(), + }, + } + return k +} + +func (k *EnvTestCluster) Start() error { + k.envMutex.Lock() + defer k.envMutex.Unlock() + + k.env.CRDDirectoryPaths = k.CRDDirectoryPaths + + _, err := k.env.Start() + if err != nil { + return err + } + k.started = true + + _ = kluctlv1.AddToScheme(k.env.Scheme) + _ = corev1.AddToScheme(k.env.Scheme) + + err = k.startCallbackServer() + if err != nil { + return err + } + + user, err := k.env.AddUser(envtest.User{Name: "default", Groups: []string{"system:masters"}}, &rest.Config{}) + if err != nil { + return err + } + k.user = user + + k.config = user.Config() + k.config.RateLimiter = flowcontrol.NewFakeAlwaysRateLimiter() + + kcfg, err := user.KubeConfig() + if err != nil { + return err + } + + kcfg = bytes.ReplaceAll(kcfg, []byte("envtest"), []byte(k.Context)) + + k.Kubeconfig = kcfg + + httpClient, err := rest.HTTPClientFor(k.config) + if err != nil { + return err + } + k.HttpClient = httpClient + + dynamicClient, err := dynamic.NewForConfigAndClient(k.config, k.HttpClient) + if err != nil { + return err + } + k.DynamicClient = dynamicClient + + c, err := client.NewWithWatch(k.config, client.Options{ + HTTPClient: httpClient, + Scheme: k.env.Scheme, + }) + k.Client = c + + discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(k.config, httpClient) + if err != nil { + return err + } + k.ServerVersion, err = discoveryClient.ServerVersion() + if err != nil { + return err + } + + return nil +} + +func (c *EnvTestCluster) Stop() { + c.envMutex.Lock() + defer c.envMutex.Unlock() + + if c.started { + _ = c.env.Stop() + c.started = false + } + if c.callbackServerStop != nil { + c.callbackServerStop() + c.callbackServerStop = nil + } +} + +func (c *EnvTestCluster) AddUser(user envtest.User, baseConfig *rest.Config) (*envtest.AuthenticatedUser, error) { + c.envMutex.Lock() + defer c.envMutex.Unlock() + return c.env.AddUser(user, baseConfig) +} + +// RESTConfig returns K8s client config to pass to clientset objects +func (c *EnvTestCluster) RESTConfig() *rest.Config { + return c.config +} + +func (c *EnvTestCluster) Get(gvr schema.GroupVersionResource, namespace string, name string) (*uo.UnstructuredObject, error) { + x, err := c.DynamicClient.Resource(gvr).Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + return uo.FromUnstructured(x), nil +} + +func (c *EnvTestCluster) MustGet(t *testing.T, gvr schema.GroupVersionResource, namespace string, name string) *uo.UnstructuredObject { + x, err := c.Get(gvr, namespace, name) + if err != nil { + t.Fatalf("error while getting %s/%s/%s: %s", gvr.String(), namespace, name, err.Error()) + } + return x +} + +func (c *EnvTestCluster) MustGetCoreV1(t *testing.T, resource string, namespace string, name string) *uo.UnstructuredObject { + return c.MustGet(t, schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: resource, + }, namespace, name) +} + +func (c *EnvTestCluster) List(gvr schema.GroupVersionResource, namespace string, labels map[string]string) ([]*uo.UnstructuredObject, error) { + labelSelector := "" + if len(labels) != 0 { + for k, v := range labels { + if labelSelector != "" { + labelSelector += "," + } + labelSelector += fmt.Sprintf("%s=%s", k, v) + } + } + + l, err := c.DynamicClient.Resource(gvr). + Namespace(namespace). + List(context.Background(), metav1.ListOptions{ + LabelSelector: labelSelector, + }) + if err != nil { + return nil, err + } + var ret []*uo.UnstructuredObject + for _, x := range l.Items { + x := x + ret = append(ret, uo.FromUnstructured(&x)) + } + return ret, nil +} + +func (c *EnvTestCluster) Apply(o *uo.UnstructuredObject) error { + var x unstructured.Unstructured + err := o.ToStruct(&x) + if err != nil { + return err + } + return c.Client.Patch(context.Background(), &x, client.Apply, client.FieldOwner("envtestcluster")) +} + +func (c *EnvTestCluster) MustApply(t *testing.T, o *uo.UnstructuredObject) { + err := c.Apply(o) + if err != nil { + t.Fatal(err) + } +} + +func (c *EnvTestCluster) ApplyStatus(o *uo.UnstructuredObject) error { + var x unstructured.Unstructured + err := o.ToStruct(&x) + if err != nil { + return err + } + return c.Client.SubResource("status").Patch(context.Background(), &x, client.Apply, client.FieldOwner("envtestcluster")) +} + +func (c *EnvTestCluster) MustApplyStatus(t *testing.T, o *uo.UnstructuredObject) { + err := c.ApplyStatus(o) + if err != nil { + t.Fatal(err) + } +} diff --git a/e2e/test-utils/envtest_cluster_callback.go b/e2e/test-utils/envtest_cluster_callback.go new file mode 100644 index 000000000..17d9518e0 --- /dev/null +++ b/e2e/test-utils/envtest_cluster_callback.go @@ -0,0 +1,166 @@ +package test_utils + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + admissionv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "net" + "net/http" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "time" +) + +type CallbackHandler func(request admission.Request) +type CallbackHandlerEntry struct { + GVR schema.GroupVersionResource + Callback CallbackHandler +} + +func (k *EnvTestCluster) buildServeCallback() http.Handler { + wh := &webhook.Admission{ + Handler: admission.HandlerFunc(func(ctx context.Context, request admission.Request) admission.Response { + k.handleWebhook(request) + return admission.Allowed("") + }), + } + wh.LogConstructor = func(base logr.Logger, req *admission.Request) logr.Logger { + return logr.New(log.NullLogSink{}) + } + return wh +} + +func (k *EnvTestCluster) startCallbackServer() error { + k.callbackServer = webhook.NewServer(webhook.Options{ + Host: k.env.WebhookInstallOptions.LocalServingHost, + Port: k.env.WebhookInstallOptions.LocalServingPort, + CertDir: k.env.WebhookInstallOptions.LocalServingCertDir, + }) + + for _, vwh := range k.env.WebhookInstallOptions.ValidatingWebhooks { + path := "/" + vwh.Name + k.callbackServer.Register(path, k.buildServeCallback()) + } + + ctx, cancel := context.WithCancel(context.Background()) + k.callbackServerStop = cancel + + go func() { + _ = k.callbackServer.Start(ctx) + }() + + tcpAddr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(k.env.WebhookInstallOptions.LocalServingHost, fmt.Sprintf("%d", k.env.WebhookInstallOptions.LocalServingPort))) + if err != nil { + return err + } + + endTime := time.Now().Add(time.Second * 10) + for true { + if time.Now().After(endTime) { + return fmt.Errorf("timeout while waiting for webhook server") + } + c, err := net.DialTCP("tcp", nil, tcpAddr) + if err != nil { + time.Sleep(100 * time.Millisecond) + continue + } + _ = c.Close() + break + } + + return nil +} + +func (k *EnvTestCluster) InitWebhookCallback(gvr schema.GroupVersionResource, isNamespaced bool) { + scope := admissionv1.ClusterScope + if isNamespaced { + scope = admissionv1.NamespacedScope + } + + failedTypeV1 := admissionv1.Fail + equivalentTypeV1 := admissionv1.Equivalent + noSideEffectsV1 := admissionv1.SideEffectClassNone + timeout := int32(30) + + group := gvr.Group + if gvr.Group == "" { + group = "none" + } + name := fmt.Sprintf("%s-%s-%s-callback", group, gvr.Version, gvr.Resource) + + k.env.WebhookInstallOptions.ValidatingWebhooks = append(k.env.WebhookInstallOptions.ValidatingWebhooks, &admissionv1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "ValidatingWebhookConfiguration", + APIVersion: "admissionregistration.k8s.io/v1", + }, + Webhooks: []admissionv1.ValidatingWebhook{ + { + Name: fmt.Sprintf("%s.callback.kubectl.io", name), + Rules: []admissionv1.RuleWithOperations{ + { + Operations: []admissionv1.OperationType{"CREATE", "UPDATE"}, + Rule: admissionv1.Rule{ + APIGroups: []string{gvr.Group}, + APIVersions: []string{gvr.Version}, + Resources: []string{gvr.Resource}, + Scope: &scope, + }, + }, + }, + FailurePolicy: &failedTypeV1, + MatchPolicy: &equivalentTypeV1, + SideEffects: &noSideEffectsV1, + ClientConfig: admissionv1.WebhookClientConfig{ + Service: &admissionv1.ServiceReference{ + Path: &name, + }, + }, + AdmissionReviewVersions: []string{"v1"}, + TimeoutSeconds: &timeout, + }, + }, + }) +} + +func (k *EnvTestCluster) handleWebhook(request admission.Request) { + k.webhookHandlersMutex.Lock() + defer k.webhookHandlersMutex.Unlock() + + for _, e := range k.webhookHandlers { + if e.GVR.Group == request.Resource.Group && e.GVR.Version == request.Resource.Version && e.GVR.Resource == request.Resource.Resource { + e.Callback(request) + } + } +} + +func (k *EnvTestCluster) AddWebhookHandler(gvr schema.GroupVersionResource, cb CallbackHandler) *CallbackHandlerEntry { + k.webhookHandlersMutex.Lock() + defer k.webhookHandlersMutex.Unlock() + + entry := &CallbackHandlerEntry{ + GVR: gvr, + Callback: cb, + } + k.webhookHandlers = append(k.webhookHandlers, entry) + + return entry +} + +func (k *EnvTestCluster) RemoveWebhookHandler(e *CallbackHandlerEntry) { + k.webhookHandlersMutex.Lock() + defer k.webhookHandlersMutex.Unlock() + old := k.webhookHandlers + k.webhookHandlers = make([]*CallbackHandlerEntry, 0, len(k.webhookHandlers)) + for _, e2 := range old { + if e != e2 { + k.webhookHandlers = append(k.webhookHandlers, e2) + } + } +} diff --git a/e2e/test-utils/helm_repo_utils.go b/e2e/test-utils/helm_repo_utils.go new file mode 100644 index 000000000..e88aecbc1 --- /dev/null +++ b/e2e/test-utils/helm_repo_utils.go @@ -0,0 +1,64 @@ +package test_utils + +import ( + "github.com/kluctl/kluctl/lib/yaml" + "github.com/kluctl/kluctl/v2/e2e/test-utils/test-helm-chart" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/getter" + "io/fs" + "os" + "path/filepath" + "testing" +) + +func CreateHelmDir(t *testing.T, name string, version string, dest string) { + err := fs.WalkDir(test_resources.HelmChartFS, ".", func(path string, d fs.DirEntry, err error) error { + if d.IsDir() { + return os.MkdirAll(filepath.Join(dest, path), 0o700) + } else { + b, err := test_resources.HelmChartFS.ReadFile(path) + if err != nil { + return err + } + return os.WriteFile(filepath.Join(dest, path), b, 0o600) + } + }) + if err != nil { + t.Fatal(err) + } + + c, err := uo.FromFile(filepath.Join(dest, "Chart.yaml")) + if err != nil { + t.Fatal(err) + } + + _ = c.SetNestedField(name, "name") + _ = c.SetNestedField(version, "version") + + err = yaml.WriteYamlFile(filepath.Join(dest, "Chart.yaml"), c) + if err != nil { + t.Fatal(err) + } +} + +func createHelmPackage(t *testing.T, name string, version string) string { + tmpDir := t.TempDir() + + CreateHelmDir(t, name, version, tmpDir) + + settings := cli.New() + client := action.NewPackage() + client.Destination = tmpDir + valueOpts := &values.Options{} + p := getter.All(settings) + vals, err := valueOpts.MergeValues(p) + retName, err := client.Run(tmpDir, vals) + if err != nil { + t.Fatal(err) + } + + return retName +} diff --git a/e2e/test-utils/helm_test_repo.go b/e2e/test-utils/helm_test_repo.go new file mode 100644 index 000000000..0f71e9562 --- /dev/null +++ b/e2e/test-utils/helm_test_repo.go @@ -0,0 +1,200 @@ +package test_utils + +import ( + "fmt" + "github.com/go-git/go-git/v5" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/kluctl/kluctl/lib/git/types" + cp "github.com/otiai10/copy" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/pusher" + registry2 "helm.sh/helm/v3/pkg/registry" + "helm.sh/helm/v3/pkg/repo" + "helm.sh/helm/v3/pkg/uploader" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "testing" +) + +type TestHelmRepoType int + +const ( + TestHelmRepo_Helm TestHelmRepoType = iota + TestHelmRepo_Oci + TestHelmRepo_Git + TestHelmRepo_Local +) + +type TestHelmRepo struct { + HttpServer TestHttpServer + GitServer *TestGitServer + + Type TestHelmRepoType + + Path string + Charts []RepoChart + + URL url.URL +} + +type RepoChart struct { + ChartName string + Version string +} + +func NewHelmTestRepo(helmType TestHelmRepoType, path string, charts []RepoChart) *TestHelmRepo { + return &TestHelmRepo{ + Type: helmType, + Path: path, + Charts: charts, + } +} + +func NewHelmTestRepoGit(t *testing.T, path string, charts []RepoChart, username string, password string) *TestHelmRepo { + r := NewHelmTestRepo(TestHelmRepo_Git, path, charts) + r.GitServer = NewTestGitServer(t, WithTestGitServerAuth(username, password)) + return r +} + +func NewHelmTestRepoLocal(path string) *TestHelmRepo { + return &TestHelmRepo{ + Type: TestHelmRepo_Local, + Path: path, + } +} + +func (s *TestHelmRepo) Start(t *testing.T) { + switch s.Type { + case TestHelmRepo_Helm: + s.startHelmRepo(t) + case TestHelmRepo_Oci: + s.startOciRepo(t) + case TestHelmRepo_Git: + s.startGitRepo(t) + } +} + +func (s *TestHelmRepo) startHelmRepo(t *testing.T) { + tmpDir := t.TempDir() + + for _, c := range s.Charts { + tgz := createHelmPackage(t, c.ChartName, c.Version) + _ = cp.Copy(tgz, filepath.Join(tmpDir, s.Path, filepath.Base(tgz))) + } + + fs := http.FileServer(http.FS(os.DirFS(tmpDir))) + s.HttpServer.Start(t, fs) + + i, err := repo.IndexDirectory(tmpDir, s.HttpServer.Server.URL) + if err != nil { + t.Fatal(err) + } + + i.SortEntries() + err = i.WriteFile(filepath.Join(tmpDir, s.Path, "index.yaml"), 0644) + if err != nil { + t.Fatal(err) + } + + path := s.Path + if path != "" { + path = "/" + path + } + + u, _ := url.Parse(s.HttpServer.Server.URL + path) + s.URL = *u +} + +func (s *TestHelmRepo) buildOciRegistryClient(t *testing.T) *registry2.Client { + var opts []registry2.ClientOption + if !s.HttpServer.TLSEnabled { + opts = append(opts, registry2.ClientOptPlainHTTP()) + } + + opts = append(opts, registry2.ClientOptHTTPClient(s.HttpServer.Server.Client())) + + if s.HttpServer.Password != "" { + tmpConfigFile := filepath.Join(t.TempDir(), "config.json") + opts = append(opts, registry2.ClientOptCredentialsFile(tmpConfigFile)) + } + + registryClient, err := registry2.NewClient(opts...) + if err != nil { + t.Fatal(err) + } + + if s.HttpServer.Password != "" { + var loginOpts []registry2.LoginOption + loginOpts = append(loginOpts, registry2.LoginOptBasicAuth(s.HttpServer.Username, s.HttpServer.Password)) + if !s.HttpServer.TLSEnabled { + loginOpts = append(loginOpts, registry2.LoginOptInsecure(true)) + } + err = registryClient.Login(s.URL.Host, loginOpts...) + if err != nil { + t.Fatal(err) + } + } + return registryClient +} + +func (s *TestHelmRepo) startOciRepo(t *testing.T) { + tmpDir := t.TempDir() + + ociRegistry := registry.New() + + s.HttpServer.Start(t, http.HandlerFunc(ociRegistry.ServeHTTP)) + + u, _ := url.Parse(s.HttpServer.Server.URL) + s.URL = *u + s.URL.Scheme = "oci" + + var out strings.Builder + settings := cli.New() + c := uploader.ChartUploader{ + Out: &out, + Pushers: pusher.All(settings), + Options: []pusher.Option{}, + } + + registryClient := s.buildOciRegistryClient(t) + c.Options = append(c.Options, pusher.WithRegistryClient(registryClient)) + + for _, chart := range s.Charts { + tgz := createHelmPackage(t, chart.ChartName, chart.Version) + _ = cp.Copy(tgz, filepath.Join(tmpDir, filepath.Base(tgz))) + + err := c.UploadTo(tgz, s.URL.String()) + if err != nil { + t.Fatal(err) + } + } + + if registryClient != nil { + registryClient.Logout(s.URL.Host) + } +} + +func (s *TestHelmRepo) startGitRepo(t *testing.T) { + s.GitServer.GitInit("helm-repo") + + for _, c := range s.Charts { + dir := s.GitServer.LocalWorkDir("helm-repo") + dir = filepath.Join(dir, s.Path, c.ChartName) + _ = os.MkdirAll(dir, 0700) + CreateHelmDir(t, c.ChartName, c.Version, dir) + msg := fmt.Sprintf("chart %s version %s", c.ChartName, c.Version) + hash := s.GitServer.CommitFiles("helm-repo", []string{c.ChartName}, false, msg) + _, err := s.GitServer.GetGitRepo("helm-repo").CreateTag(c.Version, hash, &git.CreateTagOptions{ + Message: msg, + }) + if err != nil { + t.Fatal(err) + } + } + + u := types.ParseGitUrlMust(s.GitServer.GitRepoUrl("helm-repo")) + s.URL = u.URL +} diff --git a/pkg/git/http-server/http.go b/e2e/test-utils/http-server/http.go similarity index 100% rename from pkg/git/http-server/http.go rename to e2e/test-utils/http-server/http.go diff --git a/pkg/git/http-server/utils.go b/e2e/test-utils/http-server/utils.go similarity index 100% rename from pkg/git/http-server/utils.go rename to e2e/test-utils/http-server/utils.go diff --git a/pkg/git/http-server/write_flusher.go b/e2e/test-utils/http-server/write_flusher.go similarity index 100% rename from pkg/git/http-server/write_flusher.go rename to e2e/test-utils/http-server/write_flusher.go diff --git a/e2e/test-utils/http_test_server.go b/e2e/test-utils/http_test_server.go new file mode 100644 index 000000000..ad5006bfb --- /dev/null +++ b/e2e/test-utils/http_test_server.go @@ -0,0 +1,255 @@ +package test_utils + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + port_tool "github.com/kluctl/kluctl/v2/e2e/test-utils/port-tool" + "math/big" + "net" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + "time" +) + +var nonLoopbackIp = findNonLoopbackIp() +var caBytes, serverCert, clientCert, clientKey, _ = certsetup() + +type TestHttpServer struct { + TLSEnabled bool + TLSClientCertEnabled bool + NoLoopbackProxyEnabled bool + + Username string + Password string + + Server *httptest.Server + ServerCAs []byte + ClientCAs []byte + ClientCert []byte + ClientKey []byte +} + +func (s *TestHttpServer) Start(t *testing.T, h http.Handler) { + authH := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if s.Username != "" || s.Password != "" { + u, p, ok := request.BasicAuth() + if !ok { + writer.Header().Add("WWW-Authenticate", "Basic") + http.Error(writer, "Auth header was incorrect", http.StatusUnauthorized) + return + } + if u != s.Username || p != s.Password { + http.Error(writer, "Auth header was incorrect", http.StatusUnauthorized) + return + } + } + h.ServeHTTP(writer, request) + }) + + s.Server = &httptest.Server{ + Listener: port_tool.NewListenerWithUniquePort("127.0.0.1"), + Config: &http.Server{Handler: authH}, + } + + if s.TLSEnabled { + s.Server.TLS = &tls.Config{ + Certificates: []tls.Certificate{*serverCert}, + } + s.ServerCAs = caBytes + + if s.TLSClientCertEnabled { + s.ClientCAs = caBytes + s.ClientCert = clientCert + s.ClientKey = clientKey + + certPool := x509.NewCertPool() + certPool.AppendCertsFromPEM(caBytes) + + s.Server.TLS.ClientAuth = tls.RequireAndVerifyClientCert + s.Server.TLS.ClientCAs = certPool + } + + s.Server.StartTLS() + } else { + s.Server.Start() + } + + transport := s.Server.Client().Transport.(*http.Transport) + if s.TLSClientCertEnabled { + clientCertX, _ := tls.X509KeyPair(clientCert, clientKey) + + transport.TLSClientConfig.Certificates = append(transport.TLSClientConfig.Certificates, clientCertX) + } + + if s.NoLoopbackProxyEnabled { + s.startTLSProxy(t) + } + + t.Cleanup(s.Server.Close) +} + +// the TLS proxy is required because oras is treating local registries as non-tls, so we must use a non-loopback IP +func (s *TestHttpServer) startTLSProxy(t *testing.T) { + u, _ := url.Parse(s.Server.URL) + + localhostIp := net.ParseIP(u.Hostname()) + port, _ := strconv.ParseInt(u.Port(), 10, 32) + + frontendAddr := &net.TCPAddr{IP: nonLoopbackIp} + backendAddr := &net.TCPAddr{IP: localhostIp, Port: int(port)} + + l := port_tool.NewListenerWithUniquePort(frontendAddr.IP.String()) + p, err := NewTCPProxy(l, backendAddr) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + p.Close() + }) + + u.Host = p.FrontendAddr().String() + s.Server.URL = u.String() + + go func() { + p.Run() + }() +} + +func findNonLoopbackIp() net.IP { + ifs, _ := net.Interfaces() + var goodAddr *net.IPNet +outer: + for _, x := range ifs { + addrs, _ := x.Addrs() + if len(addrs) == 0 { + continue + } + if (x.Flags&net.FlagRunning) != 0 && (x.Flags&net.FlagLoopback) == 0 { + for _, a := range addrs { + if a.Network() == "ip+net" { + a2 := a.(*net.IPNet) + if a2.IP.Equal(a2.IP.To4()) { + goodAddr = a2 + break outer + } + } + } + } + } + if goodAddr == nil { + panic("no good listen address found") + } + return goodAddr.IP +} + +func certsetup() (caPEMBytes []byte, serverCert *tls.Certificate, clientCertPEMBytes []byte, clientKeyPEMBytes []byte, err error) { + // set up our CA certificate + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"Company, INC."}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{"Golden Gate Bridge"}, + PostalCode: []string{"94016"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + // create our private and public key + caPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, nil, nil, err + } + + // create the CA + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + return nil, nil, nil, nil, err + } + + // pem encode + caPEM := new(bytes.Buffer) + pem.Encode(caPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + caPEMBytes = caPEM.Bytes() + + caPrivKeyPEM := new(bytes.Buffer) + pem.Encode(caPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), + }) + + serverCertPEM, serverKeyPEM := generateCert(ca, caPrivKey) + + serverCert2, err := tls.X509KeyPair(serverCertPEM, serverKeyPEM) + if err != nil { + return nil, nil, nil, nil, err + } + serverCert = &serverCert2 + + clientCertPEMBytes, clientKeyPEMBytes = generateCert(ca, caPrivKey) + + return +} + +func generateCert(ca *x509.Certificate, caPrivKey *rsa.PrivateKey) ([]byte, []byte) { + // set up our server certificate + cert := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"Company, INC."}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{"Golden Gate Bridge"}, + PostalCode: []string{"94016"}, + }, + IPAddresses: []net.IP{nonLoopbackIp, net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil + } + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey) + if err != nil { + return nil, nil + } + + certPEM := new(bytes.Buffer) + pem.Encode(certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + + certPrivKeyPEM := new(bytes.Buffer) + pem.Encode(certPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), + }) + + return certPEM.Bytes(), certPrivKeyPEM.Bytes() +} diff --git a/e2e/test-utils/port-tool/port-tool.go b/e2e/test-utils/port-tool/port-tool.go new file mode 100644 index 000000000..53a81262d --- /dev/null +++ b/e2e/test-utils/port-tool/port-tool.go @@ -0,0 +1,43 @@ +package port_tool + +import ( + "fmt" + "net" + "sync" +) + +var nextPort = 30000 +var mutex sync.Mutex + +func NextFreePort(ip string) int { + mutex.Lock() + defer mutex.Unlock() + + for { + a := net.TCPAddr{ + IP: net.ParseIP(ip), + Port: nextPort, + } + var ra net.TCPAddr + nextPort++ + c, err := net.DialTCP("tcp", &a, &ra) + if err == nil { + c.Close() + continue + } + return a.Port + } +} + +func NewListenerWithUniquePort(host string) net.Listener { + mutex.Lock() + defer mutex.Unlock() + + for { + l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, nextPort)) + nextPort++ + if err == nil { + return l + } + } +} diff --git a/e2e/test-utils/tcp_proxy.go b/e2e/test-utils/tcp_proxy.go new file mode 100644 index 000000000..d9b8bc56f --- /dev/null +++ b/e2e/test-utils/tcp_proxy.go @@ -0,0 +1,97 @@ +package test_utils + +import ( + "io" + "net" + "syscall" +) + +// TCPProxy is a proxy for TCP connections. It implements the Proxy interface to +// handle TCP traffic forwarding between the frontend and backend addresses. +type TCPProxy struct { + listener net.Listener + frontendAddr *net.TCPAddr + backendAddr *net.TCPAddr +} + +// NewTCPProxy creates a new TCPProxy. +func NewTCPProxy(listener net.Listener, backendAddr *net.TCPAddr, ops ...func(*TCPProxy)) (*TCPProxy, error) { + // If the port in frontendAddr was 0 then ListenTCP will have a picked + // a port to listen on, hence the call to Addr to get that actual port: + proxy := &TCPProxy{ + listener: listener, + frontendAddr: listener.Addr().(*net.TCPAddr), + backendAddr: backendAddr, + } + + for _, op := range ops { + op(proxy) + } + + return proxy, nil +} + +func (proxy *TCPProxy) clientLoop(client *net.TCPConn, quit chan bool) { + backend, err := net.DialTCP("tcp", nil, proxy.backendAddr) + if err != nil { + client.Close() + return + } + + event := make(chan int64) + var broker = func(to, from *net.TCPConn) { + written, err := io.Copy(to, from) + if err != nil { + // If the socket we are writing to is shutdown with + // SHUT_WR, forward it to the other end of the pipe: + if err, ok := err.(*net.OpError); ok && err.Err == syscall.EPIPE { + from.CloseWrite() + } + } + to.CloseRead() + event <- written + } + + go broker(client, backend) + go broker(backend, client) + + var transferred int64 + for i := 0; i < 2; i++ { + select { + case written := <-event: + transferred += written + case <-quit: + // Interrupt the two brokers and "join" them. + client.Close() + backend.Close() + for ; i < 2; i++ { + transferred += <-event + } + return + } + } + client.Close() + backend.Close() +} + +// Run starts forwarding the traffic using TCP. +func (proxy *TCPProxy) Run() { + quit := make(chan bool) + defer close(quit) + for { + client, err := proxy.listener.Accept() + if err != nil { + return + } + go proxy.clientLoop(client.(*net.TCPConn), quit) + } +} + +// Close stops forwarding the traffic. +func (proxy *TCPProxy) Close() { proxy.listener.Close() } + +// FrontendAddr returns the TCP address on which the proxy is listening. +func (proxy *TCPProxy) FrontendAddr() net.Addr { return proxy.frontendAddr } + +// BackendAddr returns the TCP proxied address. +func (proxy *TCPProxy) BackendAddr() net.Addr { return proxy.backendAddr } diff --git a/e2e/test-utils/test-helm-chart/.helmignore b/e2e/test-utils/test-helm-chart/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/e2e/test-utils/test-helm-chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/e2e/test-utils/test-helm-chart/Chart.yaml b/e2e/test-utils/test-helm-chart/Chart.yaml new file mode 100644 index 000000000..0f3baa0c1 --- /dev/null +++ b/e2e/test-utils/test-helm-chart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: test-helm-chart +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/e2e/test-utils/test-helm-chart/resources.go b/e2e/test-utils/test-helm-chart/resources.go new file mode 100644 index 000000000..2ad97a675 --- /dev/null +++ b/e2e/test-utils/test-helm-chart/resources.go @@ -0,0 +1,8 @@ +package test_resources + +import ( + "embed" +) + +//go:embed all:* +var HelmChartFS embed.FS diff --git a/e2e/test-utils/test-helm-chart/templates/_helpers.tpl b/e2e/test-utils/test-helm-chart/templates/_helpers.tpl new file mode 100644 index 000000000..bcbf57e17 --- /dev/null +++ b/e2e/test-utils/test-helm-chart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "test-helm-chart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "test-helm-chart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "test-helm-chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "test-helm-chart.labels" -}} +helm.sh/chart: {{ include "test-helm-chart.chart" . }} +{{ include "test-helm-chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "test-helm-chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "test-helm-chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "test-helm-chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "test-helm-chart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/e2e/test-utils/test-helm-chart/templates/configmap.yaml b/e2e/test-utils/test-helm-chart/templates/configmap.yaml new file mode 100644 index 000000000..f79c8c650 --- /dev/null +++ b/e2e/test-utils/test-helm-chart/templates/configmap.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "test-helm-chart.fullname" . }} + labels: + {{- include "test-helm-chart.labels" . | nindent 4 }} +data: + a: {{ .Values.data.a }} + b: {{ .Values.data.b }} + version: {{ .Chart.Version }} + kubeVersion: {{ .Capabilities.KubeVersion }} + {{ if .Values.lookup }} + {{ $lookupObj := lookup "v1" "ConfigMap" .Values.lookupNamespace .Values.lookupName }} + {{ if $lookupObj }} + lookup: {{ $lookupObj.data.a -}} + {{ else }} + lookup: lookupReturnedNil + {{ end }} + {{ end }} diff --git a/e2e/test-utils/test-helm-chart/values.yaml b/e2e/test-utils/test-helm-chart/values.yaml new file mode 100644 index 000000000..9d4b13a77 --- /dev/null +++ b/e2e/test-utils/test-helm-chart/values.yaml @@ -0,0 +1,3 @@ +data: + a: v1 + b: v2 diff --git a/e2e/test-utils/test_git_server.go b/e2e/test-utils/test_git_server.go new file mode 100644 index 000000000..23ff128d3 --- /dev/null +++ b/e2e/test-utils/test_git_server.go @@ -0,0 +1,370 @@ +package test_utils + +import ( + "bytes" + "context" + "fmt" + config2 "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/huandu/xstrings" + "github.com/kluctl/kluctl/v2/e2e/test-utils/http-server" + port_tool "github.com/kluctl/kluctl/v2/e2e/test-utils/port-tool" + "net" + "net/http" + "os" + "path/filepath" + "sigs.k8s.io/yaml" + "strings" + "sync" + "testing" + + "github.com/go-git/go-git/v5" +) + +type TestGitServer struct { + t *testing.T + + baseDir string + + gitServer *http_server.Server + gitHttpServer *http.Server + gitServerPort int + + authUsername string + authPassword string + failWhenAuth bool + + cleanupMutex sync.RWMutex + cleanupDoneCh chan struct{} +} + +type TestGitServerOpt func(*TestGitServer) + +func WithTestGitServerAuth(username string, password string) TestGitServerOpt { + return func(server *TestGitServer) { + server.authUsername = username + server.authPassword = password + } +} + +func WithTestGitServerFailWhenAuth(fail bool) TestGitServerOpt { + return func(server *TestGitServer) { + server.failWhenAuth = fail + } +} + +func NewTestGitServer(t *testing.T, opts ...TestGitServerOpt) *TestGitServer { + p := &TestGitServer{ + t: t, + baseDir: t.TempDir(), + cleanupDoneCh: make(chan struct{}), + } + + for _, o := range opts { + o(p) + } + + p.initGitServer() + + t.Cleanup(func() { + p.Cleanup() + }) + + return p +} + +func (p *TestGitServer) initGitServer() { + p.gitServer = http_server.New(p.baseDir) + + handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + p.cleanupMutex.RLock() + defer p.cleanupMutex.RUnlock() + + if p.gitServer == nil { + http.Error(writer, "server closed", http.StatusInternalServerError) + return + } + + username, password, ok := request.BasicAuth() + if p.failWhenAuth { + if ok { + writer.WriteHeader(http.StatusUnauthorized) + _, _ = writer.Write([]byte("forcing test failure")) + return + } + } else if p.authUsername != "" { + if p.authUsername != username || p.authPassword != password { + writer.WriteHeader(http.StatusUnauthorized) + _, _ = writer.Write([]byte("invalid credentials")) + return + } + } + p.gitServer.ServeHTTP(writer, request) + }) + + p.gitHttpServer = &http.Server{ + Addr: "127.0.0.1:0", + Handler: handler, + } + + ln := port_tool.NewListenerWithUniquePort("127.0.0.1") + a := ln.Addr().(*net.TCPAddr) + p.gitServerPort = a.Port + + go func() { + err := p.gitHttpServer.Serve(ln) + if err != nil { + p.t.Logf("gitHttpServer.Serve() with port %d returned error: %s", p.gitServerPort, err.Error()) + } else { + p.t.Logf("gitHttpServer.Serve() with port %d returned with no error", p.gitServerPort) + } + close(p.cleanupDoneCh) + }() +} + +func (p *TestGitServer) Cleanup() { + p.cleanupMutex.Lock() + defer p.cleanupMutex.Unlock() + + p.t.Logf("gitHttpServer.Cleanup() called for port %d", p.gitServerPort) + + if p.gitHttpServer != nil { + _ = p.gitHttpServer.Shutdown(context.Background()) + p.gitHttpServer = nil + p.gitServer = nil + <-p.cleanupDoneCh + } + + p.baseDir = "" +} + +func (p *TestGitServer) GitInit(repo string) { + gitDir := p.LocalGitDir(repo) + workDir := p.LocalWorkDir(repo) + + err := os.MkdirAll(workDir, 0o700) + if err != nil { + p.t.Fatal(err) + } + + r, err := git.PlainInit(workDir, false) + if err != nil { + p.t.Fatal(err) + } + err = os.Symlink(filepath.Join(workDir, ".git"), gitDir) + if err != nil { + p.t.Fatal(err) + } + + _, err = r.CreateRemote(&config2.RemoteConfig{ + Name: "origin", + URLs: []string{p.GitRepoUrl(repo)}, + }) + if err != nil { + p.t.Fatal(err) + } + + config, err := r.Config() + if err != nil { + p.t.Fatal(err) + } + + config.User.Name = "Test User" + config.User.Email = "no@mail.com" + config.Author = config.User + config.Committer = config.User + err = r.SetConfig(config) + if err != nil { + p.t.Fatal(err) + } + f, err := os.Create(filepath.Join(workDir, ".dummy")) + if err != nil { + p.t.Fatal(err) + } + _ = f.Close() + + wt, err := r.Worktree() + if err != nil { + p.t.Fatal(err) + } + _, err = wt.Add(".dummy") + if err != nil { + p.t.Fatal(err) + } + _, err = wt.Commit("initial", &git.CommitOptions{}) + if err != nil { + p.t.Fatal(err) + } +} + +func (p *TestGitServer) CommitFiles(repo string, add []string, all bool, message string) plumbing.Hash { + r, err := git.PlainOpen(p.LocalWorkDir(repo)) + if err != nil { + p.t.Fatal(err) + } + wt, err := r.Worktree() + if err != nil { + p.t.Fatal(err) + } + for _, a := range add { + _, err = wt.Add(a) + if err != nil { + p.t.Fatal(err) + } + } + hash, err := wt.Commit(message, &git.CommitOptions{ + All: all, + }) + if err != nil { + p.t.Fatal(err) + } + return hash +} + +func (p *TestGitServer) CommitFile(repo string, pth string, message string, content []byte) { + fullPath := filepath.Join(p.LocalWorkDir(repo), pth) + + dir, _ := filepath.Split(fullPath) + if dir != "" { + err := os.MkdirAll(dir, 0o700) + if err != nil { + panic(err) + } + } + + err := os.WriteFile(fullPath, content, 0o600) + if err != nil { + p.t.Fatal(err) + } + if message == "" { + message = fmt.Sprintf("update %s", filepath.Join(repo, pth)) + } + p.CommitFiles(repo, []string{pth}, false, message) +} + +func (p *TestGitServer) UpdateFile(repo string, pth string, update func(f string) (string, error), message string) { + fullPath := filepath.Join(p.LocalWorkDir(repo), pth) + f := "" + if _, err := os.Stat(fullPath); err == nil { + b, err := os.ReadFile(fullPath) + if err != nil { + p.t.Fatal(err) + } + f = string(b) + } + + newF, err := update(f) + if err != nil { + p.t.Fatal(err) + } + + if f == newF { + return + } + err = os.MkdirAll(filepath.Dir(fullPath), 0o700) + if err != nil { + p.t.Fatal(err) + } + err = os.WriteFile(fullPath, []byte(newF), 0o600) + if err != nil { + p.t.Fatal(err) + } + p.CommitFiles(repo, []string{pth}, false, message) +} + +func (p *TestGitServer) UpdateYaml(repo string, pth string, update func(o map[string]any) error, message string) { + fullPath := filepath.Join(p.LocalWorkDir(repo), pth) + + var o map[string]any + var origBytes []byte + isNew := false + if _, err := os.Stat(fullPath); err == nil { + origBytes, err = os.ReadFile(fullPath) + if err != nil { + p.t.Fatal(err) + } + err = yaml.Unmarshal(origBytes, &o) + if err != nil { + p.t.Fatal(err) + } + } else { + o = map[string]any{} + isNew = true + } + + err := update(o) + if err != nil { + p.t.Fatal(err) + } + + newBytes, err := yaml.Marshal(o) + if err != nil { + p.t.Fatal(err) + } + if !isNew && bytes.Equal(origBytes, newBytes) { + return + } + p.CommitFile(repo, pth, message, newBytes) +} + +func (p *TestGitServer) DeleteFile(repo string, pth string, message string) { + fullPath := filepath.Join(p.LocalWorkDir(repo), pth) + _ = os.Remove(fullPath) + + if message == "" { + message = fmt.Sprintf("delete %s", filepath.Join(repo, pth)) + } + p.CommitFiles(repo, []string{pth}, false, message) +} + +func (p *TestGitServer) ReadFile(repo string, pth string) []byte { + fullPath := filepath.Join(p.LocalWorkDir(repo), pth) + b, err := os.ReadFile(fullPath) + if err != nil { + p.t.Fatal(err) + } + return b +} + +func (p *TestGitServer) GitHost() string { + return fmt.Sprintf("localhost:%d", p.gitServerPort) +} + +func (p *TestGitServer) GitUrl() string { + return fmt.Sprintf("http://localhost:%d/%s", p.gitServerPort, p.testNameSlug()) +} + +func (p *TestGitServer) GitRepoUrl(repo string) string { + return fmt.Sprintf("%s/%s", p.GitUrl(), repo) +} + +func (p *TestGitServer) testNameSlug() string { + n := xstrings.ToKebabCase(p.t.Name()) + n = strings.ReplaceAll(n, "/", "-") + return n +} + +func (p *TestGitServer) LocalGitDir(repo string) string { + return filepath.Join(p.baseDir, p.testNameSlug(), repo) +} + +func (p *TestGitServer) LocalWorkDir(repo string) string { + return filepath.Join(p.baseDir, p.testNameSlug(), repo) + "-workdir" +} + +func (p *TestGitServer) GetGitRepo(repo string) *git.Repository { + r, err := git.PlainOpen(p.LocalWorkDir(repo)) + if err != nil { + p.t.Fatal(err) + } + return r +} + +func (p *TestGitServer) GetWorktree(repo string) *git.Worktree { + r := p.GetGitRepo(repo) + wt, err := r.Worktree() + if err != nil { + p.t.Fatal(err) + } + return wt +} diff --git a/e2e/test_project/kluctl_execute.go b/e2e/test_project/kluctl_execute.go new file mode 100644 index 000000000..602d62893 --- /dev/null +++ b/e2e/test_project/kluctl_execute.go @@ -0,0 +1,64 @@ +package test_project + +import ( + "bytes" + "context" + status2 "github.com/kluctl/kluctl/lib/status" + "github.com/kluctl/kluctl/v2/cmd/kluctl/commands" + "github.com/kluctl/kluctl/v2/pkg/utils" + "strings" + "sync" + "testing" +) + +func KluctlExecute(t *testing.T, ctx context.Context, logFn func(args ...any), args ...string) (string, string, error) { + t.Logf("Runnning kluctl: %s", strings.Join(args, " ")) + + var m sync.Mutex + + stdoutBuf := bytes.NewBuffer(nil) + stderrBuf := bytes.NewBuffer(nil) + + stdout := status2.NewLineRedirector(func(line string) { + m.Lock() + defer m.Unlock() + logFn(line) + stdoutBuf.WriteString(line + "\n") + }) + stderr := status2.NewLineRedirector(func(line string) { + m.Lock() + defer m.Unlock() + logFn(line) + stderrBuf.WriteString(line + "\n") + }) + + if utils.GetTmpBaseDirNoDefault(ctx) == "" { + ctx = utils.WithTmpBaseDir(ctx, t.TempDir()) + } + if utils.GetCacheDirNoDefault(ctx) == "" { + ctx = utils.WithCacheDir(ctx, t.TempDir()) + } + ctx = commands.WithStdStreams(ctx, stdout, stderr) + sh := status2.NewSimpleStatusHandler(func(level status2.Level, message string) { + _, _ = stderr.Write([]byte(message + "\n")) + }, true) + defer func() { + if sh != nil { + sh.Stop() + } + }() + ctx = status2.NewContext(ctx, sh) + err := commands.Execute(ctx, args, nil) + sh.Stop() + sh = nil + + _ = stdout.Close() + _ = stderr.Close() + + <-stdout.Done() + <-stderr.Done() + + m.Lock() + defer m.Unlock() + return stdoutBuf.String(), stderrBuf.String(), err +} diff --git a/e2e/test_project/project.go b/e2e/test_project/project.go new file mode 100644 index 000000000..27b02f7a8 --- /dev/null +++ b/e2e/test_project/project.go @@ -0,0 +1,626 @@ +package test_project + +import ( + "context" + "fmt" + "github.com/go-git/go-git/v5" + "github.com/huandu/xstrings" + "github.com/jinzhu/copier" + gittypes "github.com/kluctl/kluctl/lib/git/types" + "github.com/kluctl/kluctl/lib/yaml" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/pkg/types/result" + "github.com/kluctl/kluctl/v2/pkg/utils" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + cp "github.com/otiai10/copy" + "os" + "os/exec" + "path" + "path/filepath" + "reflect" + "strings" + "testing" +) + +type TestProject struct { + initialName string + + tmpBaseDir string + cacheDir string + + extraEnv utils.OrderedMap[string, string] + extraArgs []string + useProcess bool + skipProjectDirArg bool + bare bool + + gitServer *test_utils.TestGitServer + gitRepoName string + gitSubDir string +} + +type TestProjectOption func(p *TestProject) + +func WithTmpBaseDir(baseDir string) TestProjectOption { + return func(p *TestProject) { + p.tmpBaseDir = baseDir + } +} + +func WithCacheDir(dir string) TestProjectOption { + return func(p *TestProject) { + p.cacheDir = dir + } +} + +func WithUseProcess(useProcess bool) TestProjectOption { + return func(p *TestProject) { + p.useProcess = useProcess + } +} + +func WithGitServer(s *test_utils.TestGitServer) TestProjectOption { + return func(p *TestProject) { + p.gitServer = s + } +} + +func WithRepoName(n string) TestProjectOption { + return func(p *TestProject) { + p.gitRepoName = n + } +} + +func WithGitSubDir(subDir string) TestProjectOption { + return func(p *TestProject) { + p.gitSubDir = subDir + } +} + +func WithBareProject() TestProjectOption { + return func(p *TestProject) { + p.bare = true + } +} + +func WithSkipProjectDirArg(b bool) TestProjectOption { + return func(p *TestProject) { + p.skipProjectDirArg = b + } +} + +func NewTestProject(t *testing.T, opts ...TestProjectOption) *TestProject { + p := &TestProject{ + initialName: t.Name(), + gitRepoName: "kluctl-project", + } + + for _, o := range opts { + o(p) + } + + if p.tmpBaseDir == "" { + p.tmpBaseDir = t.TempDir() + } + if p.cacheDir == "" { + p.cacheDir = t.TempDir() + } + + if p.gitServer == nil { + p.gitServer = test_utils.NewTestGitServer(t) + } + if !utils.IsDirectory(p.gitServer.LocalWorkDir(p.gitRepoName)) { + p.gitServer.GitInit(p.gitRepoName) + } + + if !p.bare { + p.UpdateKluctlYaml(func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(fmt.Sprintf("%s-{{ target.name or 'no-name' }}", p.TestSlug()), "discriminator") + return nil + }) + p.UpdateDeploymentYaml(".", func(c *uo.UnstructuredObject) error { + return nil + }) + } + return p +} + +func (p *TestProject) SetSkipProjectDirArg(b bool) { + p.skipProjectDirArg = b +} + +func (p *TestProject) IsUseProcess() bool { + return p.useProcess +} + +func (p *TestProject) GitServer() *test_utils.TestGitServer { + return p.gitServer +} + +func (p *TestProject) TestSlug() string { + n := p.initialName + n = xstrings.ToKebabCase(n) + n = strings.ReplaceAll(n, "/", "-") + var x []string + for _, y := range strings.Split(n, "-") { + if y == "test" { + continue + } + x = append(x, y) + } + return strings.Join(x, "-") +} + +func (p *TestProject) Discriminator(targetName string) string { + if targetName == "" { + targetName = "no-name" + } + return fmt.Sprintf("%s-%s", p.TestSlug(), targetName) +} + +func (p *TestProject) SetEnv(k string, v string) { + p.extraEnv.Set(k, v) +} + +func (p *TestProject) AddExtraArgs(a ...string) { + p.extraArgs = append(p.extraArgs, a...) +} + +func (p *TestProject) UpdateKluctlYaml(update func(o *uo.UnstructuredObject) error) { + p.UpdateYaml(".kluctl.yml", update, "") +} + +func (p *TestProject) UpdateDeploymentYaml(dir string, update func(o *uo.UnstructuredObject) error) { + p.UpdateYaml(filepath.Join(dir, "deployment.yml"), func(o *uo.UnstructuredObject) error { + if dir == "." { + o.SetNestedField(p.TestSlug(), "commonLabels", "project_name") + } + return update(o) + }, "") +} + +func (p *TestProject) UpdateYaml(pth string, update func(o *uo.UnstructuredObject) error, message string) { + p.gitServer.UpdateYaml(p.gitRepoName, path.Join(p.gitSubDir, pth), func(o map[string]any) error { + u := uo.FromMap(o) + err := update(u) + if err != nil { + return err + } + _ = copier.CopyWithOption(&o, &u.Object, copier.Option{DeepCopy: true}) + return nil + }, message) +} + +func (p *TestProject) UpdateFile(pth string, update func(f string) (string, error), message string) { + p.gitServer.UpdateFile(p.gitRepoName, path.Join(p.gitSubDir, pth), update, message) +} + +func (p *TestProject) DeleteFile(pth string, message string) { + p.gitServer.DeleteFile(p.gitRepoName, path.Join(p.gitSubDir, pth), message) +} + +func (p *TestProject) GetYaml(path string) *uo.UnstructuredObject { + o, err := uo.FromFile(filepath.Join(p.LocalProjectDir(), path)) + if err != nil { + panic(err) + } + return o +} + +func (p *TestProject) GetDeploymentYaml(dir string) *uo.UnstructuredObject { + return p.GetYaml(filepath.Join(dir, "deployment.yml")) +} + +func (p *TestProject) ListDeploymentItemPathes(dir string, fullPath bool) []string { + var ret []string + o := p.GetDeploymentYaml(dir) + l, _, err := o.GetNestedObjectList("deployments") + if err != nil { + panic(err) + } + for _, x := range l { + pth, ok, _ := x.GetNestedString("path") + if ok { + x := pth + if fullPath { + x = filepath.Join(dir, pth) + } + ret = append(ret, x) + } + pth, ok, _ = x.GetNestedString("include") + if ok { + ret = append(ret, p.ListDeploymentItemPathes(filepath.Join(dir, pth), fullPath)...) + } + } + return ret +} + +func (p *TestProject) UpdateKustomizeDeployment(dir string, update func(o *uo.UnstructuredObject, wt *git.Worktree) error) { + wt := p.gitServer.GetWorktree(p.gitRepoName) + + pth := filepath.Join(dir, "kustomization.yml") + p.UpdateYaml(pth, func(o *uo.UnstructuredObject) error { + return update(o, wt) + }, fmt.Sprintf("Update kustomization.yml for %s", dir)) +} + +func (p *TestProject) UpdateTarget(name string, cb func(target *uo.UnstructuredObject)) { + p.UpdateNamedListItem(uo.KeyPath{"targets"}, name, cb) +} + +func (p *TestProject) UpdateSecretSet(name string, cb func(secretSet *uo.UnstructuredObject)) { + p.UpdateNamedListItem(uo.KeyPath{"secretsConfig", "secretSets"}, name, cb) +} + +func (p *TestProject) UpdateNamedListItem(path uo.KeyPath, name string, cb func(item *uo.UnstructuredObject)) { + if cb == nil { + cb = func(target *uo.UnstructuredObject) {} + } + + p.UpdateKluctlYaml(func(o *uo.UnstructuredObject) error { + l, _, _ := o.GetNestedObjectList(path...) + var newList []*uo.UnstructuredObject + found := false + for _, item := range l { + n, _, _ := item.GetNestedString("name") + if n == name { + cb(item) + found = true + } + newList = append(newList, item) + } + if !found { + n := uo.FromMap(map[string]interface{}{ + "name": name, + }) + cb(n) + newList = append(newList, n) + } + + _ = o.SetNestedObjectList(newList, path...) + return nil + }) +} + +func (p *TestProject) UpdateDeploymentItems(dir string, update func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject) { + p.UpdateDeploymentYaml(dir, func(o *uo.UnstructuredObject) error { + items, _, _ := o.GetNestedObjectList("deployments") + items = update(items) + return o.SetNestedField(items, "deployments") + }) +} + +func (p *TestProject) AddDeploymentItem(dir string, item *uo.UnstructuredObject) { + p.UpdateDeploymentItems(dir, func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + for _, x := range items { + if reflect.DeepEqual(x, item) { + return items + } + } + items = append(items, item) + return items + }) +} + +func (p *TestProject) AddDeploymentInclude(dir string, includePath string, tags []string) { + n := uo.FromMap(map[string]interface{}{ + "include": includePath, + }) + if len(tags) != 0 { + n.SetNestedField(tags, "tags") + } + p.AddDeploymentItem(dir, n) +} + +func (p *TestProject) AddDeploymentIncludes(dir string) { + var pp []string + for _, x := range strings.Split(dir, "/") { + if x != "." { + p.AddDeploymentInclude(filepath.Join(pp...), x, nil) + } + pp = append(pp, x) + } +} + +func (p *TestProject) AddKustomizeDeployment(dir string, resources []KustomizeResource, tags []string) { + deploymentDir := filepath.Dir(dir) + if deploymentDir != "" { + p.AddDeploymentIncludes(deploymentDir) + } + + absKustomizeDir := filepath.Join(p.LocalProjectDir(), dir) + + err := os.MkdirAll(absKustomizeDir, 0o700) + if err != nil { + panic(err) + } + + p.UpdateKustomizeDeployment(dir, func(o *uo.UnstructuredObject, wt *git.Worktree) error { + o.SetNestedField("kustomize.config.k8s.io/v1beta1", "apiVersion") + o.SetNestedField("Kustomization", "kind") + return nil + }) + + p.AddKustomizeResources(dir, resources) + p.UpdateDeploymentYaml(deploymentDir, func(o *uo.UnstructuredObject) error { + d, _, _ := o.GetNestedObjectList("deployments") + n := uo.FromMap(map[string]interface{}{ + "path": filepath.Base(dir), + }) + if len(tags) != 0 { + n.SetNestedField(tags, "tags") + } + d = append(d, n) + _ = o.SetNestedObjectList(d, "deployments") + return nil + }) +} + +func (p *TestProject) AddHelmDeployment(dir string, repo *test_utils.TestHelmRepo, chartName, version string, releaseName string, namespace string, values map[string]any) { + helmChartDef := map[string]any{ + "releaseName": releaseName, + "namespace": namespace, + } + + switch repo.Type { + case test_utils.TestHelmRepo_Helm: + helmChartDef["repo"] = repo.URL.String() + helmChartDef["chartName"] = chartName + helmChartDef["chartVersion"] = version + case test_utils.TestHelmRepo_Oci: + helmChartDef["repo"] = repo.URL.String() + "/" + chartName + helmChartDef["chartVersion"] = version + case test_utils.TestHelmRepo_Local: + helmChartDef["path"] = repo.Path + case test_utils.TestHelmRepo_Git: + helmChartDef["git"] = map[string]any{ + "url": repo.URL.String(), + "ref": map[string]any{ + "tag": version, + }, + "subDir": chartName, + } + } + + p.AddKustomizeDeployment(dir, []KustomizeResource{ + {Name: "helm-rendered.yaml"}, + }, nil) + + p.UpdateYaml(filepath.Join(dir, "helm-chart.yaml"), func(o *uo.UnstructuredObject) error { + *o = *uo.FromMap(map[string]interface{}{ + "helmChart": helmChartDef, + }) + return nil + }, "") + + if values != nil { + p.UpdateYaml(filepath.Join(dir, "helm-values.yaml"), func(o *uo.UnstructuredObject) error { + *o = *uo.FromMap(values) + return nil + }, "") + } +} + +func (p *TestProject) convertInterfaceToList(x interface{}) []interface{} { + var ret []interface{} + if l, ok := x.([]interface{}); ok { + return l + } + if s, ok := x.(string); ok { + l, err := yaml.ReadYamlAllString(s) + if err != nil { + panic(err) + } + return l + } + if l, ok := x.([]*uo.UnstructuredObject); ok { + for _, y := range l { + ret = append(ret, y) + } + return ret + } + if l, ok := x.([]map[string]interface{}); ok { + for _, y := range l { + ret = append(ret, y) + } + return ret + } + return []interface{}{x} +} + +type KustomizeResource struct { + Name string + FileName string + Content interface{} +} + +func (p *TestProject) AddKustomizeResources(dir string, resources []KustomizeResource) { + p.UpdateKustomizeDeployment(dir, func(o *uo.UnstructuredObject, wt *git.Worktree) error { + l, _, _ := o.GetNestedList("resources") + for _, r := range resources { + l = append(l, r.Name) + fileName := r.FileName + if fileName == "" { + fileName = r.Name + } + if r.Content != nil { + x := p.convertInterfaceToList(r.Content) + err := yaml.WriteYamlAllFile(filepath.Join(p.LocalProjectDir(), dir, fileName), x) + if err != nil { + return err + } + _, err = wt.Add(filepath.Join(path.Join(p.gitSubDir, dir), fileName)) + if err != nil { + return err + } + } + } + o.SetNestedField(l, "resources") + return nil + }) +} + +func (p *TestProject) DeleteKustomizeDeployment(dir string) { + deploymentDir := filepath.Dir(dir) + p.UpdateDeploymentItems(deploymentDir, func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + var newItems []*uo.UnstructuredObject + for _, item := range items { + pth, _, _ := item.GetNestedString("path") + if pth == filepath.Base(dir) { + continue + } + newItems = append(newItems, item) + } + return newItems + }) +} + +func (p *TestProject) GitRepoName() string { + return p.gitRepoName +} + +func (p *TestProject) GitUrl() string { + return p.gitServer.GitRepoUrl(p.gitRepoName) +} + +func (p *TestProject) GitUrlPath() string { + u, err := gittypes.ParseGitUrl(p.GitUrl()) + if err != nil { + panic(err) + } + return strings.TrimPrefix(u.Path, "/") +} + +func (p *TestProject) LocalWorkDir() string { + return p.gitServer.LocalWorkDir(p.gitRepoName) +} + +func (p *TestProject) LocalProjectDir() string { + return path.Join(p.LocalWorkDir(), p.gitSubDir) +} + +func (p *TestProject) GetGitRepo() *git.Repository { + return p.gitServer.GetGitRepo(p.gitRepoName) +} + +func (p *TestProject) GetGitWorktree() *git.Worktree { + wt, err := p.GetGitRepo().Worktree() + if err != nil { + panic(err) + } + return wt +} + +func (p *TestProject) CopyProjectSourceTo(dst string) string { + err := cp.Copy(p.LocalWorkDir(), dst) + if err != nil { + panic(err) + } + return dst +} + +func (p *TestProject) KluctlProcess(t *testing.T, argsIn ...string) (string, string, error) { + var args []string + args = append(args, p.extraArgs...) + args = append(args, argsIn...) + args = append(args, "--no-update-check") + + cwd := p.LocalProjectDir() + + args = append(args, "--debug") + + env := os.Environ() + p.extraEnv.ForEach(func(k string, v string) { + env = append(env, fmt.Sprintf("%s=%s", k, v)) + }) + + // this will cause the init() function from call_kluctl_hack.go to invoke the kluctl root command and then exit + env = append(env, "CALL_KLUCTL=true") + env = append(env, fmt.Sprintf("KLUCTL_BASE_TMP_DIR=%s", p.tmpBaseDir)) + env = append(env, fmt.Sprintf("KLUCTL_CACHE_DIR=%s", p.cacheDir)) + + t.Logf("Runnning kluctl: %s", strings.Join(args, " ")) + + testExe, err := os.Executable() + if err != nil { + panic(err) + } + + cmd := exec.Command(testExe, args...) + cmd.Dir = cwd + cmd.Env = env + + stdout, stderr, err := runHelper(t, cmd) + return stdout, stderr, err +} + +func (p *TestProject) KluctlProcessMust(t *testing.T, argsIn ...string) (string, string) { + stdout, stderr, err := p.KluctlProcess(t, argsIn...) + if err != nil { + t.Log(stderr) + t.Fatal(fmt.Errorf("kluctl failed: %w", err)) + } + return stdout, stderr +} + +func (p *TestProject) KluctlExecute(t *testing.T, argsIn ...string) (string, string, error) { + if p.extraEnv.Len() != 0 { + t.Fatal("extraEnv is only supported in KluctlProcess(...)") + } + + var args []string + args = append(args, p.extraArgs...) + if !p.skipProjectDirArg { + args = append(args, "--project-dir", p.LocalProjectDir()) + } + args = append(args, argsIn...) + + ctx := context.Background() + if p.tmpBaseDir != "" { + ctx = utils.WithTmpBaseDir(ctx, p.tmpBaseDir) + } + if p.cacheDir != "" { + ctx = utils.WithCacheDir(ctx, p.cacheDir) + } + + return KluctlExecute(t, ctx, t.Log, args...) +} + +func (p *TestProject) Kluctl(t *testing.T, argsIn ...string) (string, string, error) { + if p.useProcess { + return p.KluctlProcess(t, argsIn...) + } else { + return p.KluctlExecute(t, argsIn...) + } +} + +func (p *TestProject) KluctlMust(t *testing.T, argsIn ...string) (string, string) { + stdout, stderr, err := p.Kluctl(t, argsIn...) + if err != nil { + t.Fatal(fmt.Errorf("kluctl failed: %w", err)) + } + return stdout, stderr +} + +func (p *TestProject) KluctlCommandResult(t *testing.T, argsIn ...string) (*result.CommandResult, string, error) { + stdout, stderr, err := p.Kluctl(t, argsIn...) + if err != nil { + return nil, "", err + } + + var ret result.CompactedCommandResult + err = yaml.ReadYamlString(stdout, &ret) + if err != nil { + t.Fatal(err) + } + + return ret.ToNonCompacted(), stderr, nil +} + +func (p *TestProject) KluctlMustCommandResult(t *testing.T, argsIn ...string) (*result.CommandResult, string) { + ret, stderr, err := p.KluctlCommandResult(t, argsIn...) + if err != nil { + t.Fatal(err) + } + return ret, stderr +} diff --git a/e2e/test_project/run_helper.go b/e2e/test_project/run_helper.go new file mode 100644 index 000000000..3be85e0a4 --- /dev/null +++ b/e2e/test_project/run_helper.go @@ -0,0 +1,48 @@ +package test_project + +import ( + "bufio" + "bytes" + "io" + "os/exec" + "sync" + "testing" +) + +func runHelper(t *testing.T, cmd *exec.Cmd) (string, string, error) { + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return "", "", err + } + stderrPipe, err := cmd.StderrPipe() + if err != nil { + _ = stdoutPipe.Close() + return "", "", err + } + + var wg sync.WaitGroup + stdReader := func(testLogPrefix string, buf io.StringWriter, pipe io.Reader) { + defer wg.Done() + scanner := bufio.NewScanner(pipe) + for scanner.Scan() { + l := scanner.Text() + t.Log(testLogPrefix + l) + _, _ = buf.WriteString(l + "\n") + } + } + + stdoutBuf := bytes.NewBuffer(nil) + stderrBuf := bytes.NewBuffer(nil) + + wg.Add(2) + go stdReader("stdout: ", stdoutBuf, stdoutPipe) + go stdReader("stderr: ", stderrBuf, stderrPipe) + + err = cmd.Start() + if err != nil { + return "", "", err + } + wg.Wait() + err = cmd.Wait() + return stdoutBuf.String(), stderrBuf.String(), err +} diff --git a/e2e/test_resources/README.md b/e2e/test_resources/README.md deleted file mode 100644 index e3655fad6..000000000 --- a/e2e/test_resources/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# sealed-secrets.yaml - -helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets -helm template sealed-secrets-controller sealed-secrets/sealed-secrets -n kube-system --include-crds --skip-tests > sealed-secrets.yaml - -# vault.yaml - -helm repo add hashicorp https://helm.releases.hashicorp.com -helm template vault hashicorp/vault -n vault -f vault-values.yaml --include-crds --skip-tests > vault.yaml diff --git a/e2e/test_resources/example-crds.yaml b/e2e/test_resources/example-crds.yaml new file mode 100644 index 000000000..8457c90cb --- /dev/null +++ b/e2e/test_resources/example-crds.yaml @@ -0,0 +1,85 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + # name must match the spec fields below, and be in the form: . + name: crontabs.stable.example.com +spec: + # group name to use for REST API: /apis// + group: stable.example.com + # list of versions supported by this CustomResourceDefinition + versions: + - name: v1 + # Each version can be enabled/disabled by Served flag. + served: true + # One and only one version must be marked as the storage version. + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + cronSpec: + type: string + image: + type: string + replicas: + type: integer + # either Namespaced or Cluster + scope: Namespaced + names: + # plural name to be used in the URL: /apis/// + plural: crontabs + # singular name to be used as an alias on the CLI and for display + singular: crontab + # kind is normally the CamelCased singular type. Your resource manifests use this. + kind: CronTab + # shortNames allow shorter string to match your resource on the CLI + shortNames: + - ct +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + # name must match the spec fields below, and be in the form: . + name: crontabstatuses.stable.example.com +spec: + # group name to use for REST API: /apis// + group: stable.example.com + # list of versions supported by this CustomResourceDefinition + versions: + - name: v1 + # Each version can be enabled/disabled by Served flag. + served: true + # One and only one version must be marked as the storage version. + storage: true + subresources: + status: {} + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + cronSpec: + type: string + image: + type: string + replicas: + type: integer + status: + x-kubernetes-preserve-unknown-fields: true + # either Namespaced or Cluster + scope: Namespaced + names: + # plural name to be used in the URL: /apis/// + plural: crontabstatuses + # singular name to be used as an alias on the CLI and for display + singular: crontabstatuse + # kind is normally the CamelCased singular type. Your resource manifests use this. + kind: CronTabStatus + # shortNames allow shorter string to match your resource on the CLI + shortNames: + - cts diff --git a/e2e/test_resources/resources.go b/e2e/test_resources/resources.go index 7644d1f09..c660c5439 100644 --- a/e2e/test_resources/resources.go +++ b/e2e/test_resources/resources.go @@ -1,46 +1,131 @@ package test_resources import ( + "context" "embed" - test_utils "github.com/kluctl/kluctl/v2/internal/test-utils" - "github.com/kluctl/kluctl/v2/pkg/utils" - "os" + "github.com/kluctl/kluctl/lib/yaml" + "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/kluctl/kluctl/v2/pkg/validation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sort" + "strings" + "sync" + "testing" + "time" ) //go:embed *.yaml var Yamls embed.FS -func GetYamlTmpFile(name string) string { - tmpFile, err := os.CreateTemp("", "") +func GetYamlDocs(t *testing.T, name string) []*uo.UnstructuredObject { + b, err := Yamls.ReadFile(name) if err != nil { panic(err) } - tmpFile.Close() - err = utils.FsCopyFile(Yamls, name, tmpFile.Name()) + docs, err := uo.FromStringMulti(string(b)) if err != nil { panic(err) } - return tmpFile.Name() + return docs } -func ApplyYaml(name string, k *test_utils.KindCluster) { - tmpFile := GetYamlTmpFile(name) - defer os.Remove(tmpFile) +// poor mans resource mapper :) +func guessGVR(gvk schema.GroupVersionKind) schema.GroupVersionResource { + gvr := schema.GroupVersionResource{ + Group: gvk.Group, + Version: gvk.Version, + } + if strings.HasSuffix(gvk.Kind, "y") { + gvr.Resource = strings.ToLower(gvk.Kind)[:len(gvk.Kind)-1] + "ies" + } else { + gvr.Resource = strings.ToLower(gvk.Kind) + "s" + } + return gvr +} - _, err := k.Kubectl("apply", "-f", tmpFile) - if err != nil { - panic(err) +func prio(x *uo.UnstructuredObject) int { + switch x.GetK8sGVK().Kind { + case "Namespace": + return 100 + case "CustomResourceDefinition": + return 100 + default: + return 0 } } -func DeleteYaml(name string, k *test_utils.KindCluster) { - tmpFile := GetYamlTmpFile(name) - defer os.Remove(tmpFile) +func waitReadiness(k *test_utils.EnvTestCluster, x *uo.UnstructuredObject) { + for true { + u, err := k.DynamicClient.Resource(guessGVR(x.GetK8sGVK())).Get(context.Background(), x.GetK8sName(), metav1.GetOptions{}) + if err != nil { + panic(err) + } + vr := validation.ValidateObject(context.TODO(), nil, uo.FromUnstructured(u), true, true) + if vr.Ready { + break + } else { + time.Sleep(time.Millisecond * 100) + } + } +} - _, err := k.Kubectl("delete", "-f", tmpFile) - if err != nil { - panic(err) +func ApplyYaml(t *testing.T, name string, k *test_utils.EnvTestCluster) { + doPanic := func(err error) { + if t != nil { + t.Fatal(err) + } else { + panic(err) + } + } + + objects := GetYamlDocs(t, name) + + sort.SliceStable(objects, func(i, j int) bool { + return prio(objects[i]) > prio(objects[j]) + }) + + var wg sync.WaitGroup + prevPrio := prio(objects[0]) + for _, x := range objects { + x := x + + p := prio(x) + if p != prevPrio { + wg.Wait() + } + prevPrio = p + + wg.Add(1) + go func() { + defer wg.Done() + + data, err := yaml.WriteYamlBytes(x) + if err != nil { + doPanic(err) + } + + gvr := guessGVR(x.GetK8sGVK()) + _, err = k.DynamicClient.Resource(gvr). + Namespace(x.GetK8sNamespace()). + Patch(context.Background(), x.GetK8sName(), types.ApplyPatchType, data, metav1.PatchOptions{ + FieldManager: "e2e-tests", + }) + if err != nil { + doPanic(err) + } + + // wait for CRDs to get accepted + if x.GetK8sGVK().Kind == "CustomResourceDefinition" { + waitReadiness(k, x) + // add some safety net...for some reason the envtest api server still fails if not waiting + time.Sleep(200 * time.Millisecond) + } + }() } + wg.Wait() } diff --git a/e2e/test_resources/sealed-secrets.yaml b/e2e/test_resources/sealed-secrets.yaml deleted file mode 100644 index 8e2262447..000000000 --- a/e2e/test_resources/sealed-secrets.yaml +++ /dev/null @@ -1,307 +0,0 @@ ---- -# Source: crds/sealedsecret-crd.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: sealedsecrets.bitnami.com -spec: - group: bitnami.com - names: - kind: SealedSecret - listKind: SealedSecretList - plural: sealedsecrets - singular: sealedsecret - scope: Namespaced - versions: - - name: v1alpha1 - served: true - storage: true - subresources: - status: {} - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - x-kubernetes-preserve-unknown-fields: true - status: - x-kubernetes-preserve-unknown-fields: true - ---- -# Source: sealed-secrets/templates/service-account.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: sealed-secrets-controller - namespace: kube-system - labels: - app.kubernetes.io/name: sealed-secrets - helm.sh/chart: sealed-secrets-2.4.0 - app.kubernetes.io/instance: sealed-secrets-controller - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/version: v0.18.1 ---- -# Source: sealed-secrets/templates/cluster-role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: secrets-unsealer - labels: - app.kubernetes.io/name: sealed-secrets - helm.sh/chart: sealed-secrets-2.4.0 - app.kubernetes.io/instance: sealed-secrets-controller - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/version: v0.18.1 -rules: - - apiGroups: - - bitnami.com - resources: - - sealedsecrets - verbs: - - get - - list - - watch - - apiGroups: - - bitnami.com - resources: - - sealedsecrets/status - verbs: - - update - - apiGroups: - - "" - resources: - - secrets - verbs: - - get - - list - - create - - update - - delete - - apiGroups: - - "" - resources: - - events - verbs: - - create - - patch ---- -# Source: sealed-secrets/templates/cluster-role-binding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: sealed-secrets-controller - labels: - app.kubernetes.io/name: sealed-secrets - helm.sh/chart: sealed-secrets-2.4.0 - app.kubernetes.io/instance: sealed-secrets-controller - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/version: v0.18.1 -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: secrets-unsealer -subjects: - - apiGroup: "" - kind: ServiceAccount - name: sealed-secrets-controller - namespace: kube-system ---- -# Source: sealed-secrets/templates/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: sealed-secrets-controller-key-admin - namespace: kube-system - labels: - app.kubernetes.io/name: sealed-secrets - helm.sh/chart: sealed-secrets-2.4.0 - app.kubernetes.io/instance: sealed-secrets-controller - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/version: v0.18.1 -rules: - - apiGroups: - - "" - resourceNames: - - sealed-secrets-key - resources: - - secrets - verbs: - - get - - apiGroups: - - "" - resources: - - secrets - verbs: - - create - - list ---- -# Source: sealed-secrets/templates/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: sealed-secrets-controller-service-proxier - namespace: kube-system - labels: - app.kubernetes.io/name: sealed-secrets - helm.sh/chart: sealed-secrets-2.4.0 - app.kubernetes.io/instance: sealed-secrets-controller - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/version: v0.18.1 -rules: - - apiGroups: - - "" - resourceNames: - - sealed-secrets-controller - resources: - - services - verbs: - - get - - apiGroups: - - "" - resourceNames: - - 'http:sealed-secrets-controller:' - - 'http:sealed-secrets-controller:http' - - sealed-secrets-controller - resources: - - services/proxy - verbs: - - create - - get ---- -# Source: sealed-secrets/templates/role-binding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: sealed-secrets-controller-key-admin - namespace: kube-system - labels: - app.kubernetes.io/name: sealed-secrets - helm.sh/chart: sealed-secrets-2.4.0 - app.kubernetes.io/instance: sealed-secrets-controller - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/version: v0.18.1 -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: sealed-secrets-controller-key-admin -subjects: - - apiGroup: "" - kind: ServiceAccount - name: sealed-secrets-controller - namespace: kube-system ---- -# Source: sealed-secrets/templates/role-binding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: sealed-secrets-controller-service-proxier - namespace: kube-system - labels: - app.kubernetes.io/name: sealed-secrets - helm.sh/chart: sealed-secrets-2.4.0 - app.kubernetes.io/instance: sealed-secrets-controller - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/version: v0.18.1 -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: sealed-secrets-controller-service-proxier -subjects: -- apiGroup: rbac.authorization.k8s.io - kind: Group - name: system:authenticated ---- -# Source: sealed-secrets/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: sealed-secrets-controller - namespace: kube-system - labels: - app.kubernetes.io/name: sealed-secrets - helm.sh/chart: sealed-secrets-2.4.0 - app.kubernetes.io/instance: sealed-secrets-controller - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/version: v0.18.1 -spec: - type: ClusterIP - ports: - - name: http - port: 8080 - targetPort: http - nodePort: null - selector: - app.kubernetes.io/name: sealed-secrets - app.kubernetes.io/instance: sealed-secrets-controller ---- -# Source: sealed-secrets/templates/deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: sealed-secrets-controller - namespace: kube-system - labels: - app.kubernetes.io/name: sealed-secrets - helm.sh/chart: sealed-secrets-2.4.0 - app.kubernetes.io/instance: sealed-secrets-controller - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/version: v0.18.1 -spec: - selector: - matchLabels: - app.kubernetes.io/name: sealed-secrets - app.kubernetes.io/instance: sealed-secrets-controller - template: - metadata: - labels: - app.kubernetes.io/name: sealed-secrets - app.kubernetes.io/instance: sealed-secrets-controller - spec: - securityContext: - fsGroup: 65534 - serviceAccountName: sealed-secrets-controller - containers: - - name: controller - command: - - controller - args: - - --update-status - - --key-prefix - - "sealed-secrets-key" - image: docker.io/bitnami/sealed-secrets-controller:v0.18.1 - imagePullPolicy: IfNotPresent - ports: - - containerPort: 8080 - name: http - livenessProbe: - failureThreshold: 3 - initialDelaySeconds: 0 - periodSeconds: 10 - successThreshold: 1 - timeoutSeconds: 1 - httpGet: - path: /healthz - port: http - readinessProbe: - failureThreshold: 3 - initialDelaySeconds: 0 - periodSeconds: 10 - successThreshold: 1 - timeoutSeconds: 1 - httpGet: - path: /healthz - port: http - resources: - limits: {} - requests: {} - securityContext: - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 1001 - volumeMounts: - - mountPath: /tmp - name: tmp - volumes: - - name: tmp - emptyDir: {} diff --git a/e2e/test_resources/vault-values.yaml b/e2e/test_resources/vault-values.yaml deleted file mode 100644 index 1934c072a..000000000 --- a/e2e/test_resources/vault-values.yaml +++ /dev/null @@ -1,10 +0,0 @@ -server: - # Must be RollingUpdate as otherwise it's reported as ready much too early - updateStrategyType: RollingUpdate - dev: - enabled: true - service: - type: NodePort - nodePort: 30000 -injector: - enabled: false diff --git a/e2e/test_resources/vault.yaml b/e2e/test_resources/vault.yaml deleted file mode 100644 index 13e65b084..000000000 --- a/e2e/test_resources/vault.yaml +++ /dev/null @@ -1,235 +0,0 @@ ---- -# Source: vault/templates/server-serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: vault - namespace: vault - labels: - helm.sh/chart: vault-0.20.1 - app.kubernetes.io/name: vault - app.kubernetes.io/instance: vault - app.kubernetes.io/managed-by: Helm ---- -# Source: vault/templates/server-clusterrolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: vault-server-binding - labels: - helm.sh/chart: vault-0.20.1 - app.kubernetes.io/name: vault - app.kubernetes.io/instance: vault - app.kubernetes.io/managed-by: Helm -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: system:auth-delegator -subjects: -- kind: ServiceAccount - name: vault - namespace: vault ---- -# Source: vault/templates/server-headless-service.yaml -# Service for Vault cluster -apiVersion: v1 -kind: Service -metadata: - name: vault-internal - namespace: vault - labels: - helm.sh/chart: vault-0.20.1 - app.kubernetes.io/name: vault - app.kubernetes.io/instance: vault - app.kubernetes.io/managed-by: Helm - annotations: - -spec: - clusterIP: None - publishNotReadyAddresses: true - ports: - - name: "http" - port: 8200 - targetPort: 8200 - - name: https-internal - port: 8201 - targetPort: 8201 - selector: - app.kubernetes.io/name: vault - app.kubernetes.io/instance: vault - component: server ---- -# Source: vault/templates/server-service.yaml -# Service for Vault cluster -apiVersion: v1 -kind: Service -metadata: - name: vault - namespace: vault - labels: - helm.sh/chart: vault-0.20.1 - app.kubernetes.io/name: vault - app.kubernetes.io/instance: vault - app.kubernetes.io/managed-by: Helm - annotations: - -spec: - type: NodePort - externalTrafficPolicy: Cluster - # We want the servers to become available even if they're not ready - # since this DNS is also used for join operations. - publishNotReadyAddresses: true - ports: - - name: http - port: 8200 - targetPort: 8200 - nodePort: 30000 - - name: https-internal - port: 8201 - targetPort: 8201 - selector: - app.kubernetes.io/name: vault - app.kubernetes.io/instance: vault - component: server ---- -# Source: vault/templates/server-statefulset.yaml -# StatefulSet to run the actual vault server cluster. -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: vault - namespace: vault - labels: - app.kubernetes.io/name: vault - app.kubernetes.io/instance: vault - app.kubernetes.io/managed-by: Helm -spec: - serviceName: vault-internal - podManagementPolicy: Parallel - replicas: 1 - updateStrategy: - type: RollingUpdate - selector: - matchLabels: - app.kubernetes.io/name: vault - app.kubernetes.io/instance: vault - component: server - template: - metadata: - labels: - helm.sh/chart: vault-0.20.1 - app.kubernetes.io/name: vault - app.kubernetes.io/instance: vault - component: server - spec: - - - - - terminationGracePeriodSeconds: 10 - serviceAccountName: vault - - securityContext: - runAsNonRoot: true - runAsGroup: 1000 - runAsUser: 100 - fsGroup: 1000 - volumes: - - - name: home - emptyDir: {} - containers: - - name: vault - - image: hashicorp/vault:1.10.3 - imagePullPolicy: IfNotPresent - command: - - "/bin/sh" - - "-ec" - args: - - | - /usr/local/bin/docker-entrypoint.sh vault server -dev - - securityContext: - allowPrivilegeEscalation: false - env: - - name: HOST_IP - valueFrom: - fieldRef: - fieldPath: status.hostIP - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - - name: VAULT_K8S_POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: VAULT_K8S_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: VAULT_ADDR - value: "http://127.0.0.1:8200" - - name: VAULT_API_ADDR - value: "http://$(POD_IP):8200" - - name: SKIP_CHOWN - value: "true" - - name: SKIP_SETCAP - value: "true" - - name: HOSTNAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: VAULT_CLUSTER_ADDR - value: "https://$(HOSTNAME).vault-internal:8201" - - name: HOME - value: "/home/vault" - - - name: VAULT_DEV_ROOT_TOKEN_ID - value: root - - name: VAULT_DEV_LISTEN_ADDRESS - value: "[::]:8200" - - - - volumeMounts: - - - - - name: home - mountPath: /home/vault - ports: - - containerPort: 8200 - name: http - - containerPort: 8201 - name: https-internal - - containerPort: 8202 - name: http-rep - readinessProbe: - # Check status; unsealed vault servers return 0 - # The exit code reflects the seal status: - # 0 - unsealed - # 1 - error - # 2 - sealed - exec: - command: ["/bin/sh", "-ec", "vault status -tls-skip-verify"] - failureThreshold: 2 - initialDelaySeconds: 5 - periodSeconds: 5 - successThreshold: 1 - timeoutSeconds: 3 - lifecycle: - # Vault container doesn't receive SIGTERM from Kubernetes - # and after the grace period ends, Kube sends SIGKILL. This - # causes issues with graceful shutdowns such as deregistering itself - # from Consul (zombie services). - preStop: - exec: - command: [ - "/bin/sh", "-c", - # Adding a sleep here to give the pod eviction a - # chance to propagate, so requests will not be made - # to this pod while it's terminating - "sleep 5 && kill -SIGTERM $(pidof vault)", - ] diff --git a/e2e/utils.go b/e2e/utils.go index 274f5946c..b6b9d92d0 100644 --- a/e2e/utils.go +++ b/e2e/utils.go @@ -1,98 +1,90 @@ package e2e import ( - "bufio" - "bytes" - "fmt" - "github.com/kluctl/kluctl/v2/internal/test-utils" + "context" + "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" "github.com/kluctl/kluctl/v2/pkg/utils/uo" - "github.com/kluctl/kluctl/v2/pkg/validation" - log "github.com/sirupsen/logrus" - "io" - "os/exec" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "reflect" - "strings" + "sigs.k8s.io/controller-runtime/pkg/client" "testing" "time" ) -func recreateNamespace(t *testing.T, k *test_utils.KindCluster, namespace string) { - _, _ = k.Kubectl("delete", "ns", namespace) - k.KubectlMust(t, "create", "ns", namespace) - k.KubectlMust(t, "label", "ns", namespace, "kluctl-e2e=true") +func createTestCluster(t *testing.T, context string) *test_utils.EnvTestCluster { + k := test_utils.CreateEnvTestCluster(context) + err := k.Start() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + k.Stop() + }) + return k } -func deleteTestNamespaces(k *test_utils.KindCluster) { - _, _ = k.Kubectl("delete", "ns", "-l", "kubectl-e2e=true") -} +func createNamespace(t *testing.T, k *test_utils.EnvTestCluster, namespace string) { + r := k.DynamicClient.Resource(v1.SchemeGroupVersion.WithResource("namespaces")) + if _, err := r.Get(context.Background(), namespace, metav1.GetOptions{}); err == nil { + return + } -func waitForReadiness(t *testing.T, k *test_utils.KindCluster, namespace string, resource string, timeout time.Duration) bool { - t.Logf("Waiting for readiness: %s/%s", namespace, resource) - - startTime := time.Now() - for time.Now().Sub(startTime) < timeout { - y, err := k.KubectlYaml("-n", namespace, "get", resource) - if err != nil { - if ee, ok := err.(*exec.ExitError); !ok || strings.Index(string(ee.Stderr), "NotFound") == -1 { - t.Fatal(err) - } - time.Sleep(1 * time.Second) - continue - } - - v := validation.ValidateObject(nil, y, true) - if v.Ready { - return true - } - - if log.IsLevelEnabled(log.DebugLevel) { - errTxt := "" - for _, e := range v.Errors { - if errTxt != "" { - errTxt += "\n" - } - errTxt += fmt.Sprintf("%s: %s", e.Ref.String(), e.Error) - } - log.Debugf("validation failed for %s/%s. errors:\n%s", namespace, resource, errTxt) - } - time.Sleep(1 * time.Second) + var ns unstructured.Unstructured + ns.SetName(namespace) + _, err := r.Create(context.Background(), &ns, metav1.CreateOptions{}) + + if err != nil && !errors.IsAlreadyExists(err) { + t.Fatal(err) } - return false } -func assertReadiness(t *testing.T, k *test_utils.KindCluster, namespace string, resource string, timeout time.Duration) { - if !waitForReadiness(t, k, namespace, resource, timeout) { - t.Errorf("%s/%s did not get ready in time", namespace, resource) +func getHeadRevision(t *testing.T, p *test_project.TestProject) string { + r := p.GetGitRepo() + h, err := r.Head() + if err != nil { + t.Fatal(err) } + return h.Hash().String() } -func assertResourceExists(t *testing.T, k *test_utils.KindCluster, namespace string, resource string) *uo.UnstructuredObject { - var args []string - if namespace != "" { - args = append(args, "-n", namespace) +func assertObjectExists(t *testing.T, k *test_utils.EnvTestCluster, gvr schema.GroupVersionResource, namespace string, name string) *uo.UnstructuredObject { + x, err := k.Get(gvr, namespace, name) + if err != nil { + t.Fatalf("unexpected error '%v' while getting %s %s/%s", err, gvr.GroupResource().String(), namespace, name) } - args = append(args, "get", resource) - return k.KubectlYamlMust(t, args...) + return x } -func assertResourceNotExists(t *testing.T, k *test_utils.KindCluster, namespace string, resource string) { - var args []string - if namespace != "" { - args = append(args, "-n", namespace) - } - args = append(args, "get", resource) - _, err := k.KubectlYaml(args...) +func assertObjectNotExists(t *testing.T, k *test_utils.EnvTestCluster, gvr schema.GroupVersionResource, namespace string, name string) { + _, err := k.Get(gvr, namespace, name) if err == nil { - t.Fatalf("'kubectl get' for %s should not have succeeded", resource) - } else { - ee, ok := err.(*exec.ExitError) - if !ok { - t.Fatal(err) - } - if strings.Index(string(ee.Stderr), "(NotFound)") == -1 { - t.Fatal(err) - } + t.Fatalf("expected %s/%s to not exist", namespace, name) } + if !errors.IsNotFound(err) { + t.Fatalf("unexpected error '%v' for %s/%s, expected a NotFound error", err, namespace, name) + } +} + +func assertConfigMapExists(t *testing.T, k *test_utils.EnvTestCluster, namespace string, name string) *uo.UnstructuredObject { + return assertObjectExists(t, k, v1.SchemeGroupVersion.WithResource("configmaps"), namespace, name) +} + +func assertConfigMapNotExists(t *testing.T, k *test_utils.EnvTestCluster, namespace string, name string) { + assertObjectNotExists(t, k, v1.SchemeGroupVersion.WithResource("configmaps"), namespace, name) +} + +func assertSecretExists(t *testing.T, k *test_utils.EnvTestCluster, namespace string, name string) *uo.UnstructuredObject { + x, err := k.Get(v1.SchemeGroupVersion.WithResource("secrets"), namespace, name) + if err != nil { + t.Fatalf("unexpected error '%v' while getting Secret %s/%s", err, namespace, name) + } + return x } func assertNestedFieldEquals(t *testing.T, o *uo.UnstructuredObject, expected interface{}, keys ...interface{}) { @@ -108,32 +100,44 @@ func assertNestedFieldEquals(t *testing.T, o *uo.UnstructuredObject, expected in } } -func runHelper(t *testing.T, cmd *exec.Cmd) (string, string, error) { - stdoutPipe, err := cmd.StdoutPipe() - if err != nil { - return "", "", err - } - stderrPipe, err := cmd.StderrPipe() - if err != nil { - _ = stdoutPipe.Close() - return "", "", err - } +func updateObject(t *testing.T, k *test_utils.EnvTestCluster, o *uo.UnstructuredObject) { + _, err := k.DynamicClient.Resource(v1.SchemeGroupVersion.WithResource("configmaps")). + Namespace(o.GetK8sNamespace()). + Update(context.Background(), o.ToUnstructured(), metav1.UpdateOptions{}) + assert.NoError(t, err) +} - stdReader := func(testLogPrefix string, buf io.StringWriter, pipe io.Reader) { - scanner := bufio.NewScanner(pipe) - for scanner.Scan() { - l := scanner.Text() - t.Log(testLogPrefix + l) - _, _ = buf.WriteString(l + "\n") - } - } +func patchObject(t *testing.T, k *test_utils.EnvTestCluster, gvr schema.GroupVersionResource, namespace string, name string, cb func(o *uo.UnstructuredObject)) { + o := assertObjectExists(t, k, gvr, namespace, name) + patch := client.MergeFrom(o.ToUnstructured().DeepCopy()) + cb(o) + err := k.Client.Patch(context.Background(), o.ToUnstructured(), patch) + assert.NoError(t, err) +} + +func patchConfigMap(t *testing.T, k *test_utils.EnvTestCluster, namespace string, name string, cb func(o *uo.UnstructuredObject)) { + patchObject(t, k, v1.SchemeGroupVersion.WithResource("configmaps"), namespace, name, cb) +} - stdoutBuf := bytes.NewBuffer(nil) - stderrBuf := bytes.NewBuffer(nil) +type secondPassedBarrier struct { + last time.Time + t *testing.T +} - go stdReader("stdout: ", stdoutBuf, stdoutPipe) - go stdReader("stderr: ", stderrBuf, stderrPipe) +func newSecondPassedBarrier(t *testing.T) secondPassedBarrier { + return secondPassedBarrier{ + t: t, + last: time.Now(), + } +} - err = cmd.Run() - return stdoutBuf.String(), stderrBuf.String(), err +func (b *secondPassedBarrier) Wait() { + t := time.Now() + passed := t.Sub(b.last) + if passed < time.Second { + sleep := time.Second - passed + 100*time.Millisecond + b.t.Logf("sleeping for %s", sleep.String()) + time.Sleep(sleep) + } + b.last = time.Now() } diff --git a/e2e/utils_resources.go b/e2e/utils_resources.go index e6f9e4a20..c9a9812a7 100644 --- a/e2e/utils_resources.go +++ b/e2e/utils_resources.go @@ -1,18 +1,20 @@ package e2e import ( - "bytes" "fmt" + "github.com/kluctl/kluctl/v2/e2e/test_project" "github.com/kluctl/kluctl/v2/pkg/utils/uo" - "text/template" + "path/filepath" ) type resourceOpts struct { name string + fname string namespace string tags []string labels map[string]string annotations map[string]string + when string } func mergeMetadata(o *uo.UnstructuredObject, opts resourceOpts) { @@ -30,141 +32,56 @@ func mergeMetadata(o *uo.UnstructuredObject, opts resourceOpts) { } } -func renderTemplateHelper(tmpl string, m map[string]interface{}) string { - t := template.Must(template.New("").Parse(tmpl)) - r := bytes.NewBuffer(nil) - err := t.Execute(r, m) - if err != nil { - panic(err) - } - return r.String() -} - -func renderTemplateObjectHelper(tmpl string, m map[string]interface{}) []*uo.UnstructuredObject { - s := renderTemplateHelper(tmpl, m) - ret, err := uo.FromStringMulti(s) - if err != nil { - panic(err) - } - return ret -} - -func addConfigMapDeployment(p *testProject, dir string, data map[string]string, opts resourceOpts) { +func createCoreV1Object(kind string, opts resourceOpts) *uo.UnstructuredObject { o := uo.New() - o.SetK8sGVKs("", "v1", "ConfigMap") + o.SetK8sGVKs("", "v1", kind) mergeMetadata(o, opts) + return o +} + +func createConfigMapObject(data map[string]string, opts resourceOpts) *uo.UnstructuredObject { + o := createCoreV1Object("ConfigMap", opts) if data != nil { o.SetNestedField(data, "data") } - p.addKustomizeDeployment(dir, []kustomizeResource{ - {fmt.Sprintf("configmap-%s.yml", opts.name), "", o}, - }, opts.tags) + return o } -func addSecretDeployment(p *testProject, dir string, data map[string]string, sealedSecret bool, opts resourceOpts) { - o := uo.New() - o.SetK8sGVKs("", "v1", "Secret") - mergeMetadata(o, opts) +func createSecretObject(data map[string]string, opts resourceOpts) *uo.UnstructuredObject { + o := createCoreV1Object("Secret", opts) if data != nil { o.SetNestedField(data, "stringData") } - fname := fmt.Sprintf("secret-%s.yml", opts.name) - p.addKustomizeDeployment(dir, []kustomizeResource{ - {fname, fname + ".sealme", o}, - }, opts.tags) + return o } -func addDeploymentHelper(p *testProject, dir string, o *uo.UnstructuredObject, opts resourceOpts) { - rbac := renderTemplateObjectHelper(podRbacTemplate, map[string]interface{}{ - "name": o.GetK8sName(), - "namespace": o.GetK8sNamespace(), - }) - for _, x := range rbac { - mergeMetadata(x, opts) +func addConfigMapDeployment(p *test_project.TestProject, dir string, data map[string]string, opts resourceOpts) { + o := createConfigMapObject(data, opts) + fname := opts.fname + if fname == "" { + fname = fmt.Sprintf("configmap-%s.yml", opts.name) } - mergeMetadata(o, opts) - - resources := []kustomizeResource{ - {"rbac.yml", "", rbac}, - {"deploy.yml", "", o}, + p.AddKustomizeDeployment(dir, []test_project.KustomizeResource{ + {Name: fname, Content: o}, + }, opts.tags) + if opts.when != "" { + p.UpdateDeploymentItems(filepath.Dir(dir), func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + _ = items[len(items)-1].SetNestedField(opts.when, "when") + return items + }) } - - p.addKustomizeDeployment(dir, resources, opts.tags) } -func addJobDeployment(p *testProject, dir string, opts resourceOpts, image string, command []string, args []string) { - o := renderTemplateObjectHelper(jobTemplate, map[string]interface{}{ - "name": opts.name, - "namespace": opts.namespace, - "image": image, - }) - o[0].SetNestedField(command, "spec", "template", "spec", "containers", 0, "command") - o[0].SetNestedField(args, "spec", "template", "spec", "containers", 0, "args") - addDeploymentHelper(p, dir, o[0], opts) +func addSecretDeployment(p *test_project.TestProject, dir string, data map[string]string, opts resourceOpts) { + o := createSecretObject(data, opts) + fname := fmt.Sprintf("secret-%s.yml", opts.name) + p.AddKustomizeDeployment(dir, []test_project.KustomizeResource{ + {Name: fname, FileName: fname, Content: o}, + }, opts.tags) + if opts.when != "" { + p.UpdateDeploymentItems(filepath.Dir(dir), func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + _ = items[len(items)-1].SetNestedField(opts.when, "when") + return items + }) + } } - -const podRbacTemplate = ` -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ .name }} - namespace: {{ .namespace }} ---- -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: "{{ .name }}" - namespace: "{{ .namespace }}" -subjects: -- kind: ServiceAccount - name: "{{ .name }}" - namespace: "{{ .namespace }}" -roleRef: - kind: ClusterRole - name: "cluster-admin" - apiGroup: rbac.authorization.k8s.io -` - -const deploymentTemplate = ` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .name }} - namespace: {{ .namespace }} - labels: - app.kubernetes.io/name: {{ .name }} -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: {{ .name }} - template: - metadata: - labels: - app.kubernetes.io/name: {{ .name }} - spec: - terminationGracePeriodSeconds: 0 - serviceAccountName: {{ .name }} - containers: - - image: {{ .image }} - imagePullPolicy: IfNotPresent - name: container -` - -const jobTemplate = ` -apiVersion: batch/v1 -kind: Job -metadata: - name: {{ .name }} - namespace: {{ .namespace }} -spec: - template: - spec: - terminationGracePeriodSeconds: 0 - restartPolicy: OnFailure - serviceAccountName: {{ .name }} - containers: - - image: {{ .image }} - imagePullPolicy: IfNotPresent - name: container -` diff --git a/e2e/validate_test.go b/e2e/validate_test.go new file mode 100644 index 000000000..ca082783d --- /dev/null +++ b/e2e/validate_test.go @@ -0,0 +1,300 @@ +package e2e + +import ( + "context" + "fmt" + "github.com/kluctl/kluctl/lib/yaml" + test_utils "github.com/kluctl/kluctl/v2/e2e/test-utils" + "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/e2e/test_resources" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "testing" + "time" +) + +func buildDeployment(name string, namespace string, ready bool, annotations map[string]string) *uo.UnstructuredObject { + deployment := uo.FromStringMust(fmt.Sprintf(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: %s + namespace: %s + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 +`, name, namespace)) + if annotations != nil { + deployment.SetK8sAnnotations(annotations) + } + if ready { + deployment.Merge(uo.FromStringMust(` +status: + availableReplicas: 1 + conditions: + - lastTransitionTime: "2023-03-29T19:23:12Z" + lastUpdateTime: "2023-03-29T19:23:12Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: "True" + type: Available + - lastTransitionTime: "2023-03-29T19:22:30Z" + lastUpdateTime: "2023-03-29T19:23:12Z" + message: ReplicaSet "argocd-redis-8f7689686" has successfully progressed. + reason: NewReplicaSetAvailable + status: "True" + type: Progressing + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 +`)) + } + return deployment +} + +func prepareValidateTest(t *testing.T, k *test_utils.EnvTestCluster, annotations map[string]string) *test_project.TestProject { + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + p.UpdateTarget("test", nil) + + p.AddKustomizeDeployment("d1", []test_project.KustomizeResource{ + {Name: "d1.yml", Content: buildDeployment("d1", p.TestSlug(), false, annotations)}, + }, nil) + + return p +} + +func assertValidate(t *testing.T, p *test_project.TestProject, succeed bool) (string, string) { + args := []string{"validate"} + args = append(args, "-t", "test") + + stdout, stderr, err := p.Kluctl(t, args...) + + if succeed { + assert.NoError(t, err) + assert.NotContains(t, stdout, fmt.Sprintf("%s/Deployment/d1: readyReplicas field not in status or empty", p.TestSlug())) + assert.NotContains(t, stderr, "Validation failed") + } else { + assert.ErrorContains(t, err, "Validation failed") + assert.Contains(t, stdout, fmt.Sprintf("%s/Deployment/d1: readyReplicas field not in status or empty", p.TestSlug())) + } + + return stdout, stderr +} + +func TestValidate(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := prepareValidateTest(t, k, nil) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertObjectExists(t, k, appsv1.SchemeGroupVersion.WithResource("deployments"), p.TestSlug(), "d1") + + assertValidate(t, p, false) + + readyDeployment := buildDeployment("d1", p.TestSlug(), true, nil) + + _, err := k.DynamicClient.Resource(appsv1.SchemeGroupVersion.WithResource("deployments")).Namespace(p.TestSlug()). + Patch(context.Background(), "d1", types.ApplyPatchType, []byte(yaml.WriteJsonStringMust(readyDeployment)), metav1.PatchOptions{ + FieldManager: "test", + }, "status") + assert.NoError(t, err) + + assertValidate(t, p, true) +} + +func TestValidateSkipHooks(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := prepareValidateTest(t, k, map[string]string{ + "kluctl.io/hook": "post-deploy", + "kluctl.io/hook-wait": "false", + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertObjectExists(t, k, appsv1.SchemeGroupVersion.WithResource("deployments"), p.TestSlug(), "d1") + + assertValidate(t, p, false) + + p.UpdateYaml("d1/d1.yml", func(o *uo.UnstructuredObject) error { + o.SetK8sAnnotation("kluctl.io/hook-delete-policy", "hook-succeeded") + return nil + }, "") + + assertValidate(t, p, true) +} + +func TestValidateSkipDeleteHooks(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := prepareValidateTest(t, k, map[string]string{ + "helm.sh/hook": "post-delete", + "kluctl.io/hook-wait": "false", + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertObjectNotExists(t, k, appsv1.SchemeGroupVersion.WithResource("deployments"), p.TestSlug(), "d1") + + assertValidate(t, p, true) +} + +func TestValidateSkipRollbackHooks(t *testing.T) { + t.Parallel() + + k := defaultCluster1 + + p := prepareValidateTest(t, k, map[string]string{ + "helm.sh/hook": "post-rollback", + "kluctl.io/hook-wait": "false", + }) + + p.KluctlMust(t, "deploy", "--yes", "-t", "test") + assertObjectNotExists(t, k, appsv1.SchemeGroupVersion.WithResource("deployments"), p.TestSlug(), "d1") + + assertValidate(t, p, true) +} + +func TestValidateWithoutPermissions(t *testing.T) { + t.Parallel() + + // need our own cluster due to CRDs being involved + k := createTestCluster(t, "cluster1") + test_resources.ApplyYaml(t, "example-crds.yaml", k) + + p := test_project.NewTestProject(t) + + createNamespace(t, k, p.TestSlug()) + + username := p.TestSlug() + au, err := k.AddUser(envtest.User{Name: username}, nil) + assert.NoError(t, err) + + rbac := buildSingleNamespaceRbac(username, p.TestSlug(), nil, []schema.GroupResource{ + {Group: "", Resource: "configmaps"}, + {Group: "stable.example.com", Resource: "crontabs"}, + {Group: "stable.example.com", Resource: "crontabstatuses"}, + }) + for _, x := range rbac { + k.MustApply(t, x) + } + + kc, err := au.KubeConfig() + assert.NoError(t, err) + + p.AddExtraArgs("--kubeconfig", getKubeconfigTmpFile(t, kc)) + + cronTab := uo.New() + cronTab.SetK8sGVK(schema.GroupVersionKind{Group: "stable.example.com", Version: "v1", Kind: "CronTab"}) + cronTab.SetK8sNamespace(p.TestSlug()) + cronTab.SetK8sName("crontab") + _ = cronTab.SetNestedField(map[string]any{ + "cronSpec": "x", + }, "spec") + cronTab.SetK8sAnnotation("kluctl.io/wait-readiness", "true") + cronTab2 := cronTab.Clone() + cronTab2.SetK8sGVK(schema.GroupVersionKind{Group: "stable.example.com", Version: "v1", Kind: "CronTabStatus"}) + + p.AddKustomizeDeployment("x", []test_project.KustomizeResource{ + {Name: "crontab.yaml", Content: cronTab}, + {Name: "crontab2.yaml", Content: cronTab2}, + }, nil) + // a configmap should always be considered ready thanks to the scheme based check + addConfigMapDeployment(p, "cm", nil, resourceOpts{ + name: "cm", + namespace: p.TestSlug(), + annotations: map[string]string{ + "kluctl.io/wait-readiness": "true", + }, + }) + + // this should succeed but emit a warning about the wait not knowing if a status is expected + stdout, _, err := p.Kluctl(t, "deploy", "--yes") + assert.NoError(t, err) + assert.Contains(t, stdout, "CronTab/crontab: unable to determine if a status is expected") + assert.Contains(t, stdout, "CronTabStatus/crontab: unable to determine if a status is expected") + + testSuccess := func(globalRbacResources []schema.GroupResource, rbacResources []schema.GroupResource) { + rbac = buildSingleNamespaceRbac(username, p.TestSlug(), globalRbacResources, rbacResources) + for _, x := range rbac { + k.MustApply(t, x) + } + + // need to reset status + err = k.Client.Delete(context.TODO(), cronTab2.ToUnstructured()) + assert.NoError(t, err) + + startTime := time.Now() + + t.Logf("testSucces: startTime=%s", startTime.String()) + + // this should succeed without any warning + go func() { + t.Logf("testSuccess: in goroutine start d=%s", time.Now().Sub(startTime).String()) + // set the status sub-resource after a few seconds so that the wait finishes + time.Sleep(3 * time.Second) + x := cronTab2.Clone() + _ = x.SetNestedField("test", "status", "x") + t.Logf("testSuccess: in goroutine applying status d=%s", time.Now().Sub(startTime).String()) + k.MustApplyStatus(t, x) + }() + + stdout, _, err = p.Kluctl(t, "deploy", "--yes", "--timeout=10s") + assert.NoError(t, err) + assert.NotContains(t, stdout, "CronTab/crontab: unable to determine if a status is expected") + assert.NotContains(t, stdout, "CronTabStatus/crontab: unable to determine if a status is expected") + } + + // status requirement detection via sub-resource GET + testSuccess(nil, []schema.GroupResource{ + {Group: "", Resource: "configmaps"}, + {Group: "stable.example.com", Resource: "crontabs"}, + {Group: "stable.example.com", Resource: "crontabs/status"}, + {Group: "stable.example.com", Resource: "crontabstatuses"}, + {Group: "stable.example.com", Resource: "crontabstatuses/status"}, + }) + // status requirement detection via CRDs + testSuccess([]schema.GroupResource{ + {Group: "apiextensions.k8s.io", Resource: "customresourcedefinitions"}, + }, []schema.GroupResource{ + {Group: "", Resource: "configmaps"}, + {Group: "stable.example.com", Resource: "crontabs"}, + {Group: "stable.example.com", Resource: "crontabstatuses"}, + }) + // both + testSuccess([]schema.GroupResource{ + {Group: "apiextensions.k8s.io", Resource: "customresourcedefinitions"}, + }, []schema.GroupResource{ + {Group: "", Resource: "configmaps"}, + {Group: "stable.example.com", Resource: "crontabs"}, + {Group: "stable.example.com", Resource: "crontabs/status"}, + {Group: "stable.example.com", Resource: "crontabstatuses"}, + {Group: "stable.example.com", Resource: "crontabstatuses/status"}, + }) +} diff --git a/e2e/when_test.go b/e2e/when_test.go new file mode 100644 index 000000000..c58a2c88e --- /dev/null +++ b/e2e/when_test.go @@ -0,0 +1,91 @@ +package e2e + +import ( + test_utils "github.com/kluctl/kluctl/v2/e2e/test_project" + "github.com/kluctl/kluctl/v2/pkg/utils/uo" + "path/filepath" + "strings" + "testing" +) + +func TestWhen(t *testing.T) { + k := defaultCluster1 + + p := test_utils.NewTestProject(t) + createNamespace(t, k, p.TestSlug()) + p.UpdateTarget("test", func(target *uo.UnstructuredObject) {}) + + type testCase struct { + dir string + when string + whenInc string + depInc string + want bool + name string + } + + tests := []*testCase{ + {dir: "cm_empty", when: "", want: true}, + {dir: "cm_true", when: "True", want: true}, + {dir: "cm_false", when: "False", want: false}, + {dir: "cm_eq", when: "args.a == 'test'", want: true}, + {dir: "cm_ne", when: "args.a != 'test'", want: false}, + {dir: "cm_eq2", when: "args.b == 'test'", want: false}, + {dir: "cm_ne2", when: "args.b != 'test'", want: true}, + {dir: "inc1/cm_empty", when: "", want: true}, + {dir: "inc2/cm_true", when: "True", want: true}, + {dir: "inc3/cm_false", when: "False", want: false}, + {dir: "inc4/cm_eq", when: "args.a == 'test'", want: true}, + {dir: "inc5/cm_ne", when: "args.a != 'test'", want: false}, + {dir: "inc_when1/cm_empty", whenInc: "", want: true}, + {dir: "inc_when2/cm_true", whenInc: "True", want: true}, + {dir: "inc_when3/cm_false", whenInc: "False", want: false}, + {dir: "inc_when4/cm_eq", whenInc: "args.a == 'test'", want: true}, + {dir: "inc_when5/cm_ne", whenInc: "args.a != 'test'", want: false}, + {dir: "dep_inc_when1/cm_empty", depInc: "", want: true}, + {dir: "dep_inc_when2/cm_true", depInc: "True", want: true}, + {dir: "dep_inc_when3/cm_false", depInc: "False", want: false}, + {dir: "dep_inc_when4/cm_eq", depInc: "args.a == 'test'", want: true}, + {dir: "dep_inc_when5/cm_ne", depInc: "args.a != 'test'", want: false}, + } + + for _, test := range tests { + test.name = strings.ReplaceAll(test.dir, "/", "_") + test.name = strings.ReplaceAll(test.name, "_", "-") + addConfigMapDeployment(p, test.dir, nil, resourceOpts{ + name: test.name, + namespace: p.TestSlug(), + when: test.when, + }) + if test.whenInc != "" { + dir := filepath.Dir(test.dir) + p.UpdateDeploymentItems("", func(items []*uo.UnstructuredObject) []*uo.UnstructuredObject { + for _, it := range items { + inc, _, _ := it.GetNestedString("include") + if inc == dir { + _ = it.SetNestedField(test.whenInc, "when") + break + } + } + return items + }) + } + if test.depInc != "" { + dir := filepath.Dir(test.dir) + p.UpdateDeploymentYaml(dir, func(o *uo.UnstructuredObject) error { + _ = o.SetNestedField(test.depInc, "when") + return nil + }) + } + } + + p.KluctlMust(t, "deploy", "--yes", "-t", "test", "-aa=test", "-ab=test2") + + for _, test := range tests { + if test.want { + assertConfigMapExists(t, k, p.TestSlug(), test.name) + } else { + assertConfigMapNotExists(t, k, p.TestSlug(), test.name) + } + } +} diff --git a/go.mod b/go.mod index 2e5ea6a5c..e2597e8ab 100644 --- a/go.mod +++ b/go.mod @@ -1,220 +1,342 @@ module github.com/kluctl/kluctl/v2 -go 1.18 +go 1.24 + +toolchain go1.24.2 + +replace github.com/kluctl/kluctl/lib => ./lib require ( - github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e - github.com/Masterminds/semver/v3 v3.1.1 - github.com/aws/aws-sdk-go v1.44.68 - github.com/bitnami-labs/sealed-secrets v0.18.1 - github.com/cyphar/filepath-securejoin v0.2.3 - github.com/docker/distribution v2.8.1+incompatible - github.com/evanphx/json-patch v5.6.0+incompatible - github.com/fluxcd/pkg/kustomize v0.5.3 - github.com/go-git/go-git/v5 v5.4.2 - github.com/go-playground/validator/v10 v10.11.0 + cloud.google.com/go/secretmanager v1.14.7 + dario.cat/mergo v1.0.1 + filippo.io/age v1.2.1 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 + github.com/Masterminds/semver/v3 v3.3.1 + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/aws/aws-sdk-go-v2 v1.36.3 + github.com/aws/aws-sdk-go-v2/config v1.29.14 + github.com/aws/aws-sdk-go-v2/credentials v1.17.67 + github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.4 + github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 + github.com/aws/smithy-go v1.22.3 + github.com/coder/websocket v1.8.13 + github.com/coreos/go-oidc/v3 v3.14.1 + github.com/cyphar/filepath-securejoin v0.4.1 + github.com/dimchansky/utfbom v1.1.1 + github.com/distribution/distribution/v3 v3.0.0 + github.com/docker/cli v28.1.1+incompatible + github.com/docker/distribution v2.8.3+incompatible + github.com/evanphx/json-patch/v5 v5.9.11 + github.com/getsops/sops/v3 v3.10.2 + github.com/gin-contrib/sessions v1.0.3 + github.com/gin-gonic/gin v1.10.0 + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-git/go-git/v5 v5.16.0 + github.com/go-logr/logr v1.4.2 + github.com/go-playground/validator/v10 v10.26.0 github.com/gobwas/glob v0.2.3 - github.com/golang-jwt/jwt/v4 v4.4.2 - github.com/google/go-containerregistry v0.11.0 - github.com/hashicorp/vault/api v1.7.2 + github.com/google/go-containerregistry v0.20.3 + github.com/google/gops v0.3.28 + github.com/google/uuid v1.6.0 + github.com/googleapis/gax-go/v2 v2.14.1 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/vault/api v1.16.0 github.com/hexops/gotextdiff v1.0.3 - github.com/imdario/mergo v0.3.13 - github.com/jinzhu/copier v0.3.5 - github.com/kevinburke/ssh_config v1.2.0 - github.com/klauspost/compress v1.15.9 - github.com/mattn/go-isatty v0.0.14 - github.com/mitchellh/go-ps v1.0.0 - github.com/mitchellh/reflectwalk v1.0.2 - github.com/ohler55/ojg v1.14.3 + github.com/huandu/xstrings v1.5.0 + github.com/jinzhu/copier v0.4.0 + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/kluctl/go-embed-python v0.0.0-3.11.11-20241219-1 + github.com/kluctl/kluctl/lib v0.0.0 + github.com/mattn/go-colorable v0.1.14 + github.com/mattn/go-isatty v0.0.20 + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/ohler55/ojg v1.26.4 + github.com/onsi/gomega v1.37.0 + github.com/otiai10/copy v1.14.1 + github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.22.0 github.com/r3labs/diff/v2 v2.15.1 - github.com/rogpeppe/go-internal v1.8.1 - github.com/sirupsen/logrus v1.9.0 - github.com/spf13/cobra v1.5.0 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.12.0 - github.com/stretchr/testify v1.7.2 - github.com/vbauerster/mpb/v7 v7.4.2 - github.com/whilp/git-urls v1.0.0 - github.com/xanzy/ssh-agent v0.3.1 - golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa - golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 - golang.org/x/sys v0.0.0-20220731174439-a90be440212d - golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 - golang.org/x/text v0.3.7 - gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 - helm.sh/helm/v3 v3.9.2 - k8s.io/api v0.24.3 - k8s.io/apiextensions-apiserver v0.24.3 - k8s.io/apimachinery v0.24.3 - k8s.io/client-go v0.24.3 - k8s.io/klog/v2 v2.70.1 - sigs.k8s.io/kind v0.14.0 - sigs.k8s.io/kustomize/api v0.12.1 - sigs.k8s.io/kustomize/kyaml v0.13.9 - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 + github.com/rogpeppe/go-internal v1.14.1 + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.6 + github.com/spf13/viper v1.20.1 + github.com/stretchr/testify v1.10.0 + github.com/tkrajina/typescriptify-golang-structs v0.2.0 + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/net v0.39.0 + golang.org/x/oauth2 v0.29.0 + golang.org/x/sync v0.13.0 + golang.org/x/sys v0.32.0 + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + google.golang.org/genproto v0.0.0-20250422160041-2d3770c4ea7f + google.golang.org/grpc v1.72.0 + google.golang.org/protobuf v1.36.6 + helm.sh/helm/v3 v3.17.3 + k8s.io/api v0.32.3 + k8s.io/apiextensions-apiserver v0.32.3 + k8s.io/apimachinery v0.32.3 + k8s.io/client-go v0.32.3 + k8s.io/klog/v2 v2.130.1 + sigs.k8s.io/cli-utils v0.37.2 + sigs.k8s.io/controller-runtime v0.20.4 + sigs.k8s.io/kustomize/api v0.19.0 + sigs.k8s.io/kustomize/kyaml v0.19.0 + sigs.k8s.io/structured-merge-diff/v4 v4.7.0 + sigs.k8s.io/yaml v1.4.0 ) require ( - cloud.google.com/go/compute v1.7.0 // indirect - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.28 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/BurntSushi/toml v1.2.0 // indirect + cel.dev/expr v0.23.1 // indirect + cloud.google.com/go v0.120.1 // indirect + cloud.google.com/go/auth v0.16.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/kms v1.21.2 // indirect + cloud.google.com/go/longrunning v0.6.7 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect + cloud.google.com/go/storage v1.51.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/sprig/v3 v3.2.2 // indirect - github.com/Masterminds/squirrel v1.5.3 // indirect - github.com/Microsoft/go-winio v0.5.2 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20220730123233-d6ffb7692adf // indirect - github.com/VividCortex/ewma v1.2.0 // indirect - github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/acomagu/bufpipe v1.0.3 // indirect - github.com/alessio/shellescape v1.4.1 // indirect - github.com/armon/go-metrics v0.4.0 // indirect - github.com/armon/go-radix v1.0.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.12.9 // indirect + github.com/ProtonMail/go-crypto v1.2.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.72 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v3 v3.2.2 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect - github.com/cloudflare/circl v1.2.0 // indirect - github.com/containerd/containerd v1.6.6 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/cli v20.10.17+incompatible // indirect - github.com/docker/docker v20.10.17+incompatible // indirect - github.com/docker/docker-credential-helpers v0.6.4 // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bshuster-repo/logrus-logstash-hook v1.1.0 // indirect + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.3 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect + github.com/containerd/containerd v1.7.27 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.1.1+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 // indirect github.com/docker/go-metrics v0.0.1 // indirect - github.com/docker/go-units v0.4.0 // indirect - github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fatih/color v1.13.0 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/go-errors/errors v1.4.2 // indirect - github.com/go-git/gcfg v1.5.0 // indirect - github.com/go-git/go-billy/v5 v5.3.1 // indirect - github.com/go-gorp/gorp/v3 v3.0.2 // indirect - github.com/go-logr/logr v1.2.3 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/swag v0.21.1 // indirect - github.com/go-playground/locales v0.14.0 // indirect - github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.0 // indirect + github.com/go-logr/stdr v1.2.2 // 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/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/btree v1.1.2 // indirect - github.com/google/gnostic v0.6.9 // indirect - github.com/google/go-cmp v0.5.8 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/handlers v1.5.2 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.4.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/gosuri/uitable v0.0.4 // indirect + github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.2.2 // indirect - github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.4.4 // indirect - github.com/hashicorp/go-retryablehttp v0.7.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect - github.com/hashicorp/go-sockaddr v1.0.2 // indirect - github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/vault/sdk v0.5.3 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect - github.com/huandu/xstrings v1.3.2 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/jmoiron/sqlx v1.3.5 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/leodido/go-urn v1.2.1 // indirect - github.com/lib/pq v1.10.6 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/magiconair/properties v1.8.6 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/locker v1.0.1 // indirect - github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect - github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oklog/run v1.1.0 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.2 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/otiai10/mint v1.6.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect - github.com/pierrec/lz4 v2.6.1+incompatible // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.12.2 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect - github.com/rivo/uniseg v0.3.1 // indirect - github.com/rubenv/sql-migrate v1.1.2 // indirect - github.com/russross/blackfriday v1.6.0 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 // indirect + github.com/redis/go-redis/extra/redisotel/v9 v9.7.3 // indirect + github.com/redis/go-redis/v9 v9.7.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rubenv/sql-migrate v1.8.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - github.com/sergi/go-diff v1.2.0 // indirect - github.com/shopspring/decimal v1.3.1 // indirect - github.com/spf13/afero v1.9.2 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/subosito/gotenv v1.4.0 // indirect + github.com/sagikazarmark/locafero v0.9.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.14.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/urfave/cli v1.22.16 // indirect + github.com/vbatts/tar-split v0.12.1 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xlab/treeprint v1.1.0 // indirect - go.starlark.net v0.0.0-20220714194419-4cadf0a12139 // indirect - go.uber.org/atomic v1.9.0 // indirect - golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c // indirect - golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 // indirect - google.golang.org/grpc v1.48.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + github.com/zeebo/errs v1.4.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.60.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect + go.opentelemetry.io/contrib/exporters/autoexport v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect + go.opentelemetry.io/otel/log v0.11.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.11.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/arch v0.16.0 // indirect + golang.org/x/time v0.11.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/api v0.229.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.66.6 // indirect - gopkg.in/square/go-jose.v2 v2.6.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - k8s.io/apiserver v0.24.3 // indirect - k8s.io/cli-runtime v0.24.3 // indirect - k8s.io/component-base v0.24.3 // indirect - k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect - k8s.io/kubectl v0.24.3 // indirect - k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect - oras.land/oras-go v1.2.0 // indirect - sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiserver v0.32.3 // indirect + k8s.io/cli-runtime v0.32.3 // indirect + k8s.io/component-base v0.32.3 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/kubectl v0.32.3 // indirect + k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect + oras.land/oras-go v1.2.6 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index a2dc07990..fb6d2d0b2 100644 --- a/go.sum +++ b/go.sum @@ -1,966 +1,524 @@ -4d63.com/gochecknoglobals v0.1.0/go.mod h1:wfdC5ZjKSPr7CybKEcgJhUOgeAQW1+7WcyK8OvUilfo= -bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.60.0/go.mod h1:yw2G51M9IfRboUH61Us8GqCeF1PzPblB823Mn2q2eAU= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.5.0/go.mod h1:ZEwJccE3z93Z2HWvstpri00jOg7oO4UZDtKhwDwqF0w= -cloud.google.com/go/spanner v1.7.0/go.mod h1:sd3K2gZ9Fd0vMPLXzeCrF6fq4i63Q7aTLW/lBIfBkIk= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Antonboom/errname v0.1.5/go.mod h1:DugbBstvPFQbv/5uLcRRzfrNqKE9tVdVCqWCLp6Cifo= -github.com/Antonboom/nilnil v0.1.0/go.mod h1:PhHLvRPSghY5Y7mX4TW+BHZQYo1A8flE5H20D3IPZBo= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest v0.11.27 h1:F3R3q42aWytozkV8ihzcgMO4OA4cuqr3bNlsEuF6//A= -github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U= -github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM= -github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= -github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/adal v0.9.20 h1:gJ3E98kMpFB1MFqQCvA1yFab8vthOeD4VlFRQULxahg= -github.com/Azure/go-autorest/autorest/adal v0.9.20/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/adal v0.9.21 h1:jjQnVFXPfekaqb8vIsv2G1lxshoW+oGv4MDlhRtnYZk= -github.com/Azure/go-autorest/autorest/adal v0.9.21/go.mod h1:zua7mBUaCc5YnSLKYgGJR/w5ePdMDA6H56upLsHzA9U= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= -github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ= -github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= -github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= -github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= +c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= +cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= +cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.120.1 h1:Z+5V7yd383+9617XDCyszmK5E4wJRJL+tquMfDj9hLM= +cloud.google.com/go v0.120.1/go.mod h1:56Vs7sf/i2jYM6ZL9NYlC82r04PThNcPS5YgFmb0rp8= +cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU= +cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/kms v1.21.2 h1:c/PRUSMNQ8zXrc1sdAUnsenWWaNXN+PzTXfXOcSFdoE= +cloud.google.com/go/kms v1.21.2/go.mod h1:8wkMtHV/9Z8mLXEXr1GK7xPSBdi6knuLXIhqjuWcI6w= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/secretmanager v1.14.7 h1:VkscIRzj7GcmZyO4z9y1EH7Xf81PcoiAo7MtlD+0O80= +cloud.google.com/go/secretmanager v1.14.7/go.mod h1:uRuB4F6NTFbg0vLQ6HsT7PSsfbY7FqHbtJP1J94qxGc= +cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw= +cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= +filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 h1:mrkDCdkMsD4l9wjFGhofFHFrV43Y3c53RSLKOCJ5+Ow= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1/go.mod h1:hPv41DbqMmnxcGralanA/kVlfdH5jv3T4LxGku2E1BY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= -github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= -github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= -github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= -github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/hcsshim v0.9.3 h1:k371PzBuRrz2b+ebGuI2nVgVhgsVX60jMfSw80NECxo= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= -github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/ProtonMail/go-crypto v0.0.0-20220517143526-88bb52951d5b h1:lcbBNuQhppsc7A5gjdHmdlqUqJfgGMylBdGyDs0j7G8= -github.com/ProtonMail/go-crypto v0.0.0-20220517143526-88bb52951d5b/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/ProtonMail/go-crypto v0.0.0-20220730123233-d6ffb7692adf h1:aFFtnGZ6/2Qlvx80yxA2fFSYDQWTFjtKozQKB36A3/A= -github.com/ProtonMail/go-crypto v0.0.0-20220730123233-d6ffb7692adf/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8= -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/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= -github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= -github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= -github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg= +github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs= +github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= -github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= -github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= -github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= -github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8Q= -github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 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/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= -github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/ashanbrown/forbidigo v1.2.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBFg8t0sG2FIxmI= -github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU= -github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.36.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.44.28 h1:h/OAqEqY18wq//v6h4GNPMmCkxuzSDrWuGyrvSiRqf4= -github.com/aws/aws-sdk-go v1.44.28/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go v1.44.68 h1:7zNr5+HLG0TMq+ZcZ8KhT4eT2KyL7v+u7/jANKEIinM= -github.com/aws/aws-sdk-go v1.44.68/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= +github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= +github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.72 h1:PcKMOZfp+kNtJTw2HF2op6SjDvwPBYRvz0Y24PQLUR4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.72/go.mod h1:vq7/m7dahFXcdzWVOvvjasDI9RcsD3RsTfHmDundJYg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= +github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0 h1:E+UTVTDH6XTSjqxHWRuY8nB6s+05UllneWxnycplHFk= +github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 h1:RivOtUH3eEu6SWnUMFHKAW4MqDOzWn1vGQ3S38Y5QMg= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.3/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 h1:tWUG+4wZqdMl/znThEk9tcCy8tTMxq8dW0JTgamohrY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.4 h1:EKXYJ8kgz4fiqef8xApu7eH0eae2SrVG+oHCLFybMRI= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.4/go.mod h1:yGhDiLKguA3iFJYxbrQkQiNzuy+ddxesSZYWVeeEH5Q= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= +github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitnami-labs/sealed-secrets v0.18.0 h1:7LdfPRMyx9nGQW9204JM1F+F+s6xmaSBl8oNujlrCuc= -github.com/bitnami-labs/sealed-secrets v0.18.0/go.mod h1:uV8CUHJQVcDOY9cZ1FdC1rtybC4ath+VGH+/dUQvBu0= -github.com/bitnami-labs/sealed-secrets v0.18.1 h1:xXzi0Z6lArTykRGqCOC2rN3zN648zjQtkCzAVjJj9OY= -github.com/bitnami-labs/sealed-secrets v0.18.1/go.mod h1:pOMGS1imRiIPLm7OpdD/s/OfgQmLkKxTqWruSEiQqCM= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/blizzy78/varnamelen v0.3.0/go.mod h1:hbwRdBvoBqxk34XyQ6HA0UH3G0/1TKuv5AC4eaBT0Ec= -github.com/bombsimon/wsl/v3 v3.3.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= -github.com/breml/bidichk v0.1.1/go.mod h1:zbfeitpevDUGI7V91Uzzuwrn4Vls8MoBMrwtt78jmso= -github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= -github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= -github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 h1:7aWHqerlJ41y6FOsEUvknqgXnGmJyJSbjhAWq5pO4F8= -github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= -github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= -github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU= -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/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec= -github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= -github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= -github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBdAP4= -github.com/containerd/containerd v1.6.6 h1:xJNPhbrmz8xAMDNoVjHy9YHtWwEQNS+CDkcIRh7t8Y0= -github.com/containerd/containerd v1.6.6/go.mod h1:ZoP1geJldzCVY3Tonoz7b1IXk8rIX0Nltt5QE4OMNk0= -github.com/containerd/stargz-snapshotter/estargz v0.11.4 h1:LjrYUZpyOhiSaU7hHrdR82/RBoxfGWSaC0VeSSMXqnk= -github.com/containerd/stargz-snapshotter/estargz v0.12.0 h1:idtwRTLjk2erqiYhPWy2L844By8NRFYEwYHcXhoIWPM= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= -github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/daixiang0/gci v0.2.9/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc= -github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/bshuster-repo/logrus-logstash-hook v1.1.0 h1:o2FzZifLg+z/DN1OFmzTWzZZx/roaqt8IPZCIVco8r4= +github.com/bshuster-repo/logrus-logstash-hook v1.1.0/go.mod h1:Q2aXOe7rNuPgbBtPCOzYyWDvKX7+FpxE5sRdvcPoui0= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= +github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= +github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= +github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= +github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= +github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/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/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= +github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= +github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 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/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= -github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA= -github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684 h1:DBZ2sN7CK6dgvHVpQsQj4sRMCbWTmd17l+5SUCjnQSY= -github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 h1:hbCT8ZPPMqefiAWD2ZKjn7ypokIGViTvBBg/ExLSdCk= -github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= -github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= -github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= -github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= +github.com/docker/cli v28.1.1+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 v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= +github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +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/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 h1:EHZfspsnLAz8Hzccd67D5abwLiqoqym2jz/jOS39mCk= +github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful/v3 v3.7.5-0.20220308211933-7c971ca4d0fd/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= -github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= -github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +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/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/esimonov/ifshort v1.0.3/go.mod h1:yZqNJUrNn20K8Q9n2CrjTKYyVEmX209Hgu+M1LBpeZE= -github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= -github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -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.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= -github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= -github.com/fluxcd/pkg/kustomize v0.5.1 h1:151Ih34ltxN2z1e2mA5AvQONyE6phc4es57oVK3+plU= -github.com/fluxcd/pkg/kustomize v0.5.1/go.mod h1:58MFITy24bIbGI6cC3JkV/YpFQj648sVvgs0K1kraJw= -github.com/fluxcd/pkg/kustomize v0.5.3 h1:WpxNOV/Yklp0p7Qv85VwBegq9fABuLR9qSWaAVa3+yc= -github.com/fluxcd/pkg/kustomize v0.5.3/go.mod h1:zy1FLxkEDADUykCnrXqq6rVN48t3uMhAb3ao+zv0rFE= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= -github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -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.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= -github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= -github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= -github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= -github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-critic/go-critic v0.6.1/go.mod h1:SdNCfU0yF3UBjtaZGw6586/WocupMOJuiqgom5DsQxM= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= -github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= -github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= -github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= -github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= -github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= -github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4= -github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= +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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +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.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e h1:y/1nzrdF+RPds4lfoEpNhjfmzlgZtPqyO3jMzrqDQws= +github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM= +github.com/getsops/sops/v3 v3.10.2 h1:7t7lBXFcXJPsDMrpYoI36r8xIhjWUmEc8Qdjuwyo+WY= +github.com/getsops/sops/v3 v3.10.2/go.mod h1:Dmtg1qKzFsAl+yqvMgjtnLGTC0l7RnSM6DDtFG7TEsk= +github.com/gin-contrib/sessions v1.0.3 h1:AZ4j0AalLsGqdrKNbbrKcXx9OJZqViirvNGsJTxcQps= +github.com/gin-contrib/sessions v1.0.3/go.mod h1:5i4XMx4KPtQihnzxEqG9u1K446lO3G19jAi2GtbfsAI= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ= +github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -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.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= -github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= -github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= -github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= -github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= -github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= -github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= -github.com/go-toolsmith/astequal v1.0.1/go.mod h1:4oGA3EZXTVItV/ipGiOx7NWkY5veFfcsOJVS2YxltLw= -github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= -github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= -github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= -github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= -github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= -github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= -github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= -github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= -github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= -github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= -github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= -github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= -github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= -github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= -github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= -github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 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.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= -github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= -github.com/golangci/golangci-lint v1.43.0/go.mod h1:VIFlUqidx5ggxDfQagdvd9E67UjMXtTHBkBQ7sHoC5Q= -github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= -github.com/golangci/misspell v0.3.5/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= -github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2/go.mod h1:LK+zW4MpyytAWQRz0M4xnzEk50lSvqDQKfx304apFkY= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= -github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= -github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.10.1/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= -github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= -github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/certificate-transparency-go v1.1.1/go.mod h1:FDKqPvSXawb2ecErVRrD+nfy23RCzyl7eqVCEmlT1Zs= -github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= -github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= -github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.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.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/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.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.9.0 h1:5Ths7RjxyFV0huKChQTgY6fLzvHhZMpLTFNja8U0/0w= -github.com/google/go-containerregistry v0.9.0/go.mod h1:9eq4BnSufyT1kHNffX+vSXVonaJ7yaIOulrKZejMxnQ= -github.com/google/go-containerregistry v0.11.0 h1:Xt8x1adcREjFcmDoDK8OdOsjxu90PHkGuwNP8GiHMLM= -github.com/google/go-containerregistry v0.11.0/go.mod h1:BBaYtsHPHA42uEgAvd/NejvAfPSlz281sJWqupjSxfk= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= 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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/gops v0.3.28 h1:2Xr57tqKAmQYRAfG12E+yLcoa2Y42UJo2lOrUFL9ark= +github.com/google/gops v0.3.28/go.mod h1:6f6+Nl8LcHrzJwi8+p0ii+vmBFSlB4f8cOOkTJ7sk4c= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw= -github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/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/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= -github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254/go.mod h1:M9mZEtGIsR1oDaZagNPNG9iq9n2HrhZ17dsXk73V3Lw= -github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= -github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= -github.com/gostaticanalysis/analysisutil v0.1.0/go.mod h1:dMhHRU9KTiDcuLGdy87/2gTR8WruwYZrKdRq9m1O6uw= -github.com/gostaticanalysis/analysisutil v0.4.1/go.mod h1:18U/DLpRgIUd459wGxVHE0fRgmo1UgHDcbw7F5idXu0= -github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= -github.com/gostaticanalysis/comment v1.3.0/go.mod h1:xMicKDx7XRXYdVwY9f9wQpDJVnqWxw9wCauCMKp+IBI= -github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= -github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= -github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= -github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= -github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= -github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= +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/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw= +github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= -github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-hclog v1.2.2 h1:ihRI7YFwcZdiSD7SIenIhHfQH3OuDvWerAUBZbeQS3M= -github.com/hashicorp/go-hclog v1.2.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= -github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ= -github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= -github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 h1:p4AKXPPS24tO8Wc8i1gLvSKdmkiSY5xuju57czJ/IJQ= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.5/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= -github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= -github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E= -github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8zEbfAAzRv52KQ= +github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hashicorp/vault/api v1.6.0 h1:B8UUYod1y1OoiGHq9GtpiqSnGOUEWHaA26AY8RQEDY4= -github.com/hashicorp/vault/api v1.6.0/go.mod h1:h1K70EO2DgnBaTz5IsL6D5ERsNt5Pce93ueVS2+t0Xc= -github.com/hashicorp/vault/api v1.7.2 h1:kawHE7s/4xwrdKbkmwQi0wYaIeUhk5ueek7ljuezCVQ= -github.com/hashicorp/vault/api v1.7.2/go.mod h1:xbfA+1AvxFseDzxxdWaL0uO99n1+tndus4GCrtouy0M= -github.com/hashicorp/vault/sdk v0.5.0 h1:EED7p0OCU3OY5SAqJwSANofY1YKMytm+jDHDQ2EzGVQ= -github.com/hashicorp/vault/sdk v0.5.0/go.mod h1:UJZHlfwj7qUJG8g22CuxUgkdJouFrBNvBHCyx8XAPdo= -github.com/hashicorp/vault/sdk v0.5.3 h1:PWY8sq/9pRrK9vUIy75qCH2Jd8oeENAgkaa/qbhzFrs= -github.com/hashicorp/vault/sdk v0.5.3/go.mod h1:DoGraE9kKGNcVgPmTuX357Fm6WAx1Okvde8Vp3dPDoU= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I= -github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= +github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= -github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= -github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= -github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= -github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= -github.com/jhump/protoreflect v1.6.1 h1:4/2yi5LyDPP7nN+Hiird1SAJ6YoxUm13/oxHGRnbPd8= -github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= -github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= -github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 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/josharian/txtarfs v0.0.0-20210218200122-0702f000015a/go.mod h1:izVPOvVRsHiKkeGCT6tYBNWyDVuzj9wAaBb5R9qamfw= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/julz/importas v0.0.0-20210419104244-841f0c0fe66d/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= -github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= -github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY= -github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +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/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kluctl/go-embed-python v0.0.0-3.11.11-20241219-1 h1:yVXFTxa6cvhLC6TwkDEHmNaiWlDyaFEGiXd1DQdoeOU= +github.com/kluctl/go-embed-python v0.0.0-3.11.11-20241219-1/go.mod h1:3ebNU9QBrNpUO+Hj6bHaGpkh5pymDHQ+wwVPHTE4mCE= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 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.2/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/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 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.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/kulti/thelper v0.4.0/go.mod h1:vMu2Cizjy/grP+jmsvOFDx1kYP6+PD1lqg4Yu5exl2U= -github.com/kunwardeep/paralleltest v1.0.3/go.mod h1:vLydzomDFpk7yu5UX02RmP0H8QfRPOV/oFhWN85Mjb4= +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/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/ldez/gomoddirectives v0.2.2/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= -github.com/ldez/tagliatelle v0.2.0/go.mod h1:8s6WJQwEYHbKZDsp/LjArytKOG8qaMrKQQ3mFukHs88= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= -github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= -github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -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.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -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/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= -github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= -github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= -github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= -github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= -github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/matoous/godox v0.0.0-20210227103229-6504466cf951/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= -github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= -github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= -github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= -github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= -github.com/mgechev/revive v1.1.2/go.mod h1:bnXsMr+ZTH09V5rssEI+jHAZ4z+ZdyhgO/zsy3EhK+0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= -github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= -github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -968,256 +526,121 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN 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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= -github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1/go.mod h1:FIczTrinKo8VaLxe6PWTPEXRXDIHz2QAwiaBaP5/4a8= -github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= -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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= -github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= -github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nishanths/exhaustive v0.2.3/go.mod h1:bhIX678Nx8inLM9PbpvK1yv6oGtoP8BfaIeMzgBNKvc= -github.com/nishanths/predeclared v0.0.0-20190419143655-18a43bb90ffc/go.mod h1:62PewwiQTlm/7Rj+cxVYqZvDIUc+JjZq6GHAC1fsObQ= -github.com/nishanths/predeclared v0.2.1/go.mod h1:HvkGJcA3naj4lOwnFXFDkFxVtSqQMB9sbB1usJ+xjQE= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/ohler55/ojg v1.14.2 h1:EHdrwmDrOuTGpj1W2LrT/yKeUOkLMIk1cTYcm42Sjj0= -github.com/ohler55/ojg v1.14.2/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= -github.com/ohler55/ojg v1.14.3 h1:kaKNsntZ0PuoXPXCY4kjPDbHOLXqokor6Deq/oVxAR0= -github.com/ohler55/ojg v1.14.3/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= -github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= -github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -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.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -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.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/ohler55/ojg v1.26.4 h1:DTatWge/oPYVw7p7movCviCbVqe+s+iNkaGwo1HN6Jc= +github.com/ohler55/ojg v1.26.4/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= +github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= +github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 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.0.3-0.20220114050600-8b9d41f48198 h1:+czc/J8SlhPKLOtVLMQc+xDCFBT73ZStMsRhSsUhsSg= -github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198/go.mod h1:j4h1pJW6ZcJTgMZWP3+7RlG3zTaP02aDZ/Qw0sppK7Q= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= -github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= +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/opencontainers/runc v1.2.6 h1:P7Hqg40bsMvQGCS4S7DJYhUZOISMLJOB2iGX5COWiPk= +github.com/opencontainers/runc v1.2.6/go.mod h1:dOQeFo29xZKBNeRBI0B19mJtfHv68YgCTh1X+YphA+4= +github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= +github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= +github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= +github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= +github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= +github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= -github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= -github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8C/OCy0zs9906d/VUru+bqg= -github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +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/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= -github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +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.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.34.0 h1:RBmGO9d/FVjqHT0yUGQwBJhkwKV+wPCn7KGpvfab0uE= -github.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in3v/bX88wUwgt+U8EA= -github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= -github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= -github.com/quasilyte/go-ruleguard v0.3.1-0.20210203134552-1b5a410e1cc8/go.mod h1:KsAh3x0e7Fkpgs+Q9pNLS5XpFSvYCEVl5gP9Pp1xp30= -github.com/quasilyte/go-ruleguard v0.3.13/go.mod h1:Ul8wwdqR6kBVOCt2dipDBkE+T6vAV/iixkrKuRTN1oQ= -github.com/quasilyte/go-ruleguard/dsl v0.3.0/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/quasilyte/go-ruleguard/dsl v0.3.10/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc= -github.com/quasilyte/go-ruleguard/rules v0.0.0-20210428214800-545e0d2e0bf7/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50= -github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg= github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 h1:1AXQZkJkFxGV3f78mSnUI70l0orO6FHnYoSmBos8SZM= +github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3/go.mod h1:OgkpkwJYex1oyVAabK+VhVUKhUXw8uZUfewJYH1wG90= +github.com/redis/go-redis/extra/redisotel/v9 v9.7.3 h1:ICBA9xYh+SmZqMfBtjKpp1ohi/V5R1TEZglLZc8IxTc= +github.com/redis/go-redis/extra/redisotel/v9 v9.7.3/go.mod h1:DMzxd0CDyZ9VFw9sEPIVpIgKTAaubfGuaPQSUaS7/fo= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.3.1 h1:SDPP7SHNl1L7KrEFCSJslJ/DM9DT02Nq2C61XrfHMmk= -github.com/rivo/uniseg v0.3.1/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/rubenv/sql-migrate v1.1.1 h1:haR5Hn8hbW9/SpAICrXoZqXnywS7Q5WijwkQENPeNWY= -github.com/rubenv/sql-migrate v1.1.1/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= -github.com/rubenv/sql-migrate v1.1.2 h1:9M6oj4e//owVVHYrFISmY9LBRw6gzkCNmD9MV36tZeQ= -github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= -github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +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/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= +github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryancurrah/gomodguard v1.2.3/go.mod h1:rYbA/4Tg5c54mV1sv4sQTP5WOPBcoLtnBZ7/TEhXAbg= -github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= -github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4wdiJ1n4iHc= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= -github.com/shirou/gopsutil/v3 v3.21.10/go.mod h1:t75NhzCZ/dYyPQjyQmrAYP6c8+LCdFANeBMdLPCNnew= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= +github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sivchari/tenv v1.4.7/go.mod h1:5nF+bITvkebQVanjU6IuMbvIot/7ReNsUV7I5NbprB0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI= -github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= -github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -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/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= -github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= 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.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1225,48 +648,33 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= -github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= -github.com/sylvia7788/contextcheck v1.0.4/go.mod h1:vuPKJMQ7MQ91ZTqfdyreNKwZjyUg6KO+IebVyQDedZQ= -github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= -github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= -github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaEohUpn8= -github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= -github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= -github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tomarrell/wrapcheck/v2 v2.4.0/go.mod h1:68bQ/eJg55BROaRTbMjC7vuhL2OgfoG8bLp9ZyoBfyY= -github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= -github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= -github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/uudashr/gocognit v1.0.5/go.mod h1:wgYz0mitoKOTysqxTDMOUXg+Jb5SvtihkfmugIZYpEA= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= -github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= -github.com/vbauerster/mpb/v7 v7.4.2 h1:n917F4d8EWdUKc9c81wFkksyG6P6Mg7IETfKCE1Xqng= -github.com/vbauerster/mpb/v7 v7.4.2/go.mod h1:UmOiIUI8aPqWXIps0ciik3RKMdzx7+ooQpq+fBcXwBA= -github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= +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/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/tkrajina/typescriptify-golang-structs v0.2.0 h1:ZedWk82egydDspGTryAatbX0/1NZDQbdiZLoCbOk4f8= +github.com/tkrajina/typescriptify-golang-structs v0.2.0/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= +github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= +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/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU= -github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= -github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= -github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo= -github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w= +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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -1274,869 +682,236 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= -github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= -github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yeya24/promlinter v0.1.0/go.mod h1:rs5vtZzeBHqqMwXqFScncpCF6u06lezhZepno9AB1Oc= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/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.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= -github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= -github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6yfWfDmlWd35khcWpUa4L0xI/k= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.etcd.io/etcd/client/v3 v3.5.1/go.mod h1:OnjH4M8OnAotwaB2l9bVgZzRFKru7/ZMoS46OtKyd3Q= -go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= -go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= -go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= -go.mozilla.org/mozlog v0.0.0-20170222151521-4bb13139d403/go.mod h1:jHoPAGnDrCy6kaI2tAze5Prf0Nr0w/oNkROt2lw3n3o= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= -go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= -go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= -go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= -go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= -go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= -go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= -go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= -go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= -go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd h1:Uo/x0Ir5vQJ+683GXB9Ug+4fcjsbp7z7Ul8UaZbhsRM= -go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= -go.starlark.net v0.0.0-20220714194419-4cadf0a12139 h1:zMemyQYZSyEdPaUFixYICrXf/0Rfnil7+jiQRf5IBZ0= -go.starlark.net v0.0.0-20220714194419-4cadf0a12139/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/bridges/prometheus v0.60.0 h1:x7sPooQCwSg27SjtQee8GyIIRTQcF4s7eSkac6F2+VA= +go.opentelemetry.io/contrib/bridges/prometheus v0.60.0/go.mod h1:4K5UXgiHxV484efGs42ejD7E2J/sIlepYgdGoPXe7hE= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/exporters/autoexport v0.60.0 h1:GuQXpvSXNjpswpweIem84U9BNauqHHi2w1GtNAalvpM= +go.opentelemetry.io/contrib/exporters/autoexport v0.60.0/go.mod h1:CkmxekdHco4d7thFJNPQ7Mby4jMBgZUclnrxT4e+ryk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 h1:HMUytBT3uGhPKYY/u/G5MR9itrlSO2SMOsSD3Tk3k7A= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0/go.mod h1:hdDXsiNLmdW/9BF2jQpnHHlhFajpWCEYfM6e5m2OAZg= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 h1:C/Wi2F8wEmbxJ9Kuzw/nhP+Z9XaHYMkyDmXy6yR2cjw= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0/go.mod h1:0Lr9vmGKzadCTgsiBydxr6GEZ8SsZ7Ks53LzjWG5Ar4= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk= +go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 h1:k6KdfZk72tVW/QVZf60xlDziDvYAePj5QHwoQvrB2m8= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0/go.mod h1:5Y3ZJLqzi/x/kYtrSrPSx7TFI/SGsL7q2kME027tH6I= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= +go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= +go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c= +go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= +golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/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-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 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.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/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-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/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-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b h1:3ogNYyK4oIQdIKzTu68hQrr4iuVxF3AxKl9Aj/eDrw0= -golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw= -golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c h1:q3gFqPqH7NVofKo3c3yETAP//pPI+G5mvB7qqj1Y5kY= -golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190412183630-56d357773e84/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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20201207232520-09787c993a3a/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-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/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-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/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-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/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-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/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-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/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-20220731174439-a90be440212d h1:Sv5ogFZatcgIMMtBSTTAgMYsicp25MXBubjXNDKwm80= -golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= -golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= -golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= -golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= -golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190307163923-6a08e3108db3/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190916130336-e45ffcd953cc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/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-20200622203043-20e05c1c8ffa/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200624225443-88f3c62a19ff/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200630154851-b2d8b0336632/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200706234117-b22de6825cf7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200831203904-5a2aa26beb65/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201028025901-8cd080b735b3/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201230224404-63754364767c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= -golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 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-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/api v0.229.0 h1:p98ymMtqeJ5i3lIBMj5MpR9kzIIgzpHHh8vQ+vgAzx8= +google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -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/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200707001353-8e8330bf89df/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 h1:qRu95HZ148xXw+XeZ3dvqe85PxH4X8+jIo0iRPKcEnM= -google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 h1:QntLWYqZeuBtJkth3m/6DLznnI0AHJr+AgJXvVh/izw= -google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20250422160041-2d3770c4ea7f h1:iZiXS7qm4saaCcdK7S/i1Qx9ZHO2oa16HQqwYc1tPKY= +google.golang.org/genproto v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:Cej/8iHf9mPl71o/a+R1rrvSFrAAVCUFX9s/sbNttBc= +google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f h1:tjZsroqekhC63+WMqzmWyW5Twj/ZfR5HAlpd5YQ1Vs0= +google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 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 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= -gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 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.3/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.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.6/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.0/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.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -helm.sh/helm/v3 v3.9.0 h1:qDSWViuF6SzZX5s5AB/NVRGWmdao7T5j4S4ebIkMGag= -helm.sh/helm/v3 v3.9.0/go.mod h1:fzZfyslcPAWwSdkXrXlpKexFeE2Dei8N27FFQWt+PN0= -helm.sh/helm/v3 v3.9.2 h1:bx7kdhr5VAhYoWv9bIdT1C6qWR+/7SIoPCwLx22l78g= -helm.sh/helm/v3 v3.9.2/go.mod h1:y/dJc/0Lzcn40jgd85KQXnufhFF7sr4v6L/vYMLRaRM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= -k8s.io/api v0.24.1 h1:BjCMRDcyEYz03joa3K1+rbshwh1Ay6oB53+iUx2H8UY= -k8s.io/api v0.24.1/go.mod h1:JhoOvNiLXKTPQ60zh2g0ewpA+bnEYf5q44Flhquh4vQ= -k8s.io/api v0.24.3 h1:tt55QEmKd6L2k5DP6G/ZzdMQKvG5ro4H4teClqm0sTY= -k8s.io/api v0.24.3/go.mod h1:elGR/XSZrS7z7cSZPzVWaycpJuGIw57j9b95/1PdJNI= -k8s.io/apiextensions-apiserver v0.24.1 h1:5yBh9+ueTq/kfnHQZa0MAo6uNcPrtxPMpNQgorBaKS0= -k8s.io/apiextensions-apiserver v0.24.1/go.mod h1:A6MHfaLDGfjOc/We2nM7uewD5Oa/FnEbZ6cD7g2ca4Q= -k8s.io/apiextensions-apiserver v0.24.3 h1:kyx+Tmro1qEsTUr07ZGQOfvTsF61yn+AxnxytBWq8As= -k8s.io/apiextensions-apiserver v0.24.3/go.mod h1:cL0xkmUefpYM4f6IuOau+6NMFEIh6/7wXe/O4vPVJ8A= -k8s.io/apimachinery v0.24.1 h1:ShD4aDxTQKN5zNf8K1RQ2u98ELLdIW7jEnlO9uAMX/I= -k8s.io/apimachinery v0.24.1/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= -k8s.io/apimachinery v0.24.3 h1:hrFiNSA2cBZqllakVYyH/VyEh4B581bQRmqATJSeQTg= -k8s.io/apimachinery v0.24.3/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= -k8s.io/apiserver v0.24.1 h1:LAA5UpPOeaREEtFAQRUQOI3eE5So/j5J3zeQJjeLdz4= -k8s.io/apiserver v0.24.1/go.mod h1:dQWNMx15S8NqJMp0gpYfssyvhYnkilc1LpExd/dkLh0= -k8s.io/apiserver v0.24.3 h1:J8CKjUaZopT0hSgxjzUyp3T1GK78iixxOuFpEC0MI3k= -k8s.io/apiserver v0.24.3/go.mod h1:aXfwtIn4U27B7lYs5f2BKgz6DRbgWy+HJeYReN1jLJ8= -k8s.io/cli-runtime v0.24.1 h1:IW6L8dRBq+pPTzvXcB+m/hOabzbqXy57Bqo4XxmW7DY= -k8s.io/cli-runtime v0.24.1/go.mod h1:14aVvCTqkA7dNXY51N/6hRY3GUjchyWDOwW84qmR3bs= -k8s.io/cli-runtime v0.24.3 h1:O9YvUHrDSCQUPlsqVmaqDrueqjpJ7IO6Yas9B6xGSoo= -k8s.io/cli-runtime v0.24.3/go.mod h1:In84wauoMOqa7JDvDSXGbf8lTNlr70fOGpYlYfJtSqA= -k8s.io/client-go v0.24.1 h1:w1hNdI9PFrzu3OlovVeTnf4oHDt+FJLd9Ndluvnb42E= -k8s.io/client-go v0.24.1/go.mod h1:f1kIDqcEYmwXS/vTbbhopMUbhKp2JhOeVTfxgaCIlF8= -k8s.io/client-go v0.24.3 h1:Nl1840+6p4JqkFWEW2LnMKU667BUxw03REfLAVhuKQY= -k8s.io/client-go v0.24.3/go.mod h1:AAovolf5Z9bY1wIg2FZ8LPQlEdKHjLI7ZD4rw920BJw= -k8s.io/code-generator v0.24.1/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= -k8s.io/code-generator v0.24.3/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= -k8s.io/component-base v0.24.1 h1:APv6W/YmfOWZfo+XJ1mZwep/f7g7Tpwvdbo9CQLDuts= -k8s.io/component-base v0.24.1/go.mod h1:DW5vQGYVCog8WYpNob3PMmmsY8A3L9QZNg4j/dV3s38= -k8s.io/component-base v0.24.3 h1:u99WjuHYCRJjS1xeLOx72DdRaghuDnuMgueiGMFy1ec= -k8s.io/component-base v0.24.3/go.mod h1:bqom2IWN9Lj+vwAkPNOv2TflsP1PeVDIwIN0lRthxYY= -k8s.io/component-helpers v0.24.1/go.mod h1:q5Z1pWV/QfX9ThuNeywxasiwkLw9KsR4Q9TAOdb/Y3s= -k8s.io/component-helpers v0.24.3/go.mod h1:/1WNW8TfBOijQ1ED2uCHb4wtXYWDVNMqUll8h36iNVo= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -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/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -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.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= -k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= -k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= -k8s.io/kube-openapi v0.0.0-20220401212409-b28bf2818661/go.mod h1:daOouuuwd9JXpv1L7Y34iV3yf6nxzipkKMWWlqlvK9M= -k8s.io/kube-openapi v0.0.0-20220603121420-31174f50af60 h1:cE/M8rmDQgibspuSm+X1iW16ByTImtEaapgaHoVSLX4= -k8s.io/kube-openapi v0.0.0-20220603121420-31174f50af60/go.mod h1:ouUzE1U2mEv//HRoBwYLFE5pdqjIebvtX361vtEIlBI= -k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 h1:yEQKdMCjzAOvGeiTwG4hO/hNVNtDOuUFvMUZ0OlaIzs= -k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8/go.mod h1:mbJ+NSUoAhuR14N0S63bPkh8MGVSo3VYSGZtH/mfMe0= -k8s.io/kubectl v0.24.1 h1:gxcjHrnwntV1c+G/BHWVv4Mtk8CQJ0WTraElLBG+ddk= -k8s.io/kubectl v0.24.1/go.mod h1:NzFqQ50B004fHYWOfhHTrAm4TY6oGF5FAAL13LEaeUI= -k8s.io/kubectl v0.24.3 h1:PqY8ho/S/KuE2/hCC3Iee7X+lOtARYo0LQsNzvV/edE= -k8s.io/kubectl v0.24.3/go.mod h1:PYLcvw96sC1NLbxZEDbdlOEd6/C76VIWjGmWV5QjSk0= -k8s.io/metrics v0.24.1/go.mod h1:vMs5xpcOyY9D+/XVwlaw8oUHYCo6JTGBCZfyXOOkAhE= -k8s.io/metrics v0.24.3/go.mod h1:p1M0lhMySWfhISkSd3HEj8xIgrVnJTK3PPhFq2rA3To= -k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= -k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= -mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= -mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= -mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7/go.mod h1:hBpJkZE8H/sb+VRFvw2+rBpHNsTBcvSpk61hr8mzXZE= -oras.land/oras-go v1.1.1 h1:gI00ftziRivKXaw1BdMeEoIA4uBgga33iVlOsEwefFs= -oras.land/oras-go v1.1.1/go.mod h1:n2TE1ummt9MUyprGhT+Q7kGZUF4kVUpYysPFxeV2IpQ= -oras.land/oras-go v1.2.0 h1:yoKosVIbsPoFMqAIFHTnrmOuafHal+J/r+I5bdbVWu4= -oras.land/oras-go v1.2.0/go.mod h1:pFNs7oHp2dYsYMSS82HaX5l4mpnGO7hbpPN6EWH2ltc= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= -sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= -sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 h1:2sgAQQcY0dEW2SsQwTXhQV4vO6+rSslYx8K3XmM5hqQ= -sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/kind v0.14.0 h1:cNmI3jGBvp7UegEGbC5we8plDtCUmaNRL+bod7JoSCE= -sigs.k8s.io/kind v0.14.0/go.mod h1:UrFRPHG+2a5j0Q7qiR4gtJ4rEyn8TuMQwuOPf+m4oHg= -sigs.k8s.io/kustomize/api v0.11.4/go.mod h1:k+8RsqYbgpkIrJ4p9jcdPqe8DprLxFUUO0yNOq8C+xI= -sigs.k8s.io/kustomize/api v0.11.5 h1:vLDp++YAX7iy2y2CVPJNy9pk9CY8XaUKgHkjbVtnWag= -sigs.k8s.io/kustomize/api v0.11.5/go.mod h1:2UDpxS6AonWXow2ZbySd4AjUxmdXLeTlvGBC46uSiq8= -sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= -sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s= -sigs.k8s.io/kustomize/cmd/config v0.10.6/go.mod h1:/S4A4nUANUa4bZJ/Edt7ZQTyKOY9WCER0uBS1SW2Rco= -sigs.k8s.io/kustomize/kustomize/v4 v4.5.4/go.mod h1:Zo/Xc5FKD6sHl0lilbrieeGeZHVYCA4BzxeAaLI05Bg= -sigs.k8s.io/kustomize/kyaml v0.13.6/go.mod h1:yHP031rn1QX1lr/Xd934Ri/xdVNG8BE2ECa78Ht/kEg= -sigs.k8s.io/kustomize/kyaml v0.13.7 h1:/EZ/nPaLUzeJKF/BuJ4QCuMVJWiEVoI8iftOHY3g3tk= -sigs.k8s.io/kustomize/kyaml v0.13.7/go.mod h1:6K+IUOuir3Y7nucPRAjw9yth04KSWBnP5pqUTGwj/qU= -sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= -sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg= +helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= +k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= +k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8= +k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc= +k8s.io/cli-runtime v0.32.3 h1:khLF2ivU2T6Q77H97atx3REY9tXiA3OLOjWJxUrdvss= +k8s.io/cli-runtime v0.32.3/go.mod h1:vZT6dZq7mZAca53rwUfdFSZjdtLyfF61mkf/8q+Xjak= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= +k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= +k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= +k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/kubectl v0.32.3 h1:VMi584rbboso+yjfv0d8uBHwwxbC438LKq+dXd5tOAI= +k8s.io/kubectl v0.32.3/go.mod h1:6Euv2aso5GKzo/UVMacV6C7miuyevpfI91SvBvV9Zdg= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +oras.land/oras-go v1.2.6 h1:z8cmxQXBU8yZ4mkytWqXfo6tZcamPwjsuxYU81xJ8Lk= +oras.land/oras-go v1.2.6/go.mod h1:OVPc1PegSEe/K8YiLfosrlqlqTN9PUyFvOw5Y9gwrT8= +sigs.k8s.io/cli-utils v0.37.2 h1:GOfKw5RV2HDQZDJlru5KkfLO1tbxqMoyn1IYUxqBpNg= +sigs.k8s.io/cli-utils v0.37.2/go.mod h1:V+IZZr4UoGj7gMJXklWBg6t5xbdThFBcpj4MrZuCYco= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/api-docs/config.json b/hack/api-docs/config.json new file mode 100644 index 000000000..ae9d840f1 --- /dev/null +++ b/hack/api-docs/config.json @@ -0,0 +1,29 @@ +{ + "hideMemberFields": [ + "TypeMeta" + ], + "hideTypePatterns": [ + "ParseError$", + "List$" + ], + "externalPackages": [ + { + "typeMatchPrefix": "^k8s\\.io/apimachinery/pkg/apis/meta/v1\\.Duration$", + "docsURLTemplate": "https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration" + }, + { + "typeMatchPrefix": "^k8s\\.io/apiextensions-apiserver/pkg/apis/apiextensions/v1\\.JSON$", + "docsURLTemplate": "https://pkg.go.dev/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1?tab=doc#JSON" + }, + { + "typeMatchPrefix": "^k8s\\.io/(api|apimachinery/pkg/apis)/", + "docsURLTemplate": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#{{lower .TypeIdentifier}}-{{arrIndex .PackageSegments -1}}-{{arrIndex .PackageSegments -2}}" + } + ], + "typeDisplayNamePrefixOverrides": { + "k8s.io/api/": "Kubernetes ", + "k8s.io/apimachinery/pkg/apis/": "Kubernetes ", + "k8s.io/apiextensions-apiserver/": "Kubernetes " + }, + "markdownDisabled": false +} diff --git a/hack/api-docs/template/members.tpl b/hack/api-docs/template/members.tpl new file mode 100644 index 000000000..26e7251e4 --- /dev/null +++ b/hack/api-docs/template/members.tpl @@ -0,0 +1,46 @@ +{{ define "members" }} + {{ range .Members }} + {{ if not (hiddenMember .)}} + + + {{ fieldName . }}
    + + {{ if linkForType .Type }} + + {{ typeDisplayName .Type }} + + {{ else }} + {{ typeDisplayName .Type }} + {{ end }} + + + + {{ if fieldEmbedded . }} +

    + (Members of {{ fieldName . }} are embedded into this type.) +

    + {{ end}} + + {{ if isOptionalMember .}} + (Optional) + {{ end }} + + {{ safe (renderComments .CommentLines) }} + + {{ if and (eq (.Type.Name.Name) "ObjectMeta") }} + Refer to the Kubernetes API documentation for the fields of the + metadata field. + {{ end }} + + {{ if or (eq (fieldName .) "spec") }} +
    +
    + + {{ template "members" .Type }} +
    + {{ end }} + + + {{ end }} + {{ end }} +{{ end }} diff --git a/hack/api-docs/template/pkg.tpl b/hack/api-docs/template/pkg.tpl new file mode 100644 index 000000000..0a4c0224e --- /dev/null +++ b/hack/api-docs/template/pkg.tpl @@ -0,0 +1,46 @@ +{{ define "packages" }} +

    Kluctl Controller API reference

    + + {{ with .packages}} +

    Packages:

    + + {{ end}} + + {{ range .packages }} +

    + {{- packageDisplayName . -}} +

    + + {{ with (index .GoPackages 0 )}} + {{ with .DocComments }} + {{ safe (renderComments .) }} + {{ end }} + {{ end }} + + Resource Types: + +
      + {{- range (visibleTypes (sortedTypes .Types)) -}} + {{ if isExportedType . -}} +
    • + {{ typeDisplayName . }} +
    • + {{- end }} + {{- end -}} +
    + + {{ range (visibleTypes (sortedTypes .Types))}} + {{ template "type" . }} + {{ end }} + {{ end }} + +
    +

    This page was automatically generated with gen-crd-api-reference-docs

    +
    +{{ end }} diff --git a/hack/api-docs/template/type.tpl b/hack/api-docs/template/type.tpl new file mode 100644 index 000000000..cd2fa6981 --- /dev/null +++ b/hack/api-docs/template/type.tpl @@ -0,0 +1,60 @@ +{{ define "type" }} +

    + {{- .Name.Name }} + {{ if eq .Kind "Alias" }}({{.Underlying}} alias){{ end -}} +

    + + {{ with (typeReferences .) }} +

    + (Appears on: + {{- $prev := "" -}} + {{- range . -}} + {{- if $prev -}}, {{ end -}} + {{ $prev = . }} + {{ typeDisplayName . }} + {{- end -}} + ) +

    + {{ end }} + + {{ with .CommentLines }} + {{ safe (renderComments .) }} + {{ end }} + + {{ if .Members }} +
    +
    + + + + + + + + + {{ if isExportedType . }} + + + + + + + + + {{ end }} + {{ template "members" . }} + +
    FieldDescription
    + apiVersion
    + string
    + {{ apiGroup . }} +
    + kind
    + string +
    + {{ .Name.Name }} +
    +
    +
    + {{ end }} +{{ end }} diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 000000000..65b862271 --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ \ No newline at end of file diff --git a/hack/prepare-release.sh b/hack/prepare-release.sh new file mode 100755 index 000000000..9c9fb6110 --- /dev/null +++ b/hack/prepare-release.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +set -e + +VERSION=$1 + +VERSION_REGEX='v([0-9]*)\.([0-9]*)\.([0-9]*)' +VERSION_REGEX_SED='v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)' + +if [ ! -z "$(git status --porcelain)" ]; then + echo "working directory is dirty!" + exit 1 +fi + +if [ -z "$VERSION" ]; then + echo "No version specified, using 'git sv next-version'" + VERSION=v$(git sv next-version) +fi + +if [[ ! ($VERSION =~ $VERSION_REGEX) ]]; then + echo "version is invalid" + exit 1 +fi + +echo VERSION=$VERSION + +FILES="" +FILES="$FILES install/controller/.kluctl-library.yaml" +FILES="$FILES install/controller/controller/kustomization.yaml" +FILES="$FILES install/webui/.kluctl-library.yaml" +FILES="$FILES install/webui/webui/deployment.yaml" +FILES="$FILES install/webui/webui/kustomization.yaml" +FILES="$FILES docs/kluctl/installation.md" +FILES="$FILES docs/gitops/installation.md" +FILES="$FILES docs/webui/installation.md" +FILES="$FILES docs/webui/oidc-azure-ad.md" + +for f in $FILES; do + cat $f | sed "s/$VERSION_REGEX_SED/$VERSION/g" > $f.tmp + mv $f.tmp $f +done + +(cd internal && go run ./generate-install) +FILES="$FILES install/controller/files.json" + +for f in $FILES; do + git add $f +done + +if [ -z "$(git status --porcelain)" ]; then + echo "nothing has changed!" + exit 1 +fi + +echo "committing" +git commit -o -m "build: Preparing release $VERSION" -- $FILES + +echo "tagging" +git tag -f $VERSION diff --git a/hack/zerotier-create-network.sh b/hack/zerotier-create-network.sh deleted file mode 100755 index 000e77af9..000000000 --- a/hack/zerotier-create-network.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -set -e - -NETWORK_NAME=$1 - -NETWORK=$(curl -H"Authorization: bearer $CI_ZEROTIER_API_KEY" -XPOST -d"{}" https://my.zerotier.com/api/v1/network) -NETWORK_ID=$(echo $NETWORK | jq '.config.id' -r) - -echo "Configuring network" -cat << EOF | curl -H"Authorization: bearer $CI_ZEROTIER_API_KEY" -XPOST -d@- https://my.zerotier.com/api/v1/network/$NETWORK_ID -{ - "config": { - "private": false, - "name": "$NETWORK_NAME", - "ipAssignmentPools": [ - { - "ipRangeStart":"10.147.17.1", - "ipRangeEnd":"10.147.17.254" - } - ], - "routes": [ - { - "target":"10.147.17.0/24" - } - ] - } -} -EOF diff --git a/hack/zerotier-delete-network.sh b/hack/zerotier-delete-network.sh deleted file mode 100755 index a8c22a7f7..000000000 --- a/hack/zerotier-delete-network.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -e - -NETWORK_NAME=$1 - -echo "Searching network " -NETWORKS=$(curl -H"Authorization: bearer $CI_ZEROTIER_API_KEY" https://my.zerotier.com/api/v1/network) -NETWORK_ID=$(echo $NETWORKS | jq ".[] | select(.config.name == \"$NETWORK_NAME\") | .config.id" -r) - -if [ "$NETWORK_ID" = "" ]; then - exit 0 -fi - -echo "Deleting network" -curl -H"Authorization: bearer $CI_ZEROTIER_API_KEY" -XDELETE https://my.zerotier.com/api/v1/network/$NETWORK_ID diff --git a/hack/zerotier-join-network.sh b/hack/zerotier-join-network.sh deleted file mode 100755 index 3f2094e41..000000000 --- a/hack/zerotier-join-network.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -set -e - -NETWORK_NAME=$1 -MEMBER_NAME=$2 - -while true; do - echo "Searching network" - NETWORKS=$(curl -H"Authorization: bearer $CI_ZEROTIER_API_KEY" https://my.zerotier.com/api/v1/network) - NETWORK_ID=$(echo $NETWORKS | jq ".[] | select(.config.name == \"$NETWORK_NAME\") | .config.id" -r) - if [ "$NETWORK_ID" != "" ]; then - break - fi - sleep 5 -done - -SUDO= -if which sudo &> /dev/null; then - SUDO=sudo -fi - -echo "Joining network" -$SUDO zerotier-cli join $NETWORK_ID -MEMBER_ID=$($SUDO zerotier-cli status | awk '{print $3}') - -echo "Renaming member $MEMBER_ID" -curl -H"Authorization: bearer $CI_ZEROTIER_API_KEY" -XPOST -d"{\"name\": \"$MEMBER_NAME\"}" https://my.zerotier.com/api/v1/network/$NETWORK_ID/member/$MEMBER_ID diff --git a/hack/zerotier-setup-docker-host.sh b/hack/zerotier-setup-docker-host.sh deleted file mode 100755 index 4c06dbfc1..000000000 --- a/hack/zerotier-setup-docker-host.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -set -e - -NETWORK_NAME=$1 - -echo "Searching network " -NETWORKS=$(curl -H"Authorization: bearer $CI_ZEROTIER_API_KEY" https://my.zerotier.com/api/v1/network) -NETWORK_ID=$(echo $NETWORKS | jq ".[] | select(.config.name == \"$NETWORK_NAME\") | .config.id" -r) - -echo "Waiting for IP to be assigned to docker member" -while true; do - MEMBERS=$(curl -H"Authorization: bearer $CI_ZEROTIER_API_KEY" https://my.zerotier.com/api/v1/network/$NETWORK_ID/member) - IP=$(echo $MEMBERS | jq ".[] | select(.name == \"docker\") | .config.ipAssignments[0]" -r) - if [ "$IP" != "null" ]; then - break - fi - sleep 5 - echo "Still waiting..." -done - -echo "DOCKER_IP=$IP" >> $GITHUB_ENV -echo "DOCKER_HOST=tcp://$IP:2375" >> $GITHUB_ENV - -echo "Waiting for docker to become available" -while ! nc -z $IP 2375; do - sleep 5 - echo "Still waiting..." -done diff --git a/install/README.md b/install/README.md index a02fc9390..5d76cfbd9 100644 --- a/install/README.md +++ b/install/README.md @@ -1,41 +1,3 @@ # kluctl Installation -Binaries for macOS and Linux AMD64 are available for download on the -[release page](https://github.com/kluctl/kluctl/releases). - -To install the latest release run: - -```bash -curl -s https://raw.githubusercontent.com/kluctl/kluctl/main/install/kluctl.sh | bash -``` - -The install script does the following: -* attempts to detect your OS -* downloads and unpacks the release tar file in a temporary directory -* copies the kluctl binary to `/usr/local/bin` -* removes the temporary directory - -## Alternative installation methods - -See https://kluctl.io/docs/installation for alternative installation methods. - -## Build from source - -Clone the repository: - -```bash -git clone https://github.com/kluctl/kluctl -cd kluctl -``` - -Build the `kluctl` binary (requires go >= 1.18 and python >= 3.10): - -```bash -make build -``` - -Run the binary: - -```bash -./bin/kluctl -h -``` +Read [installation](../docs/kluctl/installation.md) for instructions. diff --git a/install/controller/.kluctl-library.yaml b/install/controller/.kluctl-library.yaml new file mode 100644 index 000000000..bf0becc12 --- /dev/null +++ b/install/controller/.kluctl-library.yaml @@ -0,0 +1,19 @@ +args: + - name: kluctl_image + default: ghcr.io/kluctl/kluctl + - name: kluctl_version + default: v2.26.0 + - name: controller_args + default: [] + - name: controller_envs + default: [] + - name: controller_resources + default: {} + - name: controller_node_selectors + default: {} + - name: controller_tolerations + default: [] + - name: controller_priority_class_name + default: {} + - name: controller_service_account_annotations + default: {} diff --git a/install/controller/controller/crd.yaml b/install/controller/controller/crd.yaml new file mode 100644 index 000000000..d302ab18e --- /dev/null +++ b/install/controller/controller/crd.yaml @@ -0,0 +1,914 @@ +# Warning, this file is generated via "make manifests", don't edit it directly but instead change the files in config/crd +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: kluctldeployments.gitops.kluctl.io +spec: + group: gitops.kluctl.io + names: + kind: KluctlDeployment + listKind: KluctlDeploymentList + plural: kluctldeployments + singular: kluctldeployment + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.suspend + name: Suspend + type: boolean + - jsonPath: .spec.dryRun + name: DryRun + type: boolean + - jsonPath: .status.lastDeployResult.commandInfo.endTime + name: Deployed + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.lastDriftDetectionResultMessage + name: Drift + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: KluctlDeployment is the Schema for the kluctldeployments 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: + properties: + abortOnError: + default: false + description: |- + ForceReplaceOnError instructs kluctl to abort deployments immediately when something fails. + Equivalent to using '--abort-on-error' when calling kluctl. + type: boolean + args: + description: Args specifies dynamic target args. + type: object + x-kubernetes-preserve-unknown-fields: true + context: + description: |- + If specified, overrides the context to be used. This will effectively make kluctl ignore the context specified + in the target. + type: string + credentials: + description: Credentials specifies the credentials used when pulling + sources + properties: + git: + description: Git specifies a list of git credentials + items: + properties: + host: + description: |- + Host specifies the hostname that this secret applies to. If set to '*', this set of credentials + applies to all hosts. + Using '*' for http(s) based repositories is not supported, meaning that such credentials sets will be ignored. + You must always set a proper hostname in that case. + type: string + path: + description: |- + Path specifies the path to be used to filter Git repositories. The path can contain wildcards. These credentials + will only be used for matching Git URLs. If omitted, all repositories are considered to match. + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + the git repository. + For HTTPS git repositories the Secret must contain 'username' and 'password' + fields. + For SSH git repositories the Secret must contain 'identity' + and 'known_hosts' fields. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - secretRef + type: object + type: array + helm: + description: Helm specifies a list of Helm credentials + items: + properties: + host: + description: Host specifies the hostname that this secret + applies to. + type: string + path: + description: |- + Path specifies the path to be used to filter Helm urls. The path can contain wildcards. These credentials + will only be used for matching URLs. If omitted, all URLs are considered to match. + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + the Helm repository. + The secret can either container basic authentication credentials via `username` and `password` or + TLS authentication via `certFile` and `keyFile`. `caFile` can be specified to override the CA to use while + contacting the repository. + The secret can also contain `insecureSkipTlsVerify: "true"`, which will disable TLS verification. + `passCredentialsAll: "true"` can be specified to make the controller pass credentials to all requests, even if + the hostname changes in-between. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - host + - secretRef + type: object + type: array + oci: + description: Oci specifies a list of OCI credentials + items: + properties: + registry: + description: Registry specifies the hostname that this secret + applies to. + type: string + repository: + description: |- + Repository specifies the org and repo name in the format 'org-name/repo-name'. + Both 'org-name' and 'repo-name' can be specified as '*', meaning that all names are matched. + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + the oci repository. + The secret must contain 'username' and 'password'. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - secretRef + type: object + type: array + type: object + decryption: + description: Decrypt Kubernetes secrets before applying them on the + cluster. + properties: + provider: + description: Provider is the name of the decryption engine. + enum: + - sops + type: string + secretRef: + description: The secret name containing the private OpenPGP keys + used for decryption. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + serviceAccount: + description: |- + ServiceAccount specifies the service account used to authenticate against cloud providers. + This is currently only usable for AWS KMS keys. The specified service account will be used to authenticate to AWS + by signing a token in an IRSA compliant way. + type: string + required: + - provider + type: object + delete: + default: false + description: Delete enables deletion of the specified target when + the KluctlDeployment object gets deleted. + type: boolean + deployInterval: + description: |- + DeployInterval specifies the interval at which to deploy the KluctlDeployment, even in cases the rendered + result does not change. + pattern: ^(([0-9]+(\.[0-9]+)?(ms|s|m|h))+) + type: string + deployMode: + default: full-deploy + description: |- + DeployMode specifies what deploy mode should be used. + The options 'full-deploy' and 'poke-images' are supported. + With the 'poke-images' option, only images are patched into the target without performing a full deployment. + enum: + - full-deploy + - poke-images + type: string + dryRun: + default: false + description: |- + DryRun instructs kluctl to run everything in dry-run mode. + Equivalent to using '--dry-run' when calling kluctl. + type: boolean + excludeDeploymentDirs: + description: |- + ExcludeDeploymentDirs instructs kluctl to exclude deployments with the given dir. + Equivalent to using '--exclude-deployment-dir' when calling kluctl. + items: + type: string + type: array + excludeTags: + description: |- + ExcludeTags instructs kluctl to exclude deployments with given tags. + Equivalent to using '--exclude-tag' when calling kluctl. + items: + type: string + type: array + forceApply: + default: false + description: |- + ForceApply instructs kluctl to force-apply in case of SSA conflicts. + Equivalent to using '--force-apply' when calling kluctl. + type: boolean + forceReplaceOnError: + default: false + description: |- + ForceReplaceOnError instructs kluctl to force-replace resources in case a normal replace fails. + Equivalent to using '--force-replace-on-error' when calling kluctl. + type: boolean + helmCredentials: + description: |- + HelmCredentials is a list of Helm credentials used when non pre-pulled Helm Charts are used inside a + Kluctl deployment. + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.helm instead. + items: + properties: + secretRef: + description: |- + SecretRef holds the name of a secret that contains the Helm credentials. + The secret must either contain the fields `credentialsId` which refers to the credentialsId + found in https://kluctl.io/docs/kluctl/reference/deployments/helm/#private-repositories or an `url` used + to match the credentials found in Kluctl projects helm-chart.yaml files. + The secret can either container basic authentication credentials via `username` and `password` or + TLS authentication via `certFile` and `keyFile`. `caFile` can be specified to override the CA to use while + contacting the repository. + The secret can also contain `insecureSkipTlsVerify: "true"`, which will disable TLS verification. + `passCredentialsAll: "true"` can be specified to make the controller pass credentials to all requests, even if + the hostname changes in-between. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + type: object + type: array + images: + description: |- + Images contains a list of fixed image overrides. + Equivalent to using '--fixed-images-file' when calling kluctl. + items: + properties: + container: + type: string + deployTags: + items: + type: string + type: array + deployedImage: + type: string + deployment: + type: string + deploymentDir: + type: string + image: + type: string + imageRegex: + type: string + namespace: + type: string + object: + properties: + group: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + version: + type: string + required: + - kind + - name + type: object + resultImage: + type: string + required: + - resultImage + type: object + type: array + includeDeploymentDirs: + description: |- + IncludeDeploymentDirs instructs kluctl to only include deployments with the given dir. + Equivalent to using '--include-deployment-dir' when calling kluctl. + items: + type: string + type: array + includeTags: + description: |- + IncludeTags instructs kluctl to only include deployments with given tags. + Equivalent to using '--include-tag' when calling kluctl. + items: + type: string + type: array + interval: + description: |- + The interval at which to reconcile the KluctlDeployment. + Reconciliation means that the deployment is fully rendered and only deployed when the result changes compared + to the last deployment. + To override this behavior, set the DeployInterval value. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + kubeConfig: + description: |- + The KubeConfig for deploying to the target cluster. + Specifies the kubeconfig to be used when invoking kluctl. Contexts in this kubeconfig must match + the context found in the kluctl target. As an alternative, specify the context to be used via 'context' + properties: + secretRef: + description: |- + SecretRef holds the 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'. The secret must be in the same namespace as + the Kustomization. + 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 + the KluctlDeployment. + 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 + manual: + description: |- + Manual enables manual deployments, meaning that the deployment will initially start as a dry run deployment + and only after manual approval cause a real deployment + type: boolean + manualObjectsHash: + description: |- + ManualObjectsHash specifies the rendered objects hash that is approved for manual deployment. + If Manual is set to true, the controller will skip deployments when the current reconciliation loops calculated + objects hash does not match this value. + There are two ways to use this value properly. + 1. Set it manually to the value found in status.lastObjectsHash. + 2. Use the Kluctl Webui to manually approve a deployment, which will set this field appropriately. + type: string + noWait: + default: false + description: |- + NoWait instructs kluctl to not wait for any resources to become ready, including hooks. + Equivalent to using '--no-wait' when calling kluctl. + type: boolean + prune: + default: false + description: Prune enables pruning after deploying. + type: boolean + replaceOnError: + default: false + description: |- + ReplaceOnError instructs kluctl to replace resources on error. + Equivalent to using '--replace-on-error' when calling kluctl. + type: boolean + retryInterval: + description: |- + The interval at which to retry a previously failed reconciliation. + When not specified, the controller uses the Interval + value to retry failures. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + serviceAccountName: + description: |- + The name of the Kubernetes service account to use while deploying. + If not specified, the default service account is used. + type: string + source: + description: Specifies the project source location + properties: + credentials: + description: |- + Credentials specifies a list of secrets with credentials + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.git instead. + items: + properties: + host: + description: |- + Host specifies the hostname that this secret applies to. If set to '*', this set of credentials + applies to all hosts. + Using '*' for http(s) based repositories is not supported, meaning that such credentials sets will be ignored. + You must always set a proper hostname in that case. + type: string + pathPrefix: + description: |- + PathPrefix specifies the path prefix to be used to filter source urls. Only urls that have this prefix will use + this set of credentials. + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + the git repository. + For HTTPS git repositories the Secret must contain 'username' and 'password' + fields. + For SSH git repositories the Secret must contain 'identity' + and 'known_hosts' fields. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - secretRef + type: object + type: array + git: + description: Git specifies a git repository as project source + properties: + path: + description: Path specifies the sub-directory to be used as + project directory + type: string + ref: + description: Ref specifies the branch, tag or commit that + should be used. If omitted, the default branch of the repo + is used. + properties: + branch: + description: Branch to use. + type: string + commit: + description: Commit SHA to use. + type: string + tag: + description: Tag to use. + type: string + type: object + url: + description: |- + URL specifies the Git url where the project source is located. If the given Git repository needs authentication, + use spec.credentials.git to specify those. + type: string + required: + - url + type: object + oci: + description: Oci specifies an OCI repository as project source + properties: + path: + description: Path specifies the sub-directory to be used as + project directory + type: string + ref: + description: Ref specifies the tag to be used. If omitted, + the "latest" tag is used. + properties: + digest: + description: |- + Digest is the image digest to pull, takes precedence over SemVer. + The value should be in the format 'sha256:'. + type: string + tag: + description: Tag is the image tag to pull, defaults to + latest. + type: string + type: object + url: + description: |- + Url specifies the Git url where the project source is located. If the given OCI repository needs authentication, + use spec.credentials.oci to specify those. + type: string + required: + - url + type: object + path: + description: |- + Path specifies the sub-directory to be used as project directory + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.path instead. + type: string + ref: + description: |- + Ref specifies the branch, tag or commit that should be used. If omitted, the default branch of the repo is used. + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.ref instead. + properties: + branch: + description: Branch to use. + type: string + commit: + description: Commit SHA to use. + type: string + tag: + description: Tag to use. + type: string + type: object + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + See ProjectSourceCredentials.SecretRef for details + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.credentials.git + instead. + WARNING using this field causes the controller to pass http basic auth credentials to ALL repositories involved. + Use spec.credentials.git with a proper Host field instead. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + url: + description: |- + Url specifies the Git url where the project source is located + DEPRECATED this field is deprecated and will be removed in the next API version bump. Use spec.git.url instead. + type: string + type: object + sourceOverrides: + description: Specifies source overrides + items: + properties: + isGroup: + type: boolean + repoKey: + type: string + url: + type: string + required: + - repoKey + - url + type: object + type: array + suspend: + description: |- + This flag tells the controller to suspend subsequent kluctl executions, + it does not apply to already started executions. Defaults to false. + type: boolean + target: + description: |- + Target specifies the kluctl target to deploy. If not specified, an empty target is used that has no name and no + context. Use 'TargetName' and 'Context' to specify the name and context in that case. + maxLength: 63 + minLength: 1 + type: string + targetNameOverride: + description: TargetNameOverride sets or overrides the target name. + This is especially useful when deployment without a target. + maxLength: 63 + minLength: 1 + type: string + timeout: + description: |- + Timeout for all operations. + Defaults to 'Interval' duration. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + validate: + default: true + description: Validate enables validation after deploying + type: boolean + validateInterval: + description: |- + ValidateInterval specifies the interval at which to validate the KluctlDeployment. + Validation is performed the same way as with 'kluctl validate -t '. + Defaults to the same value as specified in Interval. + Validate is also performed whenever a deployment is performed, independent of the value of ValidateInterval + pattern: ^(([0-9]+(\.[0-9]+)?(ms|s|m|h))+) + type: string + required: + - interval + - source + type: object + status: + description: KluctlDeploymentStatus defines the observed state of KluctlDeployment + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + 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 + deployRequestResult: + properties: + commandError: + type: string + endTime: + format: date-time + type: string + reconcileId: + type: string + request: + description: ManualRequest is used in json form inside the manual + request annotations + properties: + overridesPatch: + type: object + x-kubernetes-preserve-unknown-fields: true + requestValue: + type: string + required: + - requestValue + type: object + resultId: + type: string + startTime: + format: date-time + type: string + required: + - reconcileId + - request + - startTime + type: object + diffRequestResult: + properties: + commandError: + type: string + endTime: + format: date-time + type: string + reconcileId: + type: string + request: + description: ManualRequest is used in json form inside the manual + request annotations + properties: + overridesPatch: + type: object + x-kubernetes-preserve-unknown-fields: true + requestValue: + type: string + required: + - requestValue + type: object + resultId: + type: string + startTime: + format: date-time + type: string + required: + - reconcileId + - request + - startTime + type: object + lastDeployResult: + description: LastDeployResult is the result summary of the last deploy + command + type: object + x-kubernetes-preserve-unknown-fields: true + lastDiffResult: + description: LastDiffResult is the result summary of the last diff + command + type: object + x-kubernetes-preserve-unknown-fields: true + lastDriftDetectionResult: + description: |- + LastDriftDetectionResult is the result of the last drift detection command + optional + type: object + x-kubernetes-preserve-unknown-fields: true + lastDriftDetectionResultMessage: + description: |- + LastDriftDetectionResultMessage contains a short message that describes the drift + optional + type: string + lastManualObjectsHash: + type: string + lastObjectsHash: + type: string + lastPrepareError: + type: string + lastValidateResult: + description: LastValidateResult is the result summary of the last + validate command + type: object + x-kubernetes-preserve-unknown-fields: true + observedCommit: + description: ObservedCommit is the last commit observed + type: string + observedGeneration: + description: ObservedGeneration is the last reconciled generation. + format: int64 + type: integer + projectKey: + properties: + repoKey: + type: string + subDir: + type: string + type: object + pruneRequestResult: + properties: + commandError: + type: string + endTime: + format: date-time + type: string + reconcileId: + type: string + request: + description: ManualRequest is used in json form inside the manual + request annotations + properties: + overridesPatch: + type: object + x-kubernetes-preserve-unknown-fields: true + requestValue: + type: string + required: + - requestValue + type: object + resultId: + type: string + startTime: + format: date-time + type: string + required: + - reconcileId + - request + - startTime + type: object + reconcileRequestResult: + properties: + commandError: + type: string + endTime: + format: date-time + type: string + reconcileId: + type: string + request: + description: ManualRequest is used in json form inside the manual + request annotations + properties: + overridesPatch: + type: object + x-kubernetes-preserve-unknown-fields: true + requestValue: + type: string + required: + - requestValue + type: object + resultId: + type: string + startTime: + format: date-time + type: string + required: + - reconcileId + - request + - startTime + type: object + targetKey: + properties: + clusterId: + type: string + discriminator: + type: string + targetName: + type: string + required: + - clusterId + type: object + validateRequestResult: + properties: + commandError: + type: string + endTime: + format: date-time + type: string + reconcileId: + type: string + request: + description: ManualRequest is used in json form inside the manual + request annotations + properties: + overridesPatch: + type: object + x-kubernetes-preserve-unknown-fields: true + requestValue: + type: string + required: + - requestValue + type: object + resultId: + type: string + startTime: + format: date-time + type: string + required: + - reconcileId + - request + - startTime + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/install/controller/controller/kustomization.yaml b/install/controller/controller/kustomization.yaml new file mode 100644 index 000000000..e2fae6614 --- /dev/null +++ b/install/controller/controller/kustomization.yaml @@ -0,0 +1,64 @@ +{% set kluctl_image = get_var("args.kluctl_image", "ghcr.io/kluctl/kluctl") %} +# TODO remove controller_version +{% set kluctl_version = get_var(["args.kluctl_version", "args.controller_version"], "v2.26.0") %} +{% set pull_policy = "Always" if ("-devel" in kluctl_version or "-snapshot" in kluctl_version) else "IfNotPresent" %} + +resources: + - crd.yaml + - manager.yaml + - rbac.yaml + +patches: + - target: + kind: Deployment + name: kluctl-controller + patch: |- + - op: add + path: /spec/template/spec/containers/0/image + value: {{ kluctl_image }}:{{ kluctl_version }} + - target: + kind: Deployment + name: kluctl-controller + patch: |- + - op: add + path: /spec/template/spec/containers/0/imagePullPolicy + value: {{ pull_policy }} +{% for a in get_var("args.controller_args", []) %} + - op: add + path: /spec/template/spec/containers/0/args/- + value: "{{ a }}" +{% endfor %} +{% for a in get_var("args.controller_envs", []) %} + - op: add + path: /spec/template/spec/containers/0/env/- + value: {{ a | to_json }} +{% endfor %} +{% if get_var("args.controller_resources", none) %} + - op: replace + path: /spec/template/spec/containers/0/resources + value: {{ get_var("args.controller_resources", none) | to_json }} +{% endif %} +{% if get_var("args.controller_node_selectors", none) %} + - op: add + path: /spec/template/spec/nodeSelector + value: {{ get_var("args.controller_node_selectors", none) | to_json }} +{% endif %} +{% if get_var("args.controller_tolerations", none) %} + - op: add + path: /spec/template/spec/tolerations + value: {{ get_var("args.controller_tolerations", none) | to_json }} +{% endif %} +{% if get_var("args.controller_priority_class_name", none) %} + - op: add + path: /spec/template/spec/priorityClassName + value: {{ get_var("args.controller_priority_class_name", none) | to_json }} +{% endif %} +{% if get_var("args.controller_service_account_annotations", none) %} + - target: + kind: ServiceAccount + name: kluctl-controller + patch: |- + - op: add + path: /metadata/annotations + value: {{ get_var("args.controller_service_account_annotations", none) | to_json }} +{% endif %} diff --git a/install/controller/controller/manager.yaml b/install/controller/controller/manager.yaml new file mode 100644 index 000000000..7e9efe944 --- /dev/null +++ b/install/controller/controller/manager.yaml @@ -0,0 +1,85 @@ +# Warning, this file is generated via "make manifests", don't edit it directly but instead change the files in config/manager +apiVersion: v1 +kind: Namespace +metadata: + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: controller + app.kubernetes.io/instance: system + app.kubernetes.io/managed-by: kluctl + app.kubernetes.io/name: namespace + app.kubernetes.io/part-of: controller + control-plane: controller-manager + name: kluctl-system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: controller + app.kubernetes.io/instance: kluctl-controller + app.kubernetes.io/managed-by: kluctl + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: controller + control-plane: kluctl-controller + name: kluctl-controller + namespace: kluctl-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: kluctl-controller + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: kluctl-controller + spec: + containers: + - args: + - --leader-elect + command: + - kluctl + - controller + - run + env: [] + image: ghcr.io/kluctl/kluctl:latest + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + name: controller + ports: + - containerPort: 8080 + name: metrics + - containerPort: 8082 + name: source-override + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 500m + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: kluctl-controller + terminationGracePeriodSeconds: 10 diff --git a/install/controller/controller/rbac.yaml b/install/controller/controller/rbac.yaml new file mode 100644 index 000000000..0d9a05d94 --- /dev/null +++ b/install/controller/controller/rbac.yaml @@ -0,0 +1,165 @@ +# Warning, this file is generated via "make manifests", don't edit it directly but instead change the files in config/rbac +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/instance: kluctl-controller-sa + app.kubernetes.io/managed-by: kluctl + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/part-of: controller + name: kluctl-controller + namespace: kluctl-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/managed-by: kluctl + app.kubernetes.io/name: role + app.kubernetes.io/part-of: controller + name: kluctl-controller-leader-election-role + namespace: kluctl-system +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kluctl-controller-role +rules: +- apiGroups: + - "" + resources: + - configmaps + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/managed-by: kluctl + app.kubernetes.io/name: rolebinding + app.kubernetes.io/part-of: controller + name: kluctl-controller-leader-election-rolebinding + namespace: kluctl-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kluctl-controller-leader-election-role +subjects: +- kind: ServiceAccount + name: kluctl-controller + namespace: kluctl-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kluctl-controller-cluster-admin +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: kluctl-controller + namespace: kluctl-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/instance: kluctl-controller-rolebinding + app.kubernetes.io/managed-by: kluctl + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: controller + name: kluctl-controller-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kluctl-controller-role +subjects: +- kind: ServiceAccount + name: kluctl-controller + namespace: kluctl-system diff --git a/install/controller/deployment.yaml b/install/controller/deployment.yaml new file mode 100644 index 000000000..1ab7a6d3e --- /dev/null +++ b/install/controller/deployment.yaml @@ -0,0 +1,3 @@ + +deployments: + - path: controller diff --git a/install/controller/embed.go b/install/controller/embed.go new file mode 100644 index 000000000..dc5e78645 --- /dev/null +++ b/install/controller/embed.go @@ -0,0 +1,6 @@ +package controller + +import "embed" + +//go:embed all:* +var Project embed.FS diff --git a/install/controller/files.json b/install/controller/files.json new file mode 100644 index 000000000..18375ef86 --- /dev/null +++ b/install/controller/files.json @@ -0,0 +1,45 @@ +{ + "contentHash": "d25aab2e585a7cb9b3b36e439b6aa1edf73e816c1b87d0461c703f4efb2ebffa", + "files": [ + { + "name": ".kluctl-library.yaml", + "size": 464, + "perm": 420 + }, + { + "name": "controller", + "size": 0, + "perm": 2147484141 + }, + { + "name": "controller/crd.yaml", + "size": 42735, + "perm": 420 + }, + { + "name": "controller/kustomization.yaml", + "size": 2278, + "perm": 420 + }, + { + "name": "controller/manager.yaml", + "size": 2303, + "perm": 420 + }, + { + "name": "controller/rbac.yaml", + "size": 3452, + "perm": 420 + }, + { + "name": "deployment.yaml", + "size": 35, + "perm": 420 + }, + { + "name": "embed.go", + "size": 74, + "perm": 420 + } + ] +} \ No newline at end of file diff --git a/install/webui/.kluctl-library.yaml b/install/webui/.kluctl-library.yaml new file mode 100644 index 000000000..fc4c8fd94 --- /dev/null +++ b/install/webui/.kluctl-library.yaml @@ -0,0 +1,17 @@ +args: + - name: kluctl_image + default: ghcr.io/kluctl/kluctl + - name: kluctl_version + default: v2.26.0 + - name: webui_args + default: [] + - name: webui_envs + default: [] + - name: webui_resources + default: {} + - name: webui_node_selectors + default: {} + - name: webui_tolerations + default: [] + - name: webui_priority_class_name + default: {} diff --git a/install/webui/deployment.yaml b/install/webui/deployment.yaml new file mode 100644 index 000000000..cf765cdb9 --- /dev/null +++ b/install/webui/deployment.yaml @@ -0,0 +1,3 @@ + +deployments: + - path: webui diff --git a/install/webui/webui/admin-rbac.yaml b/install/webui/webui/admin-rbac.yaml new file mode 100644 index 000000000..5215ec007 --- /dev/null +++ b/install/webui/webui/admin-rbac.yaml @@ -0,0 +1,41 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kluctl-webui-admin-role +rules: + - apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + # Read access for all other Kubernetes objects + - apiGroups: ["*"] + resources: ["*"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/instance: kluctl-webui-rolebinding + app.kubernetes.io/managed-by: kluctl + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: controller + name: kluctl-webui-admin-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kluctl-webui-admin-role +subjects: + - kind: User + apiGroup: rbac.authorization.k8s.io + name: kluctl-webui-admin diff --git a/install/webui/webui/deployment.yaml b/install/webui/webui/deployment.yaml new file mode 100644 index 000000000..278e52786 --- /dev/null +++ b/install/webui/webui/deployment.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: deployment + app.kubernetes.io/instance: kluctl-webui + app.kubernetes.io/name: kluctl-webui + app.kubernetes.io/managed-by: kluctl + control-plane: kluctl-webui + name: kluctl-webui + namespace: kluctl-system +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: deployment + app.kubernetes.io/instance: kluctl-webui + app.kubernetes.io/name: kluctl-webui + template: + metadata: + labels: + app.kubernetes.io/component: deployment + app.kubernetes.io/instance: kluctl-webui + app.kubernetes.io/name: kluctl-webui + spec: + containers: + - name: webui + image: ghcr.io/kluctl/kluctl:latest + imagePullPolicy: IfNotPresent + command: + - kluctl + - webui + - run + - --in-cluster + args: [] + env: [] + ports: + - containerPort: 8080 + name: http + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readyz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 500m + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: kluctl-webui + terminationGracePeriodSeconds: 10 diff --git a/install/webui/webui/kustomization.yaml b/install/webui/webui/kustomization.yaml new file mode 100644 index 000000000..6f3c4537b --- /dev/null +++ b/install/webui/webui/kustomization.yaml @@ -0,0 +1,56 @@ +{% set kluctl_image = get_var("args.kluctl_image", "ghcr.io/kluctl/kluctl") %} +{% set kluctl_version = get_var("args.kluctl_version", "v2.26.0") %} +{% set pull_policy = "Always" if ("-devel" in kluctl_version or "-snapshot" in kluctl_version) else "IfNotPresent" %} + +resources: +- admin-rbac.yaml +- webui-rbac.yaml +- viewer-rbac.yaml +- deployment.yaml +- service.yaml + +patches: + - target: + kind: Deployment + name: kluctl-webui + patch: |- + - op: add + path: /spec/template/spec/containers/0/image + value: {{ kluctl_image }}:{{ kluctl_version }} + - target: + kind: Deployment + name: kluctl-webui + patch: |- + - op: add + path: /spec/template/spec/containers/0/imagePullPolicy + value: {{ pull_policy }} +{% for a in get_var("args.webui_args", []) %} + - op: add + path: /spec/template/spec/containers/0/args/- + value: "{{ a }}" +{% endfor %} +{% for a in get_var("args.webui_envs", []) %} + - op: add + path: /spec/template/spec/containers/0/env/- + value: {{ a | to_json }} +{% endfor %} +{% if get_var("args.webui_resources", none) %} + - op: replace + path: /spec/template/spec/containers/0/resources + value: {{ get_var("args.webui_resources", none) | to_json }} +{% endif %} +{% if get_var("args.webui_node_selectors", none) %} + - op: add + path: /spec/template/spec/nodeSelector + value: {{ get_var("args.webui_node_selectors", none) | to_json }} +{% endif %} +{% if get_var("args.webui_tolerations", none) %} + - op: add + path: /spec/template/spec/tolerations + value: {{ get_var("args.webui_tolerations", none) | to_json }} +{% endif %} +{% if get_var("args.webui_priority_class_name", none) %} + - op: add + path: /spec/template/spec/priorityClassName + value: {{ get_var("args.webui_priority_class_name", none) | to_json }} +{% endif %} diff --git a/install/webui/webui/service.yaml b/install/webui/webui/service.yaml new file mode 100644 index 000000000..4c28ef2c4 --- /dev/null +++ b/install/webui/webui/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: kluctl-webui + namespace: kluctl-system +spec: + ports: + - port: 8080 + targetPort: 8080 + selector: + app.kubernetes.io/component: deployment + app.kubernetes.io/instance: kluctl-webui + app.kubernetes.io/name: kluctl-webui diff --git a/install/webui/webui/viewer-rbac.yaml b/install/webui/webui/viewer-rbac.yaml new file mode 100644 index 000000000..d08ffe20b --- /dev/null +++ b/install/webui/webui/viewer-rbac.yaml @@ -0,0 +1,37 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kluctl-webui-viewer-role +rules: + - apiGroups: + - gitops.kluctl.io + resources: + - kluctldeployments + verbs: + - get + - list + - watch + # Read access for all other Kubernetes objects + - apiGroups: ["*"] + resources: ["*"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/instance: kluctl-webui-rolebinding + app.kubernetes.io/managed-by: kluctl + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: controller + name: kluctl-webui-viewer-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kluctl-webui-viewer-role +subjects: + - kind: User + apiGroup: rbac.authorization.k8s.io + name: kluctl-webui-viewer diff --git a/install/webui/webui/webui-rbac.yaml b/install/webui/webui/webui-rbac.yaml new file mode 100644 index 000000000..46ed425a2 --- /dev/null +++ b/install/webui/webui/webui-rbac.yaml @@ -0,0 +1,86 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/instance: kluctl-webui-sa + app.kubernetes.io/managed-by: kluctl + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/part-of: controller + name: kluctl-webui + namespace: kluctl-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kluctl-webui-cluster-role +rules: + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list"] + # allow access to results + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch"] + - apiGroups: ["gitops.kluctl.io"] + resources: ["kluctldeployments"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["pods", "pods/log"] + verbs: ["get", "list", "watch"] + # allow to impersonate other users, groups and serviceaccounts + - apiGroups: [""] + resources: ["users", "groups", "serviceaccounts"] + verbs: ["impersonate"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kluctl-webui-role + namespace: kluctl-system +rules: + # allow to read/write credentials + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch", "create", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/instance: kluctl-webui-rolebinding + app.kubernetes.io/managed-by: kluctl + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: controller + name: kluctl-webui-cluster-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kluctl-webui-cluster-role +subjects: + - kind: ServiceAccount + name: kluctl-webui + namespace: kluctl-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/instance: kluctl-webui-rolebinding + app.kubernetes.io/managed-by: kluctl + app.kubernetes.io/name: rolebinding + app.kubernetes.io/part-of: controller + name: kluctl-webui-rolebinding + namespace: kluctl-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kluctl-webui-role +subjects: + - kind: ServiceAccount + name: kluctl-webui + namespace: kluctl-system diff --git a/internal/generate-install/main.go b/internal/generate-install/main.go new file mode 100644 index 000000000..a969d77d9 --- /dev/null +++ b/internal/generate-install/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/kluctl/go-embed-python/embed_util" +) + +func main() { + err := embed_util.BuildAndWriteFilesList("../install/controller") + if err != nil { + panic(err) + } +} diff --git a/internal/generate.go b/internal/generate.go new file mode 100644 index 000000000..652e2d5b4 --- /dev/null +++ b/internal/generate.go @@ -0,0 +1,3 @@ +package internal + +//go:generate go run ./generate-install diff --git a/internal/ipfs-exchange-info/go.mod b/internal/ipfs-exchange-info/go.mod new file mode 100644 index 000000000..18e011155 --- /dev/null +++ b/internal/ipfs-exchange-info/go.mod @@ -0,0 +1,136 @@ +module github.com/kluctl/kluctl/hack/ipfs-exchange-info + +go 1.22.0 + +toolchain go1.23.0 + +require ( + filippo.io/age v1.2.0 + github.com/libp2p/go-libp2p v0.37.2 + github.com/libp2p/go-libp2p-kad-dht v0.28.1 + github.com/sirupsen/logrus v1.9.3 +) + +require ( + github.com/benbjohnson/clock v1.3.5 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/elastic/gosigar v0.14.3 // indirect + github.com/flynn/noise v1.1.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/pprof v0.0.0-20241128161848-dc51965c6481 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/ipfs/boxo v0.24.3 // indirect + github.com/ipfs/go-cid v0.4.1 // indirect + github.com/ipfs/go-datastore v0.6.0 // indirect + github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/ipld/go-ipld-prime v0.21.0 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/jbenet/goprocess v0.1.4 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/koron/go-ssdp v0.0.4 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-cidranger v1.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.2.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect + github.com/libp2p/go-libp2p-kbucket v0.6.4 // indirect + github.com/libp2p/go-libp2p-record v0.2.0 // indirect + github.com/libp2p/go-libp2p-routing-helpers v0.7.4 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-nat v0.2.0 // indirect + github.com/libp2p/go-netroute v0.2.2 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/libp2p/go-yamux/v4 v4.0.1 // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/miekg/dns v1.1.62 // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multiaddr v0.14.0 // indirect + github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.9.0 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect + github.com/multiformats/go-multistream v0.6.0 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/ginkgo/v2 v2.22.0 // indirect + github.com/opencontainers/runtime-spec v1.2.0 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pion/datachannel v1.5.9 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect + github.com/pion/ice/v2 v2.3.37 // indirect + github.com/pion/interceptor v0.1.37 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/mdns v0.0.12 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.14 // indirect + github.com/pion/rtp v1.8.9 // indirect + github.com/pion/sctp v1.8.34 // indirect + github.com/pion/sdp/v3 v3.0.9 // indirect + github.com/pion/srtp/v2 v2.0.20 // indirect + github.com/pion/stun v0.6.1 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pion/turn/v2 v2.1.6 // indirect + github.com/pion/webrtc/v3 v3.3.4 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/polydawn/refmt v0.89.0 // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.60.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.48.2 // indirect + github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect + github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.uber.org/dig v1.18.0 // indirect + go.uber.org/fx v1.23.0 // indirect + go.uber.org/mock v0.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.27.0 // indirect + gonum.org/v1/gonum v0.15.1 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/blake3 v1.3.0 // indirect +) diff --git a/internal/ipfs-exchange-info/go.sum b/internal/ipfs-exchange-info/go.sum new file mode 100644 index 000000000..c7653c00d --- /dev/null +++ b/internal/ipfs-exchange-info/go.sum @@ -0,0 +1,654 @@ +c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= +c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +filippo.io/age v1.2.0 h1:vRDp7pUMaAJzXNIWJVAZnEf/Dyi4Vu4wI8S1LBzufhE= +filippo.io/age v1.2.0/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +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/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +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/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= +github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-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/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +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.3.2/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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +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.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/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/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20241128161848-dc51965c6481 h1:yudKIrXagAOl99WQzrP1gbz5HLB9UjhcOFnPzdd6Qec= +github.com/google/pprof v0.0.0-20241128161848-dc51965c6481/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ipfs/boxo v0.24.3 h1:gldDPOWdM3Rz0v5LkVLtZu7A7gFNvAlWcmxhCqlHR3c= +github.com/ipfs/boxo v0.24.3/go.mod h1:h0DRzOY1IBFDHp6KNvrJLMFdSXTYID0Zf+q7X05JsNg= +github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= +github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= +github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= +github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= +github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= +github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= +github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= +github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/ipfs/go-test v0.0.4 h1:DKT66T6GBB6PsDFLoO56QZPrOmzJkqU1FZH5C9ySkew= +github.com/ipfs/go-test v0.0.4/go.mod h1:qhIM1EluEfElKKM6fnWxGn822/z9knUGM1+I/OAQNKI= +github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= +github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= +github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= +github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/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/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= +github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= +github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= +github.com/libp2p/go-libp2p v0.37.2 h1:Irh+n9aDPTLt9wJYwtlHu6AhMUipbC1cGoJtOiBqI9c= +github.com/libp2p/go-libp2p v0.37.2/go.mod h1:M8CRRywYkqC6xKHdZ45hmqVckBj5z4mRLIMLWReypz8= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-kad-dht v0.28.1 h1:DVTfzG8Ybn88g9RycIq47evWCRss5f0Wm8iWtpwyHso= +github.com/libp2p/go-libp2p-kad-dht v0.28.1/go.mod h1:0wHURlSFdAC42+wF7GEmpLoARw8JuS8do2guCtc/Y/w= +github.com/libp2p/go-libp2p-kbucket v0.6.4 h1:OjfiYxU42TKQSB8t8WYd8MKhYhMJeO2If+NiuKfb6iQ= +github.com/libp2p/go-libp2p-kbucket v0.6.4/go.mod h1:jp6w82sczYaBsAypt5ayACcRJi0lgsba7o4TzJKEfWA= +github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= +github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= +github.com/libp2p/go-libp2p-routing-helpers v0.7.4 h1:6LqS1Bzn5CfDJ4tzvP9uwh42IB7TJLNFJA6dEeGBv84= +github.com/libp2p/go-libp2p-routing-helpers v0.7.4/go.mod h1:we5WDj9tbolBXOuF1hGOkR+r7Uh1408tQbAKaT5n1LE= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= +github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= +github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8= +github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ= +github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +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/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.14.0 h1:bfrHrJhrRuh/NXH5mCnemjpbGjzRw/b+tJFOD41g2tU= +github.com/multiformats/go-multiaddr v0.14.0/go.mod h1:6EkVAxtznq2yC3QT5CM1UTAwG0GTP3EWAIcjHuzQ+r4= +github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= +github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= +github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= +github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +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/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= +github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0= +github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= +github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= +github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= +github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= +github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= +github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.34 h1:rCuD3m53i0oGxCSp7FLQKvqVx0Nf5AUAHhMRXTTQjBc= +github.com/pion/sctp v1.8.34/go.mod h1:yWkCClkXlzVW7BXfI2PjrUGBwUI0CjXJBkhLt+sdo4U= +github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= +github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= +github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= +github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= +github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= +github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= +github.com/pion/webrtc/v3 v3.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk= +github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= +github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= +github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= +github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= +github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= +github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= +github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= +github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +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.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= +go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= +go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +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/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +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.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/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-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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.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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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-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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/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-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/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-20210330210617-4fbd30eecc44/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-20210630005230-0f9fa26af87c/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.7.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.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= +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= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +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.8/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-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= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= +lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/internal/ipfs-exchange-info/main.go b/internal/ipfs-exchange-info/main.go new file mode 100644 index 000000000..9a8f6397e --- /dev/null +++ b/internal/ipfs-exchange-info/main.go @@ -0,0 +1,465 @@ +package main + +import ( + "bufio" + "bytes" + "context" + "encoding/gob" + "encoding/json" + "filippo.io/age" + "flag" + "fmt" + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + drouting "github.com/libp2p/go-libp2p/p2p/discovery/routing" + log "github.com/sirupsen/logrus" + "io" + "net/http" + "os" + "strings" + "sync" + "time" + + dht "github.com/libp2p/go-libp2p-kad-dht" + dutil "github.com/libp2p/go-libp2p/p2p/discovery/util" +) + +var modeFlag string +var topicFlag string +var ipfsId string +var prNumber int +var ageKeyFile string +var agePubKey string +var repoName string +var outFile string + +func ParseFlags() error { + flag.StringVar(&modeFlag, "mode", "", "Mode") + flag.StringVar(&topicFlag, "topic", "", "pubsub topic") + flag.StringVar(&ipfsId, "ipfs-id", "", "IPFS ID") + flag.IntVar(&prNumber, "pr-number", 0, "PR number") + flag.StringVar(&ageKeyFile, "age-key-file", "", "AGE key file") + flag.StringVar(&agePubKey, "age-pub-key", "", "AGE pubkey") + flag.StringVar(&repoName, "repo-name", "", "Repo name") + flag.StringVar(&outFile, "out-file", "", "Output file") + flag.Parse() + + return nil +} + +func main() { + //logging.SetLogLevel("*", "INFO") + + err := ParseFlags() + if err != nil { + panic(err) + } + + if topicFlag == "test" { + reader := bufio.NewReader(os.Stdin) + fmt.Print("Enter num: ") + text, _ := reader.ReadString('\n') + topicFlag = "my-test-topic-" + strings.TrimSpace(text) + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + + var h host.Host + h, err = libp2p.New( + libp2p.ListenAddrStrings("/ip4/0.0.0.0/tcp/0"), + libp2p.EnableRelay(), + libp2p.EnableNATService(), + libp2p.NATPortMap(), + libp2p.ForceReachabilityPrivate(), + libp2p.EnableAutoRelayWithPeerSource(findRelayPeers(func() host.Host { + return h + }))) + if err != nil { + panic(err) + } + + log.Infof("own ID: %s", h.ID().String()) + + kademliaDHT, err := initDHT(ctx, h) + if err != nil { + log.Error(err) + log.Exit(1) + } + discovery := drouting.NewRoutingDiscovery(kademliaDHT) + + switch modeFlag { + case "publish": + err = doPublish(ctx, h, discovery) + case "subscribe": + err = doSubscribe(ctx, h, discovery) + default: + err = fmt.Errorf("unknown mode %s", modeFlag) + } + + if err != nil { + log.Error(err) + log.Exit(1) + } else { + log.Exit(0) + } +} + +func initDHT(ctx context.Context, h host.Host) (*dht.IpfsDHT, error) { + // Start a DHT, for use in peer discovery. We can't just make a new DHT + // client because we want each peer to maintain its own local copy of the + // DHT, so that the bootstrapping node of the DHT can go down without + // inhibiting future peer discovery. + kademliaDHT, err := dht.New(ctx, h) + if err != nil { + return nil, err + } + if err = kademliaDHT.Bootstrap(ctx); err != nil { + return nil, err + } + var wg sync.WaitGroup + for _, peerAddr := range dht.DefaultBootstrapPeers { + peerinfo, _ := peer.AddrInfoFromP2pAddr(peerAddr) + wg.Add(1) + go func() { + defer wg.Done() + if err := h.Connect(ctx, *peerinfo); err != nil { + log.Info("Bootstrap warning:", err) + } else { + log.Infof("Connected to bootstrap peer: %s", peerinfo.String()) + } + }() + } + wg.Wait() + + return kademliaDHT, nil +} + +func findRelayPeers(h func() host.Host) func(ctx context.Context, num int) <-chan peer.AddrInfo { + return func(ctx context.Context, num int) <-chan peer.AddrInfo { + ch := make(chan peer.AddrInfo) + go func() { + sent := 0 + outer: + for { + for _, id := range h().Peerstore().PeersWithAddrs() { + if sent >= num { + break + } + protos, err := h().Peerstore().GetProtocols(id) + if err != nil { + continue + } + for _, proto := range protos { + if strings.HasPrefix(string(proto), "/libp2p/circuit/relay/") { + ch <- peer.AddrInfo{ + ID: id, + Addrs: h().Peerstore().Addrs(id), + } + sent++ + break + } + } + } + select { + case <-time.After(time.Second): + continue + case <-ctx.Done(): + break outer + } + } + close(ch) + }() + return ch + } +} + +type workflowInfo struct { + PrNumber int `json:"prNumber"` + IpfsId string `json:"ipfsId"` + GithubToken string `json:"githubToken"` +} + +func doPublish(ctx context.Context, h host.Host, discovery *drouting.RoutingDiscovery) error { + info := workflowInfo{ + PrNumber: prNumber, + GithubToken: os.Getenv("GITHUB_TOKEN"), + IpfsId: ipfsId, + } + + b, err := json.Marshal(&info) + if err != nil { + return err + } + + ageRecipient, err := age.ParseX25519Recipient(agePubKey) + if err != nil { + return err + } + + w := bytes.NewBuffer(nil) + e, err := age.Encrypt(w, ageRecipient) + if err != nil { + return err + } + _, err = e.Write(b) + if err != nil { + return err + } + err = e.Close() + if err != nil { + return err + } + b = w.Bytes() + + log.Info("Sending info...") + + for { + peersCh, err := discovery.FindPeers(ctx, topicFlag) + if err != nil { + return err + } + didSend := false + for peer := range peersCh { + if peer.ID == h.ID() { + continue // No self connection + } + + log.Infof("Trying %s with addrs %v", peer.ID, peer.Addrs) + + err = h.Connect(ctx, peer) + if err != nil { + log.Info(err) + continue + } + + err = sendFile(ctx, h, peer.ID, b) + if err != nil { + log.Warn(err) + continue + } + didSend = true + } + if didSend { + break + } + } + + log.Info("Done sending info.") + + return nil +} + +func doSubscribe(ctx context.Context, h host.Host, discovery *drouting.RoutingDiscovery) error { + doneCh := make(chan bool) + h.SetStreamHandler("/x/kluctl-preview-info", func(s network.Stream) { + defer s.Close() + + enc := gob.NewEncoder(s) + dec := gob.NewDecoder(s) + + var b []byte + err := dec.Decode(&b) + if err != nil { + log.Infof("Receive failed: %v", err) + return + } + + err = handleInfo(ctx, b) + isDone := err == nil + if err != nil { + log.Infof("handle failed: %v", err) + _ = enc.Encode("not ok") + } else { + err = enc.Encode("ok") + if err != nil { + log.Infof("Sending ok failed: %v", err) + return + } + } + + var closeMsg string + err = dec.Decode(&closeMsg) + if err != nil { + log.Infof("Receiving close msg failed: %v", err) + } + + if isDone { + doneCh <- true + } + }) + + dutil.Advertise(ctx, discovery, topicFlag) + <-doneCh + h.RemoveStreamHandler("/x/kluctl-preview-info") + + return nil +} + +func handleInfo(ctx context.Context, data []byte) error { + idsBytes, err := os.ReadFile(ageKeyFile) + if err != nil { + return err + } + ageIds, err := age.ParseIdentities(bytes.NewReader(idsBytes)) + if err != nil { + return err + } + d, err := age.Decrypt(bytes.NewReader(data), ageIds...) + if err != nil { + return err + } + + w := bytes.NewBuffer(nil) + _, err = io.Copy(w, d) + if err != nil { + return err + } + data = w.Bytes() + + var info workflowInfo + err = json.Unmarshal(data, &info) + if err != nil { + return err + } + + if info.PrNumber != prNumber { + return fmt.Errorf("%d is not the expected (%d) PR number", info.PrNumber, prNumber) + } + + log.Info("Checking Github token...") + + err = checkGithubToken(ctx, info.GithubToken) + if err != nil { + return err + } + + log.Info("Done checking Github token...") + + info.GithubToken = "" + + data, err = json.Marshal(&info) + if err != nil { + return err + } + err = os.WriteFile(outFile, data, 0o600) + if err != nil { + return err + } + return nil +} + +func doGithubRequest(ctx context.Context, method string, url string, body string, token string) ([]byte, error) { + log.Infof("request: %s %s", method, url) + + req, err := http.NewRequest(method, url, strings.NewReader(body)) + if err != nil { + log.Error("NewRequest failed: ", err) + return nil, err + } + req = req.WithContext(ctx) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Error("Request failed: ", err) + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + log.Error(fmt.Sprintf("Request failed: %d - %v", resp.StatusCode, resp.Status)) + return nil, fmt.Errorf("http error: %s", resp.Status) + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + log.Error("Failed to read body: ", err) + return nil, err + } + + return b, nil +} + +func checkGithubToken(ctx context.Context, token string) error { + body := fmt.Sprintf(`{"query": "query UserCurrent{viewer{login}}"}`) + b, err := doGithubRequest(ctx, "POST", "https://api.github.com/graphql", body, token) + if err != nil { + return err + } + log.Info("body=", string(b)) + + var r struct { + Data struct { + Viewer struct { + Login string `json:"login"` + } `json:"viewer"` + } `json:"data"` + } + err = json.Unmarshal(b, &r) + if err != nil { + log.Error("Unmarshal failed: ", err) + return err + } + if r.Data.Viewer.Login != "github-actions[bot]" { + log.Error("unexpected response from github") + return fmt.Errorf("unexpected response from github") + } + + log.Info("Querying repositories...") + + b, err = doGithubRequest(ctx, "GET", "https://api.github.com/installation/repositories", "", token) + if err != nil { + return err + } + log.Info("body=", string(b)) + + var r2 struct { + Repositories []struct { + FullName string `json:"full_name"` + } `json:"repositories"` + } + err = json.Unmarshal(b, &r2) + if err != nil { + return err + } + if len(r2.Repositories) != 1 { + return fmt.Errorf("unexpected repositories count %d", len(r2.Repositories)) + } + if r2.Repositories[0].FullName != repoName { + return fmt.Errorf("%s is not the expected repo name", r2.Repositories[0].FullName) + } + + return nil +} + +func sendFile(ctx context.Context, h host.Host, ipfsId peer.ID, data []byte) error { + s, err := h.NewStream(ctx, ipfsId, "/x/kluctl-preview-info") + if err != nil { + return fmt.Errorf("failed to connect to %s: %w", ipfsId.String(), err) + } + defer s.Close() + + enc := gob.NewEncoder(s) + dec := gob.NewDecoder(s) + + err = enc.Encode(data) + if err != nil { + return fmt.Errorf("failed to send msg: %w", err) + } + + var ok string + err = dec.Decode(&ok) + if err != nil { + return fmt.Errorf("failed to read ok: %w", err) + } + _ = enc.Encode("close") + if ok != "ok" { + return fmt.Errorf("received '%s' instead of ok", ok) + } + + return nil +} diff --git a/internal/replace-commands-help/main.go b/internal/replace-commands-help/main.go new file mode 100644 index 000000000..6fbbf7924 --- /dev/null +++ b/internal/replace-commands-help/main.go @@ -0,0 +1,175 @@ +package main + +import ( + "flag" + "fmt" + "github.com/kluctl/kluctl/v2/cmd/kluctl/commands" + "io/fs" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "reflect" + "regexp" + "strings" + "syscall" +) + +var docsDir = flag.String("docs-dir", "", "Path to documentation") + +type section struct { + start int + end int + command string + section string + code bool +} + +func main() { + _ = syscall.Setenv("COLUMNS", "120") + + if os.Getenv("CALL_KLUCTL") == "true" { + kluctlMain() + return + } + + flag.Parse() + + filepath.WalkDir(*docsDir, func(path string, d fs.DirEntry, err error) error { + if !strings.HasSuffix(path, ".md") { + return nil + } + processFile(path) + return nil + }) +} + +func kluctlMain() { + commands.Main() +} + +func processFile(path string) { + text, err := ioutil.ReadFile(path) + if err != nil { + log.Fatal(err) + } + lines := strings.Split(string(text), "\n") + + var newLines []string + pos := 0 + for true { + s := findNextSection(lines, pos) + if s == nil { + newLines = append(newLines, lines[pos:]...) + break + } + + newLines = append(newLines, lines[pos:s.start+1]...) + + s2 := getHelpSection(s.command, s.section) + + if s.code { + newLines = append(newLines, "```") + } + newLines = append(newLines, s2...) + if s.code { + newLines = append(newLines, "```") + } + + newLines = append(newLines, lines[s.end]) + pos = s.end + 1 + } + + if !reflect.DeepEqual(lines, newLines) { + err = ioutil.WriteFile(path, []byte(strings.Join(newLines, "\n")), 0o600) + if err != nil { + log.Fatal(err) + } + } +} + +var beginPattern = regexp.MustCompile(``) +var endPattern = regexp.MustCompile(``) + +func findNextSection(lines []string, start int) *section { + + for i := start; i < len(lines); i++ { + m := beginPattern.FindSubmatch([]byte(lines[i])) + if m == nil { + continue + } + + var s section + s.start = i + s.command = string(m[1]) + s.section = string(m[2]) + s.code = string(m[3]) == "true" + + for j := i + 1; j < len(lines); j++ { + m = endPattern.FindSubmatch([]byte(lines[j])) + if m == nil { + continue + } + s.end = j + return &s + } + } + return nil +} + +func countIndent(str string) int { + for i := 0; i < len(str); i++ { + if str[i] != ' ' { + return i + } + } + return 0 +} + +func getHelpSection(command string, section string) []string { + log.Printf("Getting section '%s' from command '%s'", section, command) + + exe, err := os.Executable() + if err != nil { + log.Fatal(err) + } + + args := strings.Split(command, " ") + args = append(args, "--help") + + helpCmd := exec.Command(exe, args...) + helpCmd.Env = os.Environ() + helpCmd.Env = append(helpCmd.Env, "CALL_KLUCTL=true") + + out, err := helpCmd.CombinedOutput() + if err != nil { + log.Fatal(err) + } + + lines := strings.Split(string(out), "\n") + + sectionStart := -1 + for i := 0; i < len(lines); i++ { + indent := countIndent(lines[i]) + if strings.HasPrefix(lines[i][indent:], fmt.Sprintf("%s:", section)) { + sectionStart = i + break + } + } + if sectionStart == -1 { + log.Fatalf("Section %s not found in command %s", section, command) + } + + var ret []string + ret = append(ret, lines[sectionStart]) + for i := sectionStart + 1; i < len(lines); i++ { + indent := countIndent(lines[i]) + if len(lines[i]) != 0 && indent == 0 && lines[i][len(lines[i])-1] == ':' { + // new section has started + break + } + ret = append(ret, lines[i]) + } + return ret +} diff --git a/internal/test-utils/git_server.go b/internal/test-utils/git_server.go deleted file mode 100644 index 29727fdeb..000000000 --- a/internal/test-utils/git_server.go +++ /dev/null @@ -1,223 +0,0 @@ -package test_utils - -import ( - "context" - "fmt" - "github.com/go-git/go-git/v5" - http_server "github.com/kluctl/kluctl/v2/pkg/git/http-server" - "github.com/kluctl/kluctl/v2/pkg/utils" - "github.com/kluctl/kluctl/v2/pkg/utils/uo" - "github.com/kluctl/kluctl/v2/pkg/yaml" - "io/ioutil" - "log" - "net" - "net/http" - "os" - "path/filepath" - "reflect" - "testing" -) - -type GitServer struct { - t *testing.T - - baseDir string - - gitServer *http_server.Server - gitHttpServer *http.Server - gitServerPort int -} - -func NewGitServer(t *testing.T) *GitServer { - p := &GitServer{ - t: t, - } - - baseDir, err := ioutil.TempDir(os.TempDir(), "kluctl-tests-") - if err != nil { - p.t.Fatal(err) - } - p.baseDir = baseDir - - p.initGitServer() - - t.Cleanup(func() { - p.Cleanup() - }) - - return p -} - -func (p *GitServer) initGitServer() { - p.gitServer = http_server.New(p.baseDir) - - p.gitHttpServer = &http.Server{ - Addr: "127.0.0.1:0", - Handler: p.gitServer, - } - - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - log.Fatal(err) - } - a := ln.Addr().(*net.TCPAddr) - p.gitServerPort = a.Port - - go func() { - _ = p.gitHttpServer.Serve(ln) - }() -} - -func (p *GitServer) Cleanup() { - if p.gitHttpServer != nil { - _ = p.gitHttpServer.Shutdown(context.Background()) - p.gitHttpServer = nil - p.gitServer = nil - } - - if p.baseDir == "" { - return - } - _ = os.RemoveAll(p.baseDir) - p.baseDir = "" -} - -func (p *GitServer) GitInit(repo string) { - dir := p.LocalRepoDir(repo) - - err := os.MkdirAll(dir, 0o700) - if err != nil { - p.t.Fatal(err) - } - - r, err := git.PlainInit(dir, false) - if err != nil { - p.t.Fatal(err) - } - config, err := r.Config() - if err != nil { - p.t.Fatal(err) - } - wt, err := r.Worktree() - if err != nil { - p.t.Fatal(err) - } - - config.User.Name = "Test User" - config.User.Email = "no@mail.com" - config.Author = config.User - config.Committer = config.User - err = r.SetConfig(config) - if err != nil { - p.t.Fatal(err) - } - err = utils.Touch(filepath.Join(dir, ".dummy")) - if err != nil { - p.t.Fatal(err) - } - _, err = wt.Add(".dummy") - if err != nil { - p.t.Fatal(err) - } - _, err = wt.Commit("initial", &git.CommitOptions{}) - if err != nil { - p.t.Fatal(err) - } -} - -func (p *GitServer) CommitFiles(repo string, add []string, all bool, message string) { - r, err := git.PlainOpen(p.LocalRepoDir(repo)) - if err != nil { - p.t.Fatal(err) - } - wt, err := r.Worktree() - if err != nil { - p.t.Fatal(err) - } - for _, a := range add { - _, err = wt.Add(a) - if err != nil { - p.t.Fatal(err) - } - } - _, err = wt.Commit(message, &git.CommitOptions{ - All: all, - }) - if err != nil { - p.t.Fatal(err) - } -} - -func (p *GitServer) CommitYaml(repo string, pth string, message string, y *uo.UnstructuredObject) { - fullPath := filepath.Join(p.LocalRepoDir(repo), pth) - - err := yaml.WriteYamlFile(fullPath, y) - if err != nil { - p.t.Fatal(err) - } - if message == "" { - message = fmt.Sprintf("update %s", filepath.Join(repo, pth)) - } - p.CommitFiles(repo, []string{pth}, false, message) -} - -func (p *GitServer) UpdateYaml(repo string, pth string, update func(o *uo.UnstructuredObject) error, message string) { - fullPath := filepath.Join(p.LocalRepoDir(repo), pth) - - o := uo.New() - if utils.Exists(fullPath) { - err := yaml.ReadYamlFile(fullPath, o) - if err != nil { - p.t.Fatal(err) - } - } - orig := o.Clone() - err := update(o) - if err != nil { - p.t.Fatal(err) - } - if reflect.DeepEqual(o, orig) { - return - } - p.CommitYaml(repo, pth, message, o) -} - -func (p *GitServer) convertInterfaceToList(x interface{}) []interface{} { - var ret []interface{} - if l, ok := x.([]interface{}); ok { - return l - } - if l, ok := x.([]*uo.UnstructuredObject); ok { - for _, y := range l { - ret = append(ret, y) - } - return ret - } - if l, ok := x.([]map[string]interface{}); ok { - for _, y := range l { - ret = append(ret, y) - } - return ret - } - return []interface{}{x} -} - -func (p *GitServer) LocalGitUrl(repo string) string { - return fmt.Sprintf("http://localhost:%d/%s/.git", p.gitServerPort, repo) -} - -func (p *GitServer) LocalRepoDir(repo string) string { - return filepath.Join(p.baseDir, repo) -} - -func (p *GitServer) GetWorktree(repo string) *git.Worktree { - r, err := git.PlainOpen(p.LocalRepoDir(repo)) - if err != nil { - p.t.Fatal(err) - } - wt, err := r.Worktree() - if err != nil { - p.t.Fatal(err) - } - return wt -} diff --git a/internal/test-utils/kind_cluster.go b/internal/test-utils/kind_cluster.go deleted file mode 100644 index 99ad404d7..000000000 --- a/internal/test-utils/kind_cluster.go +++ /dev/null @@ -1,181 +0,0 @@ -package test_utils - -import ( - "fmt" - "github.com/kluctl/kluctl/v2/pkg/utils/uo" - "github.com/kluctl/kluctl/v2/pkg/yaml" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "net/url" - "os" - "os/exec" - "sigs.k8s.io/kind/pkg/apis/config/v1alpha4" - "sigs.k8s.io/kind/pkg/cluster" - kindcmd "sigs.k8s.io/kind/pkg/cmd" - "testing" - "time" -) - -type KindCluster struct { - Name string - Context string - Kubeconfig string - config *rest.Config -} - -func CreateKindCluster(name, apiServerHost string, apiServerPort int, extraPorts map[int]int, kubeconfigPath string) (*KindCluster, error) { - provider := cluster.NewProvider(cluster.ProviderWithLogger(kindcmd.NewLogger())) - - c := &KindCluster{ - Name: name, - Context: fmt.Sprintf("kind-%s", name), - Kubeconfig: kubeconfigPath, - } - - n, err := provider.ListNodes(name) - if err != nil { - return nil, err - } - if len(n) == 0 { - if err := kindCreate(name, apiServerHost, apiServerPort, extraPorts, kubeconfigPath); err != nil { - return nil, err - } - } - - return c, nil -} - -// Delete removes the cluster from kind. The cluster may not be deleted straight away - this only issues a delete command -func (c *KindCluster) Delete() error { - provider := cluster.NewProvider(cluster.ProviderWithLogger(kindcmd.NewLogger())) - return provider.Delete(c.Name, c.Kubeconfig) -} - -// RESTConfig returns K8s client config to pass to clientset objects -func (c *KindCluster) RESTConfig() *rest.Config { - if c.config == nil { - var err error - c.config, err = clientcmd.BuildConfigFromFlags("", c.Kubeconfig) - if err != nil { - panic(err) - } - } - return c.config -} - -func (c *KindCluster) Kubectl(args ...string) (string, error) { - cmd := exec.Command("kubectl", args...) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, fmt.Sprintf("KUBECONFIG=%s", c.Kubeconfig)) - - stdout, err := cmd.Output() - return string(stdout), err -} - -func (c *KindCluster) KubectlMust(t *testing.T, args ...string) string { - stdout, err := c.Kubectl(args...) - if err != nil { - if e, ok := err.(*exec.ExitError); ok { - t.Fatalf("%v, stderr=%s\n", err, string(e.Stderr)) - } else { - t.Fatal(err) - } - } - return stdout -} - -func (c *KindCluster) KubectlYaml(args ...string) (*uo.UnstructuredObject, error) { - args = append(args, "-oyaml") - stdout, err := c.Kubectl(args...) - if err != nil { - return nil, err - } - ret := uo.New() - err = yaml.ReadYamlString(stdout, ret) - return ret, err -} - -func (c *KindCluster) KubectlYamlMust(t *testing.T, args ...string) *uo.UnstructuredObject { - o, err := c.KubectlYaml(args...) - if err != nil { - if e, ok := err.(*exec.ExitError); ok { - t.Fatalf("%v, stderr=%s\n", err, string(e.Stderr)) - } else { - t.Fatal(err) - } - } - return o -} - -// kindCreate creates the kind cluster. It will retry up to 10 times if cluster creation fails. -func kindCreate(name string, apiServerHost string, apiServerPort int, extraPorts map[int]int, kubeconfig string) error { - var err error - for i := 0; i < 10; i++ { - err = kindCreate2(name, apiServerHost, apiServerPort, extraPorts, kubeconfig) - if err == nil { - return nil - } - } - return err -} - -func kindCreate2(name string, apiServerHost string, apiServerPort int, extraPorts map[int]int, kubeconfig string) error { - fmt.Printf("🌧️ Creating kind cluster %s with apiServerHost=%s, apiServerPort=%d, extraPorts=%v...\n", name, apiServerHost, apiServerPort, extraPorts) - provider := cluster.NewProvider(cluster.ProviderWithLogger(kindcmd.NewLogger())) - config := v1alpha4.Cluster{ - Name: name, - Nodes: []v1alpha4.Node{{ - Role: "control-plane", - }}, - Networking: v1alpha4.Networking{ - APIServerAddress: "0.0.0.0", - APIServerPort: int32(apiServerPort), - }, - } - for hostPort, containerPort := range extraPorts { - config.Nodes[0].ExtraPortMappings = append(config.Nodes[0].ExtraPortMappings, v1alpha4.PortMapping{ - ContainerPort: int32(containerPort), - HostPort: int32(hostPort), - ListenAddress: "0.0.0.0", - Protocol: "TCP", - }) - } - - err := provider.Create( - name, - cluster.CreateWithV1Alpha4Config(&config), - cluster.CreateWithNodeImage(""), - cluster.CreateWithRetain(false), - cluster.CreateWithWaitForReady(time.Duration(0)), - cluster.CreateWithKubeconfigPath(kubeconfig), - cluster.CreateWithDisplayUsage(false), - ) - if err != nil { - return err - } - - kcfg, err := clientcmd.LoadFromFile(kubeconfig) - if err != nil { - return err - } - - c := kcfg.Clusters[fmt.Sprintf("kind-%s", name)] - - u, err := url.Parse(c.Server) - if err != nil { - return err - } - - // override api server host and disable TLS verification - // this is needed to make it work with remote docker hosts - c.InsecureSkipTLSVerify = true - c.CertificateAuthorityData = nil - c.Server = fmt.Sprintf("https://%s:%s", apiServerHost, u.Port()) - - err = clientcmd.WriteToFile(*kcfg, kubeconfig) - if err != nil { - return err - } - - return nil -} diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 000000000..103ecfe27 --- /dev/null +++ b/lib/README.md @@ -0,0 +1,2 @@ +This directory contains go modules that are used by Kluctl itself and some of the sub-projects of Kluctl (like template controller). +These liberary were introduced to reduce the amount of dependencies the sub-projects pull-in from Kluctl. \ No newline at end of file diff --git a/lib/envutils/env.go b/lib/envutils/env.go new file mode 100644 index 000000000..7cbd0132d --- /dev/null +++ b/lib/envutils/env.go @@ -0,0 +1,109 @@ +package envutils + +import ( + "fmt" + "os" + "regexp" + "sort" + "strconv" + "strings" +) + +func ParseEnvBool(name string, def bool) (bool, error) { + if x, ok := os.LookupEnv(name); ok { + b, err := strconv.ParseBool(x) + if err != nil { + return def, err + } + return b, nil + } + return def, nil +} + +type EnvSet struct { + Index int + Map map[string]string +} + +func parseEnv(prefix string, withIndex bool, withSuffix bool) []EnvSet { + envSetMap := make(map[int]map[string]string) + + rs := prefix + curGroup := 1 + indexGroup := -1 + suffixGroup := -1 + if withIndex { + rs += `(_\d+)?` + indexGroup = curGroup + curGroup++ + } + if withSuffix { + rs += `_(.*)` + suffixGroup = curGroup + curGroup++ + } + rs = fmt.Sprintf("^%s$", rs) + r := regexp.MustCompile(rs) + + for _, e := range os.Environ() { + eq := strings.Index(e, "=") + if eq == -1 { + continue + } + n := e[:eq] + v := e[eq+1:] + + idx := -1 + suffix := "" + + m := r.FindStringSubmatch(n) + if m == nil { + continue + } + + if withIndex { + idxStr := m[indexGroup] + if idxStr != "" { + idxStr = idxStr[1:] // remove leading _ + x, err := strconv.ParseInt(idxStr, 10, 32) + if err != nil { + continue + } + idx = int(x) + } + } + if withSuffix { + suffix = m[suffixGroup] + } + + if _, ok := envSetMap[idx]; !ok { + envSetMap[idx] = map[string]string{} + } + envSetMap[idx][suffix] = v + } + + var ret []EnvSet + for idx, v := range envSetMap { + ret = append(ret, EnvSet{ + Index: idx, + Map: v, + }) + } + sort.Slice(ret, func(i, j int) bool { + return ret[i].Index < ret[j].Index + }) + return ret +} + +func ParseEnvConfigSets(prefix string) []EnvSet { + return parseEnv(prefix, true, true) +} + +func ParseEnvConfigList(prefix string) map[int]string { + ret := make(map[int]string) + + for _, s := range parseEnv(prefix, true, false) { + ret[s.Index] = s.Map[""] + } + return ret +} diff --git a/lib/envutils/env_test.go b/lib/envutils/env_test.go new file mode 100644 index 000000000..427b07c61 --- /dev/null +++ b/lib/envutils/env_test.go @@ -0,0 +1,32 @@ +package envutils + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestParseEnvConfigSets_Prefixes(t *testing.T) { + t.Setenv("PREFIX_A", "a") + t.Setenv("PREFIX_B", "b") + t.Setenv("PREFIX2_C", "c") + assert.Equal(t, []EnvSet(nil), ParseEnvConfigSets("DUMMY")) + assert.Equal(t, []EnvSet{{Index: -1, Map: map[string]string{"A": "a", "B": "b"}}}, ParseEnvConfigSets("PREFIX")) + assert.Equal(t, []EnvSet{{Index: -1, Map: map[string]string{"C": "c"}}}, ParseEnvConfigSets("PREFIX2")) +} + +func TestParseEnvConfigSets_Indexes(t *testing.T) { + t.Setenv("PREFIX_A", "a") + t.Setenv("PREFIX_0_A", "a0") + t.Setenv("PREFIX_0_B", "b0") + t.Setenv("PREFIX_1_A", "a1") + assert.Equal(t, []EnvSet{{Index: -1, Map: map[string]string{"A": "a"}}, {Index: 0, Map: map[string]string{"A": "a0", "B": "b0"}}, {Index: 1, Map: map[string]string{"A": "a1"}}}, ParseEnvConfigSets("PREFIX")) +} + +func TestParseEnvConfigList(t *testing.T) { + t.Setenv("PREFIX", "a") + assert.Equal(t, map[int]string{-1: "a"}, ParseEnvConfigList("PREFIX")) + + t.Setenv("PREFIX_0", "b") + t.Setenv("PREFIX_1", "c") + assert.Equal(t, map[int]string{-1: "a", 0: "b", 1: "c"}, ParseEnvConfigList("PREFIX")) +} diff --git a/lib/git/auth/auth_provider.go b/lib/git/auth/auth_provider.go new file mode 100644 index 000000000..dc376943b --- /dev/null +++ b/lib/git/auth/auth_provider.go @@ -0,0 +1,71 @@ +package auth + +import ( + "context" + "fmt" + "github.com/go-git/go-git/v5/plumbing/transport" + ssh2 "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "github.com/hashicorp/go-multierror" + "github.com/kluctl/kluctl/lib/git/messages" + "github.com/kluctl/kluctl/lib/git/types" + "golang.org/x/crypto/ssh" +) + +type AuthMethodAndCA struct { + AuthMethod transport.AuthMethod + CABundle []byte + + Hash func() ([]byte, error) +} + +func (a *AuthMethodAndCA) SshClientConfig() (*ssh.ClientConfig, error) { + gitSshAuth, ok := a.AuthMethod.(ssh2.AuthMethod) + if !ok { + return nil, fmt.Errorf("auth is not a git ssh.AuthMethod") + } + return gitSshAuth.ClientConfig() +} + +type GitAuthProvider interface { + BuildAuth(ctx context.Context, gitUrl types.GitUrl) (AuthMethodAndCA, error) +} + +type GitAuthProviders struct { + authProviders []GitAuthProvider +} + +func (a *GitAuthProviders) RegisterAuthProvider(p GitAuthProvider, last bool) { + if last { + a.authProviders = append(a.authProviders, p) + } else { + a.authProviders = append([]GitAuthProvider{p}, a.authProviders...) + } +} + +func (a *GitAuthProviders) BuildAuth(ctx context.Context, gitUrl types.GitUrl) (AuthMethodAndCA, error) { + var errs *multierror.Error + for _, p := range a.authProviders { + auth, err := p.BuildAuth(ctx, gitUrl) + if err != nil { + errs = multierror.Append(errs, err) + continue + } + if auth.AuthMethod == nil { + continue + } + return auth, nil + } + return AuthMethodAndCA{}, errs.ErrorOrNil() +} + +func NewDefaultAuthProviders(envPrefix string, messageCallbacks *messages.MessageCallbacks) *GitAuthProviders { + if messageCallbacks == nil { + messageCallbacks = &messages.MessageCallbacks{} + } + + a := &GitAuthProviders{} + a.RegisterAuthProvider(&GitEnvAuthProvider{MessageCallbacks: *messageCallbacks, Prefix: envPrefix}, true) + a.RegisterAuthProvider(&GitCredentialsFileAuthProvider{MessageCallbacks: *messageCallbacks}, true) + a.RegisterAuthProvider(&GitSshAuthProvider{MessageCallbacks: *messageCallbacks}, true) + return a +} diff --git a/lib/git/auth/env_auth_provider.go b/lib/git/auth/env_auth_provider.go new file mode 100644 index 000000000..a0a36be28 --- /dev/null +++ b/lib/git/auth/env_auth_provider.go @@ -0,0 +1,103 @@ +package auth + +import ( + "context" + "github.com/gobwas/glob" + "github.com/kluctl/kluctl/lib/envutils" + "github.com/kluctl/kluctl/lib/git/messages" + "github.com/kluctl/kluctl/lib/git/types" + "github.com/kluctl/kluctl/lib/status" + "os" + "sync" +) + +type GitEnvAuthProvider struct { + MessageCallbacks messages.MessageCallbacks + + Prefix string + + mutext sync.Mutex + list *ListAuthProvider + listErr error +} + +func (a *GitEnvAuthProvider) buildList(ctx context.Context) error { + a.mutext.Lock() + defer a.mutext.Unlock() + if a.listErr != nil { + return a.listErr + } + if a.list != nil { + return nil + } + a.listErr = a.doBuildList(ctx) + return a.listErr +} + +func (a *GitEnvAuthProvider) doBuildList(ctx context.Context) error { + a.list = &ListAuthProvider{} + + for _, s := range envutils.ParseEnvConfigSets(a.Prefix) { + m := s.Map + e := AuthEntry{ + Host: m["HOST"], + Username: m["USERNAME"], + Password: m["PASSWORD"], + } + if e.Host == "" { + continue + } + + path, ok := m["PATH"] + if !ok { + path, ok = m["PATH_PREFIX"] + if ok { + status.Deprecation(ctx, "git-prefix-path", "The environment variable KLUCTL_GIT_PREFIX_PATH is deprecated and support for it will be removed in a future Kluctl version. Please switch to KLUCTL_GIT_PATH with wildcards instead.") + path += "**" + } + } + if path != "" { + e.PathStr = path + g, err := glob.Compile(e.PathStr, '/') + if err != nil { + return err + } + e.PathGlob = g + } + + ssh_key_path, _ := m["SSH_KEY"] + + a.MessageCallbacks.Trace("GitEnvAuthProvider: adding entry host=%s, path=%s, username=%s, ssh_key=%s", e.Host, e.PathStr, e.Username, ssh_key_path) + + if ssh_key_path != "" { + ssh_key_path = expandHomeDir(ssh_key_path) + b, err := os.ReadFile(ssh_key_path) + if err != nil { + a.MessageCallbacks.Trace("GitEnvAuthProvider: failed to read key %s: %v", ssh_key_path, err) + } else { + e.SshKey = b + } + } + ca_bundle_path := m["CA_BUNDLE"] + if ca_bundle_path != "" { + ca_bundle_path = expandHomeDir(ca_bundle_path) + b, err := os.ReadFile(ca_bundle_path) + if err != nil { + a.MessageCallbacks.Trace("GitEnvAuthProvider: failed to read ca bundle %s: %v", ca_bundle_path, err) + } else { + e.CABundle = b + } + } + a.list.AddEntry(e) + } + + return nil +} + +func (a *GitEnvAuthProvider) BuildAuth(ctx context.Context, gitUrl types.GitUrl) (AuthMethodAndCA, error) { + err := a.buildList(ctx) + if err != nil { + return AuthMethodAndCA{}, err + } + return a.list.BuildAuth(ctx, gitUrl) +} diff --git a/lib/git/auth/git_credentials_file.go b/lib/git/auth/git_credentials_file.go new file mode 100644 index 000000000..3a5180f5b --- /dev/null +++ b/lib/git/auth/git_credentials_file.go @@ -0,0 +1,105 @@ +package auth + +import ( + "bufio" + "context" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/kluctl/kluctl/lib/git/messages" + "github.com/kluctl/kluctl/lib/git/types" + "net/url" + "os" + "path/filepath" +) + +type GitCredentialsFileAuthProvider struct { + MessageCallbacks messages.MessageCallbacks +} + +func (a *GitCredentialsFileAuthProvider) BuildAuth(ctx context.Context, gitUrl types.GitUrl) (AuthMethodAndCA, error) { + if gitUrl.Scheme != "http" && gitUrl.Scheme != "https" { + return AuthMethodAndCA{}, nil + } + a.MessageCallbacks.Trace("GitCredentialsFileAuthProvider: BuildAuth for %s", gitUrl.String()) + + home, err := os.UserHomeDir() + if err != nil { + a.MessageCallbacks.Warning("Could not determine home directory: %v", err) + return AuthMethodAndCA{}, err + } + auth := a.tryBuildAuth(gitUrl, filepath.Join(home, ".git-credentials")) + if auth != nil { + return *auth, nil + } + + if xdgHome, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok && xdgHome != "" { + auth = a.tryBuildAuth(gitUrl, filepath.Join(xdgHome, ".git-credentials")) + if auth != nil { + return *auth, nil + } + } else { + auth = a.tryBuildAuth(gitUrl, filepath.Join(home, ".config/.git-credentials")) + if auth != nil { + return *auth, nil + } + } + return AuthMethodAndCA{}, nil +} + +func (a *GitCredentialsFileAuthProvider) tryBuildAuth(gitUrl types.GitUrl, gitCredentialsPath string) *AuthMethodAndCA { + st, err := os.Stat(gitCredentialsPath) + if err != nil || st.Mode().IsDir() { + return nil + } + + a.MessageCallbacks.Trace("GitCredentialsFileAuthProvider: trying file %s", gitCredentialsPath) + + f, err := os.Open(gitCredentialsPath) + if err != nil { + a.MessageCallbacks.Warning("Failed to open %s: %v", gitCredentialsPath, err) + return nil + } + defer f.Close() + + s := bufio.NewScanner(f) + for s.Scan() { + if s.Text() == "" || s.Text()[0] == '#' { + continue + } + u, err := types.ParseGitUrl(s.Text()) + if err != nil { + continue + } + // create temporary url without password, which can be printed + tmpU := *u + tmpU.User = url.User(u.User.Username()) + + if u.User == nil || u.User.Username() == "" { + a.MessageCallbacks.Trace("GitCredentialsFileAuthProvider: ignoring %s", tmpU.String()) + continue + } + a.MessageCallbacks.Trace("GitCredentialsFileAuthProvider: trying %s", tmpU.String()) + + if u.Host != gitUrl.Host { + a.MessageCallbacks.Trace("GitCredentialsFileAuthProvider: host does not match") + continue + } + if gitUrl.User != nil && gitUrl.User.Username() != u.User.Username() { + a.MessageCallbacks.Trace("GitCredentialsFileAuthProvider: user does not match") + continue + } + password, ok := u.User.Password() + if !ok { + a.MessageCallbacks.Trace("GitCredentialsFileAuthProvider: no password provided") + continue + } + + a.MessageCallbacks.Trace("GitCredentialsFileAuthProvider: matched") + return &AuthMethodAndCA{ + AuthMethod: &http.BasicAuth{ + Username: u.User.Username(), + Password: password, + }, + } + } + return nil +} diff --git a/pkg/git/auth/goph/hosts.go b/lib/git/auth/goph/hosts.go similarity index 91% rename from pkg/git/auth/goph/hosts.go rename to lib/git/auth/goph/hosts.go index e2d9bec1e..10b8cdeb3 100644 --- a/pkg/git/auth/goph/hosts.go +++ b/lib/git/auth/goph/hosts.go @@ -96,14 +96,7 @@ func AddKnownHost(host string, remote net.Addr, key ssh.PublicKey, knownFile str defer f.Close() - remoteNormalized := knownhosts.Normalize(remote.String()) - hostNormalized := knownhosts.Normalize(host) - addresses := []string{remoteNormalized} - - if hostNormalized != remoteNormalized { - addresses = append(addresses, hostNormalized) - } - + addresses := []string{host} _, err = f.WriteString(knownhosts.Line(addresses, key) + "\n") return err diff --git a/pkg/git/auth/hash.go b/lib/git/auth/hash.go similarity index 100% rename from pkg/git/auth/hash.go rename to lib/git/auth/hash.go diff --git a/lib/git/auth/list_auth_provider.go b/lib/git/auth/list_auth_provider.go new file mode 100644 index 000000000..40706131e --- /dev/null +++ b/lib/git/auth/list_auth_provider.go @@ -0,0 +1,118 @@ +package auth + +import ( + "context" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "github.com/gobwas/glob" + "github.com/kluctl/kluctl/lib/git/messages" + "github.com/kluctl/kluctl/lib/git/types" + "strings" +) + +type ListAuthProvider struct { + MessageCallbacks messages.MessageCallbacks + + entries []AuthEntry +} + +type AuthEntry struct { + AllowWildcardHostForHttp bool + + Host string + PathGlob glob.Glob + PathStr string + + Username string + Password string + + SshKey []byte + KnownHosts []byte + + CABundle []byte +} + +func (a *ListAuthProvider) AddEntry(e AuthEntry) { + a.entries = append(a.entries, e) +} + +func (a *ListAuthProvider) BuildAuth(ctx context.Context, gitUrlIn types.GitUrl) (AuthMethodAndCA, error) { + gitUrl := gitUrlIn.Normalize() + + a.MessageCallbacks.Trace("ListAuthProvider: BuildAuth for %s", gitUrl.String()) + a.MessageCallbacks.Trace("ListAuthProvider: path=%s, username=%s, scheme=%s", gitUrl.Path, gitUrl.User.Username(), gitUrl.Scheme) + + for _, e := range a.entries { + a.MessageCallbacks.Trace("ListAuthProvider: try host=%s, path=%s, username=%s", e.Host, e.PathStr, e.Username) + + if !e.AllowWildcardHostForHttp && e.Host == "*" && !gitUrl.IsSsh() { + a.MessageCallbacks.Trace("ListAuthProvider: wildcard hosts are not allowed for http urls") + continue + } + + if e.Host != "*" && e.Host != gitUrl.Host { + continue + } + if e.PathGlob != nil { + urlPath := strings.TrimPrefix(gitUrl.Path, "/") + if !e.PathGlob.Match(urlPath) { + continue + } + } + if e.Username == "" { + continue + } + + username := "" + if gitUrl.User != nil { + username = gitUrl.User.Username() + } + + if username != "" && e.Username != "*" && username != e.Username { + continue + } + + if username == "" { + username = e.Username + } + + if username == "*" { + // can't use "*" as username + continue + } + + if gitUrl.IsSsh() { + if e.SshKey == nil { + a.MessageCallbacks.Trace("ListAuthProvider: empty ssh key is not accepted") + continue + } + a.MessageCallbacks.Trace("ListAuthProvider: using username+sshKey") + pk, err := ssh.NewPublicKeys(username, e.SshKey, "") + if err != nil { + a.MessageCallbacks.Trace("ListAuthProvider: failed to parse private key: %v", err) + } else { + pk.HostKeyCallback = buildVerifyHostCallback(a.MessageCallbacks, e.KnownHosts) + return AuthMethodAndCA{ + AuthMethod: pk, + Hash: func() ([]byte, error) { + return buildHash(pk.Signer) + }, + }, nil + } + } else { + if e.Password == "" { + a.MessageCallbacks.Trace("ListAuthProvider: empty password is not accepted") + continue + } + a.MessageCallbacks.Trace("ListAuthProvider: using username+password") + return AuthMethodAndCA{ + AuthMethod: &http.BasicAuth{ + Username: username, + Password: e.Password, + }, + CABundle: e.CABundle, + }, nil + } + } + return AuthMethodAndCA{}, nil +} diff --git a/pkg/git/auth/ssh_auth_provider.go b/lib/git/auth/ssh_auth_provider.go similarity index 62% rename from pkg/git/auth/ssh_auth_provider.go rename to lib/git/auth/ssh_auth_provider.go index 9c347192b..7e5465db5 100644 --- a/pkg/git/auth/ssh_auth_provider.go +++ b/lib/git/auth/ssh_auth_provider.go @@ -6,20 +6,23 @@ import ( "encoding/binary" "fmt" "github.com/kevinburke/ssh_config" - git_url "github.com/kluctl/kluctl/v2/pkg/git/git-url" - "github.com/kluctl/kluctl/v2/pkg/status" - "github.com/kluctl/kluctl/v2/pkg/utils" + "github.com/kluctl/kluctl/lib/git/messages" + "github.com/kluctl/kluctl/lib/git/types" sshagent "github.com/xanzy/ssh-agent" "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" "io" - "io/ioutil" + "net" "os" "os/user" "path/filepath" + "runtime" "sync" ) type GitSshAuthProvider struct { + MessageCallbacks messages.MessageCallbacks + passphrases map[string][]byte passphrasesMutex sync.Mutex } @@ -45,7 +48,7 @@ func (a *sshDefaultIdentityAndAgent) ClientConfig() (*ssh.ClientConfig, error) { User: a.user, Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(a.Signers)}, } - cc.HostKeyCallback = buildVerifyHostCallback(a.ctx, nil) + cc.HostKeyCallback = buildVerifyHostCallback(a.authProvider.MessageCallbacks, nil) return cc, nil } @@ -53,60 +56,100 @@ func (a *sshDefaultIdentityAndAgent) Signers() ([]ssh.Signer, error) { return a.signers, nil } -func (a *sshDefaultIdentityAndAgent) addDefaultIdentity(gitUrl git_url.GitUrl) { - status.Trace(a.ctx, "trying to add default identity") +func (a *sshDefaultIdentityAndAgent) addDefaultIdentities(gitUrl types.GitUrl) { + a.authProvider.MessageCallbacks.Trace("trying to add default identity") u, err := user.Current() if err != nil { - status.Trace(a.ctx, "No current user: %v", err) - } else { - path := filepath.Join(u.HomeDir, ".ssh", "id_rsa") + a.authProvider.MessageCallbacks.Trace("No current user: %v", err) + return + } + + doAdd := func(name string) { + path := filepath.Join(u.HomeDir, ".ssh", name) signer, err := a.authProvider.readKey(a.ctx, path) if err != nil && !os.IsNotExist(err) { - status.Warning(a.ctx, "Failed to read default identity file for url %s: %v", gitUrl.String(), err) + a.authProvider.MessageCallbacks.Warning("Failed to read default identity file for url %s: %v", gitUrl.String(), err) } else if signer != nil { - status.Trace(a.ctx, "...added '%s' as default identity", path) + a.authProvider.MessageCallbacks.Trace("...added '%s' as default identity", path) a.signers = append(a.signers, signer) } } + + doAdd("id_rsa") + doAdd("id_ecdsa") + doAdd("id_ed25519") + doAdd("id_xmss") + doAdd("id_dsa") } -func (a *sshDefaultIdentityAndAgent) addConfigIdentities(gitUrl git_url.GitUrl) { - status.Trace(a.ctx, "trying to add identities from ssh config") +func (a *sshDefaultIdentityAndAgent) addConfigIdentities(gitUrl types.GitUrl) { + a.authProvider.MessageCallbacks.Trace("trying to add identities from ssh config") for _, id := range ssh_config.GetAll(gitUrl.Hostname(), "IdentityFile") { - expanded := utils.ExpandPath(id) - status.Trace(a.ctx, "...trying '%s' (expanded: '%s')", id, expanded) + expanded := expandHomeDir(id) + a.authProvider.MessageCallbacks.Trace("...trying '%s' (expanded: '%s')", id, expanded) signer, err := a.authProvider.readKey(a.ctx, expanded) if err != nil && !os.IsNotExist(err) { - status.Warning(a.ctx, "Failed to read key %s for url %s: %v", id, gitUrl.String(), err) + a.authProvider.MessageCallbacks.Warning("Failed to read key %s for url %s: %v", id, gitUrl.String(), err) } else if err == nil { - status.Trace(a.ctx, "...added '%s' from ssh config", expanded) + a.authProvider.MessageCallbacks.Trace("...added '%s' from ssh config", expanded) a.signers = append(a.signers, signer) } } } -func (a *sshDefaultIdentityAndAgent) addAgentIdentities(gitUrl git_url.GitUrl) { - status.Trace(a.ctx, "trying to add agent keys") - agent, _, err := sshagent.New() +func (a *sshDefaultIdentityAndAgent) createAgent(gitUrl types.GitUrl) (agent.Agent, error) { + if runtime.GOOS == "windows" { + a, _, err := sshagent.New() + return a, err + } + + // see `man ssh_config` + + sshAuthSock := ssh_config.Get(gitUrl.Hostname(), "IdentityAgent") + if sshAuthSock == "none" { + return nil, nil + } + if sshAuthSock == "" || sshAuthSock == "SSH_AUTH_SOCK" { + a, _, err := sshagent.New() + return a, err + } + + sshAuthSock = os.ExpandEnv(sshAuthSock) + sshAuthSock = expandHomeDir(sshAuthSock) + + conn, err := net.Dial("unix", sshAuthSock) + if err != nil { + return nil, fmt.Errorf("error connecting to unix socket: %v", err) + } + + return agent.NewClient(conn), nil +} + +func (a *sshDefaultIdentityAndAgent) addAgentIdentities(gitUrl types.GitUrl) { + a.authProvider.MessageCallbacks.Trace("trying to add agent keys") + agent, err := a.createAgent(gitUrl) if err != nil { - status.Warning(a.ctx, "Failed to connect to ssh agent for url %s: %v", gitUrl.String(), err) + a.authProvider.MessageCallbacks.Warning("Failed to connect to ssh agent for url %s: %v", gitUrl.String(), err) } else { + if agent == nil { + return + } signers, err := agent.Signers() if err != nil { - status.Warning(a.ctx, "Failed to get signers from ssh agent for url %s: %v", gitUrl.String(), err) + a.authProvider.MessageCallbacks.Warning("Failed to get signers from ssh agent for url %s: %v", gitUrl.String(), err) return } - status.Trace(a.ctx, "...added %d agent keys", len(signers)) + a.authProvider.MessageCallbacks.Trace("...added %d agent keys", len(signers)) a.signers = append(a.signers, signers...) } } -func (a *GitSshAuthProvider) BuildAuth(ctx context.Context, gitUrl git_url.GitUrl) AuthMethodAndCA { +func (a *GitSshAuthProvider) BuildAuth(ctx context.Context, gitUrl types.GitUrl) (AuthMethodAndCA, error) { if !gitUrl.IsSsh() { - return AuthMethodAndCA{} + return AuthMethodAndCA{}, nil } if gitUrl.User == nil { - return AuthMethodAndCA{} + return AuthMethodAndCA{}, nil } auth := &sshDefaultIdentityAndAgent{ @@ -118,7 +161,7 @@ func (a *GitSshAuthProvider) BuildAuth(ctx context.Context, gitUrl git_url.GitUr // Try agent identities first. They might be unencrypted already, making passphrase prompts unnecessary auth.addAgentIdentities(gitUrl) - auth.addDefaultIdentity(gitUrl) + auth.addDefaultIdentities(gitUrl) auth.addConfigIdentities(gitUrl) return AuthMethodAndCA{ @@ -130,8 +173,7 @@ func (a *GitSshAuthProvider) BuildAuth(ctx context.Context, gitUrl git_url.GitUr } return buildHashForList(signers) }, - ClientConfig: auth.ClientConfig, - } + }, nil } // we defer asking for passphrase so that we don't unnecessarily ask for passphrases for keys that are already provided @@ -158,7 +200,7 @@ func (k *deferredPassphraseKey) getPassphrase() ([]byte, error) { return passphrase, nil } - passphraseStr, err := status.AskForPassword(k.ctx, fmt.Sprintf("Enter passphrase for key '%s'", k.path)) + passphraseStr, err := k.a.MessageCallbacks.AskForPassword(fmt.Sprintf("Enter passphrase for key '%s'", k.path)) if err != nil { k.a.passphrases[k.path] = nil return nil, err @@ -172,21 +214,21 @@ func (k *deferredPassphraseKey) parse() { passphrase, err := k.getPassphrase() if err != nil { k.err = err - status.Warning(k.ctx, "Failed to parse key %s: %v", k.path, err) + k.a.MessageCallbacks.Warning("Failed to parse key %s: %v", k.path, err) return } - pemBytes, err := ioutil.ReadFile(k.path) + pemBytes, err := os.ReadFile(k.path) if err != nil { k.err = err - status.Warning(k.ctx, "Failed to parse key %s: %v", k.path, err) + k.a.MessageCallbacks.Warning("Failed to parse key %s: %v", k.path, err) return } s, err := ssh.ParsePrivateKeyWithPassphrase(pemBytes, passphrase) if err != nil { k.err = err - status.Warning(k.ctx, "Failed to parse key %s: %v", k.path, err) + k.a.MessageCallbacks.Warning("Failed to parse key %s: %v", k.path, err) return } k.parsed = s @@ -213,7 +255,7 @@ func (k *dummyPublicKey) Verify(data []byte, sig *ssh.Signature) error { } func (k *deferredPassphraseKey) Hash() ([]byte, error) { - pemBytes, err := ioutil.ReadFile(k.path) + pemBytes, err := os.ReadFile(k.path) if err != nil { return nil, err } @@ -252,7 +294,7 @@ func (k *deferredPassphraseKey) Sign(rand io.Reader, data []byte) (*ssh.Signatur } func (a *GitSshAuthProvider) readKey(ctx context.Context, path string) (ssh.Signer, error) { - pemBytes, err := ioutil.ReadFile(path) + pemBytes, err := os.ReadFile(path) if err != nil { return nil, err } else { diff --git a/pkg/git/auth/ssh_known_hosts.go b/lib/git/auth/ssh_known_hosts.go similarity index 52% rename from pkg/git/auth/ssh_known_hosts.go rename to lib/git/auth/ssh_known_hosts.go index ec5df2966..9428a56f4 100644 --- a/pkg/git/auth/ssh_known_hosts.go +++ b/lib/git/auth/ssh_known_hosts.go @@ -1,12 +1,12 @@ package auth import ( - "context" + "errors" "fmt" - "github.com/kluctl/kluctl/v2/pkg/git/auth/goph" - "github.com/kluctl/kluctl/v2/pkg/status" - "github.com/kluctl/kluctl/v2/pkg/utils" + "github.com/kluctl/kluctl/lib/git/auth/goph" + "github.com/kluctl/kluctl/lib/git/messages" "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" "net" "os" "path/filepath" @@ -16,13 +16,13 @@ import ( var askHostMutex sync.Mutex -func buildVerifyHostCallback(ctx context.Context, knownHosts []byte) func(hostname string, remote net.Addr, key ssh.PublicKey) error { +func buildVerifyHostCallback(messageCallbacks messages.MessageCallbacks, knownHosts []byte) func(hostname string, remote net.Addr, key ssh.PublicKey) error { return func(hostname string, remote net.Addr, key ssh.PublicKey) error { - return verifyHost(ctx, hostname, remote, key, knownHosts) + return verifyHost(messageCallbacks, hostname, remote, key, knownHosts) } } -func verifyHost(ctx context.Context, host string, remote net.Addr, key ssh.PublicKey, knownHosts []byte) error { +func verifyHost(messageCallbacks messages.MessageCallbacks, host string, remote net.Addr, key ssh.PublicKey, knownHosts []byte) error { // Ensure only one prompt happens at a time askHostMutex.Lock() defer askHostMutex.Unlock() @@ -48,7 +48,7 @@ func verifyHost(ctx context.Context, host string, remote net.Addr, key ssh.Publi } f := filepath.Join(home, ".ssh", "known_hosts") - if !utils.Exists(filepath.Dir(f)) { + if _, err := os.Stat(filepath.Dir(f)); err != nil { err = os.MkdirAll(filepath.Dir(f), 0o700) if err != nil { return err @@ -59,7 +59,7 @@ func verifyHost(ctx context.Context, host string, remote net.Addr, key ssh.Publi allowAdd = true } } else { - tmpFile, err := os.CreateTemp(utils.GetTmpBaseDir(), "known_hosts-") + tmpFile, err := os.CreateTemp("", "known_hosts-") if err != nil { return err } @@ -74,11 +74,26 @@ func verifyHost(ctx context.Context, host string, remote net.Addr, key ssh.Publi files = append(files, tmpFile.Name()) } + if key.Type() == "fake-public-key" { + // this makes us compatible to knownhosts.HostKeyAlgorithms which calls us with a fake public key and expects us + // to return all known keys + var keyErr knownhosts.KeyError + for _, f := range files { + hostFound, err := goph.CheckKnownHost(host, remote, key, f) + if hostFound && err == nil { + return fmt.Errorf("fake-public-key was unexpectadly found") + } + var tmpKeyErr *knownhosts.KeyError + if !errors.As(err, &tmpKeyErr) { + return fmt.Errorf("CheckKnownHost did not return expected KeyError: %v", err) + } + keyErr.Want = append(keyErr.Want, tmpKeyErr.Want...) + } + return &keyErr + } + for _, f := range files { hostFound, err := goph.CheckKnownHost(host, remote, key, f) - if hostFound && err != nil { - return err - } if hostFound && err == nil { return nil } @@ -87,14 +102,14 @@ func verifyHost(ctx context.Context, host string, remote net.Addr, key ssh.Publi return fmt.Errorf("host not found and SSH_KNOWN_HOSTS has been set") } - if !askIsHostTrusted(ctx, host, key) { + if !askIsHostTrusted(messageCallbacks, host, key) { return fmt.Errorf("aborted") } return goph.AddKnownHost(host, remote, key, "") } -func askIsHostTrusted(ctx context.Context, host string, key ssh.PublicKey) bool { +func askIsHostTrusted(messageCallbacks messages.MessageCallbacks, host string, key ssh.PublicKey) bool { prompt := fmt.Sprintf("Unknown Host: %s\nFingerprint: %s\nWould you like to add it? ", host, ssh.FingerprintSHA256(key)) - return status.AskForConfirmation(ctx, prompt) + return messageCallbacks.AskForConfirmation(prompt) } diff --git a/lib/git/auth/utils.go b/lib/git/auth/utils.go new file mode 100644 index 000000000..ff0c9e834 --- /dev/null +++ b/lib/git/auth/utils.go @@ -0,0 +1,13 @@ +package auth + +import ( + "k8s.io/client-go/util/homedir" + "strings" +) + +func expandHomeDir(p string) string { + if strings.HasPrefix(p, "~/") { + p = homedir.HomeDir() + p[1:] + } + return p +} diff --git a/lib/git/build_git_info.go b/lib/git/build_git_info.go new file mode 100644 index 000000000..af04695b5 --- /dev/null +++ b/lib/git/build_git_info.go @@ -0,0 +1,89 @@ +package git + +import ( + "context" + "errors" + git2 "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/kluctl/kluctl/lib/git/types" + "os" + "path/filepath" +) + +func BuildGitInfo(ctx context.Context, repoRoot string, projectDir string) (types.GitInfo, types.ProjectKey, error) { + var gitInfo types.GitInfo + var projectKey types.ProjectKey + if repoRoot == "" { + return gitInfo, projectKey, nil + } + if _, err := os.Stat(filepath.Join(repoRoot, ".git")); os.IsNotExist(err) { + return gitInfo, projectKey, nil + } + + projectDirAbs, err := filepath.Abs(projectDir) + if err != nil { + return gitInfo, projectKey, err + } + + subDir, err := filepath.Rel(repoRoot, projectDirAbs) + if err != nil { + return gitInfo, projectKey, err + } + if subDir == "." { + subDir = "" + } + + g, err := git2.PlainOpen(repoRoot) + if err != nil { + return gitInfo, projectKey, err + } + + s, err := GetWorktreeStatus(ctx, repoRoot) + if err != nil { + return gitInfo, projectKey, err + } + + remotes, err := g.Remotes() + if err != nil { + return gitInfo, projectKey, err + } + + var originUrl *types.GitUrl + for _, r := range remotes { + if r.Config().Name == "origin" { + originUrl, err = types.ParseGitUrl(r.Config().URLs[0]) + if err != nil { + return gitInfo, projectKey, err + } + } + } + + gitInfo = types.GitInfo{ + Url: originUrl, + SubDir: subDir, + Dirty: !s.IsClean(), + } + + head, err := g.Head() + if err == nil { + gitInfo.Commit = head.Hash().String() + if head.Name().IsBranch() { + gitInfo.Ref = &types.GitRef{ + Branch: head.Name().Short(), + } + } else if head.Name().IsTag() { + gitInfo.Ref = &types.GitRef{ + Tag: head.Name().Short(), + } + } + } else if !errors.Is(err, plumbing.ErrReferenceNotFound) { + return gitInfo, projectKey, err + } + + if originUrl != nil { + projectKey.RepoKey = originUrl.RepoKey() + } + projectKey.SubDir = subDir + + return gitInfo, projectKey, nil +} diff --git a/lib/git/list_refs.go b/lib/git/list_refs.go new file mode 100644 index 000000000..dd9674757 --- /dev/null +++ b/lib/git/list_refs.go @@ -0,0 +1,104 @@ +package git + +import ( + "bytes" + "context" + "fmt" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/protocol/packp" + "github.com/go-git/go-git/v5/storage/memory" + auth2 "github.com/kluctl/kluctl/lib/git/auth" + ssh_pool "github.com/kluctl/kluctl/lib/git/ssh-pool" + "github.com/kluctl/kluctl/lib/git/types" + "strconv" +) + +// ListRemoteRefsFastSsh will reuse existing ssh connections from a pool +func ListRemoteRefsFastSsh(ctx context.Context, url types.GitUrl, sshPool *ssh_pool.SshPool, auth auth2.AuthMethodAndCA) ([]*plumbing.Reference, error) { + var portInt int64 = 22 + if url.Port() != "" { + var err error + portInt, err = strconv.ParseInt(url.Port(), 10, 32) + if err != nil { + return nil, err + } + } + + s, err := sshPool.GetSession(ctx, url.Hostname(), int(portInt), auth) + if err != nil { + return nil, err + } + defer s.Close() + + cmd := fmt.Sprintf("git-upload-pack %s", url.Path) + + stdout := bytes.NewBuffer(nil) + stderr := bytes.NewBuffer(nil) + stdin := bytes.NewBuffer([]byte("0000\n")) + + s.Session.Stdout = stdout + s.Session.Stderr = stderr + s.Session.Stdin = stdin + + err = s.Session.Run(cmd) + if err != nil { + return nil, fmt.Errorf("git-upload-pack failed: %w\nstderr=%s", err, stderr.String()) + } + + ar := packp.NewAdvRefs() + err = ar.Decode(stdout) + if err != nil { + return nil, err + } + + allRefs, err := ar.AllReferences() + if err != nil { + return nil, err + } + + refs, err := allRefs.IterReferences() + if err != nil { + return nil, err + } + + var resultRefs []*plumbing.Reference + err = refs.ForEach(func(ref *plumbing.Reference) error { + resultRefs = append(resultRefs, ref) + return nil + }) + if err != nil { + return nil, err + } + + return resultRefs, nil +} + +func ListRemoteRefsSlow(ctx context.Context, url types.GitUrl, auth auth2.AuthMethodAndCA) ([]*plumbing.Reference, error) { + storage := memory.NewStorage() + remote := git.NewRemote(storage, &config.RemoteConfig{ + Name: "origin", + URLs: []string{url.String()}, + Fetch: defaultFetch, + }) + + remoteRefs, err := remote.ListContext(ctx, &git.ListOptions{ + Auth: auth.AuthMethod, + CABundle: auth.CABundle, + }) + if err != nil { + return nil, err + } + return remoteRefs, nil +} + +func ListRemoteRefs(ctx context.Context, url types.GitUrl, sshPool *ssh_pool.SshPool, auth auth2.AuthMethodAndCA) ([]*plumbing.Reference, error) { + if url.IsSsh() { + refs, err := ListRemoteRefsFastSsh(ctx, url, sshPool, auth) + if err == nil { + return refs, nil + } + } + return ListRemoteRefsSlow(ctx, url, auth) +} diff --git a/lib/git/messages/message_callbacks.go b/lib/git/messages/message_callbacks.go new file mode 100644 index 000000000..0ec2839bc --- /dev/null +++ b/lib/git/messages/message_callbacks.go @@ -0,0 +1,39 @@ +package messages + +import "fmt" + +type MessageCallbacks struct { + WarningFn func(s string) + TraceFn func(s string) + AskForPasswordFn func(prompt string) (string, error) + AskForConfirmationFn func(prompt string) bool +} + +func (l MessageCallbacks) Warning(s string, args ...any) { + if l.WarningFn != nil { + l.WarningFn(fmt.Sprintf(s, args...)) + } +} + +func (l MessageCallbacks) Trace(s string, args ...any) { + if l.TraceFn != nil { + l.TraceFn(fmt.Sprintf(s, args...)) + } +} + +func (l MessageCallbacks) AskForPassword(prompt string) (string, error) { + if l.AskForPasswordFn != nil { + return l.AskForPasswordFn(prompt) + } + err := fmt.Errorf("AskForPasswordFn not provided, skipping prompt: %s", prompt) + l.Warning("%s", err.Error()) + return "", err +} + +func (l MessageCallbacks) AskForConfirmation(prompt string) bool { + if l.AskForConfirmationFn != nil { + return l.AskForConfirmationFn(prompt) + } + l.Warning("Not a terminal, suppressed prompt: %s", prompt) + return false +} diff --git a/pkg/git/mirrored_repo.go b/lib/git/mirrored_repo.go similarity index 63% rename from pkg/git/mirrored_repo.go rename to lib/git/mirrored_repo.go index 3c3a271c1..b55df5569 100644 --- a/pkg/git/mirrored_repo.go +++ b/lib/git/mirrored_repo.go @@ -2,19 +2,21 @@ package git import ( "context" + "crypto/sha256" + "encoding/hex" "fmt" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" - auth2 "github.com/kluctl/kluctl/v2/pkg/git/auth" - git_url "github.com/kluctl/kluctl/v2/pkg/git/git-url" - _ "github.com/kluctl/kluctl/v2/pkg/git/ssh-pool" - ssh_pool "github.com/kluctl/kluctl/v2/pkg/git/ssh-pool" - "github.com/kluctl/kluctl/v2/pkg/status" - "github.com/kluctl/kluctl/v2/pkg/utils" + "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" + "github.com/go-git/go-git/v5/plumbing/transport" + auth2 "github.com/kluctl/kluctl/lib/git/auth" + _ "github.com/kluctl/kluctl/lib/git/ssh-pool" + ssh_pool "github.com/kluctl/kluctl/lib/git/ssh-pool" + "github.com/kluctl/kluctl/lib/git/types" + "github.com/kluctl/kluctl/lib/status" "github.com/rogpeppe/go-internal/lockedfile" - "io/ioutil" "os" "path/filepath" "strings" @@ -22,15 +24,31 @@ import ( "time" ) -var cacheBaseDir = filepath.Join(utils.GetTmpBaseDir(), "git-cache") +func init() { + // see https://github.com/go-git/go-git/pull/613 + old := transport.UnsupportedCapabilities + transport.UnsupportedCapabilities = nil + for _, c := range old { + if c == capability.MultiACK || c == capability.MultiACKDetailed { + continue + } + transport.UnsupportedCapabilities = append(transport.UnsupportedCapabilities, c) + } +} + +var defaultFetch = []config.RefSpec{ + "+refs/heads/*:refs/heads/*", + "+refs/tags/*:refs/tags/*", +} type MirroredGitRepo struct { ctx context.Context + baseDir string sshPool *ssh_pool.SshPool authProviders *auth2.GitAuthProviders - url git_url.GitUrl + url types.GitUrl mirrorDir string hasUpdated bool @@ -41,28 +59,32 @@ type MirroredGitRepo struct { mutex sync.Mutex } -func NewMirroredGitRepo(ctx context.Context, u git_url.GitUrl, sshPool *ssh_pool.SshPool, authProviders *auth2.GitAuthProviders) (*MirroredGitRepo, error) { +func NewMirroredGitRepo(ctx context.Context, u types.GitUrl, baseDir string, sshPool *ssh_pool.SshPool, authProviders *auth2.GitAuthProviders) (*MirroredGitRepo, error) { mirrorRepoName := buildMirrorRepoName(u) o := &MirroredGitRepo{ ctx: ctx, + baseDir: baseDir, sshPool: sshPool, authProviders: authProviders, url: u, - mirrorDir: filepath.Join(cacheBaseDir, mirrorRepoName), + mirrorDir: filepath.Join(baseDir, mirrorRepoName), } - if !utils.IsDirectory(o.mirrorDir) { - err := os.MkdirAll(o.mirrorDir, 0o700) + st, err := os.Stat(o.mirrorDir) + if err != nil { + err = os.MkdirAll(o.mirrorDir, 0o700) if err != nil { return nil, fmt.Errorf("failed to create mirror repo for %v: %w", u.String(), err) } + } else if !st.IsDir() { + return nil, fmt.Errorf("%s is not a directory", o.mirrorDir) } o.fileLockPath = filepath.Join(o.mirrorDir, ".cache.lock") return o, nil } -func (g *MirroredGitRepo) Url() git_url.GitUrl { +func (g *MirroredGitRepo) Url() types.GitUrl { return g.url } @@ -101,7 +123,6 @@ func (g *MirroredGitRepo) Unlock() error { err := g.fileLock.Close() if err != nil { - status.Warning(g.ctx, "Unlock of %s failed: %v", g.fileLockPath, err) return err } g.fileLock = nil @@ -115,7 +136,7 @@ func (g *MirroredGitRepo) IsLocked() bool { } func (g *MirroredGitRepo) LastUpdateTime() time.Time { - s, err := ioutil.ReadFile(filepath.Join(g.mirrorDir, ".update-time")) + s, err := os.ReadFile(filepath.Join(g.mirrorDir, ".update-time")) if err != nil { return time.Time{} } @@ -177,8 +198,9 @@ func (g *MirroredGitRepo) buildRepositoryObject() (*git.Repository, error) { } func (g *MirroredGitRepo) cleanupMirrorDir() error { - if utils.IsDirectory(g.mirrorDir) { - files, err := ioutil.ReadDir(g.mirrorDir) + st, err := os.Stat(g.mirrorDir) + if err == nil && st.IsDir() { + files, err := os.ReadDir(g.mirrorDir) if err != nil { return err } @@ -192,15 +214,18 @@ func (g *MirroredGitRepo) cleanupMirrorDir() error { return nil } -func (g *MirroredGitRepo) update(s *status.StatusContext, repoDir string) error { +func (g *MirroredGitRepo) update(repoDir string) error { r, err := git.PlainOpen(repoDir) if err != nil { return err } - auth := g.authProviders.BuildAuth(g.ctx, g.url) + auth, err := g.authProviders.BuildAuth(g.ctx, g.url) + if err != nil { + return err + } - remoteRefs, err := g.listRemoteRefs(r, auth) + remoteRefs, err := ListRemoteRefs(g.ctx, g.url, g.sshPool, auth) if err != nil { return err } @@ -245,11 +270,17 @@ func (g *MirroredGitRepo) update(s *status.StatusContext, repoDir string) error if err != nil { return err } - err = remote.FetchContext(g.ctx, &git.FetchOptions{ - Auth: auth.AuthMethod, - CABundle: auth.CABundle, - Tags: git.AllTags, - Force: true, + + // go-git does not respect the context deadline in some situations, especially after errors occur internally. + // This leads to hanging fetches, which can easily deadlock the whole kluctl process. The only way to handle + // this currently is to panic when the deadline is exceeded too much. + err = RunWithDeadlineAndPanic(g.ctx, 5*time.Second, func() error { + return remote.FetchContext(g.ctx, &git.FetchOptions{ + Auth: auth.AuthMethod, + CABundle: auth.CABundle, + Tags: git.AllTags, + Force: true, + }) }) if err != nil && err != git.NoErrAlreadyUpToDate { return err @@ -275,22 +306,34 @@ func (g *MirroredGitRepo) update(s *status.StatusContext, repoDir string) error } } - _ = ioutil.WriteFile(filepath.Join(g.mirrorDir, ".update-time"), []byte(time.Now().Format(time.RFC3339Nano)), 0644) + _ = os.WriteFile(filepath.Join(g.mirrorDir, ".update-time"), []byte(time.Now().Format(time.RFC3339Nano)), 0644) return nil } -func (g *MirroredGitRepo) cloneOrUpdate(s *status.StatusContext) error { +func (g *MirroredGitRepo) cloneOrUpdate() error { initMarker := filepath.Join(g.mirrorDir, ".cache2.init") - if utils.IsFile(initMarker) { - return g.update(s, g.mirrorDir) + st, err := os.Stat(initMarker) + if err == nil && st.Mode().IsRegular() { + err = g.update(g.mirrorDir) + if err == nil { + return nil + } else if strings.Contains(err.Error(), "multi_ack") { + // looks like the server tried to do multi_ack/multi_ack_detailed, which is not supported. + // in that case, retry a full clone which does hopefully not rely on multi_ack. + // See https://github.com/go-git/go-git/pull/613 + // TODO remove this when https://github.com/go-git/go-git/issues/64 gets fully fixed + status.Tracef(g.ctx, "Got multi_ack related error from remote. Retrying full clone: %v", err) + } else { + return err + } } - err := g.cleanupMirrorDir() + err = g.cleanupMirrorDir() if err != nil { return err } - tmpMirrorDir, err := ioutil.TempDir(utils.GetTmpBaseDir(), "mirror-") + tmpMirrorDir, err := os.MkdirTemp(g.baseDir, "tmp-mirror-") if err != nil { return err } @@ -302,23 +345,20 @@ func (g *MirroredGitRepo) cloneOrUpdate(s *status.StatusContext) error { } _, err = repo.CreateRemote(&config.RemoteConfig{ - Name: "origin", - URLs: []string{g.url.String()}, - Fetch: []config.RefSpec{ - "+refs/heads/*:refs/heads/*", - "+refs/tags/*:refs/tags/*", - }, + Name: "origin", + URLs: []string{g.url.String()}, + Fetch: defaultFetch, }) if err != nil { return err } - err = g.update(s, tmpMirrorDir) + err = g.update(tmpMirrorDir) if err != nil { return err } - files, err := ioutil.ReadDir(tmpMirrorDir) + files, err := os.ReadDir(tmpMirrorDir) if err != nil { return err } @@ -328,22 +368,20 @@ func (g *MirroredGitRepo) cloneOrUpdate(s *status.StatusContext) error { return err } } - err = utils.Touch(initMarker) + f, err := os.Create(initMarker) if err != nil { return err } + defer f.Close() return nil } func (g *MirroredGitRepo) Update() error { - s := status.Start(g.ctx, "Updating git cache for %s", g.url.String()) - err := g.cloneOrUpdate(s) + err := g.cloneOrUpdate() if err != nil { - s.FailedWithMessage(err.Error()) return err } g.hasUpdated = true - s.Success() return nil } @@ -352,8 +390,6 @@ func (g *MirroredGitRepo) CloneProjectByCommit(commit string, targetDir string) panic("tried to clone from a project that is not locked/updated") } - status.Trace(g.ctx, "Cloning git project: url='%s', commit='%s', target='%s'", g.url.String(), commit, targetDir) - err := PoorMansClone(g.mirrorDir, targetDir, &git.CheckoutOptions{Hash: plumbing.NewHash(commit)}) if err != nil { return fmt.Errorf("failed to clone %s from %s: %w", commit, g.url.String(), err) @@ -388,13 +424,31 @@ func (g *MirroredGitRepo) GetGitTreeByCommit(commitHash string) (*object.Tree, e return tree, nil } -func buildMirrorRepoName(u git_url.GitUrl) string { +func (g *MirroredGitRepo) GetObjectByHash(hash string) (object.Object, error) { + if !g.IsLocked() || !g.hasUpdated { + panic("tried to read a file from a project that is not locked/updated") + } + + r, err := git.PlainOpen(g.mirrorDir) + if err != nil { + return nil, err + } + + h := plumbing.NewHash(hash) + return object.GetObject(r.Storer, h) +} + +func buildMirrorRepoName(u types.GitUrl) string { + h := sha256.New() + h.Write([]byte(u.String())) + h2 := hex.EncodeToString(h.Sum(nil)) + r := filepath.Base(u.Path) r = strings.ReplaceAll(r, "/", "-") r = strings.ReplaceAll(r, "\\", "-") - if strings.HasSuffix(r, ".git") { + if r != ".git" && strings.HasSuffix(r, ".git") { r = r[:len(r)-len(".git")] } - r += "-" + utils.Sha256String(u.String())[:6] + r += "-" + h2[:6] return r } diff --git a/pkg/git/poor_mans_clone.go b/lib/git/poor_mans_clone.go similarity index 81% rename from pkg/git/poor_mans_clone.go rename to lib/git/poor_mans_clone.go index d5216cb49..975112564 100644 --- a/pkg/git/poor_mans_clone.go +++ b/lib/git/poor_mans_clone.go @@ -3,8 +3,7 @@ package git import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" - "github.com/kluctl/kluctl/v2/pkg/utils" - "io/ioutil" + cp "github.com/otiai10/copy" "os" "path/filepath" "runtime" @@ -33,15 +32,13 @@ func PoorMansClone(sourceDir string, targetDir string, coOptions *git.CheckoutOp if de.Name() == "objects" { err = os.Symlink(s, d) if err != nil && runtime.GOOS == "windows" { - // windows 10 does not support symlinks as unprivileged users... - err = utils.CopyDir(s, d) + // Windows 10 does not support symlinks as unprivileged users, so we revert to deep copying + err = cp.Copy(s, d, cp.Options{OnSymlink: func(src string) cp.SymlinkAction { + return cp.Deep + }}) } } else { - if de.IsDir() { - err = utils.CopyDir(s, d) - } else { - err = utils.CopyFile(s, d) - } + err = cp.Copy(s, d) } if err != nil { return err @@ -63,7 +60,7 @@ func PoorMansClone(sourceDir string, targetDir string, coOptions *git.CheckoutOp if err != nil { return err } - err = ioutil.WriteFile(filepath.Join(targetDir, ".git", "config"), b, 0o600) + err = os.WriteFile(filepath.Join(targetDir, ".git", "config"), b, 0o600) if err != nil { return err } diff --git a/lib/git/sourceignore/sourceignore.go b/lib/git/sourceignore/sourceignore.go new file mode 100644 index 000000000..999f21ea0 --- /dev/null +++ b/lib/git/sourceignore/sourceignore.go @@ -0,0 +1,131 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sourceignore + +import ( + "bufio" + "io" + "os" + "path/filepath" + "strings" + + "github.com/go-git/go-git/v5/plumbing/format/gitignore" +) + +const ( + IgnoreFile = ".sourceignore" + ExcludeVCS = ".git/,.gitignore,.gitmodules,.gitattributes" + ExcludeExt = "*.jpg,*.jpeg,*.gif,*.png,*.wmv,*.flv,*.tar.gz,*.zip" + ExcludeCI = ".github/,.circleci/,.travis.yml,.gitlab-ci.yml,appveyor.yml,.drone.yml,cloudbuild.yaml,codeship-services.yml,codeship-steps.yml" + ExcludeExtra = "**/.goreleaser.yml,**/.sops.yaml,**/.flux.yaml" +) + +// NewMatcher returns a gitignore.Matcher for the given gitignore.Pattern +// slice. It mainly exists to compliment the API. +func NewMatcher(ps []gitignore.Pattern) gitignore.Matcher { + return gitignore.NewMatcher(ps) +} + +// NewDefaultMatcher returns a gitignore.Matcher with the DefaultPatterns +// as lowest priority patterns. +func NewDefaultMatcher(ps []gitignore.Pattern, domain []string) gitignore.Matcher { + var defaultPs []gitignore.Pattern + defaultPs = append(defaultPs, VCSPatterns(domain)...) + defaultPs = append(defaultPs, DefaultPatterns(domain)...) + ps = append(defaultPs, ps...) + return gitignore.NewMatcher(ps) +} + +// VCSPatterns returns a gitignore.Pattern slice with ExcludeVCS +// patterns. +func VCSPatterns(domain []string) []gitignore.Pattern { + var ps []gitignore.Pattern + for _, p := range strings.Split(ExcludeVCS, ",") { + ps = append(ps, gitignore.ParsePattern(p, domain)) + } + return ps +} + +// DefaultPatterns returns a gitignore.Pattern slice with the default +// ExcludeExt, ExcludeCI, ExcludeExtra patterns. +func DefaultPatterns(domain []string) []gitignore.Pattern { + all := strings.Join([]string{ExcludeExt, ExcludeCI, ExcludeExtra}, ",") + var ps []gitignore.Pattern + for _, p := range strings.Split(all, ",") { + ps = append(ps, gitignore.ParsePattern(p, domain)) + } + return ps +} + +// ReadPatterns collects ignore patterns from the given reader and +// returns them as a gitignore.Pattern slice. +// If a domain is supplied, this is used as the scope of the read +// patterns. +func ReadPatterns(reader io.Reader, domain []string) []gitignore.Pattern { + var ps []gitignore.Pattern + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + s := scanner.Text() + if !strings.HasPrefix(s, "#") && len(strings.TrimSpace(s)) > 0 { + ps = append(ps, gitignore.ParsePattern(s, domain)) + } + } + return ps +} + +// ReadIgnoreFile attempts to read the file at the given path and +// returns the read patterns. +func ReadIgnoreFile(path string, domain []string) ([]gitignore.Pattern, error) { + var ps []gitignore.Pattern + if f, err := os.Open(path); err == nil { + defer f.Close() + ps = append(ps, ReadPatterns(f, domain)...) + } else if !os.IsNotExist(err) { + return nil, err + } + return ps, nil +} + +// LoadIgnorePatterns recursively loads the IgnoreFile patterns found +// in the directory. +func LoadIgnorePatterns(dir string, domain []string, ignoreFile string) ([]gitignore.Pattern, error) { + // Make a copy of the domain so that the underlying string array of domain + // in the gitignore patterns are unique without any side effects. + dom := make([]string, len(domain)) + copy(dom, domain) + + ps, err := ReadIgnoreFile(filepath.Join(dir, ignoreFile), dom) + if err != nil { + return nil, err + } + fis, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, fi := range fis { + if fi.IsDir() && fi.Name() != ".git" { + var subps []gitignore.Pattern + if subps, err = LoadIgnorePatterns(filepath.Join(dir, fi.Name()), append(dom, fi.Name()), ignoreFile); err != nil { + return nil, err + } + if len(subps) > 0 { + ps = append(ps, subps...) + } + } + } + return ps, nil +} diff --git a/lib/git/sourceignore/sourceignore_test.go b/lib/git/sourceignore/sourceignore_test.go new file mode 100644 index 000000000..f03a74c2d --- /dev/null +++ b/lib/git/sourceignore/sourceignore_test.go @@ -0,0 +1,258 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sourceignore + +import ( + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/go-git/go-git/v5/plumbing/format/gitignore" + "gotest.tools/assert" +) + +func TestReadPatterns(t *testing.T) { + tests := []struct { + name string + ignore string + domain []string + matches []string + mismatches []string + }{ + { + name: "simple", + ignore: `ignore-dir/* +!ignore-dir/include +`, + matches: []string{"ignore-dir/file.yaml"}, + mismatches: []string{"file.yaml", "ignore-dir/include"}, + }, + { + name: "with comments", + ignore: `ignore-dir/* +# !ignore-dir/include`, + matches: []string{"ignore-dir/file.yaml", "ignore-dir/include"}, + }, + { + name: "domain scoped", + domain: []string{"domain", "scoped"}, + ignore: "ignore-dir/*", + matches: []string{"domain/scoped/ignore-dir/file.yaml"}, + mismatches: []string{"ignore-dir/file.yaml"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := strings.NewReader(tt.ignore) + ps := ReadPatterns(reader, tt.domain) + matcher := NewMatcher(ps) + for _, m := range tt.matches { + assert.Equal(t, matcher.Match(strings.Split(m, "/"), false), true, "expected %s to match", m) + } + for _, m := range tt.mismatches { + assert.Equal(t, matcher.Match(strings.Split(m, "/"), false), false, "expected %s to not match", m) + } + }) + } +} + +func TestReadIgnoreFile(t *testing.T) { + f, err := os.CreateTemp("", IgnoreFile) + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + if _, err = f.Write([]byte(`# .sourceignore +ignore-this.txt`)); err != nil { + t.Fatal(err) + } + f.Close() + + tests := []struct { + name string + path string + domain []string + want []gitignore.Pattern + }{ + { + name: IgnoreFile, + path: f.Name(), + want: []gitignore.Pattern{ + gitignore.ParsePattern("ignore-this.txt", nil), + }, + }, + { + name: "with domain", + path: f.Name(), + domain: strings.Split(filepath.Dir(f.Name()), string(filepath.Separator)), + want: []gitignore.Pattern{ + gitignore.ParsePattern("ignore-this.txt", strings.Split(filepath.Dir(f.Name()), string(filepath.Separator))), + }, + }, + { + name: "non existing", + path: "", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ReadIgnoreFile(tt.path, tt.domain) + if err != nil { + t.Error(err) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ReadIgnoreFile() got = %d, want %#v", got, tt.want) + } + }) + } +} + +func TestVCSPatterns(t *testing.T) { + tests := []struct { + name string + domain []string + patterns []gitignore.Pattern + matches []string + mismatches []string + }{ + { + name: "simple matches", + matches: []string{".git/config", ".gitignore"}, + mismatches: []string{"workload.yaml", "workload.yml", "simple.txt"}, + }, + { + name: "domain scoped matches", + domain: []string{"directory"}, + matches: []string{"directory/.git/config", "directory/.gitignore"}, + mismatches: []string{"other/.git/config"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matcher := NewDefaultMatcher(tt.patterns, tt.domain) + for _, m := range tt.matches { + assert.Equal(t, matcher.Match(strings.Split(m, "/"), false), true, "expected %s to match", m) + } + for _, m := range tt.mismatches { + assert.Equal(t, matcher.Match(strings.Split(m, "/"), false), false, "expected %s to not match", m) + } + }) + } +} + +func TestDefaultPatterns(t *testing.T) { + tests := []struct { + name string + domain []string + patterns []gitignore.Pattern + matches []string + mismatches []string + }{ + { + name: "simple matches", + matches: []string{"image.jpg", "archive.tar.gz", ".github/workflows/workflow.yaml", "subdir/.flux.yaml", "subdir2/.sops.yaml"}, + mismatches: []string{"workload.yaml", "workload.yml", "simple.txt"}, + }, + { + name: "domain scoped matches", + domain: []string{"directory"}, + matches: []string{"directory/image.jpg", "directory/archive.tar.gz"}, + mismatches: []string{"other/image.jpg", "other/archive.tar.gz"}, + }, + { + name: "patterns", + patterns: []gitignore.Pattern{gitignore.ParsePattern("!*.jpg", nil)}, + mismatches: []string{"image.jpg"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matcher := NewDefaultMatcher(tt.patterns, tt.domain) + for _, m := range tt.matches { + assert.Equal(t, matcher.Match(strings.Split(m, "/"), false), true, "expected %s to match", m) + } + for _, m := range tt.mismatches { + assert.Equal(t, matcher.Match(strings.Split(m, "/"), false), false, "expected %s to not match", m) + } + }) + } +} + +func TestLoadExcludePatterns(t *testing.T) { + tmpDir := t.TempDir() + files := map[string]string{ + ".sourceignore": "root.txt", + "d/.gitignore": "ignored", + "z/.sourceignore": "last.txt", + "a/b/.sourceignore": "subdir.txt", + "e/last.txt": "foo", + "a/c/subdir.txt": "bar", + } + for n, c := range files { + if err := os.MkdirAll(filepath.Join(tmpDir, filepath.Dir(n)), 0o750); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tmpDir, n), []byte(c), 0o640); err != nil { + t.Fatal(err) + } + } + tests := []struct { + name string + dir string + domain []string + want []gitignore.Pattern + }{ + { + name: "traverse loads", + dir: tmpDir, + want: []gitignore.Pattern{ + gitignore.ParsePattern("root.txt", []string{}), + gitignore.ParsePattern("subdir.txt", []string{"a", "b"}), + gitignore.ParsePattern("last.txt", []string{"z"}), + }, + }, + { + name: "domain", + dir: tmpDir, + domain: strings.Split(tmpDir, string(filepath.Separator)), + want: []gitignore.Pattern{ + gitignore.ParsePattern("root.txt", strings.Split(tmpDir, string(filepath.Separator))), + gitignore.ParsePattern("subdir.txt", append(strings.Split(tmpDir, string(filepath.Separator)), "a", "b")), + gitignore.ParsePattern("last.txt", append(strings.Split(tmpDir, string(filepath.Separator)), "z")), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := LoadIgnorePatterns(tt.dir, tt.domain, IgnoreFile) + if err != nil { + t.Error(err) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("LoadIgnorePatterns() got = %#v, want %#v", got, tt.want) + for _, v := range got { + t.Error(v) + } + } + }) + } +} diff --git a/pkg/git/ssh-pool/hostport.go b/lib/git/ssh-pool/hostport.go similarity index 100% rename from pkg/git/ssh-pool/hostport.go rename to lib/git/ssh-pool/hostport.go diff --git a/pkg/git/ssh-pool/ssh_pool.go b/lib/git/ssh-pool/ssh_pool.go similarity index 92% rename from pkg/git/ssh-pool/ssh_pool.go rename to lib/git/ssh-pool/ssh_pool.go index fb60c220c..27e54279a 100644 --- a/pkg/git/ssh-pool/ssh_pool.go +++ b/lib/git/ssh-pool/ssh_pool.go @@ -6,8 +6,7 @@ import ( "encoding/binary" "encoding/hex" "fmt" - "github.com/kluctl/kluctl/v2/pkg/git/auth" - "github.com/kluctl/kluctl/v2/pkg/status" + "github.com/kluctl/kluctl/lib/git/auth" "golang.org/x/crypto/ssh" "golang.org/x/net/proxy" "sync" @@ -78,7 +77,6 @@ func (p *SshPool) GetSession(ctx context.Context, host string, port int, auth au s, err := pe.pc.client.NewSession() if err != nil { - origErr := err _ = pe.pc.client.Close() pe.pc = nil if isNew { @@ -92,7 +90,7 @@ func (p *SshPool) GetSession(ctx context.Context, host string, port int, auth au p.pool.Delete(h) return nil, err } - status.Trace(ctx, "Successfully retries failed ssh connection. Old error: %s", origErr) + pe.pc = &poolClient{ time: time.Now(), client: client, @@ -141,7 +139,7 @@ func (p *SshPool) newClient(ctx context.Context, addr string, auth auth.AuthMeth return nil, err } - config, err := auth.ClientConfig() + config, err := auth.SshClientConfig() if err != nil { return nil, err } @@ -159,7 +157,7 @@ func (p *SshPool) buildHash(addr string, auth auth.AuthMethodAndCA) (string, err return "", fmt.Errorf("auth has no Hash") } - config, err := auth.ClientConfig() + config, err := auth.SshClientConfig() if err != nil { return "", err } diff --git a/lib/git/types/doc.go b/lib/git/types/doc.go new file mode 100644 index 000000000..b4756cc8a --- /dev/null +++ b/lib/git/types/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package types contains types used in Kluctl projects +// +kubebuilder:object:generate=true +package types diff --git a/lib/git/types/git_info.go b/lib/git/types/git_info.go new file mode 100644 index 000000000..af4d2a0ee --- /dev/null +++ b/lib/git/types/git_info.go @@ -0,0 +1,10 @@ +package types + +// GitInfo represents the result of BuildGitInfo, which gathers all info from a local cloned git repository +type GitInfo struct { + Url *GitUrl `json:"url"` + Ref *GitRef `json:"ref"` + SubDir string `json:"subDir"` + Commit string `json:"commit"` + Dirty bool `json:"dirty"` +} diff --git a/lib/git/types/git_ref.go b/lib/git/types/git_ref.go new file mode 100644 index 000000000..e091645b0 --- /dev/null +++ b/lib/git/types/git_ref.go @@ -0,0 +1,91 @@ +package types + +import ( + "encoding/json" + "fmt" + "github.com/kluctl/kluctl/lib/yaml" + "strings" +) + +type GitRef struct { + // Branch to use. + // +optional + Branch string `json:"branch,omitempty"` + + // Tag to use. + // +optional + Tag string `json:"tag,omitempty"` + + // Ref is only used to keep compatibility to the old string based ref field in GitProject + // You are not allowed to use it directly + Ref string `json:"-"` + + // Commit SHA to use. + // +optional + Commit string `json:"commit,omitempty"` +} + +func (ref *GitRef) UnmarshalJSON(b []byte) error { + if err := yaml.ReadYamlBytes(b, &ref.Ref); err == nil { + // it's a simple string + return nil + } + ref.Ref = "" + type raw GitRef + err := yaml.ReadYamlBytes(b, (*raw)(ref)) + if err != nil { + return err + } + + cnt := 0 + if ref.Tag != "" { + cnt++ + } + if ref.Branch != "" { + cnt++ + } + if ref.Commit != "" { + cnt++ + } + if cnt == 0 { + return fmt.Errorf("either branch, tag or commit must be set") + } + if cnt != 1 { + return fmt.Errorf("only one of the ref fields can be set") + } + return nil +} + +func (ref GitRef) MarshalJSON() ([]byte, error) { + if ref.Ref != "" { + return json.Marshal(ref.Ref) + } + + type raw GitRef + return json.Marshal((*raw)(&ref)) +} + +func (ref *GitRef) String() string { + if ref == nil { + return "" + } + if ref.Tag != "" { + return fmt.Sprintf("refs/tags/%s", ref.Tag) + } else if ref.Branch != "" { + return fmt.Sprintf("refs/heads/%s", ref.Branch) + } else if ref.Ref != "" { + return ref.Ref + } else { + return "" + } +} + +func ParseGitRef(s string) (GitRef, error) { + if strings.HasPrefix(s, "refs/heads/") { + return GitRef{Branch: strings.SplitN(s, "/", 3)[2]}, nil + } else if strings.HasPrefix(s, "refs/tags/") { + return GitRef{Tag: strings.SplitN(s, "/", 3)[2]}, nil + } else { + return GitRef{}, fmt.Errorf("can't parse %s as GitRef", s) + } +} diff --git a/lib/git/types/git_url.go b/lib/git/types/git_url.go new file mode 100644 index 000000000..e4bcd48e9 --- /dev/null +++ b/lib/git/types/git_url.go @@ -0,0 +1,140 @@ +package types + +import ( + "encoding/json" + "fmt" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/kluctl/kluctl/lib/yaml" + "net/url" + "path" + "strings" +) + +// +kubebuilder:validation:Type=string +type GitUrl struct { + url.URL `json:"-"` +} + +func ParseGitUrl(u string) (*GitUrl, error) { + // we re-use go-git's parsing capabilities (especially in regard to SCP urls) + ep, err := transport.NewEndpoint(u) + if err != nil { + return nil, err + } + + if ep.Protocol == "file" { + return nil, fmt.Errorf("file:// protocol is not supported: %s", u) + } + + u2 := ep.String() + + // and we also rely on the standard lib to treat escaping properly + u3, err := url.Parse(u2) + if err != nil { + return nil, err + } + + return &GitUrl{ + URL: *u3, + }, nil +} + +func ParseGitUrlMust(u string) *GitUrl { + u2, err := ParseGitUrl(u) + if err != nil { + panic(err) + } + return u2 +} + +func (in *GitUrl) DeepCopyInto(out *GitUrl) { + out.URL = in.URL + if out.URL.User != nil { + out.URL.User = &*in.URL.User + } +} + +func (u *GitUrl) UnmarshalJSON(b []byte) error { + var s string + err := yaml.ReadYamlBytes(b, &s) + if err != nil { + return err + } + u2, err := ParseGitUrl(s) + if err != nil { + return err + } + *u = *u2 + return err +} + +func (u GitUrl) MarshalJSON() ([]byte, error) { + return json.Marshal(u.String()) +} + +func (u *GitUrl) IsSsh() bool { + return u.Scheme == "ssh" || u.Scheme == "git" || u.Scheme == "git+ssh" +} + +func (u *GitUrl) NormalizePort() string { + port := u.Port() + if port == "" { + return "" + } + + defaultPort := "" + switch u.Scheme { + case "http": + defaultPort = "80" + case "https": + defaultPort = "443" + case "git": + defaultPort = "22" + case "git+ssh": + defaultPort = "22" + case "ssh": + defaultPort = "22" + case "ftp": + defaultPort = "21" + case "rsync": + defaultPort = "873" + case "file": + break + default: + return port + } + + if defaultPort == "" || port == defaultPort { + return "" + } + return port +} + +func (u *GitUrl) Normalize() *GitUrl { + p := strings.ToLower(u.Path) + if path.Base(p) != ".git" { + // we only trim it with it does not end with /.git + p = strings.TrimSuffix(p, ".git") + } + p = strings.TrimSuffix(p, "/") + + hostname := strings.ToLower(u.Hostname()) + port := u.NormalizePort() + + if p != "" && hostname != "" && !strings.HasPrefix(p, "/") { + p = "/" + p + } + + u2 := *u + u2.Path = p + u2.Host = hostname + if port != "" { + u2.Host += ":" + port + } + return &u2 +} + +func (u *GitUrl) RepoKey() RepoKey { + u2 := u.Normalize() + return NewRepoKey("git", u2.Host, u2.Path) +} diff --git a/lib/git/types/repo_key.go b/lib/git/types/repo_key.go new file mode 100644 index 000000000..e2a2d92e5 --- /dev/null +++ b/lib/git/types/repo_key.go @@ -0,0 +1,157 @@ +package types + +import ( + "encoding/json" + "fmt" + "github.com/kluctl/kluctl/lib/yaml" + "net/url" + "regexp" + "strconv" + "strings" +) + +// +kubebuilder:validation:Type=string +type RepoKey struct { + Type string `json:"-"` + Host string `json:"-"` + Path string `json:"-"` +} + +func NewRepoKey(type_ string, host string, path string) RepoKey { + path = strings.TrimSuffix(path, "/") + path = strings.TrimPrefix(path, "/") + return RepoKey{ + Type: type_, + Host: host, + Path: path, + } +} + +func NewRepoKeyFromUrl(urlIn string) (RepoKey, error) { + u, err := url.Parse(urlIn) + if err != nil { + return RepoKey{}, err + } + t := "git" + if u.Scheme == "oci" { + t = "oci" + } + p := u.Path + if t == "git" { + p = strings.TrimSuffix(p, ".git") + } + return NewRepoKey(t, u.Host, p), nil +} + +func NewRepoKeyFromGitUrl(urlIn string) (RepoKey, error) { + u, err := ParseGitUrl(urlIn) + if err != nil { + return RepoKey{}, err + } + return NewRepoKeyFromUrl(u.String()) +} + +var hostNameRegex = regexp.MustCompile(`^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$`) +var ipRegex = regexp.MustCompile(`^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$`) + +func ParseRepoKey(s string, defaultType string) (RepoKey, error) { + if s == "" { + return RepoKey{}, nil + } + + var t string + if strings.HasPrefix(s, "git://") { + t = "git" + s = strings.TrimPrefix(s, "git://") + } else if strings.HasPrefix(s, "oci://") { + t = "oci" + s = strings.TrimPrefix(s, "oci://") + } else { + t = defaultType + } + + s2 := strings.SplitN(s, "/", 2) + if len(s2) != 2 { + return RepoKey{}, fmt.Errorf("invalid repo key: %s", s) + } + + var host, port string + if strings.Contains(s2[0], ":") { + s3 := strings.SplitN(s2[0], ":", 2) + host = s3[0] + port = s3[1] + } else { + host = s2[0] + } + + if !hostNameRegex.MatchString(host) && !ipRegex.MatchString(host) { + return RepoKey{}, fmt.Errorf("invalid repo key: %s", s) + } + + if port != "" { + if _, err := strconv.ParseInt(port, 10, 32); err != nil { + return RepoKey{}, fmt.Errorf("invalid repo key: %s", s) + } + } + + return RepoKey{ + Type: t, + Host: s2[0], + Path: s2[1], + }, nil +} + +func (u RepoKey) String() string { + if u.Host == "" && u.Path == "" { + return "" + } + t := u.Type + if t == "" { + t = "git" + } + return fmt.Sprintf("%s://%s/%s", t, u.Host, u.Path) +} + +func (u *RepoKey) UnmarshalJSON(b []byte) error { + var s string + err := yaml.ReadYamlBytes(b, &s) + if err != nil { + return err + } + if s == "" { + u.Type = "" + u.Host = "" + u.Path = "" + return nil + } + x, err := ParseRepoKey(s, "git") + if err != nil { + return err + } + *u = x + return nil +} + +func (u RepoKey) MarshalJSON() ([]byte, error) { + b, err := json.Marshal(u.String()) + if err != nil { + return nil, err + } + + return b, err +} + +type ProjectKey struct { + RepoKey RepoKey `json:"repoKey,omitempty"` + SubDir string `json:"subDir,omitempty"` +} + +func (k ProjectKey) Less(o ProjectKey) bool { + if k.RepoKey != o.RepoKey { + return k.RepoKey.String() < o.RepoKey.String() + } + if k.SubDir != o.SubDir { + return k.SubDir < o.SubDir + } + return false +} diff --git a/lib/git/types/zz_generated.deepcopy.go b/lib/git/types/zz_generated.deepcopy.go new file mode 100644 index 000000000..cbb469c75 --- /dev/null +++ b/lib/git/types/zz_generated.deepcopy.go @@ -0,0 +1,103 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 controller-gen. DO NOT EDIT. + +package types + +import () + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitInfo) DeepCopyInto(out *GitInfo) { + *out = *in + if in.Url != nil { + in, out := &in.Url, &out.Url + *out = (*in).DeepCopy() + } + if in.Ref != nil { + in, out := &in.Ref, &out.Ref + *out = new(GitRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitInfo. +func (in *GitInfo) DeepCopy() *GitInfo { + if in == nil { + return nil + } + out := new(GitInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitRef) DeepCopyInto(out *GitRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRef. +func (in *GitRef) DeepCopy() *GitRef { + if in == nil { + return nil + } + out := new(GitRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitUrl. +func (in *GitUrl) DeepCopy() *GitUrl { + if in == nil { + return nil + } + out := new(GitUrl) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectKey) DeepCopyInto(out *ProjectKey) { + *out = *in + out.RepoKey = in.RepoKey +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectKey. +func (in *ProjectKey) DeepCopy() *ProjectKey { + if in == nil { + return nil + } + out := new(ProjectKey) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepoKey) DeepCopyInto(out *RepoKey) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepoKey. +func (in *RepoKey) DeepCopy() *RepoKey { + if in == nil { + return nil + } + out := new(RepoKey) + in.DeepCopyInto(out) + return out +} diff --git a/lib/git/utils.go b/lib/git/utils.go new file mode 100644 index 000000000..00a13cbdf --- /dev/null +++ b/lib/git/utils.go @@ -0,0 +1,158 @@ +package git + +import ( + "bufio" + "bytes" + "context" + "fmt" + "github.com/go-errors/errors" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/format/gitignore" + "github.com/kluctl/kluctl/lib/git/sourceignore" + "github.com/kluctl/kluctl/lib/git/types" + "github.com/kluctl/kluctl/lib/status" + "os" + "os/exec" + "path/filepath" + "strings" + "sync/atomic" + "time" +) + +type CheckoutInfo struct { + CheckedOutRef types.GitRef `json:"checkedOutRef"` + CheckedOutCommit string `json:"checkedOutCommit"` +} + +func DetectGitRepositoryRoot(path string) (string, error) { + path, err := filepath.Abs(path) + if err != nil { + return "", err + } + for true { + st, err := os.Stat(filepath.Join(path, ".git")) + if err == nil && st.IsDir() { + break + } + old := path + path = filepath.Dir(path) + if old == path { + return "", fmt.Errorf("could not detect git repository root") + } + } + return path, nil +} + +// GetWorktreeStatus returns the worktree status of the given repo +// When modified files are found, it will invoke the git binary for "git status --porcelain" to check that things have +// really changed. This is required because go-git in not properly handling CRLF: https://github.com/go-git/go-git/issues/594 +func GetWorktreeStatus(ctx context.Context, path string) (git.Status, error) { + g, err := git.PlainOpen(path) + if err != nil { + return nil, err + } + wt, err := g.Worktree() + if err != nil { + return nil, err + } + gitStatus, err := wt.Status() + if err != nil { + return nil, err + } + + isModified := false + for _, s := range gitStatus { + if s.Worktree == git.Modified { + isModified = true + break + } + } + if !isModified { + return gitStatus, nil + } + + status.Trace(ctx, "Running git status --porcelain to verify CRLF issues are not what cause the dirty status") + + commandPath, err := exec.LookPath("git") + if err != nil { + status.Tracef(ctx, "Failed to lookup git binary: %v", err) + return nil, err + } + + out := bytes.NewBuffer(nil) + + cmd := &exec.Cmd{Path: commandPath, Dir: path, Env: os.Environ(), Args: []string{"git", "status", "--porcelain"}, Stdout: out, Stderr: out} + err = cmd.Run() + if err != nil { + status.Tracef(ctx, "Failed to run git status --porcelain: err=%v, out=%s", err, out.String()) + return gitStatus, nil + } + + parsedStatus, err := parsePorcelainStatus(out.String()) + if err != nil { + status.Tracef(ctx, "Failed to parse output of git status --porcelain: %v", err) + return gitStatus, err + } + + return parsedStatus, nil +} + +func parsePorcelainStatus(out string) (git.Status, error) { + s := bufio.NewScanner(strings.NewReader(out)) + + ret := git.Status{} + for s.Scan() { + if len(s.Text()) < 1 { + continue + } + line := s.Text() + var fs git.FileStatus + var path string + _, err := fmt.Sscanf(line, "%c%c %s", &fs.Staging, &fs.Worktree, &path) + if err != nil { + return nil, err + } + ret[path] = &fs + } + return ret, nil +} + +func LoadGitignore(p string) ([]gitignore.Pattern, error) { + p = filepath.Clean(p) + domain := strings.Split(p, string(filepath.Separator)) + ignorePatterns, err := sourceignore.LoadIgnorePatterns(p, domain, ".gitignore") + if err != nil { + return nil, err + } + ignorePatterns = append(ignorePatterns, sourceignore.ReadPatterns(strings.NewReader(".git"), domain)...) + return ignorePatterns, nil +} + +func RunWithDeadlineAndPanic(ctx context.Context, extraDeadline time.Duration, f func() error) error { + deadline, hasDeadline := ctx.Deadline() + + if !hasDeadline { + return f() + } + + var finished atomic.Bool + + wait := deadline.Sub(time.Now()) + extraDeadline + if wait < 0 { + return ctx.Err() + } + + deadlineErr := errors.New(fmt.Errorf("deadline exceeded while calling function")) + + t := time.AfterFunc(wait, func() { + if !finished.Load() { + panic(deadlineErr.ErrorStack()) + } + }) + defer t.Stop() + + err := f() + finished.Store(true) + + return err +} diff --git a/lib/go-jinja2/.gitattributes b/lib/go-jinja2/.gitattributes new file mode 100644 index 000000000..eb1b98a73 --- /dev/null +++ b/lib/go-jinja2/.gitattributes @@ -0,0 +1 @@ +internal/data/** linguist-vendored diff --git a/lib/go-jinja2/.github/dependabot.yml b/lib/go-jinja2/.github/dependabot.yml new file mode 100644 index 000000000..4143be270 --- /dev/null +++ b/lib/go-jinja2/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" diff --git a/lib/go-jinja2/.github/workflows/tests.yml b/lib/go-jinja2/.github/workflows/tests.yml new file mode 100644 index 000000000..dc4a9afe0 --- /dev/null +++ b/lib/go-jinja2/.github/workflows/tests.yml @@ -0,0 +1,58 @@ +name: tests + +on: + push: + branches: + - '**' + +jobs: + tests: + strategy: + matrix: + include: + - os: ubuntu-22.04 + binary-suffix: linux-amd64 + - os: macos-12 + binary-suffix: darwin-amd64 + - os: windows-2019 + binary-suffix: windows-amd64 + os: [ubuntu-22.04, macos-12, windows-2019] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.19 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-embed-python-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-embed-python- + - name: Go generate + if: runner.os != 'Windows' + shell: bash + run: | + go generate ./... + - name: Verify nothing changed + if: runner.os != 'Windows' + shell: bash + run: | + if [ ! -z "$(git status --porcelain)" ]; then + echo "working directory not clean:" + git status + git diff + exit 1 + fi + - name: Run unit tests + shell: bash + run: | + go test ./... diff --git a/lib/go-jinja2/.gitignore b/lib/go-jinja2/.gitignore new file mode 100644 index 000000000..99ec4282e --- /dev/null +++ b/lib/go-jinja2/.gitignore @@ -0,0 +1,2 @@ +*.pyc +.idea diff --git a/lib/go-jinja2/README.md b/lib/go-jinja2/README.md new file mode 100644 index 000000000..fa9d15ace --- /dev/null +++ b/lib/go-jinja2/README.md @@ -0,0 +1,37 @@ +# Embedded Jinja2 Renderer + +This library provides an embedded Jinja2 renderer, based on https://github.com/kluctl/go-embed-python. It works +by spawning one (or multiple) Jinaj2 renderer processes which communicate with your Go application via stdin/stdout. + +Whenever something needs to be rendered, it is sent to the renderer, which will then process it. The result is returned +to the caller. + +Here is an example: + +```go +package main + +import ( + "fmt" + "github.com/kluctl/kluctl/lib/go-jinja2" +) + +func main() { + j2, err := jinja2.NewJinja2("example", 1, + jinja2.WithGlobal("test_var1", 1), + jinja2.WithGlobal("test_var2", map[string]any{"test": 2})) + if err != nil { + panic(err) + } + defer j2.Close() + + template := "{{ test_var1 }}" + + s, err := j2.RenderString(template) + if err != nil { + panic(err) + } + + fmt.Printf("template: %s\nresult: %s", template, s) +} +``` diff --git a/lib/go-jinja2/example/main.go b/lib/go-jinja2/example/main.go new file mode 100644 index 000000000..c8e3f334e --- /dev/null +++ b/lib/go-jinja2/example/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/kluctl/kluctl/lib/go-jinja2" +) + +func main() { + j2, err := jinja2.NewJinja2("example", 1, + jinja2.WithGlobal("test_var1", 1), + jinja2.WithGlobal("test_var2", map[string]any{"test": 2})) + if err != nil { + panic(err) + } + defer j2.Close() + + template := "{{ test_var1 }}" + + s, err := j2.RenderString(template) + if err != nil { + panic(err) + } + + fmt.Printf("template: %s\nresult: %s", template, s) +} diff --git a/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/INSTALLER b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/LICENSE.txt b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/LICENSE.txt new file mode 100644 index 000000000..9d227a0cc --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/METADATA b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/METADATA new file mode 100644 index 000000000..82261f2a4 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/METADATA @@ -0,0 +1,92 @@ +Metadata-Version: 2.1 +Name: MarkupSafe +Version: 3.0.2 +Summary: Safely add untrusted strings to HTML/XML markup. +Maintainer-email: Pallets +License: Copyright 2010 Pallets + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://markupsafe.palletsprojects.com/ +Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/ +Project-URL: Source, https://github.com/pallets/markupsafe/ +Project-URL: Chat, https://discord.gg/pallets +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Text Processing :: Markup :: HTML +Classifier: Typing :: Typed +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-File: LICENSE.txt + +# MarkupSafe + +MarkupSafe implements a text object that escapes characters so it is +safe to use in HTML and XML. Characters that have special meanings are +replaced so that they display as the actual characters. This mitigates +injection attacks, meaning untrusted user input can safely be displayed +on a page. + + +## Examples + +```pycon +>>> from markupsafe import Markup, escape + +>>> # escape replaces special characters and wraps in Markup +>>> escape("") +Markup('<script>alert(document.cookie);</script>') + +>>> # wrap in Markup to mark text "safe" and prevent escaping +>>> Markup("Hello") +Markup('hello') + +>>> escape(Markup("Hello")) +Markup('hello') + +>>> # Markup is a str subclass +>>> # methods and operators escape their arguments +>>> template = Markup("Hello {name}") +>>> template.format(name='"World"') +Markup('Hello "World"') +``` + +## Donate + +The Pallets organization develops and supports MarkupSafe and other +popular packages. In order to grow the community of contributors and +users, and allow the maintainers to devote more time to the projects, +[please donate today][]. + +[please donate today]: https://palletsprojects.com/donate diff --git a/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/RECORD b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/RECORD new file mode 100644 index 000000000..f99a453cf --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/RECORD @@ -0,0 +1,15 @@ +MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475 +MarkupSafe-3.0.2.dist-info/METADATA,sha256=aAwbZhSmXdfFuMM-rEHpeiHRkBOGESyVLJIuwzHP-nw,3975 +MarkupSafe-3.0.2.dist-info/RECORD,, +MarkupSafe-3.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +MarkupSafe-3.0.2.dist-info/WHEEL,sha256=HhlOYVXy1Wa9n3P9SVx0GD3487GzK6yrtex_WJ2pR1I,114 +MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11 +markupsafe/__init__.py,sha256=sr-U6_27DfaSrj5jnHYxWN-pvhM27sjlDplMDPZKm7k,13214 +markupsafe/__pycache__/__init__.cpython-311.pyc,, +markupsafe/__pycache__/_native.cpython-311.pyc,, +markupsafe/_native.py,sha256=hSLs8Jmz5aqayuengJJ3kdT5PwNpBWpKrmQSdipndC8,210 +markupsafe/_speedups.c,sha256=O7XulmTo-epI6n2FtMVOrJXl8EAaIwD2iNYmBI5SEoQ,4149 +markupsafe/_speedups.cpython-311-darwin.so,sha256=waUcSZ9Yl-0bacMoWW2_J3dUQtRqGgaUpZHEccQMe2I,67056 +markupsafe/_speedups.pyi,sha256=ENd1bYe7gbBUf2ywyYWOGUpnXOHNJ-cgTNqetlW8h5k,41 +markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/pkg/jinja2/embed/python_src.dummy b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/REQUESTED similarity index 100% rename from pkg/jinja2/embed/python_src.dummy rename to lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/REQUESTED diff --git a/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/WHEEL b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/WHEEL new file mode 100644 index 000000000..ed9b49798 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.2.0) +Root-Is-Purelib: false +Tag: cp311-cp311-macosx_10_9_universal2 + diff --git a/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/top_level.txt b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/top_level.txt new file mode 100644 index 000000000..75bf72925 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/MarkupSafe-3.0.2.dist-info/top_level.txt @@ -0,0 +1 @@ +markupsafe diff --git a/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/INSTALLER b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/LICENSE b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/LICENSE new file mode 100644 index 000000000..2f1b8e15e --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017-2021 Ingy döt Net +Copyright (c) 2006-2016 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/METADATA b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/METADATA new file mode 100644 index 000000000..db029b770 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/METADATA @@ -0,0 +1,46 @@ +Metadata-Version: 2.1 +Name: PyYAML +Version: 6.0.2 +Summary: YAML parser and emitter for Python +Home-page: https://pyyaml.org/ +Download-URL: https://pypi.org/project/PyYAML/ +Author: Kirill Simonov +Author-email: xi@resolvent.net +License: MIT +Project-URL: Bug Tracker, https://github.com/yaml/pyyaml/issues +Project-URL: CI, https://github.com/yaml/pyyaml/actions +Project-URL: Documentation, https://pyyaml.org/wiki/PyYAMLDocumentation +Project-URL: Mailing lists, http://lists.sourceforge.net/lists/listinfo/yaml-core +Project-URL: Source Code, https://github.com/yaml/pyyaml +Platform: Any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Cython +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Text Processing :: Markup +Requires-Python: >=3.8 +License-File: LICENSE + +YAML is a data serialization format designed for human readability +and interaction with scripting languages. PyYAML is a YAML parser +and emitter for Python. + +PyYAML features a complete YAML 1.1 parser, Unicode support, pickle +support, capable extension API, and sensible error messages. PyYAML +supports standard YAML tags and provides Python-specific tags that +allow to represent an arbitrary Python object. + +PyYAML is applicable for a broad range of tasks from complex +configuration files to object serialization and persistence. diff --git a/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/RECORD b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/RECORD new file mode 100644 index 000000000..cb0f31a7a --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/RECORD @@ -0,0 +1,44 @@ +PyYAML-6.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +PyYAML-6.0.2.dist-info/LICENSE,sha256=jTko-dxEkP1jVwfLiOsmvXZBAqcoKVQwfT5RZ6V36KQ,1101 +PyYAML-6.0.2.dist-info/METADATA,sha256=9-odFB5seu4pGPcEv7E8iyxNF51_uKnaNGjLAhz2lto,2060 +PyYAML-6.0.2.dist-info/RECORD,, +PyYAML-6.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +PyYAML-6.0.2.dist-info/WHEEL,sha256=LEbCNePz8y7PesR3P6dljM_9-vCh34Kh1BfkRxyF9i0,111 +PyYAML-6.0.2.dist-info/top_level.txt,sha256=rpj0IVMTisAjh_1vG3Ccf9v5jpCQwAz6cD1IVU5ZdhQ,11 +_yaml/__init__.py,sha256=04Ae_5osxahpJHa3XBZUAf4wi6XX32gR8D6X6p64GEA,1402 +_yaml/__pycache__/__init__.cpython-311.pyc,, +yaml/__init__.py,sha256=N35S01HMesFTe0aRRMWkPj0Pa8IEbHpE9FK7cr5Bdtw,12311 +yaml/__pycache__/__init__.cpython-311.pyc,, +yaml/__pycache__/composer.cpython-311.pyc,, +yaml/__pycache__/constructor.cpython-311.pyc,, +yaml/__pycache__/cyaml.cpython-311.pyc,, +yaml/__pycache__/dumper.cpython-311.pyc,, +yaml/__pycache__/emitter.cpython-311.pyc,, +yaml/__pycache__/error.cpython-311.pyc,, +yaml/__pycache__/events.cpython-311.pyc,, +yaml/__pycache__/loader.cpython-311.pyc,, +yaml/__pycache__/nodes.cpython-311.pyc,, +yaml/__pycache__/parser.cpython-311.pyc,, +yaml/__pycache__/reader.cpython-311.pyc,, +yaml/__pycache__/representer.cpython-311.pyc,, +yaml/__pycache__/resolver.cpython-311.pyc,, +yaml/__pycache__/scanner.cpython-311.pyc,, +yaml/__pycache__/serializer.cpython-311.pyc,, +yaml/__pycache__/tokens.cpython-311.pyc,, +yaml/_yaml.cpython-311-darwin.so,sha256=NyiwVq57ST7jr_m8ZDCrZEATp1-1-JyHzz8nlWXIB04,397104 +yaml/composer.py,sha256=_Ko30Wr6eDWUeUpauUGT3Lcg9QPBnOPVlTnIMRGJ9FM,4883 +yaml/constructor.py,sha256=kNgkfaeLUkwQYY_Q6Ff1Tz2XVw_pG1xVE9Ak7z-viLA,28639 +yaml/cyaml.py,sha256=6ZrAG9fAYvdVe2FK_w0hmXoG7ZYsoYUwapG8CiC72H0,3851 +yaml/dumper.py,sha256=PLctZlYwZLp7XmeUdwRuv4nYOZ2UBnDIUy8-lKfLF-o,2837 +yaml/emitter.py,sha256=jghtaU7eFwg31bG0B7RZea_29Adi9CKmXq_QjgQpCkQ,43006 +yaml/error.py,sha256=Ah9z-toHJUbE9j-M8YpxgSRM5CgLCcwVzJgLLRF2Fxo,2533 +yaml/events.py,sha256=50_TksgQiE4up-lKo_V-nBy-tAIxkIPQxY5qDhKCeHw,2445 +yaml/loader.py,sha256=UVa-zIqmkFSCIYq_PgSGm4NSJttHY2Rf_zQ4_b1fHN0,2061 +yaml/nodes.py,sha256=gPKNj8pKCdh2d4gr3gIYINnPOaOxGhJAUiYhGRnPE84,1440 +yaml/parser.py,sha256=ilWp5vvgoHFGzvOZDItFoGjD6D42nhlZrZyjAwa0oJo,25495 +yaml/reader.py,sha256=0dmzirOiDG4Xo41RnuQS7K9rkY3xjHiVasfDMNTqCNw,6794 +yaml/representer.py,sha256=IuWP-cAW9sHKEnS0gCqSa894k1Bg4cgTxaDwIcbRQ-Y,14190 +yaml/resolver.py,sha256=9L-VYfm4mWHxUD1Vg4X7rjDRK_7VZd6b92wzq7Y2IKY,9004 +yaml/scanner.py,sha256=YEM3iLZSaQwXcQRg2l2R4MdT0zGP2F9eHkKGKnHyWQY,51279 +yaml/serializer.py,sha256=ChuFgmhU01hj4xgI8GaKv6vfM2Bujwa9i7d2FAHj7cA,4165 +yaml/tokens.py,sha256=lTQIzSVw8Mg9wv459-TjiOQe6wVziqaRlqX2_89rp54,2573 diff --git a/pkg/python/embed/python-darwin-amd64.dummy b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/REQUESTED similarity index 100% rename from pkg/python/embed/python-darwin-amd64.dummy rename to lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/REQUESTED diff --git a/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/WHEEL b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/WHEEL new file mode 100644 index 000000000..e3328a793 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.44.0) +Root-Is-Purelib: false +Tag: cp311-cp311-macosx_10_9_x86_64 + diff --git a/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/top_level.txt b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/top_level.txt new file mode 100644 index 000000000..e6475e911 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/PyYAML-6.0.2.dist-info/top_level.txt @@ -0,0 +1,2 @@ +_yaml +yaml diff --git a/lib/go-jinja2/internal/data/darwin-amd64/_yaml/__init__.py b/lib/go-jinja2/internal/data/darwin-amd64/_yaml/__init__.py new file mode 100644 index 000000000..7baa8c4b6 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/_yaml/__init__.py @@ -0,0 +1,33 @@ +# This is a stub package designed to roughly emulate the _yaml +# extension module, which previously existed as a standalone module +# and has been moved into the `yaml` package namespace. +# It does not perfectly mimic its old counterpart, but should get +# close enough for anyone who's relying on it even when they shouldn't. +import yaml + +# in some circumstances, the yaml module we imoprted may be from a different version, so we need +# to tread carefully when poking at it here (it may not have the attributes we expect) +if not getattr(yaml, '__with_libyaml__', False): + from sys import version_info + + exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError + raise exc("No module named '_yaml'") +else: + from yaml._yaml import * + import warnings + warnings.warn( + 'The _yaml extension module is now located at yaml._yaml' + ' and its location is subject to change. To use the' + ' LibYAML-based parser and emitter, import from `yaml`:' + ' `from yaml import CLoader as Loader, CDumper as Dumper`.', + DeprecationWarning + ) + del warnings + # Don't `del yaml` here because yaml is actually an existing + # namespace member of _yaml. + +__name__ = '_yaml' +# If the module is top-level (i.e. not a part of any specific package) +# then the attribute should be set to ''. +# https://docs.python.org/3.8/library/types.html +__package__ = '' diff --git a/lib/go-jinja2/internal/data/darwin-amd64/bin/jsonpath_ng b/lib/go-jinja2/internal/data/darwin-amd64/bin/jsonpath_ng new file mode 100644 index 000000000..28a424903 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/bin/jsonpath_ng @@ -0,0 +1,8 @@ +#!/tmp/python-pip-darwin-amd64/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from jsonpath_ng.bin.jsonpath import entry_point +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(entry_point()) diff --git a/lib/go-jinja2/internal/data/darwin-amd64/bin/slugify b/lib/go-jinja2/internal/data/darwin-amd64/bin/slugify new file mode 100644 index 000000000..bc16b756e --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/bin/slugify @@ -0,0 +1,8 @@ +#!/tmp/python-pip-darwin-amd64/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from slugify.__main__ import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/INSTALLER b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/LICENSE.rst b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/LICENSE.rst new file mode 100644 index 000000000..d12a84918 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2014 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/METADATA b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/METADATA new file mode 100644 index 000000000..7a6bbb24b --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/METADATA @@ -0,0 +1,103 @@ +Metadata-Version: 2.1 +Name: click +Version: 8.1.7 +Summary: Composable command line interface toolkit +Home-page: https://palletsprojects.com/p/click/ +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://click.palletsprojects.com/ +Project-URL: Changes, https://click.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/click/ +Project-URL: Issue Tracker, https://github.com/pallets/click/issues/ +Project-URL: Chat, https://discord.gg/pallets +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +License-File: LICENSE.rst +Requires-Dist: colorama ; platform_system == "Windows" +Requires-Dist: importlib-metadata ; python_version < "3.8" + +\$ click\_ +========== + +Click is a Python package for creating beautiful command line interfaces +in a composable way with as little code as necessary. It's the "Command +Line Interface Creation Kit". It's highly configurable but comes with +sensible defaults out of the box. + +It aims to make the process of writing command line tools quick and fun +while also preventing any frustration caused by the inability to +implement an intended CLI API. + +Click in three points: + +- Arbitrary nesting of commands +- Automatic help page generation +- Supports lazy loading of subcommands at runtime + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U click + +.. _pip: https://pip.pypa.io/en/stable/getting-started/ + + +A Simple Example +---------------- + +.. code-block:: python + + import click + + @click.command() + @click.option("--count", default=1, help="Number of greetings.") + @click.option("--name", prompt="Your name", help="The person to greet.") + def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for _ in range(count): + click.echo(f"Hello, {name}!") + + if __name__ == '__main__': + hello() + +.. code-block:: text + + $ python hello.py --count=3 + Your name: Click + Hello, Click! + Hello, Click! + Hello, Click! + + +Donate +------ + +The Pallets organization develops and supports Click and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, `please +donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://click.palletsprojects.com/ +- Changes: https://click.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/click/ +- Source Code: https://github.com/pallets/click +- Issue Tracker: https://github.com/pallets/click/issues +- Chat: https://discord.gg/pallets diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/RECORD b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/RECORD new file mode 100644 index 000000000..dcffbf004 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/RECORD @@ -0,0 +1,40 @@ +click-8.1.7.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +click-8.1.7.dist-info/LICENSE.rst,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475 +click-8.1.7.dist-info/METADATA,sha256=qIMevCxGA9yEmJOM_4WHuUJCwWpsIEVbCPOhs45YPN4,3014 +click-8.1.7.dist-info/RECORD,, +click-8.1.7.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +click-8.1.7.dist-info/WHEEL,sha256=5sUXSg9e4bi7lTLOHcm6QEYwO5TIF1TNbTSVFVjcJcc,92 +click-8.1.7.dist-info/top_level.txt,sha256=J1ZQogalYS4pphY_lPECoNMfw0HzTSrZglC4Yfwo4xA,6 +click/__init__.py,sha256=YDDbjm406dTOA0V8bTtdGnhN7zj5j-_dFRewZF_pLvw,3138 +click/__pycache__/__init__.cpython-311.pyc,, +click/__pycache__/_compat.cpython-311.pyc,, +click/__pycache__/_termui_impl.cpython-311.pyc,, +click/__pycache__/_textwrap.cpython-311.pyc,, +click/__pycache__/_winconsole.cpython-311.pyc,, +click/__pycache__/core.cpython-311.pyc,, +click/__pycache__/decorators.cpython-311.pyc,, +click/__pycache__/exceptions.cpython-311.pyc,, +click/__pycache__/formatting.cpython-311.pyc,, +click/__pycache__/globals.cpython-311.pyc,, +click/__pycache__/parser.cpython-311.pyc,, +click/__pycache__/shell_completion.cpython-311.pyc,, +click/__pycache__/termui.cpython-311.pyc,, +click/__pycache__/testing.cpython-311.pyc,, +click/__pycache__/types.cpython-311.pyc,, +click/__pycache__/utils.cpython-311.pyc,, +click/_compat.py,sha256=5318agQpbt4kroKsbqDOYpTSWzL_YCZVUQiTT04yXmc,18744 +click/_termui_impl.py,sha256=3dFYv4445Nw-rFvZOTBMBPYwB1bxnmNk9Du6Dm_oBSU,24069 +click/_textwrap.py,sha256=10fQ64OcBUMuK7mFvh8363_uoOxPlRItZBmKzRJDgoY,1353 +click/_winconsole.py,sha256=5ju3jQkcZD0W27WEMGqmEP4y_crUVzPCqsX_FYb7BO0,7860 +click/core.py,sha256=j6oEWtGgGna8JarD6WxhXmNnxLnfRjwXglbBc-8jr7U,114086 +click/decorators.py,sha256=-ZlbGYgV-oI8jr_oH4RpuL1PFS-5QmeuEAsLDAYgxtw,18719 +click/exceptions.py,sha256=fyROO-47HWFDjt2qupo7A3J32VlpM-ovJnfowu92K3s,9273 +click/formatting.py,sha256=Frf0-5W33-loyY_i9qrwXR8-STnW3m5gvyxLVUdyxyk,9706 +click/globals.py,sha256=TP-qM88STzc7f127h35TD_v920FgfOD2EwzqA0oE8XU,1961 +click/parser.py,sha256=LKyYQE9ZLj5KgIDXkrcTHQRXIggfoivX14_UVIn56YA,19067 +click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +click/shell_completion.py,sha256=Ty3VM_ts0sQhj6u7eFTiLwHPoTgcXTGEAUg2OpLqYKw,18460 +click/termui.py,sha256=H7Q8FpmPelhJ2ovOhfCRhjMtCpNyjFXryAMLZODqsdc,28324 +click/testing.py,sha256=1Qd4kS5bucn1hsNIRryd0WtTMuCpkA93grkWxT8POsU,16084 +click/types.py,sha256=TZvz3hKvBztf-Hpa2enOmP4eznSPLzijjig5b_0XMxE,36391 +click/utils.py,sha256=1476UduUNY6UePGU4m18uzVHLt1sKM2PP3yWsQhbItM,20298 diff --git a/pkg/python/embed/python-darwin-arm64.dummy b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/REQUESTED similarity index 100% rename from pkg/python/embed/python-darwin-arm64.dummy rename to lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/REQUESTED diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/WHEEL b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/WHEEL new file mode 100644 index 000000000..2c08da084 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/top_level.txt b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/top_level.txt new file mode 100644 index 000000000..dca9a9096 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click-8.1.7.dist-info/top_level.txt @@ -0,0 +1 @@ +click diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/__init__.py b/lib/go-jinja2/internal/data/darwin-amd64/click/__init__.py new file mode 100644 index 000000000..9a1dab048 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/__init__.py @@ -0,0 +1,73 @@ +""" +Click is a simple Python module inspired by the stdlib optparse to make +writing command line scripts fun. Unlike other modules, it's based +around a simple API that does not come with too much magic and is +composable. +""" +from .core import Argument as Argument +from .core import BaseCommand as BaseCommand +from .core import Command as Command +from .core import CommandCollection as CommandCollection +from .core import Context as Context +from .core import Group as Group +from .core import MultiCommand as MultiCommand +from .core import Option as Option +from .core import Parameter as Parameter +from .decorators import argument as argument +from .decorators import command as command +from .decorators import confirmation_option as confirmation_option +from .decorators import group as group +from .decorators import help_option as help_option +from .decorators import make_pass_decorator as make_pass_decorator +from .decorators import option as option +from .decorators import pass_context as pass_context +from .decorators import pass_obj as pass_obj +from .decorators import password_option as password_option +from .decorators import version_option as version_option +from .exceptions import Abort as Abort +from .exceptions import BadArgumentUsage as BadArgumentUsage +from .exceptions import BadOptionUsage as BadOptionUsage +from .exceptions import BadParameter as BadParameter +from .exceptions import ClickException as ClickException +from .exceptions import FileError as FileError +from .exceptions import MissingParameter as MissingParameter +from .exceptions import NoSuchOption as NoSuchOption +from .exceptions import UsageError as UsageError +from .formatting import HelpFormatter as HelpFormatter +from .formatting import wrap_text as wrap_text +from .globals import get_current_context as get_current_context +from .parser import OptionParser as OptionParser +from .termui import clear as clear +from .termui import confirm as confirm +from .termui import echo_via_pager as echo_via_pager +from .termui import edit as edit +from .termui import getchar as getchar +from .termui import launch as launch +from .termui import pause as pause +from .termui import progressbar as progressbar +from .termui import prompt as prompt +from .termui import secho as secho +from .termui import style as style +from .termui import unstyle as unstyle +from .types import BOOL as BOOL +from .types import Choice as Choice +from .types import DateTime as DateTime +from .types import File as File +from .types import FLOAT as FLOAT +from .types import FloatRange as FloatRange +from .types import INT as INT +from .types import IntRange as IntRange +from .types import ParamType as ParamType +from .types import Path as Path +from .types import STRING as STRING +from .types import Tuple as Tuple +from .types import UNPROCESSED as UNPROCESSED +from .types import UUID as UUID +from .utils import echo as echo +from .utils import format_filename as format_filename +from .utils import get_app_dir as get_app_dir +from .utils import get_binary_stream as get_binary_stream +from .utils import get_text_stream as get_text_stream +from .utils import open_file as open_file + +__version__ = "8.1.7" diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/_compat.py b/lib/go-jinja2/internal/data/darwin-amd64/click/_compat.py new file mode 100644 index 000000000..23f886659 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/_compat.py @@ -0,0 +1,623 @@ +import codecs +import io +import os +import re +import sys +import typing as t +from weakref import WeakKeyDictionary + +CYGWIN = sys.platform.startswith("cygwin") +WIN = sys.platform.startswith("win") +auto_wrap_for_ansi: t.Optional[t.Callable[[t.TextIO], t.TextIO]] = None +_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") + + +def _make_text_stream( + stream: t.BinaryIO, + encoding: t.Optional[str], + errors: t.Optional[str], + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: + if encoding is None: + encoding = get_best_encoding(stream) + if errors is None: + errors = "replace" + return _NonClosingTextIOWrapper( + stream, + encoding, + errors, + line_buffering=True, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def is_ascii_encoding(encoding: str) -> bool: + """Checks if a given encoding is ascii.""" + try: + return codecs.lookup(encoding).name == "ascii" + except LookupError: + return False + + +def get_best_encoding(stream: t.IO[t.Any]) -> str: + """Returns the default stream encoding if not found.""" + rv = getattr(stream, "encoding", None) or sys.getdefaultencoding() + if is_ascii_encoding(rv): + return "utf-8" + return rv + + +class _NonClosingTextIOWrapper(io.TextIOWrapper): + def __init__( + self, + stream: t.BinaryIO, + encoding: t.Optional[str], + errors: t.Optional[str], + force_readable: bool = False, + force_writable: bool = False, + **extra: t.Any, + ) -> None: + self._stream = stream = t.cast( + t.BinaryIO, _FixupStream(stream, force_readable, force_writable) + ) + super().__init__(stream, encoding, errors, **extra) + + def __del__(self) -> None: + try: + self.detach() + except Exception: + pass + + def isatty(self) -> bool: + # https://bitbucket.org/pypy/pypy/issue/1803 + return self._stream.isatty() + + +class _FixupStream: + """The new io interface needs more from streams than streams + traditionally implement. As such, this fix-up code is necessary in + some circumstances. + + The forcing of readable and writable flags are there because some tools + put badly patched objects on sys (one such offender are certain version + of jupyter notebook). + """ + + def __init__( + self, + stream: t.BinaryIO, + force_readable: bool = False, + force_writable: bool = False, + ): + self._stream = stream + self._force_readable = force_readable + self._force_writable = force_writable + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._stream, name) + + def read1(self, size: int) -> bytes: + f = getattr(self._stream, "read1", None) + + if f is not None: + return t.cast(bytes, f(size)) + + return self._stream.read(size) + + def readable(self) -> bool: + if self._force_readable: + return True + x = getattr(self._stream, "readable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.read(0) + except Exception: + return False + return True + + def writable(self) -> bool: + if self._force_writable: + return True + x = getattr(self._stream, "writable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.write("") # type: ignore + except Exception: + try: + self._stream.write(b"") + except Exception: + return False + return True + + def seekable(self) -> bool: + x = getattr(self._stream, "seekable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.seek(self._stream.tell()) + except Exception: + return False + return True + + +def _is_binary_reader(stream: t.IO[t.Any], default: bool = False) -> bool: + try: + return isinstance(stream.read(0), bytes) + except Exception: + return default + # This happens in some cases where the stream was already + # closed. In this case, we assume the default. + + +def _is_binary_writer(stream: t.IO[t.Any], default: bool = False) -> bool: + try: + stream.write(b"") + except Exception: + try: + stream.write("") + return False + except Exception: + pass + return default + return True + + +def _find_binary_reader(stream: t.IO[t.Any]) -> t.Optional[t.BinaryIO]: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_reader(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_reader(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _find_binary_writer(stream: t.IO[t.Any]) -> t.Optional[t.BinaryIO]: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_writer(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_writer(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _stream_is_misconfigured(stream: t.TextIO) -> bool: + """A stream is misconfigured if its encoding is ASCII.""" + # If the stream does not have an encoding set, we assume it's set + # to ASCII. This appears to happen in certain unittest + # environments. It's not quite clear what the correct behavior is + # but this at least will force Click to recover somehow. + return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii") + + +def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: t.Optional[str]) -> bool: + """A stream attribute is compatible if it is equal to the + desired value or the desired value is unset and the attribute + has a value. + """ + stream_value = getattr(stream, attr, None) + return stream_value == value or (value is None and stream_value is not None) + + +def _is_compatible_text_stream( + stream: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str] +) -> bool: + """Check if a stream's encoding and errors attributes are + compatible with the desired values. + """ + return _is_compat_stream_attr( + stream, "encoding", encoding + ) and _is_compat_stream_attr(stream, "errors", errors) + + +def _force_correct_text_stream( + text_stream: t.IO[t.Any], + encoding: t.Optional[str], + errors: t.Optional[str], + is_binary: t.Callable[[t.IO[t.Any], bool], bool], + find_binary: t.Callable[[t.IO[t.Any]], t.Optional[t.BinaryIO]], + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: + if is_binary(text_stream, False): + binary_reader = t.cast(t.BinaryIO, text_stream) + else: + text_stream = t.cast(t.TextIO, text_stream) + # If the stream looks compatible, and won't default to a + # misconfigured ascii encoding, return it as-is. + if _is_compatible_text_stream(text_stream, encoding, errors) and not ( + encoding is None and _stream_is_misconfigured(text_stream) + ): + return text_stream + + # Otherwise, get the underlying binary reader. + possible_binary_reader = find_binary(text_stream) + + # If that's not possible, silently use the original reader + # and get mojibake instead of exceptions. + if possible_binary_reader is None: + return text_stream + + binary_reader = possible_binary_reader + + # Default errors to replace instead of strict in order to get + # something that works. + if errors is None: + errors = "replace" + + # Wrap the binary stream in a text stream with the correct + # encoding parameters. + return _make_text_stream( + binary_reader, + encoding, + errors, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def _force_correct_text_reader( + text_reader: t.IO[t.Any], + encoding: t.Optional[str], + errors: t.Optional[str], + force_readable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_reader, + encoding, + errors, + _is_binary_reader, + _find_binary_reader, + force_readable=force_readable, + ) + + +def _force_correct_text_writer( + text_writer: t.IO[t.Any], + encoding: t.Optional[str], + errors: t.Optional[str], + force_writable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_writer, + encoding, + errors, + _is_binary_writer, + _find_binary_writer, + force_writable=force_writable, + ) + + +def get_binary_stdin() -> t.BinaryIO: + reader = _find_binary_reader(sys.stdin) + if reader is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdin.") + return reader + + +def get_binary_stdout() -> t.BinaryIO: + writer = _find_binary_writer(sys.stdout) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdout.") + return writer + + +def get_binary_stderr() -> t.BinaryIO: + writer = _find_binary_writer(sys.stderr) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stderr.") + return writer + + +def get_text_stdin( + encoding: t.Optional[str] = None, errors: t.Optional[str] = None +) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True) + + +def get_text_stdout( + encoding: t.Optional[str] = None, errors: t.Optional[str] = None +) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True) + + +def get_text_stderr( + encoding: t.Optional[str] = None, errors: t.Optional[str] = None +) -> t.TextIO: + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True) + + +def _wrap_io_open( + file: t.Union[str, "os.PathLike[str]", int], + mode: str, + encoding: t.Optional[str], + errors: t.Optional[str], +) -> t.IO[t.Any]: + """Handles not passing ``encoding`` and ``errors`` in binary mode.""" + if "b" in mode: + return open(file, mode) + + return open(file, mode, encoding=encoding, errors=errors) + + +def open_stream( + filename: "t.Union[str, os.PathLike[str]]", + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + atomic: bool = False, +) -> t.Tuple[t.IO[t.Any], bool]: + binary = "b" in mode + filename = os.fspath(filename) + + # Standard streams first. These are simple because they ignore the + # atomic flag. Use fsdecode to handle Path("-"). + if os.fsdecode(filename) == "-": + if any(m in mode for m in ["w", "a", "x"]): + if binary: + return get_binary_stdout(), False + return get_text_stdout(encoding=encoding, errors=errors), False + if binary: + return get_binary_stdin(), False + return get_text_stdin(encoding=encoding, errors=errors), False + + # Non-atomic writes directly go out through the regular open functions. + if not atomic: + return _wrap_io_open(filename, mode, encoding, errors), True + + # Some usability stuff for atomic writes + if "a" in mode: + raise ValueError( + "Appending to an existing file is not supported, because that" + " would involve an expensive `copy`-operation to a temporary" + " file. Open the file in normal `w`-mode and copy explicitly" + " if that's what you're after." + ) + if "x" in mode: + raise ValueError("Use the `overwrite`-parameter instead.") + if "w" not in mode: + raise ValueError("Atomic writes only make sense with `w`-mode.") + + # Atomic writes are more complicated. They work by opening a file + # as a proxy in the same folder and then using the fdopen + # functionality to wrap it in a Python file. Then we wrap it in an + # atomic file that moves the file over on close. + import errno + import random + + try: + perm: t.Optional[int] = os.stat(filename).st_mode + except OSError: + perm = None + + flags = os.O_RDWR | os.O_CREAT | os.O_EXCL + + if binary: + flags |= getattr(os, "O_BINARY", 0) + + while True: + tmp_filename = os.path.join( + os.path.dirname(filename), + f".__atomic-write{random.randrange(1 << 32):08x}", + ) + try: + fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm) + break + except OSError as e: + if e.errno == errno.EEXIST or ( + os.name == "nt" + and e.errno == errno.EACCES + and os.path.isdir(e.filename) + and os.access(e.filename, os.W_OK) + ): + continue + raise + + if perm is not None: + os.chmod(tmp_filename, perm) # in case perm includes bits in umask + + f = _wrap_io_open(fd, mode, encoding, errors) + af = _AtomicFile(f, tmp_filename, os.path.realpath(filename)) + return t.cast(t.IO[t.Any], af), True + + +class _AtomicFile: + def __init__(self, f: t.IO[t.Any], tmp_filename: str, real_filename: str) -> None: + self._f = f + self._tmp_filename = tmp_filename + self._real_filename = real_filename + self.closed = False + + @property + def name(self) -> str: + return self._real_filename + + def close(self, delete: bool = False) -> None: + if self.closed: + return + self._f.close() + os.replace(self._tmp_filename, self._real_filename) + self.closed = True + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._f, name) + + def __enter__(self) -> "_AtomicFile": + return self + + def __exit__(self, exc_type: t.Optional[t.Type[BaseException]], *_: t.Any) -> None: + self.close(delete=exc_type is not None) + + def __repr__(self) -> str: + return repr(self._f) + + +def strip_ansi(value: str) -> str: + return _ansi_re.sub("", value) + + +def _is_jupyter_kernel_output(stream: t.IO[t.Any]) -> bool: + while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)): + stream = stream._stream + + return stream.__class__.__module__.startswith("ipykernel.") + + +def should_strip_ansi( + stream: t.Optional[t.IO[t.Any]] = None, color: t.Optional[bool] = None +) -> bool: + if color is None: + if stream is None: + stream = sys.stdin + return not isatty(stream) and not _is_jupyter_kernel_output(stream) + return not color + + +# On Windows, wrap the output streams with colorama to support ANSI +# color codes. +# NOTE: double check is needed so mypy does not analyze this on Linux +if sys.platform.startswith("win") and WIN: + from ._winconsole import _get_windows_console_stream + + def _get_argv_encoding() -> str: + import locale + + return locale.getpreferredencoding() + + _ansi_stream_wrappers: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() + + def auto_wrap_for_ansi( # noqa: F811 + stream: t.TextIO, color: t.Optional[bool] = None + ) -> t.TextIO: + """Support ANSI color and style codes on Windows by wrapping a + stream with colorama. + """ + try: + cached = _ansi_stream_wrappers.get(stream) + except Exception: + cached = None + + if cached is not None: + return cached + + import colorama + + strip = should_strip_ansi(stream, color) + ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip) + rv = t.cast(t.TextIO, ansi_wrapper.stream) + _write = rv.write + + def _safe_write(s): + try: + return _write(s) + except BaseException: + ansi_wrapper.reset_all() + raise + + rv.write = _safe_write + + try: + _ansi_stream_wrappers[stream] = rv + except Exception: + pass + + return rv + +else: + + def _get_argv_encoding() -> str: + return getattr(sys.stdin, "encoding", None) or sys.getfilesystemencoding() + + def _get_windows_console_stream( + f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str] + ) -> t.Optional[t.TextIO]: + return None + + +def term_len(x: str) -> int: + return len(strip_ansi(x)) + + +def isatty(stream: t.IO[t.Any]) -> bool: + try: + return stream.isatty() + except Exception: + return False + + +def _make_cached_stream_func( + src_func: t.Callable[[], t.Optional[t.TextIO]], + wrapper_func: t.Callable[[], t.TextIO], +) -> t.Callable[[], t.Optional[t.TextIO]]: + cache: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() + + def func() -> t.Optional[t.TextIO]: + stream = src_func() + + if stream is None: + return None + + try: + rv = cache.get(stream) + except Exception: + rv = None + if rv is not None: + return rv + rv = wrapper_func() + try: + cache[stream] = rv + except Exception: + pass + return rv + + return func + + +_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin) +_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout) +_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr) + + +binary_streams: t.Mapping[str, t.Callable[[], t.BinaryIO]] = { + "stdin": get_binary_stdin, + "stdout": get_binary_stdout, + "stderr": get_binary_stderr, +} + +text_streams: t.Mapping[ + str, t.Callable[[t.Optional[str], t.Optional[str]], t.TextIO] +] = { + "stdin": get_text_stdin, + "stdout": get_text_stdout, + "stderr": get_text_stderr, +} diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/_termui_impl.py b/lib/go-jinja2/internal/data/darwin-amd64/click/_termui_impl.py new file mode 100644 index 000000000..f74465775 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/_termui_impl.py @@ -0,0 +1,739 @@ +""" +This module contains implementations for the termui module. To keep the +import time of Click down, some infrequently used functionality is +placed in this module and only imported as needed. +""" +import contextlib +import math +import os +import sys +import time +import typing as t +from gettext import gettext as _ +from io import StringIO +from types import TracebackType + +from ._compat import _default_text_stdout +from ._compat import CYGWIN +from ._compat import get_best_encoding +from ._compat import isatty +from ._compat import open_stream +from ._compat import strip_ansi +from ._compat import term_len +from ._compat import WIN +from .exceptions import ClickException +from .utils import echo + +V = t.TypeVar("V") + +if os.name == "nt": + BEFORE_BAR = "\r" + AFTER_BAR = "\n" +else: + BEFORE_BAR = "\r\033[?25l" + AFTER_BAR = "\033[?25h\n" + + +class ProgressBar(t.Generic[V]): + def __init__( + self, + iterable: t.Optional[t.Iterable[V]], + length: t.Optional[int] = None, + fill_char: str = "#", + empty_char: str = " ", + bar_template: str = "%(bar)s", + info_sep: str = " ", + show_eta: bool = True, + show_percent: t.Optional[bool] = None, + show_pos: bool = False, + item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None, + label: t.Optional[str] = None, + file: t.Optional[t.TextIO] = None, + color: t.Optional[bool] = None, + update_min_steps: int = 1, + width: int = 30, + ) -> None: + self.fill_char = fill_char + self.empty_char = empty_char + self.bar_template = bar_template + self.info_sep = info_sep + self.show_eta = show_eta + self.show_percent = show_percent + self.show_pos = show_pos + self.item_show_func = item_show_func + self.label: str = label or "" + + if file is None: + file = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if file is None: + file = StringIO() + + self.file = file + self.color = color + self.update_min_steps = update_min_steps + self._completed_intervals = 0 + self.width: int = width + self.autowidth: bool = width == 0 + + if length is None: + from operator import length_hint + + length = length_hint(iterable, -1) + + if length == -1: + length = None + if iterable is None: + if length is None: + raise TypeError("iterable or length is required") + iterable = t.cast(t.Iterable[V], range(length)) + self.iter: t.Iterable[V] = iter(iterable) + self.length = length + self.pos = 0 + self.avg: t.List[float] = [] + self.last_eta: float + self.start: float + self.start = self.last_eta = time.time() + self.eta_known: bool = False + self.finished: bool = False + self.max_width: t.Optional[int] = None + self.entered: bool = False + self.current_item: t.Optional[V] = None + self.is_hidden: bool = not isatty(self.file) + self._last_line: t.Optional[str] = None + + def __enter__(self) -> "ProgressBar[V]": + self.entered = True + self.render_progress() + return self + + def __exit__( + self, + exc_type: t.Optional[t.Type[BaseException]], + exc_value: t.Optional[BaseException], + tb: t.Optional[TracebackType], + ) -> None: + self.render_finish() + + def __iter__(self) -> t.Iterator[V]: + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + self.render_progress() + return self.generator() + + def __next__(self) -> V: + # Iteration is defined in terms of a generator function, + # returned by iter(self); use that to define next(). This works + # because `self.iter` is an iterable consumed by that generator, + # so it is re-entry safe. Calling `next(self.generator())` + # twice works and does "what you want". + return next(iter(self)) + + def render_finish(self) -> None: + if self.is_hidden: + return + self.file.write(AFTER_BAR) + self.file.flush() + + @property + def pct(self) -> float: + if self.finished: + return 1.0 + return min(self.pos / (float(self.length or 1) or 1), 1.0) + + @property + def time_per_iteration(self) -> float: + if not self.avg: + return 0.0 + return sum(self.avg) / float(len(self.avg)) + + @property + def eta(self) -> float: + if self.length is not None and not self.finished: + return self.time_per_iteration * (self.length - self.pos) + return 0.0 + + def format_eta(self) -> str: + if self.eta_known: + t = int(self.eta) + seconds = t % 60 + t //= 60 + minutes = t % 60 + t //= 60 + hours = t % 24 + t //= 24 + if t > 0: + return f"{t}d {hours:02}:{minutes:02}:{seconds:02}" + else: + return f"{hours:02}:{minutes:02}:{seconds:02}" + return "" + + def format_pos(self) -> str: + pos = str(self.pos) + if self.length is not None: + pos += f"/{self.length}" + return pos + + def format_pct(self) -> str: + return f"{int(self.pct * 100): 4}%"[1:] + + def format_bar(self) -> str: + if self.length is not None: + bar_length = int(self.pct * self.width) + bar = self.fill_char * bar_length + bar += self.empty_char * (self.width - bar_length) + elif self.finished: + bar = self.fill_char * self.width + else: + chars = list(self.empty_char * (self.width or 1)) + if self.time_per_iteration != 0: + chars[ + int( + (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5) + * self.width + ) + ] = self.fill_char + bar = "".join(chars) + return bar + + def format_progress_line(self) -> str: + show_percent = self.show_percent + + info_bits = [] + if self.length is not None and show_percent is None: + show_percent = not self.show_pos + + if self.show_pos: + info_bits.append(self.format_pos()) + if show_percent: + info_bits.append(self.format_pct()) + if self.show_eta and self.eta_known and not self.finished: + info_bits.append(self.format_eta()) + if self.item_show_func is not None: + item_info = self.item_show_func(self.current_item) + if item_info is not None: + info_bits.append(item_info) + + return ( + self.bar_template + % { + "label": self.label, + "bar": self.format_bar(), + "info": self.info_sep.join(info_bits), + } + ).rstrip() + + def render_progress(self) -> None: + import shutil + + if self.is_hidden: + # Only output the label as it changes if the output is not a + # TTY. Use file=stderr if you expect to be piping stdout. + if self._last_line != self.label: + self._last_line = self.label + echo(self.label, file=self.file, color=self.color) + + return + + buf = [] + # Update width in case the terminal has been resized + if self.autowidth: + old_width = self.width + self.width = 0 + clutter_length = term_len(self.format_progress_line()) + new_width = max(0, shutil.get_terminal_size().columns - clutter_length) + if new_width < old_width: + buf.append(BEFORE_BAR) + buf.append(" " * self.max_width) # type: ignore + self.max_width = new_width + self.width = new_width + + clear_width = self.width + if self.max_width is not None: + clear_width = self.max_width + + buf.append(BEFORE_BAR) + line = self.format_progress_line() + line_len = term_len(line) + if self.max_width is None or self.max_width < line_len: + self.max_width = line_len + + buf.append(line) + buf.append(" " * (clear_width - line_len)) + line = "".join(buf) + # Render the line only if it changed. + + if line != self._last_line: + self._last_line = line + echo(line, file=self.file, color=self.color, nl=False) + self.file.flush() + + def make_step(self, n_steps: int) -> None: + self.pos += n_steps + if self.length is not None and self.pos >= self.length: + self.finished = True + + if (time.time() - self.last_eta) < 1.0: + return + + self.last_eta = time.time() + + # self.avg is a rolling list of length <= 7 of steps where steps are + # defined as time elapsed divided by the total progress through + # self.length. + if self.pos: + step = (time.time() - self.start) / self.pos + else: + step = time.time() - self.start + + self.avg = self.avg[-6:] + [step] + + self.eta_known = self.length is not None + + def update(self, n_steps: int, current_item: t.Optional[V] = None) -> None: + """Update the progress bar by advancing a specified number of + steps, and optionally set the ``current_item`` for this new + position. + + :param n_steps: Number of steps to advance. + :param current_item: Optional item to set as ``current_item`` + for the updated position. + + .. versionchanged:: 8.0 + Added the ``current_item`` optional parameter. + + .. versionchanged:: 8.0 + Only render when the number of steps meets the + ``update_min_steps`` threshold. + """ + if current_item is not None: + self.current_item = current_item + + self._completed_intervals += n_steps + + if self._completed_intervals >= self.update_min_steps: + self.make_step(self._completed_intervals) + self.render_progress() + self._completed_intervals = 0 + + def finish(self) -> None: + self.eta_known = False + self.current_item = None + self.finished = True + + def generator(self) -> t.Iterator[V]: + """Return a generator which yields the items added to the bar + during construction, and updates the progress bar *after* the + yielded block returns. + """ + # WARNING: the iterator interface for `ProgressBar` relies on + # this and only works because this is a simple generator which + # doesn't create or manage additional state. If this function + # changes, the impact should be evaluated both against + # `iter(bar)` and `next(bar)`. `next()` in particular may call + # `self.generator()` repeatedly, and this must remain safe in + # order for that interface to work. + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + + if self.is_hidden: + yield from self.iter + else: + for rv in self.iter: + self.current_item = rv + + # This allows show_item_func to be updated before the + # item is processed. Only trigger at the beginning of + # the update interval. + if self._completed_intervals == 0: + self.render_progress() + + yield rv + self.update(1) + + self.finish() + self.render_progress() + + +def pager(generator: t.Iterable[str], color: t.Optional[bool] = None) -> None: + """Decide what method to use for paging through text.""" + stdout = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if stdout is None: + stdout = StringIO() + + if not isatty(sys.stdin) or not isatty(stdout): + return _nullpager(stdout, generator, color) + pager_cmd = (os.environ.get("PAGER", None) or "").strip() + if pager_cmd: + if WIN: + return _tempfilepager(generator, pager_cmd, color) + return _pipepager(generator, pager_cmd, color) + if os.environ.get("TERM") in ("dumb", "emacs"): + return _nullpager(stdout, generator, color) + if WIN or sys.platform.startswith("os2"): + return _tempfilepager(generator, "more <", color) + if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0: + return _pipepager(generator, "less", color) + + import tempfile + + fd, filename = tempfile.mkstemp() + os.close(fd) + try: + if hasattr(os, "system") and os.system(f'more "{filename}"') == 0: + return _pipepager(generator, "more", color) + return _nullpager(stdout, generator, color) + finally: + os.unlink(filename) + + +def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> None: + """Page through text by feeding it to another program. Invoking a + pager through this might support colors. + """ + import subprocess + + env = dict(os.environ) + + # If we're piping to less we might support colors under the + # condition that + cmd_detail = cmd.rsplit("/", 1)[-1].split() + if color is None and cmd_detail[0] == "less": + less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_detail[1:])}" + if not less_flags: + env["LESS"] = "-R" + color = True + elif "r" in less_flags or "R" in less_flags: + color = True + + c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env) + stdin = t.cast(t.BinaryIO, c.stdin) + encoding = get_best_encoding(stdin) + try: + for text in generator: + if not color: + text = strip_ansi(text) + + stdin.write(text.encode(encoding, "replace")) + except (OSError, KeyboardInterrupt): + pass + else: + stdin.close() + + # Less doesn't respect ^C, but catches it for its own UI purposes (aborting + # search or other commands inside less). + # + # That means when the user hits ^C, the parent process (click) terminates, + # but less is still alive, paging the output and messing up the terminal. + # + # If the user wants to make the pager exit on ^C, they should set + # `LESS='-K'`. It's not our decision to make. + while True: + try: + c.wait() + except KeyboardInterrupt: + pass + else: + break + + +def _tempfilepager( + generator: t.Iterable[str], cmd: str, color: t.Optional[bool] +) -> None: + """Page through text by invoking a program on a temporary file.""" + import tempfile + + fd, filename = tempfile.mkstemp() + # TODO: This never terminates if the passed generator never terminates. + text = "".join(generator) + if not color: + text = strip_ansi(text) + encoding = get_best_encoding(sys.stdout) + with open_stream(filename, "wb")[0] as f: + f.write(text.encode(encoding)) + try: + os.system(f'{cmd} "{filename}"') + finally: + os.close(fd) + os.unlink(filename) + + +def _nullpager( + stream: t.TextIO, generator: t.Iterable[str], color: t.Optional[bool] +) -> None: + """Simply print unformatted text. This is the ultimate fallback.""" + for text in generator: + if not color: + text = strip_ansi(text) + stream.write(text) + + +class Editor: + def __init__( + self, + editor: t.Optional[str] = None, + env: t.Optional[t.Mapping[str, str]] = None, + require_save: bool = True, + extension: str = ".txt", + ) -> None: + self.editor = editor + self.env = env + self.require_save = require_save + self.extension = extension + + def get_editor(self) -> str: + if self.editor is not None: + return self.editor + for key in "VISUAL", "EDITOR": + rv = os.environ.get(key) + if rv: + return rv + if WIN: + return "notepad" + for editor in "sensible-editor", "vim", "nano": + if os.system(f"which {editor} >/dev/null 2>&1") == 0: + return editor + return "vi" + + def edit_file(self, filename: str) -> None: + import subprocess + + editor = self.get_editor() + environ: t.Optional[t.Dict[str, str]] = None + + if self.env: + environ = os.environ.copy() + environ.update(self.env) + + try: + c = subprocess.Popen(f'{editor} "{filename}"', env=environ, shell=True) + exit_code = c.wait() + if exit_code != 0: + raise ClickException( + _("{editor}: Editing failed").format(editor=editor) + ) + except OSError as e: + raise ClickException( + _("{editor}: Editing failed: {e}").format(editor=editor, e=e) + ) from e + + def edit(self, text: t.Optional[t.AnyStr]) -> t.Optional[t.AnyStr]: + import tempfile + + if not text: + data = b"" + elif isinstance(text, (bytes, bytearray)): + data = text + else: + if text and not text.endswith("\n"): + text += "\n" + + if WIN: + data = text.replace("\n", "\r\n").encode("utf-8-sig") + else: + data = text.encode("utf-8") + + fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension) + f: t.BinaryIO + + try: + with os.fdopen(fd, "wb") as f: + f.write(data) + + # If the filesystem resolution is 1 second, like Mac OS + # 10.12 Extended, or 2 seconds, like FAT32, and the editor + # closes very fast, require_save can fail. Set the modified + # time to be 2 seconds in the past to work around this. + os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2)) + # Depending on the resolution, the exact value might not be + # recorded, so get the new recorded value. + timestamp = os.path.getmtime(name) + + self.edit_file(name) + + if self.require_save and os.path.getmtime(name) == timestamp: + return None + + with open(name, "rb") as f: + rv = f.read() + + if isinstance(text, (bytes, bytearray)): + return rv + + return rv.decode("utf-8-sig").replace("\r\n", "\n") # type: ignore + finally: + os.unlink(name) + + +def open_url(url: str, wait: bool = False, locate: bool = False) -> int: + import subprocess + + def _unquote_file(url: str) -> str: + from urllib.parse import unquote + + if url.startswith("file://"): + url = unquote(url[7:]) + + return url + + if sys.platform == "darwin": + args = ["open"] + if wait: + args.append("-W") + if locate: + args.append("-R") + args.append(_unquote_file(url)) + null = open("/dev/null", "w") + try: + return subprocess.Popen(args, stderr=null).wait() + finally: + null.close() + elif WIN: + if locate: + url = _unquote_file(url.replace('"', "")) + args = f'explorer /select,"{url}"' + else: + url = url.replace('"', "") + wait_str = "/WAIT" if wait else "" + args = f'start {wait_str} "" "{url}"' + return os.system(args) + elif CYGWIN: + if locate: + url = os.path.dirname(_unquote_file(url).replace('"', "")) + args = f'cygstart "{url}"' + else: + url = url.replace('"', "") + wait_str = "-w" if wait else "" + args = f'cygstart {wait_str} "{url}"' + return os.system(args) + + try: + if locate: + url = os.path.dirname(_unquote_file(url)) or "." + else: + url = _unquote_file(url) + c = subprocess.Popen(["xdg-open", url]) + if wait: + return c.wait() + return 0 + except OSError: + if url.startswith(("http://", "https://")) and not locate and not wait: + import webbrowser + + webbrowser.open(url) + return 0 + return 1 + + +def _translate_ch_to_exc(ch: str) -> t.Optional[BaseException]: + if ch == "\x03": + raise KeyboardInterrupt() + + if ch == "\x04" and not WIN: # Unix-like, Ctrl+D + raise EOFError() + + if ch == "\x1a" and WIN: # Windows, Ctrl+Z + raise EOFError() + + return None + + +if WIN: + import msvcrt + + @contextlib.contextmanager + def raw_terminal() -> t.Iterator[int]: + yield -1 + + def getchar(echo: bool) -> str: + # The function `getch` will return a bytes object corresponding to + # the pressed character. Since Windows 10 build 1803, it will also + # return \x00 when called a second time after pressing a regular key. + # + # `getwch` does not share this probably-bugged behavior. Moreover, it + # returns a Unicode object by default, which is what we want. + # + # Either of these functions will return \x00 or \xe0 to indicate + # a special key, and you need to call the same function again to get + # the "rest" of the code. The fun part is that \u00e0 is + # "latin small letter a with grave", so if you type that on a French + # keyboard, you _also_ get a \xe0. + # E.g., consider the Up arrow. This returns \xe0 and then \x48. The + # resulting Unicode string reads as "a with grave" + "capital H". + # This is indistinguishable from when the user actually types + # "a with grave" and then "capital H". + # + # When \xe0 is returned, we assume it's part of a special-key sequence + # and call `getwch` again, but that means that when the user types + # the \u00e0 character, `getchar` doesn't return until a second + # character is typed. + # The alternative is returning immediately, but that would mess up + # cross-platform handling of arrow keys and others that start with + # \xe0. Another option is using `getch`, but then we can't reliably + # read non-ASCII characters, because return values of `getch` are + # limited to the current 8-bit codepage. + # + # Anyway, Click doesn't claim to do this Right(tm), and using `getwch` + # is doing the right thing in more situations than with `getch`. + func: t.Callable[[], str] + + if echo: + func = msvcrt.getwche # type: ignore + else: + func = msvcrt.getwch # type: ignore + + rv = func() + + if rv in ("\x00", "\xe0"): + # \x00 and \xe0 are control characters that indicate special key, + # see above. + rv += func() + + _translate_ch_to_exc(rv) + return rv + +else: + import tty + import termios + + @contextlib.contextmanager + def raw_terminal() -> t.Iterator[int]: + f: t.Optional[t.TextIO] + fd: int + + if not isatty(sys.stdin): + f = open("/dev/tty") + fd = f.fileno() + else: + fd = sys.stdin.fileno() + f = None + + try: + old_settings = termios.tcgetattr(fd) + + try: + tty.setraw(fd) + yield fd + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + sys.stdout.flush() + + if f is not None: + f.close() + except termios.error: + pass + + def getchar(echo: bool) -> str: + with raw_terminal() as fd: + ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace") + + if echo and isatty(sys.stdout): + sys.stdout.write(ch) + + _translate_ch_to_exc(ch) + return ch diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/_textwrap.py b/lib/go-jinja2/internal/data/darwin-amd64/click/_textwrap.py new file mode 100644 index 000000000..b47dcbd42 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/_textwrap.py @@ -0,0 +1,49 @@ +import textwrap +import typing as t +from contextlib import contextmanager + + +class TextWrapper(textwrap.TextWrapper): + def _handle_long_word( + self, + reversed_chunks: t.List[str], + cur_line: t.List[str], + cur_len: int, + width: int, + ) -> None: + space_left = max(width - cur_len, 1) + + if self.break_long_words: + last = reversed_chunks[-1] + cut = last[:space_left] + res = last[space_left:] + cur_line.append(cut) + reversed_chunks[-1] = res + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + @contextmanager + def extra_indent(self, indent: str) -> t.Iterator[None]: + old_initial_indent = self.initial_indent + old_subsequent_indent = self.subsequent_indent + self.initial_indent += indent + self.subsequent_indent += indent + + try: + yield + finally: + self.initial_indent = old_initial_indent + self.subsequent_indent = old_subsequent_indent + + def indent_only(self, text: str) -> str: + rv = [] + + for idx, line in enumerate(text.splitlines()): + indent = self.initial_indent + + if idx > 0: + indent = self.subsequent_indent + + rv.append(f"{indent}{line}") + + return "\n".join(rv) diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/_winconsole.py b/lib/go-jinja2/internal/data/darwin-amd64/click/_winconsole.py new file mode 100644 index 000000000..6b20df315 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/_winconsole.py @@ -0,0 +1,279 @@ +# This module is based on the excellent work by Adam Bartoš who +# provided a lot of what went into the implementation here in +# the discussion to issue1602 in the Python bug tracker. +# +# There are some general differences in regards to how this works +# compared to the original patches as we do not need to patch +# the entire interpreter but just work in our little world of +# echo and prompt. +import io +import sys +import time +import typing as t +from ctypes import byref +from ctypes import c_char +from ctypes import c_char_p +from ctypes import c_int +from ctypes import c_ssize_t +from ctypes import c_ulong +from ctypes import c_void_p +from ctypes import POINTER +from ctypes import py_object +from ctypes import Structure +from ctypes.wintypes import DWORD +from ctypes.wintypes import HANDLE +from ctypes.wintypes import LPCWSTR +from ctypes.wintypes import LPWSTR + +from ._compat import _NonClosingTextIOWrapper + +assert sys.platform == "win32" +import msvcrt # noqa: E402 +from ctypes import windll # noqa: E402 +from ctypes import WINFUNCTYPE # noqa: E402 + +c_ssize_p = POINTER(c_ssize_t) + +kernel32 = windll.kernel32 +GetStdHandle = kernel32.GetStdHandle +ReadConsoleW = kernel32.ReadConsoleW +WriteConsoleW = kernel32.WriteConsoleW +GetConsoleMode = kernel32.GetConsoleMode +GetLastError = kernel32.GetLastError +GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32)) +CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( + ("CommandLineToArgvW", windll.shell32) +) +LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32)) + +STDIN_HANDLE = GetStdHandle(-10) +STDOUT_HANDLE = GetStdHandle(-11) +STDERR_HANDLE = GetStdHandle(-12) + +PyBUF_SIMPLE = 0 +PyBUF_WRITABLE = 1 + +ERROR_SUCCESS = 0 +ERROR_NOT_ENOUGH_MEMORY = 8 +ERROR_OPERATION_ABORTED = 995 + +STDIN_FILENO = 0 +STDOUT_FILENO = 1 +STDERR_FILENO = 2 + +EOF = b"\x1a" +MAX_BYTES_WRITTEN = 32767 + +try: + from ctypes import pythonapi +except ImportError: + # On PyPy we cannot get buffers so our ability to operate here is + # severely limited. + get_buffer = None +else: + + class Py_buffer(Structure): + _fields_ = [ + ("buf", c_void_p), + ("obj", py_object), + ("len", c_ssize_t), + ("itemsize", c_ssize_t), + ("readonly", c_int), + ("ndim", c_int), + ("format", c_char_p), + ("shape", c_ssize_p), + ("strides", c_ssize_p), + ("suboffsets", c_ssize_p), + ("internal", c_void_p), + ] + + PyObject_GetBuffer = pythonapi.PyObject_GetBuffer + PyBuffer_Release = pythonapi.PyBuffer_Release + + def get_buffer(obj, writable=False): + buf = Py_buffer() + flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE + PyObject_GetBuffer(py_object(obj), byref(buf), flags) + + try: + buffer_type = c_char * buf.len + return buffer_type.from_address(buf.buf) + finally: + PyBuffer_Release(byref(buf)) + + +class _WindowsConsoleRawIOBase(io.RawIOBase): + def __init__(self, handle): + self.handle = handle + + def isatty(self): + super().isatty() + return True + + +class _WindowsConsoleReader(_WindowsConsoleRawIOBase): + def readable(self): + return True + + def readinto(self, b): + bytes_to_be_read = len(b) + if not bytes_to_be_read: + return 0 + elif bytes_to_be_read % 2: + raise ValueError( + "cannot read odd number of bytes from UTF-16-LE encoded console" + ) + + buffer = get_buffer(b, writable=True) + code_units_to_be_read = bytes_to_be_read // 2 + code_units_read = c_ulong() + + rv = ReadConsoleW( + HANDLE(self.handle), + buffer, + code_units_to_be_read, + byref(code_units_read), + None, + ) + if GetLastError() == ERROR_OPERATION_ABORTED: + # wait for KeyboardInterrupt + time.sleep(0.1) + if not rv: + raise OSError(f"Windows error: {GetLastError()}") + + if buffer[0] == EOF: + return 0 + return 2 * code_units_read.value + + +class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): + def writable(self): + return True + + @staticmethod + def _get_error_message(errno): + if errno == ERROR_SUCCESS: + return "ERROR_SUCCESS" + elif errno == ERROR_NOT_ENOUGH_MEMORY: + return "ERROR_NOT_ENOUGH_MEMORY" + return f"Windows error {errno}" + + def write(self, b): + bytes_to_be_written = len(b) + buf = get_buffer(b) + code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2 + code_units_written = c_ulong() + + WriteConsoleW( + HANDLE(self.handle), + buf, + code_units_to_be_written, + byref(code_units_written), + None, + ) + bytes_written = 2 * code_units_written.value + + if bytes_written == 0 and bytes_to_be_written > 0: + raise OSError(self._get_error_message(GetLastError())) + return bytes_written + + +class ConsoleStream: + def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None: + self._text_stream = text_stream + self.buffer = byte_stream + + @property + def name(self) -> str: + return self.buffer.name + + def write(self, x: t.AnyStr) -> int: + if isinstance(x, str): + return self._text_stream.write(x) + try: + self.flush() + except Exception: + pass + return self.buffer.write(x) + + def writelines(self, lines: t.Iterable[t.AnyStr]) -> None: + for line in lines: + self.write(line) + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._text_stream, name) + + def isatty(self) -> bool: + return self.buffer.isatty() + + def __repr__(self): + return f"" + + +def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +_stream_factories: t.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = { + 0: _get_text_stdin, + 1: _get_text_stdout, + 2: _get_text_stderr, +} + + +def _is_console(f: t.TextIO) -> bool: + if not hasattr(f, "fileno"): + return False + + try: + fileno = f.fileno() + except (OSError, io.UnsupportedOperation): + return False + + handle = msvcrt.get_osfhandle(fileno) + return bool(GetConsoleMode(handle, byref(DWORD()))) + + +def _get_windows_console_stream( + f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str] +) -> t.Optional[t.TextIO]: + if ( + get_buffer is not None + and encoding in {"utf-16-le", None} + and errors in {"strict", None} + and _is_console(f) + ): + func = _stream_factories.get(f.fileno()) + if func is not None: + b = getattr(f, "buffer", None) + + if b is None: + return None + + return func(b) diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/core.py b/lib/go-jinja2/internal/data/darwin-amd64/click/core.py new file mode 100644 index 000000000..cc65e896b --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/core.py @@ -0,0 +1,3042 @@ +import enum +import errno +import inspect +import os +import sys +import typing as t +from collections import abc +from contextlib import contextmanager +from contextlib import ExitStack +from functools import update_wrapper +from gettext import gettext as _ +from gettext import ngettext +from itertools import repeat +from types import TracebackType + +from . import types +from .exceptions import Abort +from .exceptions import BadParameter +from .exceptions import ClickException +from .exceptions import Exit +from .exceptions import MissingParameter +from .exceptions import UsageError +from .formatting import HelpFormatter +from .formatting import join_options +from .globals import pop_context +from .globals import push_context +from .parser import _flag_needs_value +from .parser import OptionParser +from .parser import split_opt +from .termui import confirm +from .termui import prompt +from .termui import style +from .utils import _detect_program_name +from .utils import _expand_args +from .utils import echo +from .utils import make_default_short_help +from .utils import make_str +from .utils import PacifyFlushWrapper + +if t.TYPE_CHECKING: + import typing_extensions as te + from .shell_completion import CompletionItem + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +V = t.TypeVar("V") + + +def _complete_visible_commands( + ctx: "Context", incomplete: str +) -> t.Iterator[t.Tuple[str, "Command"]]: + """List all the subcommands of a group that start with the + incomplete value and aren't hidden. + + :param ctx: Invocation context for the group. + :param incomplete: Value being completed. May be empty. + """ + multi = t.cast(MultiCommand, ctx.command) + + for name in multi.list_commands(ctx): + if name.startswith(incomplete): + command = multi.get_command(ctx, name) + + if command is not None and not command.hidden: + yield name, command + + +def _check_multicommand( + base_command: "MultiCommand", cmd_name: str, cmd: "Command", register: bool = False +) -> None: + if not base_command.chain or not isinstance(cmd, MultiCommand): + return + if register: + hint = ( + "It is not possible to add multi commands as children to" + " another multi command that is in chain mode." + ) + else: + hint = ( + "Found a multi command as subcommand to a multi command" + " that is in chain mode. This is not supported." + ) + raise RuntimeError( + f"{hint}. Command {base_command.name!r} is set to chain and" + f" {cmd_name!r} was added as a subcommand but it in itself is a" + f" multi command. ({cmd_name!r} is a {type(cmd).__name__}" + f" within a chained {type(base_command).__name__} named" + f" {base_command.name!r})." + ) + + +def batch(iterable: t.Iterable[V], batch_size: int) -> t.List[t.Tuple[V, ...]]: + return list(zip(*repeat(iter(iterable), batch_size))) + + +@contextmanager +def augment_usage_errors( + ctx: "Context", param: t.Optional["Parameter"] = None +) -> t.Iterator[None]: + """Context manager that attaches extra information to exceptions.""" + try: + yield + except BadParameter as e: + if e.ctx is None: + e.ctx = ctx + if param is not None and e.param is None: + e.param = param + raise + except UsageError as e: + if e.ctx is None: + e.ctx = ctx + raise + + +def iter_params_for_processing( + invocation_order: t.Sequence["Parameter"], + declaration_order: t.Sequence["Parameter"], +) -> t.List["Parameter"]: + """Given a sequence of parameters in the order as should be considered + for processing and an iterable of parameters that exist, this returns + a list in the correct order as they should be processed. + """ + + def sort_key(item: "Parameter") -> t.Tuple[bool, float]: + try: + idx: float = invocation_order.index(item) + except ValueError: + idx = float("inf") + + return not item.is_eager, idx + + return sorted(declaration_order, key=sort_key) + + +class ParameterSource(enum.Enum): + """This is an :class:`~enum.Enum` that indicates the source of a + parameter's value. + + Use :meth:`click.Context.get_parameter_source` to get the + source for a parameter by name. + + .. versionchanged:: 8.0 + Use :class:`~enum.Enum` and drop the ``validate`` method. + + .. versionchanged:: 8.0 + Added the ``PROMPT`` value. + """ + + COMMANDLINE = enum.auto() + """The value was provided by the command line args.""" + ENVIRONMENT = enum.auto() + """The value was provided with an environment variable.""" + DEFAULT = enum.auto() + """Used the default specified by the parameter.""" + DEFAULT_MAP = enum.auto() + """Used a default provided by :attr:`Context.default_map`.""" + PROMPT = enum.auto() + """Used a prompt to confirm a default or provide a value.""" + + +class Context: + """The context is a special internal object that holds state relevant + for the script execution at every single level. It's normally invisible + to commands unless they opt-in to getting access to it. + + The context is useful as it can pass internal objects around and can + control special execution features such as reading data from + environment variables. + + A context can be used as context manager in which case it will call + :meth:`close` on teardown. + + :param command: the command class for this context. + :param parent: the parent context. + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it is usually + the name of the script, for commands below it it's + the name of the script. + :param obj: an arbitrary object of user data. + :param auto_envvar_prefix: the prefix to use for automatic environment + variables. If this is `None` then reading + from environment variables is disabled. This + does not affect manually set environment + variables which are always read. + :param default_map: a dictionary (like object) with default values + for parameters. + :param terminal_width: the width of the terminal. The default is + inherit from parent context. If no context + defines the terminal width then auto + detection will be applied. + :param max_content_width: the maximum width for content rendered by + Click (this currently only affects help + pages). This defaults to 80 characters if + not overridden. In other words: even if the + terminal is larger than that, Click will not + format things wider than 80 characters by + default. In addition to that, formatters might + add some safety mapping on the right. + :param resilient_parsing: if this flag is enabled then Click will + parse without any interactivity or callback + invocation. Default values will also be + ignored. This is useful for implementing + things such as completion support. + :param allow_extra_args: if this is set to `True` then extra arguments + at the end will not raise an error and will be + kept on the context. The default is to inherit + from the command. + :param allow_interspersed_args: if this is set to `False` then options + and arguments cannot be mixed. The + default is to inherit from the command. + :param ignore_unknown_options: instructs click to ignore options it does + not know and keeps them for later + processing. + :param help_option_names: optionally a list of strings that define how + the default help parameter is named. The + default is ``['--help']``. + :param token_normalize_func: an optional function that is used to + normalize tokens (options, choices, + etc.). This for instance can be used to + implement case insensitive behavior. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are used in texts that Click prints which is by + default not the case. This for instance would affect + help output. + :param show_default: Show the default value for commands. If this + value is not set, it defaults to the value from the parent + context. ``Command.show_default`` overrides this default for the + specific command. + + .. versionchanged:: 8.1 + The ``show_default`` parameter is overridden by + ``Command.show_default``, instead of the other way around. + + .. versionchanged:: 8.0 + The ``show_default`` parameter defaults to the value from the + parent context. + + .. versionchanged:: 7.1 + Added the ``show_default`` parameter. + + .. versionchanged:: 4.0 + Added the ``color``, ``ignore_unknown_options``, and + ``max_content_width`` parameters. + + .. versionchanged:: 3.0 + Added the ``allow_extra_args`` and ``allow_interspersed_args`` + parameters. + + .. versionchanged:: 2.0 + Added the ``resilient_parsing``, ``help_option_names``, and + ``token_normalize_func`` parameters. + """ + + #: The formatter class to create with :meth:`make_formatter`. + #: + #: .. versionadded:: 8.0 + formatter_class: t.Type["HelpFormatter"] = HelpFormatter + + def __init__( + self, + command: "Command", + parent: t.Optional["Context"] = None, + info_name: t.Optional[str] = None, + obj: t.Optional[t.Any] = None, + auto_envvar_prefix: t.Optional[str] = None, + default_map: t.Optional[t.MutableMapping[str, t.Any]] = None, + terminal_width: t.Optional[int] = None, + max_content_width: t.Optional[int] = None, + resilient_parsing: bool = False, + allow_extra_args: t.Optional[bool] = None, + allow_interspersed_args: t.Optional[bool] = None, + ignore_unknown_options: t.Optional[bool] = None, + help_option_names: t.Optional[t.List[str]] = None, + token_normalize_func: t.Optional[t.Callable[[str], str]] = None, + color: t.Optional[bool] = None, + show_default: t.Optional[bool] = None, + ) -> None: + #: the parent context or `None` if none exists. + self.parent = parent + #: the :class:`Command` for this context. + self.command = command + #: the descriptive information name + self.info_name = info_name + #: Map of parameter names to their parsed values. Parameters + #: with ``expose_value=False`` are not stored. + self.params: t.Dict[str, t.Any] = {} + #: the leftover arguments. + self.args: t.List[str] = [] + #: protected arguments. These are arguments that are prepended + #: to `args` when certain parsing scenarios are encountered but + #: must be never propagated to another arguments. This is used + #: to implement nested parsing. + self.protected_args: t.List[str] = [] + #: the collected prefixes of the command's options. + self._opt_prefixes: t.Set[str] = set(parent._opt_prefixes) if parent else set() + + if obj is None and parent is not None: + obj = parent.obj + + #: the user object stored. + self.obj: t.Any = obj + self._meta: t.Dict[str, t.Any] = getattr(parent, "meta", {}) + + #: A dictionary (-like object) with defaults for parameters. + if ( + default_map is None + and info_name is not None + and parent is not None + and parent.default_map is not None + ): + default_map = parent.default_map.get(info_name) + + self.default_map: t.Optional[t.MutableMapping[str, t.Any]] = default_map + + #: This flag indicates if a subcommand is going to be executed. A + #: group callback can use this information to figure out if it's + #: being executed directly or because the execution flow passes + #: onwards to a subcommand. By default it's None, but it can be + #: the name of the subcommand to execute. + #: + #: If chaining is enabled this will be set to ``'*'`` in case + #: any commands are executed. It is however not possible to + #: figure out which ones. If you require this knowledge you + #: should use a :func:`result_callback`. + self.invoked_subcommand: t.Optional[str] = None + + if terminal_width is None and parent is not None: + terminal_width = parent.terminal_width + + #: The width of the terminal (None is autodetection). + self.terminal_width: t.Optional[int] = terminal_width + + if max_content_width is None and parent is not None: + max_content_width = parent.max_content_width + + #: The maximum width of formatted content (None implies a sensible + #: default which is 80 for most things). + self.max_content_width: t.Optional[int] = max_content_width + + if allow_extra_args is None: + allow_extra_args = command.allow_extra_args + + #: Indicates if the context allows extra args or if it should + #: fail on parsing. + #: + #: .. versionadded:: 3.0 + self.allow_extra_args = allow_extra_args + + if allow_interspersed_args is None: + allow_interspersed_args = command.allow_interspersed_args + + #: Indicates if the context allows mixing of arguments and + #: options or not. + #: + #: .. versionadded:: 3.0 + self.allow_interspersed_args: bool = allow_interspersed_args + + if ignore_unknown_options is None: + ignore_unknown_options = command.ignore_unknown_options + + #: Instructs click to ignore options that a command does not + #: understand and will store it on the context for later + #: processing. This is primarily useful for situations where you + #: want to call into external programs. Generally this pattern is + #: strongly discouraged because it's not possibly to losslessly + #: forward all arguments. + #: + #: .. versionadded:: 4.0 + self.ignore_unknown_options: bool = ignore_unknown_options + + if help_option_names is None: + if parent is not None: + help_option_names = parent.help_option_names + else: + help_option_names = ["--help"] + + #: The names for the help options. + self.help_option_names: t.List[str] = help_option_names + + if token_normalize_func is None and parent is not None: + token_normalize_func = parent.token_normalize_func + + #: An optional normalization function for tokens. This is + #: options, choices, commands etc. + self.token_normalize_func: t.Optional[ + t.Callable[[str], str] + ] = token_normalize_func + + #: Indicates if resilient parsing is enabled. In that case Click + #: will do its best to not cause any failures and default values + #: will be ignored. Useful for completion. + self.resilient_parsing: bool = resilient_parsing + + # If there is no envvar prefix yet, but the parent has one and + # the command on this level has a name, we can expand the envvar + # prefix automatically. + if auto_envvar_prefix is None: + if ( + parent is not None + and parent.auto_envvar_prefix is not None + and self.info_name is not None + ): + auto_envvar_prefix = ( + f"{parent.auto_envvar_prefix}_{self.info_name.upper()}" + ) + else: + auto_envvar_prefix = auto_envvar_prefix.upper() + + if auto_envvar_prefix is not None: + auto_envvar_prefix = auto_envvar_prefix.replace("-", "_") + + self.auto_envvar_prefix: t.Optional[str] = auto_envvar_prefix + + if color is None and parent is not None: + color = parent.color + + #: Controls if styling output is wanted or not. + self.color: t.Optional[bool] = color + + if show_default is None and parent is not None: + show_default = parent.show_default + + #: Show option default values when formatting help text. + self.show_default: t.Optional[bool] = show_default + + self._close_callbacks: t.List[t.Callable[[], t.Any]] = [] + self._depth = 0 + self._parameter_source: t.Dict[str, ParameterSource] = {} + self._exit_stack = ExitStack() + + def to_info_dict(self) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. This traverses the entire CLI + structure. + + .. code-block:: python + + with Context(cli) as ctx: + info = ctx.to_info_dict() + + .. versionadded:: 8.0 + """ + return { + "command": self.command.to_info_dict(self), + "info_name": self.info_name, + "allow_extra_args": self.allow_extra_args, + "allow_interspersed_args": self.allow_interspersed_args, + "ignore_unknown_options": self.ignore_unknown_options, + "auto_envvar_prefix": self.auto_envvar_prefix, + } + + def __enter__(self) -> "Context": + self._depth += 1 + push_context(self) + return self + + def __exit__( + self, + exc_type: t.Optional[t.Type[BaseException]], + exc_value: t.Optional[BaseException], + tb: t.Optional[TracebackType], + ) -> None: + self._depth -= 1 + if self._depth == 0: + self.close() + pop_context() + + @contextmanager + def scope(self, cleanup: bool = True) -> t.Iterator["Context"]: + """This helper method can be used with the context object to promote + it to the current thread local (see :func:`get_current_context`). + The default behavior of this is to invoke the cleanup functions which + can be disabled by setting `cleanup` to `False`. The cleanup + functions are typically used for things such as closing file handles. + + If the cleanup is intended the context object can also be directly + used as a context manager. + + Example usage:: + + with ctx.scope(): + assert get_current_context() is ctx + + This is equivalent:: + + with ctx: + assert get_current_context() is ctx + + .. versionadded:: 5.0 + + :param cleanup: controls if the cleanup functions should be run or + not. The default is to run these functions. In + some situations the context only wants to be + temporarily pushed in which case this can be disabled. + Nested pushes automatically defer the cleanup. + """ + if not cleanup: + self._depth += 1 + try: + with self as rv: + yield rv + finally: + if not cleanup: + self._depth -= 1 + + @property + def meta(self) -> t.Dict[str, t.Any]: + """This is a dictionary which is shared with all the contexts + that are nested. It exists so that click utilities can store some + state here if they need to. It is however the responsibility of + that code to manage this dictionary well. + + The keys are supposed to be unique dotted strings. For instance + module paths are a good choice for it. What is stored in there is + irrelevant for the operation of click. However what is important is + that code that places data here adheres to the general semantics of + the system. + + Example usage:: + + LANG_KEY = f'{__name__}.lang' + + def set_language(value): + ctx = get_current_context() + ctx.meta[LANG_KEY] = value + + def get_language(): + return get_current_context().meta.get(LANG_KEY, 'en_US') + + .. versionadded:: 5.0 + """ + return self._meta + + def make_formatter(self) -> HelpFormatter: + """Creates the :class:`~click.HelpFormatter` for the help and + usage output. + + To quickly customize the formatter class used without overriding + this method, set the :attr:`formatter_class` attribute. + + .. versionchanged:: 8.0 + Added the :attr:`formatter_class` attribute. + """ + return self.formatter_class( + width=self.terminal_width, max_width=self.max_content_width + ) + + def with_resource(self, context_manager: t.ContextManager[V]) -> V: + """Register a resource as if it were used in a ``with`` + statement. The resource will be cleaned up when the context is + popped. + + Uses :meth:`contextlib.ExitStack.enter_context`. It calls the + resource's ``__enter__()`` method and returns the result. When + the context is popped, it closes the stack, which calls the + resource's ``__exit__()`` method. + + To register a cleanup function for something that isn't a + context manager, use :meth:`call_on_close`. Or use something + from :mod:`contextlib` to turn it into a context manager first. + + .. code-block:: python + + @click.group() + @click.option("--name") + @click.pass_context + def cli(ctx): + ctx.obj = ctx.with_resource(connect_db(name)) + + :param context_manager: The context manager to enter. + :return: Whatever ``context_manager.__enter__()`` returns. + + .. versionadded:: 8.0 + """ + return self._exit_stack.enter_context(context_manager) + + def call_on_close(self, f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + """Register a function to be called when the context tears down. + + This can be used to close resources opened during the script + execution. Resources that support Python's context manager + protocol which would be used in a ``with`` statement should be + registered with :meth:`with_resource` instead. + + :param f: The function to execute on teardown. + """ + return self._exit_stack.callback(f) + + def close(self) -> None: + """Invoke all close callbacks registered with + :meth:`call_on_close`, and exit all context managers entered + with :meth:`with_resource`. + """ + self._exit_stack.close() + # In case the context is reused, create a new exit stack. + self._exit_stack = ExitStack() + + @property + def command_path(self) -> str: + """The computed command path. This is used for the ``usage`` + information on the help page. It's automatically created by + combining the info names of the chain of contexts to the root. + """ + rv = "" + if self.info_name is not None: + rv = self.info_name + if self.parent is not None: + parent_command_path = [self.parent.command_path] + + if isinstance(self.parent.command, Command): + for param in self.parent.command.get_params(self): + parent_command_path.extend(param.get_usage_pieces(self)) + + rv = f"{' '.join(parent_command_path)} {rv}" + return rv.lstrip() + + def find_root(self) -> "Context": + """Finds the outermost context.""" + node = self + while node.parent is not None: + node = node.parent + return node + + def find_object(self, object_type: t.Type[V]) -> t.Optional[V]: + """Finds the closest object of a given type.""" + node: t.Optional["Context"] = self + + while node is not None: + if isinstance(node.obj, object_type): + return node.obj + + node = node.parent + + return None + + def ensure_object(self, object_type: t.Type[V]) -> V: + """Like :meth:`find_object` but sets the innermost object to a + new instance of `object_type` if it does not exist. + """ + rv = self.find_object(object_type) + if rv is None: + self.obj = rv = object_type() + return rv + + @t.overload + def lookup_default( + self, name: str, call: "te.Literal[True]" = True + ) -> t.Optional[t.Any]: + ... + + @t.overload + def lookup_default( + self, name: str, call: "te.Literal[False]" = ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + ... + + def lookup_default(self, name: str, call: bool = True) -> t.Optional[t.Any]: + """Get the default for a parameter from :attr:`default_map`. + + :param name: Name of the parameter. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. + """ + if self.default_map is not None: + value = self.default_map.get(name) + + if call and callable(value): + return value() + + return value + + return None + + def fail(self, message: str) -> "te.NoReturn": + """Aborts the execution of the program with a specific error + message. + + :param message: the error message to fail with. + """ + raise UsageError(message, self) + + def abort(self) -> "te.NoReturn": + """Aborts the script.""" + raise Abort() + + def exit(self, code: int = 0) -> "te.NoReturn": + """Exits the application with a given exit code.""" + raise Exit(code) + + def get_usage(self) -> str: + """Helper method to get formatted usage string for the current + context and command. + """ + return self.command.get_usage(self) + + def get_help(self) -> str: + """Helper method to get formatted help page for the current + context and command. + """ + return self.command.get_help(self) + + def _make_sub_context(self, command: "Command") -> "Context": + """Create a new context of the same type as this context, but + for a new command. + + :meta private: + """ + return type(self)(command, info_name=command.name, parent=self) + + @t.overload + def invoke( + __self, # noqa: B902 + __callback: "t.Callable[..., V]", + *args: t.Any, + **kwargs: t.Any, + ) -> V: + ... + + @t.overload + def invoke( + __self, # noqa: B902 + __callback: "Command", + *args: t.Any, + **kwargs: t.Any, + ) -> t.Any: + ... + + def invoke( + __self, # noqa: B902 + __callback: t.Union["Command", "t.Callable[..., V]"], + *args: t.Any, + **kwargs: t.Any, + ) -> t.Union[t.Any, V]: + """Invokes a command callback in exactly the way it expects. There + are two ways to invoke this method: + + 1. the first argument can be a callback and all other arguments and + keyword arguments are forwarded directly to the function. + 2. the first argument is a click command object. In that case all + arguments are forwarded as well but proper click parameters + (options and click arguments) must be keyword arguments and Click + will fill in defaults. + + Note that before Click 3.2 keyword arguments were not properly filled + in against the intention of this code and no context was created. For + more information about this change and why it was done in a bugfix + release see :ref:`upgrade-to-3.2`. + + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if :meth:`forward` is called at multiple levels. + """ + if isinstance(__callback, Command): + other_cmd = __callback + + if other_cmd.callback is None: + raise TypeError( + "The given command does not have a callback that can be invoked." + ) + else: + __callback = t.cast("t.Callable[..., V]", other_cmd.callback) + + ctx = __self._make_sub_context(other_cmd) + + for param in other_cmd.params: + if param.name not in kwargs and param.expose_value: + kwargs[param.name] = param.type_cast_value( # type: ignore + ctx, param.get_default(ctx) + ) + + # Track all kwargs as params, so that forward() will pass + # them on in subsequent calls. + ctx.params.update(kwargs) + else: + ctx = __self + + with augment_usage_errors(__self): + with ctx: + return __callback(*args, **kwargs) + + def forward( + __self, __cmd: "Command", *args: t.Any, **kwargs: t.Any # noqa: B902 + ) -> t.Any: + """Similar to :meth:`invoke` but fills in default keyword + arguments from the current context if the other command expects + it. This cannot invoke callbacks directly, only other commands. + + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if ``forward`` is called at multiple levels. + """ + # Can only forward to other commands, not direct callbacks. + if not isinstance(__cmd, Command): + raise TypeError("Callback is not a command.") + + for param in __self.params: + if param not in kwargs: + kwargs[param] = __self.params[param] + + return __self.invoke(__cmd, *args, **kwargs) + + def set_parameter_source(self, name: str, source: ParameterSource) -> None: + """Set the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + :param name: The name of the parameter. + :param source: A member of :class:`~click.core.ParameterSource`. + """ + self._parameter_source[name] = source + + def get_parameter_source(self, name: str) -> t.Optional[ParameterSource]: + """Get the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + This can be useful for determining when a user specified a value + on the command line that is the same as the default value. It + will be :attr:`~click.core.ParameterSource.DEFAULT` only if the + value was actually taken from the default. + + :param name: The name of the parameter. + :rtype: ParameterSource + + .. versionchanged:: 8.0 + Returns ``None`` if the parameter was not provided from any + source. + """ + return self._parameter_source.get(name) + + +class BaseCommand: + """The base command implements the minimal API contract of commands. + Most code will never use this as it does not implement a lot of useful + functionality but it can act as the direct subclass of alternative + parsing methods that do not depend on the Click parser. + + For instance, this can be used to bridge Click and other systems like + argparse or docopt. + + Because base commands do not implement a lot of the API that other + parts of Click take for granted, they are not supported for all + operations. For instance, they cannot be used with the decorators + usually and they have no built-in callback system. + + .. versionchanged:: 2.0 + Added the `context_settings` parameter. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + """ + + #: The context class to create with :meth:`make_context`. + #: + #: .. versionadded:: 8.0 + context_class: t.Type[Context] = Context + #: the default for the :attr:`Context.allow_extra_args` flag. + allow_extra_args = False + #: the default for the :attr:`Context.allow_interspersed_args` flag. + allow_interspersed_args = True + #: the default for the :attr:`Context.ignore_unknown_options` flag. + ignore_unknown_options = False + + def __init__( + self, + name: t.Optional[str], + context_settings: t.Optional[t.MutableMapping[str, t.Any]] = None, + ) -> None: + #: the name the command thinks it has. Upon registering a command + #: on a :class:`Group` the group will default the command name + #: with this information. You should instead use the + #: :class:`Context`\'s :attr:`~Context.info_name` attribute. + self.name = name + + if context_settings is None: + context_settings = {} + + #: an optional dictionary with defaults passed to the context. + self.context_settings: t.MutableMapping[str, t.Any] = context_settings + + def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. This traverses the entire structure + below this command. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + :param ctx: A :class:`Context` representing this command. + + .. versionadded:: 8.0 + """ + return {"name": self.name} + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + def get_usage(self, ctx: Context) -> str: + raise NotImplementedError("Base commands cannot get usage") + + def get_help(self, ctx: Context) -> str: + raise NotImplementedError("Base commands cannot get help") + + def make_context( + self, + info_name: t.Optional[str], + args: t.List[str], + parent: t.Optional[Context] = None, + **extra: t.Any, + ) -> Context: + """This function when given an info name and arguments will kick + off the parsing and create a new :class:`Context`. It does not + invoke the actual command callback though. + + To quickly customize the context class used without overriding + this method, set the :attr:`context_class` attribute. + + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it's usually + the name of the script, for commands below it's + the name of the command. + :param args: the arguments to parse as list of strings. + :param parent: the parent context if available. + :param extra: extra keyword arguments forwarded to the context + constructor. + + .. versionchanged:: 8.0 + Added the :attr:`context_class` attribute. + """ + for key, value in self.context_settings.items(): + if key not in extra: + extra[key] = value + + ctx = self.context_class( + self, info_name=info_name, parent=parent, **extra # type: ignore + ) + + with ctx.scope(cleanup=False): + self.parse_args(ctx, args) + return ctx + + def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: + """Given a context and a list of arguments this creates the parser + and parses the arguments, then modifies the context as necessary. + This is automatically invoked by :meth:`make_context`. + """ + raise NotImplementedError("Base commands do not know how to parse arguments.") + + def invoke(self, ctx: Context) -> t.Any: + """Given a context, this invokes the command. The default + implementation is raising a not implemented error. + """ + raise NotImplementedError("Base commands are not invocable by default") + + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. Looks + at the names of chained multi-commands. + + Any command could be part of a chained multi-command, so sibling + commands are valid at any point during command completion. Other + command classes will return more completions. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results: t.List["CompletionItem"] = [] + + while ctx.parent is not None: + ctx = ctx.parent + + if isinstance(ctx.command, MultiCommand) and ctx.command.chain: + results.extend( + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + if name not in ctx.protected_args + ) + + return results + + @t.overload + def main( + self, + args: t.Optional[t.Sequence[str]] = None, + prog_name: t.Optional[str] = None, + complete_var: t.Optional[str] = None, + standalone_mode: "te.Literal[True]" = True, + **extra: t.Any, + ) -> "te.NoReturn": + ... + + @t.overload + def main( + self, + args: t.Optional[t.Sequence[str]] = None, + prog_name: t.Optional[str] = None, + complete_var: t.Optional[str] = None, + standalone_mode: bool = ..., + **extra: t.Any, + ) -> t.Any: + ... + + def main( + self, + args: t.Optional[t.Sequence[str]] = None, + prog_name: t.Optional[str] = None, + complete_var: t.Optional[str] = None, + standalone_mode: bool = True, + windows_expand_args: bool = True, + **extra: t.Any, + ) -> t.Any: + """This is the way to invoke a script with all the bells and + whistles as a command line application. This will always terminate + the application after a call. If this is not wanted, ``SystemExit`` + needs to be caught. + + This method is also available by directly calling the instance of + a :class:`Command`. + + :param args: the arguments that should be used for parsing. If not + provided, ``sys.argv[1:]`` is used. + :param prog_name: the program name that should be used. By default + the program name is constructed by taking the file + name from ``sys.argv[0]``. + :param complete_var: the environment variable that controls the + bash completion support. The default is + ``"__COMPLETE"`` with prog_name in + uppercase. + :param standalone_mode: the default behavior is to invoke the script + in standalone mode. Click will then + handle exceptions and convert them into + error messages and the function will never + return but shut down the interpreter. If + this is set to `False` they will be + propagated to the caller and the return + value of this function is the return value + of :meth:`invoke`. + :param windows_expand_args: Expand glob patterns, user dir, and + env vars in command line args on Windows. + :param extra: extra keyword arguments are forwarded to the context + constructor. See :class:`Context` for more information. + + .. versionchanged:: 8.0.1 + Added the ``windows_expand_args`` parameter to allow + disabling command line arg expansion on Windows. + + .. versionchanged:: 8.0 + When taking arguments from ``sys.argv`` on Windows, glob + patterns, user dir, and env vars are expanded. + + .. versionchanged:: 3.0 + Added the ``standalone_mode`` parameter. + """ + if args is None: + args = sys.argv[1:] + + if os.name == "nt" and windows_expand_args: + args = _expand_args(args) + else: + args = list(args) + + if prog_name is None: + prog_name = _detect_program_name() + + # Process shell completion requests and exit early. + self._main_shell_completion(extra, prog_name, complete_var) + + try: + try: + with self.make_context(prog_name, args, **extra) as ctx: + rv = self.invoke(ctx) + if not standalone_mode: + return rv + # it's not safe to `ctx.exit(rv)` here! + # note that `rv` may actually contain data like "1" which + # has obvious effects + # more subtle case: `rv=[None, None]` can come out of + # chained commands which all returned `None` -- so it's not + # even always obvious that `rv` indicates success/failure + # by its truthiness/falsiness + ctx.exit() + except (EOFError, KeyboardInterrupt) as e: + echo(file=sys.stderr) + raise Abort() from e + except ClickException as e: + if not standalone_mode: + raise + e.show() + sys.exit(e.exit_code) + except OSError as e: + if e.errno == errno.EPIPE: + sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout)) + sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr)) + sys.exit(1) + else: + raise + except Exit as e: + if standalone_mode: + sys.exit(e.exit_code) + else: + # in non-standalone mode, return the exit code + # note that this is only reached if `self.invoke` above raises + # an Exit explicitly -- thus bypassing the check there which + # would return its result + # the results of non-standalone execution may therefore be + # somewhat ambiguous: if there are codepaths which lead to + # `ctx.exit(1)` and to `return 1`, the caller won't be able to + # tell the difference between the two + return e.exit_code + except Abort: + if not standalone_mode: + raise + echo(_("Aborted!"), file=sys.stderr) + sys.exit(1) + + def _main_shell_completion( + self, + ctx_args: t.MutableMapping[str, t.Any], + prog_name: str, + complete_var: t.Optional[str] = None, + ) -> None: + """Check if the shell is asking for tab completion, process + that, then exit early. Called from :meth:`main` before the + program is invoked. + + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. Defaults to + ``_{PROG_NAME}_COMPLETE``. + + .. versionchanged:: 8.2.0 + Dots (``.``) in ``prog_name`` are replaced with underscores (``_``). + """ + if complete_var is None: + complete_name = prog_name.replace("-", "_").replace(".", "_") + complete_var = f"_{complete_name}_COMPLETE".upper() + + instruction = os.environ.get(complete_var) + + if not instruction: + return + + from .shell_completion import shell_complete + + rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction) + sys.exit(rv) + + def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: + """Alias for :meth:`main`.""" + return self.main(*args, **kwargs) + + +class Command(BaseCommand): + """Commands are the basic building block of command line interfaces in + Click. A basic command handles command line parsing and might dispatch + more parsing to commands nested below it. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + :param callback: the callback to invoke. This is optional. + :param params: the parameters to register with this command. This can + be either :class:`Option` or :class:`Argument` objects. + :param help: the help string to use for this command. + :param epilog: like the help string but it's printed at the end of the + help page after everything else. + :param short_help: the short help to use for this command. This is + shown on the command listing of the parent command. + :param add_help_option: by default each command registers a ``--help`` + option. This can be disabled by this parameter. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is disabled by default. + If enabled this will add ``--help`` as argument + if no arguments are passed + :param hidden: hide this command from help outputs. + + :param deprecated: issues a message indicating that + the command is deprecated. + + .. versionchanged:: 8.1 + ``help``, ``epilog``, and ``short_help`` are stored unprocessed, + all formatting is done when outputting help text, not at init, + and is done even if not using the ``@command`` decorator. + + .. versionchanged:: 8.0 + Added a ``repr`` showing the command name. + + .. versionchanged:: 7.1 + Added the ``no_args_is_help`` parameter. + + .. versionchanged:: 2.0 + Added the ``context_settings`` parameter. + """ + + def __init__( + self, + name: t.Optional[str], + context_settings: t.Optional[t.MutableMapping[str, t.Any]] = None, + callback: t.Optional[t.Callable[..., t.Any]] = None, + params: t.Optional[t.List["Parameter"]] = None, + help: t.Optional[str] = None, + epilog: t.Optional[str] = None, + short_help: t.Optional[str] = None, + options_metavar: t.Optional[str] = "[OPTIONS]", + add_help_option: bool = True, + no_args_is_help: bool = False, + hidden: bool = False, + deprecated: bool = False, + ) -> None: + super().__init__(name, context_settings) + #: the callback to execute when the command fires. This might be + #: `None` in which case nothing happens. + self.callback = callback + #: the list of parameters for this command in the order they + #: should show up in the help page and execute. Eager parameters + #: will automatically be handled before non eager ones. + self.params: t.List["Parameter"] = params or [] + self.help = help + self.epilog = epilog + self.options_metavar = options_metavar + self.short_help = short_help + self.add_help_option = add_help_option + self.no_args_is_help = no_args_is_help + self.hidden = hidden + self.deprecated = deprecated + + def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict(ctx) + info_dict.update( + params=[param.to_info_dict() for param in self.get_params(ctx)], + help=self.help, + epilog=self.epilog, + short_help=self.short_help, + hidden=self.hidden, + deprecated=self.deprecated, + ) + return info_dict + + def get_usage(self, ctx: Context) -> str: + """Formats the usage line into a string and returns it. + + Calls :meth:`format_usage` internally. + """ + formatter = ctx.make_formatter() + self.format_usage(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_params(self, ctx: Context) -> t.List["Parameter"]: + rv = self.params + help_option = self.get_help_option(ctx) + + if help_option is not None: + rv = [*rv, help_option] + + return rv + + def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the usage line into the formatter. + + This is a low-level method called by :meth:`get_usage`. + """ + pieces = self.collect_usage_pieces(ctx) + formatter.write_usage(ctx.command_path, " ".join(pieces)) + + def collect_usage_pieces(self, ctx: Context) -> t.List[str]: + """Returns all the pieces that go into the usage line and returns + it as a list of strings. + """ + rv = [self.options_metavar] if self.options_metavar else [] + + for param in self.get_params(ctx): + rv.extend(param.get_usage_pieces(ctx)) + + return rv + + def get_help_option_names(self, ctx: Context) -> t.List[str]: + """Returns the names for the help option.""" + all_names = set(ctx.help_option_names) + for param in self.params: + all_names.difference_update(param.opts) + all_names.difference_update(param.secondary_opts) + return list(all_names) + + def get_help_option(self, ctx: Context) -> t.Optional["Option"]: + """Returns the help option object.""" + help_options = self.get_help_option_names(ctx) + + if not help_options or not self.add_help_option: + return None + + def show_help(ctx: Context, param: "Parameter", value: str) -> None: + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + return Option( + help_options, + is_flag=True, + is_eager=True, + expose_value=False, + callback=show_help, + help=_("Show this message and exit."), + ) + + def make_parser(self, ctx: Context) -> OptionParser: + """Creates the underlying option parser for this command.""" + parser = OptionParser(ctx) + for param in self.get_params(ctx): + param.add_to_parser(parser, ctx) + return parser + + def get_help(self, ctx: Context) -> str: + """Formats the help into a string and returns it. + + Calls :meth:`format_help` internally. + """ + formatter = ctx.make_formatter() + self.format_help(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_short_help_str(self, limit: int = 45) -> str: + """Gets short help for the command or makes it by shortening the + long help string. + """ + if self.short_help: + text = inspect.cleandoc(self.short_help) + elif self.help: + text = make_default_short_help(self.help, limit) + else: + text = "" + + if self.deprecated: + text = _("(Deprecated) {text}").format(text=text) + + return text.strip() + + def format_help(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the help into the formatter if it exists. + + This is a low-level method called by :meth:`get_help`. + + This calls the following methods: + + - :meth:`format_usage` + - :meth:`format_help_text` + - :meth:`format_options` + - :meth:`format_epilog` + """ + self.format_usage(ctx, formatter) + self.format_help_text(ctx, formatter) + self.format_options(ctx, formatter) + self.format_epilog(ctx, formatter) + + def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the help text to the formatter if it exists.""" + if self.help is not None: + # truncate the help text to the first form feed + text = inspect.cleandoc(self.help).partition("\f")[0] + else: + text = "" + + if self.deprecated: + text = _("(Deprecated) {text}").format(text=text) + + if text: + formatter.write_paragraph() + + with formatter.indentation(): + formatter.write_text(text) + + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes all the options into the formatter if they exist.""" + opts = [] + for param in self.get_params(ctx): + rv = param.get_help_record(ctx) + if rv is not None: + opts.append(rv) + + if opts: + with formatter.section(_("Options")): + formatter.write_dl(opts) + + def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the epilog into the formatter if it exists.""" + if self.epilog: + epilog = inspect.cleandoc(self.epilog) + formatter.write_paragraph() + + with formatter.indentation(): + formatter.write_text(epilog) + + def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: + if not args and self.no_args_is_help and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + parser = self.make_parser(ctx) + opts, args, param_order = parser.parse_args(args=args) + + for param in iter_params_for_processing(param_order, self.get_params(ctx)): + value, args = param.handle_parse_result(ctx, opts, args) + + if args and not ctx.allow_extra_args and not ctx.resilient_parsing: + ctx.fail( + ngettext( + "Got unexpected extra argument ({args})", + "Got unexpected extra arguments ({args})", + len(args), + ).format(args=" ".join(map(str, args))) + ) + + ctx.args = args + ctx._opt_prefixes.update(parser._opt_prefixes) + return args + + def invoke(self, ctx: Context) -> t.Any: + """Given a context, this invokes the attached callback (if it exists) + in the right way. + """ + if self.deprecated: + message = _( + "DeprecationWarning: The command {name!r} is deprecated." + ).format(name=self.name) + echo(style(message, fg="red"), err=True) + + if self.callback is not None: + return ctx.invoke(self.callback, **ctx.params) + + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. Looks + at the names of options and chained multi-commands. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results: t.List["CompletionItem"] = [] + + if incomplete and not incomplete[0].isalnum(): + for param in self.get_params(ctx): + if ( + not isinstance(param, Option) + or param.hidden + or ( + not param.multiple + and ctx.get_parameter_source(param.name) # type: ignore + is ParameterSource.COMMANDLINE + ) + ): + continue + + results.extend( + CompletionItem(name, help=param.help) + for name in [*param.opts, *param.secondary_opts] + if name.startswith(incomplete) + ) + + results.extend(super().shell_complete(ctx, incomplete)) + return results + + +class MultiCommand(Command): + """A multi command is the basic implementation of a command that + dispatches to subcommands. The most common version is the + :class:`Group`. + + :param invoke_without_command: this controls how the multi command itself + is invoked. By default it's only invoked + if a subcommand is provided. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is enabled by default if + `invoke_without_command` is disabled or disabled + if it's enabled. If enabled this will add + ``--help`` as argument if no arguments are + passed. + :param subcommand_metavar: the string that is used in the documentation + to indicate the subcommand place. + :param chain: if this is set to `True` chaining of multiple subcommands + is enabled. This restricts the form of commands in that + they cannot have optional arguments but it allows + multiple commands to be chained together. + :param result_callback: The result callback to attach to this multi + command. This can be set or changed later with the + :meth:`result_callback` decorator. + :param attrs: Other command arguments described in :class:`Command`. + """ + + allow_extra_args = True + allow_interspersed_args = False + + def __init__( + self, + name: t.Optional[str] = None, + invoke_without_command: bool = False, + no_args_is_help: t.Optional[bool] = None, + subcommand_metavar: t.Optional[str] = None, + chain: bool = False, + result_callback: t.Optional[t.Callable[..., t.Any]] = None, + **attrs: t.Any, + ) -> None: + super().__init__(name, **attrs) + + if no_args_is_help is None: + no_args_is_help = not invoke_without_command + + self.no_args_is_help = no_args_is_help + self.invoke_without_command = invoke_without_command + + if subcommand_metavar is None: + if chain: + subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." + else: + subcommand_metavar = "COMMAND [ARGS]..." + + self.subcommand_metavar = subcommand_metavar + self.chain = chain + # The result callback that is stored. This can be set or + # overridden with the :func:`result_callback` decorator. + self._result_callback = result_callback + + if self.chain: + for param in self.params: + if isinstance(param, Argument) and not param.required: + raise RuntimeError( + "Multi commands in chain mode cannot have" + " optional arguments." + ) + + def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict(ctx) + commands = {} + + for name in self.list_commands(ctx): + command = self.get_command(ctx, name) + + if command is None: + continue + + sub_ctx = ctx._make_sub_context(command) + + with sub_ctx.scope(cleanup=False): + commands[name] = command.to_info_dict(sub_ctx) + + info_dict.update(commands=commands, chain=self.chain) + return info_dict + + def collect_usage_pieces(self, ctx: Context) -> t.List[str]: + rv = super().collect_usage_pieces(ctx) + rv.append(self.subcommand_metavar) + return rv + + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: + super().format_options(ctx, formatter) + self.format_commands(ctx, formatter) + + def result_callback(self, replace: bool = False) -> t.Callable[[F], F]: + """Adds a result callback to the command. By default if a + result callback is already registered this will chain them but + this can be disabled with the `replace` parameter. The result + callback is invoked with the return value of the subcommand + (or the list of return values from all subcommands if chaining + is enabled) as well as the parameters as they would be passed + to the main callback. + + Example:: + + @click.group() + @click.option('-i', '--input', default=23) + def cli(input): + return 42 + + @cli.result_callback() + def process_result(result, input): + return result + input + + :param replace: if set to `True` an already existing result + callback will be removed. + + .. versionchanged:: 8.0 + Renamed from ``resultcallback``. + + .. versionadded:: 3.0 + """ + + def decorator(f: F) -> F: + old_callback = self._result_callback + + if old_callback is None or replace: + self._result_callback = f + return f + + def function(__value, *args, **kwargs): # type: ignore + inner = old_callback(__value, *args, **kwargs) + return f(inner, *args, **kwargs) + + self._result_callback = rv = update_wrapper(t.cast(F, function), f) + return rv + + return decorator + + def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None: + """Extra format methods for multi methods that adds all the commands + after the options. + """ + commands = [] + for subcommand in self.list_commands(ctx): + cmd = self.get_command(ctx, subcommand) + # What is this, the tool lied about a command. Ignore it + if cmd is None: + continue + if cmd.hidden: + continue + + commands.append((subcommand, cmd)) + + # allow for 3 times the default spacing + if len(commands): + limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands) + + rows = [] + for subcommand, cmd in commands: + help = cmd.get_short_help_str(limit) + rows.append((subcommand, help)) + + if rows: + with formatter.section(_("Commands")): + formatter.write_dl(rows) + + def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: + if not args and self.no_args_is_help and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + rest = super().parse_args(ctx, args) + + if self.chain: + ctx.protected_args = rest + ctx.args = [] + elif rest: + ctx.protected_args, ctx.args = rest[:1], rest[1:] + + return ctx.args + + def invoke(self, ctx: Context) -> t.Any: + def _process_result(value: t.Any) -> t.Any: + if self._result_callback is not None: + value = ctx.invoke(self._result_callback, value, **ctx.params) + return value + + if not ctx.protected_args: + if self.invoke_without_command: + # No subcommand was invoked, so the result callback is + # invoked with the group return value for regular + # groups, or an empty list for chained groups. + with ctx: + rv = super().invoke(ctx) + return _process_result([] if self.chain else rv) + ctx.fail(_("Missing command.")) + + # Fetch args back out + args = [*ctx.protected_args, *ctx.args] + ctx.args = [] + ctx.protected_args = [] + + # If we're not in chain mode, we only allow the invocation of a + # single command but we also inform the current context about the + # name of the command to invoke. + if not self.chain: + # Make sure the context is entered so we do not clean up + # resources until the result processor has worked. + with ctx: + cmd_name, cmd, args = self.resolve_command(ctx, args) + assert cmd is not None + ctx.invoked_subcommand = cmd_name + super().invoke(ctx) + sub_ctx = cmd.make_context(cmd_name, args, parent=ctx) + with sub_ctx: + return _process_result(sub_ctx.command.invoke(sub_ctx)) + + # In chain mode we create the contexts step by step, but after the + # base command has been invoked. Because at that point we do not + # know the subcommands yet, the invoked subcommand attribute is + # set to ``*`` to inform the command that subcommands are executed + # but nothing else. + with ctx: + ctx.invoked_subcommand = "*" if args else None + super().invoke(ctx) + + # Otherwise we make every single context and invoke them in a + # chain. In that case the return value to the result processor + # is the list of all invoked subcommand's results. + contexts = [] + while args: + cmd_name, cmd, args = self.resolve_command(ctx, args) + assert cmd is not None + sub_ctx = cmd.make_context( + cmd_name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + ) + contexts.append(sub_ctx) + args, sub_ctx.args = sub_ctx.args, [] + + rv = [] + for sub_ctx in contexts: + with sub_ctx: + rv.append(sub_ctx.command.invoke(sub_ctx)) + return _process_result(rv) + + def resolve_command( + self, ctx: Context, args: t.List[str] + ) -> t.Tuple[t.Optional[str], t.Optional[Command], t.List[str]]: + cmd_name = make_str(args[0]) + original_cmd_name = cmd_name + + # Get the command + cmd = self.get_command(ctx, cmd_name) + + # If we can't find the command but there is a normalization + # function available, we try with that one. + if cmd is None and ctx.token_normalize_func is not None: + cmd_name = ctx.token_normalize_func(cmd_name) + cmd = self.get_command(ctx, cmd_name) + + # If we don't find the command we want to show an error message + # to the user that it was not provided. However, there is + # something else we should do: if the first argument looks like + # an option we want to kick off parsing again for arguments to + # resolve things like --help which now should go to the main + # place. + if cmd is None and not ctx.resilient_parsing: + if split_opt(cmd_name)[0]: + self.parse_args(ctx, ctx.args) + ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name)) + return cmd_name if cmd else None, cmd, args[1:] + + def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: + """Given a context and a command name, this returns a + :class:`Command` object if it exists or returns `None`. + """ + raise NotImplementedError + + def list_commands(self, ctx: Context) -> t.List[str]: + """Returns a list of subcommand names in the order they should + appear. + """ + return [] + + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. Looks + at the names of options, subcommands, and chained + multi-commands. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results = [ + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + ] + results.extend(super().shell_complete(ctx, incomplete)) + return results + + +class Group(MultiCommand): + """A group allows a command to have subcommands attached. This is + the most common way to implement nesting in Click. + + :param name: The name of the group command. + :param commands: A dict mapping names to :class:`Command` objects. + Can also be a list of :class:`Command`, which will use + :attr:`Command.name` to create the dict. + :param attrs: Other command arguments described in + :class:`MultiCommand`, :class:`Command`, and + :class:`BaseCommand`. + + .. versionchanged:: 8.0 + The ``commands`` argument can be a list of command objects. + """ + + #: If set, this is used by the group's :meth:`command` decorator + #: as the default :class:`Command` class. This is useful to make all + #: subcommands use a custom command class. + #: + #: .. versionadded:: 8.0 + command_class: t.Optional[t.Type[Command]] = None + + #: If set, this is used by the group's :meth:`group` decorator + #: as the default :class:`Group` class. This is useful to make all + #: subgroups use a custom group class. + #: + #: If set to the special value :class:`type` (literally + #: ``group_class = type``), this group's class will be used as the + #: default class. This makes a custom group class continue to make + #: custom groups. + #: + #: .. versionadded:: 8.0 + group_class: t.Optional[t.Union[t.Type["Group"], t.Type[type]]] = None + # Literal[type] isn't valid, so use Type[type] + + def __init__( + self, + name: t.Optional[str] = None, + commands: t.Optional[ + t.Union[t.MutableMapping[str, Command], t.Sequence[Command]] + ] = None, + **attrs: t.Any, + ) -> None: + super().__init__(name, **attrs) + + if commands is None: + commands = {} + elif isinstance(commands, abc.Sequence): + commands = {c.name: c for c in commands if c.name is not None} + + #: The registered subcommands by their exported names. + self.commands: t.MutableMapping[str, Command] = commands + + def add_command(self, cmd: Command, name: t.Optional[str] = None) -> None: + """Registers another :class:`Command` with this group. If the name + is not provided, the name of the command is used. + """ + name = name or cmd.name + if name is None: + raise TypeError("Command has no name.") + _check_multicommand(self, name, cmd, register=True) + self.commands[name] = cmd + + @t.overload + def command(self, __func: t.Callable[..., t.Any]) -> Command: + ... + + @t.overload + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Command]: + ... + + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Union[t.Callable[[t.Callable[..., t.Any]], Command], Command]: + """A shortcut decorator for declaring and attaching a command to + the group. This takes the same arguments as :func:`command` and + immediately registers the created command with this group by + calling :meth:`add_command`. + + To customize the command class used, set the + :attr:`command_class` attribute. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.0 + Added the :attr:`command_class` attribute. + """ + from .decorators import command + + func: t.Optional[t.Callable[..., t.Any]] = None + + if args and callable(args[0]): + assert ( + len(args) == 1 and not kwargs + ), "Use 'command(**kwargs)(callable)' to provide arguments." + (func,) = args + args = () + + if self.command_class and kwargs.get("cls") is None: + kwargs["cls"] = self.command_class + + def decorator(f: t.Callable[..., t.Any]) -> Command: + cmd: Command = command(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + if func is not None: + return decorator(func) + + return decorator + + @t.overload + def group(self, __func: t.Callable[..., t.Any]) -> "Group": + ... + + @t.overload + def group( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], "Group"]: + ... + + def group( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Union[t.Callable[[t.Callable[..., t.Any]], "Group"], "Group"]: + """A shortcut decorator for declaring and attaching a group to + the group. This takes the same arguments as :func:`group` and + immediately registers the created group with this group by + calling :meth:`add_command`. + + To customize the group class used, set the :attr:`group_class` + attribute. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.0 + Added the :attr:`group_class` attribute. + """ + from .decorators import group + + func: t.Optional[t.Callable[..., t.Any]] = None + + if args and callable(args[0]): + assert ( + len(args) == 1 and not kwargs + ), "Use 'group(**kwargs)(callable)' to provide arguments." + (func,) = args + args = () + + if self.group_class is not None and kwargs.get("cls") is None: + if self.group_class is type: + kwargs["cls"] = type(self) + else: + kwargs["cls"] = self.group_class + + def decorator(f: t.Callable[..., t.Any]) -> "Group": + cmd: Group = group(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + if func is not None: + return decorator(func) + + return decorator + + def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: + return self.commands.get(cmd_name) + + def list_commands(self, ctx: Context) -> t.List[str]: + return sorted(self.commands) + + +class CommandCollection(MultiCommand): + """A command collection is a multi command that merges multiple multi + commands together into one. This is a straightforward implementation + that accepts a list of different multi commands as sources and + provides all the commands for each of them. + + See :class:`MultiCommand` and :class:`Command` for the description of + ``name`` and ``attrs``. + """ + + def __init__( + self, + name: t.Optional[str] = None, + sources: t.Optional[t.List[MultiCommand]] = None, + **attrs: t.Any, + ) -> None: + super().__init__(name, **attrs) + #: The list of registered multi commands. + self.sources: t.List[MultiCommand] = sources or [] + + def add_source(self, multi_cmd: MultiCommand) -> None: + """Adds a new multi command to the chain dispatcher.""" + self.sources.append(multi_cmd) + + def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: + for source in self.sources: + rv = source.get_command(ctx, cmd_name) + + if rv is not None: + if self.chain: + _check_multicommand(self, cmd_name, rv) + + return rv + + return None + + def list_commands(self, ctx: Context) -> t.List[str]: + rv: t.Set[str] = set() + + for source in self.sources: + rv.update(source.list_commands(ctx)) + + return sorted(rv) + + +def _check_iter(value: t.Any) -> t.Iterator[t.Any]: + """Check if the value is iterable but not a string. Raises a type + error, or return an iterator over the value. + """ + if isinstance(value, str): + raise TypeError + + return iter(value) + + +class Parameter: + r"""A parameter to a command comes in two versions: they are either + :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently + not supported by design as some of the internals for parsing are + intentionally not finalized. + + Some settings are supported by both options and arguments. + + :param param_decls: the parameter declarations for this option or + argument. This is a list of flags or argument + names. + :param type: the type that should be used. Either a :class:`ParamType` + or a Python type. The latter is converted into the former + automatically if supported. + :param required: controls if this is optional or not. + :param default: the default value if omitted. This can also be a callable, + in which case it's invoked when the default is needed + without any arguments. + :param callback: A function to further process or validate the value + after type conversion. It is called as ``f(ctx, param, value)`` + and must return the value. It is called for all sources, + including prompts. + :param nargs: the number of arguments to match. If not ``1`` the return + value is a tuple instead of single value. The default for + nargs is ``1`` (except if the type is a tuple, then it's + the arity of the tuple). If ``nargs=-1``, all remaining + parameters are collected. + :param metavar: how the value is represented in the help page. + :param expose_value: if this is `True` then the value is passed onwards + to the command callback and stored on the context, + otherwise it's skipped. + :param is_eager: eager values are processed before non eager ones. This + should not be set for arguments or it will inverse the + order of processing. + :param envvar: a string or list of strings that are environment variables + that should be checked. + :param shell_complete: A function that returns custom shell + completions. Used instead of the param's type completion if + given. Takes ``ctx, param, incomplete`` and must return a list + of :class:`~click.shell_completion.CompletionItem` or a list of + strings. + + .. versionchanged:: 8.0 + ``process_value`` validates required parameters and bounded + ``nargs``, and invokes the parameter callback before returning + the value. This allows the callback to validate prompts. + ``full_process_value`` is removed. + + .. versionchanged:: 8.0 + ``autocompletion`` is renamed to ``shell_complete`` and has new + semantics described above. The old name is deprecated and will + be removed in 8.1, until then it will be wrapped to match the + new requirements. + + .. versionchanged:: 8.0 + For ``multiple=True, nargs>1``, the default must be a list of + tuples. + + .. versionchanged:: 8.0 + Setting a default is no longer required for ``nargs>1``, it will + default to ``None``. ``multiple=True`` or ``nargs=-1`` will + default to ``()``. + + .. versionchanged:: 7.1 + Empty environment variables are ignored rather than taking the + empty string value. This makes it possible for scripts to clear + variables if they can't unset them. + + .. versionchanged:: 2.0 + Changed signature for parameter callback to also be passed the + parameter. The old callback format will still work, but it will + raise a warning to give you a chance to migrate the code easier. + """ + + param_type_name = "parameter" + + def __init__( + self, + param_decls: t.Optional[t.Sequence[str]] = None, + type: t.Optional[t.Union[types.ParamType, t.Any]] = None, + required: bool = False, + default: t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]] = None, + callback: t.Optional[t.Callable[[Context, "Parameter", t.Any], t.Any]] = None, + nargs: t.Optional[int] = None, + multiple: bool = False, + metavar: t.Optional[str] = None, + expose_value: bool = True, + is_eager: bool = False, + envvar: t.Optional[t.Union[str, t.Sequence[str]]] = None, + shell_complete: t.Optional[ + t.Callable[ + [Context, "Parameter", str], + t.Union[t.List["CompletionItem"], t.List[str]], + ] + ] = None, + ) -> None: + self.name: t.Optional[str] + self.opts: t.List[str] + self.secondary_opts: t.List[str] + self.name, self.opts, self.secondary_opts = self._parse_decls( + param_decls or (), expose_value + ) + self.type: types.ParamType = types.convert_type(type, default) + + # Default nargs to what the type tells us if we have that + # information available. + if nargs is None: + if self.type.is_composite: + nargs = self.type.arity + else: + nargs = 1 + + self.required = required + self.callback = callback + self.nargs = nargs + self.multiple = multiple + self.expose_value = expose_value + self.default = default + self.is_eager = is_eager + self.metavar = metavar + self.envvar = envvar + self._custom_shell_complete = shell_complete + + if __debug__: + if self.type.is_composite and nargs != self.type.arity: + raise ValueError( + f"'nargs' must be {self.type.arity} (or None) for" + f" type {self.type!r}, but it was {nargs}." + ) + + # Skip no default or callable default. + check_default = default if not callable(default) else None + + if check_default is not None: + if multiple: + try: + # Only check the first value against nargs. + check_default = next(_check_iter(check_default), None) + except TypeError: + raise ValueError( + "'default' must be a list when 'multiple' is true." + ) from None + + # Can be None for multiple with empty default. + if nargs != 1 and check_default is not None: + try: + _check_iter(check_default) + except TypeError: + if multiple: + message = ( + "'default' must be a list of lists when 'multiple' is" + " true and 'nargs' != 1." + ) + else: + message = "'default' must be a list when 'nargs' != 1." + + raise ValueError(message) from None + + if nargs > 1 and len(check_default) != nargs: + subject = "item length" if multiple else "length" + raise ValueError( + f"'default' {subject} must match nargs={nargs}." + ) + + def to_info_dict(self) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionadded:: 8.0 + """ + return { + "name": self.name, + "param_type_name": self.param_type_name, + "opts": self.opts, + "secondary_opts": self.secondary_opts, + "type": self.type.to_info_dict(), + "required": self.required, + "nargs": self.nargs, + "multiple": self.multiple, + "default": self.default, + "envvar": self.envvar, + } + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + def _parse_decls( + self, decls: t.Sequence[str], expose_value: bool + ) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]: + raise NotImplementedError() + + @property + def human_readable_name(self) -> str: + """Returns the human readable name of this parameter. This is the + same as the name for options, but the metavar for arguments. + """ + return self.name # type: ignore + + def make_metavar(self) -> str: + if self.metavar is not None: + return self.metavar + + metavar = self.type.get_metavar(self) + + if metavar is None: + metavar = self.type.name.upper() + + if self.nargs != 1: + metavar += "..." + + return metavar + + @t.overload + def get_default( + self, ctx: Context, call: "te.Literal[True]" = True + ) -> t.Optional[t.Any]: + ... + + @t.overload + def get_default( + self, ctx: Context, call: bool = ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + ... + + def get_default( + self, ctx: Context, call: bool = True + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + """Get the default for the parameter. Tries + :meth:`Context.lookup_default` first, then the local default. + + :param ctx: Current context. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0.2 + Type casting is no longer performed when getting a default. + + .. versionchanged:: 8.0.1 + Type casting can fail in resilient parsing mode. Invalid + defaults will not prevent showing help text. + + .. versionchanged:: 8.0 + Looks at ``ctx.default_map`` first. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. + """ + value = ctx.lookup_default(self.name, call=False) # type: ignore + + if value is None: + value = self.default + + if call and callable(value): + value = value() + + return value + + def add_to_parser(self, parser: OptionParser, ctx: Context) -> None: + raise NotImplementedError() + + def consume_value( + self, ctx: Context, opts: t.Mapping[str, t.Any] + ) -> t.Tuple[t.Any, ParameterSource]: + value = opts.get(self.name) # type: ignore + source = ParameterSource.COMMANDLINE + + if value is None: + value = self.value_from_envvar(ctx) + source = ParameterSource.ENVIRONMENT + + if value is None: + value = ctx.lookup_default(self.name) # type: ignore + source = ParameterSource.DEFAULT_MAP + + if value is None: + value = self.get_default(ctx) + source = ParameterSource.DEFAULT + + return value, source + + def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any: + """Convert and validate a value against the option's + :attr:`type`, :attr:`multiple`, and :attr:`nargs`. + """ + if value is None: + return () if self.multiple or self.nargs == -1 else None + + def check_iter(value: t.Any) -> t.Iterator[t.Any]: + try: + return _check_iter(value) + except TypeError: + # This should only happen when passing in args manually, + # the parser should construct an iterable when parsing + # the command line. + raise BadParameter( + _("Value must be an iterable."), ctx=ctx, param=self + ) from None + + if self.nargs == 1 or self.type.is_composite: + + def convert(value: t.Any) -> t.Any: + return self.type(value, param=self, ctx=ctx) + + elif self.nargs == -1: + + def convert(value: t.Any) -> t.Any: # t.Tuple[t.Any, ...] + return tuple(self.type(x, self, ctx) for x in check_iter(value)) + + else: # nargs > 1 + + def convert(value: t.Any) -> t.Any: # t.Tuple[t.Any, ...] + value = tuple(check_iter(value)) + + if len(value) != self.nargs: + raise BadParameter( + ngettext( + "Takes {nargs} values but 1 was given.", + "Takes {nargs} values but {len} were given.", + len(value), + ).format(nargs=self.nargs, len=len(value)), + ctx=ctx, + param=self, + ) + + return tuple(self.type(x, self, ctx) for x in value) + + if self.multiple: + return tuple(convert(x) for x in check_iter(value)) + + return convert(value) + + def value_is_missing(self, value: t.Any) -> bool: + if value is None: + return True + + if (self.nargs != 1 or self.multiple) and value == (): + return True + + return False + + def process_value(self, ctx: Context, value: t.Any) -> t.Any: + value = self.type_cast_value(ctx, value) + + if self.required and self.value_is_missing(value): + raise MissingParameter(ctx=ctx, param=self) + + if self.callback is not None: + value = self.callback(ctx, self, value) + + return value + + def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]: + if self.envvar is None: + return None + + if isinstance(self.envvar, str): + rv = os.environ.get(self.envvar) + + if rv: + return rv + else: + for envvar in self.envvar: + rv = os.environ.get(envvar) + + if rv: + return rv + + return None + + def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]: + rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx) + + if rv is not None and self.nargs != 1: + rv = self.type.split_envvar_value(rv) + + return rv + + def handle_parse_result( + self, ctx: Context, opts: t.Mapping[str, t.Any], args: t.List[str] + ) -> t.Tuple[t.Any, t.List[str]]: + with augment_usage_errors(ctx, param=self): + value, source = self.consume_value(ctx, opts) + ctx.set_parameter_source(self.name, source) # type: ignore + + try: + value = self.process_value(ctx, value) + except Exception: + if not ctx.resilient_parsing: + raise + + value = None + + if self.expose_value: + ctx.params[self.name] = value # type: ignore + + return value, args + + def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]: + pass + + def get_usage_pieces(self, ctx: Context) -> t.List[str]: + return [] + + def get_error_hint(self, ctx: Context) -> str: + """Get a stringified version of the param for use in error messages to + indicate which param caused the error. + """ + hint_list = self.opts or [self.human_readable_name] + return " / ".join(f"'{x}'" for x in hint_list) + + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. If a + ``shell_complete`` function was given during init, it is used. + Otherwise, the :attr:`type` + :meth:`~click.types.ParamType.shell_complete` function is used. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + if self._custom_shell_complete is not None: + results = self._custom_shell_complete(ctx, self, incomplete) + + if results and isinstance(results[0], str): + from click.shell_completion import CompletionItem + + results = [CompletionItem(c) for c in results] + + return t.cast(t.List["CompletionItem"], results) + + return self.type.shell_complete(ctx, self, incomplete) + + +class Option(Parameter): + """Options are usually optional values on the command line and + have some extra features that arguments don't have. + + All other parameters are passed onwards to the parameter constructor. + + :param show_default: Show the default value for this option in its + help text. Values are not shown by default, unless + :attr:`Context.show_default` is ``True``. If this value is a + string, it shows that string in parentheses instead of the + actual value. This is particularly useful for dynamic options. + For single option boolean flags, the default remains hidden if + its value is ``False``. + :param show_envvar: Controls if an environment variable should be + shown on the help page. Normally, environment variables are not + shown. + :param prompt: If set to ``True`` or a non empty string then the + user will be prompted for input. If set to ``True`` the prompt + will be the option name capitalized. + :param confirmation_prompt: Prompt a second time to confirm the + value if it was prompted for. Can be set to a string instead of + ``True`` to customize the message. + :param prompt_required: If set to ``False``, the user will be + prompted for input only when the option was specified as a flag + without a value. + :param hide_input: If this is ``True`` then the input on the prompt + will be hidden from the user. This is useful for password input. + :param is_flag: forces this option to act as a flag. The default is + auto detection. + :param flag_value: which value should be used for this flag if it's + enabled. This is set to a boolean automatically if + the option string contains a slash to mark two options. + :param multiple: if this is set to `True` then the argument is accepted + multiple times and recorded. This is similar to ``nargs`` + in how it works but supports arbitrary number of + arguments. + :param count: this flag makes an option increment an integer. + :param allow_from_autoenv: if this is enabled then the value of this + parameter will be pulled from an environment + variable in case a prefix is defined on the + context. + :param help: the help string. + :param hidden: hide this option from help outputs. + :param attrs: Other command arguments described in :class:`Parameter`. + + .. versionchanged:: 8.1.0 + Help text indentation is cleaned here instead of only in the + ``@option`` decorator. + + .. versionchanged:: 8.1.0 + The ``show_default`` parameter overrides + ``Context.show_default``. + + .. versionchanged:: 8.1.0 + The default of a single option boolean flag is not shown if the + default value is ``False``. + + .. versionchanged:: 8.0.1 + ``type`` is detected from ``flag_value`` if given. + """ + + param_type_name = "option" + + def __init__( + self, + param_decls: t.Optional[t.Sequence[str]] = None, + show_default: t.Union[bool, str, None] = None, + prompt: t.Union[bool, str] = False, + confirmation_prompt: t.Union[bool, str] = False, + prompt_required: bool = True, + hide_input: bool = False, + is_flag: t.Optional[bool] = None, + flag_value: t.Optional[t.Any] = None, + multiple: bool = False, + count: bool = False, + allow_from_autoenv: bool = True, + type: t.Optional[t.Union[types.ParamType, t.Any]] = None, + help: t.Optional[str] = None, + hidden: bool = False, + show_choices: bool = True, + show_envvar: bool = False, + **attrs: t.Any, + ) -> None: + if help: + help = inspect.cleandoc(help) + + default_is_missing = "default" not in attrs + super().__init__(param_decls, type=type, multiple=multiple, **attrs) + + if prompt is True: + if self.name is None: + raise TypeError("'name' is required with 'prompt=True'.") + + prompt_text: t.Optional[str] = self.name.replace("_", " ").capitalize() + elif prompt is False: + prompt_text = None + else: + prompt_text = prompt + + self.prompt = prompt_text + self.confirmation_prompt = confirmation_prompt + self.prompt_required = prompt_required + self.hide_input = hide_input + self.hidden = hidden + + # If prompt is enabled but not required, then the option can be + # used as a flag to indicate using prompt or flag_value. + self._flag_needs_value = self.prompt is not None and not self.prompt_required + + if is_flag is None: + if flag_value is not None: + # Implicitly a flag because flag_value was set. + is_flag = True + elif self._flag_needs_value: + # Not a flag, but when used as a flag it shows a prompt. + is_flag = False + else: + # Implicitly a flag because flag options were given. + is_flag = bool(self.secondary_opts) + elif is_flag is False and not self._flag_needs_value: + # Not a flag, and prompt is not enabled, can be used as a + # flag if flag_value is set. + self._flag_needs_value = flag_value is not None + + self.default: t.Union[t.Any, t.Callable[[], t.Any]] + + if is_flag and default_is_missing and not self.required: + if multiple: + self.default = () + else: + self.default = False + + if flag_value is None: + flag_value = not self.default + + self.type: types.ParamType + if is_flag and type is None: + # Re-guess the type from the flag value instead of the + # default. + self.type = types.convert_type(None, flag_value) + + self.is_flag: bool = is_flag + self.is_bool_flag: bool = is_flag and isinstance(self.type, types.BoolParamType) + self.flag_value: t.Any = flag_value + + # Counting + self.count = count + if count: + if type is None: + self.type = types.IntRange(min=0) + if default_is_missing: + self.default = 0 + + self.allow_from_autoenv = allow_from_autoenv + self.help = help + self.show_default = show_default + self.show_choices = show_choices + self.show_envvar = show_envvar + + if __debug__: + if self.nargs == -1: + raise TypeError("nargs=-1 is not supported for options.") + + if self.prompt and self.is_flag and not self.is_bool_flag: + raise TypeError("'prompt' is not valid for non-boolean flag.") + + if not self.is_bool_flag and self.secondary_opts: + raise TypeError("Secondary flag is not valid for non-boolean flag.") + + if self.is_bool_flag and self.hide_input and self.prompt is not None: + raise TypeError( + "'prompt' with 'hide_input' is not valid for boolean flag." + ) + + if self.count: + if self.multiple: + raise TypeError("'count' is not valid with 'multiple'.") + + if self.is_flag: + raise TypeError("'count' is not valid with 'is_flag'.") + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + help=self.help, + prompt=self.prompt, + is_flag=self.is_flag, + flag_value=self.flag_value, + count=self.count, + hidden=self.hidden, + ) + return info_dict + + def _parse_decls( + self, decls: t.Sequence[str], expose_value: bool + ) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]: + opts = [] + secondary_opts = [] + name = None + possible_names = [] + + for decl in decls: + if decl.isidentifier(): + if name is not None: + raise TypeError(f"Name '{name}' defined twice") + name = decl + else: + split_char = ";" if decl[:1] == "/" else "/" + if split_char in decl: + first, second = decl.split(split_char, 1) + first = first.rstrip() + if first: + possible_names.append(split_opt(first)) + opts.append(first) + second = second.lstrip() + if second: + secondary_opts.append(second.lstrip()) + if first == second: + raise ValueError( + f"Boolean option {decl!r} cannot use the" + " same flag for true/false." + ) + else: + possible_names.append(split_opt(decl)) + opts.append(decl) + + if name is None and possible_names: + possible_names.sort(key=lambda x: -len(x[0])) # group long options first + name = possible_names[0][1].replace("-", "_").lower() + if not name.isidentifier(): + name = None + + if name is None: + if not expose_value: + return None, opts, secondary_opts + raise TypeError("Could not determine name for option") + + if not opts and not secondary_opts: + raise TypeError( + f"No options defined but a name was passed ({name})." + " Did you mean to declare an argument instead? Did" + f" you mean to pass '--{name}'?" + ) + + return name, opts, secondary_opts + + def add_to_parser(self, parser: OptionParser, ctx: Context) -> None: + if self.multiple: + action = "append" + elif self.count: + action = "count" + else: + action = "store" + + if self.is_flag: + action = f"{action}_const" + + if self.is_bool_flag and self.secondary_opts: + parser.add_option( + obj=self, opts=self.opts, dest=self.name, action=action, const=True + ) + parser.add_option( + obj=self, + opts=self.secondary_opts, + dest=self.name, + action=action, + const=False, + ) + else: + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + const=self.flag_value, + ) + else: + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + nargs=self.nargs, + ) + + def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]: + if self.hidden: + return None + + any_prefix_is_slash = False + + def _write_opts(opts: t.Sequence[str]) -> str: + nonlocal any_prefix_is_slash + + rv, any_slashes = join_options(opts) + + if any_slashes: + any_prefix_is_slash = True + + if not self.is_flag and not self.count: + rv += f" {self.make_metavar()}" + + return rv + + rv = [_write_opts(self.opts)] + + if self.secondary_opts: + rv.append(_write_opts(self.secondary_opts)) + + help = self.help or "" + extra = [] + + if self.show_envvar: + envvar = self.envvar + + if envvar is None: + if ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name is not None + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + + if envvar is not None: + var_str = ( + envvar + if isinstance(envvar, str) + else ", ".join(str(d) for d in envvar) + ) + extra.append(_("env var: {var}").format(var=var_str)) + + # Temporarily enable resilient parsing to avoid type casting + # failing for the default. Might be possible to extend this to + # help formatting in general. + resilient = ctx.resilient_parsing + ctx.resilient_parsing = True + + try: + default_value = self.get_default(ctx, call=False) + finally: + ctx.resilient_parsing = resilient + + show_default = False + show_default_is_str = False + + if self.show_default is not None: + if isinstance(self.show_default, str): + show_default_is_str = show_default = True + else: + show_default = self.show_default + elif ctx.show_default is not None: + show_default = ctx.show_default + + if show_default_is_str or (show_default and (default_value is not None)): + if show_default_is_str: + default_string = f"({self.show_default})" + elif isinstance(default_value, (list, tuple)): + default_string = ", ".join(str(d) for d in default_value) + elif inspect.isfunction(default_value): + default_string = _("(dynamic)") + elif self.is_bool_flag and self.secondary_opts: + # For boolean flags that have distinct True/False opts, + # use the opt without prefix instead of the value. + default_string = split_opt( + (self.opts if self.default else self.secondary_opts)[0] + )[1] + elif self.is_bool_flag and not self.secondary_opts and not default_value: + default_string = "" + else: + default_string = str(default_value) + + if default_string: + extra.append(_("default: {default}").format(default=default_string)) + + if ( + isinstance(self.type, types._NumberRangeBase) + # skip count with default range type + and not (self.count and self.type.min == 0 and self.type.max is None) + ): + range_str = self.type._describe_range() + + if range_str: + extra.append(range_str) + + if self.required: + extra.append(_("required")) + + if extra: + extra_str = "; ".join(extra) + help = f"{help} [{extra_str}]" if help else f"[{extra_str}]" + + return ("; " if any_prefix_is_slash else " / ").join(rv), help + + @t.overload + def get_default( + self, ctx: Context, call: "te.Literal[True]" = True + ) -> t.Optional[t.Any]: + ... + + @t.overload + def get_default( + self, ctx: Context, call: bool = ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + ... + + def get_default( + self, ctx: Context, call: bool = True + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + # If we're a non boolean flag our default is more complex because + # we need to look at all flags in the same group to figure out + # if we're the default one in which case we return the flag + # value as default. + if self.is_flag and not self.is_bool_flag: + for param in ctx.command.params: + if param.name == self.name and param.default: + return t.cast(Option, param).flag_value + + return None + + return super().get_default(ctx, call=call) + + def prompt_for_value(self, ctx: Context) -> t.Any: + """This is an alternative flow that can be activated in the full + value processing if a value does not exist. It will prompt the + user until a valid value exists and then returns the processed + value as result. + """ + assert self.prompt is not None + + # Calculate the default before prompting anything to be stable. + default = self.get_default(ctx) + + # If this is a prompt for a flag we need to handle this + # differently. + if self.is_bool_flag: + return confirm(self.prompt, default) + + return prompt( + self.prompt, + default=default, + type=self.type, + hide_input=self.hide_input, + show_choices=self.show_choices, + confirmation_prompt=self.confirmation_prompt, + value_proc=lambda x: self.process_value(ctx, x), + ) + + def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]: + rv = super().resolve_envvar_value(ctx) + + if rv is not None: + return rv + + if ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name is not None + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + rv = os.environ.get(envvar) + + if rv: + return rv + + return None + + def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]: + rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx) + + if rv is None: + return None + + value_depth = (self.nargs != 1) + bool(self.multiple) + + if value_depth > 0: + rv = self.type.split_envvar_value(rv) + + if self.multiple and self.nargs != 1: + rv = batch(rv, self.nargs) + + return rv + + def consume_value( + self, ctx: Context, opts: t.Mapping[str, "Parameter"] + ) -> t.Tuple[t.Any, ParameterSource]: + value, source = super().consume_value(ctx, opts) + + # The parser will emit a sentinel value if the option can be + # given as a flag without a value. This is different from None + # to distinguish from the flag not being given at all. + if value is _flag_needs_value: + if self.prompt is not None and not ctx.resilient_parsing: + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + else: + value = self.flag_value + source = ParameterSource.COMMANDLINE + + elif ( + self.multiple + and value is not None + and any(v is _flag_needs_value for v in value) + ): + value = [self.flag_value if v is _flag_needs_value else v for v in value] + source = ParameterSource.COMMANDLINE + + # The value wasn't set, or used the param's default, prompt if + # prompting is enabled. + elif ( + source in {None, ParameterSource.DEFAULT} + and self.prompt is not None + and (self.required or self.prompt_required) + and not ctx.resilient_parsing + ): + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + + return value, source + + +class Argument(Parameter): + """Arguments are positional parameters to a command. They generally + provide fewer features than options but can have infinite ``nargs`` + and are required by default. + + All parameters are passed onwards to the constructor of :class:`Parameter`. + """ + + param_type_name = "argument" + + def __init__( + self, + param_decls: t.Sequence[str], + required: t.Optional[bool] = None, + **attrs: t.Any, + ) -> None: + if required is None: + if attrs.get("default") is not None: + required = False + else: + required = attrs.get("nargs", 1) > 0 + + if "multiple" in attrs: + raise TypeError("__init__() got an unexpected keyword argument 'multiple'.") + + super().__init__(param_decls, required=required, **attrs) + + if __debug__: + if self.default is not None and self.nargs == -1: + raise TypeError("'default' is not supported for nargs=-1.") + + @property + def human_readable_name(self) -> str: + if self.metavar is not None: + return self.metavar + return self.name.upper() # type: ignore + + def make_metavar(self) -> str: + if self.metavar is not None: + return self.metavar + var = self.type.get_metavar(self) + if not var: + var = self.name.upper() # type: ignore + if not self.required: + var = f"[{var}]" + if self.nargs != 1: + var += "..." + return var + + def _parse_decls( + self, decls: t.Sequence[str], expose_value: bool + ) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]: + if not decls: + if not expose_value: + return None, [], [] + raise TypeError("Could not determine name for argument") + if len(decls) == 1: + name = arg = decls[0] + name = name.replace("-", "_").lower() + else: + raise TypeError( + "Arguments take exactly one parameter declaration, got" + f" {len(decls)}." + ) + return name, [arg], [] + + def get_usage_pieces(self, ctx: Context) -> t.List[str]: + return [self.make_metavar()] + + def get_error_hint(self, ctx: Context) -> str: + return f"'{self.make_metavar()}'" + + def add_to_parser(self, parser: OptionParser, ctx: Context) -> None: + parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/decorators.py b/lib/go-jinja2/internal/data/darwin-amd64/click/decorators.py new file mode 100644 index 000000000..d9bba9502 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/decorators.py @@ -0,0 +1,561 @@ +import inspect +import types +import typing as t +from functools import update_wrapper +from gettext import gettext as _ + +from .core import Argument +from .core import Command +from .core import Context +from .core import Group +from .core import Option +from .core import Parameter +from .globals import get_current_context +from .utils import echo + +if t.TYPE_CHECKING: + import typing_extensions as te + + P = te.ParamSpec("P") + +R = t.TypeVar("R") +T = t.TypeVar("T") +_AnyCallable = t.Callable[..., t.Any] +FC = t.TypeVar("FC", bound=t.Union[_AnyCallable, Command]) + + +def pass_context(f: "t.Callable[te.Concatenate[Context, P], R]") -> "t.Callable[P, R]": + """Marks a callback as wanting to receive the current context + object as first argument. + """ + + def new_func(*args: "P.args", **kwargs: "P.kwargs") -> "R": + return f(get_current_context(), *args, **kwargs) + + return update_wrapper(new_func, f) + + +def pass_obj(f: "t.Callable[te.Concatenate[t.Any, P], R]") -> "t.Callable[P, R]": + """Similar to :func:`pass_context`, but only pass the object on the + context onwards (:attr:`Context.obj`). This is useful if that object + represents the state of a nested system. + """ + + def new_func(*args: "P.args", **kwargs: "P.kwargs") -> "R": + return f(get_current_context().obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + +def make_pass_decorator( + object_type: t.Type[T], ensure: bool = False +) -> t.Callable[["t.Callable[te.Concatenate[T, P], R]"], "t.Callable[P, R]"]: + """Given an object type this creates a decorator that will work + similar to :func:`pass_obj` but instead of passing the object of the + current context, it will find the innermost context of type + :func:`object_type`. + + This generates a decorator that works roughly like this:: + + from functools import update_wrapper + + def decorator(f): + @pass_context + def new_func(ctx, *args, **kwargs): + obj = ctx.find_object(object_type) + return ctx.invoke(f, obj, *args, **kwargs) + return update_wrapper(new_func, f) + return decorator + + :param object_type: the type of the object to pass. + :param ensure: if set to `True`, a new object will be created and + remembered on the context if it's not there yet. + """ + + def decorator(f: "t.Callable[te.Concatenate[T, P], R]") -> "t.Callable[P, R]": + def new_func(*args: "P.args", **kwargs: "P.kwargs") -> "R": + ctx = get_current_context() + + obj: t.Optional[T] + if ensure: + obj = ctx.ensure_object(object_type) + else: + obj = ctx.find_object(object_type) + + if obj is None: + raise RuntimeError( + "Managed to invoke callback without a context" + f" object of type {object_type.__name__!r}" + " existing." + ) + + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + return decorator # type: ignore[return-value] + + +def pass_meta_key( + key: str, *, doc_description: t.Optional[str] = None +) -> "t.Callable[[t.Callable[te.Concatenate[t.Any, P], R]], t.Callable[P, R]]": + """Create a decorator that passes a key from + :attr:`click.Context.meta` as the first argument to the decorated + function. + + :param key: Key in ``Context.meta`` to pass. + :param doc_description: Description of the object being passed, + inserted into the decorator's docstring. Defaults to "the 'key' + key from Context.meta". + + .. versionadded:: 8.0 + """ + + def decorator(f: "t.Callable[te.Concatenate[t.Any, P], R]") -> "t.Callable[P, R]": + def new_func(*args: "P.args", **kwargs: "P.kwargs") -> R: + ctx = get_current_context() + obj = ctx.meta[key] + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + if doc_description is None: + doc_description = f"the {key!r} key from :attr:`click.Context.meta`" + + decorator.__doc__ = ( + f"Decorator that passes {doc_description} as the first argument" + " to the decorated function." + ) + return decorator # type: ignore[return-value] + + +CmdType = t.TypeVar("CmdType", bound=Command) + + +# variant: no call, directly as decorator for a function. +@t.overload +def command(name: _AnyCallable) -> Command: + ... + + +# variant: with positional name and with positional or keyword cls argument: +# @command(namearg, CommandCls, ...) or @command(namearg, cls=CommandCls, ...) +@t.overload +def command( + name: t.Optional[str], + cls: t.Type[CmdType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], CmdType]: + ... + + +# variant: name omitted, cls _must_ be a keyword argument, @command(cls=CommandCls, ...) +@t.overload +def command( + name: None = None, + *, + cls: t.Type[CmdType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], CmdType]: + ... + + +# variant: with optional string name, no cls argument provided. +@t.overload +def command( + name: t.Optional[str] = ..., cls: None = None, **attrs: t.Any +) -> t.Callable[[_AnyCallable], Command]: + ... + + +def command( + name: t.Union[t.Optional[str], _AnyCallable] = None, + cls: t.Optional[t.Type[CmdType]] = None, + **attrs: t.Any, +) -> t.Union[Command, t.Callable[[_AnyCallable], t.Union[Command, CmdType]]]: + r"""Creates a new :class:`Command` and uses the decorated function as + callback. This will also automatically attach all decorated + :func:`option`\s and :func:`argument`\s as parameters to the command. + + The name of the command defaults to the name of the function with + underscores replaced by dashes. If you want to change that, you can + pass the intended name as the first argument. + + All keyword arguments are forwarded to the underlying command class. + For the ``params`` argument, any decorated params are appended to + the end of the list. + + Once decorated the function turns into a :class:`Command` instance + that can be invoked as a command line utility or be attached to a + command :class:`Group`. + + :param name: the name of the command. This defaults to the function + name with underscores replaced by dashes. + :param cls: the command class to instantiate. This defaults to + :class:`Command`. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.1 + The ``params`` argument can be used. Decorated params are + appended to the end of the list. + """ + + func: t.Optional[t.Callable[[_AnyCallable], t.Any]] = None + + if callable(name): + func = name + name = None + assert cls is None, "Use 'command(cls=cls)(callable)' to specify a class." + assert not attrs, "Use 'command(**kwargs)(callable)' to provide arguments." + + if cls is None: + cls = t.cast(t.Type[CmdType], Command) + + def decorator(f: _AnyCallable) -> CmdType: + if isinstance(f, Command): + raise TypeError("Attempted to convert a callback into a command twice.") + + attr_params = attrs.pop("params", None) + params = attr_params if attr_params is not None else [] + + try: + decorator_params = f.__click_params__ # type: ignore + except AttributeError: + pass + else: + del f.__click_params__ # type: ignore + params.extend(reversed(decorator_params)) + + if attrs.get("help") is None: + attrs["help"] = f.__doc__ + + if t.TYPE_CHECKING: + assert cls is not None + assert not callable(name) + + cmd = cls( + name=name or f.__name__.lower().replace("_", "-"), + callback=f, + params=params, + **attrs, + ) + cmd.__doc__ = f.__doc__ + return cmd + + if func is not None: + return decorator(func) + + return decorator + + +GrpType = t.TypeVar("GrpType", bound=Group) + + +# variant: no call, directly as decorator for a function. +@t.overload +def group(name: _AnyCallable) -> Group: + ... + + +# variant: with positional name and with positional or keyword cls argument: +# @group(namearg, GroupCls, ...) or @group(namearg, cls=GroupCls, ...) +@t.overload +def group( + name: t.Optional[str], + cls: t.Type[GrpType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], GrpType]: + ... + + +# variant: name omitted, cls _must_ be a keyword argument, @group(cmd=GroupCls, ...) +@t.overload +def group( + name: None = None, + *, + cls: t.Type[GrpType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], GrpType]: + ... + + +# variant: with optional string name, no cls argument provided. +@t.overload +def group( + name: t.Optional[str] = ..., cls: None = None, **attrs: t.Any +) -> t.Callable[[_AnyCallable], Group]: + ... + + +def group( + name: t.Union[str, _AnyCallable, None] = None, + cls: t.Optional[t.Type[GrpType]] = None, + **attrs: t.Any, +) -> t.Union[Group, t.Callable[[_AnyCallable], t.Union[Group, GrpType]]]: + """Creates a new :class:`Group` with a function as callback. This + works otherwise the same as :func:`command` just that the `cls` + parameter is set to :class:`Group`. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + """ + if cls is None: + cls = t.cast(t.Type[GrpType], Group) + + if callable(name): + return command(cls=cls, **attrs)(name) + + return command(name, cls, **attrs) + + +def _param_memo(f: t.Callable[..., t.Any], param: Parameter) -> None: + if isinstance(f, Command): + f.params.append(param) + else: + if not hasattr(f, "__click_params__"): + f.__click_params__ = [] # type: ignore + + f.__click_params__.append(param) # type: ignore + + +def argument( + *param_decls: str, cls: t.Optional[t.Type[Argument]] = None, **attrs: t.Any +) -> t.Callable[[FC], FC]: + """Attaches an argument to the command. All positional arguments are + passed as parameter declarations to :class:`Argument`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Argument` instance manually + and attaching it to the :attr:`Command.params` list. + + For the default argument class, refer to :class:`Argument` and + :class:`Parameter` for descriptions of parameters. + + :param cls: the argument class to instantiate. This defaults to + :class:`Argument`. + :param param_decls: Passed as positional arguments to the constructor of + ``cls``. + :param attrs: Passed as keyword arguments to the constructor of ``cls``. + """ + if cls is None: + cls = Argument + + def decorator(f: FC) -> FC: + _param_memo(f, cls(param_decls, **attrs)) + return f + + return decorator + + +def option( + *param_decls: str, cls: t.Optional[t.Type[Option]] = None, **attrs: t.Any +) -> t.Callable[[FC], FC]: + """Attaches an option to the command. All positional arguments are + passed as parameter declarations to :class:`Option`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Option` instance manually + and attaching it to the :attr:`Command.params` list. + + For the default option class, refer to :class:`Option` and + :class:`Parameter` for descriptions of parameters. + + :param cls: the option class to instantiate. This defaults to + :class:`Option`. + :param param_decls: Passed as positional arguments to the constructor of + ``cls``. + :param attrs: Passed as keyword arguments to the constructor of ``cls``. + """ + if cls is None: + cls = Option + + def decorator(f: FC) -> FC: + _param_memo(f, cls(param_decls, **attrs)) + return f + + return decorator + + +def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--yes`` option which shows a prompt before continuing if + not passed. If the prompt is declined, the program will exit. + + :param param_decls: One or more option names. Defaults to the single + value ``"--yes"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value: + ctx.abort() + + if not param_decls: + param_decls = ("--yes",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("callback", callback) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("prompt", "Do you want to continue?") + kwargs.setdefault("help", "Confirm the action without prompting.") + return option(*param_decls, **kwargs) + + +def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--password`` option which prompts for a password, hiding + input and asking to enter the value again for confirmation. + + :param param_decls: One or more option names. Defaults to the single + value ``"--password"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + if not param_decls: + param_decls = ("--password",) + + kwargs.setdefault("prompt", True) + kwargs.setdefault("confirmation_prompt", True) + kwargs.setdefault("hide_input", True) + return option(*param_decls, **kwargs) + + +def version_option( + version: t.Optional[str] = None, + *param_decls: str, + package_name: t.Optional[str] = None, + prog_name: t.Optional[str] = None, + message: t.Optional[str] = None, + **kwargs: t.Any, +) -> t.Callable[[FC], FC]: + """Add a ``--version`` option which immediately prints the version + number and exits the program. + + If ``version`` is not provided, Click will try to detect it using + :func:`importlib.metadata.version` to get the version for the + ``package_name``. On Python < 3.8, the ``importlib_metadata`` + backport must be installed. + + If ``package_name`` is not provided, Click will try to detect it by + inspecting the stack frames. This will be used to detect the + version, so it must match the name of the installed package. + + :param version: The version number to show. If not provided, Click + will try to detect it. + :param param_decls: One or more option names. Defaults to the single + value ``"--version"``. + :param package_name: The package name to detect the version from. If + not provided, Click will try to detect it. + :param prog_name: The name of the CLI to show in the message. If not + provided, it will be detected from the command. + :param message: The message to show. The values ``%(prog)s``, + ``%(package)s``, and ``%(version)s`` are available. Defaults to + ``"%(prog)s, version %(version)s"``. + :param kwargs: Extra arguments are passed to :func:`option`. + :raise RuntimeError: ``version`` could not be detected. + + .. versionchanged:: 8.0 + Add the ``package_name`` parameter, and the ``%(package)s`` + value for messages. + + .. versionchanged:: 8.0 + Use :mod:`importlib.metadata` instead of ``pkg_resources``. The + version is detected based on the package name, not the entry + point name. The Python package name must match the installed + package name, or be passed with ``package_name=``. + """ + if message is None: + message = _("%(prog)s, version %(version)s") + + if version is None and package_name is None: + frame = inspect.currentframe() + f_back = frame.f_back if frame is not None else None + f_globals = f_back.f_globals if f_back is not None else None + # break reference cycle + # https://docs.python.org/3/library/inspect.html#the-interpreter-stack + del frame + + if f_globals is not None: + package_name = f_globals.get("__name__") + + if package_name == "__main__": + package_name = f_globals.get("__package__") + + if package_name: + package_name = package_name.partition(".")[0] + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + nonlocal prog_name + nonlocal version + + if prog_name is None: + prog_name = ctx.find_root().info_name + + if version is None and package_name is not None: + metadata: t.Optional[types.ModuleType] + + try: + from importlib import metadata # type: ignore + except ImportError: + # Python < 3.8 + import importlib_metadata as metadata # type: ignore + + try: + version = metadata.version(package_name) # type: ignore + except metadata.PackageNotFoundError: # type: ignore + raise RuntimeError( + f"{package_name!r} is not installed. Try passing" + " 'package_name' instead." + ) from None + + if version is None: + raise RuntimeError( + f"Could not determine the version for {package_name!r} automatically." + ) + + echo( + message % {"prog": prog_name, "package": package_name, "version": version}, + color=ctx.color, + ) + ctx.exit() + + if not param_decls: + param_decls = ("--version",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show the version and exit.")) + kwargs["callback"] = callback + return option(*param_decls, **kwargs) + + +def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--help`` option which immediately prints the help page + and exits the program. + + This is usually unnecessary, as the ``--help`` option is added to + each command automatically unless ``add_help_option=False`` is + passed. + + :param param_decls: One or more option names. Defaults to the single + value ``"--help"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + if not param_decls: + param_decls = ("--help",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show this message and exit.")) + kwargs["callback"] = callback + return option(*param_decls, **kwargs) diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/exceptions.py b/lib/go-jinja2/internal/data/darwin-amd64/click/exceptions.py new file mode 100644 index 000000000..fe68a3613 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/exceptions.py @@ -0,0 +1,288 @@ +import typing as t +from gettext import gettext as _ +from gettext import ngettext + +from ._compat import get_text_stderr +from .utils import echo +from .utils import format_filename + +if t.TYPE_CHECKING: + from .core import Command + from .core import Context + from .core import Parameter + + +def _join_param_hints( + param_hint: t.Optional[t.Union[t.Sequence[str], str]] +) -> t.Optional[str]: + if param_hint is not None and not isinstance(param_hint, str): + return " / ".join(repr(x) for x in param_hint) + + return param_hint + + +class ClickException(Exception): + """An exception that Click can handle and show to the user.""" + + #: The exit code for this exception. + exit_code = 1 + + def __init__(self, message: str) -> None: + super().__init__(message) + self.message = message + + def format_message(self) -> str: + return self.message + + def __str__(self) -> str: + return self.message + + def show(self, file: t.Optional[t.IO[t.Any]] = None) -> None: + if file is None: + file = get_text_stderr() + + echo(_("Error: {message}").format(message=self.format_message()), file=file) + + +class UsageError(ClickException): + """An internal exception that signals a usage error. This typically + aborts any further handling. + + :param message: the error message to display. + :param ctx: optionally the context that caused this error. Click will + fill in the context automatically in some situations. + """ + + exit_code = 2 + + def __init__(self, message: str, ctx: t.Optional["Context"] = None) -> None: + super().__init__(message) + self.ctx = ctx + self.cmd: t.Optional["Command"] = self.ctx.command if self.ctx else None + + def show(self, file: t.Optional[t.IO[t.Any]] = None) -> None: + if file is None: + file = get_text_stderr() + color = None + hint = "" + if ( + self.ctx is not None + and self.ctx.command.get_help_option(self.ctx) is not None + ): + hint = _("Try '{command} {option}' for help.").format( + command=self.ctx.command_path, option=self.ctx.help_option_names[0] + ) + hint = f"{hint}\n" + if self.ctx is not None: + color = self.ctx.color + echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color) + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=color, + ) + + +class BadParameter(UsageError): + """An exception that formats out a standardized error message for a + bad parameter. This is useful when thrown from a callback or type as + Click will attach contextual information to it (for instance, which + parameter it is). + + .. versionadded:: 2.0 + + :param param: the parameter object that caused this error. This can + be left out, and Click will attach this info itself + if possible. + :param param_hint: a string that shows up as parameter name. This + can be used as alternative to `param` in cases + where custom validation should happen. If it is + a string it's used as such, if it's a list then + each item is quoted and separated. + """ + + def __init__( + self, + message: str, + ctx: t.Optional["Context"] = None, + param: t.Optional["Parameter"] = None, + param_hint: t.Optional[str] = None, + ) -> None: + super().__init__(message, ctx) + self.param = param + self.param_hint = param_hint + + def format_message(self) -> str: + if self.param_hint is not None: + param_hint = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) # type: ignore + else: + return _("Invalid value: {message}").format(message=self.message) + + return _("Invalid value for {param_hint}: {message}").format( + param_hint=_join_param_hints(param_hint), message=self.message + ) + + +class MissingParameter(BadParameter): + """Raised if click required an option or argument but it was not + provided when invoking the script. + + .. versionadded:: 4.0 + + :param param_type: a string that indicates the type of the parameter. + The default is to inherit the parameter type from + the given `param`. Valid values are ``'parameter'``, + ``'option'`` or ``'argument'``. + """ + + def __init__( + self, + message: t.Optional[str] = None, + ctx: t.Optional["Context"] = None, + param: t.Optional["Parameter"] = None, + param_hint: t.Optional[str] = None, + param_type: t.Optional[str] = None, + ) -> None: + super().__init__(message or "", ctx, param, param_hint) + self.param_type = param_type + + def format_message(self) -> str: + if self.param_hint is not None: + param_hint: t.Optional[str] = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) # type: ignore + else: + param_hint = None + + param_hint = _join_param_hints(param_hint) + param_hint = f" {param_hint}" if param_hint else "" + + param_type = self.param_type + if param_type is None and self.param is not None: + param_type = self.param.param_type_name + + msg = self.message + if self.param is not None: + msg_extra = self.param.type.get_missing_message(self.param) + if msg_extra: + if msg: + msg += f". {msg_extra}" + else: + msg = msg_extra + + msg = f" {msg}" if msg else "" + + # Translate param_type for known types. + if param_type == "argument": + missing = _("Missing argument") + elif param_type == "option": + missing = _("Missing option") + elif param_type == "parameter": + missing = _("Missing parameter") + else: + missing = _("Missing {param_type}").format(param_type=param_type) + + return f"{missing}{param_hint}.{msg}" + + def __str__(self) -> str: + if not self.message: + param_name = self.param.name if self.param else None + return _("Missing parameter: {param_name}").format(param_name=param_name) + else: + return self.message + + +class NoSuchOption(UsageError): + """Raised if click attempted to handle an option that does not + exist. + + .. versionadded:: 4.0 + """ + + def __init__( + self, + option_name: str, + message: t.Optional[str] = None, + possibilities: t.Optional[t.Sequence[str]] = None, + ctx: t.Optional["Context"] = None, + ) -> None: + if message is None: + message = _("No such option: {name}").format(name=option_name) + + super().__init__(message, ctx) + self.option_name = option_name + self.possibilities = possibilities + + def format_message(self) -> str: + if not self.possibilities: + return self.message + + possibility_str = ", ".join(sorted(self.possibilities)) + suggest = ngettext( + "Did you mean {possibility}?", + "(Possible options: {possibilities})", + len(self.possibilities), + ).format(possibility=possibility_str, possibilities=possibility_str) + return f"{self.message} {suggest}" + + +class BadOptionUsage(UsageError): + """Raised if an option is generally supplied but the use of the option + was incorrect. This is for instance raised if the number of arguments + for an option is not correct. + + .. versionadded:: 4.0 + + :param option_name: the name of the option being used incorrectly. + """ + + def __init__( + self, option_name: str, message: str, ctx: t.Optional["Context"] = None + ) -> None: + super().__init__(message, ctx) + self.option_name = option_name + + +class BadArgumentUsage(UsageError): + """Raised if an argument is generally supplied but the use of the argument + was incorrect. This is for instance raised if the number of values + for an argument is not correct. + + .. versionadded:: 6.0 + """ + + +class FileError(ClickException): + """Raised if a file cannot be opened.""" + + def __init__(self, filename: str, hint: t.Optional[str] = None) -> None: + if hint is None: + hint = _("unknown error") + + super().__init__(hint) + self.ui_filename: str = format_filename(filename) + self.filename = filename + + def format_message(self) -> str: + return _("Could not open file {filename!r}: {message}").format( + filename=self.ui_filename, message=self.message + ) + + +class Abort(RuntimeError): + """An internal signalling exception that signals Click to abort.""" + + +class Exit(RuntimeError): + """An exception that indicates that the application should exit with some + status code. + + :param code: the status code to exit with. + """ + + __slots__ = ("exit_code",) + + def __init__(self, code: int = 0) -> None: + self.exit_code: int = code diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/formatting.py b/lib/go-jinja2/internal/data/darwin-amd64/click/formatting.py new file mode 100644 index 000000000..ddd2a2f82 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/formatting.py @@ -0,0 +1,301 @@ +import typing as t +from contextlib import contextmanager +from gettext import gettext as _ + +from ._compat import term_len +from .parser import split_opt + +# Can force a width. This is used by the test system +FORCED_WIDTH: t.Optional[int] = None + + +def measure_table(rows: t.Iterable[t.Tuple[str, str]]) -> t.Tuple[int, ...]: + widths: t.Dict[int, int] = {} + + for row in rows: + for idx, col in enumerate(row): + widths[idx] = max(widths.get(idx, 0), term_len(col)) + + return tuple(y for x, y in sorted(widths.items())) + + +def iter_rows( + rows: t.Iterable[t.Tuple[str, str]], col_count: int +) -> t.Iterator[t.Tuple[str, ...]]: + for row in rows: + yield row + ("",) * (col_count - len(row)) + + +def wrap_text( + text: str, + width: int = 78, + initial_indent: str = "", + subsequent_indent: str = "", + preserve_paragraphs: bool = False, +) -> str: + """A helper function that intelligently wraps text. By default, it + assumes that it operates on a single paragraph of text but if the + `preserve_paragraphs` parameter is provided it will intelligently + handle paragraphs (defined by two empty lines). + + If paragraphs are handled, a paragraph can be prefixed with an empty + line containing the ``\\b`` character (``\\x08``) to indicate that + no rewrapping should happen in that block. + + :param text: the text that should be rewrapped. + :param width: the maximum width for the text. + :param initial_indent: the initial indent that should be placed on the + first line as a string. + :param subsequent_indent: the indent string that should be placed on + each consecutive line. + :param preserve_paragraphs: if this flag is set then the wrapping will + intelligently handle paragraphs. + """ + from ._textwrap import TextWrapper + + text = text.expandtabs() + wrapper = TextWrapper( + width, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + replace_whitespace=False, + ) + if not preserve_paragraphs: + return wrapper.fill(text) + + p: t.List[t.Tuple[int, bool, str]] = [] + buf: t.List[str] = [] + indent = None + + def _flush_par() -> None: + if not buf: + return + if buf[0].strip() == "\b": + p.append((indent or 0, True, "\n".join(buf[1:]))) + else: + p.append((indent or 0, False, " ".join(buf))) + del buf[:] + + for line in text.splitlines(): + if not line: + _flush_par() + indent = None + else: + if indent is None: + orig_len = term_len(line) + line = line.lstrip() + indent = orig_len - term_len(line) + buf.append(line) + _flush_par() + + rv = [] + for indent, raw, text in p: + with wrapper.extra_indent(" " * indent): + if raw: + rv.append(wrapper.indent_only(text)) + else: + rv.append(wrapper.fill(text)) + + return "\n\n".join(rv) + + +class HelpFormatter: + """This class helps with formatting text-based help pages. It's + usually just needed for very special internal cases, but it's also + exposed so that developers can write their own fancy outputs. + + At present, it always writes into memory. + + :param indent_increment: the additional increment for each level. + :param width: the width for the text. This defaults to the terminal + width clamped to a maximum of 78. + """ + + def __init__( + self, + indent_increment: int = 2, + width: t.Optional[int] = None, + max_width: t.Optional[int] = None, + ) -> None: + import shutil + + self.indent_increment = indent_increment + if max_width is None: + max_width = 80 + if width is None: + width = FORCED_WIDTH + if width is None: + width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50) + self.width = width + self.current_indent = 0 + self.buffer: t.List[str] = [] + + def write(self, string: str) -> None: + """Writes a unicode string into the internal buffer.""" + self.buffer.append(string) + + def indent(self) -> None: + """Increases the indentation.""" + self.current_indent += self.indent_increment + + def dedent(self) -> None: + """Decreases the indentation.""" + self.current_indent -= self.indent_increment + + def write_usage( + self, prog: str, args: str = "", prefix: t.Optional[str] = None + ) -> None: + """Writes a usage line into the buffer. + + :param prog: the program name. + :param args: whitespace separated list of arguments. + :param prefix: The prefix for the first line. Defaults to + ``"Usage: "``. + """ + if prefix is None: + prefix = f"{_('Usage:')} " + + usage_prefix = f"{prefix:>{self.current_indent}}{prog} " + text_width = self.width - self.current_indent + + if text_width >= (term_len(usage_prefix) + 20): + # The arguments will fit to the right of the prefix. + indent = " " * term_len(usage_prefix) + self.write( + wrap_text( + args, + text_width, + initial_indent=usage_prefix, + subsequent_indent=indent, + ) + ) + else: + # The prefix is too long, put the arguments on the next line. + self.write(usage_prefix) + self.write("\n") + indent = " " * (max(self.current_indent, term_len(prefix)) + 4) + self.write( + wrap_text( + args, text_width, initial_indent=indent, subsequent_indent=indent + ) + ) + + self.write("\n") + + def write_heading(self, heading: str) -> None: + """Writes a heading into the buffer.""" + self.write(f"{'':>{self.current_indent}}{heading}:\n") + + def write_paragraph(self) -> None: + """Writes a paragraph into the buffer.""" + if self.buffer: + self.write("\n") + + def write_text(self, text: str) -> None: + """Writes re-indented text into the buffer. This rewraps and + preserves paragraphs. + """ + indent = " " * self.current_indent + self.write( + wrap_text( + text, + self.width, + initial_indent=indent, + subsequent_indent=indent, + preserve_paragraphs=True, + ) + ) + self.write("\n") + + def write_dl( + self, + rows: t.Sequence[t.Tuple[str, str]], + col_max: int = 30, + col_spacing: int = 2, + ) -> None: + """Writes a definition list into the buffer. This is how options + and commands are usually formatted. + + :param rows: a list of two item tuples for the terms and values. + :param col_max: the maximum width of the first column. + :param col_spacing: the number of spaces between the first and + second column. + """ + rows = list(rows) + widths = measure_table(rows) + if len(widths) != 2: + raise TypeError("Expected two columns for definition list") + + first_col = min(widths[0], col_max) + col_spacing + + for first, second in iter_rows(rows, len(widths)): + self.write(f"{'':>{self.current_indent}}{first}") + if not second: + self.write("\n") + continue + if term_len(first) <= first_col - col_spacing: + self.write(" " * (first_col - term_len(first))) + else: + self.write("\n") + self.write(" " * (first_col + self.current_indent)) + + text_width = max(self.width - first_col - 2, 10) + wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) + lines = wrapped_text.splitlines() + + if lines: + self.write(f"{lines[0]}\n") + + for line in lines[1:]: + self.write(f"{'':>{first_col + self.current_indent}}{line}\n") + else: + self.write("\n") + + @contextmanager + def section(self, name: str) -> t.Iterator[None]: + """Helpful context manager that writes a paragraph, a heading, + and the indents. + + :param name: the section name that is written as heading. + """ + self.write_paragraph() + self.write_heading(name) + self.indent() + try: + yield + finally: + self.dedent() + + @contextmanager + def indentation(self) -> t.Iterator[None]: + """A context manager that increases the indentation.""" + self.indent() + try: + yield + finally: + self.dedent() + + def getvalue(self) -> str: + """Returns the buffer contents.""" + return "".join(self.buffer) + + +def join_options(options: t.Sequence[str]) -> t.Tuple[str, bool]: + """Given a list of option strings this joins them in the most appropriate + way and returns them in the form ``(formatted_string, + any_prefix_is_slash)`` where the second item in the tuple is a flag that + indicates if any of the option prefixes was a slash. + """ + rv = [] + any_prefix_is_slash = False + + for opt in options: + prefix = split_opt(opt)[0] + + if prefix == "/": + any_prefix_is_slash = True + + rv.append((len(prefix), opt)) + + rv.sort(key=lambda x: x[0]) + return ", ".join(x[1] for x in rv), any_prefix_is_slash diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/globals.py b/lib/go-jinja2/internal/data/darwin-amd64/click/globals.py new file mode 100644 index 000000000..480058f10 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/globals.py @@ -0,0 +1,68 @@ +import typing as t +from threading import local + +if t.TYPE_CHECKING: + import typing_extensions as te + from .core import Context + +_local = local() + + +@t.overload +def get_current_context(silent: "te.Literal[False]" = False) -> "Context": + ... + + +@t.overload +def get_current_context(silent: bool = ...) -> t.Optional["Context"]: + ... + + +def get_current_context(silent: bool = False) -> t.Optional["Context"]: + """Returns the current click context. This can be used as a way to + access the current context object from anywhere. This is a more implicit + alternative to the :func:`pass_context` decorator. This function is + primarily useful for helpers such as :func:`echo` which might be + interested in changing its behavior based on the current context. + + To push the current context, :meth:`Context.scope` can be used. + + .. versionadded:: 5.0 + + :param silent: if set to `True` the return value is `None` if no context + is available. The default behavior is to raise a + :exc:`RuntimeError`. + """ + try: + return t.cast("Context", _local.stack[-1]) + except (AttributeError, IndexError) as e: + if not silent: + raise RuntimeError("There is no active click context.") from e + + return None + + +def push_context(ctx: "Context") -> None: + """Pushes a new context to the current stack.""" + _local.__dict__.setdefault("stack", []).append(ctx) + + +def pop_context() -> None: + """Removes the top level from the stack.""" + _local.stack.pop() + + +def resolve_color_default(color: t.Optional[bool] = None) -> t.Optional[bool]: + """Internal helper to get the default value of the color flag. If a + value is passed it's returned unchanged, otherwise it's looked up from + the current context. + """ + if color is not None: + return color + + ctx = get_current_context(silent=True) + + if ctx is not None: + return ctx.color + + return None diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/parser.py b/lib/go-jinja2/internal/data/darwin-amd64/click/parser.py new file mode 100644 index 000000000..5fa7adfac --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/parser.py @@ -0,0 +1,529 @@ +""" +This module started out as largely a copy paste from the stdlib's +optparse module with the features removed that we do not need from +optparse because we implement them in Click on a higher level (for +instance type handling, help formatting and a lot more). + +The plan is to remove more and more from here over time. + +The reason this is a different module and not optparse from the stdlib +is that there are differences in 2.x and 3.x about the error messages +generated and optparse in the stdlib uses gettext for no good reason +and might cause us issues. + +Click uses parts of optparse written by Gregory P. Ward and maintained +by the Python Software Foundation. This is limited to code in parser.py. + +Copyright 2001-2006 Gregory P. Ward. All rights reserved. +Copyright 2002-2006 Python Software Foundation. All rights reserved. +""" +# This code uses parts of optparse written by Gregory P. Ward and +# maintained by the Python Software Foundation. +# Copyright 2001-2006 Gregory P. Ward +# Copyright 2002-2006 Python Software Foundation +import typing as t +from collections import deque +from gettext import gettext as _ +from gettext import ngettext + +from .exceptions import BadArgumentUsage +from .exceptions import BadOptionUsage +from .exceptions import NoSuchOption +from .exceptions import UsageError + +if t.TYPE_CHECKING: + import typing_extensions as te + from .core import Argument as CoreArgument + from .core import Context + from .core import Option as CoreOption + from .core import Parameter as CoreParameter + +V = t.TypeVar("V") + +# Sentinel value that indicates an option was passed as a flag without a +# value but is not a flag option. Option.consume_value uses this to +# prompt or use the flag_value. +_flag_needs_value = object() + + +def _unpack_args( + args: t.Sequence[str], nargs_spec: t.Sequence[int] +) -> t.Tuple[t.Sequence[t.Union[str, t.Sequence[t.Optional[str]], None]], t.List[str]]: + """Given an iterable of arguments and an iterable of nargs specifications, + it returns a tuple with all the unpacked arguments at the first index + and all remaining arguments as the second. + + The nargs specification is the number of arguments that should be consumed + or `-1` to indicate that this position should eat up all the remainders. + + Missing items are filled with `None`. + """ + args = deque(args) + nargs_spec = deque(nargs_spec) + rv: t.List[t.Union[str, t.Tuple[t.Optional[str], ...], None]] = [] + spos: t.Optional[int] = None + + def _fetch(c: "te.Deque[V]") -> t.Optional[V]: + try: + if spos is None: + return c.popleft() + else: + return c.pop() + except IndexError: + return None + + while nargs_spec: + nargs = _fetch(nargs_spec) + + if nargs is None: + continue + + if nargs == 1: + rv.append(_fetch(args)) + elif nargs > 1: + x = [_fetch(args) for _ in range(nargs)] + + # If we're reversed, we're pulling in the arguments in reverse, + # so we need to turn them around. + if spos is not None: + x.reverse() + + rv.append(tuple(x)) + elif nargs < 0: + if spos is not None: + raise TypeError("Cannot have two nargs < 0") + + spos = len(rv) + rv.append(None) + + # spos is the position of the wildcard (star). If it's not `None`, + # we fill it with the remainder. + if spos is not None: + rv[spos] = tuple(args) + args = [] + rv[spos + 1 :] = reversed(rv[spos + 1 :]) + + return tuple(rv), list(args) + + +def split_opt(opt: str) -> t.Tuple[str, str]: + first = opt[:1] + if first.isalnum(): + return "", opt + if opt[1:2] == first: + return opt[:2], opt[2:] + return first, opt[1:] + + +def normalize_opt(opt: str, ctx: t.Optional["Context"]) -> str: + if ctx is None or ctx.token_normalize_func is None: + return opt + prefix, opt = split_opt(opt) + return f"{prefix}{ctx.token_normalize_func(opt)}" + + +def split_arg_string(string: str) -> t.List[str]: + """Split an argument string as with :func:`shlex.split`, but don't + fail if the string is incomplete. Ignores a missing closing quote or + incomplete escape sequence and uses the partial token as-is. + + .. code-block:: python + + split_arg_string("example 'my file") + ["example", "my file"] + + split_arg_string("example my\\") + ["example", "my"] + + :param string: String to split. + """ + import shlex + + lex = shlex.shlex(string, posix=True) + lex.whitespace_split = True + lex.commenters = "" + out = [] + + try: + for token in lex: + out.append(token) + except ValueError: + # Raised when end-of-string is reached in an invalid state. Use + # the partial token as-is. The quote or escape character is in + # lex.state, not lex.token. + out.append(lex.token) + + return out + + +class Option: + def __init__( + self, + obj: "CoreOption", + opts: t.Sequence[str], + dest: t.Optional[str], + action: t.Optional[str] = None, + nargs: int = 1, + const: t.Optional[t.Any] = None, + ): + self._short_opts = [] + self._long_opts = [] + self.prefixes: t.Set[str] = set() + + for opt in opts: + prefix, value = split_opt(opt) + if not prefix: + raise ValueError(f"Invalid start character for option ({opt})") + self.prefixes.add(prefix[0]) + if len(prefix) == 1 and len(value) == 1: + self._short_opts.append(opt) + else: + self._long_opts.append(opt) + self.prefixes.add(prefix) + + if action is None: + action = "store" + + self.dest = dest + self.action = action + self.nargs = nargs + self.const = const + self.obj = obj + + @property + def takes_value(self) -> bool: + return self.action in ("store", "append") + + def process(self, value: t.Any, state: "ParsingState") -> None: + if self.action == "store": + state.opts[self.dest] = value # type: ignore + elif self.action == "store_const": + state.opts[self.dest] = self.const # type: ignore + elif self.action == "append": + state.opts.setdefault(self.dest, []).append(value) # type: ignore + elif self.action == "append_const": + state.opts.setdefault(self.dest, []).append(self.const) # type: ignore + elif self.action == "count": + state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore + else: + raise ValueError(f"unknown action '{self.action}'") + state.order.append(self.obj) + + +class Argument: + def __init__(self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1): + self.dest = dest + self.nargs = nargs + self.obj = obj + + def process( + self, + value: t.Union[t.Optional[str], t.Sequence[t.Optional[str]]], + state: "ParsingState", + ) -> None: + if self.nargs > 1: + assert value is not None + holes = sum(1 for x in value if x is None) + if holes == len(value): + value = None + elif holes != 0: + raise BadArgumentUsage( + _("Argument {name!r} takes {nargs} values.").format( + name=self.dest, nargs=self.nargs + ) + ) + + if self.nargs == -1 and self.obj.envvar is not None and value == (): + # Replace empty tuple with None so that a value from the + # environment may be tried. + value = None + + state.opts[self.dest] = value # type: ignore + state.order.append(self.obj) + + +class ParsingState: + def __init__(self, rargs: t.List[str]) -> None: + self.opts: t.Dict[str, t.Any] = {} + self.largs: t.List[str] = [] + self.rargs = rargs + self.order: t.List["CoreParameter"] = [] + + +class OptionParser: + """The option parser is an internal class that is ultimately used to + parse options and arguments. It's modelled after optparse and brings + a similar but vastly simplified API. It should generally not be used + directly as the high level Click classes wrap it for you. + + It's not nearly as extensible as optparse or argparse as it does not + implement features that are implemented on a higher level (such as + types or defaults). + + :param ctx: optionally the :class:`~click.Context` where this parser + should go with. + """ + + def __init__(self, ctx: t.Optional["Context"] = None) -> None: + #: The :class:`~click.Context` for this parser. This might be + #: `None` for some advanced use cases. + self.ctx = ctx + #: This controls how the parser deals with interspersed arguments. + #: If this is set to `False`, the parser will stop on the first + #: non-option. Click uses this to implement nested subcommands + #: safely. + self.allow_interspersed_args: bool = True + #: This tells the parser how to deal with unknown options. By + #: default it will error out (which is sensible), but there is a + #: second mode where it will ignore it and continue processing + #: after shifting all the unknown options into the resulting args. + self.ignore_unknown_options: bool = False + + if ctx is not None: + self.allow_interspersed_args = ctx.allow_interspersed_args + self.ignore_unknown_options = ctx.ignore_unknown_options + + self._short_opt: t.Dict[str, Option] = {} + self._long_opt: t.Dict[str, Option] = {} + self._opt_prefixes = {"-", "--"} + self._args: t.List[Argument] = [] + + def add_option( + self, + obj: "CoreOption", + opts: t.Sequence[str], + dest: t.Optional[str], + action: t.Optional[str] = None, + nargs: int = 1, + const: t.Optional[t.Any] = None, + ) -> None: + """Adds a new option named `dest` to the parser. The destination + is not inferred (unlike with optparse) and needs to be explicitly + provided. Action can be any of ``store``, ``store_const``, + ``append``, ``append_const`` or ``count``. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + opts = [normalize_opt(opt, self.ctx) for opt in opts] + option = Option(obj, opts, dest, action=action, nargs=nargs, const=const) + self._opt_prefixes.update(option.prefixes) + for opt in option._short_opts: + self._short_opt[opt] = option + for opt in option._long_opts: + self._long_opt[opt] = option + + def add_argument( + self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1 + ) -> None: + """Adds a positional argument named `dest` to the parser. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + self._args.append(Argument(obj, dest=dest, nargs=nargs)) + + def parse_args( + self, args: t.List[str] + ) -> t.Tuple[t.Dict[str, t.Any], t.List[str], t.List["CoreParameter"]]: + """Parses positional arguments and returns ``(values, args, order)`` + for the parsed options and arguments as well as the leftover + arguments if there are any. The order is a list of objects as they + appear on the command line. If arguments appear multiple times they + will be memorized multiple times as well. + """ + state = ParsingState(args) + try: + self._process_args_for_options(state) + self._process_args_for_args(state) + except UsageError: + if self.ctx is None or not self.ctx.resilient_parsing: + raise + return state.opts, state.largs, state.order + + def _process_args_for_args(self, state: ParsingState) -> None: + pargs, args = _unpack_args( + state.largs + state.rargs, [x.nargs for x in self._args] + ) + + for idx, arg in enumerate(self._args): + arg.process(pargs[idx], state) + + state.largs = args + state.rargs = [] + + def _process_args_for_options(self, state: ParsingState) -> None: + while state.rargs: + arg = state.rargs.pop(0) + arglen = len(arg) + # Double dashes always handled explicitly regardless of what + # prefixes are valid. + if arg == "--": + return + elif arg[:1] in self._opt_prefixes and arglen > 1: + self._process_opts(arg, state) + elif self.allow_interspersed_args: + state.largs.append(arg) + else: + state.rargs.insert(0, arg) + return + + # Say this is the original argument list: + # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)] + # ^ + # (we are about to process arg(i)). + # + # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of + # [arg0, ..., arg(i-1)] (any options and their arguments will have + # been removed from largs). + # + # The while loop will usually consume 1 or more arguments per pass. + # If it consumes 1 (eg. arg is an option that takes no arguments), + # then after _process_arg() is done the situation is: + # + # largs = subset of [arg0, ..., arg(i)] + # rargs = [arg(i+1), ..., arg(N-1)] + # + # If allow_interspersed_args is false, largs will always be + # *empty* -- still a subset of [arg0, ..., arg(i-1)], but + # not a very interesting subset! + + def _match_long_opt( + self, opt: str, explicit_value: t.Optional[str], state: ParsingState + ) -> None: + if opt not in self._long_opt: + from difflib import get_close_matches + + possibilities = get_close_matches(opt, self._long_opt) + raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) + + option = self._long_opt[opt] + if option.takes_value: + # At this point it's safe to modify rargs by injecting the + # explicit value, because no exception is raised in this + # branch. This means that the inserted value will be fully + # consumed. + if explicit_value is not None: + state.rargs.insert(0, explicit_value) + + value = self._get_value_from_state(opt, option, state) + + elif explicit_value is not None: + raise BadOptionUsage( + opt, _("Option {name!r} does not take a value.").format(name=opt) + ) + + else: + value = None + + option.process(value, state) + + def _match_short_opt(self, arg: str, state: ParsingState) -> None: + stop = False + i = 1 + prefix = arg[0] + unknown_options = [] + + for ch in arg[1:]: + opt = normalize_opt(f"{prefix}{ch}", self.ctx) + option = self._short_opt.get(opt) + i += 1 + + if not option: + if self.ignore_unknown_options: + unknown_options.append(ch) + continue + raise NoSuchOption(opt, ctx=self.ctx) + if option.takes_value: + # Any characters left in arg? Pretend they're the + # next arg, and stop consuming characters of arg. + if i < len(arg): + state.rargs.insert(0, arg[i:]) + stop = True + + value = self._get_value_from_state(opt, option, state) + + else: + value = None + + option.process(value, state) + + if stop: + break + + # If we got any unknown options we recombine the string of the + # remaining options and re-attach the prefix, then report that + # to the state as new larg. This way there is basic combinatorics + # that can be achieved while still ignoring unknown arguments. + if self.ignore_unknown_options and unknown_options: + state.largs.append(f"{prefix}{''.join(unknown_options)}") + + def _get_value_from_state( + self, option_name: str, option: Option, state: ParsingState + ) -> t.Any: + nargs = option.nargs + + if len(state.rargs) < nargs: + if option.obj._flag_needs_value: + # Option allows omitting the value. + value = _flag_needs_value + else: + raise BadOptionUsage( + option_name, + ngettext( + "Option {name!r} requires an argument.", + "Option {name!r} requires {nargs} arguments.", + nargs, + ).format(name=option_name, nargs=nargs), + ) + elif nargs == 1: + next_rarg = state.rargs[0] + + if ( + option.obj._flag_needs_value + and isinstance(next_rarg, str) + and next_rarg[:1] in self._opt_prefixes + and len(next_rarg) > 1 + ): + # The next arg looks like the start of an option, don't + # use it as the value if omitting the value is allowed. + value = _flag_needs_value + else: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + return value + + def _process_opts(self, arg: str, state: ParsingState) -> None: + explicit_value = None + # Long option handling happens in two parts. The first part is + # supporting explicitly attached values. In any case, we will try + # to long match the option first. + if "=" in arg: + long_opt, explicit_value = arg.split("=", 1) + else: + long_opt = arg + norm_long_opt = normalize_opt(long_opt, self.ctx) + + # At this point we will match the (assumed) long option through + # the long option matching code. Note that this allows options + # like "-foo" to be matched as long options. + try: + self._match_long_opt(norm_long_opt, explicit_value, state) + except NoSuchOption: + # At this point the long option matching failed, and we need + # to try with short options. However there is a special rule + # which says, that if we have a two character options prefix + # (applies to "--foo" for instance), we do not dispatch to the + # short option code and will instead raise the no option + # error. + if arg[:2] not in self._opt_prefixes: + self._match_short_opt(arg, state) + return + + if not self.ignore_unknown_options: + raise + + state.largs.append(arg) diff --git a/pkg/python/embed/python-linux-amd64.dummy b/lib/go-jinja2/internal/data/darwin-amd64/click/py.typed similarity index 100% rename from pkg/python/embed/python-linux-amd64.dummy rename to lib/go-jinja2/internal/data/darwin-amd64/click/py.typed diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/shell_completion.py b/lib/go-jinja2/internal/data/darwin-amd64/click/shell_completion.py new file mode 100644 index 000000000..dc9e00b9b --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/shell_completion.py @@ -0,0 +1,596 @@ +import os +import re +import typing as t +from gettext import gettext as _ + +from .core import Argument +from .core import BaseCommand +from .core import Context +from .core import MultiCommand +from .core import Option +from .core import Parameter +from .core import ParameterSource +from .parser import split_arg_string +from .utils import echo + + +def shell_complete( + cli: BaseCommand, + ctx_args: t.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str, + instruction: str, +) -> int: + """Perform shell completion for the given CLI program. + + :param cli: Command being called. + :param ctx_args: Extra arguments to pass to + ``cli.make_context``. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + :param instruction: Value of ``complete_var`` with the completion + instruction and shell, in the form ``instruction_shell``. + :return: Status code to exit with. + """ + shell, _, instruction = instruction.partition("_") + comp_cls = get_completion_class(shell) + + if comp_cls is None: + return 1 + + comp = comp_cls(cli, ctx_args, prog_name, complete_var) + + if instruction == "source": + echo(comp.source()) + return 0 + + if instruction == "complete": + echo(comp.complete()) + return 0 + + return 1 + + +class CompletionItem: + """Represents a completion value and metadata about the value. The + default metadata is ``type`` to indicate special shell handling, + and ``help`` if a shell supports showing a help string next to the + value. + + Arbitrary parameters can be passed when creating the object, and + accessed using ``item.attr``. If an attribute wasn't passed, + accessing it returns ``None``. + + :param value: The completion suggestion. + :param type: Tells the shell script to provide special completion + support for the type. Click uses ``"dir"`` and ``"file"``. + :param help: String shown next to the value if supported. + :param kwargs: Arbitrary metadata. The built-in implementations + don't use this, but custom type completions paired with custom + shell support could use it. + """ + + __slots__ = ("value", "type", "help", "_info") + + def __init__( + self, + value: t.Any, + type: str = "plain", + help: t.Optional[str] = None, + **kwargs: t.Any, + ) -> None: + self.value: t.Any = value + self.type: str = type + self.help: t.Optional[str] = help + self._info = kwargs + + def __getattr__(self, name: str) -> t.Any: + return self._info.get(name) + + +# Only Bash >= 4.4 has the nosort option. +_SOURCE_BASH = """\ +%(complete_func)s() { + local IFS=$'\\n' + local response + + response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \ +%(complete_var)s=bash_complete $1) + + for completion in $response; do + IFS=',' read type value <<< "$completion" + + if [[ $type == 'dir' ]]; then + COMPREPLY=() + compopt -o dirnames + elif [[ $type == 'file' ]]; then + COMPREPLY=() + compopt -o default + elif [[ $type == 'plain' ]]; then + COMPREPLY+=($value) + fi + done + + return 0 +} + +%(complete_func)s_setup() { + complete -o nosort -F %(complete_func)s %(prog_name)s +} + +%(complete_func)s_setup; +""" + +_SOURCE_ZSH = """\ +#compdef %(prog_name)s + +%(complete_func)s() { + local -a completions + local -a completions_with_descriptions + local -a response + (( ! $+commands[%(prog_name)s] )) && return 1 + + response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \ +%(complete_var)s=zsh_complete %(prog_name)s)}") + + for type key descr in ${response}; do + if [[ "$type" == "plain" ]]; then + if [[ "$descr" == "_" ]]; then + completions+=("$key") + else + completions_with_descriptions+=("$key":"$descr") + fi + elif [[ "$type" == "dir" ]]; then + _path_files -/ + elif [[ "$type" == "file" ]]; then + _path_files -f + fi + done + + if [ -n "$completions_with_descriptions" ]; then + _describe -V unsorted completions_with_descriptions -U + fi + + if [ -n "$completions" ]; then + compadd -U -V unsorted -a completions + fi +} + +if [[ $zsh_eval_context[-1] == loadautofunc ]]; then + # autoload from fpath, call function directly + %(complete_func)s "$@" +else + # eval/source/. command, register function for later + compdef %(complete_func)s %(prog_name)s +fi +""" + +_SOURCE_FISH = """\ +function %(complete_func)s; + set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \ +COMP_CWORD=(commandline -t) %(prog_name)s); + + for completion in $response; + set -l metadata (string split "," $completion); + + if test $metadata[1] = "dir"; + __fish_complete_directories $metadata[2]; + else if test $metadata[1] = "file"; + __fish_complete_path $metadata[2]; + else if test $metadata[1] = "plain"; + echo $metadata[2]; + end; + end; +end; + +complete --no-files --command %(prog_name)s --arguments \ +"(%(complete_func)s)"; +""" + + +class ShellComplete: + """Base class for providing shell completion support. A subclass for + a given shell will override attributes and methods to implement the + completion instructions (``source`` and ``complete``). + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + + .. versionadded:: 8.0 + """ + + name: t.ClassVar[str] + """Name to register the shell as with :func:`add_completion_class`. + This is used in completion instructions (``{name}_source`` and + ``{name}_complete``). + """ + + source_template: t.ClassVar[str] + """Completion script template formatted by :meth:`source`. This must + be provided by subclasses. + """ + + def __init__( + self, + cli: BaseCommand, + ctx_args: t.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str, + ) -> None: + self.cli = cli + self.ctx_args = ctx_args + self.prog_name = prog_name + self.complete_var = complete_var + + @property + def func_name(self) -> str: + """The name of the shell function defined by the completion + script. + """ + safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), flags=re.ASCII) + return f"_{safe_name}_completion" + + def source_vars(self) -> t.Dict[str, t.Any]: + """Vars for formatting :attr:`source_template`. + + By default this provides ``complete_func``, ``complete_var``, + and ``prog_name``. + """ + return { + "complete_func": self.func_name, + "complete_var": self.complete_var, + "prog_name": self.prog_name, + } + + def source(self) -> str: + """Produce the shell script that defines the completion + function. By default this ``%``-style formats + :attr:`source_template` with the dict returned by + :meth:`source_vars`. + """ + return self.source_template % self.source_vars() + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + """Use the env vars defined by the shell script to return a + tuple of ``args, incomplete``. This must be implemented by + subclasses. + """ + raise NotImplementedError + + def get_completions( + self, args: t.List[str], incomplete: str + ) -> t.List[CompletionItem]: + """Determine the context and last complete command or parameter + from the complete args. Call that object's ``shell_complete`` + method to get the completions for the incomplete value. + + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args) + obj, incomplete = _resolve_incomplete(ctx, args, incomplete) + return obj.shell_complete(ctx, incomplete) + + def format_completion(self, item: CompletionItem) -> str: + """Format a completion item into the form recognized by the + shell script. This must be implemented by subclasses. + + :param item: Completion item to format. + """ + raise NotImplementedError + + def complete(self) -> str: + """Produce the completion data to send back to the shell. + + By default this calls :meth:`get_completion_args`, gets the + completions, then calls :meth:`format_completion` for each + completion. + """ + args, incomplete = self.get_completion_args() + completions = self.get_completions(args, incomplete) + out = [self.format_completion(item) for item in completions] + return "\n".join(out) + + +class BashComplete(ShellComplete): + """Shell completion for Bash.""" + + name = "bash" + source_template = _SOURCE_BASH + + @staticmethod + def _check_version() -> None: + import subprocess + + output = subprocess.run( + ["bash", "-c", 'echo "${BASH_VERSION}"'], stdout=subprocess.PIPE + ) + match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode()) + + if match is not None: + major, minor = match.groups() + + if major < "4" or major == "4" and minor < "4": + echo( + _( + "Shell completion is not supported for Bash" + " versions older than 4.4." + ), + err=True, + ) + else: + echo( + _("Couldn't detect Bash version, shell completion is not supported."), + err=True, + ) + + def source(self) -> str: + self._check_version() + return super().source() + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + return f"{item.type},{item.value}" + + +class ZshComplete(ShellComplete): + """Shell completion for Zsh.""" + + name = "zsh" + source_template = _SOURCE_ZSH + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}" + + +class FishComplete(ShellComplete): + """Shell completion for Fish.""" + + name = "fish" + source_template = _SOURCE_FISH + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + incomplete = os.environ["COMP_CWORD"] + args = cwords[1:] + + # Fish stores the partial word in both COMP_WORDS and + # COMP_CWORD, remove it from complete args. + if incomplete and args and args[-1] == incomplete: + args.pop() + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + if item.help: + return f"{item.type},{item.value}\t{item.help}" + + return f"{item.type},{item.value}" + + +ShellCompleteType = t.TypeVar("ShellCompleteType", bound=t.Type[ShellComplete]) + + +_available_shells: t.Dict[str, t.Type[ShellComplete]] = { + "bash": BashComplete, + "fish": FishComplete, + "zsh": ZshComplete, +} + + +def add_completion_class( + cls: ShellCompleteType, name: t.Optional[str] = None +) -> ShellCompleteType: + """Register a :class:`ShellComplete` subclass under the given name. + The name will be provided by the completion instruction environment + variable during completion. + + :param cls: The completion class that will handle completion for the + shell. + :param name: Name to register the class under. Defaults to the + class's ``name`` attribute. + """ + if name is None: + name = cls.name + + _available_shells[name] = cls + + return cls + + +def get_completion_class(shell: str) -> t.Optional[t.Type[ShellComplete]]: + """Look up a registered :class:`ShellComplete` subclass by the name + provided by the completion instruction environment variable. If the + name isn't registered, returns ``None``. + + :param shell: Name the class is registered under. + """ + return _available_shells.get(shell) + + +def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool: + """Determine if the given parameter is an argument that can still + accept values. + + :param ctx: Invocation context for the command represented by the + parsed complete args. + :param param: Argument object being checked. + """ + if not isinstance(param, Argument): + return False + + assert param.name is not None + # Will be None if expose_value is False. + value = ctx.params.get(param.name) + return ( + param.nargs == -1 + or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE + or ( + param.nargs > 1 + and isinstance(value, (tuple, list)) + and len(value) < param.nargs + ) + ) + + +def _start_of_option(ctx: Context, value: str) -> bool: + """Check if the value looks like the start of an option.""" + if not value: + return False + + c = value[0] + return c in ctx._opt_prefixes + + +def _is_incomplete_option(ctx: Context, args: t.List[str], param: Parameter) -> bool: + """Determine if the given parameter is an option that needs a value. + + :param args: List of complete args before the incomplete value. + :param param: Option object being checked. + """ + if not isinstance(param, Option): + return False + + if param.is_flag or param.count: + return False + + last_option = None + + for index, arg in enumerate(reversed(args)): + if index + 1 > param.nargs: + break + + if _start_of_option(ctx, arg): + last_option = arg + + return last_option is not None and last_option in param.opts + + +def _resolve_context( + cli: BaseCommand, + ctx_args: t.MutableMapping[str, t.Any], + prog_name: str, + args: t.List[str], +) -> Context: + """Produce the context hierarchy starting with the command and + traversing the complete arguments. This only follows the commands, + it doesn't trigger input prompts or callbacks. + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param args: List of complete args before the incomplete value. + """ + ctx_args["resilient_parsing"] = True + ctx = cli.make_context(prog_name, args.copy(), **ctx_args) + args = ctx.protected_args + ctx.args + + while args: + command = ctx.command + + if isinstance(command, MultiCommand): + if not command.chain: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True) + args = ctx.protected_args + ctx.args + else: + sub_ctx = ctx + + while args: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + sub_ctx = cmd.make_context( + name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + resilient_parsing=True, + ) + args = sub_ctx.args + + ctx = sub_ctx + args = [*sub_ctx.protected_args, *sub_ctx.args] + else: + break + + return ctx + + +def _resolve_incomplete( + ctx: Context, args: t.List[str], incomplete: str +) -> t.Tuple[t.Union[BaseCommand, Parameter], str]: + """Find the Click object that will handle the completion of the + incomplete value. Return the object and the incomplete value. + + :param ctx: Invocation context for the command represented by + the parsed complete args. + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + # Different shells treat an "=" between a long option name and + # value differently. Might keep the value joined, return the "=" + # as a separate item, or return the split name and value. Always + # split and discard the "=" to make completion easier. + if incomplete == "=": + incomplete = "" + elif "=" in incomplete and _start_of_option(ctx, incomplete): + name, _, incomplete = incomplete.partition("=") + args.append(name) + + # The "--" marker tells Click to stop treating values as options + # even if they start with the option character. If it hasn't been + # given and the incomplete arg looks like an option, the current + # command will provide option name completions. + if "--" not in args and _start_of_option(ctx, incomplete): + return ctx.command, incomplete + + params = ctx.command.get_params(ctx) + + # If the last complete arg is an option name with an incomplete + # value, the option will provide value completions. + for param in params: + if _is_incomplete_option(ctx, args, param): + return param, incomplete + + # It's not an option name or value. The first argument without a + # parsed value will provide value completions. + for param in params: + if _is_incomplete_argument(ctx, param): + return param, incomplete + + # There were no unparsed arguments, the command may be a group that + # will provide command name completions. + return ctx.command, incomplete diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/termui.py b/lib/go-jinja2/internal/data/darwin-amd64/click/termui.py new file mode 100644 index 000000000..db7a4b286 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/termui.py @@ -0,0 +1,784 @@ +import inspect +import io +import itertools +import sys +import typing as t +from gettext import gettext as _ + +from ._compat import isatty +from ._compat import strip_ansi +from .exceptions import Abort +from .exceptions import UsageError +from .globals import resolve_color_default +from .types import Choice +from .types import convert_type +from .types import ParamType +from .utils import echo +from .utils import LazyFile + +if t.TYPE_CHECKING: + from ._termui_impl import ProgressBar + +V = t.TypeVar("V") + +# The prompt functions to use. The doc tools currently override these +# functions to customize how they work. +visible_prompt_func: t.Callable[[str], str] = input + +_ansi_colors = { + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, + "reset": 39, + "bright_black": 90, + "bright_red": 91, + "bright_green": 92, + "bright_yellow": 93, + "bright_blue": 94, + "bright_magenta": 95, + "bright_cyan": 96, + "bright_white": 97, +} +_ansi_reset_all = "\033[0m" + + +def hidden_prompt_func(prompt: str) -> str: + import getpass + + return getpass.getpass(prompt) + + +def _build_prompt( + text: str, + suffix: str, + show_default: bool = False, + default: t.Optional[t.Any] = None, + show_choices: bool = True, + type: t.Optional[ParamType] = None, +) -> str: + prompt = text + if type is not None and show_choices and isinstance(type, Choice): + prompt += f" ({', '.join(map(str, type.choices))})" + if default is not None and show_default: + prompt = f"{prompt} [{_format_default(default)}]" + return f"{prompt}{suffix}" + + +def _format_default(default: t.Any) -> t.Any: + if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"): + return default.name + + return default + + +def prompt( + text: str, + default: t.Optional[t.Any] = None, + hide_input: bool = False, + confirmation_prompt: t.Union[bool, str] = False, + type: t.Optional[t.Union[ParamType, t.Any]] = None, + value_proc: t.Optional[t.Callable[[str], t.Any]] = None, + prompt_suffix: str = ": ", + show_default: bool = True, + err: bool = False, + show_choices: bool = True, +) -> t.Any: + """Prompts a user for input. This is a convenience function that can + be used to prompt a user for input later. + + If the user aborts the input by sending an interrupt signal, this + function will catch it and raise a :exc:`Abort` exception. + + :param text: the text to show for the prompt. + :param default: the default value to use if no input happens. If this + is not given it will prompt until it's aborted. + :param hide_input: if this is set to true then the input value will + be hidden. + :param confirmation_prompt: Prompt a second time to confirm the + value. Can be set to a string instead of ``True`` to customize + the message. + :param type: the type to use to check the value against. + :param value_proc: if this parameter is provided it's a function that + is invoked instead of the type conversion to + convert a value. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + :param show_choices: Show or hide choices if the passed type is a Choice. + For example if type is a Choice of either day or week, + show_choices is true and text is "Group by" then the + prompt will be "Group by (day, week): ". + + .. versionadded:: 8.0 + ``confirmation_prompt`` can be a custom string. + + .. versionadded:: 7.0 + Added the ``show_choices`` parameter. + + .. versionadded:: 6.0 + Added unicode support for cmd.exe on Windows. + + .. versionadded:: 4.0 + Added the `err` parameter. + + """ + + def prompt_func(text: str) -> str: + f = hidden_prompt_func if hide_input else visible_prompt_func + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(text.rstrip(" "), nl=False, err=err) + # Echo a space to stdout to work around an issue where + # readline causes backspace to clear the whole line. + return f(" ") + except (KeyboardInterrupt, EOFError): + # getpass doesn't print a newline if the user aborts input with ^C. + # Allegedly this behavior is inherited from getpass(3). + # A doc bug has been filed at https://bugs.python.org/issue24711 + if hide_input: + echo(None, err=err) + raise Abort() from None + + if value_proc is None: + value_proc = convert_type(type, default) + + prompt = _build_prompt( + text, prompt_suffix, show_default, default, show_choices, type + ) + + if confirmation_prompt: + if confirmation_prompt is True: + confirmation_prompt = _("Repeat for confirmation") + + confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix) + + while True: + while True: + value = prompt_func(prompt) + if value: + break + elif default is not None: + value = default + break + try: + result = value_proc(value) + except UsageError as e: + if hide_input: + echo(_("Error: The value you entered was invalid."), err=err) + else: + echo(_("Error: {e.message}").format(e=e), err=err) # noqa: B306 + continue + if not confirmation_prompt: + return result + while True: + value2 = prompt_func(confirmation_prompt) + is_empty = not value and not value2 + if value2 or is_empty: + break + if value == value2: + return result + echo(_("Error: The two entered values do not match."), err=err) + + +def confirm( + text: str, + default: t.Optional[bool] = False, + abort: bool = False, + prompt_suffix: str = ": ", + show_default: bool = True, + err: bool = False, +) -> bool: + """Prompts for confirmation (yes/no question). + + If the user aborts the input by sending a interrupt signal this + function will catch it and raise a :exc:`Abort` exception. + + :param text: the question to ask. + :param default: The default value to use when no input is given. If + ``None``, repeat until input is given. + :param abort: if this is set to `True` a negative answer aborts the + exception by raising :exc:`Abort`. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + + .. versionchanged:: 8.0 + Repeat until input is given if ``default`` is ``None``. + + .. versionadded:: 4.0 + Added the ``err`` parameter. + """ + prompt = _build_prompt( + text, + prompt_suffix, + show_default, + "y/n" if default is None else ("Y/n" if default else "y/N"), + ) + + while True: + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(prompt.rstrip(" "), nl=False, err=err) + # Echo a space to stdout to work around an issue where + # readline causes backspace to clear the whole line. + value = visible_prompt_func(" ").lower().strip() + except (KeyboardInterrupt, EOFError): + raise Abort() from None + if value in ("y", "yes"): + rv = True + elif value in ("n", "no"): + rv = False + elif default is not None and value == "": + rv = default + else: + echo(_("Error: invalid input"), err=err) + continue + break + if abort and not rv: + raise Abort() + return rv + + +def echo_via_pager( + text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str], + color: t.Optional[bool] = None, +) -> None: + """This function takes a text and shows it via an environment specific + pager on stdout. + + .. versionchanged:: 3.0 + Added the `color` flag. + + :param text_or_generator: the text to page, or alternatively, a + generator emitting the text to page. + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + color = resolve_color_default(color) + + if inspect.isgeneratorfunction(text_or_generator): + i = t.cast(t.Callable[[], t.Iterable[str]], text_or_generator)() + elif isinstance(text_or_generator, str): + i = [text_or_generator] + else: + i = iter(t.cast(t.Iterable[str], text_or_generator)) + + # convert every element of i to a text type if necessary + text_generator = (el if isinstance(el, str) else str(el) for el in i) + + from ._termui_impl import pager + + return pager(itertools.chain(text_generator, "\n"), color) + + +def progressbar( + iterable: t.Optional[t.Iterable[V]] = None, + length: t.Optional[int] = None, + label: t.Optional[str] = None, + show_eta: bool = True, + show_percent: t.Optional[bool] = None, + show_pos: bool = False, + item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.Optional[t.TextIO] = None, + color: t.Optional[bool] = None, + update_min_steps: int = 1, +) -> "ProgressBar[V]": + """This function creates an iterable context manager that can be used + to iterate over something while showing a progress bar. It will + either iterate over the `iterable` or `length` items (that are counted + up). While iteration happens, this function will print a rendered + progress bar to the given `file` (defaults to stdout) and will attempt + to calculate remaining time and more. By default, this progress bar + will not be rendered if the file is not a terminal. + + The context manager creates the progress bar. When the context + manager is entered the progress bar is already created. With every + iteration over the progress bar, the iterable passed to the bar is + advanced and the bar is updated. When the context manager exits, + a newline is printed and the progress bar is finalized on screen. + + Note: The progress bar is currently designed for use cases where the + total progress can be expected to take at least several seconds. + Because of this, the ProgressBar class object won't display + progress that is considered too fast, and progress where the time + between steps is less than a second. + + No printing must happen or the progress bar will be unintentionally + destroyed. + + Example usage:: + + with progressbar(items) as bar: + for item in bar: + do_something_with(item) + + Alternatively, if no iterable is specified, one can manually update the + progress bar through the `update()` method instead of directly + iterating over the progress bar. The update method accepts the number + of steps to increment the bar with:: + + with progressbar(length=chunks.total_bytes) as bar: + for chunk in chunks: + process_chunk(chunk) + bar.update(chunks.bytes) + + The ``update()`` method also takes an optional value specifying the + ``current_item`` at the new position. This is useful when used + together with ``item_show_func`` to customize the output for each + manual step:: + + with click.progressbar( + length=total_size, + label='Unzipping archive', + item_show_func=lambda a: a.filename + ) as bar: + for archive in zip_file: + archive.extract() + bar.update(archive.size, archive) + + :param iterable: an iterable to iterate over. If not provided the length + is required. + :param length: the number of items to iterate over. By default the + progressbar will attempt to ask the iterator about its + length, which might or might not work. If an iterable is + also provided this parameter can be used to override the + length. If an iterable is not provided the progress bar + will iterate over a range of that length. + :param label: the label to show next to the progress bar. + :param show_eta: enables or disables the estimated time display. This is + automatically disabled if the length cannot be + determined. + :param show_percent: enables or disables the percentage display. The + default is `True` if the iterable has a length or + `False` if not. + :param show_pos: enables or disables the absolute position display. The + default is `False`. + :param item_show_func: A function called with the current item which + can return a string to show next to the progress bar. If the + function returns ``None`` nothing is shown. The current item can + be ``None``, such as when entering and exiting the bar. + :param fill_char: the character to use to show the filled part of the + progress bar. + :param empty_char: the character to use to show the non-filled part of + the progress bar. + :param bar_template: the format string to use as template for the bar. + The parameters in it are ``label`` for the label, + ``bar`` for the progress bar and ``info`` for the + info section. + :param info_sep: the separator between multiple info items (eta etc.) + :param width: the width of the progress bar in characters, 0 means full + terminal width + :param file: The file to write to. If this is not a terminal then + only the label is printed. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are included anywhere in the progress bar output + which is not the case by default. + :param update_min_steps: Render only when this many updates have + completed. This allows tuning for very fast iterators. + + .. versionchanged:: 8.0 + Output is shown even if execution time is less than 0.5 seconds. + + .. versionchanged:: 8.0 + ``item_show_func`` shows the current item, not the previous one. + + .. versionchanged:: 8.0 + Labels are echoed if the output is not a TTY. Reverts a change + in 7.0 that removed all output. + + .. versionadded:: 8.0 + Added the ``update_min_steps`` parameter. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. Added the ``update`` method to + the object. + + .. versionadded:: 2.0 + """ + from ._termui_impl import ProgressBar + + color = resolve_color_default(color) + return ProgressBar( + iterable=iterable, + length=length, + show_eta=show_eta, + show_percent=show_percent, + show_pos=show_pos, + item_show_func=item_show_func, + fill_char=fill_char, + empty_char=empty_char, + bar_template=bar_template, + info_sep=info_sep, + file=file, + label=label, + width=width, + color=color, + update_min_steps=update_min_steps, + ) + + +def clear() -> None: + """Clears the terminal screen. This will have the effect of clearing + the whole visible space of the terminal and moving the cursor to the + top left. This does not do anything if not connected to a terminal. + + .. versionadded:: 2.0 + """ + if not isatty(sys.stdout): + return + + # ANSI escape \033[2J clears the screen, \033[1;1H moves the cursor + echo("\033[2J\033[1;1H", nl=False) + + +def _interpret_color( + color: t.Union[int, t.Tuple[int, int, int], str], offset: int = 0 +) -> str: + if isinstance(color, int): + return f"{38 + offset};5;{color:d}" + + if isinstance(color, (tuple, list)): + r, g, b = color + return f"{38 + offset};2;{r:d};{g:d};{b:d}" + + return str(_ansi_colors[color] + offset) + + +def style( + text: t.Any, + fg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None, + bg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None, + bold: t.Optional[bool] = None, + dim: t.Optional[bool] = None, + underline: t.Optional[bool] = None, + overline: t.Optional[bool] = None, + italic: t.Optional[bool] = None, + blink: t.Optional[bool] = None, + reverse: t.Optional[bool] = None, + strikethrough: t.Optional[bool] = None, + reset: bool = True, +) -> str: + """Styles a text with ANSI styles and returns the new string. By + default the styling is self contained which means that at the end + of the string a reset code is issued. This can be prevented by + passing ``reset=False``. + + Examples:: + + click.echo(click.style('Hello World!', fg='green')) + click.echo(click.style('ATTENTION!', blink=True)) + click.echo(click.style('Some things', reverse=True, fg='cyan')) + click.echo(click.style('More colors', fg=(255, 12, 128), bg=117)) + + Supported color names: + + * ``black`` (might be a gray) + * ``red`` + * ``green`` + * ``yellow`` (might be an orange) + * ``blue`` + * ``magenta`` + * ``cyan`` + * ``white`` (might be light gray) + * ``bright_black`` + * ``bright_red`` + * ``bright_green`` + * ``bright_yellow`` + * ``bright_blue`` + * ``bright_magenta`` + * ``bright_cyan`` + * ``bright_white`` + * ``reset`` (reset the color code only) + + If the terminal supports it, color may also be specified as: + + - An integer in the interval [0, 255]. The terminal must support + 8-bit/256-color mode. + - An RGB tuple of three integers in [0, 255]. The terminal must + support 24-bit/true-color mode. + + See https://en.wikipedia.org/wiki/ANSI_color and + https://gist.github.com/XVilka/8346728 for more information. + + :param text: the string to style with ansi codes. + :param fg: if provided this will become the foreground color. + :param bg: if provided this will become the background color. + :param bold: if provided this will enable or disable bold mode. + :param dim: if provided this will enable or disable dim mode. This is + badly supported. + :param underline: if provided this will enable or disable underline. + :param overline: if provided this will enable or disable overline. + :param italic: if provided this will enable or disable italic. + :param blink: if provided this will enable or disable blinking. + :param reverse: if provided this will enable or disable inverse + rendering (foreground becomes background and the + other way round). + :param strikethrough: if provided this will enable or disable + striking through text. + :param reset: by default a reset-all code is added at the end of the + string which means that styles do not carry over. This + can be disabled to compose styles. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. + + .. versionchanged:: 8.0 + Added support for 256 and RGB color codes. + + .. versionchanged:: 8.0 + Added the ``strikethrough``, ``italic``, and ``overline`` + parameters. + + .. versionchanged:: 7.0 + Added support for bright colors. + + .. versionadded:: 2.0 + """ + if not isinstance(text, str): + text = str(text) + + bits = [] + + if fg: + try: + bits.append(f"\033[{_interpret_color(fg)}m") + except KeyError: + raise TypeError(f"Unknown color {fg!r}") from None + + if bg: + try: + bits.append(f"\033[{_interpret_color(bg, 10)}m") + except KeyError: + raise TypeError(f"Unknown color {bg!r}") from None + + if bold is not None: + bits.append(f"\033[{1 if bold else 22}m") + if dim is not None: + bits.append(f"\033[{2 if dim else 22}m") + if underline is not None: + bits.append(f"\033[{4 if underline else 24}m") + if overline is not None: + bits.append(f"\033[{53 if overline else 55}m") + if italic is not None: + bits.append(f"\033[{3 if italic else 23}m") + if blink is not None: + bits.append(f"\033[{5 if blink else 25}m") + if reverse is not None: + bits.append(f"\033[{7 if reverse else 27}m") + if strikethrough is not None: + bits.append(f"\033[{9 if strikethrough else 29}m") + bits.append(text) + if reset: + bits.append(_ansi_reset_all) + return "".join(bits) + + +def unstyle(text: str) -> str: + """Removes ANSI styling information from a string. Usually it's not + necessary to use this function as Click's echo function will + automatically remove styling if necessary. + + .. versionadded:: 2.0 + + :param text: the text to remove style information from. + """ + return strip_ansi(text) + + +def secho( + message: t.Optional[t.Any] = None, + file: t.Optional[t.IO[t.AnyStr]] = None, + nl: bool = True, + err: bool = False, + color: t.Optional[bool] = None, + **styles: t.Any, +) -> None: + """This function combines :func:`echo` and :func:`style` into one + call. As such the following two calls are the same:: + + click.secho('Hello World!', fg='green') + click.echo(click.style('Hello World!', fg='green')) + + All keyword arguments are forwarded to the underlying functions + depending on which one they go with. + + Non-string types will be converted to :class:`str`. However, + :class:`bytes` are passed directly to :meth:`echo` without applying + style. If you want to style bytes that represent text, call + :meth:`bytes.decode` first. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. Bytes are + passed through without style applied. + + .. versionadded:: 2.0 + """ + if message is not None and not isinstance(message, (bytes, bytearray)): + message = style(message, **styles) + + return echo(message, file=file, nl=nl, err=err, color=color) + + +def edit( + text: t.Optional[t.AnyStr] = None, + editor: t.Optional[str] = None, + env: t.Optional[t.Mapping[str, str]] = None, + require_save: bool = True, + extension: str = ".txt", + filename: t.Optional[str] = None, +) -> t.Optional[t.AnyStr]: + r"""Edits the given text in the defined editor. If an editor is given + (should be the full path to the executable but the regular operating + system search path is used for finding the executable) it overrides + the detected editor. Optionally, some environment variables can be + used. If the editor is closed without changes, `None` is returned. In + case a file is edited directly the return value is always `None` and + `require_save` and `extension` are ignored. + + If the editor cannot be opened a :exc:`UsageError` is raised. + + Note for Windows: to simplify cross-platform usage, the newlines are + automatically converted from POSIX to Windows and vice versa. As such, + the message here will have ``\n`` as newline markers. + + :param text: the text to edit. + :param editor: optionally the editor to use. Defaults to automatic + detection. + :param env: environment variables to forward to the editor. + :param require_save: if this is true, then not saving in the editor + will make the return value become `None`. + :param extension: the extension to tell the editor about. This defaults + to `.txt` but changing this might change syntax + highlighting. + :param filename: if provided it will edit this file instead of the + provided text contents. It will not use a temporary + file as an indirection in that case. + """ + from ._termui_impl import Editor + + ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension) + + if filename is None: + return ed.edit(text) + + ed.edit_file(filename) + return None + + +def launch(url: str, wait: bool = False, locate: bool = False) -> int: + """This function launches the given URL (or filename) in the default + viewer application for this file type. If this is an executable, it + might launch the executable in a new session. The return value is + the exit code of the launched application. Usually, ``0`` indicates + success. + + Examples:: + + click.launch('https://click.palletsprojects.com/') + click.launch('/my/downloaded/file', locate=True) + + .. versionadded:: 2.0 + + :param url: URL or filename of the thing to launch. + :param wait: Wait for the program to exit before returning. This + only works if the launched program blocks. In particular, + ``xdg-open`` on Linux does not block. + :param locate: if this is set to `True` then instead of launching the + application associated with the URL it will attempt to + launch a file manager with the file located. This + might have weird effects if the URL does not point to + the filesystem. + """ + from ._termui_impl import open_url + + return open_url(url, wait=wait, locate=locate) + + +# If this is provided, getchar() calls into this instead. This is used +# for unittesting purposes. +_getchar: t.Optional[t.Callable[[bool], str]] = None + + +def getchar(echo: bool = False) -> str: + """Fetches a single character from the terminal and returns it. This + will always return a unicode character and under certain rare + circumstances this might return more than one character. The + situations which more than one character is returned is when for + whatever reason multiple characters end up in the terminal buffer or + standard input was not actually a terminal. + + Note that this will always read from the terminal, even if something + is piped into the standard input. + + Note for Windows: in rare cases when typing non-ASCII characters, this + function might wait for a second character and then return both at once. + This is because certain Unicode characters look like special-key markers. + + .. versionadded:: 2.0 + + :param echo: if set to `True`, the character read will also show up on + the terminal. The default is to not show it. + """ + global _getchar + + if _getchar is None: + from ._termui_impl import getchar as f + + _getchar = f + + return _getchar(echo) + + +def raw_terminal() -> t.ContextManager[int]: + from ._termui_impl import raw_terminal as f + + return f() + + +def pause(info: t.Optional[str] = None, err: bool = False) -> None: + """This command stops execution and waits for the user to press any + key to continue. This is similar to the Windows batch "pause" + command. If the program is not run through a terminal, this command + will instead do nothing. + + .. versionadded:: 2.0 + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param info: The message to print before pausing. Defaults to + ``"Press any key to continue..."``. + :param err: if set to message goes to ``stderr`` instead of + ``stdout``, the same as with echo. + """ + if not isatty(sys.stdin) or not isatty(sys.stdout): + return + + if info is None: + info = _("Press any key to continue...") + + try: + if info: + echo(info, nl=False, err=err) + try: + getchar() + except (KeyboardInterrupt, EOFError): + pass + finally: + if info: + echo(err=err) diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/testing.py b/lib/go-jinja2/internal/data/darwin-amd64/click/testing.py new file mode 100644 index 000000000..e0df0d2a6 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/testing.py @@ -0,0 +1,479 @@ +import contextlib +import io +import os +import shlex +import shutil +import sys +import tempfile +import typing as t +from types import TracebackType + +from . import formatting +from . import termui +from . import utils +from ._compat import _find_binary_reader + +if t.TYPE_CHECKING: + from .core import BaseCommand + + +class EchoingStdin: + def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None: + self._input = input + self._output = output + self._paused = False + + def __getattr__(self, x: str) -> t.Any: + return getattr(self._input, x) + + def _echo(self, rv: bytes) -> bytes: + if not self._paused: + self._output.write(rv) + + return rv + + def read(self, n: int = -1) -> bytes: + return self._echo(self._input.read(n)) + + def read1(self, n: int = -1) -> bytes: + return self._echo(self._input.read1(n)) # type: ignore + + def readline(self, n: int = -1) -> bytes: + return self._echo(self._input.readline(n)) + + def readlines(self) -> t.List[bytes]: + return [self._echo(x) for x in self._input.readlines()] + + def __iter__(self) -> t.Iterator[bytes]: + return iter(self._echo(x) for x in self._input) + + def __repr__(self) -> str: + return repr(self._input) + + +@contextlib.contextmanager +def _pause_echo(stream: t.Optional[EchoingStdin]) -> t.Iterator[None]: + if stream is None: + yield + else: + stream._paused = True + yield + stream._paused = False + + +class _NamedTextIOWrapper(io.TextIOWrapper): + def __init__( + self, buffer: t.BinaryIO, name: str, mode: str, **kwargs: t.Any + ) -> None: + super().__init__(buffer, **kwargs) + self._name = name + self._mode = mode + + @property + def name(self) -> str: + return self._name + + @property + def mode(self) -> str: + return self._mode + + +def make_input_stream( + input: t.Optional[t.Union[str, bytes, t.IO[t.Any]]], charset: str +) -> t.BinaryIO: + # Is already an input stream. + if hasattr(input, "read"): + rv = _find_binary_reader(t.cast(t.IO[t.Any], input)) + + if rv is not None: + return rv + + raise TypeError("Could not find binary reader for input stream.") + + if input is None: + input = b"" + elif isinstance(input, str): + input = input.encode(charset) + + return io.BytesIO(input) + + +class Result: + """Holds the captured result of an invoked CLI script.""" + + def __init__( + self, + runner: "CliRunner", + stdout_bytes: bytes, + stderr_bytes: t.Optional[bytes], + return_value: t.Any, + exit_code: int, + exception: t.Optional[BaseException], + exc_info: t.Optional[ + t.Tuple[t.Type[BaseException], BaseException, TracebackType] + ] = None, + ): + #: The runner that created the result + self.runner = runner + #: The standard output as bytes. + self.stdout_bytes = stdout_bytes + #: The standard error as bytes, or None if not available + self.stderr_bytes = stderr_bytes + #: The value returned from the invoked command. + #: + #: .. versionadded:: 8.0 + self.return_value = return_value + #: The exit code as integer. + self.exit_code = exit_code + #: The exception that happened if one did. + self.exception = exception + #: The traceback + self.exc_info = exc_info + + @property + def output(self) -> str: + """The (standard) output as unicode string.""" + return self.stdout + + @property + def stdout(self) -> str: + """The standard output as unicode string.""" + return self.stdout_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + @property + def stderr(self) -> str: + """The standard error as unicode string.""" + if self.stderr_bytes is None: + raise ValueError("stderr not separately captured") + return self.stderr_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + def __repr__(self) -> str: + exc_str = repr(self.exception) if self.exception else "okay" + return f"<{type(self).__name__} {exc_str}>" + + +class CliRunner: + """The CLI runner provides functionality to invoke a Click command line + script for unittesting purposes in a isolated environment. This only + works in single-threaded systems without any concurrency as it changes the + global interpreter state. + + :param charset: the character set for the input and output data. + :param env: a dictionary with environment variables for overriding. + :param echo_stdin: if this is set to `True`, then reading from stdin writes + to stdout. This is useful for showing examples in + some circumstances. Note that regular prompts + will automatically echo the input. + :param mix_stderr: if this is set to `False`, then stdout and stderr are + preserved as independent streams. This is useful for + Unix-philosophy apps that have predictable stdout and + noisy stderr, such that each may be measured + independently + """ + + def __init__( + self, + charset: str = "utf-8", + env: t.Optional[t.Mapping[str, t.Optional[str]]] = None, + echo_stdin: bool = False, + mix_stderr: bool = True, + ) -> None: + self.charset = charset + self.env: t.Mapping[str, t.Optional[str]] = env or {} + self.echo_stdin = echo_stdin + self.mix_stderr = mix_stderr + + def get_default_prog_name(self, cli: "BaseCommand") -> str: + """Given a command object it will return the default program name + for it. The default is the `name` attribute or ``"root"`` if not + set. + """ + return cli.name or "root" + + def make_env( + self, overrides: t.Optional[t.Mapping[str, t.Optional[str]]] = None + ) -> t.Mapping[str, t.Optional[str]]: + """Returns the environment overrides for invoking a script.""" + rv = dict(self.env) + if overrides: + rv.update(overrides) + return rv + + @contextlib.contextmanager + def isolation( + self, + input: t.Optional[t.Union[str, bytes, t.IO[t.Any]]] = None, + env: t.Optional[t.Mapping[str, t.Optional[str]]] = None, + color: bool = False, + ) -> t.Iterator[t.Tuple[io.BytesIO, t.Optional[io.BytesIO]]]: + """A context manager that sets up the isolation for invoking of a + command line tool. This sets up stdin with the given input data + and `os.environ` with the overrides from the given dictionary. + This also rebinds some internals in Click to be mocked (like the + prompt functionality). + + This is automatically done in the :meth:`invoke` method. + + :param input: the input stream to put into sys.stdin. + :param env: the environment overrides as dictionary. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + + .. versionchanged:: 8.0 + ``stderr`` is opened with ``errors="backslashreplace"`` + instead of the default ``"strict"``. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + """ + bytes_input = make_input_stream(input, self.charset) + echo_input = None + + old_stdin = sys.stdin + old_stdout = sys.stdout + old_stderr = sys.stderr + old_forced_width = formatting.FORCED_WIDTH + formatting.FORCED_WIDTH = 80 + + env = self.make_env(env) + + bytes_output = io.BytesIO() + + if self.echo_stdin: + bytes_input = echo_input = t.cast( + t.BinaryIO, EchoingStdin(bytes_input, bytes_output) + ) + + sys.stdin = text_input = _NamedTextIOWrapper( + bytes_input, encoding=self.charset, name="", mode="r" + ) + + if self.echo_stdin: + # Force unbuffered reads, otherwise TextIOWrapper reads a + # large chunk which is echoed early. + text_input._CHUNK_SIZE = 1 # type: ignore + + sys.stdout = _NamedTextIOWrapper( + bytes_output, encoding=self.charset, name="", mode="w" + ) + + bytes_error = None + if self.mix_stderr: + sys.stderr = sys.stdout + else: + bytes_error = io.BytesIO() + sys.stderr = _NamedTextIOWrapper( + bytes_error, + encoding=self.charset, + name="", + mode="w", + errors="backslashreplace", + ) + + @_pause_echo(echo_input) # type: ignore + def visible_input(prompt: t.Optional[str] = None) -> str: + sys.stdout.write(prompt or "") + val = text_input.readline().rstrip("\r\n") + sys.stdout.write(f"{val}\n") + sys.stdout.flush() + return val + + @_pause_echo(echo_input) # type: ignore + def hidden_input(prompt: t.Optional[str] = None) -> str: + sys.stdout.write(f"{prompt or ''}\n") + sys.stdout.flush() + return text_input.readline().rstrip("\r\n") + + @_pause_echo(echo_input) # type: ignore + def _getchar(echo: bool) -> str: + char = sys.stdin.read(1) + + if echo: + sys.stdout.write(char) + + sys.stdout.flush() + return char + + default_color = color + + def should_strip_ansi( + stream: t.Optional[t.IO[t.Any]] = None, color: t.Optional[bool] = None + ) -> bool: + if color is None: + return not default_color + return not color + + old_visible_prompt_func = termui.visible_prompt_func + old_hidden_prompt_func = termui.hidden_prompt_func + old__getchar_func = termui._getchar + old_should_strip_ansi = utils.should_strip_ansi # type: ignore + termui.visible_prompt_func = visible_input + termui.hidden_prompt_func = hidden_input + termui._getchar = _getchar + utils.should_strip_ansi = should_strip_ansi # type: ignore + + old_env = {} + try: + for key, value in env.items(): + old_env[key] = os.environ.get(key) + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + yield (bytes_output, bytes_error) + finally: + for key, value in old_env.items(): + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + sys.stdout = old_stdout + sys.stderr = old_stderr + sys.stdin = old_stdin + termui.visible_prompt_func = old_visible_prompt_func + termui.hidden_prompt_func = old_hidden_prompt_func + termui._getchar = old__getchar_func + utils.should_strip_ansi = old_should_strip_ansi # type: ignore + formatting.FORCED_WIDTH = old_forced_width + + def invoke( + self, + cli: "BaseCommand", + args: t.Optional[t.Union[str, t.Sequence[str]]] = None, + input: t.Optional[t.Union[str, bytes, t.IO[t.Any]]] = None, + env: t.Optional[t.Mapping[str, t.Optional[str]]] = None, + catch_exceptions: bool = True, + color: bool = False, + **extra: t.Any, + ) -> Result: + """Invokes a command in an isolated environment. The arguments are + forwarded directly to the command line script, the `extra` keyword + arguments are passed to the :meth:`~clickpkg.Command.main` function of + the command. + + This returns a :class:`Result` object. + + :param cli: the command to invoke + :param args: the arguments to invoke. It may be given as an iterable + or a string. When given as string it will be interpreted + as a Unix shell command. More details at + :func:`shlex.split`. + :param input: the input data for `sys.stdin`. + :param env: the environment overrides. + :param catch_exceptions: Whether to catch any other exceptions than + ``SystemExit``. + :param extra: the keyword arguments to pass to :meth:`main`. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + + .. versionchanged:: 8.0 + The result object has the ``return_value`` attribute with + the value returned from the invoked command. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + + .. versionchanged:: 3.0 + Added the ``catch_exceptions`` parameter. + + .. versionchanged:: 3.0 + The result object has the ``exc_info`` attribute with the + traceback if available. + """ + exc_info = None + with self.isolation(input=input, env=env, color=color) as outstreams: + return_value = None + exception: t.Optional[BaseException] = None + exit_code = 0 + + if isinstance(args, str): + args = shlex.split(args) + + try: + prog_name = extra.pop("prog_name") + except KeyError: + prog_name = self.get_default_prog_name(cli) + + try: + return_value = cli.main(args=args or (), prog_name=prog_name, **extra) + except SystemExit as e: + exc_info = sys.exc_info() + e_code = t.cast(t.Optional[t.Union[int, t.Any]], e.code) + + if e_code is None: + e_code = 0 + + if e_code != 0: + exception = e + + if not isinstance(e_code, int): + sys.stdout.write(str(e_code)) + sys.stdout.write("\n") + e_code = 1 + + exit_code = e_code + + except Exception as e: + if not catch_exceptions: + raise + exception = e + exit_code = 1 + exc_info = sys.exc_info() + finally: + sys.stdout.flush() + stdout = outstreams[0].getvalue() + if self.mix_stderr: + stderr = None + else: + stderr = outstreams[1].getvalue() # type: ignore + + return Result( + runner=self, + stdout_bytes=stdout, + stderr_bytes=stderr, + return_value=return_value, + exit_code=exit_code, + exception=exception, + exc_info=exc_info, # type: ignore + ) + + @contextlib.contextmanager + def isolated_filesystem( + self, temp_dir: t.Optional[t.Union[str, "os.PathLike[str]"]] = None + ) -> t.Iterator[str]: + """A context manager that creates a temporary directory and + changes the current working directory to it. This isolates tests + that affect the contents of the CWD to prevent them from + interfering with each other. + + :param temp_dir: Create the temporary directory under this + directory. If given, the created directory is not removed + when exiting. + + .. versionchanged:: 8.0 + Added the ``temp_dir`` parameter. + """ + cwd = os.getcwd() + dt = tempfile.mkdtemp(dir=temp_dir) + os.chdir(dt) + + try: + yield dt + finally: + os.chdir(cwd) + + if temp_dir is None: + try: + shutil.rmtree(dt) + except OSError: # noqa: B014 + pass diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/types.py b/lib/go-jinja2/internal/data/darwin-amd64/click/types.py new file mode 100644 index 000000000..2b1d1797f --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/types.py @@ -0,0 +1,1089 @@ +import os +import stat +import sys +import typing as t +from datetime import datetime +from gettext import gettext as _ +from gettext import ngettext + +from ._compat import _get_argv_encoding +from ._compat import open_stream +from .exceptions import BadParameter +from .utils import format_filename +from .utils import LazyFile +from .utils import safecall + +if t.TYPE_CHECKING: + import typing_extensions as te + from .core import Context + from .core import Parameter + from .shell_completion import CompletionItem + + +class ParamType: + """Represents the type of a parameter. Validates and converts values + from the command line or Python into the correct type. + + To implement a custom type, subclass and implement at least the + following: + + - The :attr:`name` class attribute must be set. + - Calling an instance of the type with ``None`` must return + ``None``. This is already implemented by default. + - :meth:`convert` must convert string values to the correct type. + - :meth:`convert` must accept values that are already the correct + type. + - It must be able to convert a value if the ``ctx`` and ``param`` + arguments are ``None``. This can occur when converting prompt + input. + """ + + is_composite: t.ClassVar[bool] = False + arity: t.ClassVar[int] = 1 + + #: the descriptive name of this type + name: str + + #: if a list of this type is expected and the value is pulled from a + #: string environment variable, this is what splits it up. `None` + #: means any whitespace. For all parameters the general rule is that + #: whitespace splits them up. The exception are paths and files which + #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on + #: Windows). + envvar_list_splitter: t.ClassVar[t.Optional[str]] = None + + def to_info_dict(self) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionadded:: 8.0 + """ + # The class name without the "ParamType" suffix. + param_type = type(self).__name__.partition("ParamType")[0] + param_type = param_type.partition("ParameterType")[0] + + # Custom subclasses might not remember to set a name. + if hasattr(self, "name"): + name = self.name + else: + name = param_type + + return {"param_type": param_type, "name": name} + + def __call__( + self, + value: t.Any, + param: t.Optional["Parameter"] = None, + ctx: t.Optional["Context"] = None, + ) -> t.Any: + if value is not None: + return self.convert(value, param, ctx) + + def get_metavar(self, param: "Parameter") -> t.Optional[str]: + """Returns the metavar default for this param if it provides one.""" + + def get_missing_message(self, param: "Parameter") -> t.Optional[str]: + """Optionally might return extra information about a missing + parameter. + + .. versionadded:: 2.0 + """ + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + """Convert the value to the correct type. This is not called if + the value is ``None`` (the missing value). + + This must accept string values from the command line, as well as + values that are already the correct type. It may also convert + other compatible types. + + The ``param`` and ``ctx`` arguments may be ``None`` in certain + situations, such as when converting prompt input. + + If the value cannot be converted, call :meth:`fail` with a + descriptive message. + + :param value: The value to convert. + :param param: The parameter that is using this type to convert + its value. May be ``None``. + :param ctx: The current context that arrived at this value. May + be ``None``. + """ + return value + + def split_envvar_value(self, rv: str) -> t.Sequence[str]: + """Given a value from an environment variable this splits it up + into small chunks depending on the defined envvar list splitter. + + If the splitter is set to `None`, which means that whitespace splits, + then leading and trailing whitespace is ignored. Otherwise, leading + and trailing splitters usually lead to empty items being included. + """ + return (rv or "").split(self.envvar_list_splitter) + + def fail( + self, + message: str, + param: t.Optional["Parameter"] = None, + ctx: t.Optional["Context"] = None, + ) -> "t.NoReturn": + """Helper method to fail with an invalid value message.""" + raise BadParameter(message, ctx=ctx, param=param) + + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Return a list of + :class:`~click.shell_completion.CompletionItem` objects for the + incomplete value. Most types do not provide completions, but + some do, and this allows custom types to provide custom + completions as well. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + return [] + + +class CompositeParamType(ParamType): + is_composite = True + + @property + def arity(self) -> int: # type: ignore + raise NotImplementedError() + + +class FuncParamType(ParamType): + def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None: + self.name: str = func.__name__ + self.func = func + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["func"] = self.func + return info_dict + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + try: + return self.func(value) + except ValueError: + try: + value = str(value) + except UnicodeError: + value = value.decode("utf-8", "replace") + + self.fail(value, param, ctx) + + +class UnprocessedParamType(ParamType): + name = "text" + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + return value + + def __repr__(self) -> str: + return "UNPROCESSED" + + +class StringParamType(ParamType): + name = "text" + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + if isinstance(value, bytes): + enc = _get_argv_encoding() + try: + value = value.decode(enc) + except UnicodeError: + fs_enc = sys.getfilesystemencoding() + if fs_enc != enc: + try: + value = value.decode(fs_enc) + except UnicodeError: + value = value.decode("utf-8", "replace") + else: + value = value.decode("utf-8", "replace") + return value + return str(value) + + def __repr__(self) -> str: + return "STRING" + + +class Choice(ParamType): + """The choice type allows a value to be checked against a fixed set + of supported values. All of these values have to be strings. + + You should only pass a list or tuple of choices. Other iterables + (like generators) may lead to surprising results. + + The resulting value will always be one of the originally passed choices + regardless of ``case_sensitive`` or any ``ctx.token_normalize_func`` + being specified. + + See :ref:`choice-opts` for an example. + + :param case_sensitive: Set to false to make choices case + insensitive. Defaults to true. + """ + + name = "choice" + + def __init__(self, choices: t.Sequence[str], case_sensitive: bool = True) -> None: + self.choices = choices + self.case_sensitive = case_sensitive + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["choices"] = self.choices + info_dict["case_sensitive"] = self.case_sensitive + return info_dict + + def get_metavar(self, param: "Parameter") -> str: + choices_str = "|".join(self.choices) + + # Use curly braces to indicate a required argument. + if param.required and param.param_type_name == "argument": + return f"{{{choices_str}}}" + + # Use square braces to indicate an option or optional argument. + return f"[{choices_str}]" + + def get_missing_message(self, param: "Parameter") -> str: + return _("Choose from:\n\t{choices}").format(choices=",\n\t".join(self.choices)) + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + # Match through normalization and case sensitivity + # first do token_normalize_func, then lowercase + # preserve original `value` to produce an accurate message in + # `self.fail` + normed_value = value + normed_choices = {choice: choice for choice in self.choices} + + if ctx is not None and ctx.token_normalize_func is not None: + normed_value = ctx.token_normalize_func(value) + normed_choices = { + ctx.token_normalize_func(normed_choice): original + for normed_choice, original in normed_choices.items() + } + + if not self.case_sensitive: + normed_value = normed_value.casefold() + normed_choices = { + normed_choice.casefold(): original + for normed_choice, original in normed_choices.items() + } + + if normed_value in normed_choices: + return normed_choices[normed_value] + + choices_str = ", ".join(map(repr, self.choices)) + self.fail( + ngettext( + "{value!r} is not {choice}.", + "{value!r} is not one of {choices}.", + len(self.choices), + ).format(value=value, choice=choices_str, choices=choices_str), + param, + ctx, + ) + + def __repr__(self) -> str: + return f"Choice({list(self.choices)})" + + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Complete choices that start with the incomplete value. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + str_choices = map(str, self.choices) + + if self.case_sensitive: + matched = (c for c in str_choices if c.startswith(incomplete)) + else: + incomplete = incomplete.lower() + matched = (c for c in str_choices if c.lower().startswith(incomplete)) + + return [CompletionItem(c) for c in matched] + + +class DateTime(ParamType): + """The DateTime type converts date strings into `datetime` objects. + + The format strings which are checked are configurable, but default to some + common (non-timezone aware) ISO 8601 formats. + + When specifying *DateTime* formats, you should only pass a list or a tuple. + Other iterables, like generators, may lead to surprising results. + + The format strings are processed using ``datetime.strptime``, and this + consequently defines the format strings which are allowed. + + Parsing is tried using each format, in order, and the first format which + parses successfully is used. + + :param formats: A list or tuple of date format strings, in the order in + which they should be tried. Defaults to + ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``, + ``'%Y-%m-%d %H:%M:%S'``. + """ + + name = "datetime" + + def __init__(self, formats: t.Optional[t.Sequence[str]] = None): + self.formats: t.Sequence[str] = formats or [ + "%Y-%m-%d", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d %H:%M:%S", + ] + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["formats"] = self.formats + return info_dict + + def get_metavar(self, param: "Parameter") -> str: + return f"[{'|'.join(self.formats)}]" + + def _try_to_convert_date(self, value: t.Any, format: str) -> t.Optional[datetime]: + try: + return datetime.strptime(value, format) + except ValueError: + return None + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + if isinstance(value, datetime): + return value + + for format in self.formats: + converted = self._try_to_convert_date(value, format) + + if converted is not None: + return converted + + formats_str = ", ".join(map(repr, self.formats)) + self.fail( + ngettext( + "{value!r} does not match the format {format}.", + "{value!r} does not match the formats {formats}.", + len(self.formats), + ).format(value=value, format=formats_str, formats=formats_str), + param, + ctx, + ) + + def __repr__(self) -> str: + return "DateTime" + + +class _NumberParamTypeBase(ParamType): + _number_class: t.ClassVar[t.Type[t.Any]] + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + try: + return self._number_class(value) + except ValueError: + self.fail( + _("{value!r} is not a valid {number_type}.").format( + value=value, number_type=self.name + ), + param, + ctx, + ) + + +class _NumberRangeBase(_NumberParamTypeBase): + def __init__( + self, + min: t.Optional[float] = None, + max: t.Optional[float] = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: + self.min = min + self.max = max + self.min_open = min_open + self.max_open = max_open + self.clamp = clamp + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + min=self.min, + max=self.max, + min_open=self.min_open, + max_open=self.max_open, + clamp=self.clamp, + ) + return info_dict + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + import operator + + rv = super().convert(value, param, ctx) + lt_min: bool = self.min is not None and ( + operator.le if self.min_open else operator.lt + )(rv, self.min) + gt_max: bool = self.max is not None and ( + operator.ge if self.max_open else operator.gt + )(rv, self.max) + + if self.clamp: + if lt_min: + return self._clamp(self.min, 1, self.min_open) # type: ignore + + if gt_max: + return self._clamp(self.max, -1, self.max_open) # type: ignore + + if lt_min or gt_max: + self.fail( + _("{value} is not in the range {range}.").format( + value=rv, range=self._describe_range() + ), + param, + ctx, + ) + + return rv + + def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float: + """Find the valid value to clamp to bound in the given + direction. + + :param bound: The boundary value. + :param dir: 1 or -1 indicating the direction to move. + :param open: If true, the range does not include the bound. + """ + raise NotImplementedError + + def _describe_range(self) -> str: + """Describe the range for use in help text.""" + if self.min is None: + op = "<" if self.max_open else "<=" + return f"x{op}{self.max}" + + if self.max is None: + op = ">" if self.min_open else ">=" + return f"x{op}{self.min}" + + lop = "<" if self.min_open else "<=" + rop = "<" if self.max_open else "<=" + return f"{self.min}{lop}x{rop}{self.max}" + + def __repr__(self) -> str: + clamp = " clamped" if self.clamp else "" + return f"<{type(self).__name__} {self._describe_range()}{clamp}>" + + +class IntParamType(_NumberParamTypeBase): + name = "integer" + _number_class = int + + def __repr__(self) -> str: + return "INT" + + +class IntRange(_NumberRangeBase, IntParamType): + """Restrict an :data:`click.INT` value to a range of accepted + values. See :ref:`ranges`. + + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. + """ + + name = "integer range" + + def _clamp( # type: ignore + self, bound: int, dir: "te.Literal[1, -1]", open: bool + ) -> int: + if not open: + return bound + + return bound + dir + + +class FloatParamType(_NumberParamTypeBase): + name = "float" + _number_class = float + + def __repr__(self) -> str: + return "FLOAT" + + +class FloatRange(_NumberRangeBase, FloatParamType): + """Restrict a :data:`click.FLOAT` value to a range of accepted + values. See :ref:`ranges`. + + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. This is not supported if either + boundary is marked ``open``. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. + """ + + name = "float range" + + def __init__( + self, + min: t.Optional[float] = None, + max: t.Optional[float] = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: + super().__init__( + min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp + ) + + if (min_open or max_open) and clamp: + raise TypeError("Clamping is not supported for open bounds.") + + def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float: + if not open: + return bound + + # Could use Python 3.9's math.nextafter here, but clamping an + # open float range doesn't seem to be particularly useful. It's + # left up to the user to write a callback to do it if needed. + raise RuntimeError("Clamping is not supported for open bounds.") + + +class BoolParamType(ParamType): + name = "boolean" + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + if value in {False, True}: + return bool(value) + + norm = value.strip().lower() + + if norm in {"1", "true", "t", "yes", "y", "on"}: + return True + + if norm in {"0", "false", "f", "no", "n", "off"}: + return False + + self.fail( + _("{value!r} is not a valid boolean.").format(value=value), param, ctx + ) + + def __repr__(self) -> str: + return "BOOL" + + +class UUIDParameterType(ParamType): + name = "uuid" + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + import uuid + + if isinstance(value, uuid.UUID): + return value + + value = value.strip() + + try: + return uuid.UUID(value) + except ValueError: + self.fail( + _("{value!r} is not a valid UUID.").format(value=value), param, ctx + ) + + def __repr__(self) -> str: + return "UUID" + + +class File(ParamType): + """Declares a parameter to be a file for reading or writing. The file + is automatically closed once the context tears down (after the command + finished working). + + Files can be opened for reading or writing. The special value ``-`` + indicates stdin or stdout depending on the mode. + + By default, the file is opened for reading text data, but it can also be + opened in binary mode or for writing. The encoding parameter can be used + to force a specific encoding. + + The `lazy` flag controls if the file should be opened immediately or upon + first IO. The default is to be non-lazy for standard input and output + streams as well as files opened for reading, `lazy` otherwise. When opening a + file lazily for reading, it is still opened temporarily for validation, but + will not be held open until first IO. lazy is mainly useful when opening + for writing to avoid creating the file until it is needed. + + Starting with Click 2.0, files can also be opened atomically in which + case all writes go into a separate file in the same folder and upon + completion the file will be moved over to the original location. This + is useful if a file regularly read by other users is modified. + + See :ref:`file-args` for more information. + """ + + name = "filename" + envvar_list_splitter: t.ClassVar[str] = os.path.pathsep + + def __init__( + self, + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + lazy: t.Optional[bool] = None, + atomic: bool = False, + ) -> None: + self.mode = mode + self.encoding = encoding + self.errors = errors + self.lazy = lazy + self.atomic = atomic + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update(mode=self.mode, encoding=self.encoding) + return info_dict + + def resolve_lazy_flag(self, value: "t.Union[str, os.PathLike[str]]") -> bool: + if self.lazy is not None: + return self.lazy + if os.fspath(value) == "-": + return False + elif "w" in self.mode: + return True + return False + + def convert( + self, + value: t.Union[str, "os.PathLike[str]", t.IO[t.Any]], + param: t.Optional["Parameter"], + ctx: t.Optional["Context"], + ) -> t.IO[t.Any]: + if _is_file_like(value): + return value + + value = t.cast("t.Union[str, os.PathLike[str]]", value) + + try: + lazy = self.resolve_lazy_flag(value) + + if lazy: + lf = LazyFile( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + + if ctx is not None: + ctx.call_on_close(lf.close_intelligently) + + return t.cast(t.IO[t.Any], lf) + + f, should_close = open_stream( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + + # If a context is provided, we automatically close the file + # at the end of the context execution (or flush out). If a + # context does not exist, it's the caller's responsibility to + # properly close the file. This for instance happens when the + # type is used with prompts. + if ctx is not None: + if should_close: + ctx.call_on_close(safecall(f.close)) + else: + ctx.call_on_close(safecall(f.flush)) + + return f + except OSError as e: # noqa: B014 + self.fail(f"'{format_filename(value)}': {e.strerror}", param, ctx) + + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Return a special completion marker that tells the completion + system to use the shell to provide file path completions. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + return [CompletionItem(incomplete, type="file")] + + +def _is_file_like(value: t.Any) -> "te.TypeGuard[t.IO[t.Any]]": + return hasattr(value, "read") or hasattr(value, "write") + + +class Path(ParamType): + """The ``Path`` type is similar to the :class:`File` type, but + returns the filename instead of an open file. Various checks can be + enabled to validate the type of file and permissions. + + :param exists: The file or directory needs to exist for the value to + be valid. If this is not set to ``True``, and the file does not + exist, then all further checks are silently skipped. + :param file_okay: Allow a file as a value. + :param dir_okay: Allow a directory as a value. + :param readable: if true, a readable check is performed. + :param writable: if true, a writable check is performed. + :param executable: if true, an executable check is performed. + :param resolve_path: Make the value absolute and resolve any + symlinks. A ``~`` is not expanded, as this is supposed to be + done by the shell only. + :param allow_dash: Allow a single dash as a value, which indicates + a standard stream (but does not open it). Use + :func:`~click.open_file` to handle opening this value. + :param path_type: Convert the incoming path value to this type. If + ``None``, keep Python's default, which is ``str``. Useful to + convert to :class:`pathlib.Path`. + + .. versionchanged:: 8.1 + Added the ``executable`` parameter. + + .. versionchanged:: 8.0 + Allow passing ``path_type=pathlib.Path``. + + .. versionchanged:: 6.0 + Added the ``allow_dash`` parameter. + """ + + envvar_list_splitter: t.ClassVar[str] = os.path.pathsep + + def __init__( + self, + exists: bool = False, + file_okay: bool = True, + dir_okay: bool = True, + writable: bool = False, + readable: bool = True, + resolve_path: bool = False, + allow_dash: bool = False, + path_type: t.Optional[t.Type[t.Any]] = None, + executable: bool = False, + ): + self.exists = exists + self.file_okay = file_okay + self.dir_okay = dir_okay + self.readable = readable + self.writable = writable + self.executable = executable + self.resolve_path = resolve_path + self.allow_dash = allow_dash + self.type = path_type + + if self.file_okay and not self.dir_okay: + self.name: str = _("file") + elif self.dir_okay and not self.file_okay: + self.name = _("directory") + else: + self.name = _("path") + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + exists=self.exists, + file_okay=self.file_okay, + dir_okay=self.dir_okay, + writable=self.writable, + readable=self.readable, + allow_dash=self.allow_dash, + ) + return info_dict + + def coerce_path_result( + self, value: "t.Union[str, os.PathLike[str]]" + ) -> "t.Union[str, bytes, os.PathLike[str]]": + if self.type is not None and not isinstance(value, self.type): + if self.type is str: + return os.fsdecode(value) + elif self.type is bytes: + return os.fsencode(value) + else: + return t.cast("os.PathLike[str]", self.type(value)) + + return value + + def convert( + self, + value: "t.Union[str, os.PathLike[str]]", + param: t.Optional["Parameter"], + ctx: t.Optional["Context"], + ) -> "t.Union[str, bytes, os.PathLike[str]]": + rv = value + + is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-") + + if not is_dash: + if self.resolve_path: + # os.path.realpath doesn't resolve symlinks on Windows + # until Python 3.8. Use pathlib for now. + import pathlib + + rv = os.fsdecode(pathlib.Path(rv).resolve()) + + try: + st = os.stat(rv) + except OSError: + if not self.exists: + return self.coerce_path_result(rv) + self.fail( + _("{name} {filename!r} does not exist.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if not self.file_okay and stat.S_ISREG(st.st_mode): + self.fail( + _("{name} {filename!r} is a file.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + if not self.dir_okay and stat.S_ISDIR(st.st_mode): + self.fail( + _("{name} '{filename}' is a directory.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.readable and not os.access(rv, os.R_OK): + self.fail( + _("{name} {filename!r} is not readable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.writable and not os.access(rv, os.W_OK): + self.fail( + _("{name} {filename!r} is not writable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.executable and not os.access(value, os.X_OK): + self.fail( + _("{name} {filename!r} is not executable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + return self.coerce_path_result(rv) + + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Return a special completion marker that tells the completion + system to use the shell to provide path completions for only + directories or any paths. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + type = "dir" if self.dir_okay and not self.file_okay else "file" + return [CompletionItem(incomplete, type=type)] + + +class Tuple(CompositeParamType): + """The default behavior of Click is to apply a type on a value directly. + This works well in most cases, except for when `nargs` is set to a fixed + count and different types should be used for different items. In this + case the :class:`Tuple` type can be used. This type can only be used + if `nargs` is set to a fixed number. + + For more information see :ref:`tuple-type`. + + This can be selected by using a Python tuple literal as a type. + + :param types: a list of types that should be used for the tuple items. + """ + + def __init__(self, types: t.Sequence[t.Union[t.Type[t.Any], ParamType]]) -> None: + self.types: t.Sequence[ParamType] = [convert_type(ty) for ty in types] + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["types"] = [t.to_info_dict() for t in self.types] + return info_dict + + @property + def name(self) -> str: # type: ignore + return f"<{' '.join(ty.name for ty in self.types)}>" + + @property + def arity(self) -> int: # type: ignore + return len(self.types) + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + len_type = len(self.types) + len_value = len(value) + + if len_value != len_type: + self.fail( + ngettext( + "{len_type} values are required, but {len_value} was given.", + "{len_type} values are required, but {len_value} were given.", + len_value, + ).format(len_type=len_type, len_value=len_value), + param=param, + ctx=ctx, + ) + + return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value)) + + +def convert_type(ty: t.Optional[t.Any], default: t.Optional[t.Any] = None) -> ParamType: + """Find the most appropriate :class:`ParamType` for the given Python + type. If the type isn't provided, it can be inferred from a default + value. + """ + guessed_type = False + + if ty is None and default is not None: + if isinstance(default, (tuple, list)): + # If the default is empty, ty will remain None and will + # return STRING. + if default: + item = default[0] + + # A tuple of tuples needs to detect the inner types. + # Can't call convert recursively because that would + # incorrectly unwind the tuple to a single type. + if isinstance(item, (tuple, list)): + ty = tuple(map(type, item)) + else: + ty = type(item) + else: + ty = type(default) + + guessed_type = True + + if isinstance(ty, tuple): + return Tuple(ty) + + if isinstance(ty, ParamType): + return ty + + if ty is str or ty is None: + return STRING + + if ty is int: + return INT + + if ty is float: + return FLOAT + + if ty is bool: + return BOOL + + if guessed_type: + return STRING + + if __debug__: + try: + if issubclass(ty, ParamType): + raise AssertionError( + f"Attempted to use an uninstantiated parameter type ({ty})." + ) + except TypeError: + # ty is an instance (correct), so issubclass fails. + pass + + return FuncParamType(ty) + + +#: A dummy parameter type that just does nothing. From a user's +#: perspective this appears to just be the same as `STRING` but +#: internally no string conversion takes place if the input was bytes. +#: This is usually useful when working with file paths as they can +#: appear in bytes and unicode. +#: +#: For path related uses the :class:`Path` type is a better choice but +#: there are situations where an unprocessed type is useful which is why +#: it is is provided. +#: +#: .. versionadded:: 4.0 +UNPROCESSED = UnprocessedParamType() + +#: A unicode string parameter type which is the implicit default. This +#: can also be selected by using ``str`` as type. +STRING = StringParamType() + +#: An integer parameter. This can also be selected by using ``int`` as +#: type. +INT = IntParamType() + +#: A floating point value parameter. This can also be selected by using +#: ``float`` as type. +FLOAT = FloatParamType() + +#: A boolean parameter. This is the default for boolean flags. This can +#: also be selected by using ``bool`` as a type. +BOOL = BoolParamType() + +#: A UUID parameter. +UUID = UUIDParameterType() diff --git a/lib/go-jinja2/internal/data/darwin-amd64/click/utils.py b/lib/go-jinja2/internal/data/darwin-amd64/click/utils.py new file mode 100644 index 000000000..d536434f0 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/click/utils.py @@ -0,0 +1,624 @@ +import os +import re +import sys +import typing as t +from functools import update_wrapper +from types import ModuleType +from types import TracebackType + +from ._compat import _default_text_stderr +from ._compat import _default_text_stdout +from ._compat import _find_binary_writer +from ._compat import auto_wrap_for_ansi +from ._compat import binary_streams +from ._compat import open_stream +from ._compat import should_strip_ansi +from ._compat import strip_ansi +from ._compat import text_streams +from ._compat import WIN +from .globals import resolve_color_default + +if t.TYPE_CHECKING: + import typing_extensions as te + + P = te.ParamSpec("P") + +R = t.TypeVar("R") + + +def _posixify(name: str) -> str: + return "-".join(name.split()).lower() + + +def safecall(func: "t.Callable[P, R]") -> "t.Callable[P, t.Optional[R]]": + """Wraps a function so that it swallows exceptions.""" + + def wrapper(*args: "P.args", **kwargs: "P.kwargs") -> t.Optional[R]: + try: + return func(*args, **kwargs) + except Exception: + pass + return None + + return update_wrapper(wrapper, func) + + +def make_str(value: t.Any) -> str: + """Converts a value into a valid string.""" + if isinstance(value, bytes): + try: + return value.decode(sys.getfilesystemencoding()) + except UnicodeError: + return value.decode("utf-8", "replace") + return str(value) + + +def make_default_short_help(help: str, max_length: int = 45) -> str: + """Returns a condensed version of help string.""" + # Consider only the first paragraph. + paragraph_end = help.find("\n\n") + + if paragraph_end != -1: + help = help[:paragraph_end] + + # Collapse newlines, tabs, and spaces. + words = help.split() + + if not words: + return "" + + # The first paragraph started with a "no rewrap" marker, ignore it. + if words[0] == "\b": + words = words[1:] + + total_length = 0 + last_index = len(words) - 1 + + for i, word in enumerate(words): + total_length += len(word) + (i > 0) + + if total_length > max_length: # too long, truncate + break + + if word[-1] == ".": # sentence end, truncate without "..." + return " ".join(words[: i + 1]) + + if total_length == max_length and i != last_index: + break # not at sentence end, truncate with "..." + else: + return " ".join(words) # no truncation needed + + # Account for the length of the suffix. + total_length += len("...") + + # remove words until the length is short enough + while i > 0: + total_length -= len(words[i]) + (i > 0) + + if total_length <= max_length: + break + + i -= 1 + + return " ".join(words[:i]) + "..." + + +class LazyFile: + """A lazy file works like a regular file but it does not fully open + the file but it does perform some basic checks early to see if the + filename parameter does make sense. This is useful for safely opening + files for writing. + """ + + def __init__( + self, + filename: t.Union[str, "os.PathLike[str]"], + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + atomic: bool = False, + ): + self.name: str = os.fspath(filename) + self.mode = mode + self.encoding = encoding + self.errors = errors + self.atomic = atomic + self._f: t.Optional[t.IO[t.Any]] + self.should_close: bool + + if self.name == "-": + self._f, self.should_close = open_stream(filename, mode, encoding, errors) + else: + if "r" in mode: + # Open and close the file in case we're opening it for + # reading so that we can catch at least some errors in + # some cases early. + open(filename, mode).close() + self._f = None + self.should_close = True + + def __getattr__(self, name: str) -> t.Any: + return getattr(self.open(), name) + + def __repr__(self) -> str: + if self._f is not None: + return repr(self._f) + return f"" + + def open(self) -> t.IO[t.Any]: + """Opens the file if it's not yet open. This call might fail with + a :exc:`FileError`. Not handling this error will produce an error + that Click shows. + """ + if self._f is not None: + return self._f + try: + rv, self.should_close = open_stream( + self.name, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + except OSError as e: # noqa: E402 + from .exceptions import FileError + + raise FileError(self.name, hint=e.strerror) from e + self._f = rv + return rv + + def close(self) -> None: + """Closes the underlying file, no matter what.""" + if self._f is not None: + self._f.close() + + def close_intelligently(self) -> None: + """This function only closes the file if it was opened by the lazy + file wrapper. For instance this will never close stdin. + """ + if self.should_close: + self.close() + + def __enter__(self) -> "LazyFile": + return self + + def __exit__( + self, + exc_type: t.Optional[t.Type[BaseException]], + exc_value: t.Optional[BaseException], + tb: t.Optional[TracebackType], + ) -> None: + self.close_intelligently() + + def __iter__(self) -> t.Iterator[t.AnyStr]: + self.open() + return iter(self._f) # type: ignore + + +class KeepOpenFile: + def __init__(self, file: t.IO[t.Any]) -> None: + self._file: t.IO[t.Any] = file + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._file, name) + + def __enter__(self) -> "KeepOpenFile": + return self + + def __exit__( + self, + exc_type: t.Optional[t.Type[BaseException]], + exc_value: t.Optional[BaseException], + tb: t.Optional[TracebackType], + ) -> None: + pass + + def __repr__(self) -> str: + return repr(self._file) + + def __iter__(self) -> t.Iterator[t.AnyStr]: + return iter(self._file) + + +def echo( + message: t.Optional[t.Any] = None, + file: t.Optional[t.IO[t.Any]] = None, + nl: bool = True, + err: bool = False, + color: t.Optional[bool] = None, +) -> None: + """Print a message and newline to stdout or a file. This should be + used instead of :func:`print` because it provides better support + for different data, files, and environments. + + Compared to :func:`print`, this does the following: + + - Ensures that the output encoding is not misconfigured on Linux. + - Supports Unicode in the Windows console. + - Supports writing to binary outputs, and supports writing bytes + to text outputs. + - Supports colors and styles on Windows. + - Removes ANSI color and style codes if the output does not look + like an interactive terminal. + - Always flushes the output. + + :param message: The string or bytes to output. Other objects are + converted to strings. + :param file: The file to write to. Defaults to ``stdout``. + :param err: Write to ``stderr`` instead of ``stdout``. + :param nl: Print a newline after the message. Enabled by default. + :param color: Force showing or hiding colors and other styles. By + default Click will remove color if the output does not look like + an interactive terminal. + + .. versionchanged:: 6.0 + Support Unicode output on the Windows console. Click does not + modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()`` + will still not support Unicode. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + + .. versionadded:: 3.0 + Added the ``err`` parameter. + + .. versionchanged:: 2.0 + Support colors on Windows if colorama is installed. + """ + if file is None: + if err: + file = _default_text_stderr() + else: + file = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if file is None: + return + + # Convert non bytes/text into the native string type. + if message is not None and not isinstance(message, (str, bytes, bytearray)): + out: t.Optional[t.Union[str, bytes]] = str(message) + else: + out = message + + if nl: + out = out or "" + if isinstance(out, str): + out += "\n" + else: + out += b"\n" + + if not out: + file.flush() + return + + # If there is a message and the value looks like bytes, we manually + # need to find the binary stream and write the message in there. + # This is done separately so that most stream types will work as you + # would expect. Eg: you can write to StringIO for other cases. + if isinstance(out, (bytes, bytearray)): + binary_file = _find_binary_writer(file) + + if binary_file is not None: + file.flush() + binary_file.write(out) + binary_file.flush() + return + + # ANSI style code support. For no message or bytes, nothing happens. + # When outputting to a file instead of a terminal, strip codes. + else: + color = resolve_color_default(color) + + if should_strip_ansi(file, color): + out = strip_ansi(out) + elif WIN: + if auto_wrap_for_ansi is not None: + file = auto_wrap_for_ansi(file) # type: ignore + elif not color: + out = strip_ansi(out) + + file.write(out) # type: ignore + file.flush() + + +def get_binary_stream(name: "te.Literal['stdin', 'stdout', 'stderr']") -> t.BinaryIO: + """Returns a system stream for byte processing. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + """ + opener = binary_streams.get(name) + if opener is None: + raise TypeError(f"Unknown standard stream '{name}'") + return opener() + + +def get_text_stream( + name: "te.Literal['stdin', 'stdout', 'stderr']", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", +) -> t.TextIO: + """Returns a system stream for text processing. This usually returns + a wrapped stream around a binary stream returned from + :func:`get_binary_stream` but it also can take shortcuts for already + correctly configured streams. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + :param encoding: overrides the detected default encoding. + :param errors: overrides the default error mode. + """ + opener = text_streams.get(name) + if opener is None: + raise TypeError(f"Unknown standard stream '{name}'") + return opener(encoding, errors) + + +def open_file( + filename: str, + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + lazy: bool = False, + atomic: bool = False, +) -> t.IO[t.Any]: + """Open a file, with extra behavior to handle ``'-'`` to indicate + a standard stream, lazy open on write, and atomic write. Similar to + the behavior of the :class:`~click.File` param type. + + If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is + wrapped so that using it in a context manager will not close it. + This makes it possible to use the function without accidentally + closing a standard stream: + + .. code-block:: python + + with open_file(filename) as f: + ... + + :param filename: The name of the file to open, or ``'-'`` for + ``stdin``/``stdout``. + :param mode: The mode in which to open the file. + :param encoding: The encoding to decode or encode a file opened in + text mode. + :param errors: The error handling mode. + :param lazy: Wait to open the file until it is accessed. For read + mode, the file is temporarily opened to raise access errors + early, then closed until it is read again. + :param atomic: Write to a temporary file and replace the given file + on close. + + .. versionadded:: 3.0 + """ + if lazy: + return t.cast( + t.IO[t.Any], LazyFile(filename, mode, encoding, errors, atomic=atomic) + ) + + f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) + + if not should_close: + f = t.cast(t.IO[t.Any], KeepOpenFile(f)) + + return f + + +def format_filename( + filename: "t.Union[str, bytes, os.PathLike[str], os.PathLike[bytes]]", + shorten: bool = False, +) -> str: + """Format a filename as a string for display. Ensures the filename can be + displayed by replacing any invalid bytes or surrogate escapes in the name + with the replacement character ``�``. + + Invalid bytes or surrogate escapes will raise an error when written to a + stream with ``errors="strict". This will typically happen with ``stdout`` + when the locale is something like ``en_GB.UTF-8``. + + Many scenarios *are* safe to write surrogates though, due to PEP 538 and + PEP 540, including: + + - Writing to ``stderr``, which uses ``errors="backslashreplace"``. + - The system has ``LANG=C.UTF-8``, ``C``, or ``POSIX``. Python opens + stdout and stderr with ``errors="surrogateescape"``. + - None of ``LANG/LC_*`` are set. Python assumes ``LANG=C.UTF-8``. + - Python is started in UTF-8 mode with ``PYTHONUTF8=1`` or ``-X utf8``. + Python opens stdout and stderr with ``errors="surrogateescape"``. + + :param filename: formats a filename for UI display. This will also convert + the filename into unicode without failing. + :param shorten: this optionally shortens the filename to strip of the + path that leads up to it. + """ + if shorten: + filename = os.path.basename(filename) + else: + filename = os.fspath(filename) + + if isinstance(filename, bytes): + filename = filename.decode(sys.getfilesystemencoding(), "replace") + else: + filename = filename.encode("utf-8", "surrogateescape").decode( + "utf-8", "replace" + ) + + return filename + + +def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str: + r"""Returns the config folder for the application. The default behavior + is to return whatever is most appropriate for the operating system. + + To give you an idea, for an app called ``"Foo Bar"``, something like + the following folders could be returned: + + Mac OS X: + ``~/Library/Application Support/Foo Bar`` + Mac OS X (POSIX): + ``~/.foo-bar`` + Unix: + ``~/.config/foo-bar`` + Unix (POSIX): + ``~/.foo-bar`` + Windows (roaming): + ``C:\Users\\AppData\Roaming\Foo Bar`` + Windows (not roaming): + ``C:\Users\\AppData\Local\Foo Bar`` + + .. versionadded:: 2.0 + + :param app_name: the application name. This should be properly capitalized + and can contain whitespace. + :param roaming: controls if the folder should be roaming or not on Windows. + Has no effect otherwise. + :param force_posix: if this is set to `True` then on any POSIX system the + folder will be stored in the home folder with a leading + dot instead of the XDG config home or darwin's + application support folder. + """ + if WIN: + key = "APPDATA" if roaming else "LOCALAPPDATA" + folder = os.environ.get(key) + if folder is None: + folder = os.path.expanduser("~") + return os.path.join(folder, app_name) + if force_posix: + return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}")) + if sys.platform == "darwin": + return os.path.join( + os.path.expanduser("~/Library/Application Support"), app_name + ) + return os.path.join( + os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), + _posixify(app_name), + ) + + +class PacifyFlushWrapper: + """This wrapper is used to catch and suppress BrokenPipeErrors resulting + from ``.flush()`` being called on broken pipe during the shutdown/final-GC + of the Python interpreter. Notably ``.flush()`` is always called on + ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any + other cleanup code, and the case where the underlying file is not a broken + pipe, all calls and attributes are proxied. + """ + + def __init__(self, wrapped: t.IO[t.Any]) -> None: + self.wrapped = wrapped + + def flush(self) -> None: + try: + self.wrapped.flush() + except OSError as e: + import errno + + if e.errno != errno.EPIPE: + raise + + def __getattr__(self, attr: str) -> t.Any: + return getattr(self.wrapped, attr) + + +def _detect_program_name( + path: t.Optional[str] = None, _main: t.Optional[ModuleType] = None +) -> str: + """Determine the command used to run the program, for use in help + text. If a file or entry point was executed, the file name is + returned. If ``python -m`` was used to execute a module or package, + ``python -m name`` is returned. + + This doesn't try to be too precise, the goal is to give a concise + name for help text. Files are only shown as their name without the + path. ``python`` is only shown for modules, and the full path to + ``sys.executable`` is not shown. + + :param path: The Python file being executed. Python puts this in + ``sys.argv[0]``, which is used by default. + :param _main: The ``__main__`` module. This should only be passed + during internal testing. + + .. versionadded:: 8.0 + Based on command args detection in the Werkzeug reloader. + + :meta private: + """ + if _main is None: + _main = sys.modules["__main__"] + + if not path: + path = sys.argv[0] + + # The value of __package__ indicates how Python was called. It may + # not exist if a setuptools script is installed as an egg. It may be + # set incorrectly for entry points created with pip on Windows. + # It is set to "" inside a Shiv or PEX zipapp. + if getattr(_main, "__package__", None) in {None, ""} or ( + os.name == "nt" + and _main.__package__ == "" + and not os.path.exists(path) + and os.path.exists(f"{path}.exe") + ): + # Executed a file, like "python app.py". + return os.path.basename(path) + + # Executed a module, like "python -m example". + # Rewritten by Python from "-m script" to "/path/to/script.py". + # Need to look at main module to determine how it was executed. + py_module = t.cast(str, _main.__package__) + name = os.path.splitext(os.path.basename(path))[0] + + # A submodule like "example.cli". + if name != "__main__": + py_module = f"{py_module}.{name}" + + return f"python -m {py_module.lstrip('.')}" + + +def _expand_args( + args: t.Iterable[str], + *, + user: bool = True, + env: bool = True, + glob_recursive: bool = True, +) -> t.List[str]: + """Simulate Unix shell expansion with Python functions. + + See :func:`glob.glob`, :func:`os.path.expanduser`, and + :func:`os.path.expandvars`. + + This is intended for use on Windows, where the shell does not do any + expansion. It may not exactly match what a Unix shell would do. + + :param args: List of command line arguments to expand. + :param user: Expand user home directory. + :param env: Expand environment variables. + :param glob_recursive: ``**`` matches directories recursively. + + .. versionchanged:: 8.1 + Invalid glob patterns are treated as empty expansions rather + than raising an error. + + .. versionadded:: 8.0 + + :meta private: + """ + from glob import glob + + out = [] + + for arg in args: + if user: + arg = os.path.expanduser(arg) + + if env: + arg = os.path.expandvars(arg) + + try: + matches = glob(arg, recursive=glob_recursive) + except re.error: + matches = [] + + if not matches: + out.append(arg) + else: + out.extend(matches) + + return out diff --git a/lib/go-jinja2/internal/data/darwin-amd64/files.json b/lib/go-jinja2/internal/data/darwin-amd64/files.json new file mode 100644 index 000000000..42b2a611b --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/files.json @@ -0,0 +1,888 @@ +{ + "contentHash": "b0bda8aebc925623072dface975a71cf42040dea26fa8a6a91d5ad33d58a3288", + "files": [ + { + "name": "MarkupSafe-3.0.2.dist-info", + "size": 0, + "perm": 2147484141 + }, + { + "name": "MarkupSafe-3.0.2.dist-info/INSTALLER", + "size": 4, + "perm": 420 + }, + { + "name": "MarkupSafe-3.0.2.dist-info/LICENSE.txt", + "size": 1475, + "perm": 420 + }, + { + "name": "MarkupSafe-3.0.2.dist-info/METADATA", + "size": 3975, + "perm": 420 + }, + { + "name": "MarkupSafe-3.0.2.dist-info/RECORD", + "size": 1188, + "perm": 420 + }, + { + "name": "MarkupSafe-3.0.2.dist-info/REQUESTED", + "size": 0, + "perm": 420 + }, + { + "name": "MarkupSafe-3.0.2.dist-info/WHEEL", + "size": 114, + "perm": 420 + }, + { + "name": "MarkupSafe-3.0.2.dist-info/top_level.txt", + "size": 11, + "perm": 420 + }, + { + "name": "PyYAML-6.0.2.dist-info", + "size": 0, + "perm": 2147484141 + }, + { + "name": "PyYAML-6.0.2.dist-info/INSTALLER", + "size": 4, + "perm": 420 + }, + { + "name": "PyYAML-6.0.2.dist-info/LICENSE", + "size": 1101, + "perm": 420 + }, + { + "name": "PyYAML-6.0.2.dist-info/METADATA", + "size": 2060, + "perm": 420 + }, + { + "name": "PyYAML-6.0.2.dist-info/RECORD", + "size": 2773, + "perm": 420 + }, + { + "name": "PyYAML-6.0.2.dist-info/REQUESTED", + "size": 0, + "perm": 420 + }, + { + "name": "PyYAML-6.0.2.dist-info/WHEEL", + "size": 111, + "perm": 420 + }, + { + "name": "PyYAML-6.0.2.dist-info/top_level.txt", + "size": 11, + "perm": 420 + }, + { + "name": "_yaml", + "size": 0, + "perm": 2147484141 + }, + { + "name": "_yaml/__init__.py", + "size": 1402, + "perm": 420 + }, + { + "name": "bin", + "size": 0, + "perm": 2147484141 + }, + { + "name": "bin/jsonpath_ng", + "size": 261, + "perm": 493 + }, + { + "name": "bin/slugify", + "size": 239, + "perm": 493 + }, + { + "name": "click", + "size": 0, + "perm": 2147484141 + }, + { + "name": "click-8.1.7.dist-info", + "size": 0, + "perm": 2147484141 + }, + { + "name": "click-8.1.7.dist-info/INSTALLER", + "size": 4, + "perm": 420 + }, + { + "name": "click-8.1.7.dist-info/LICENSE.rst", + "size": 1475, + "perm": 420 + }, + { + "name": "click-8.1.7.dist-info/METADATA", + "size": 3014, + "perm": 420 + }, + { + "name": "click-8.1.7.dist-info/RECORD", + "size": 2582, + "perm": 420 + }, + { + "name": "click-8.1.7.dist-info/REQUESTED", + "size": 0, + "perm": 420 + }, + { + "name": "click-8.1.7.dist-info/WHEEL", + "size": 92, + "perm": 420 + }, + { + "name": "click-8.1.7.dist-info/top_level.txt", + "size": 6, + "perm": 420 + }, + { + "name": "click/__init__.py", + "size": 3138, + "perm": 420 + }, + { + "name": "click/_compat.py", + "size": 18744, + "perm": 420 + }, + { + "name": "click/_termui_impl.py", + "size": 24069, + "perm": 420 + }, + { + "name": "click/_textwrap.py", + "size": 1353, + "perm": 420 + }, + { + "name": "click/_winconsole.py", + "size": 7860, + "perm": 420 + }, + { + "name": "click/core.py", + "size": 114086, + "perm": 420 + }, + { + "name": "click/decorators.py", + "size": 18719, + "perm": 420 + }, + { + "name": "click/exceptions.py", + "size": 9273, + "perm": 420 + }, + { + "name": "click/formatting.py", + "size": 9706, + "perm": 420 + }, + { + "name": "click/globals.py", + "size": 1961, + "perm": 420 + }, + { + "name": "click/parser.py", + "size": 19067, + "perm": 420 + }, + { + "name": "click/py.typed", + "size": 0, + "perm": 420 + }, + { + "name": "click/shell_completion.py", + "size": 18460, + "perm": 420 + }, + { + "name": "click/termui.py", + "size": 28324, + "perm": 420 + }, + { + "name": "click/testing.py", + "size": 16084, + "perm": 420 + }, + { + "name": "click/types.py", + "size": 36391, + "perm": 420 + }, + { + "name": "click/utils.py", + "size": 20298, + "perm": 420 + }, + { + "name": "jinja2", + "size": 0, + "perm": 2147484141 + }, + { + "name": "jinja2-3.1.4.dist-info", + "size": 0, + "perm": 2147484141 + }, + { + "name": "jinja2-3.1.4.dist-info/INSTALLER", + "size": 4, + "perm": 420 + }, + { + "name": "jinja2-3.1.4.dist-info/LICENSE.txt", + "size": 1475, + "perm": 420 + }, + { + "name": "jinja2-3.1.4.dist-info/METADATA", + "size": 2640, + "perm": 420 + }, + { + "name": "jinja2-3.1.4.dist-info/RECORD", + "size": 3697, + "perm": 420 + }, + { + "name": "jinja2-3.1.4.dist-info/REQUESTED", + "size": 0, + "perm": 420 + }, + { + "name": "jinja2-3.1.4.dist-info/WHEEL", + "size": 81, + "perm": 420 + }, + { + "name": "jinja2-3.1.4.dist-info/entry_points.txt", + "size": 58, + "perm": 420 + }, + { + "name": "jinja2/__init__.py", + "size": 1928, + "perm": 420 + }, + { + "name": "jinja2/_identifier.py", + "size": 1958, + "perm": 420 + }, + { + "name": "jinja2/async_utils.py", + "size": 2477, + "perm": 420 + }, + { + "name": "jinja2/bccache.py", + "size": 14061, + "perm": 420 + }, + { + "name": "jinja2/compiler.py", + "size": 72271, + "perm": 420 + }, + { + "name": "jinja2/constants.py", + "size": 1433, + "perm": 420 + }, + { + "name": "jinja2/debug.py", + "size": 6299, + "perm": 420 + }, + { + "name": "jinja2/defaults.py", + "size": 1267, + "perm": 420 + }, + { + "name": "jinja2/environment.py", + "size": 61538, + "perm": 420 + }, + { + "name": "jinja2/exceptions.py", + "size": 5071, + "perm": 420 + }, + { + "name": "jinja2/ext.py", + "size": 31877, + "perm": 420 + }, + { + "name": "jinja2/filters.py", + "size": 54611, + "perm": 420 + }, + { + "name": "jinja2/idtracking.py", + "size": 10704, + "perm": 420 + }, + { + "name": "jinja2/lexer.py", + "size": 29754, + "perm": 420 + }, + { + "name": "jinja2/loaders.py", + "size": 23167, + "perm": 420 + }, + { + "name": "jinja2/meta.py", + "size": 4397, + "perm": 420 + }, + { + "name": "jinja2/nativetypes.py", + "size": 4210, + "perm": 420 + }, + { + "name": "jinja2/nodes.py", + "size": 34579, + "perm": 420 + }, + { + "name": "jinja2/optimizer.py", + "size": 1651, + "perm": 420 + }, + { + "name": "jinja2/parser.py", + "size": 39890, + "perm": 420 + }, + { + "name": "jinja2/py.typed", + "size": 0, + "perm": 420 + }, + { + "name": "jinja2/runtime.py", + "size": 33435, + "perm": 420 + }, + { + "name": "jinja2/sandbox.py", + "size": 14616, + "perm": 420 + }, + { + "name": "jinja2/tests.py", + "size": 5926, + "perm": 420 + }, + { + "name": "jinja2/utils.py", + "size": 23952, + "perm": 420 + }, + { + "name": "jinja2/visitor.py", + "size": 3557, + "perm": 420 + }, + { + "name": "jsonpath_ng", + "size": 0, + "perm": 2147484141 + }, + { + "name": "jsonpath_ng-1.7.0.dist-info", + "size": 0, + "perm": 2147484141 + }, + { + "name": "jsonpath_ng-1.7.0.dist-info/INSTALLER", + "size": 4, + "perm": 420 + }, + { + "name": "jsonpath_ng-1.7.0.dist-info/LICENSE", + "size": 11358, + "perm": 420 + }, + { + "name": "jsonpath_ng-1.7.0.dist-info/METADATA", + "size": 18400, + "perm": 420 + }, + { + "name": "jsonpath_ng-1.7.0.dist-info/RECORD", + "size": 2549, + "perm": 420 + }, + { + "name": "jsonpath_ng-1.7.0.dist-info/REQUESTED", + "size": 0, + "perm": 420 + }, + { + "name": "jsonpath_ng-1.7.0.dist-info/WHEEL", + "size": 92, + "perm": 420 + }, + { + "name": "jsonpath_ng-1.7.0.dist-info/entry_points.txt", + "size": 70, + "perm": 420 + }, + { + "name": "jsonpath_ng-1.7.0.dist-info/top_level.txt", + "size": 12, + "perm": 420 + }, + { + "name": "jsonpath_ng/__init__.py", + "size": 116, + "perm": 420 + }, + { + "name": "jsonpath_ng/bin", + "size": 0, + "perm": 2147484141 + }, + { + "name": "jsonpath_ng/bin/__init__.py", + "size": 0, + "perm": 420 + }, + { + "name": "jsonpath_ng/bin/jsonpath.py", + "size": 2057, + "perm": 420 + }, + { + "name": "jsonpath_ng/exceptions.py", + "size": 146, + "perm": 420 + }, + { + "name": "jsonpath_ng/ext", + "size": 0, + "perm": 2147484141 + }, + { + "name": "jsonpath_ng/ext/__init__.py", + "size": 605, + "perm": 420 + }, + { + "name": "jsonpath_ng/ext/arithmetic.py", + "size": 2381, + "perm": 420 + }, + { + "name": "jsonpath_ng/ext/filter.py", + "size": 4312, + "perm": 420 + }, + { + "name": "jsonpath_ng/ext/iterable.py", + "size": 4302, + "perm": 420 + }, + { + "name": "jsonpath_ng/ext/parser.py", + "size": 5416, + "perm": 420 + }, + { + "name": "jsonpath_ng/ext/string.py", + "size": 4045, + "perm": 420 + }, + { + "name": "jsonpath_ng/jsonpath.py", + "size": 27219, + "perm": 420 + }, + { + "name": "jsonpath_ng/lexer.py", + "size": 5276, + "perm": 420 + }, + { + "name": "jsonpath_ng/parser.py", + "size": 6077, + "perm": 420 + }, + { + "name": "markupsafe", + "size": 0, + "perm": 2147484141 + }, + { + "name": "markupsafe/__init__.py", + "size": 13214, + "perm": 420 + }, + { + "name": "markupsafe/_native.py", + "size": 210, + "perm": 420 + }, + { + "name": "markupsafe/_speedups.c", + "size": 4149, + "perm": 420 + }, + { + "name": "markupsafe/_speedups.cpython-311-darwin.so", + "size": 67056, + "perm": 493, + "compressed": true + }, + { + "name": "markupsafe/_speedups.pyi", + "size": 41, + "perm": 420 + }, + { + "name": "markupsafe/py.typed", + "size": 0, + "perm": 420 + }, + { + "name": "ply", + "size": 0, + "perm": 2147484141 + }, + { + "name": "ply-3.11.dist-info", + "size": 0, + "perm": 2147484141 + }, + { + "name": "ply-3.11.dist-info/DESCRIPTION.rst", + "size": 519, + "perm": 420 + }, + { + "name": "ply-3.11.dist-info/INSTALLER", + "size": 4, + "perm": 420 + }, + { + "name": "ply-3.11.dist-info/METADATA", + "size": 844, + "perm": 420 + }, + { + "name": "ply-3.11.dist-info/RECORD", + "size": 1211, + "perm": 420 + }, + { + "name": "ply-3.11.dist-info/WHEEL", + "size": 110, + "perm": 420 + }, + { + "name": "ply-3.11.dist-info/metadata.json", + "size": 515, + "perm": 420 + }, + { + "name": "ply-3.11.dist-info/top_level.txt", + "size": 4, + "perm": 420 + }, + { + "name": "ply/__init__.py", + "size": 103, + "perm": 420 + }, + { + "name": "ply/cpp.py", + "size": 33639, + "perm": 420 + }, + { + "name": "ply/ctokens.py", + "size": 3155, + "perm": 420 + }, + { + "name": "ply/lex.py", + "size": 42905, + "perm": 420 + }, + { + "name": "ply/yacc.py", + "size": 137736, + "perm": 420 + }, + { + "name": "ply/ygen.py", + "size": 2246, + "perm": 420 + }, + { + "name": "python_slugify-8.0.4.dist-info", + "size": 0, + "perm": 2147484141 + }, + { + "name": "python_slugify-8.0.4.dist-info/INSTALLER", + "size": 4, + "perm": 420 + }, + { + "name": "python_slugify-8.0.4.dist-info/LICENSE", + "size": 1103, + "perm": 420 + }, + { + "name": "python_slugify-8.0.4.dist-info/METADATA", + "size": 8469, + "perm": 420 + }, + { + "name": "python_slugify-8.0.4.dist-info/RECORD", + "size": 1489, + "perm": 420 + }, + { + "name": "python_slugify-8.0.4.dist-info/REQUESTED", + "size": 0, + "perm": 420 + }, + { + "name": "python_slugify-8.0.4.dist-info/WHEEL", + "size": 110, + "perm": 420 + }, + { + "name": "python_slugify-8.0.4.dist-info/entry_points.txt", + "size": 50, + "perm": 420 + }, + { + "name": "python_slugify-8.0.4.dist-info/top_level.txt", + "size": 8, + "perm": 420 + }, + { + "name": "slugify", + "size": 0, + "perm": 2147484141 + }, + { + "name": "slugify/__init__.py", + "size": 346, + "perm": 420 + }, + { + "name": "slugify/__main__.py", + "size": 3961, + "perm": 420 + }, + { + "name": "slugify/__version__.py", + "size": 325, + "perm": 420 + }, + { + "name": "slugify/py.typed", + "size": 0, + "perm": 420 + }, + { + "name": "slugify/slugify.py", + "size": 6180, + "perm": 420 + }, + { + "name": "slugify/special.py", + "size": 1222, + "perm": 420 + }, + { + "name": "text_unidecode", + "size": 0, + "perm": 2147484141 + }, + { + "name": "text_unidecode-1.3.dist-info", + "size": 0, + "perm": 2147484141 + }, + { + "name": "text_unidecode-1.3.dist-info/DESCRIPTION.rst", + "size": 1199, + "perm": 420 + }, + { + "name": "text_unidecode-1.3.dist-info/INSTALLER", + "size": 4, + "perm": 420 + }, + { + "name": "text_unidecode-1.3.dist-info/LICENSE.txt", + "size": 6535, + "perm": 420 + }, + { + "name": "text_unidecode-1.3.dist-info/METADATA", + "size": 2422, + "perm": 420 + }, + { + "name": "text_unidecode-1.3.dist-info/RECORD", + "size": 937, + "perm": 420 + }, + { + "name": "text_unidecode-1.3.dist-info/WHEEL", + "size": 110, + "perm": 420 + }, + { + "name": "text_unidecode-1.3.dist-info/metadata.json", + "size": 1299, + "perm": 420 + }, + { + "name": "text_unidecode-1.3.dist-info/top_level.txt", + "size": 15, + "perm": 420 + }, + { + "name": "text_unidecode/__init__.py", + "size": 484, + "perm": 420 + }, + { + "name": "text_unidecode/data.bin", + "size": 311077, + "perm": 420, + "compressed": true + }, + { + "name": "yaml", + "size": 0, + "perm": 2147484141 + }, + { + "name": "yaml/__init__.py", + "size": 12311, + "perm": 420 + }, + { + "name": "yaml/_yaml.cpython-311-darwin.so", + "size": 397104, + "perm": 493, + "compressed": true + }, + { + "name": "yaml/composer.py", + "size": 4883, + "perm": 420 + }, + { + "name": "yaml/constructor.py", + "size": 28639, + "perm": 420 + }, + { + "name": "yaml/cyaml.py", + "size": 3851, + "perm": 420 + }, + { + "name": "yaml/dumper.py", + "size": 2837, + "perm": 420 + }, + { + "name": "yaml/emitter.py", + "size": 43006, + "perm": 420 + }, + { + "name": "yaml/error.py", + "size": 2533, + "perm": 420 + }, + { + "name": "yaml/events.py", + "size": 2445, + "perm": 420 + }, + { + "name": "yaml/loader.py", + "size": 2061, + "perm": 420 + }, + { + "name": "yaml/nodes.py", + "size": 1440, + "perm": 420 + }, + { + "name": "yaml/parser.py", + "size": 25495, + "perm": 420 + }, + { + "name": "yaml/reader.py", + "size": 6794, + "perm": 420 + }, + { + "name": "yaml/representer.py", + "size": 14190, + "perm": 420 + }, + { + "name": "yaml/resolver.py", + "size": 9004, + "perm": 420 + }, + { + "name": "yaml/scanner.py", + "size": 51279, + "perm": 420 + }, + { + "name": "yaml/serializer.py", + "size": 4165, + "perm": 420 + }, + { + "name": "yaml/tokens.py", + "size": 2573, + "perm": 420 + } + ] +} \ No newline at end of file diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/INSTALLER b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/LICENSE.txt b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/LICENSE.txt new file mode 100644 index 000000000..c37cae49e --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2007 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/METADATA b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/METADATA new file mode 100644 index 000000000..265cc32e1 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/METADATA @@ -0,0 +1,76 @@ +Metadata-Version: 2.1 +Name: Jinja2 +Version: 3.1.4 +Summary: A very fast and expressive template engine. +Maintainer-email: Pallets +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Text Processing :: Markup :: HTML +Classifier: Typing :: Typed +Requires-Dist: MarkupSafe>=2.0 +Requires-Dist: Babel>=2.7 ; extra == "i18n" +Project-URL: Changes, https://jinja.palletsprojects.com/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://jinja.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Source, https://github.com/pallets/jinja/ +Provides-Extra: i18n + +# Jinja + +Jinja is a fast, expressive, extensible templating engine. Special +placeholders in the template allow writing code similar to Python +syntax. Then the template is passed data to render the final document. + +It includes: + +- Template inheritance and inclusion. +- Define and import macros within templates. +- HTML templates can use autoescaping to prevent XSS from untrusted + user input. +- A sandboxed environment can safely render untrusted templates. +- AsyncIO support for generating templates and calling async + functions. +- I18N support with Babel. +- Templates are compiled to optimized Python code just-in-time and + cached, or can be compiled ahead-of-time. +- Exceptions point to the correct line in templates to make debugging + easier. +- Extensible filters, tests, functions, and even syntax. + +Jinja's philosophy is that while application logic belongs in Python if +possible, it shouldn't make the template designer's job difficult by +restricting functionality too much. + + +## In A Nutshell + +.. code-block:: jinja + + {% extends "base.html" %} + {% block title %}Members{% endblock %} + {% block content %} + + {% endblock %} + + +## Donate + +The Pallets organization develops and supports Jinja and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, [please +donate today][]. + +[please donate today]: https://palletsprojects.com/donate + diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/RECORD b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/RECORD new file mode 100644 index 000000000..e9bdca35c --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/RECORD @@ -0,0 +1,58 @@ +jinja2-3.1.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jinja2-3.1.4.dist-info/LICENSE.txt,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475 +jinja2-3.1.4.dist-info/METADATA,sha256=R_brzpPQVBvpGcsm-WbrtgotO7suQ1D0F-qkhTzeEfY,2640 +jinja2-3.1.4.dist-info/RECORD,, +jinja2-3.1.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jinja2-3.1.4.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81 +jinja2-3.1.4.dist-info/entry_points.txt,sha256=OL85gYU1eD8cuPlikifFngXpeBjaxl6rIJ8KkC_3r-I,58 +jinja2/__init__.py,sha256=wIl45IM20KGw-kfr7jJhaBxxX5g4-kihlBYjxopX7Pw,1928 +jinja2/__pycache__/__init__.cpython-311.pyc,, +jinja2/__pycache__/_identifier.cpython-311.pyc,, +jinja2/__pycache__/async_utils.cpython-311.pyc,, +jinja2/__pycache__/bccache.cpython-311.pyc,, +jinja2/__pycache__/compiler.cpython-311.pyc,, +jinja2/__pycache__/constants.cpython-311.pyc,, +jinja2/__pycache__/debug.cpython-311.pyc,, +jinja2/__pycache__/defaults.cpython-311.pyc,, +jinja2/__pycache__/environment.cpython-311.pyc,, +jinja2/__pycache__/exceptions.cpython-311.pyc,, +jinja2/__pycache__/ext.cpython-311.pyc,, +jinja2/__pycache__/filters.cpython-311.pyc,, +jinja2/__pycache__/idtracking.cpython-311.pyc,, +jinja2/__pycache__/lexer.cpython-311.pyc,, +jinja2/__pycache__/loaders.cpython-311.pyc,, +jinja2/__pycache__/meta.cpython-311.pyc,, +jinja2/__pycache__/nativetypes.cpython-311.pyc,, +jinja2/__pycache__/nodes.cpython-311.pyc,, +jinja2/__pycache__/optimizer.cpython-311.pyc,, +jinja2/__pycache__/parser.cpython-311.pyc,, +jinja2/__pycache__/runtime.cpython-311.pyc,, +jinja2/__pycache__/sandbox.cpython-311.pyc,, +jinja2/__pycache__/tests.cpython-311.pyc,, +jinja2/__pycache__/utils.cpython-311.pyc,, +jinja2/__pycache__/visitor.cpython-311.pyc,, +jinja2/_identifier.py,sha256=_zYctNKzRqlk_murTNlzrju1FFJL7Va_Ijqqd7ii2lU,1958 +jinja2/async_utils.py,sha256=JXKWCAXmTx0iZB4-hAsF50vgjxw_RJTjiLOlGGTBso0,2477 +jinja2/bccache.py,sha256=gh0qs9rulnXo0PhX5jTJy2UHzI8wFnQ63o_vw7nhzRg,14061 +jinja2/compiler.py,sha256=dpV-n6_iQUP4uSwlXwGUavJmwjvXdyxKzJ-AonFjPBk,72271 +jinja2/constants.py,sha256=GMoFydBF_kdpaRKPoM5cl5MviquVRLVyZtfp5-16jg0,1433 +jinja2/debug.py,sha256=iWJ432RadxJNnaMOPrjIDInz50UEgni3_HKuFXi2vuQ,6299 +jinja2/defaults.py,sha256=boBcSw78h-lp20YbaXSJsqkAI2uN_mD_TtCydpeq5wU,1267 +jinja2/environment.py,sha256=xhFkmxO0CESA76Ki5tz4XWq9yzGu-t0p93JCCVBVNps,61538 +jinja2/exceptions.py,sha256=ioHeHrWwCWNaXX1inHmHVblvc4haO7AXsjCp3GfWvx0,5071 +jinja2/ext.py,sha256=igsBH7c6C0byHaOtMbE-ugpt4GjLGgR-ywskyXtKgq8,31877 +jinja2/filters.py,sha256=bKeqjFjjz88TkHVLSyyMIEB75CzAN6b3Airgx0phJDg,54611 +jinja2/idtracking.py,sha256=GfNmadir4oDALVxzn3DL9YInhJDr69ebXeA2ygfuCGA,10704 +jinja2/lexer.py,sha256=xnWWXhPndHFsoqzpc5VTjheDE9JuKk9MUo9DZkrM8Os,29754 +jinja2/loaders.py,sha256=ru0GIWHo5KiHJi7_MoI_LvGDoBBvP6rd0hiC1ReaTwk,23167 +jinja2/meta.py,sha256=OTDPkaFvU2Hgvx-6akz7154F8BIWaRmvJcBFvwopHww,4397 +jinja2/nativetypes.py,sha256=7GIGALVJgdyL80oZJdQUaUfwSt5q2lSSZbXt0dNf_M4,4210 +jinja2/nodes.py,sha256=m1Duzcr6qhZI8JQ6VyJgUNinjAf5bQzijSmDnMsvUx8,34579 +jinja2/optimizer.py,sha256=rJnCRlQ7pZsEEmMhsQDgC_pKyDHxP5TPS6zVPGsgcu8,1651 +jinja2/parser.py,sha256=DV1iF1FR2Rsaj_5zl8rmx7j6Bj4S8iLHoYsvJ0bfEis,39890 +jinja2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jinja2/runtime.py,sha256=POXT3tKNKJRENx2CymwUsOOXH2JwGPjW702njB5__cQ,33435 +jinja2/sandbox.py,sha256=TJjBNS9qRJ2ZgBMWdAgRBpyDLOHea2kT-2mk4PrjYx0,14616 +jinja2/tests.py,sha256=VLsBhVFnWg-PxSBz1MhRnNWgP1ovXk3neO1FLQMeC9Q,5926 +jinja2/utils.py,sha256=nV7IpWLvRCMyHW1irBAK8CIPAnOFfkb2ukggDBjbBEY,23952 +jinja2/visitor.py,sha256=EcnL1PIwf_4RVCOMxsRNuR8AXHbS1qfAdMOE2ngKJz4,3557 diff --git a/pkg/python/embed/python-linux-arm64.dummy b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/REQUESTED similarity index 100% rename from pkg/python/embed/python-linux-arm64.dummy rename to lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/REQUESTED diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/WHEEL b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/WHEEL new file mode 100644 index 000000000..3b5e64b5e --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.9.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/entry_points.txt b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/entry_points.txt new file mode 100644 index 000000000..abc3eae3b --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2-3.1.4.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[babel.extractors] +jinja2=jinja2.ext:babel_extract[i18n] + diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2/__init__.py b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/__init__.py new file mode 100644 index 000000000..2f0b5b286 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/__init__.py @@ -0,0 +1,38 @@ +"""Jinja is a template engine written in pure Python. It provides a +non-XML syntax that supports inline expressions and an optional +sandboxed environment. +""" + +from .bccache import BytecodeCache as BytecodeCache +from .bccache import FileSystemBytecodeCache as FileSystemBytecodeCache +from .bccache import MemcachedBytecodeCache as MemcachedBytecodeCache +from .environment import Environment as Environment +from .environment import Template as Template +from .exceptions import TemplateAssertionError as TemplateAssertionError +from .exceptions import TemplateError as TemplateError +from .exceptions import TemplateNotFound as TemplateNotFound +from .exceptions import TemplateRuntimeError as TemplateRuntimeError +from .exceptions import TemplatesNotFound as TemplatesNotFound +from .exceptions import TemplateSyntaxError as TemplateSyntaxError +from .exceptions import UndefinedError as UndefinedError +from .loaders import BaseLoader as BaseLoader +from .loaders import ChoiceLoader as ChoiceLoader +from .loaders import DictLoader as DictLoader +from .loaders import FileSystemLoader as FileSystemLoader +from .loaders import FunctionLoader as FunctionLoader +from .loaders import ModuleLoader as ModuleLoader +from .loaders import PackageLoader as PackageLoader +from .loaders import PrefixLoader as PrefixLoader +from .runtime import ChainableUndefined as ChainableUndefined +from .runtime import DebugUndefined as DebugUndefined +from .runtime import make_logging_undefined as make_logging_undefined +from .runtime import StrictUndefined as StrictUndefined +from .runtime import Undefined as Undefined +from .utils import clear_caches as clear_caches +from .utils import is_undefined as is_undefined +from .utils import pass_context as pass_context +from .utils import pass_environment as pass_environment +from .utils import pass_eval_context as pass_eval_context +from .utils import select_autoescape as select_autoescape + +__version__ = "3.1.4" diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2/_identifier.py b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/_identifier.py new file mode 100644 index 000000000..928c1503c --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/_identifier.py @@ -0,0 +1,6 @@ +import re + +# generated by scripts/generate_identifier_pattern.py +pattern = re.compile( + r"[\w·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߽߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛࣓-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣ৾ਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣૺ-૿ଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఄా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഀ-ഃ഻഼ാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳷-᳹᷀-᷹᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꣿꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𐴤-𐽆𐴧-𐽐𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑄴𑅅𑅆𑅳𑆀-𑆂𑆳-𑇀𑇉-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌻𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑑞𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑠬-𑠺𑨁-𑨊𑨳-𑨹𑨻-𑨾𑩇𑩑-𑩛𑪊-𑪙𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𑴱-𑴶𑴺𑴼𑴽𑴿-𑵅𑵇𑶊-𑶎𑶐𑶑𑶓-𑶗𑻳-𑻶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯]+" # noqa: B950 +) diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2/async_utils.py b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/async_utils.py new file mode 100644 index 000000000..e65219e49 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/async_utils.py @@ -0,0 +1,84 @@ +import inspect +import typing as t +from functools import WRAPPER_ASSIGNMENTS +from functools import wraps + +from .utils import _PassArg +from .utils import pass_eval_context + +V = t.TypeVar("V") + + +def async_variant(normal_func): # type: ignore + def decorator(async_func): # type: ignore + pass_arg = _PassArg.from_obj(normal_func) + need_eval_context = pass_arg is None + + if pass_arg is _PassArg.environment: + + def is_async(args: t.Any) -> bool: + return t.cast(bool, args[0].is_async) + + else: + + def is_async(args: t.Any) -> bool: + return t.cast(bool, args[0].environment.is_async) + + # Take the doc and annotations from the sync function, but the + # name from the async function. Pallets-Sphinx-Themes + # build_function_directive expects __wrapped__ to point to the + # sync function. + async_func_attrs = ("__module__", "__name__", "__qualname__") + normal_func_attrs = tuple(set(WRAPPER_ASSIGNMENTS).difference(async_func_attrs)) + + @wraps(normal_func, assigned=normal_func_attrs) + @wraps(async_func, assigned=async_func_attrs, updated=()) + def wrapper(*args, **kwargs): # type: ignore + b = is_async(args) + + if need_eval_context: + args = args[1:] + + if b: + return async_func(*args, **kwargs) + + return normal_func(*args, **kwargs) + + if need_eval_context: + wrapper = pass_eval_context(wrapper) + + wrapper.jinja_async_variant = True # type: ignore[attr-defined] + return wrapper + + return decorator + + +_common_primitives = {int, float, bool, str, list, dict, tuple, type(None)} + + +async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V": + # Avoid a costly call to isawaitable + if type(value) in _common_primitives: + return t.cast("V", value) + + if inspect.isawaitable(value): + return await t.cast("t.Awaitable[V]", value) + + return t.cast("V", value) + + +async def auto_aiter( + iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", +) -> "t.AsyncIterator[V]": + if hasattr(iterable, "__aiter__"): + async for item in t.cast("t.AsyncIterable[V]", iterable): + yield item + else: + for item in iterable: + yield item + + +async def auto_to_list( + value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", +) -> t.List["V"]: + return [x async for x in auto_aiter(value)] diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2/bccache.py b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/bccache.py new file mode 100644 index 000000000..ada8b099f --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/bccache.py @@ -0,0 +1,408 @@ +"""The optional bytecode cache system. This is useful if you have very +complex template situations and the compilation of all those templates +slows down your application too much. + +Situations where this is useful are often forking web applications that +are initialized on the first request. +""" + +import errno +import fnmatch +import marshal +import os +import pickle +import stat +import sys +import tempfile +import typing as t +from hashlib import sha1 +from io import BytesIO +from types import CodeType + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .environment import Environment + + class _MemcachedClient(te.Protocol): + def get(self, key: str) -> bytes: ... + + def set( + self, key: str, value: bytes, timeout: t.Optional[int] = None + ) -> None: ... + + +bc_version = 5 +# Magic bytes to identify Jinja bytecode cache files. Contains the +# Python major and minor version to avoid loading incompatible bytecode +# if a project upgrades its Python version. +bc_magic = ( + b"j2" + + pickle.dumps(bc_version, 2) + + pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1], 2) +) + + +class Bucket: + """Buckets are used to store the bytecode for one template. It's created + and initialized by the bytecode cache and passed to the loading functions. + + The buckets get an internal checksum from the cache assigned and use this + to automatically reject outdated cache material. Individual bytecode + cache subclasses don't have to care about cache invalidation. + """ + + def __init__(self, environment: "Environment", key: str, checksum: str) -> None: + self.environment = environment + self.key = key + self.checksum = checksum + self.reset() + + def reset(self) -> None: + """Resets the bucket (unloads the bytecode).""" + self.code: t.Optional[CodeType] = None + + def load_bytecode(self, f: t.BinaryIO) -> None: + """Loads bytecode from a file or file like object.""" + # make sure the magic header is correct + magic = f.read(len(bc_magic)) + if magic != bc_magic: + self.reset() + return + # the source code of the file changed, we need to reload + checksum = pickle.load(f) + if self.checksum != checksum: + self.reset() + return + # if marshal_load fails then we need to reload + try: + self.code = marshal.load(f) + except (EOFError, ValueError, TypeError): + self.reset() + return + + def write_bytecode(self, f: t.IO[bytes]) -> None: + """Dump the bytecode into the file or file like object passed.""" + if self.code is None: + raise TypeError("can't write empty bucket") + f.write(bc_magic) + pickle.dump(self.checksum, f, 2) + marshal.dump(self.code, f) + + def bytecode_from_string(self, string: bytes) -> None: + """Load bytecode from bytes.""" + self.load_bytecode(BytesIO(string)) + + def bytecode_to_string(self) -> bytes: + """Return the bytecode as bytes.""" + out = BytesIO() + self.write_bytecode(out) + return out.getvalue() + + +class BytecodeCache: + """To implement your own bytecode cache you have to subclass this class + and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of + these methods are passed a :class:`~jinja2.bccache.Bucket`. + + A very basic bytecode cache that saves the bytecode on the file system:: + + from os import path + + class MyCache(BytecodeCache): + + def __init__(self, directory): + self.directory = directory + + def load_bytecode(self, bucket): + filename = path.join(self.directory, bucket.key) + if path.exists(filename): + with open(filename, 'rb') as f: + bucket.load_bytecode(f) + + def dump_bytecode(self, bucket): + filename = path.join(self.directory, bucket.key) + with open(filename, 'wb') as f: + bucket.write_bytecode(f) + + A more advanced version of a filesystem based bytecode cache is part of + Jinja. + """ + + def load_bytecode(self, bucket: Bucket) -> None: + """Subclasses have to override this method to load bytecode into a + bucket. If they are not able to find code in the cache for the + bucket, it must not do anything. + """ + raise NotImplementedError() + + def dump_bytecode(self, bucket: Bucket) -> None: + """Subclasses have to override this method to write the bytecode + from a bucket back to the cache. If it unable to do so it must not + fail silently but raise an exception. + """ + raise NotImplementedError() + + def clear(self) -> None: + """Clears the cache. This method is not used by Jinja but should be + implemented to allow applications to clear the bytecode cache used + by a particular environment. + """ + + def get_cache_key( + self, name: str, filename: t.Optional[t.Union[str]] = None + ) -> str: + """Returns the unique hash key for this template name.""" + hash = sha1(name.encode("utf-8")) + + if filename is not None: + hash.update(f"|{filename}".encode()) + + return hash.hexdigest() + + def get_source_checksum(self, source: str) -> str: + """Returns a checksum for the source.""" + return sha1(source.encode("utf-8")).hexdigest() + + def get_bucket( + self, + environment: "Environment", + name: str, + filename: t.Optional[str], + source: str, + ) -> Bucket: + """Return a cache bucket for the given template. All arguments are + mandatory but filename may be `None`. + """ + key = self.get_cache_key(name, filename) + checksum = self.get_source_checksum(source) + bucket = Bucket(environment, key, checksum) + self.load_bytecode(bucket) + return bucket + + def set_bucket(self, bucket: Bucket) -> None: + """Put the bucket into the cache.""" + self.dump_bytecode(bucket) + + +class FileSystemBytecodeCache(BytecodeCache): + """A bytecode cache that stores bytecode on the filesystem. It accepts + two arguments: The directory where the cache items are stored and a + pattern string that is used to build the filename. + + If no directory is specified a default cache directory is selected. On + Windows the user's temp directory is used, on UNIX systems a directory + is created for the user in the system temp directory. + + The pattern can be used to have multiple separate caches operate on the + same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s`` + is replaced with the cache key. + + >>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache') + + This bytecode cache supports clearing of the cache using the clear method. + """ + + def __init__( + self, directory: t.Optional[str] = None, pattern: str = "__jinja2_%s.cache" + ) -> None: + if directory is None: + directory = self._get_default_cache_dir() + self.directory = directory + self.pattern = pattern + + def _get_default_cache_dir(self) -> str: + def _unsafe_dir() -> "te.NoReturn": + raise RuntimeError( + "Cannot determine safe temp directory. You " + "need to explicitly provide one." + ) + + tmpdir = tempfile.gettempdir() + + # On windows the temporary directory is used specific unless + # explicitly forced otherwise. We can just use that. + if os.name == "nt": + return tmpdir + if not hasattr(os, "getuid"): + _unsafe_dir() + + dirname = f"_jinja2-cache-{os.getuid()}" + actual_dir = os.path.join(tmpdir, dirname) + + try: + os.mkdir(actual_dir, stat.S_IRWXU) + except OSError as e: + if e.errno != errno.EEXIST: + raise + try: + os.chmod(actual_dir, stat.S_IRWXU) + actual_dir_stat = os.lstat(actual_dir) + if ( + actual_dir_stat.st_uid != os.getuid() + or not stat.S_ISDIR(actual_dir_stat.st_mode) + or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU + ): + _unsafe_dir() + except OSError as e: + if e.errno != errno.EEXIST: + raise + + actual_dir_stat = os.lstat(actual_dir) + if ( + actual_dir_stat.st_uid != os.getuid() + or not stat.S_ISDIR(actual_dir_stat.st_mode) + or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU + ): + _unsafe_dir() + + return actual_dir + + def _get_cache_filename(self, bucket: Bucket) -> str: + return os.path.join(self.directory, self.pattern % (bucket.key,)) + + def load_bytecode(self, bucket: Bucket) -> None: + filename = self._get_cache_filename(bucket) + + # Don't test for existence before opening the file, since the + # file could disappear after the test before the open. + try: + f = open(filename, "rb") + except (FileNotFoundError, IsADirectoryError, PermissionError): + # PermissionError can occur on Windows when an operation is + # in progress, such as calling clear(). + return + + with f: + bucket.load_bytecode(f) + + def dump_bytecode(self, bucket: Bucket) -> None: + # Write to a temporary file, then rename to the real name after + # writing. This avoids another process reading the file before + # it is fully written. + name = self._get_cache_filename(bucket) + f = tempfile.NamedTemporaryFile( + mode="wb", + dir=os.path.dirname(name), + prefix=os.path.basename(name), + suffix=".tmp", + delete=False, + ) + + def remove_silent() -> None: + try: + os.remove(f.name) + except OSError: + # Another process may have called clear(). On Windows, + # another program may be holding the file open. + pass + + try: + with f: + bucket.write_bytecode(f) + except BaseException: + remove_silent() + raise + + try: + os.replace(f.name, name) + except OSError: + # Another process may have called clear(). On Windows, + # another program may be holding the file open. + remove_silent() + except BaseException: + remove_silent() + raise + + def clear(self) -> None: + # imported lazily here because google app-engine doesn't support + # write access on the file system and the function does not exist + # normally. + from os import remove + + files = fnmatch.filter(os.listdir(self.directory), self.pattern % ("*",)) + for filename in files: + try: + remove(os.path.join(self.directory, filename)) + except OSError: + pass + + +class MemcachedBytecodeCache(BytecodeCache): + """This class implements a bytecode cache that uses a memcache cache for + storing the information. It does not enforce a specific memcache library + (tummy's memcache or cmemcache) but will accept any class that provides + the minimal interface required. + + Libraries compatible with this class: + + - `cachelib `_ + - `python-memcached `_ + + (Unfortunately the django cache interface is not compatible because it + does not support storing binary data, only text. You can however pass + the underlying cache client to the bytecode cache which is available + as `django.core.cache.cache._client`.) + + The minimal interface for the client passed to the constructor is this: + + .. class:: MinimalClientInterface + + .. method:: set(key, value[, timeout]) + + Stores the bytecode in the cache. `value` is a string and + `timeout` the timeout of the key. If timeout is not provided + a default timeout or no timeout should be assumed, if it's + provided it's an integer with the number of seconds the cache + item should exist. + + .. method:: get(key) + + Returns the value for the cache key. If the item does not + exist in the cache the return value must be `None`. + + The other arguments to the constructor are the prefix for all keys that + is added before the actual cache key and the timeout for the bytecode in + the cache system. We recommend a high (or no) timeout. + + This bytecode cache does not support clearing of used items in the cache. + The clear method is a no-operation function. + + .. versionadded:: 2.7 + Added support for ignoring memcache errors through the + `ignore_memcache_errors` parameter. + """ + + def __init__( + self, + client: "_MemcachedClient", + prefix: str = "jinja2/bytecode/", + timeout: t.Optional[int] = None, + ignore_memcache_errors: bool = True, + ): + self.client = client + self.prefix = prefix + self.timeout = timeout + self.ignore_memcache_errors = ignore_memcache_errors + + def load_bytecode(self, bucket: Bucket) -> None: + try: + code = self.client.get(self.prefix + bucket.key) + except Exception: + if not self.ignore_memcache_errors: + raise + else: + bucket.bytecode_from_string(code) + + def dump_bytecode(self, bucket: Bucket) -> None: + key = self.prefix + bucket.key + value = bucket.bytecode_to_string() + + try: + if self.timeout is not None: + self.client.set(key, value, self.timeout) + else: + self.client.set(key, value) + except Exception: + if not self.ignore_memcache_errors: + raise diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2/compiler.py b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/compiler.py new file mode 100644 index 000000000..274071750 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/compiler.py @@ -0,0 +1,1960 @@ +"""Compiles nodes from the parser into Python code.""" + +import typing as t +from contextlib import contextmanager +from functools import update_wrapper +from io import StringIO +from itertools import chain +from keyword import iskeyword as is_python_keyword + +from markupsafe import escape +from markupsafe import Markup + +from . import nodes +from .exceptions import TemplateAssertionError +from .idtracking import Symbols +from .idtracking import VAR_LOAD_ALIAS +from .idtracking import VAR_LOAD_PARAMETER +from .idtracking import VAR_LOAD_RESOLVE +from .idtracking import VAR_LOAD_UNDEFINED +from .nodes import EvalContext +from .optimizer import Optimizer +from .utils import _PassArg +from .utils import concat +from .visitor import NodeVisitor + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .environment import Environment + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + +operators = { + "eq": "==", + "ne": "!=", + "gt": ">", + "gteq": ">=", + "lt": "<", + "lteq": "<=", + "in": "in", + "notin": "not in", +} + + +def optimizeconst(f: F) -> F: + def new_func( + self: "CodeGenerator", node: nodes.Expr, frame: "Frame", **kwargs: t.Any + ) -> t.Any: + # Only optimize if the frame is not volatile + if self.optimizer is not None and not frame.eval_ctx.volatile: + new_node = self.optimizer.visit(node, frame.eval_ctx) + + if new_node != node: + return self.visit(new_node, frame) + + return f(self, node, frame, **kwargs) + + return update_wrapper(t.cast(F, new_func), f) + + +def _make_binop(op: str) -> t.Callable[["CodeGenerator", nodes.BinExpr, "Frame"], None]: + @optimizeconst + def visitor(self: "CodeGenerator", node: nodes.BinExpr, frame: Frame) -> None: + if ( + self.environment.sandboxed and op in self.environment.intercepted_binops # type: ignore + ): + self.write(f"environment.call_binop(context, {op!r}, ") + self.visit(node.left, frame) + self.write(", ") + self.visit(node.right, frame) + else: + self.write("(") + self.visit(node.left, frame) + self.write(f" {op} ") + self.visit(node.right, frame) + + self.write(")") + + return visitor + + +def _make_unop( + op: str, +) -> t.Callable[["CodeGenerator", nodes.UnaryExpr, "Frame"], None]: + @optimizeconst + def visitor(self: "CodeGenerator", node: nodes.UnaryExpr, frame: Frame) -> None: + if ( + self.environment.sandboxed and op in self.environment.intercepted_unops # type: ignore + ): + self.write(f"environment.call_unop(context, {op!r}, ") + self.visit(node.node, frame) + else: + self.write("(" + op) + self.visit(node.node, frame) + + self.write(")") + + return visitor + + +def generate( + node: nodes.Template, + environment: "Environment", + name: t.Optional[str], + filename: t.Optional[str], + stream: t.Optional[t.TextIO] = None, + defer_init: bool = False, + optimized: bool = True, +) -> t.Optional[str]: + """Generate the python source for a node tree.""" + if not isinstance(node, nodes.Template): + raise TypeError("Can't compile non template nodes") + + generator = environment.code_generator_class( + environment, name, filename, stream, defer_init, optimized + ) + generator.visit(node) + + if stream is None: + return generator.stream.getvalue() # type: ignore + + return None + + +def has_safe_repr(value: t.Any) -> bool: + """Does the node have a safe representation?""" + if value is None or value is NotImplemented or value is Ellipsis: + return True + + if type(value) in {bool, int, float, complex, range, str, Markup}: + return True + + if type(value) in {tuple, list, set, frozenset}: + return all(has_safe_repr(v) for v in value) + + if type(value) is dict: # noqa E721 + return all(has_safe_repr(k) and has_safe_repr(v) for k, v in value.items()) + + return False + + +def find_undeclared( + nodes: t.Iterable[nodes.Node], names: t.Iterable[str] +) -> t.Set[str]: + """Check if the names passed are accessed undeclared. The return value + is a set of all the undeclared names from the sequence of names found. + """ + visitor = UndeclaredNameVisitor(names) + try: + for node in nodes: + visitor.visit(node) + except VisitorExit: + pass + return visitor.undeclared + + +class MacroRef: + def __init__(self, node: t.Union[nodes.Macro, nodes.CallBlock]) -> None: + self.node = node + self.accesses_caller = False + self.accesses_kwargs = False + self.accesses_varargs = False + + +class Frame: + """Holds compile time information for us.""" + + def __init__( + self, + eval_ctx: EvalContext, + parent: t.Optional["Frame"] = None, + level: t.Optional[int] = None, + ) -> None: + self.eval_ctx = eval_ctx + + # the parent of this frame + self.parent = parent + + if parent is None: + self.symbols = Symbols(level=level) + + # in some dynamic inheritance situations the compiler needs to add + # write tests around output statements. + self.require_output_check = False + + # inside some tags we are using a buffer rather than yield statements. + # this for example affects {% filter %} or {% macro %}. If a frame + # is buffered this variable points to the name of the list used as + # buffer. + self.buffer: t.Optional[str] = None + + # the name of the block we're in, otherwise None. + self.block: t.Optional[str] = None + + else: + self.symbols = Symbols(parent.symbols, level=level) + self.require_output_check = parent.require_output_check + self.buffer = parent.buffer + self.block = parent.block + + # a toplevel frame is the root + soft frames such as if conditions. + self.toplevel = False + + # the root frame is basically just the outermost frame, so no if + # conditions. This information is used to optimize inheritance + # situations. + self.rootlevel = False + + # variables set inside of loops and blocks should not affect outer frames, + # but they still needs to be kept track of as part of the active context. + self.loop_frame = False + self.block_frame = False + + # track whether the frame is being used in an if-statement or conditional + # expression as it determines which errors should be raised during runtime + # or compile time. + self.soft_frame = False + + def copy(self) -> "Frame": + """Create a copy of the current one.""" + rv = object.__new__(self.__class__) + rv.__dict__.update(self.__dict__) + rv.symbols = self.symbols.copy() + return rv + + def inner(self, isolated: bool = False) -> "Frame": + """Return an inner frame.""" + if isolated: + return Frame(self.eval_ctx, level=self.symbols.level + 1) + return Frame(self.eval_ctx, self) + + def soft(self) -> "Frame": + """Return a soft frame. A soft frame may not be modified as + standalone thing as it shares the resources with the frame it + was created of, but it's not a rootlevel frame any longer. + + This is only used to implement if-statements and conditional + expressions. + """ + rv = self.copy() + rv.rootlevel = False + rv.soft_frame = True + return rv + + __copy__ = copy + + +class VisitorExit(RuntimeError): + """Exception used by the `UndeclaredNameVisitor` to signal a stop.""" + + +class DependencyFinderVisitor(NodeVisitor): + """A visitor that collects filter and test calls.""" + + def __init__(self) -> None: + self.filters: t.Set[str] = set() + self.tests: t.Set[str] = set() + + def visit_Filter(self, node: nodes.Filter) -> None: + self.generic_visit(node) + self.filters.add(node.name) + + def visit_Test(self, node: nodes.Test) -> None: + self.generic_visit(node) + self.tests.add(node.name) + + def visit_Block(self, node: nodes.Block) -> None: + """Stop visiting at blocks.""" + + +class UndeclaredNameVisitor(NodeVisitor): + """A visitor that checks if a name is accessed without being + declared. This is different from the frame visitor as it will + not stop at closure frames. + """ + + def __init__(self, names: t.Iterable[str]) -> None: + self.names = set(names) + self.undeclared: t.Set[str] = set() + + def visit_Name(self, node: nodes.Name) -> None: + if node.ctx == "load" and node.name in self.names: + self.undeclared.add(node.name) + if self.undeclared == self.names: + raise VisitorExit() + else: + self.names.discard(node.name) + + def visit_Block(self, node: nodes.Block) -> None: + """Stop visiting a blocks.""" + + +class CompilerExit(Exception): + """Raised if the compiler encountered a situation where it just + doesn't make sense to further process the code. Any block that + raises such an exception is not further processed. + """ + + +class CodeGenerator(NodeVisitor): + def __init__( + self, + environment: "Environment", + name: t.Optional[str], + filename: t.Optional[str], + stream: t.Optional[t.TextIO] = None, + defer_init: bool = False, + optimized: bool = True, + ) -> None: + if stream is None: + stream = StringIO() + self.environment = environment + self.name = name + self.filename = filename + self.stream = stream + self.created_block_context = False + self.defer_init = defer_init + self.optimizer: t.Optional[Optimizer] = None + + if optimized: + self.optimizer = Optimizer(environment) + + # aliases for imports + self.import_aliases: t.Dict[str, str] = {} + + # a registry for all blocks. Because blocks are moved out + # into the global python scope they are registered here + self.blocks: t.Dict[str, nodes.Block] = {} + + # the number of extends statements so far + self.extends_so_far = 0 + + # some templates have a rootlevel extends. In this case we + # can safely assume that we're a child template and do some + # more optimizations. + self.has_known_extends = False + + # the current line number + self.code_lineno = 1 + + # registry of all filters and tests (global, not block local) + self.tests: t.Dict[str, str] = {} + self.filters: t.Dict[str, str] = {} + + # the debug information + self.debug_info: t.List[t.Tuple[int, int]] = [] + self._write_debug_info: t.Optional[int] = None + + # the number of new lines before the next write() + self._new_lines = 0 + + # the line number of the last written statement + self._last_line = 0 + + # true if nothing was written so far. + self._first_write = True + + # used by the `temporary_identifier` method to get new + # unique, temporary identifier + self._last_identifier = 0 + + # the current indentation + self._indentation = 0 + + # Tracks toplevel assignments + self._assign_stack: t.List[t.Set[str]] = [] + + # Tracks parameter definition blocks + self._param_def_block: t.List[t.Set[str]] = [] + + # Tracks the current context. + self._context_reference_stack = ["context"] + + @property + def optimized(self) -> bool: + return self.optimizer is not None + + # -- Various compilation helpers + + def fail(self, msg: str, lineno: int) -> "te.NoReturn": + """Fail with a :exc:`TemplateAssertionError`.""" + raise TemplateAssertionError(msg, lineno, self.name, self.filename) + + def temporary_identifier(self) -> str: + """Get a new unique identifier.""" + self._last_identifier += 1 + return f"t_{self._last_identifier}" + + def buffer(self, frame: Frame) -> None: + """Enable buffering for the frame from that point onwards.""" + frame.buffer = self.temporary_identifier() + self.writeline(f"{frame.buffer} = []") + + def return_buffer_contents( + self, frame: Frame, force_unescaped: bool = False + ) -> None: + """Return the buffer contents of the frame.""" + if not force_unescaped: + if frame.eval_ctx.volatile: + self.writeline("if context.eval_ctx.autoescape:") + self.indent() + self.writeline(f"return Markup(concat({frame.buffer}))") + self.outdent() + self.writeline("else:") + self.indent() + self.writeline(f"return concat({frame.buffer})") + self.outdent() + return + elif frame.eval_ctx.autoescape: + self.writeline(f"return Markup(concat({frame.buffer}))") + return + self.writeline(f"return concat({frame.buffer})") + + def indent(self) -> None: + """Indent by one.""" + self._indentation += 1 + + def outdent(self, step: int = 1) -> None: + """Outdent by step.""" + self._indentation -= step + + def start_write(self, frame: Frame, node: t.Optional[nodes.Node] = None) -> None: + """Yield or write into the frame buffer.""" + if frame.buffer is None: + self.writeline("yield ", node) + else: + self.writeline(f"{frame.buffer}.append(", node) + + def end_write(self, frame: Frame) -> None: + """End the writing process started by `start_write`.""" + if frame.buffer is not None: + self.write(")") + + def simple_write( + self, s: str, frame: Frame, node: t.Optional[nodes.Node] = None + ) -> None: + """Simple shortcut for start_write + write + end_write.""" + self.start_write(frame, node) + self.write(s) + self.end_write(frame) + + def blockvisit(self, nodes: t.Iterable[nodes.Node], frame: Frame) -> None: + """Visit a list of nodes as block in a frame. If the current frame + is no buffer a dummy ``if 0: yield None`` is written automatically. + """ + try: + self.writeline("pass") + for node in nodes: + self.visit(node, frame) + except CompilerExit: + pass + + def write(self, x: str) -> None: + """Write a string into the output stream.""" + if self._new_lines: + if not self._first_write: + self.stream.write("\n" * self._new_lines) + self.code_lineno += self._new_lines + if self._write_debug_info is not None: + self.debug_info.append((self._write_debug_info, self.code_lineno)) + self._write_debug_info = None + self._first_write = False + self.stream.write(" " * self._indentation) + self._new_lines = 0 + self.stream.write(x) + + def writeline( + self, x: str, node: t.Optional[nodes.Node] = None, extra: int = 0 + ) -> None: + """Combination of newline and write.""" + self.newline(node, extra) + self.write(x) + + def newline(self, node: t.Optional[nodes.Node] = None, extra: int = 0) -> None: + """Add one or more newlines before the next write.""" + self._new_lines = max(self._new_lines, 1 + extra) + if node is not None and node.lineno != self._last_line: + self._write_debug_info = node.lineno + self._last_line = node.lineno + + def signature( + self, + node: t.Union[nodes.Call, nodes.Filter, nodes.Test], + frame: Frame, + extra_kwargs: t.Optional[t.Mapping[str, t.Any]] = None, + ) -> None: + """Writes a function call to the stream for the current node. + A leading comma is added automatically. The extra keyword + arguments may not include python keywords otherwise a syntax + error could occur. The extra keyword arguments should be given + as python dict. + """ + # if any of the given keyword arguments is a python keyword + # we have to make sure that no invalid call is created. + kwarg_workaround = any( + is_python_keyword(t.cast(str, k)) + for k in chain((x.key for x in node.kwargs), extra_kwargs or ()) + ) + + for arg in node.args: + self.write(", ") + self.visit(arg, frame) + + if not kwarg_workaround: + for kwarg in node.kwargs: + self.write(", ") + self.visit(kwarg, frame) + if extra_kwargs is not None: + for key, value in extra_kwargs.items(): + self.write(f", {key}={value}") + if node.dyn_args: + self.write(", *") + self.visit(node.dyn_args, frame) + + if kwarg_workaround: + if node.dyn_kwargs is not None: + self.write(", **dict({") + else: + self.write(", **{") + for kwarg in node.kwargs: + self.write(f"{kwarg.key!r}: ") + self.visit(kwarg.value, frame) + self.write(", ") + if extra_kwargs is not None: + for key, value in extra_kwargs.items(): + self.write(f"{key!r}: {value}, ") + if node.dyn_kwargs is not None: + self.write("}, **") + self.visit(node.dyn_kwargs, frame) + self.write(")") + else: + self.write("}") + + elif node.dyn_kwargs is not None: + self.write(", **") + self.visit(node.dyn_kwargs, frame) + + def pull_dependencies(self, nodes: t.Iterable[nodes.Node]) -> None: + """Find all filter and test names used in the template and + assign them to variables in the compiled namespace. Checking + that the names are registered with the environment is done when + compiling the Filter and Test nodes. If the node is in an If or + CondExpr node, the check is done at runtime instead. + + .. versionchanged:: 3.0 + Filters and tests in If and CondExpr nodes are checked at + runtime instead of compile time. + """ + visitor = DependencyFinderVisitor() + + for node in nodes: + visitor.visit(node) + + for id_map, names, dependency in ( + (self.filters, visitor.filters, "filters"), + ( + self.tests, + visitor.tests, + "tests", + ), + ): + for name in sorted(names): + if name not in id_map: + id_map[name] = self.temporary_identifier() + + # add check during runtime that dependencies used inside of executed + # blocks are defined, as this step may be skipped during compile time + self.writeline("try:") + self.indent() + self.writeline(f"{id_map[name]} = environment.{dependency}[{name!r}]") + self.outdent() + self.writeline("except KeyError:") + self.indent() + self.writeline("@internalcode") + self.writeline(f"def {id_map[name]}(*unused):") + self.indent() + self.writeline( + f'raise TemplateRuntimeError("No {dependency[:-1]}' + f' named {name!r} found.")' + ) + self.outdent() + self.outdent() + + def enter_frame(self, frame: Frame) -> None: + undefs = [] + for target, (action, param) in frame.symbols.loads.items(): + if action == VAR_LOAD_PARAMETER: + pass + elif action == VAR_LOAD_RESOLVE: + self.writeline(f"{target} = {self.get_resolve_func()}({param!r})") + elif action == VAR_LOAD_ALIAS: + self.writeline(f"{target} = {param}") + elif action == VAR_LOAD_UNDEFINED: + undefs.append(target) + else: + raise NotImplementedError("unknown load instruction") + if undefs: + self.writeline(f"{' = '.join(undefs)} = missing") + + def leave_frame(self, frame: Frame, with_python_scope: bool = False) -> None: + if not with_python_scope: + undefs = [] + for target in frame.symbols.loads: + undefs.append(target) + if undefs: + self.writeline(f"{' = '.join(undefs)} = missing") + + def choose_async(self, async_value: str = "async ", sync_value: str = "") -> str: + return async_value if self.environment.is_async else sync_value + + def func(self, name: str) -> str: + return f"{self.choose_async()}def {name}" + + def macro_body( + self, node: t.Union[nodes.Macro, nodes.CallBlock], frame: Frame + ) -> t.Tuple[Frame, MacroRef]: + """Dump the function def of a macro or call block.""" + frame = frame.inner() + frame.symbols.analyze_node(node) + macro_ref = MacroRef(node) + + explicit_caller = None + skip_special_params = set() + args = [] + + for idx, arg in enumerate(node.args): + if arg.name == "caller": + explicit_caller = idx + if arg.name in ("kwargs", "varargs"): + skip_special_params.add(arg.name) + args.append(frame.symbols.ref(arg.name)) + + undeclared = find_undeclared(node.body, ("caller", "kwargs", "varargs")) + + if "caller" in undeclared: + # In older Jinja versions there was a bug that allowed caller + # to retain the special behavior even if it was mentioned in + # the argument list. However thankfully this was only really + # working if it was the last argument. So we are explicitly + # checking this now and error out if it is anywhere else in + # the argument list. + if explicit_caller is not None: + try: + node.defaults[explicit_caller - len(node.args)] + except IndexError: + self.fail( + "When defining macros or call blocks the " + 'special "caller" argument must be omitted ' + "or be given a default.", + node.lineno, + ) + else: + args.append(frame.symbols.declare_parameter("caller")) + macro_ref.accesses_caller = True + if "kwargs" in undeclared and "kwargs" not in skip_special_params: + args.append(frame.symbols.declare_parameter("kwargs")) + macro_ref.accesses_kwargs = True + if "varargs" in undeclared and "varargs" not in skip_special_params: + args.append(frame.symbols.declare_parameter("varargs")) + macro_ref.accesses_varargs = True + + # macros are delayed, they never require output checks + frame.require_output_check = False + frame.symbols.analyze_node(node) + self.writeline(f"{self.func('macro')}({', '.join(args)}):", node) + self.indent() + + self.buffer(frame) + self.enter_frame(frame) + + self.push_parameter_definitions(frame) + for idx, arg in enumerate(node.args): + ref = frame.symbols.ref(arg.name) + self.writeline(f"if {ref} is missing:") + self.indent() + try: + default = node.defaults[idx - len(node.args)] + except IndexError: + self.writeline( + f'{ref} = undefined("parameter {arg.name!r} was not provided",' + f" name={arg.name!r})" + ) + else: + self.writeline(f"{ref} = ") + self.visit(default, frame) + self.mark_parameter_stored(ref) + self.outdent() + self.pop_parameter_definitions() + + self.blockvisit(node.body, frame) + self.return_buffer_contents(frame, force_unescaped=True) + self.leave_frame(frame, with_python_scope=True) + self.outdent() + + return frame, macro_ref + + def macro_def(self, macro_ref: MacroRef, frame: Frame) -> None: + """Dump the macro definition for the def created by macro_body.""" + arg_tuple = ", ".join(repr(x.name) for x in macro_ref.node.args) + name = getattr(macro_ref.node, "name", None) + if len(macro_ref.node.args) == 1: + arg_tuple += "," + self.write( + f"Macro(environment, macro, {name!r}, ({arg_tuple})," + f" {macro_ref.accesses_kwargs!r}, {macro_ref.accesses_varargs!r}," + f" {macro_ref.accesses_caller!r}, context.eval_ctx.autoescape)" + ) + + def position(self, node: nodes.Node) -> str: + """Return a human readable position for the node.""" + rv = f"line {node.lineno}" + if self.name is not None: + rv = f"{rv} in {self.name!r}" + return rv + + def dump_local_context(self, frame: Frame) -> str: + items_kv = ", ".join( + f"{name!r}: {target}" + for name, target in frame.symbols.dump_stores().items() + ) + return f"{{{items_kv}}}" + + def write_commons(self) -> None: + """Writes a common preamble that is used by root and block functions. + Primarily this sets up common local helpers and enforces a generator + through a dead branch. + """ + self.writeline("resolve = context.resolve_or_missing") + self.writeline("undefined = environment.undefined") + self.writeline("concat = environment.concat") + # always use the standard Undefined class for the implicit else of + # conditional expressions + self.writeline("cond_expr_undefined = Undefined") + self.writeline("if 0: yield None") + + def push_parameter_definitions(self, frame: Frame) -> None: + """Pushes all parameter targets from the given frame into a local + stack that permits tracking of yet to be assigned parameters. In + particular this enables the optimization from `visit_Name` to skip + undefined expressions for parameters in macros as macros can reference + otherwise unbound parameters. + """ + self._param_def_block.append(frame.symbols.dump_param_targets()) + + def pop_parameter_definitions(self) -> None: + """Pops the current parameter definitions set.""" + self._param_def_block.pop() + + def mark_parameter_stored(self, target: str) -> None: + """Marks a parameter in the current parameter definitions as stored. + This will skip the enforced undefined checks. + """ + if self._param_def_block: + self._param_def_block[-1].discard(target) + + def push_context_reference(self, target: str) -> None: + self._context_reference_stack.append(target) + + def pop_context_reference(self) -> None: + self._context_reference_stack.pop() + + def get_context_ref(self) -> str: + return self._context_reference_stack[-1] + + def get_resolve_func(self) -> str: + target = self._context_reference_stack[-1] + if target == "context": + return "resolve" + return f"{target}.resolve" + + def derive_context(self, frame: Frame) -> str: + return f"{self.get_context_ref()}.derived({self.dump_local_context(frame)})" + + def parameter_is_undeclared(self, target: str) -> bool: + """Checks if a given target is an undeclared parameter.""" + if not self._param_def_block: + return False + return target in self._param_def_block[-1] + + def push_assign_tracking(self) -> None: + """Pushes a new layer for assignment tracking.""" + self._assign_stack.append(set()) + + def pop_assign_tracking(self, frame: Frame) -> None: + """Pops the topmost level for assignment tracking and updates the + context variables if necessary. + """ + vars = self._assign_stack.pop() + if ( + not frame.block_frame + and not frame.loop_frame + and not frame.toplevel + or not vars + ): + return + public_names = [x for x in vars if x[:1] != "_"] + if len(vars) == 1: + name = next(iter(vars)) + ref = frame.symbols.ref(name) + if frame.loop_frame: + self.writeline(f"_loop_vars[{name!r}] = {ref}") + return + if frame.block_frame: + self.writeline(f"_block_vars[{name!r}] = {ref}") + return + self.writeline(f"context.vars[{name!r}] = {ref}") + else: + if frame.loop_frame: + self.writeline("_loop_vars.update({") + elif frame.block_frame: + self.writeline("_block_vars.update({") + else: + self.writeline("context.vars.update({") + for idx, name in enumerate(vars): + if idx: + self.write(", ") + ref = frame.symbols.ref(name) + self.write(f"{name!r}: {ref}") + self.write("})") + if not frame.block_frame and not frame.loop_frame and public_names: + if len(public_names) == 1: + self.writeline(f"context.exported_vars.add({public_names[0]!r})") + else: + names_str = ", ".join(map(repr, public_names)) + self.writeline(f"context.exported_vars.update(({names_str}))") + + # -- Statement Visitors + + def visit_Template( + self, node: nodes.Template, frame: t.Optional[Frame] = None + ) -> None: + assert frame is None, "no root frame allowed" + eval_ctx = EvalContext(self.environment, self.name) + + from .runtime import async_exported + from .runtime import exported + + if self.environment.is_async: + exported_names = sorted(exported + async_exported) + else: + exported_names = sorted(exported) + + self.writeline("from jinja2.runtime import " + ", ".join(exported_names)) + + # if we want a deferred initialization we cannot move the + # environment into a local name + envenv = "" if self.defer_init else ", environment=environment" + + # do we have an extends tag at all? If not, we can save some + # overhead by just not processing any inheritance code. + have_extends = node.find(nodes.Extends) is not None + + # find all blocks + for block in node.find_all(nodes.Block): + if block.name in self.blocks: + self.fail(f"block {block.name!r} defined twice", block.lineno) + self.blocks[block.name] = block + + # find all imports and import them + for import_ in node.find_all(nodes.ImportedName): + if import_.importname not in self.import_aliases: + imp = import_.importname + self.import_aliases[imp] = alias = self.temporary_identifier() + if "." in imp: + module, obj = imp.rsplit(".", 1) + self.writeline(f"from {module} import {obj} as {alias}") + else: + self.writeline(f"import {imp} as {alias}") + + # add the load name + self.writeline(f"name = {self.name!r}") + + # generate the root render function. + self.writeline( + f"{self.func('root')}(context, missing=missing{envenv}):", extra=1 + ) + self.indent() + self.write_commons() + + # process the root + frame = Frame(eval_ctx) + if "self" in find_undeclared(node.body, ("self",)): + ref = frame.symbols.declare_parameter("self") + self.writeline(f"{ref} = TemplateReference(context)") + frame.symbols.analyze_node(node) + frame.toplevel = frame.rootlevel = True + frame.require_output_check = have_extends and not self.has_known_extends + if have_extends: + self.writeline("parent_template = None") + self.enter_frame(frame) + self.pull_dependencies(node.body) + self.blockvisit(node.body, frame) + self.leave_frame(frame, with_python_scope=True) + self.outdent() + + # make sure that the parent root is called. + if have_extends: + if not self.has_known_extends: + self.indent() + self.writeline("if parent_template is not None:") + self.indent() + if not self.environment.is_async: + self.writeline("yield from parent_template.root_render_func(context)") + else: + self.writeline( + "async for event in parent_template.root_render_func(context):" + ) + self.indent() + self.writeline("yield event") + self.outdent() + self.outdent(1 + (not self.has_known_extends)) + + # at this point we now have the blocks collected and can visit them too. + for name, block in self.blocks.items(): + self.writeline( + f"{self.func('block_' + name)}(context, missing=missing{envenv}):", + block, + 1, + ) + self.indent() + self.write_commons() + # It's important that we do not make this frame a child of the + # toplevel template. This would cause a variety of + # interesting issues with identifier tracking. + block_frame = Frame(eval_ctx) + block_frame.block_frame = True + undeclared = find_undeclared(block.body, ("self", "super")) + if "self" in undeclared: + ref = block_frame.symbols.declare_parameter("self") + self.writeline(f"{ref} = TemplateReference(context)") + if "super" in undeclared: + ref = block_frame.symbols.declare_parameter("super") + self.writeline(f"{ref} = context.super({name!r}, block_{name})") + block_frame.symbols.analyze_node(block) + block_frame.block = name + self.writeline("_block_vars = {}") + self.enter_frame(block_frame) + self.pull_dependencies(block.body) + self.blockvisit(block.body, block_frame) + self.leave_frame(block_frame, with_python_scope=True) + self.outdent() + + blocks_kv_str = ", ".join(f"{x!r}: block_{x}" for x in self.blocks) + self.writeline(f"blocks = {{{blocks_kv_str}}}", extra=1) + debug_kv_str = "&".join(f"{k}={v}" for k, v in self.debug_info) + self.writeline(f"debug_info = {debug_kv_str!r}") + + def visit_Block(self, node: nodes.Block, frame: Frame) -> None: + """Call a block and register it for the template.""" + level = 0 + if frame.toplevel: + # if we know that we are a child template, there is no need to + # check if we are one + if self.has_known_extends: + return + if self.extends_so_far > 0: + self.writeline("if parent_template is None:") + self.indent() + level += 1 + + if node.scoped: + context = self.derive_context(frame) + else: + context = self.get_context_ref() + + if node.required: + self.writeline(f"if len(context.blocks[{node.name!r}]) <= 1:", node) + self.indent() + self.writeline( + f'raise TemplateRuntimeError("Required block {node.name!r} not found")', + node, + ) + self.outdent() + + if not self.environment.is_async and frame.buffer is None: + self.writeline( + f"yield from context.blocks[{node.name!r}][0]({context})", node + ) + else: + self.writeline( + f"{self.choose_async()}for event in" + f" context.blocks[{node.name!r}][0]({context}):", + node, + ) + self.indent() + self.simple_write("event", frame) + self.outdent() + + self.outdent(level) + + def visit_Extends(self, node: nodes.Extends, frame: Frame) -> None: + """Calls the extender.""" + if not frame.toplevel: + self.fail("cannot use extend from a non top-level scope", node.lineno) + + # if the number of extends statements in general is zero so + # far, we don't have to add a check if something extended + # the template before this one. + if self.extends_so_far > 0: + # if we have a known extends we just add a template runtime + # error into the generated code. We could catch that at compile + # time too, but i welcome it not to confuse users by throwing the + # same error at different times just "because we can". + if not self.has_known_extends: + self.writeline("if parent_template is not None:") + self.indent() + self.writeline('raise TemplateRuntimeError("extended multiple times")') + + # if we have a known extends already we don't need that code here + # as we know that the template execution will end here. + if self.has_known_extends: + raise CompilerExit() + else: + self.outdent() + + self.writeline("parent_template = environment.get_template(", node) + self.visit(node.template, frame) + self.write(f", {self.name!r})") + self.writeline("for name, parent_block in parent_template.blocks.items():") + self.indent() + self.writeline("context.blocks.setdefault(name, []).append(parent_block)") + self.outdent() + + # if this extends statement was in the root level we can take + # advantage of that information and simplify the generated code + # in the top level from this point onwards + if frame.rootlevel: + self.has_known_extends = True + + # and now we have one more + self.extends_so_far += 1 + + def visit_Include(self, node: nodes.Include, frame: Frame) -> None: + """Handles includes.""" + if node.ignore_missing: + self.writeline("try:") + self.indent() + + func_name = "get_or_select_template" + if isinstance(node.template, nodes.Const): + if isinstance(node.template.value, str): + func_name = "get_template" + elif isinstance(node.template.value, (tuple, list)): + func_name = "select_template" + elif isinstance(node.template, (nodes.Tuple, nodes.List)): + func_name = "select_template" + + self.writeline(f"template = environment.{func_name}(", node) + self.visit(node.template, frame) + self.write(f", {self.name!r})") + if node.ignore_missing: + self.outdent() + self.writeline("except TemplateNotFound:") + self.indent() + self.writeline("pass") + self.outdent() + self.writeline("else:") + self.indent() + + skip_event_yield = False + if node.with_context: + self.writeline( + f"{self.choose_async()}for event in template.root_render_func(" + "template.new_context(context.get_all(), True," + f" {self.dump_local_context(frame)})):" + ) + elif self.environment.is_async: + self.writeline( + "for event in (await template._get_default_module_async())" + "._body_stream:" + ) + else: + self.writeline("yield from template._get_default_module()._body_stream") + skip_event_yield = True + + if not skip_event_yield: + self.indent() + self.simple_write("event", frame) + self.outdent() + + if node.ignore_missing: + self.outdent() + + def _import_common( + self, node: t.Union[nodes.Import, nodes.FromImport], frame: Frame + ) -> None: + self.write(f"{self.choose_async('await ')}environment.get_template(") + self.visit(node.template, frame) + self.write(f", {self.name!r}).") + + if node.with_context: + f_name = f"make_module{self.choose_async('_async')}" + self.write( + f"{f_name}(context.get_all(), True, {self.dump_local_context(frame)})" + ) + else: + self.write(f"_get_default_module{self.choose_async('_async')}(context)") + + def visit_Import(self, node: nodes.Import, frame: Frame) -> None: + """Visit regular imports.""" + self.writeline(f"{frame.symbols.ref(node.target)} = ", node) + if frame.toplevel: + self.write(f"context.vars[{node.target!r}] = ") + + self._import_common(node, frame) + + if frame.toplevel and not node.target.startswith("_"): + self.writeline(f"context.exported_vars.discard({node.target!r})") + + def visit_FromImport(self, node: nodes.FromImport, frame: Frame) -> None: + """Visit named imports.""" + self.newline(node) + self.write("included_template = ") + self._import_common(node, frame) + var_names = [] + discarded_names = [] + for name in node.names: + if isinstance(name, tuple): + name, alias = name + else: + alias = name + self.writeline( + f"{frame.symbols.ref(alias)} =" + f" getattr(included_template, {name!r}, missing)" + ) + self.writeline(f"if {frame.symbols.ref(alias)} is missing:") + self.indent() + message = ( + "the template {included_template.__name__!r}" + f" (imported on {self.position(node)})" + f" does not export the requested name {name!r}" + ) + self.writeline( + f"{frame.symbols.ref(alias)} = undefined(f{message!r}, name={name!r})" + ) + self.outdent() + if frame.toplevel: + var_names.append(alias) + if not alias.startswith("_"): + discarded_names.append(alias) + + if var_names: + if len(var_names) == 1: + name = var_names[0] + self.writeline(f"context.vars[{name!r}] = {frame.symbols.ref(name)}") + else: + names_kv = ", ".join( + f"{name!r}: {frame.symbols.ref(name)}" for name in var_names + ) + self.writeline(f"context.vars.update({{{names_kv}}})") + if discarded_names: + if len(discarded_names) == 1: + self.writeline(f"context.exported_vars.discard({discarded_names[0]!r})") + else: + names_str = ", ".join(map(repr, discarded_names)) + self.writeline( + f"context.exported_vars.difference_update(({names_str}))" + ) + + def visit_For(self, node: nodes.For, frame: Frame) -> None: + loop_frame = frame.inner() + loop_frame.loop_frame = True + test_frame = frame.inner() + else_frame = frame.inner() + + # try to figure out if we have an extended loop. An extended loop + # is necessary if the loop is in recursive mode if the special loop + # variable is accessed in the body if the body is a scoped block. + extended_loop = ( + node.recursive + or "loop" + in find_undeclared(node.iter_child_nodes(only=("body",)), ("loop",)) + or any(block.scoped for block in node.find_all(nodes.Block)) + ) + + loop_ref = None + if extended_loop: + loop_ref = loop_frame.symbols.declare_parameter("loop") + + loop_frame.symbols.analyze_node(node, for_branch="body") + if node.else_: + else_frame.symbols.analyze_node(node, for_branch="else") + + if node.test: + loop_filter_func = self.temporary_identifier() + test_frame.symbols.analyze_node(node, for_branch="test") + self.writeline(f"{self.func(loop_filter_func)}(fiter):", node.test) + self.indent() + self.enter_frame(test_frame) + self.writeline(self.choose_async("async for ", "for ")) + self.visit(node.target, loop_frame) + self.write(" in ") + self.write(self.choose_async("auto_aiter(fiter)", "fiter")) + self.write(":") + self.indent() + self.writeline("if ", node.test) + self.visit(node.test, test_frame) + self.write(":") + self.indent() + self.writeline("yield ") + self.visit(node.target, loop_frame) + self.outdent(3) + self.leave_frame(test_frame, with_python_scope=True) + + # if we don't have an recursive loop we have to find the shadowed + # variables at that point. Because loops can be nested but the loop + # variable is a special one we have to enforce aliasing for it. + if node.recursive: + self.writeline( + f"{self.func('loop')}(reciter, loop_render_func, depth=0):", node + ) + self.indent() + self.buffer(loop_frame) + + # Use the same buffer for the else frame + else_frame.buffer = loop_frame.buffer + + # make sure the loop variable is a special one and raise a template + # assertion error if a loop tries to write to loop + if extended_loop: + self.writeline(f"{loop_ref} = missing") + + for name in node.find_all(nodes.Name): + if name.ctx == "store" and name.name == "loop": + self.fail( + "Can't assign to special loop variable in for-loop target", + name.lineno, + ) + + if node.else_: + iteration_indicator = self.temporary_identifier() + self.writeline(f"{iteration_indicator} = 1") + + self.writeline(self.choose_async("async for ", "for "), node) + self.visit(node.target, loop_frame) + if extended_loop: + self.write(f", {loop_ref} in {self.choose_async('Async')}LoopContext(") + else: + self.write(" in ") + + if node.test: + self.write(f"{loop_filter_func}(") + if node.recursive: + self.write("reciter") + else: + if self.environment.is_async and not extended_loop: + self.write("auto_aiter(") + self.visit(node.iter, frame) + if self.environment.is_async and not extended_loop: + self.write(")") + if node.test: + self.write(")") + + if node.recursive: + self.write(", undefined, loop_render_func, depth):") + else: + self.write(", undefined):" if extended_loop else ":") + + self.indent() + self.enter_frame(loop_frame) + + self.writeline("_loop_vars = {}") + self.blockvisit(node.body, loop_frame) + if node.else_: + self.writeline(f"{iteration_indicator} = 0") + self.outdent() + self.leave_frame( + loop_frame, with_python_scope=node.recursive and not node.else_ + ) + + if node.else_: + self.writeline(f"if {iteration_indicator}:") + self.indent() + self.enter_frame(else_frame) + self.blockvisit(node.else_, else_frame) + self.leave_frame(else_frame) + self.outdent() + + # if the node was recursive we have to return the buffer contents + # and start the iteration code + if node.recursive: + self.return_buffer_contents(loop_frame) + self.outdent() + self.start_write(frame, node) + self.write(f"{self.choose_async('await ')}loop(") + if self.environment.is_async: + self.write("auto_aiter(") + self.visit(node.iter, frame) + if self.environment.is_async: + self.write(")") + self.write(", loop)") + self.end_write(frame) + + # at the end of the iteration, clear any assignments made in the + # loop from the top level + if self._assign_stack: + self._assign_stack[-1].difference_update(loop_frame.symbols.stores) + + def visit_If(self, node: nodes.If, frame: Frame) -> None: + if_frame = frame.soft() + self.writeline("if ", node) + self.visit(node.test, if_frame) + self.write(":") + self.indent() + self.blockvisit(node.body, if_frame) + self.outdent() + for elif_ in node.elif_: + self.writeline("elif ", elif_) + self.visit(elif_.test, if_frame) + self.write(":") + self.indent() + self.blockvisit(elif_.body, if_frame) + self.outdent() + if node.else_: + self.writeline("else:") + self.indent() + self.blockvisit(node.else_, if_frame) + self.outdent() + + def visit_Macro(self, node: nodes.Macro, frame: Frame) -> None: + macro_frame, macro_ref = self.macro_body(node, frame) + self.newline() + if frame.toplevel: + if not node.name.startswith("_"): + self.write(f"context.exported_vars.add({node.name!r})") + self.writeline(f"context.vars[{node.name!r}] = ") + self.write(f"{frame.symbols.ref(node.name)} = ") + self.macro_def(macro_ref, macro_frame) + + def visit_CallBlock(self, node: nodes.CallBlock, frame: Frame) -> None: + call_frame, macro_ref = self.macro_body(node, frame) + self.writeline("caller = ") + self.macro_def(macro_ref, call_frame) + self.start_write(frame, node) + self.visit_Call(node.call, frame, forward_caller=True) + self.end_write(frame) + + def visit_FilterBlock(self, node: nodes.FilterBlock, frame: Frame) -> None: + filter_frame = frame.inner() + filter_frame.symbols.analyze_node(node) + self.enter_frame(filter_frame) + self.buffer(filter_frame) + self.blockvisit(node.body, filter_frame) + self.start_write(frame, node) + self.visit_Filter(node.filter, filter_frame) + self.end_write(frame) + self.leave_frame(filter_frame) + + def visit_With(self, node: nodes.With, frame: Frame) -> None: + with_frame = frame.inner() + with_frame.symbols.analyze_node(node) + self.enter_frame(with_frame) + for target, expr in zip(node.targets, node.values): + self.newline() + self.visit(target, with_frame) + self.write(" = ") + self.visit(expr, frame) + self.blockvisit(node.body, with_frame) + self.leave_frame(with_frame) + + def visit_ExprStmt(self, node: nodes.ExprStmt, frame: Frame) -> None: + self.newline(node) + self.visit(node.node, frame) + + class _FinalizeInfo(t.NamedTuple): + const: t.Optional[t.Callable[..., str]] + src: t.Optional[str] + + @staticmethod + def _default_finalize(value: t.Any) -> t.Any: + """The default finalize function if the environment isn't + configured with one. Or, if the environment has one, this is + called on that function's output for constants. + """ + return str(value) + + _finalize: t.Optional[_FinalizeInfo] = None + + def _make_finalize(self) -> _FinalizeInfo: + """Build the finalize function to be used on constants and at + runtime. Cached so it's only created once for all output nodes. + + Returns a ``namedtuple`` with the following attributes: + + ``const`` + A function to finalize constant data at compile time. + + ``src`` + Source code to output around nodes to be evaluated at + runtime. + """ + if self._finalize is not None: + return self._finalize + + finalize: t.Optional[t.Callable[..., t.Any]] + finalize = default = self._default_finalize + src = None + + if self.environment.finalize: + src = "environment.finalize(" + env_finalize = self.environment.finalize + pass_arg = { + _PassArg.context: "context", + _PassArg.eval_context: "context.eval_ctx", + _PassArg.environment: "environment", + }.get( + _PassArg.from_obj(env_finalize) # type: ignore + ) + finalize = None + + if pass_arg is None: + + def finalize(value: t.Any) -> t.Any: # noqa: F811 + return default(env_finalize(value)) + + else: + src = f"{src}{pass_arg}, " + + if pass_arg == "environment": + + def finalize(value: t.Any) -> t.Any: # noqa: F811 + return default(env_finalize(self.environment, value)) + + self._finalize = self._FinalizeInfo(finalize, src) + return self._finalize + + def _output_const_repr(self, group: t.Iterable[t.Any]) -> str: + """Given a group of constant values converted from ``Output`` + child nodes, produce a string to write to the template module + source. + """ + return repr(concat(group)) + + def _output_child_to_const( + self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo + ) -> str: + """Try to optimize a child of an ``Output`` node by trying to + convert it to constant, finalized data at compile time. + + If :exc:`Impossible` is raised, the node is not constant and + will be evaluated at runtime. Any other exception will also be + evaluated at runtime for easier debugging. + """ + const = node.as_const(frame.eval_ctx) + + if frame.eval_ctx.autoescape: + const = escape(const) + + # Template data doesn't go through finalize. + if isinstance(node, nodes.TemplateData): + return str(const) + + return finalize.const(const) # type: ignore + + def _output_child_pre( + self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo + ) -> None: + """Output extra source code before visiting a child of an + ``Output`` node. + """ + if frame.eval_ctx.volatile: + self.write("(escape if context.eval_ctx.autoescape else str)(") + elif frame.eval_ctx.autoescape: + self.write("escape(") + else: + self.write("str(") + + if finalize.src is not None: + self.write(finalize.src) + + def _output_child_post( + self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo + ) -> None: + """Output extra source code after visiting a child of an + ``Output`` node. + """ + self.write(")") + + if finalize.src is not None: + self.write(")") + + def visit_Output(self, node: nodes.Output, frame: Frame) -> None: + # If an extends is active, don't render outside a block. + if frame.require_output_check: + # A top-level extends is known to exist at compile time. + if self.has_known_extends: + return + + self.writeline("if parent_template is None:") + self.indent() + + finalize = self._make_finalize() + body: t.List[t.Union[t.List[t.Any], nodes.Expr]] = [] + + # Evaluate constants at compile time if possible. Each item in + # body will be either a list of static data or a node to be + # evaluated at runtime. + for child in node.nodes: + try: + if not ( + # If the finalize function requires runtime context, + # constants can't be evaluated at compile time. + finalize.const + # Unless it's basic template data that won't be + # finalized anyway. + or isinstance(child, nodes.TemplateData) + ): + raise nodes.Impossible() + + const = self._output_child_to_const(child, frame, finalize) + except (nodes.Impossible, Exception): + # The node was not constant and needs to be evaluated at + # runtime. Or another error was raised, which is easier + # to debug at runtime. + body.append(child) + continue + + if body and isinstance(body[-1], list): + body[-1].append(const) + else: + body.append([const]) + + if frame.buffer is not None: + if len(body) == 1: + self.writeline(f"{frame.buffer}.append(") + else: + self.writeline(f"{frame.buffer}.extend((") + + self.indent() + + for item in body: + if isinstance(item, list): + # A group of constant data to join and output. + val = self._output_const_repr(item) + + if frame.buffer is None: + self.writeline("yield " + val) + else: + self.writeline(val + ",") + else: + if frame.buffer is None: + self.writeline("yield ", item) + else: + self.newline(item) + + # A node to be evaluated at runtime. + self._output_child_pre(item, frame, finalize) + self.visit(item, frame) + self._output_child_post(item, frame, finalize) + + if frame.buffer is not None: + self.write(",") + + if frame.buffer is not None: + self.outdent() + self.writeline(")" if len(body) == 1 else "))") + + if frame.require_output_check: + self.outdent() + + def visit_Assign(self, node: nodes.Assign, frame: Frame) -> None: + self.push_assign_tracking() + self.newline(node) + self.visit(node.target, frame) + self.write(" = ") + self.visit(node.node, frame) + self.pop_assign_tracking(frame) + + def visit_AssignBlock(self, node: nodes.AssignBlock, frame: Frame) -> None: + self.push_assign_tracking() + block_frame = frame.inner() + # This is a special case. Since a set block always captures we + # will disable output checks. This way one can use set blocks + # toplevel even in extended templates. + block_frame.require_output_check = False + block_frame.symbols.analyze_node(node) + self.enter_frame(block_frame) + self.buffer(block_frame) + self.blockvisit(node.body, block_frame) + self.newline(node) + self.visit(node.target, frame) + self.write(" = (Markup if context.eval_ctx.autoescape else identity)(") + if node.filter is not None: + self.visit_Filter(node.filter, block_frame) + else: + self.write(f"concat({block_frame.buffer})") + self.write(")") + self.pop_assign_tracking(frame) + self.leave_frame(block_frame) + + # -- Expression Visitors + + def visit_Name(self, node: nodes.Name, frame: Frame) -> None: + if node.ctx == "store" and ( + frame.toplevel or frame.loop_frame or frame.block_frame + ): + if self._assign_stack: + self._assign_stack[-1].add(node.name) + ref = frame.symbols.ref(node.name) + + # If we are looking up a variable we might have to deal with the + # case where it's undefined. We can skip that case if the load + # instruction indicates a parameter which are always defined. + if node.ctx == "load": + load = frame.symbols.find_load(ref) + if not ( + load is not None + and load[0] == VAR_LOAD_PARAMETER + and not self.parameter_is_undeclared(ref) + ): + self.write( + f"(undefined(name={node.name!r}) if {ref} is missing else {ref})" + ) + return + + self.write(ref) + + def visit_NSRef(self, node: nodes.NSRef, frame: Frame) -> None: + # NSRefs can only be used to store values; since they use the normal + # `foo.bar` notation they will be parsed as a normal attribute access + # when used anywhere but in a `set` context + ref = frame.symbols.ref(node.name) + self.writeline(f"if not isinstance({ref}, Namespace):") + self.indent() + self.writeline( + "raise TemplateRuntimeError" + '("cannot assign attribute on non-namespace object")' + ) + self.outdent() + self.writeline(f"{ref}[{node.attr!r}]") + + def visit_Const(self, node: nodes.Const, frame: Frame) -> None: + val = node.as_const(frame.eval_ctx) + if isinstance(val, float): + self.write(str(val)) + else: + self.write(repr(val)) + + def visit_TemplateData(self, node: nodes.TemplateData, frame: Frame) -> None: + try: + self.write(repr(node.as_const(frame.eval_ctx))) + except nodes.Impossible: + self.write( + f"(Markup if context.eval_ctx.autoescape else identity)({node.data!r})" + ) + + def visit_Tuple(self, node: nodes.Tuple, frame: Frame) -> None: + self.write("(") + idx = -1 + for idx, item in enumerate(node.items): + if idx: + self.write(", ") + self.visit(item, frame) + self.write(",)" if idx == 0 else ")") + + def visit_List(self, node: nodes.List, frame: Frame) -> None: + self.write("[") + for idx, item in enumerate(node.items): + if idx: + self.write(", ") + self.visit(item, frame) + self.write("]") + + def visit_Dict(self, node: nodes.Dict, frame: Frame) -> None: + self.write("{") + for idx, item in enumerate(node.items): + if idx: + self.write(", ") + self.visit(item.key, frame) + self.write(": ") + self.visit(item.value, frame) + self.write("}") + + visit_Add = _make_binop("+") + visit_Sub = _make_binop("-") + visit_Mul = _make_binop("*") + visit_Div = _make_binop("/") + visit_FloorDiv = _make_binop("//") + visit_Pow = _make_binop("**") + visit_Mod = _make_binop("%") + visit_And = _make_binop("and") + visit_Or = _make_binop("or") + visit_Pos = _make_unop("+") + visit_Neg = _make_unop("-") + visit_Not = _make_unop("not ") + + @optimizeconst + def visit_Concat(self, node: nodes.Concat, frame: Frame) -> None: + if frame.eval_ctx.volatile: + func_name = "(markup_join if context.eval_ctx.volatile else str_join)" + elif frame.eval_ctx.autoescape: + func_name = "markup_join" + else: + func_name = "str_join" + self.write(f"{func_name}((") + for arg in node.nodes: + self.visit(arg, frame) + self.write(", ") + self.write("))") + + @optimizeconst + def visit_Compare(self, node: nodes.Compare, frame: Frame) -> None: + self.write("(") + self.visit(node.expr, frame) + for op in node.ops: + self.visit(op, frame) + self.write(")") + + def visit_Operand(self, node: nodes.Operand, frame: Frame) -> None: + self.write(f" {operators[node.op]} ") + self.visit(node.expr, frame) + + @optimizeconst + def visit_Getattr(self, node: nodes.Getattr, frame: Frame) -> None: + if self.environment.is_async: + self.write("(await auto_await(") + + self.write("environment.getattr(") + self.visit(node.node, frame) + self.write(f", {node.attr!r})") + + if self.environment.is_async: + self.write("))") + + @optimizeconst + def visit_Getitem(self, node: nodes.Getitem, frame: Frame) -> None: + # slices bypass the environment getitem method. + if isinstance(node.arg, nodes.Slice): + self.visit(node.node, frame) + self.write("[") + self.visit(node.arg, frame) + self.write("]") + else: + if self.environment.is_async: + self.write("(await auto_await(") + + self.write("environment.getitem(") + self.visit(node.node, frame) + self.write(", ") + self.visit(node.arg, frame) + self.write(")") + + if self.environment.is_async: + self.write("))") + + def visit_Slice(self, node: nodes.Slice, frame: Frame) -> None: + if node.start is not None: + self.visit(node.start, frame) + self.write(":") + if node.stop is not None: + self.visit(node.stop, frame) + if node.step is not None: + self.write(":") + self.visit(node.step, frame) + + @contextmanager + def _filter_test_common( + self, node: t.Union[nodes.Filter, nodes.Test], frame: Frame, is_filter: bool + ) -> t.Iterator[None]: + if self.environment.is_async: + self.write("(await auto_await(") + + if is_filter: + self.write(f"{self.filters[node.name]}(") + func = self.environment.filters.get(node.name) + else: + self.write(f"{self.tests[node.name]}(") + func = self.environment.tests.get(node.name) + + # When inside an If or CondExpr frame, allow the filter to be + # undefined at compile time and only raise an error if it's + # actually called at runtime. See pull_dependencies. + if func is None and not frame.soft_frame: + type_name = "filter" if is_filter else "test" + self.fail(f"No {type_name} named {node.name!r}.", node.lineno) + + pass_arg = { + _PassArg.context: "context", + _PassArg.eval_context: "context.eval_ctx", + _PassArg.environment: "environment", + }.get( + _PassArg.from_obj(func) # type: ignore + ) + + if pass_arg is not None: + self.write(f"{pass_arg}, ") + + # Back to the visitor function to handle visiting the target of + # the filter or test. + yield + + self.signature(node, frame) + self.write(")") + + if self.environment.is_async: + self.write("))") + + @optimizeconst + def visit_Filter(self, node: nodes.Filter, frame: Frame) -> None: + with self._filter_test_common(node, frame, True): + # if the filter node is None we are inside a filter block + # and want to write to the current buffer + if node.node is not None: + self.visit(node.node, frame) + elif frame.eval_ctx.volatile: + self.write( + f"(Markup(concat({frame.buffer}))" + f" if context.eval_ctx.autoescape else concat({frame.buffer}))" + ) + elif frame.eval_ctx.autoescape: + self.write(f"Markup(concat({frame.buffer}))") + else: + self.write(f"concat({frame.buffer})") + + @optimizeconst + def visit_Test(self, node: nodes.Test, frame: Frame) -> None: + with self._filter_test_common(node, frame, False): + self.visit(node.node, frame) + + @optimizeconst + def visit_CondExpr(self, node: nodes.CondExpr, frame: Frame) -> None: + frame = frame.soft() + + def write_expr2() -> None: + if node.expr2 is not None: + self.visit(node.expr2, frame) + return + + self.write( + f'cond_expr_undefined("the inline if-expression on' + f" {self.position(node)} evaluated to false and no else" + f' section was defined.")' + ) + + self.write("(") + self.visit(node.expr1, frame) + self.write(" if ") + self.visit(node.test, frame) + self.write(" else ") + write_expr2() + self.write(")") + + @optimizeconst + def visit_Call( + self, node: nodes.Call, frame: Frame, forward_caller: bool = False + ) -> None: + if self.environment.is_async: + self.write("(await auto_await(") + if self.environment.sandboxed: + self.write("environment.call(context, ") + else: + self.write("context.call(") + self.visit(node.node, frame) + extra_kwargs = {"caller": "caller"} if forward_caller else None + loop_kwargs = {"_loop_vars": "_loop_vars"} if frame.loop_frame else {} + block_kwargs = {"_block_vars": "_block_vars"} if frame.block_frame else {} + if extra_kwargs: + extra_kwargs.update(loop_kwargs, **block_kwargs) + elif loop_kwargs or block_kwargs: + extra_kwargs = dict(loop_kwargs, **block_kwargs) + self.signature(node, frame, extra_kwargs) + self.write(")") + if self.environment.is_async: + self.write("))") + + def visit_Keyword(self, node: nodes.Keyword, frame: Frame) -> None: + self.write(node.key + "=") + self.visit(node.value, frame) + + # -- Unused nodes for extensions + + def visit_MarkSafe(self, node: nodes.MarkSafe, frame: Frame) -> None: + self.write("Markup(") + self.visit(node.expr, frame) + self.write(")") + + def visit_MarkSafeIfAutoescape( + self, node: nodes.MarkSafeIfAutoescape, frame: Frame + ) -> None: + self.write("(Markup if context.eval_ctx.autoescape else identity)(") + self.visit(node.expr, frame) + self.write(")") + + def visit_EnvironmentAttribute( + self, node: nodes.EnvironmentAttribute, frame: Frame + ) -> None: + self.write("environment." + node.name) + + def visit_ExtensionAttribute( + self, node: nodes.ExtensionAttribute, frame: Frame + ) -> None: + self.write(f"environment.extensions[{node.identifier!r}].{node.name}") + + def visit_ImportedName(self, node: nodes.ImportedName, frame: Frame) -> None: + self.write(self.import_aliases[node.importname]) + + def visit_InternalName(self, node: nodes.InternalName, frame: Frame) -> None: + self.write(node.name) + + def visit_ContextReference( + self, node: nodes.ContextReference, frame: Frame + ) -> None: + self.write("context") + + def visit_DerivedContextReference( + self, node: nodes.DerivedContextReference, frame: Frame + ) -> None: + self.write(self.derive_context(frame)) + + def visit_Continue(self, node: nodes.Continue, frame: Frame) -> None: + self.writeline("continue", node) + + def visit_Break(self, node: nodes.Break, frame: Frame) -> None: + self.writeline("break", node) + + def visit_Scope(self, node: nodes.Scope, frame: Frame) -> None: + scope_frame = frame.inner() + scope_frame.symbols.analyze_node(node) + self.enter_frame(scope_frame) + self.blockvisit(node.body, scope_frame) + self.leave_frame(scope_frame) + + def visit_OverlayScope(self, node: nodes.OverlayScope, frame: Frame) -> None: + ctx = self.temporary_identifier() + self.writeline(f"{ctx} = {self.derive_context(frame)}") + self.writeline(f"{ctx}.vars = ") + self.visit(node.context, frame) + self.push_context_reference(ctx) + + scope_frame = frame.inner(isolated=True) + scope_frame.symbols.analyze_node(node) + self.enter_frame(scope_frame) + self.blockvisit(node.body, scope_frame) + self.leave_frame(scope_frame) + self.pop_context_reference() + + def visit_EvalContextModifier( + self, node: nodes.EvalContextModifier, frame: Frame + ) -> None: + for keyword in node.options: + self.writeline(f"context.eval_ctx.{keyword.key} = ") + self.visit(keyword.value, frame) + try: + val = keyword.value.as_const(frame.eval_ctx) + except nodes.Impossible: + frame.eval_ctx.volatile = True + else: + setattr(frame.eval_ctx, keyword.key, val) + + def visit_ScopedEvalContextModifier( + self, node: nodes.ScopedEvalContextModifier, frame: Frame + ) -> None: + old_ctx_name = self.temporary_identifier() + saved_ctx = frame.eval_ctx.save() + self.writeline(f"{old_ctx_name} = context.eval_ctx.save()") + self.visit_EvalContextModifier(node, frame) + for child in node.body: + self.visit(child, frame) + frame.eval_ctx.revert(saved_ctx) + self.writeline(f"context.eval_ctx.revert({old_ctx_name})") diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2/constants.py b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/constants.py new file mode 100644 index 000000000..41a1c23b0 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/constants.py @@ -0,0 +1,20 @@ +#: list of lorem ipsum words used by the lipsum() helper function +LOREM_IPSUM_WORDS = """\ +a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at +auctor augue bibendum blandit class commodo condimentum congue consectetuer +consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus +diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend +elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames +faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac +hendrerit hymenaeos iaculis id imperdiet in inceptos integer interdum ipsum +justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem +luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie +mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non +nonummy nostra nulla nullam nunc odio orci ornare parturient pede pellentesque +penatibus per pharetra phasellus placerat platea porta porttitor posuere +potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus +ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit +sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor +tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices +ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus +viverra volutpat vulputate""" diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2/debug.py b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/debug.py new file mode 100644 index 000000000..7ed7e9297 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/debug.py @@ -0,0 +1,191 @@ +import sys +import typing as t +from types import CodeType +from types import TracebackType + +from .exceptions import TemplateSyntaxError +from .utils import internal_code +from .utils import missing + +if t.TYPE_CHECKING: + from .runtime import Context + + +def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException: + """Rewrite the current exception to replace any tracebacks from + within compiled template code with tracebacks that look like they + came from the template source. + + This must be called within an ``except`` block. + + :param source: For ``TemplateSyntaxError``, the original source if + known. + :return: The original exception with the rewritten traceback. + """ + _, exc_value, tb = sys.exc_info() + exc_value = t.cast(BaseException, exc_value) + tb = t.cast(TracebackType, tb) + + if isinstance(exc_value, TemplateSyntaxError) and not exc_value.translated: + exc_value.translated = True + exc_value.source = source + # Remove the old traceback, otherwise the frames from the + # compiler still show up. + exc_value.with_traceback(None) + # Outside of runtime, so the frame isn't executing template + # code, but it still needs to point at the template. + tb = fake_traceback( + exc_value, None, exc_value.filename or "", exc_value.lineno + ) + else: + # Skip the frame for the render function. + tb = tb.tb_next + + stack = [] + + # Build the stack of traceback object, replacing any in template + # code with the source file and line information. + while tb is not None: + # Skip frames decorated with @internalcode. These are internal + # calls that aren't useful in template debugging output. + if tb.tb_frame.f_code in internal_code: + tb = tb.tb_next + continue + + template = tb.tb_frame.f_globals.get("__jinja_template__") + + if template is not None: + lineno = template.get_corresponding_lineno(tb.tb_lineno) + fake_tb = fake_traceback(exc_value, tb, template.filename, lineno) + stack.append(fake_tb) + else: + stack.append(tb) + + tb = tb.tb_next + + tb_next = None + + # Assign tb_next in reverse to avoid circular references. + for tb in reversed(stack): + tb.tb_next = tb_next + tb_next = tb + + return exc_value.with_traceback(tb_next) + + +def fake_traceback( # type: ignore + exc_value: BaseException, tb: t.Optional[TracebackType], filename: str, lineno: int +) -> TracebackType: + """Produce a new traceback object that looks like it came from the + template source instead of the compiled code. The filename, line + number, and location name will point to the template, and the local + variables will be the current template context. + + :param exc_value: The original exception to be re-raised to create + the new traceback. + :param tb: The original traceback to get the local variables and + code info from. + :param filename: The template filename. + :param lineno: The line number in the template source. + """ + if tb is not None: + # Replace the real locals with the context that would be + # available at that point in the template. + locals = get_template_locals(tb.tb_frame.f_locals) + locals.pop("__jinja_exception__", None) + else: + locals = {} + + globals = { + "__name__": filename, + "__file__": filename, + "__jinja_exception__": exc_value, + } + # Raise an exception at the correct line number. + code: CodeType = compile( + "\n" * (lineno - 1) + "raise __jinja_exception__", filename, "exec" + ) + + # Build a new code object that points to the template file and + # replaces the location with a block name. + location = "template" + + if tb is not None: + function = tb.tb_frame.f_code.co_name + + if function == "root": + location = "top-level template code" + elif function.startswith("block_"): + location = f"block {function[6:]!r}" + + if sys.version_info >= (3, 8): + code = code.replace(co_name=location) + else: + code = CodeType( + code.co_argcount, + code.co_kwonlyargcount, + code.co_nlocals, + code.co_stacksize, + code.co_flags, + code.co_code, + code.co_consts, + code.co_names, + code.co_varnames, + code.co_filename, + location, + code.co_firstlineno, + code.co_lnotab, + code.co_freevars, + code.co_cellvars, + ) + + # Execute the new code, which is guaranteed to raise, and return + # the new traceback without this frame. + try: + exec(code, globals, locals) + except BaseException: + return sys.exc_info()[2].tb_next # type: ignore + + +def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]: + """Based on the runtime locals, get the context that would be + available at that point in the template. + """ + # Start with the current template context. + ctx: "t.Optional[Context]" = real_locals.get("context") + + if ctx is not None: + data: t.Dict[str, t.Any] = ctx.get_all().copy() + else: + data = {} + + # Might be in a derived context that only sets local variables + # rather than pushing a context. Local variables follow the scheme + # l_depth_name. Find the highest-depth local that has a value for + # each name. + local_overrides: t.Dict[str, t.Tuple[int, t.Any]] = {} + + for name, value in real_locals.items(): + if not name.startswith("l_") or value is missing: + # Not a template variable, or no longer relevant. + continue + + try: + _, depth_str, name = name.split("_", 2) + depth = int(depth_str) + except ValueError: + continue + + cur_depth = local_overrides.get(name, (-1,))[0] + + if cur_depth < depth: + local_overrides[name] = (depth, value) + + # Modify the context with any derived context. + for name, (_, value) in local_overrides.items(): + if value is missing: + data.pop(name, None) + else: + data[name] = value + + return data diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2/defaults.py b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/defaults.py new file mode 100644 index 000000000..638cad3d2 --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/defaults.py @@ -0,0 +1,48 @@ +import typing as t + +from .filters import FILTERS as DEFAULT_FILTERS # noqa: F401 +from .tests import TESTS as DEFAULT_TESTS # noqa: F401 +from .utils import Cycler +from .utils import generate_lorem_ipsum +from .utils import Joiner +from .utils import Namespace + +if t.TYPE_CHECKING: + import typing_extensions as te + +# defaults for the parser / lexer +BLOCK_START_STRING = "{%" +BLOCK_END_STRING = "%}" +VARIABLE_START_STRING = "{{" +VARIABLE_END_STRING = "}}" +COMMENT_START_STRING = "{#" +COMMENT_END_STRING = "#}" +LINE_STATEMENT_PREFIX: t.Optional[str] = None +LINE_COMMENT_PREFIX: t.Optional[str] = None +TRIM_BLOCKS = False +LSTRIP_BLOCKS = False +NEWLINE_SEQUENCE: "te.Literal['\\n', '\\r\\n', '\\r']" = "\n" +KEEP_TRAILING_NEWLINE = False + +# default filters, tests and namespace + +DEFAULT_NAMESPACE = { + "range": range, + "dict": dict, + "lipsum": generate_lorem_ipsum, + "cycler": Cycler, + "joiner": Joiner, + "namespace": Namespace, +} + +# default policies +DEFAULT_POLICIES: t.Dict[str, t.Any] = { + "compiler.ascii_str": True, + "urlize.rel": "noopener", + "urlize.target": None, + "urlize.extra_schemes": None, + "truncate.leeway": 5, + "json.dumps_function": None, + "json.dumps_kwargs": {"sort_keys": True}, + "ext.i18n.trimmed": False, +} diff --git a/lib/go-jinja2/internal/data/darwin-amd64/jinja2/environment.py b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/environment.py new file mode 100644 index 000000000..1d3be0bed --- /dev/null +++ b/lib/go-jinja2/internal/data/darwin-amd64/jinja2/environment.py @@ -0,0 +1,1675 @@ +"""Classes for managing templates and their runtime and compile time +options. +""" + +import os +import typing +import typing as t +import weakref +from collections import ChainMap +from functools import lru_cache +from functools import partial +from functools import reduce +from types import CodeType + +from markupsafe import Markup + +from . import nodes +from .compiler import CodeGenerator +from .compiler import generate +from .defaults import BLOCK_END_STRING +from .defaults import BLOCK_START_STRING +from .defaults import COMMENT_END_STRING +from .defaults import COMMENT_START_STRING +from .defaults import DEFAULT_FILTERS # type: ignore[attr-defined] +from .defaults import DEFAULT_NAMESPACE +from .defaults import DEFAULT_POLICIES +from .defaults import DEFAULT_TESTS # type: ignore[attr-defined] +from .defaults import KEEP_TRAILING_NEWLINE +from .defaults import LINE_COMMENT_PREFIX +from .defaults import LINE_STATEMENT_PREFIX +from .defaults import LSTRIP_BLOCKS +from .defaults import NEWLINE_SEQUENCE +from .defaults import TRIM_BLOCKS +from .defaults import VARIABLE_END_STRING +from .defaults import VARIABLE_START_STRING +from .exceptions import TemplateNotFound +from .exceptions import TemplateRuntimeError +from .exceptions import TemplatesNotFound +from .exceptions import TemplateSyntaxError +from .exceptions import UndefinedError +from .lexer import get_lexer +from .lexer import Lexer +from .lexer import TokenStream +from .nodes import EvalContext +from .parser import Parser +from .runtime import Context +from .runtime import new_context +from .runtime import Undefined +from .utils import _PassArg +from .utils import concat +from .utils import consume +from .utils import import_string +from .utils import internalcode +from .utils import LRUCache +from .utils import missing + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .bccache import BytecodeCache + from .ext import Extension + from .loaders import BaseLoader + +_env_bound = t.TypeVar("_env_bound", bound="Environment") + + +# for direct template usage we have up to ten living environments +@lru_cache(maxsize=10) +def get_spontaneous_environment(cls: t.Type[_env_bound], *args: t.Any) -> _env_bound: + """Return a new spontaneous environment. A spontaneous environment + is used for templates created directly rather than through an + existing environment. + + :param cls: Environment class to create. + :param args: Positional arguments passed to environment. + """ + env = cls(*args) + env.shared = True + return env + + +def create_cache( + size: int, +) -> t.Optional[t.MutableMapping[t.Tuple["weakref.ref[t.Any]", str], "Template"]]: + """Return the cache class for the given size.""" + if size == 0: + return None + + if size < 0: + return {} + + return LRUCache(size) # type: ignore + + +def copy_cache( + cache: t.Optional[t.MutableMapping[t.Any, t.Any]], +) -> t.Optional[t.MutableMapping[t.Tuple["weakref.ref[t.Any]", str], "Template"]]: + """Create an empty copy of the given cache.""" + if cache is None: + return None + + if type(cache) is dict: # noqa E721 + return {} + + return LRUCache(cache.capacity) # type: ignore + + +def load_extensions( + environment: "Environment", + extensions: t.Sequence[t.Union[str, t.Type["Extension"]]], +) -> t.Dict[str, "Extension"]: + """Load the extensions from the list and bind it to the environment. + Returns a dict of instantiated extensions. + """ + result = {} + + for extension in extensions: + if isinstance(extension, str): + extension = t.cast(t.Type["Extension"], import_string(extension)) + + result[extension.identifier] = extension(environment) + + return result + + +def _environment_config_check(environment: "Environment") -> "Environment": + """Perform a sanity check on the environment.""" + assert issubclass( + environment.undefined, Undefined + ), "'undefined' must be a subclass of 'jinja2.Undefined'." + assert ( + environment.block_start_string + != environment.variable_start_string + != environment.comment_start_string + ), "block, variable and comment start strings must be different." + assert environment.newline_sequence in { + "\r", + "\r\n", + "\n", + }, "'newline_sequence' must be one of '\\n', '\\r\\n', or '\\r'." + return environment + + +class Environment: + r"""The core component of Jinja is the `Environment`. It contains + important shared variables like configuration, filters, tests, + globals and others. Instances of this class may be modified if + they are not shared and if no template was loaded so far. + Modifications on environments after the first template was loaded + will lead to surprising effects and undefined behavior. + + Here are the possible initialization parameters: + + `block_start_string` + The string marking the beginning of a block. Defaults to ``'{%'``. + + `block_end_string` + The string marking the end of a block. Defaults to ``'%}'``. + + `variable_start_string` + The string marking the beginning of a print statement. + Defaults to ``'{{'``. + + `variable_end_string` + The string marking the end of a print statement. Defaults to + ``'}}'``. + + `comment_start_string` + The string marking the beginning of a comment. Defaults to ``'{#'``. + + `comment_end_string` + The string marking the end of a comment. Defaults to ``'#}'``. + + `line_statement_prefix` + If given and a string, this will be used as prefix for line based + statements. See also :ref:`line-statements`. + + `line_comment_prefix` + If given and a string, this will be used as prefix for line based + comments. See also :ref:`line-statements`. + + .. versionadded:: 2.2 + + `trim_blocks` + If this is set to ``True`` the first newline after a block is + removed (block, not variable tag!). Defaults to `False`. + + `lstrip_blocks` + If this is set to ``True`` leading spaces and tabs are stripped + from the start of a line to a block. Defaults to `False`. + + `newline_sequence` + The sequence that starts a newline. Must be one of ``'\r'``, + ``'\n'`` or ``'\r\n'``. The default is ``'\n'`` which is a + useful default for Linux and OS X systems as well as web + applications. + + `keep_trailing_newline` + Preserve the trailing newline when rendering templates. + The default is ``False``, which causes a single newline, + if present, to be stripped from the end of the template. + + .. versionadded:: 2.7 + + `extensions` + List of Jinja extensions to use. This can either be import paths + as strings or extension classes. For more information have a + look at :ref:`the extensions documentation `. + + `optimized` + should the optimizer be enabled? Default is ``True``. + + `undefined` + :class:`Undefined` or a subclass of it that is used to represent + undefined values in the template. + + `finalize` + A callable that can be used to process the result of a variable + expression before it is output. For example one can convert + ``None`` implicitly into an empty string here. + + `autoescape` + If set to ``True`` the XML/HTML autoescaping feature is enabled by + default. For more details about autoescaping see + :class:`~markupsafe.Markup`. As of Jinja 2.4 this can also + be a callable that is passed the template name and has to + return ``True`` or ``False`` depending on autoescape should be + enabled by default. + + .. versionchanged:: 2.4 + `autoescape` can now be a function + + `loader` + The template loader for this environment. + + `cache_size` + The size of the cache. Per default this is ``400`` which means + that if more than 400 templates are loaded the loader will clean + out the least recently used template. If the cache size is set to + ``0`` templates are recompiled all the time, if the cache size is + ``-1`` the cache will not be cleaned. + + .. versionchanged:: 2.8 + The cache size was increased to 400 from a low 50. + + `auto_reload` + Some loaders load templates from locations where the template + sources may change (ie: file system or database). If + ``auto_reload`` is set to ``True`` (default) every time a template is + requested the loader checks if the source changed and if yes, it + will reload the template. For higher performance it's possible to + disable that. + + `bytecode_cache` + If set to a bytecode cache object, this object will provide a + cache for the internal Jinja bytecode so that templates don't + have to be parsed if they were not changed. + + See :ref:`bytecode-cache` for more information. + + `enable_async` + If set to true this enables async template execution which + allows using async functions and generators. + """ + + #: if this environment is sandboxed. Modifying this variable won't make + #: the environment sandboxed though. For a real sandboxed environment + #: have a look at jinja2.sandbox. This flag alone controls the code + #: generation by the compiler. + sandboxed = False + + #: True if the environment is just an overlay + overlayed = False + + #: the environment this environment is linked to if it is an overlay + linked_to: t.Optional["Environment"] = None + + #: shared environments have this set to `True`. A shared environment + #: must not be modified + shared = False + + #: the class that is used for code generation. See + #: :class:`~jinja2.compiler.CodeGenerator` for more information. + code_generator_class: t.Type["CodeGenerator"] = CodeGenerator + + concat = "".join + + #: the context class that is used for templates. See + #: :class:`~jinja2.runtime.Context` for more information. + context_class: t.Type[Context] = Context + + template_class: t.Type["Template"] + + def __init__( + self, + block_start_string: str = BLOCK_START_STRING, + block_end_string: str = BLOCK_END_STRING, + variable_start_string: str = VARIABLE_START_STRING, + variable_end_string: str = VARIABLE_END_STRING, + comment_start_string: str = COMMENT_START_STRING, + comment_end_string: str = COMMENT_END_STRING, + line_statement_prefix: t.Optional[str] = LINE_STATEMENT_PREFIX, + line_comment_prefix: t.Optional[str] = LINE_COMMENT_PREFIX, + trim_blocks: bool = TRIM_BLOCKS, + lstrip_blocks: bool = LSTRIP_BLOCKS, + newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = NEWLINE_SEQUENCE, + keep_trailing_newline: bool = KEEP_TRAILING_NEWLINE, + extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = (), + optimized: bool = True, + undefined: t.Type[Undefined] = Undefined, + finalize: t.Optional[t.Callable[..., t.Any]] = None, + autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = False, + loader: t.Optional["BaseLoader"] = None, + cache_size: int = 400, + auto_reload: bool = True, + bytecode_cache: t.Optional["BytecodeCache"] = None, + enable_async: bool = False, + ): + # !!Important notice!! + # The constructor accepts quite a few arguments that should be + # passed by keyword rather than position. However it's important to + # not change the order of arguments because it's used at least + # internally in those cases: + # - spontaneous environments (i18n extension and Template) + # - unittests + # If parameter changes are required only add parameters at the end + # and don't change the arguments (or the defaults!) of the arguments + # existing already. + + # lexer / parser information + self.block_start_string = block_start_string + self.block_end_string = block_end_string + self.variable_start_string = variable_start_string + self.variable_end_string = variable_end_string + self.comment_start_string = comment_start_string + self.comment_end_string = comment_end_string + self.line_statement_prefix = line_statement_prefix + self.line_comment_prefix = line_comment_prefix + self.trim_blocks = trim_blocks + self.lstrip_blocks = lstrip_blocks + self.newline_sequence = newline_sequence + self.keep_trailing_newline = keep_trailing_newline + + # runtime information + self.undefined: t.Type[Undefined] = undefined + self.optimized = optimized + self.finalize = finalize + self.autoescape = autoescape + + # defaults + self.filters = DEFAULT_FILTERS.copy() + self.tests = DEFAULT_TESTS.copy() + self.globals = DEFAULT_NAMESPACE.copy() + + # set the loader provided + self.loader = loader + self.cache = create_cache(cache_size) + self.bytecode_cache = bytecode_cache + self.auto_reload = auto_reload + + # configurable policies + self.policies = DEFAULT_POLICIES.copy() + + # load extensions + self.extensions = load_extensions(self, extensions) + + self.is_async = enable_async + _environment_config_check(self) + + def add_extension(self, extension: t.Union[str, t.Type["Extension"]]) -> None: + """Adds an extension after the environment was created. + + .. versionadded:: 2.5 + """ + self.extensions.update(load_extensions(self, [extension])) + + def extend(self, **attributes: t.Any) -> None: + """Add the items to the instance of the environment if they do not exist + yet. This is used by :ref:`extensions ` to register + callbacks and configuration values without breaking inheritance. + """ + for key, value in attributes.items(): + if not hasattr(self, key): + setattr(self, key, value) + + def overlay( + self, + block_start_string: str = missing, + block_end_string: str = missing, + variable_start_string: str = missing, + variable_end_string: str = missing, + comment_start_string: str = missing, + comment_end_string: str = missing, + line_statement_prefix: t.Optional[str] = missing, + line_comment_prefix: t.Optional[str] = missing, + trim_blocks: bool = missing, + lstrip_blocks: bool = missing, + newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = missing, + keep_trailing_newline: bool = missing, + extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = missing, + optimized: bool = missing, + undefined: t.Type[Undefined] = missing, + finalize: t.Optional[t.Callable[..., t.Any]] = missing, + autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = missing, + loader: t.Optional["BaseLoader"] = missing, + cache_size: int = missing, + auto_reload: bool = missing, + bytecode_cache: t.Optional["BytecodeCache"] = missing, + enable_async: bool = False, + ) -> "Environment": + """Create a new overlay environment that shares all the data with the + current environment except for cache and the overridden attributes. + Extensions cannot be removed for an overlayed environment. An overlayed + environment automatically gets all the extensions of the environment it + is linked to plus optional extra extensions. + + Creating overlays should happen after the initial environment was set + up completely. Not all attributes are truly linked, some are just + copied over so modifications on the original environment may not shine + through. + + .. versionchanged:: 3.1.2 + Added the ``newline_sequence``,, ``keep_trailing_newline``, + and ``enable_async`` parameters to match ``__init__``. + """ + args = dict(locals()) + del args["self"], args["cache_size"], args["extensions"], args["enable_async"] + + rv = object.__new__(self.__class__) + rv.__dict__.update(self.__dict__) + rv.overlayed = True + rv.linked_to = self + + for key, value in args.items(): + if value is not missing: + setattr(rv, key, value) + + if cache_size is not missing: + rv.cache = create_cache(cache_size) + else: + rv.cache = copy_cache(self.cache) + + rv.extensions = {} + for key, value in self.extensions.items(): + rv.extensions[key] = value.bind(rv) + if extensions is not missing: + rv.extensions.update(load_extensions(rv, extensions)) + + if enable_async is not missing: + rv.is_async = enable_async + + return _environment_config_check(rv) + + @property + def lexer(self) -> Lexer: + """The lexer for this environment.""" + return get_lexer(self) + + def iter_extensions(self) -> t.Iterator["Extension"]: + """Iterates over the extensions by priority.""" + return iter(sorted(self.extensions.values(), key=lambda x: x.priority)) + + def getitem( + self, obj: t.Any, argument: t.Union[str, t.Any] + ) -> t.Union[t.Any, Undefined]: + """Get an item or attribute of an object but prefer the item.""" + try: + return obj[argument] + except (AttributeError, TypeError, LookupError): + if isinstance(argument, str): + try: + attr = str(argument) + except Exception: + pass + else: + try: + return getattr(obj, attr) + except AttributeError: + pass + return self.undefined(obj=obj, name=argument) + + def getattr(self, obj: t.Any, attribute: str) -> t.Any: + """Get an item or attribute of an object but prefer the attribute. + Unlike :meth:`getitem` the attribute *must* be a string. + """ + try: + return getattr(obj, attribute) + except AttributeError: + pass + try: + return obj[attribute] + except (TypeError, LookupError, AttributeError): + return self.undefined(obj=obj, name=attribute) + + def _filter_test_common( + self, + name: t.Union[str, Undefined], + value: t.Any, + args: t.Optional[t.Sequence[t.Any]], + kwargs: t.Optional[t.Mapping[str, t.Any]], + context: t.Optional[Context], + eval_ctx: t.Optional[EvalContext], + is_filter: bool, + ) -> t.Any: + if is_filter: + env_map = self.filters + type_name = "filter" + else: + env_map = self.tests + type_name = "test" + + func = env_map.get(name) # type: ignore + + if func is None: + msg = f"No {type_name} named {name!r}." + + if isinstance(name, Undefined): + try: + name._fail_with_undefined_error() + except Exception as e: + msg = f"{msg} ({e}; did you forget to quote the callable name?)" + + raise TemplateRuntimeError(msg) + + args = [value, *(args if args is not None else ())] + kwargs = kwargs if kwargs is not None else {} + pass_arg = _PassArg.from_obj(func) + + if pass_arg is _PassArg.context: + if context is None: + raise TemplateRuntimeError( + f"Attempted to invoke a context {type_name} without context." + ) + + args.insert(0, context) + elif pass_arg is _PassArg.eval_context: + if eval_ctx is None: + if context is not None: + eval_ctx = context.eval_ctx + else: + eval_ctx = EvalContext(self) + + args.insert(0, eval_ctx) + elif pass_arg is _PassArg.environment: + args.insert(0, self) + + return func(*args, **kwargs) + + def call_filter( + self, + name: str, + value: t.Any, + args: t.Optional[t.Sequence[t.Any]] = None, + kwargs: t.Optional[t.Mapping[str, t.Any]] = None, + context: t.Optional[Context] = None, + eval_ctx: t.Optional[EvalContext] = None, + ) -> t.Any: + """Invoke a filter on a value the same way the compiler does. + + This might return a coroutine if the filter is running from an + environment in async mode and the filter supports async + execution. It's your responsibility to await this if needed. + + .. versionadded:: 2.7 + """ + return self._filter_test_common( + name, value, args, kwargs, context, eval_ctx, True + ) + + def call_test( + self, + name: str, + value: t.Any, + args: t.Optional[t.Sequence[t.Any]] = None, + kwargs: t.Optional[t.Mapping[str, t.Any]] = None, + context: t.Optional[Context] = None, + eval_ctx: t.Optional[EvalContext] = None, + ) -> t.Any: + """Invoke a test on a value the same way the compiler does. + + This might return a coroutine if the test is running from an + environment in async mode and the test supports async execution. + It's your responsibility to await this if needed. + + .. versionchanged:: 3.0 + Tests support ``@pass_context``, etc. decorators. Added + the ``context`` and ``eval_ctx`` parameters. + + .. versionadded:: 2.7 + """ + return self._filter_test_common( + name, value, args, kwargs, context, eval_ctx, False + ) + + @internalcode + def parse( + self, + source: str, + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + ) -> nodes.Template: + """Parse the sourcecode and return the abstract syntax tree. This + tree of nodes is used by the compiler to convert the template into + executable source- or bytecode. This is useful for debugging or to + extract information from templates. + + If you are :ref:`developing Jinja extensions ` + this gives you a good overview of the node tree generated. + """ + try: + return self._parse(source, name, filename) + except TemplateSyntaxError: + self.handle_exception(source=source) + + def _parse( + self, source: str, name: t.Optional[str], filename: t.Optional[str] + ) -> nodes.Template: + """Internal parsing function used by `parse` and `compile`.""" + return Parser(self, source, name, filename).parse() + + def lex( + self, + source: str, + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + ) -> t.Iterator[t.Tuple[int, str, str]]: + """Lex the given sourcecode and return a generator that yields + tokens as tuples in the form ``(lineno, token_type, value)``. + This can be useful for :ref:`extension development ` + and debugging templates. + + This does not perform preprocessing. If you want the preprocessing + of the extensions to be applied you have to filter source through + the :meth:`preprocess` method. + """ + source = str(source) + try: + return self.lexer.tokeniter(source, name, filename) + except TemplateSyntaxError: + self.handle_exception(source=source) + + def preprocess( + self, + source: str, + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + ) -> str: + """Preprocesses the source with all extensions. This is automatically + called for all parsing and compiling methods but *not* for :meth:`lex` + because there you usually only want the actual source tokenized. + """ + return reduce( + lambda s, e: e.preprocess(s, name, filename), + self.iter_extensions(), + str(source), + ) + + def _tokenize( + self, + source: str, + name: t.Optional[str], + filename: t.Optional[str] = None, + state: t.Optional[str] = None, + ) -> TokenStream: + """Called by the parser to do the preprocessing and filtering + for all the extensions. Returns a :class:`~jinja2.lexer.TokenStream`. + """ + source = self.preprocess(source, name, filename) + stream = self.lexer.tokenize(source, name, filename, state) + + for ext in self.iter_extensions(): + stream = ext.filter_stream(stream) # type: ignore + + if not isinstance(stream, TokenStream): + stream = TokenStream(stream, name, filename) + + return stream + + def _generate( + self, + source: nodes.Template, + name: t.Optional[str], + filename: t.Optional[str], + defer_init: bool = False, + ) -> str: + """Internal hook that can be overridden to hook a different generate + method in. + + .. versionadded:: 2.5 + """ + return generate( # type: ignore + source, + self, + name, + filename, + defer_init=defer_init, + optimized=self.optimized, + ) + + def _compile(self, source: str, filename: str) -> CodeType: + """Internal hook that can be overridden to hook a different compile + method in. + + .. versionadded:: 2.5 + """ + return compile(source, filename, "exec") + + @typing.overload + def compile( # type: ignore + self, + source: t.Union[str, nodes.Template], + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + raw: "te.Literal[False]" = False, + defer_init: bool = False, + ) -> CodeType: ... + + @typing.overload + def compile( + self, + source: t.Union[str, nodes.Template], + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + raw: "te.Literal[True]" = ..., + defer_init: bool = False, + ) -> str: ... + + @internalcode + def compile( + self, + source: t.Union[str, nodes.Template], + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + raw: bool = False, + defer_init: bool = False, + ) -> t.Union[str, CodeType]: + """Compile a node or template source code. The `name` parameter is + the load name of the template after it was joined using + :meth:`join_path` if necessary, not the filename on the file system. + the `filename` parameter is the estimated filename of the template on + the file system. If the template came from a database or memory this + can be omitted. + + The return value of this method is a python code object. If the `raw` + parameter is `True` the return value will be a string with python + code equivalent to the bytecode returned otherwise. This method is + mainly used internally. + + `defer_init` is use internally to aid the module code generator. This + causes the generated code to be able to import without the global + environment variable to be set. + + .. versionadded:: 2.4 + `defer_init` parameter added. + """ + source_hint = None + try: + if isinstance(source, str): + source_hint = source + source = self._parse(source, name, filename) + source = self._generate(source, name, filename, defer_init=defer_init) + if raw: + return source + if filename is None: + filename = "