From 51dc4e76b2be5018d359ed010b04e90ae65e6858 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Thu, 7 May 2026 16:06:55 +0200 Subject: [PATCH 01/13] test: add integration coverage and support templating engine annotation --- .github/workflows/release-pullrequest.yaml | 24 +- .github/workflows/release-tag.yaml | 36 +- go.mod | 38 +- go.sum | 80 +- internal/clients/git/git.go | 27 +- internal/controllers/repo/repo.go | 98 ++- internal/controllers/repo/repo_test.go | 836 +++++++++++++++++++++ internal/tools/copier/copier.go | 2 +- internal/tools/copier/copier_test.go | 5 - internal/utils/errors.go | 19 + 10 files changed, 1017 insertions(+), 148 deletions(-) create mode 100644 internal/controllers/repo/repo_test.go create mode 100644 internal/utils/errors.go diff --git a/.github/workflows/release-pullrequest.yaml b/.github/workflows/release-pullrequest.yaml index 9d9ffe8..d958d34 100644 --- a/.github/workflows/release-pullrequest.yaml +++ b/.github/workflows/release-pullrequest.yaml @@ -13,11 +13,11 @@ jobs: name: Build and Push strategy: matrix: - runner: [buildjet-2vcpu-ubuntu-2204-arm, buildjet-2vcpu-ubuntu-2204] + runner: [ubuntu-22.04-arm, ubuntu-22.04] include: - - runner: buildjet-2vcpu-ubuntu-2204-arm + - runner: ubuntu-22.04-arm platform: linux/arm64 - - runner: buildjet-2vcpu-ubuntu-2204 + - runner: ubuntu-22.04 platform: linux/amd64 runs-on: ${{ matrix.runner }} @@ -33,23 +33,23 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.GHCR_REPO }} - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and NOT push id: build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: platforms: ${{ matrix.platform }} push: false @@ -58,15 +58,15 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 with: go-version: 'stable' - name: Gather dependencies run: go mod download - name: Run coverage - run: go test -race -tags=unit,integration -p 1 -coverprofile=coverage.txt -timeout 30m -covermode=atomic ./... + run: go test -race -tags=unit,integration -p 1 -timeout 30m -coverprofile=coverage.txt -covermode=atomic ./... -coverpkg=./... - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v6 with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release-tag.yaml b/.github/workflows/release-tag.yaml index 7e6b2f3..82958db 100644 --- a/.github/workflows/release-tag.yaml +++ b/.github/workflows/release-tag.yaml @@ -2,7 +2,7 @@ name: release-tag on: push: - tags: [ '[0-9]+.[0-9]+.[0-9]+' ] + tags: [ '[0-9]+.[0-9]+.[0-9]+', '[0-9]+.[0-9]+.[0-9]+-*' ] env: GHCR_REPO: ghcr.io/${{ github.repository }} @@ -13,11 +13,11 @@ jobs: strategy: fail-fast: false matrix: - runner: [buildjet-2vcpu-ubuntu-2204-arm, buildjet-2vcpu-ubuntu-2204] + runner: [ubuntu-22.04-arm, ubuntu-22.04] include: - - runner: buildjet-2vcpu-ubuntu-2204-arm + - runner: ubuntu-22.04-arm platform: linux/arm64 - - runner: buildjet-2vcpu-ubuntu-2204 + - runner: ubuntu-22.04 platform: linux/amd64 runs-on: ${{ matrix.runner }} @@ -33,23 +33,23 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.GHCR_REPO }} - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and push by digest id: build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: platforms: ${{ matrix.platform }} push: true @@ -63,7 +63,7 @@ jobs: touch "/tmp/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: digests-${{ env.PLATFORM_PAIR }} path: /tmp/digests/* @@ -79,25 +79,25 @@ jobs: packages: write steps: - name: Download digests - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: /tmp/digests pattern: digests-* merge-multiple: true - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.GHCR_REPO }} @@ -114,20 +114,18 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 with: go-version: 'stable' - name: Gather dependencies run: go mod download - name: Run coverage - run: go test -race -tags=unit,integration -p 1 -coverprofile=coverage.txt -timeout 30m -covermode=atomic ./... + run: go test -race -tags=unit,integration -p 1 -timeout 30m -coverprofile=coverage.txt -covermode=atomic ./... -coverpkg=./... - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} - - update-crd-doc: runs-on: ubuntu-latest steps: diff --git a/go.mod b/go.mod index 85b78f5..31886fe 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,10 @@ go 1.25.0 require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/cbroglie/mustache v1.4.0 - github.com/go-git/go-billy/v5 v5.6.2 + github.com/evanphx/json-patch/v5 v5.9.11 + github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 - github.com/go-git/go-git/v5 v5.13.1 + github.com/go-git/go-git/v5 v5.19.0 github.com/go-logr/logr v1.4.3 github.com/krateoplatformops/plumbing v0.7.2 github.com/krateoplatformops/provider-runtime v0.10.2 @@ -16,7 +17,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/stoewer/go-strcase v1.3.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 k8s.io/api v0.34.1 k8s.io/apiextensions-apiserver v0.34.1 k8s.io/apimachinery v0.34.1 @@ -31,14 +32,14 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.1.5 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.6.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/cyphar/filepath-securejoin v0.4.0 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect @@ -46,7 +47,6 @@ require ( github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.9.11 // 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 @@ -73,6 +73,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect @@ -80,7 +81,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/mmcloughlin/avo v0.6.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -89,16 +89,16 @@ require ( 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.1.1 // indirect - github.com/pjbgf/sha1cd v0.3.1 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/skeema/knownhosts v1.3.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.7 // indirect @@ -112,16 +112,16 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.43.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/tools v0.43.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.36.7 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index c504a25..68ff8f4 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= -github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= 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/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= @@ -29,16 +29,16 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 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/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= 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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.4.0 h1:PioTG9TBRSApBpYGnDU8HC+miIsX8vitBH9LGNNMoLQ= -github.com/cyphar/filepath-securejoin v0.4.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -49,8 +49,8 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= 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/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= -github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= +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= @@ -73,12 +73,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 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-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= 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.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= -github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= +github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= +github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -141,6 +141,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 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= @@ -165,8 +167,6 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY= -github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8= 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/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg= @@ -197,8 +197,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pjbgf/sha1cd v0.3.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI= -github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -214,8 +214,8 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= @@ -224,8 +224,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG 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.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= @@ -247,8 +247,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vladimirvivien/gexe v0.4.1 h1:W9gWkp8vSPjDoXDu04Yp4KljpVMaSt8IQuHswLDd5LY= github.com/vladimirvivien/gexe v0.4.1/go.mod h1:3gjgTqE2c0VyHnU5UOIwk7gyNzZDGulPb/DJPgcw64E= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -291,28 +291,28 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= 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.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= 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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -323,24 +323,24 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= 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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= diff --git a/internal/clients/git/git.go b/internal/clients/git/git.go index bfe2bac..f01c637 100644 --- a/internal/clients/git/git.go +++ b/internal/clients/git/git.go @@ -3,9 +3,7 @@ package git import ( "bytes" "context" - "errors" "fmt" - "io/fs" "net/http" "net/http/cookiejar" "net/url" @@ -33,6 +31,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/storage/filesystem" + "github.com/krateoplatformops/git-provider/internal/utils" "github.com/krateoplatformops/plumbing/ptr" ) @@ -42,10 +41,10 @@ var ( ) var ( - ErrRepositoryNotFound = errors.New("repository not found") - ErrEmptyRemoteRepository = errors.New("remote repository is empty") - ErrAuthenticationRequired = errors.New("authentication required") - ErrAuthorizationFailed = errors.New("authorization failed") + ErrRepositoryNotFound = fmt.Errorf("repository not found: %w", transport.ErrRepositoryNotFound) + ErrEmptyRemoteRepository = fmt.Errorf("remote repository is empty: %w", transport.ErrEmptyRemoteRepository) + ErrAuthenticationRequired = fmt.Errorf("authentication required: %w", transport.ErrAuthenticationRequired) + ErrAuthorizationFailed = fmt.Errorf("authorization failed: %w", transport.ErrAuthorizationFailed) NoErrAlreadyUpToDate = git.NoErrAlreadyUpToDate ) @@ -500,7 +499,7 @@ func Clone(opts CloneOptions) (*Repo, error) { Auth: opts.Auth, InsecureSkipTLS: opts.Insecure, } - if opts.AlternativeBranch != nil { + if opts.AlternativeBranch != nil && len(ptr.Deref(opts.AlternativeBranch, "")) > 0 { isOrphan = false cloneOpts.ReferenceName = plumbing.NewBranchReferenceName(ptr.Deref(opts.AlternativeBranch, "")) cloneOpts.SingleBranch = true @@ -514,19 +513,19 @@ func Clone(opts CloneOptions) (*Repo, error) { } res.repo, err = git.Clone(res.storer, res.fs, &cloneOpts) if err != nil { - if errors.Is(err, transport.ErrRepositoryNotFound) { + if utils.IsErr(ErrRepositoryNotFound, err) { return nil, ErrRepositoryNotFound } - if errors.Is(err, transport.ErrEmptyRemoteRepository) { + if utils.IsErr(ErrEmptyRemoteRepository, err) { return nil, ErrEmptyRemoteRepository } - if errors.Is(err, transport.ErrAuthenticationRequired) { + if utils.IsErr(ErrAuthenticationRequired, err) { return nil, ErrAuthenticationRequired } - if errors.Is(err, transport.ErrAuthorizationFailed) { + if utils.IsErr(ErrAuthorizationFailed, err) { return nil, ErrAuthorizationFailed } return nil, err @@ -548,8 +547,8 @@ func (s *Repo) Exists(path string) (bool, error) { defer s.setDefaultHTTPSClient() _, err := s.fs.Stat(path) if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return false, nil + if utils.IsErr(ErrRepositoryNotFound, err) { + return false, ErrRepositoryNotFound } return false, err @@ -761,7 +760,7 @@ func Pull(s *Repo, insecure bool) error { }) if err != nil { - if errors.Is(err, git.NoErrAlreadyUpToDate) { + if utils.IsErr(git.NoErrAlreadyUpToDate, err) { err = nil } } diff --git a/internal/controllers/repo/repo.go b/internal/controllers/repo/repo.go index 86691ea..c8cfec3 100644 --- a/internal/controllers/repo/repo.go +++ b/internal/controllers/repo/repo.go @@ -7,10 +7,7 @@ import ( "os" "strings" - "github.com/pkg/errors" - commonv1 "github.com/krateoplatformops/provider-runtime/apis/common/v1" - "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -27,22 +24,30 @@ import ( "github.com/krateoplatformops/git-provider/internal/clients/git" "github.com/krateoplatformops/git-provider/internal/controllers/common/option" "github.com/krateoplatformops/git-provider/internal/tools/copier" + "github.com/krateoplatformops/git-provider/internal/tools/template" + plumbingevent "github.com/krateoplatformops/plumbing/kubeutil/event" + "github.com/krateoplatformops/plumbing/kubeutil/eventrecorder" "github.com/krateoplatformops/plumbing/ptr" - - corev1 "k8s.io/api/core/v1" + record "k8s.io/client-go/tools/events" ) -const ( - errNotRepo = "managed resource is not a repo custom resource" +var ( + errNotRepo = fmt.Errorf("managed resource is not a repo custom resource") + homeDir string ) +const AnnotationTemplatingEngine = "krateo.io/templating-engine" + // Setup adds a controller that reconciles Token managed resources. func Setup(mgr ctrl.Manager, o option.SetupOptions) error { name := reconciler.ControllerName(repov1alpha1.RepoGroupKind) log := o.Controller.Logger.WithValues("controller", name) - recorder := mgr.GetEventRecorderFor(name) + recorder, err := eventrecorder.Create(context.Background(), mgr.GetConfig(), name, nil) + if err != nil { + return fmt.Errorf("failed to create event recorder: %w", err) + } r := reconciler.NewReconciler(mgr, resource.ManagedKind(repov1alpha1.RepoGroupVersionKind), @@ -53,7 +58,7 @@ func Setup(mgr ctrl.Manager, o option.SetupOptions) error { }), reconciler.WithPollInterval(o.Controller.PollInterval), reconciler.WithLogger(log), - reconciler.WithRecorder(event.NewAPIRecorder(recorder)), + reconciler.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), reconciler.WithTimeout(o.Controller.Timeout), ) @@ -73,7 +78,7 @@ type connector struct { func (c *connector) Connect(ctx context.Context, mg resource.Managed) (reconciler.ExternalClient, error) { cr, ok := mg.(*repov1alpha1.Repo) if !ok { - return nil, errors.New(errNotRepo) + return nil, errNotRepo } cfg, err := loadExternalClientOpts(ctx, c.kube, cr) @@ -88,11 +93,16 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (reconcile log := c.log.WithValues("name", cr.Name, "namespace", cr.Namespace) + rec := plumbingevent.NewAPIRecorder(c.recorder) + if rec == nil { + return nil, fmt.Errorf("failed to create event recorder") + } + return &external{ kube: c.kube, log: log, cfg: cfg, - rec: c.recorder, + rec: *rec, }, nil } @@ -102,15 +112,13 @@ type external struct { kube client.Client log logging.Logger cfg *externalClientOpts - rec record.EventRecorder + rec plumbingevent.APIRecorder } -var homeDir string - func (e *external) Observe(ctx context.Context, mg resource.Managed) (reconciler.ExternalObservation, error) { cr, ok := mg.(*repov1alpha1.Repo) if !ok { - return reconciler.ExternalObservation{}, errors.New(errNotRepo) + return reconciler.ExternalObservation{}, errNotRepo } e.log.Info("Observing resource") @@ -205,7 +213,7 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (reconciler func (e *external) Create(ctx context.Context, mg resource.Managed) error { cr, ok := mg.(*repov1alpha1.Repo) if !ok { - return errors.New(errNotRepo) + return errNotRepo } if !meta.IsActionAllowed(cr, meta.ActionCreate) { e.log.Debug("External resource should not be created by provider, skip creating.") @@ -219,7 +227,7 @@ func (e *external) Create(ctx context.Context, mg resource.Managed) error { func (e *external) Update(ctx context.Context, mg resource.Managed) error { cr, ok := mg.(*repov1alpha1.Repo) if !ok { - return errors.New(errNotRepo) + return errNotRepo } if !cr.Spec.EnableUpdate { @@ -239,7 +247,7 @@ func (e *external) Update(ctx context.Context, mg resource.Managed) error { func (e *external) Delete(ctx context.Context, mg resource.Managed) error { cr, ok := mg.(*repov1alpha1.Repo) if !ok { - return errors.New(errNotRepo) + return errNotRepo } if !meta.IsActionAllowed(cr, meta.ActionDelete) { e.log.Debug("External resource should not be deleted by provider, skip deleting.") @@ -278,13 +286,18 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM spec := cr.Spec.DeepCopy() + var altBranch *string + if len(spec.ToRepo.CloneFromBranch) > 0 { + altBranch = ptr.To(spec.ToRepo.CloneFromBranch) + } + toRepo, err := git.Clone(git.CloneOptions{ URL: spec.ToRepo.Url, Auth: e.cfg.ToRepoCreds, Insecure: e.cfg.Insecure, UnsupportedCapabilities: e.cfg.UnsupportedCapabilities, Branch: spec.ToRepo.Branch, - AlternativeBranch: ptr.To(cr.Spec.ToRepo.CloneFromBranch), + AlternativeBranch: altBranch, GitCookies: e.cfg.ToRepoCookieFile, HomeDir: homeDir, // Use the configured home directory for temporary files }) @@ -294,8 +307,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM defer toRepo.Cleanup() e.log.Debug("Target repo cloned", "url", spec.ToRepo.Url) - e.rec.Eventf(cr, corev1.EventTypeNormal, "TargetRepoCloned", - "Successfully cloned target repo: %s", spec.ToRepo.Url) + e.rec.Event(cr, plumbingevent.Normal("TargetRepoCloned", "Reconciling", fmt.Sprintf("Successfully cloned target repo: %s", spec.ToRepo.Url))) e.log.Debug(fmt.Sprintf("Target repo on branch %s", toRepo.CurrentBranch())) fromRepo, err := git.Clone(git.CloneOptions{ @@ -312,8 +324,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM } defer fromRepo.Cleanup() e.log.Debug("Origin repo cloned", "url", spec.FromRepo.Url) - e.rec.Eventf(cr, corev1.EventTypeNormal, "OriginRepoCloned", - "Successfully cloned origin repo: %s", spec.FromRepo.Url) + e.rec.Event(cr, plumbingevent.Normal("OriginRepoCloned", "Reconciling", fmt.Sprintf("Successfully cloned origin repo: %s", spec.FromRepo.Url))) e.log.Debug(fmt.Sprintf("Origin repo on branch %s", fromRepo.CurrentBranch())) fromRepoCommitId, err := fromRepo.GetLatestCommit(fromRepo.CurrentBranch()) @@ -335,8 +346,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM values, err = e.loadValuesFromConfigMap(ctx, spec.ConfigMapKeyRef) if err != nil { e.log.Debug("Unable to load configmap with template data", "msg", err.Error()) - e.rec.Eventf(cr, corev1.EventTypeWarning, "CannotLoadConfigMap", - "Unable to load configmap with template data: %s", err.Error()) + e.rec.Event(cr, plumbingevent.Warning("CannotLoadConfigMap", "Reconciling", fmt.Errorf("Unable to load configmap with template data: %s", err.Error()))) } e.log.Debug("Loaded values from config map", @@ -346,20 +356,36 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM "values", values, ) } - co, err := copier.NewCopier(fromRepo.FS(), toRepo.FS(), + opts := []copier.Option{ copier.WithOriginCopyPath(fromPath), copier.WithTargetCopyPath(toPath), copier.WithIgnorePath(spec.FromRepo.KrateoIgnorePath), - copier.WithMustacheTemplate(values), - ) + } + + if values != nil { + engine := cr.GetAnnotations()[AnnotationTemplatingEngine] + if engine == "gotemplate" { + tplVals := make([]template.TemplateValue, 0, len(values)) + for k, v := range values { + tplVals = append(tplVals, template.TemplateValue{ + Key: k, + Value: fmt.Sprintf("%v", v), + }) + } + opts = append(opts, copier.WithGoTemplate(tplVals)) + } else { + opts = append(opts, copier.WithMustacheTemplate(values)) + } + } + + co, err := copier.NewCopier(fromRepo.FS(), toRepo.FS(), opts...) if err != nil { return fmt.Errorf("unable to create copier: %w", err) } if override { e.log.Debug("Override is true, overriding all files in target repo") if fromPath == "/" && toPath == "/" { - e.rec.Eventf(cr, corev1.EventTypeWarning, "OverrideWarning", - "Override is set to true, but originPath and targetPath are both set to '/', this will override also service folders like .git, .github, .gitignore, etc. Consider using a different path for originPath or targetPath. This can broke the target repository causing the impossibility to push changes.") + e.rec.Event(cr, plumbingevent.Warning("OverrideWarning", "Reconciling", fmt.Errorf("Override is set to true, but originPath and targetPath are both set to '/', this will override also service folders like .git, .github, .gitignore, etc. Consider using a different path for originPath or targetPath. This can broke the target repository causing the impossibility to push changes."))) e.log.Info("Override is set to true, but originPath and targetPath are both set to '/', this will override also service folders like .git, .github, .gitignore, etc. Consider using a different path for originPath or targetPath. This can broke the target repository causing the impossibility to push changes.") } } @@ -372,8 +398,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM "toUrl", spec.ToRepo.Url, "fromPath", fromPath, "toPath", toPath) - e.rec.Eventf(cr, corev1.EventTypeNormal, "RepoSyncSuccess", - "Origin and target repo synchronized") + e.rec.Event(cr, plumbingevent.Normal("RepoSyncSuccess", "Reconciling", "Origin and target repo synchronized")) toRepoCommitIdObj, err := toRepo.Commit(".", commitMessage, &git.IndexOptions{ OriginRepo: fromRepo, @@ -387,8 +412,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM return fmt.Errorf("unable to get latest commit from target repo: %w", err) } e.log.Info("Target repo not commited", "branch", toRepo.CurrentBranch(), "status", "repository already up-to-date") - e.rec.Eventf(cr, corev1.EventTypeNormal, "RepoAlreadyUpToDate", - fmt.Sprintf("Target repo already up-to-date on branch %s", toRepo.CurrentBranch())) + e.rec.Event(cr, plumbingevent.Normal("RepoAlreadyUpToDate", "Reconciling", fmt.Sprintf("Target repo already up-to-date on branch %s", toRepo.CurrentBranch()))) meta.SetExternalName(cr, toRepoCommitId) cr.Status.OriginCommitId = fromRepoCommitId @@ -405,16 +429,14 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM return fmt.Errorf("unable to commit target repo: %w", err) } e.log.Info("Target repo committed", "branch", toRepo.CurrentBranch(), "commitId", toRepoCommitId) - e.rec.Eventf(cr, corev1.EventTypeNormal, "RepoCommitSuccess", - fmt.Sprintf("Target repo committed on branch %s", toRepo.CurrentBranch())) + e.rec.Event(cr, plumbingevent.Normal("RepoCommitSuccess", "Reconciling", fmt.Sprintf("Target repo committed on branch %s", toRepo.CurrentBranch()))) err = toRepo.Push("origin", toRepo.CurrentBranch(), e.cfg.Insecure) if err != nil { return fmt.Errorf("unable to push target repo: %w", err) } e.log.Info("Target repo pushed", "branch", toRepo.CurrentBranch(), "commitId", toRepoCommitId) - e.rec.Eventf(cr, corev1.EventTypeNormal, "RepoPushSuccess", - fmt.Sprintf("Target repo pushed branch %s", toRepo.CurrentBranch())) + e.rec.Event(cr, plumbingevent.Normal("RepoPushSuccess", "Reconciling", fmt.Sprintf("Target repo pushed branch %s", toRepo.CurrentBranch()))) meta.SetExternalName(cr, toRepoCommitId) cr.Status.OriginCommitId = fromRepoCommitId diff --git a/internal/controllers/repo/repo_test.go b/internal/controllers/repo/repo_test.go new file mode 100644 index 0000000..4e805a6 --- /dev/null +++ b/internal/controllers/repo/repo_test.go @@ -0,0 +1,836 @@ +//go:build integration +// +build integration + +package repo + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "net/netip" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/go-git/go-billy/v5" + githttp "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-logr/logr" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" + "github.com/stretchr/testify/require" + + "github.com/krateoplatformops/git-provider/apis" + repov1alpha1 "github.com/krateoplatformops/git-provider/apis/repo/v1alpha1" + gitclient "github.com/krateoplatformops/git-provider/internal/clients/git" + "github.com/krateoplatformops/git-provider/internal/controllers/common/option" + prettylog "github.com/krateoplatformops/plumbing/slogs/pretty" + commonv1 "github.com/krateoplatformops/provider-runtime/apis/common/v1" + "github.com/krateoplatformops/provider-runtime/pkg/controller" + "github.com/krateoplatformops/provider-runtime/pkg/logging" + "github.com/krateoplatformops/provider-runtime/pkg/ratelimiter" + + v1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientsetscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + xenv "github.com/krateoplatformops/plumbing/env" + "sigs.k8s.io/e2e-framework/klient/decoder" + "sigs.k8s.io/e2e-framework/klient/k8s/resources" + "sigs.k8s.io/e2e-framework/pkg/env" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/envfuncs" + "sigs.k8s.io/e2e-framework/pkg/features" + "sigs.k8s.io/e2e-framework/support/kind" +) + +var ( + testenv env.Environment + clusterName string +) + +const ( + crdPath = "../../../crds" + namespace = "test-system" + giteaBaseURL = "https://127.0.0.1:8443" + gitAuthSecretName = "git-creds" + gitBadAuthSecretName = "git-creds-bad" + repoContentPath = "/content" +) + +var ( + giteaUsername = "admin" + giteaPassword = "admin123" +) + +func TestMain(m *testing.M) { + xenv.SetTestMode(true) + + clusterName = "krateo-repo-provider-controller" + testenv = env.New() + kindCluster := kind.NewCluster(clusterName) + + _ = apiextensionsv1.AddToScheme(clientsetscheme.Scheme) + _ = apis.AddToScheme(clientsetscheme.Scheme) + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + defer cli.Close() + + var containerID string + + testenv.Setup( + envfuncs.CreateCluster(kindCluster, clusterName), + func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { + r, err := resources.New(cfg.Client().RESTConfig()) + if err != nil { + return ctx, err + } + return ctx, r.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}) + }, + func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { + tmpdir, err := os.MkdirTemp(os.TempDir(), "repo-test-gitea-*") + if err != nil { + return ctx, err + } + + imageName := "gitea/gitea:latest" + reader, err := cli.ImagePull(ctx, imageName, client.ImagePullOptions{}) + if err != nil { + return ctx, err + } + defer reader.Close() + _, _ = io.Copy(os.Stdout, reader) + + httpPort, _ := network.ParsePort("3000/tcp") + sshPort, _ := network.ParsePort("22/tcp") + httpsPort, _ := network.ParsePort("443/tcp") + + portBinding := network.PortMap{ + httpPort: []network.PortBinding{{HostIP: netip.AddrFrom4([4]byte{127, 0, 0, 1}), HostPort: "3000"}}, + sshPort: []network.PortBinding{{HostIP: netip.AddrFrom4([4]byte{127, 0, 0, 1}), HostPort: "2222"}}, + httpsPort: []network.PortBinding{{HostIP: netip.AddrFrom4([4]byte{127, 0, 0, 1}), HostPort: "8443"}}, + } + + containerConfig := &container.Config{ + Image: imageName, + ExposedPorts: network.PortSet{ + httpPort: struct{}{}, + sshPort: struct{}{}, + httpsPort: struct{}{}, + }, + Env: []string{ + "GITEA__database__DB_TYPE=sqlite3", + "GITEA__security__INSTALL_LOCK=true", + "USER_UID=1000", + "USER_GID=1000", + "GITEA__server__DOMAIN=127.0.0.1", + "GITEA__server__HTTP_PORT=443", + fmt.Sprintf("GITEA__server__ROOT_URL=%s", giteaBaseURL), + "GITEA__server__PROTOCOL=https", + "GITEA__server__CERT_FILE=/data/cert.pem", + "GITEA__server__KEY_FILE=/data/key.pem", + }, + Entrypoint: []string{"/bin/sh", "-c"}, + Cmd: []string{ + fmt.Sprintf(` + if [ ! -f /data/cert.pem ]; then + cd /data && /usr/local/bin/gitea cert --host localhost,127.0.0.1 --ca + fi + chown -R 1000:1000 /data + echo 'su-exec git /usr/local/bin/gitea migrate' >> /etc/s6/gitea/setup + echo 'su-exec git /usr/local/bin/gitea admin user create --username %s --password %s --email admin@local --admin --must-change-password=false' >> /etc/s6/gitea/setup + /usr/bin/entrypoint /usr/bin/s6-svscan /etc/s6 + `, giteaUsername, giteaPassword), + }, + } + + hostConfig := &container.HostConfig{ + PortBindings: portBinding, + RestartPolicy: container.RestartPolicy{ + Name: "always", + }, + Binds: []string{ + fmt.Sprintf("%s:/data", tmpdir), + }, + } + + resp, err := cli.ContainerCreate(ctx, client.ContainerCreateOptions{ + Config: containerConfig, + HostConfig: hostConfig, + NetworkingConfig: &network.NetworkingConfig{}, + Name: "gitea-repo-test", + }) + if err != nil { + return ctx, err + } + + go func() { + logsReader, err := cli.ContainerLogs(ctx, resp.ID, client.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + }) + if err != nil { + return + } + defer logsReader.Close() + _, _ = io.Copy(os.Stdout, logsReader) + }() + + _, err = cli.ContainerStart(ctx, resp.ID, client.ContainerStartOptions{}) + if err != nil { + return ctx, err + } + containerID = resp.ID + + return ctx, waitForGitea(ctx) + }, + func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { + r, err := resources.New(cfg.Client().RESTConfig()) + if err != nil { + return ctx, err + } + r.WithNamespace(namespace) + + err = decoder.DecodeEachFile( + ctx, os.DirFS(filepath.Join(crdPath)), "*.yaml", + decoder.CreateIgnoreAlreadyExists(r), + ) + return ctx, err + }, + ) + + testenv.Finish( + envfuncs.DestroyCluster(clusterName), + func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { + if containerID != "" { + _, _ = cli.ContainerStop(ctx, containerID, client.ContainerStopOptions{}) + _, _ = cli.ContainerRemove(ctx, containerID, client.ContainerRemoveOptions{Force: true}) + } + return ctx, nil + }, + ) + + os.Exit(testenv.Run(m)) +} + +func waitForGitea(ctx context.Context) error { + httpClient := newInsecureHTTPClient() + for i := 0; i < 30; i++ { + resp, err := httpClient.Get(giteaBaseURL + "/api/v1/swagger") + if err == nil && resp != nil { + _ = resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return nil + } + } + time.Sleep(2 * time.Second) + } + return fmt.Errorf("gitea not ready") +} + +func setupController(ctx context.Context, cfg *envconf.Config) (context.Context, error) { + lh := prettylog.New(&slog.HandlerOptions{ + Level: slog.LevelDebug, + AddSource: false, + }, + prettylog.WithDestinationWriter(os.Stderr), + prettylog.WithColor(), + prettylog.WithOutputEmptyAttrs(), + ) + + logrlog := logr.FromSlogHandler(slog.New(lh).Handler()) + log := logging.NewLogrLogger(logrlog) + + ctrl.SetLogger(logrlog) + + mgr, err := ctrl.NewManager(cfg.Client().RESTConfig(), ctrl.Options{ + Metrics: server.Options{ + BindAddress: "0", + }, + }) + if err != nil { + return ctx, err + } + + o := controller.Options{ + Logger: log, + MaxConcurrentReconciles: 1, + PollInterval: 2 * time.Second, + GlobalRateLimiter: ratelimiter.NewGlobalExponential(1*time.Second, 1*time.Minute), + } + + tmpdir, _ := os.MkdirTemp(os.TempDir(), "repo-test-home-*") + if err := Setup(mgr, option.SetupOptions{ + Controller: option.ControllerOptions{ + Options: o, + Timeout: 3 * time.Minute, + }, + Git: option.GitOptions{ + CommitAuthorName: "test-author", + CommitAuthorEmail: "test@email.com", + HomeDir: tmpdir, + }, + }); err != nil { + return ctx, err + } + + go func() { + if err := mgr.Start(ctx); err != nil { + panic(err) + } + }() + return ctx, nil +} + +func newInsecureHTTPClient() *http.Client { + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + Timeout: 10 * time.Second, + } +} + +func createGiteaRepo(t *testing.T, name, defaultBranch string) { + t.Helper() + + payload := map[string]any{ + "name": name, + "description": "integration test repository", + "private": false, + "auto_init": true, + "readme": "Default", + } + if defaultBranch != "" { + payload["default_branch"] = defaultBranch + } + + body, err := json.Marshal(payload) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, giteaBaseURL+"/api/v1/user/repos", strings.NewReader(string(body))) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.SetBasicAuth(giteaUsername, giteaPassword) + + resp, err := newInsecureHTTPClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusConflict, "unexpected status creating repo %s: %s", name, resp.Status) +} + +func cloneGiteaRepo(t *testing.T, repoName, branch string) *gitclient.Repo { + t.Helper() + + repo, err := gitclient.Clone(gitclient.CloneOptions{ + URL: giteaRepoURL(repoName), + Auth: gitHTTPAuth(), + Insecure: true, + Branch: branch, + HomeDir: os.TempDir(), + }) + require.NoError(t, err) + return repo +} + +func gitHTTPAuth() *githttp.BasicAuth { + return &githttp.BasicAuth{ + Username: giteaUsername, + Password: giteaPassword, + } +} + +func giteaRepoURL(name string) string { + return fmt.Sprintf("%s/%s/%s.git", giteaBaseURL, giteaUsername, name) +} + +func writeRepoFile(t *testing.T, fs billy.Filesystem, path, content string) { + t.Helper() + + cleanPath := strings.TrimPrefix(filepath.Clean(path), string(filepath.Separator)) + dir := filepath.Dir(cleanPath) + if dir != "." { + require.NoError(t, fs.MkdirAll(dir, 0o755)) + } + + f, err := fs.OpenFile(cleanPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) + require.NoError(t, err) + defer f.Close() + + _, err = f.Write([]byte(content)) + require.NoError(t, err) +} + +func readRepoFile(t *testing.T, fs billy.Filesystem, path string) string { + t.Helper() + + cleanPath := strings.TrimPrefix(filepath.Clean(path), string(filepath.Separator)) + f, err := fs.Open(cleanPath) + require.NoError(t, err) + defer f.Close() + + bs, err := io.ReadAll(f) + require.NoError(t, err) + return string(bs) +} + +func commitFilesToRepo(t *testing.T, repoName, branch, message string, files map[string]string) string { + t.Helper() + + repo := cloneGiteaRepo(t, repoName, branch) + defer repo.Cleanup() + + for path, content := range files { + writeRepoFile(t, repo.FS(), path, content) + } + + _, err := repo.Commit(".", message, &gitclient.IndexOptions{ + OriginRepo: repo, + FromPath: "/", + ToPath: "/", + }) + require.NoError(t, err) + require.NoError(t, repo.Push("origin", branch, true)) + + commitID, err := repo.GetLatestCommit(repo.CurrentBranch()) + require.NoError(t, err) + return commitID +} + +func readRemoteFile(t *testing.T, repoName, branch, path string) string { + t.Helper() + + repo := cloneGiteaRepo(t, repoName, branch) + defer repo.Cleanup() + + return readRepoFile(t, repo.FS(), path) +} + +func assertRemoteFileAbsent(t *testing.T, repoName, branch, path string) { + t.Helper() + + repo := cloneGiteaRepo(t, repoName, branch) + defer repo.Cleanup() + + cleanPath := strings.TrimPrefix(filepath.Clean(path), string(filepath.Separator)) + _, err := repo.FS().Stat(cleanPath) + require.True(t, os.IsNotExist(err), "expected %s to be absent in %s/%s, got err=%v", path, repoName, branch, err) +} + +func latestRemoteCommit(t *testing.T, repoName, branch string) string { + t.Helper() + + commitID, err := gitclient.GetLatestCommitRemote(gitclient.ListOptions{ + URL: giteaRepoURL(repoName), + Auth: gitHTTPAuth(), + Insecure: true, + Branch: branch, + HomeDir: os.TempDir(), + }) + require.NoError(t, err) + require.NotNil(t, commitID) + return *commitID +} + +func waitForRepo(ctx context.Context, r *resources.Resources, name string, timeout time.Duration, predicate func(*repov1alpha1.Repo) bool) (*repov1alpha1.Repo, error) { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + current := &repov1alpha1.Repo{} + err := r.Get(ctx, name, namespace, current) + if err == nil && predicate(current) { + return current, nil + } + time.Sleep(2 * time.Second) + } + return nil, fmt.Errorf("timeout waiting for repo %s", name) +} + +func waitForRepoCondition(ctx context.Context, r *resources.Resources, name string, ctype commonv1.ConditionType, status metav1.ConditionStatus, timeout time.Duration) (*repov1alpha1.Repo, error) { + return waitForRepo(ctx, r, name, timeout, func(repo *repov1alpha1.Repo) bool { + return repo.GetCondition(ctype).Status == status + }) +} + +func secretSelector(secretName, key string) *commonv1.SecretKeySelector { + return &commonv1.SecretKeySelector{ + Key: key, + Reference: commonv1.Reference{ + Name: secretName, + Namespace: namespace, + }, + } +} + +func configMapSelector(name, key string) *commonv1.ConfigMapKeySelector { + return &commonv1.ConfigMapKeySelector{ + Key: key, + Reference: commonv1.Reference{ + Name: name, + Namespace: namespace, + }, + } +} + +func newRepoResource(name, fromRepo, fromBranch, toRepo, toBranch string) *repov1alpha1.Repo { + return &repov1alpha1.Repo{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: repov1alpha1.RepoSpec{ + FromRepo: repov1alpha1.FromRepoOpts{ + KrateoIgnorePath: repoContentPath, + RepoOpts: repov1alpha1.RepoOpts{ + Url: giteaRepoURL(fromRepo), + Path: repoContentPath, + Branch: fromBranch, + SecretRef: secretSelector(gitAuthSecretName, "token"), + UsernameRef: secretSelector(gitAuthSecretName, "username"), + }, + }, + ToRepo: repov1alpha1.RepoOpts{ + Url: giteaRepoURL(toRepo), + Path: repoContentPath, + Branch: toBranch, + SecretRef: secretSelector(gitAuthSecretName, "token"), + UsernameRef: secretSelector(gitAuthSecretName, "username"), + }, + Insecure: true, + }, + } +} + +func TestController(t *testing.T) { + type testCase struct { + name string + setup func(ctx context.Context, t *testing.T, r *resources.Resources) + repo *repov1alpha1.Repo + verify func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) + } + + cases := []testCase{ + { + name: "TC01-InitialSync", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc01", "main") + createGiteaRepo(t, "dst-tc01", "main") + commitFilesToRepo(t, "src-tc01", "main", "seed source", map[string]string{ + "content/README.md": "initial sync\n", + "content/configs/app.yaml": "enabled: true\n", + }) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc01-sync", "src-tc01", "main", "dst-tc01", "main") + repo.Spec.Override = true + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + current, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + require.NotEmpty(t, current.Status.OriginCommitId) + require.NotEmpty(t, current.Status.TargetCommitId) + require.Equal(t, "main", current.Status.OriginBranch) + require.Equal(t, "main", current.Status.TargetBranch) + require.Equal(t, current.Status.OriginCommitId, latestRemoteCommit(t, "src-tc01", "main")) + require.Equal(t, current.Status.TargetCommitId, latestRemoteCommit(t, "dst-tc01", "main")) + require.Equal(t, "initial sync\n", readRemoteFile(t, "dst-tc01", "main", "content/README.md")) + require.Equal(t, "enabled: true\n", readRemoteFile(t, "dst-tc01", "main", "content/configs/app.yaml")) + }, + }, + { + name: "TC02-Idempotency", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc02", "main") + createGiteaRepo(t, "dst-tc02", "main") + commitFilesToRepo(t, "src-tc02", "main", "seed source", map[string]string{ + "content/idempotent.txt": "one pass only\n", + }) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc02-idempotency", "src-tc02", "main", "dst-tc02", "main") + repo.Spec.Override = true + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + current, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + + firstOrigin := current.Status.OriginCommitId + firstTarget := current.Status.TargetCommitId + firstRemoteTarget := latestRemoteCommit(t, "dst-tc02", "main") + + time.Sleep(6 * time.Second) + + later, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 30*time.Second) + require.NoError(t, err) + require.Equal(t, firstOrigin, later.Status.OriginCommitId) + require.Equal(t, firstTarget, later.Status.TargetCommitId) + require.Equal(t, firstRemoteTarget, latestRemoteCommit(t, "dst-tc02", "main")) + }, + }, + { + name: "TC03-ContentUpdate", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc03", "main") + createGiteaRepo(t, "dst-tc03", "main") + commitFilesToRepo(t, "src-tc03", "main", "seed source", map[string]string{ + "content/app.txt": "v1\n", + }) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc03-update", "src-tc03", "main", "dst-tc03", "main") + repo.Spec.EnableUpdate = true + repo.Spec.Override = true + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + current, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + + initialOrigin := current.Status.OriginCommitId + initialTarget := current.Status.TargetCommitId + + updatedOrigin := commitFilesToRepo(t, "src-tc03", "main", "update source", map[string]string{ + "content/app.txt": "v2\n", + "content/feature.txt": "new content\n", + }) + + updated, err := waitForRepo(ctx, r, repoName, 2*time.Minute, func(repo *repov1alpha1.Repo) bool { + return repo.GetCondition(commonv1.TypeReady).Status == metav1.ConditionTrue && + repo.Status.OriginCommitId != "" && + repo.Status.TargetCommitId != "" && + repo.Status.OriginCommitId != initialOrigin && + repo.Status.TargetCommitId != initialTarget + }) + require.NoError(t, err) + require.Equal(t, updatedOrigin, updated.Status.OriginCommitId) + require.Equal(t, "v2\n", readRemoteFile(t, "dst-tc03", "main", "content/app.txt")) + require.Equal(t, "new content\n", readRemoteFile(t, "dst-tc03", "main", "content/feature.txt")) + }, + }, + { + name: "TC04-FallbackBranch", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc04", "main") + createGiteaRepo(t, "dst-tc04", "main") + commitFilesToRepo(t, "src-tc04", "main", "seed source", map[string]string{ + "content/from-source.txt": "copied through fallback\n", + }) + commitFilesToRepo(t, "dst-tc04", "master", "seed master", map[string]string{ + "content/bootstrap.txt": "master baseline\n", + }) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc04-fallback", "src-tc04", "main", "dst-tc04", "release") + repo.Spec.ToRepo.CloneFromBranch = "master" + repo.Spec.Override = true + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + current, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + require.Equal(t, "release", current.Status.TargetBranch) + require.Equal(t, "copied through fallback\n", readRemoteFile(t, "dst-tc04", "release", "content/from-source.txt")) + }, + }, + { + name: "TC05-AuthFailure", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc05", "main") + createGiteaRepo(t, "dst-tc05", "main") + commitFilesToRepo(t, "src-tc05", "main", "seed source", map[string]string{ + "content/auth.txt": "should never sync\n", + }) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc05-authfail", "src-tc05", "main", "dst-tc05", "main") + repo.Spec.FromRepo.SecretRef = secretSelector(gitBadAuthSecretName, "token") + repo.Spec.FromRepo.UsernameRef = secretSelector(gitBadAuthSecretName, "username") + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + current, err := waitForRepo(ctx, r, repoName, 90*time.Second, func(repo *repov1alpha1.Repo) bool { + cond := repo.GetCondition(commonv1.TypeSynced) + return cond.Status == metav1.ConditionFalse && cond.Reason == commonv1.ReasonReconcileError + }) + require.NoError(t, err) + require.Equal(t, commonv1.ReasonReconcileError, current.GetCondition(commonv1.TypeSynced).Reason) + }, + }, + { + name: "TC06-OverrideFalse", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc06", "main") + createGiteaRepo(t, "dst-tc06", "main") + commitFilesToRepo(t, "src-tc06", "main", "seed source", map[string]string{ + "content/existing.txt": "source version\n", + "content/new.txt": "fresh file\n", + }) + commitFilesToRepo(t, "dst-tc06", "main", "seed target", map[string]string{ + "content/existing.txt": "target version\n", + }) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc06-override-false", "src-tc06", "main", "dst-tc06", "main") + repo.Spec.Override = false + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + _, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + require.Equal(t, "target version\n", readRemoteFile(t, "dst-tc06", "main", "content/existing.txt")) + require.Equal(t, "fresh file\n", readRemoteFile(t, "dst-tc06", "main", "content/new.txt")) + }, + }, + { + name: "TC07-KrateoIgnore", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc07", "main") + createGiteaRepo(t, "dst-tc07", "main") + commitFilesToRepo(t, "src-tc07", "main", "seed source", map[string]string{ + "content/.krateoignore": "/content/ignored.txt\n", + "content/ignored.txt": "Hello {{name}}!\n", + "content/kept.txt": "Hello {{name}}!\n", + }) + require.NoError(t, r.Create(ctx, &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "tc07-values", Namespace: namespace}, + Data: map[string]string{ + "values": `{"name":"Krateo"}`, + }, + })) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc07-krateoignore", "src-tc07", "main", "dst-tc07", "main") + repo.Spec.Override = true + repo.Spec.ConfigMapKeyRef = configMapSelector("tc07-values", "values") + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + _, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + require.Equal(t, "Hello Krateo!\n", readRemoteFile(t, "dst-tc07", "main", "content/kept.txt")) + require.Equal(t, "Hello {{name}}!\n", readRemoteFile(t, "dst-tc07", "main", "content/ignored.txt")) + }, + }, + { + name: "TC08-MustacheDefault", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc08", "main") + createGiteaRepo(t, "dst-tc08", "main") + commitFilesToRepo(t, "src-tc08", "main", "seed source", map[string]string{ + "content/template.txt": "Hello {{name}}!\n", + }) + require.NoError(t, r.Create(ctx, &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "tc08-values", Namespace: namespace}, + Data: map[string]string{ + "values": `{"name":"Krateo"}`, + }, + })) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc08-mustache", "src-tc08", "main", "dst-tc08", "main") + repo.Spec.Override = true + repo.Spec.ConfigMapKeyRef = configMapSelector("tc08-values", "values") + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + _, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + require.Equal(t, "Hello Krateo!\n", readRemoteFile(t, "dst-tc08", "main", "content/template.txt")) + }, + }, + { + name: "TC09-GoTemplateAnnotation", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc09", "main") + createGiteaRepo(t, "dst-tc09", "main") + commitFilesToRepo(t, "src-tc09", "main", "seed source", map[string]string{ + "content/template.txt": "Hello {{ .name }}!\n", + }) + require.NoError(t, r.Create(ctx, &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "tc09-values", Namespace: namespace}, + Data: map[string]string{ + "values": `{"name":"GoTemplate"}`, + }, + })) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc09-gotemplate", "src-tc09", "main", "dst-tc09", "main") + repo.ObjectMeta.Annotations = map[string]string{ + AnnotationTemplatingEngine: "gotemplate", + } + repo.Spec.Override = true + repo.Spec.ConfigMapKeyRef = configMapSelector("tc09-values", "values") + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + _, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + require.Equal(t, "Hello GoTemplate!\n", readRemoteFile(t, "dst-tc09", "main", "content/template.txt")) + }, + }, + } + + f := features.New("RepoControllerFeatures"). + Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { + ctx, err := setupController(ctx, cfg) + require.NoError(t, err) + + r, err := resources.New(cfg.Client().RESTConfig()) + require.NoError(t, err) + + require.NoError(t, r.Create(ctx, &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: gitAuthSecretName, Namespace: namespace}, + Data: map[string][]byte{ + "token": []byte(giteaPassword), + "username": []byte(giteaUsername), + }, + })) + + require.NoError(t, r.Create(ctx, &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: gitBadAuthSecretName, Namespace: namespace}, + Data: map[string][]byte{ + "token": []byte("wrong-password"), + "username": []byte(giteaUsername), + }, + })) + + return ctx + }) + + for _, tc := range cases { + tc := tc + f.Assess(tc.name, func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { + r, err := resources.New(cfg.Client().RESTConfig()) + require.NoError(t, err) + + if tc.setup != nil { + tc.setup(ctx, t, r) + } + + require.NoError(t, r.Create(ctx, tc.repo)) + + if tc.verify != nil { + tc.verify(ctx, t, r, tc.repo.Name) + } + + return ctx + }) + } + + testenv.Test(t, f.Feature()) +} diff --git a/internal/tools/copier/copier.go b/internal/tools/copier/copier.go index 4fb2bcc..6d17747 100644 --- a/internal/tools/copier/copier.go +++ b/internal/tools/copier/copier.go @@ -316,7 +316,7 @@ func (co *Copier) setKrateoIgnore() error { } func (co *Copier) setTargetIgnore() error { - if _, err := co.fromFS.Stat(co.targetCopyPath); err == nil { + if _, err := co.toFS.Stat(co.targetCopyPath); err == nil { var flist []string err = loadFilesFromPath(co.toFS, co.targetCopyPath, &flist) if err != nil { diff --git a/internal/tools/copier/copier_test.go b/internal/tools/copier/copier_test.go index 0cbf0b3..72111c0 100644 --- a/internal/tools/copier/copier_test.go +++ b/internal/tools/copier/copier_test.go @@ -115,11 +115,6 @@ func TestTargetIgnoreSkipsExisting(t *testing.T) { writeFile(t, from, "/src/skip.txt", "from-skip") writeFile(t, from, "/src/keep.txt", "from-keep") - // create target dir in FROM FS so setTargetIgnore will proceed to load files from TO FS - if err := from.MkdirAll("/dst", 0o755); err != nil { - t.Fatalf("mkdirall from:/dst: %v", err) - } - // create an existing file in target (TO FS) that should be considered for ignoring writeFile(t, to, "/dst/skip.txt", "to-skip-original") diff --git a/internal/utils/errors.go b/internal/utils/errors.go new file mode 100644 index 0000000..d028220 --- /dev/null +++ b/internal/utils/errors.go @@ -0,0 +1,19 @@ +package utils + +type unwrapper interface { + Unwrap() error +} + +func IsErr(target, err error) bool { + for err != nil { + if err == target { + return true + } + u, ok := err.(unwrapper) + if !ok { + break + } + err = u.Unwrap() + } + return false +} From 5864336a60d6091cd3e19b907150212660262cff Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Fri, 8 May 2026 10:52:48 +0200 Subject: [PATCH 02/13] feat: Refactor event recording and error handling in Repo and LocalResource controllers - Updated event recording to use plumbing event recorder in localresource.go and repo.go. - Enhanced error handling by introducing failSync method to mark resources as unavailable and update their status accordingly. - Removed unnecessary conditions in the Observe method to streamline the reconciliation logic. - Added unit tests for failSync functionality to ensure proper status updates on errors. - Introduced new test cases to validate behavior when EnableUpdate is false and when target drift is detected. --- go.mod | 74 +++--- go.sum | 224 ++++++++---------- .../localresource/localresource.go | 16 +- internal/controllers/repo/repo.go | 51 ++-- internal/controllers/repo/repo_status_test.go | 50 ++++ internal/controllers/repo/repo_test.go | 136 +++++++++++ 6 files changed, 359 insertions(+), 192 deletions(-) create mode 100644 internal/controllers/repo/repo_status_test.go diff --git a/go.mod b/go.mod index 31886fe..009c51b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/krateoplatformops/git-provider -go 1.25.0 +go 1.25.6 require ( github.com/Masterminds/sprig/v3 v3.3.0 @@ -10,27 +10,27 @@ require ( github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 github.com/go-git/go-git/v5 v5.19.0 github.com/go-logr/logr v1.4.3 - github.com/krateoplatformops/plumbing v0.7.2 - github.com/krateoplatformops/provider-runtime v0.10.2 + github.com/krateoplatformops/plumbing v1.7.1 + github.com/krateoplatformops/provider-runtime v1.2.1 github.com/moby/moby/api v1.52.0 github.com/moby/moby/client v0.1.0 github.com/pkg/errors v0.9.1 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/stoewer/go-strcase v1.3.0 github.com/stretchr/testify v1.11.1 - k8s.io/api v0.34.1 - k8s.io/apiextensions-apiserver v0.34.1 - k8s.io/apimachinery v0.34.1 - k8s.io/client-go v0.34.1 - sigs.k8s.io/controller-runtime v0.22.3 - sigs.k8s.io/controller-tools v0.19.0 + k8s.io/api v0.35.3 + k8s.io/apiextensions-apiserver v0.35.0 + k8s.io/apimachinery v0.35.3 + k8s.io/client-go v0.35.3 + sigs.k8s.io/controller-runtime v0.23.3 + sigs.k8s.io/controller-tools v0.20.1 sigs.k8s.io/e2e-framework v0.6.0 ) require ( - dario.cat/mergo v1.0.1 // indirect + dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -46,18 +46,16 @@ require ( github.com/docker/go-units v0.5.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 v4.12.0+incompatible // 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.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.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.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/gobuffalo/flect v1.0.3 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/btree v1.1.3 // indirect @@ -91,53 +89,53 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/pflag v1.0.7 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/vladimirvivien/gexe v0.4.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - go.opentelemetry.io/auto/sdk v1.1.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/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.53.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/term v0.42.0 // indirect golang.org/x/text v0.36.0 // indirect - golang.org/x/time v0.11.0 // indirect + golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.43.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.36.7 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/code-generator v0.34.1 // indirect - k8s.io/component-base v0.34.1 // indirect - k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f // indirect + k8s.io/code-generator v0.35.0 // indirect + k8s.io/component-base v0.35.0 // indirect + k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 68ff8f4..adc5af2 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 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/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -55,8 +55,8 @@ github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf 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/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -86,18 +86,16 @@ 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.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +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.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= -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/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= @@ -113,14 +111,14 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= 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= @@ -137,8 +135,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/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.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -151,10 +147,10 @@ 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/krateoplatformops/plumbing v0.7.2 h1:4UuWy9747p9ligMtNEiOOQGsuK6d9lczg7R1no8ERsE= -github.com/krateoplatformops/plumbing v0.7.2/go.mod h1:mQ/sm0viyKgfR2ARzHuwCpY0rcyMKqCv8a8SOu52yYQ= -github.com/krateoplatformops/provider-runtime v0.10.2 h1:56PpG0hUkF8TiklyTfxzj4wfAqmUm3E4N9CFX70McKI= -github.com/krateoplatformops/provider-runtime v0.10.2/go.mod h1:nKW3ULWw6vji68J/XYlkyS/QMMnrkOKnD0Hn7FncK9I= +github.com/krateoplatformops/plumbing v1.7.1 h1:ZAjeAbfSNE9isrfb90k51IGtSp1hQjw55ndVnxcHtaE= +github.com/krateoplatformops/plumbing v1.7.1/go.mod h1:L8dMKmq9hO1tz9NzJPlryBj618J5w0PYt8z6fzAbBvs= +github.com/krateoplatformops/provider-runtime v1.2.1 h1:eS+oTc0Oscdp9GURe77uS22Q6FdqfUFc86iIpdUu5NQ= +github.com/krateoplatformops/provider-runtime v1.2.1/go.mod h1:QoRytq8drA9+qPiXT3iVwLPOhgxR4beOKcmsiImc0lY= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -189,10 +185,10 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.38.1 h1:FaLA8GlcpXDwsb7m0h2A9ew2aTk3vnZMlzFgg5tz/pk= -github.com/onsi/gomega v1.38.1/go.mod h1:LfcV8wZLvwcYRwPiJysphKAEsmcFnLMK/9c+PjvlX8g= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -205,14 +201,14 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= @@ -228,11 +224,11 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/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/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -255,68 +251,51 @@ 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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -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/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/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= -go.opentelemetry.io/otel/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/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.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= -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.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= -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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -328,43 +307,34 @@ golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= -golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= -golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -379,43 +349,43 @@ 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.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= -k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= -k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= -k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= -k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= -k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= -k8s.io/code-generator v0.34.1 h1:WpphT26E+j7tEgIUfFr5WfbJrktCGzB3JoJH9149xYc= -k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg= -k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= -k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= -k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q= -k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= +k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= +k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= +k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= +k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= +k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= +k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= +k8s.io/code-generator v0.35.0 h1:TvrtfKYZTm9oDF2z+veFKSCcgZE3Igv0svY+ehCmjHQ= +k8s.io/code-generator v0.35.0/go.mod h1:iS1gvVf3c/T71N5DOGYO+Gt3PdJ6B9LYSvIyQ4FHzgc= +k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= +k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y= -sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= -sigs.k8s.io/controller-tools v0.19.0 h1:OU7jrPPiZusryu6YK0jYSjPqg8Vhf8cAzluP9XGI5uk= -sigs.k8s.io/controller-tools v0.19.0/go.mod h1:y5HY/iNDFkmFla2CfQoVb2AQXMsBk4ad84iR1PLANB0= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/controller-tools v0.20.1 h1:gkfMt9YodI0K85oT8rVi80NTXO/kDmabKR5Ajn5GYxs= +sigs.k8s.io/controller-tools v0.20.1/go.mod h1:b4qPmjGU3iZwqn34alUU5tILhNa9+VXK+J3QV0fT/uU= sigs.k8s.io/e2e-framework v0.6.0 h1:p7hFzHnLKO7eNsWGI2AbC1Mo2IYxidg49BiT4njxkrM= sigs.k8s.io/e2e-framework v0.6.0/go.mod h1:IREnCHnKgRCioLRmNi0hxSJ1kJ+aAdjEKK/gokcZu4k= -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/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/controllers/localresource/localresource.go b/internal/controllers/localresource/localresource.go index 32fca88..b2518da 100644 --- a/internal/controllers/localresource/localresource.go +++ b/internal/controllers/localresource/localresource.go @@ -13,12 +13,13 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" - "k8s.io/client-go/tools/record" + record "k8s.io/client-go/tools/events" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/krateoplatformops/provider-runtime/pkg/event" + plumbingevent "github.com/krateoplatformops/plumbing/kubeutil/event" + "github.com/krateoplatformops/plumbing/kubeutil/eventrecorder" "github.com/krateoplatformops/provider-runtime/pkg/logging" "github.com/krateoplatformops/provider-runtime/pkg/meta" "github.com/krateoplatformops/provider-runtime/pkg/ratelimiter" @@ -48,7 +49,10 @@ func Setup(mgr ctrl.Manager, o option.SetupOptions) error { log := o.Controller.Logger.WithValues("controller", name) - recorder := mgr.GetEventRecorderFor(name) + recorder, err := eventrecorder.Create(context.Background(), mgr.GetConfig(), name, nil) + if err != nil { + return fmt.Errorf("failed to create event recorder: %w", err) + } r := reconciler.NewReconciler(mgr, resource.ManagedKind(localResourcev1alpha1.LocalResourceGroupVersionKind), @@ -61,7 +65,7 @@ func Setup(mgr ctrl.Manager, o option.SetupOptions) error { }), reconciler.WithPollInterval(o.Controller.PollInterval), reconciler.WithLogger(log), - reconciler.WithRecorder(event.NewAPIRecorder(recorder)), + reconciler.WithRecorder(plumbingevent.NewAPIRecorder(recorder)), reconciler.WithTimeout(o.Controller.Timeout), ) @@ -314,7 +318,7 @@ func (e *external) SyncLocalResources(ctx context.Context, cr *localResourcev1al log := contexttools.LoggerFromCtx(ctx, e.log) log.Debug("Target LocalResource cloned", "url", spec.ToRepo.Url) - e.rec.Eventf(cr, corev1.EventTypeNormal, "TargetLocalResourceCloned", + e.rec.Eventf(cr, nil, corev1.EventTypeNormal, "TargetLocalResourceCloned", "", "Successfully cloned target LocalResource: %s", spec.ToRepo.Url) log.Debug(fmt.Sprintf("Target LocalResource on branch %s", toRepo.CurrentBranch())) @@ -438,7 +442,7 @@ func (e *external) SyncLocalResources(ctx context.Context, cr *localResourcev1al return fmt.Errorf("unable to push target LocalResource: %w", err) } log.Info("Target LocalResource pushed", "branch", toRepo.CurrentBranch(), "commitId", toLocalResourceCommitId) - e.rec.Eventf(cr, corev1.EventTypeNormal, "LocalResourcePushSuccess", + e.rec.Eventf(cr, nil, corev1.EventTypeNormal, "LocalResourcePushSuccess", "", fmt.Sprintf("Target LocalResource pushed branch %s", toRepo.CurrentBranch())) meta.SetExternalName(cr, toLocalResourceCommitId) diff --git a/internal/controllers/repo/repo.go b/internal/controllers/repo/repo.go index c8cfec3..010d338 100644 --- a/internal/controllers/repo/repo.go +++ b/internal/controllers/repo/repo.go @@ -12,7 +12,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/krateoplatformops/provider-runtime/pkg/event" "github.com/krateoplatformops/provider-runtime/pkg/logging" "github.com/krateoplatformops/provider-runtime/pkg/meta" "github.com/krateoplatformops/provider-runtime/pkg/ratelimiter" @@ -58,7 +57,7 @@ func Setup(mgr ctrl.Manager, o option.SetupOptions) error { }), reconciler.WithPollInterval(o.Controller.PollInterval), reconciler.WithLogger(log), - reconciler.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + reconciler.WithRecorder(plumbingevent.NewAPIRecorder(recorder)), reconciler.WithTimeout(o.Controller.Timeout), ) @@ -140,15 +139,6 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (reconciler } } - if !cr.Spec.EnableUpdate && cr.Status.TargetCommitId != "" && cr.Status.OriginCommitId != "" && cr.Status.TargetBranch != "" && cr.Status.OriginBranch != "" { - e.log.Debug("External resource should not be observed by provider, skip observing. EnableUpdate is false.", "name", cr.Name) - cr.Status.SetConditions(commonv1.Available()) - return reconciler.ExternalObservation{ - ResourceExists: true, - ResourceUpToDate: true, - }, e.kube.Status().Update(ctx, cr) - } - if cr.Status.TargetCommitId != "" { meta.SetExternalName(cr, cr.Status.TargetCommitId) } @@ -188,6 +178,9 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (reconciler if ptr.Deref(latestCommit, "") != cr.Status.OriginCommitId { e.log.Debug("Origin commit not found in origin remote repository", "commitId", cr.Status.OriginCommitId, "branch", cr.Status.OriginBranch) + if !cr.Spec.EnableUpdate { + return reconciler.ExternalObservation{}, e.failSync(ctx, cr, fmt.Errorf("origin commit %s is no longer the latest commit on branch %s while enableUpdate is false", cr.Status.OriginCommitId, cr.Status.OriginBranch)) + } return reconciler.ExternalObservation{ ResourceExists: true, ResourceUpToDate: false, @@ -196,6 +189,9 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (reconciler if !isTargetRepoSynced { e.log.Debug("Target commit not found in target remote repository", "commitId", cr.Status.TargetCommitId, "branch", cr.Status.TargetBranch) + if !cr.Spec.EnableUpdate { + return reconciler.ExternalObservation{}, e.failSync(ctx, cr, fmt.Errorf("target commit %s not found on branch %s while enableUpdate is false", cr.Status.TargetCommitId, cr.Status.TargetBranch)) + } return reconciler.ExternalObservation{ ResourceExists: true, ResourceUpToDate: false, @@ -282,6 +278,19 @@ func (e *external) loadValuesFromConfigMap(ctx context.Context, ref *commonv1.Co return res, nil } +func (e *external) failSync(ctx context.Context, cr *repov1alpha1.Repo, err error) error { + if err == nil { + return nil + } + + cr.Status.SetConditions(commonv1.Unavailable(), commonv1.ReconcileError(err)) + if updateErr := e.kube.Status().Update(ctx, cr); updateErr != nil { + return fmt.Errorf("%w; unable to update status after failure: %v", err, updateErr) + } + + return err +} + func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitMessage string) error { spec := cr.Spec.DeepCopy() @@ -302,7 +311,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM HomeDir: homeDir, // Use the configured home directory for temporary files }) if err != nil { - return fmt.Errorf("cloning toRepo: %w", err) + return e.failSync(ctx, cr, fmt.Errorf("cloning toRepo: %w", err)) } defer toRepo.Cleanup() @@ -320,7 +329,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM HomeDir: homeDir, // Use the configured home directory for temporary files }) if err != nil { - return fmt.Errorf("cloning fromRepo: %w", err) + return e.failSync(ctx, cr, fmt.Errorf("cloning fromRepo: %w", err)) } defer fromRepo.Cleanup() e.log.Debug("Origin repo cloned", "url", spec.FromRepo.Url) @@ -329,7 +338,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM fromRepoCommitId, err := fromRepo.GetLatestCommit(fromRepo.CurrentBranch()) if err != nil { - return err + return e.failSync(ctx, cr, err) } fromPath := spec.FromRepo.Path @@ -380,7 +389,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM co, err := copier.NewCopier(fromRepo.FS(), toRepo.FS(), opts...) if err != nil { - return fmt.Errorf("unable to create copier: %w", err) + return e.failSync(ctx, cr, fmt.Errorf("unable to create copier: %w", err)) } if override { e.log.Debug("Override is true, overriding all files in target repo") @@ -390,7 +399,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM } } if err := co.Copy(override); err != nil { - return fmt.Errorf("unable to copy files: %w", err) + return e.failSync(ctx, cr, fmt.Errorf("unable to copy files: %w", err)) } e.log.Info("Origin and target repo synchronized", @@ -409,7 +418,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM if err == git.NoErrAlreadyUpToDate { toRepoCommitId, err := toRepo.GetLatestCommit(toRepo.CurrentBranch()) if err != nil { - return fmt.Errorf("unable to get latest commit from target repo: %w", err) + return e.failSync(ctx, cr, fmt.Errorf("unable to get latest commit from target repo: %w", err)) } e.log.Info("Target repo not commited", "branch", toRepo.CurrentBranch(), "status", "repository already up-to-date") e.rec.Event(cr, plumbingevent.Normal("RepoAlreadyUpToDate", "Reconciling", fmt.Sprintf("Target repo already up-to-date on branch %s", toRepo.CurrentBranch()))) @@ -422,18 +431,18 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM err = e.kube.Status().Update(ctx, cr) if err != nil { - return fmt.Errorf("unable to update status: %w", err) + return e.failSync(ctx, cr, fmt.Errorf("unable to update status: %w", err)) } return nil } else if err != nil { - return fmt.Errorf("unable to commit target repo: %w", err) + return e.failSync(ctx, cr, fmt.Errorf("unable to commit target repo: %w", err)) } e.log.Info("Target repo committed", "branch", toRepo.CurrentBranch(), "commitId", toRepoCommitId) e.rec.Event(cr, plumbingevent.Normal("RepoCommitSuccess", "Reconciling", fmt.Sprintf("Target repo committed on branch %s", toRepo.CurrentBranch()))) err = toRepo.Push("origin", toRepo.CurrentBranch(), e.cfg.Insecure) if err != nil { - return fmt.Errorf("unable to push target repo: %w", err) + return e.failSync(ctx, cr, fmt.Errorf("unable to push target repo: %w", err)) } e.log.Info("Target repo pushed", "branch", toRepo.CurrentBranch(), "commitId", toRepoCommitId) e.rec.Event(cr, plumbingevent.Normal("RepoPushSuccess", "Reconciling", fmt.Sprintf("Target repo pushed branch %s", toRepo.CurrentBranch()))) @@ -445,7 +454,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM cr.Status.OriginBranch = fromRepo.CurrentBranch() err = e.kube.Status().Update(ctx, cr) if err != nil { - return fmt.Errorf("unable to update status: %w", err) + return e.failSync(ctx, cr, fmt.Errorf("unable to update status: %w", err)) } return nil } diff --git a/internal/controllers/repo/repo_status_test.go b/internal/controllers/repo/repo_status_test.go new file mode 100644 index 0000000..b56ae51 --- /dev/null +++ b/internal/controllers/repo/repo_status_test.go @@ -0,0 +1,50 @@ +package repo + +import ( + "context" + "errors" + "testing" + + "github.com/krateoplatformops/git-provider/apis" + repov1alpha1 "github.com/krateoplatformops/git-provider/apis/repo/v1alpha1" + commonv1 "github.com/krateoplatformops/provider-runtime/apis/common/v1" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientsetscheme "k8s.io/client-go/kubernetes/scheme" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestFailSyncMarksResourceUnavailableAndSyncedFalse(t *testing.T) { + require.NoError(t, apis.AddToScheme(clientsetscheme.Scheme)) + + cr := &repov1alpha1.Repo{ + ObjectMeta: metav1.ObjectMeta{Name: "sample", Namespace: "test-system"}, + } + cr.Status.SetConditions(commonv1.Available()) + + kubeClient := fake.NewClientBuilder(). + WithScheme(clientsetscheme.Scheme). + WithStatusSubresource(cr). + WithObjects(cr). + Build() + + e := &external{kube: kubeClient} + + inputErr := errors.New("push failed") + returnedErr := e.failSync(context.Background(), cr, inputErr) + + require.ErrorIs(t, returnedErr, inputErr) + + updated := &repov1alpha1.Repo{} + require.NoError(t, kubeClient.Get(context.Background(), ctrlclient.ObjectKey{Name: "sample", Namespace: "test-system"}, updated)) + + ready := updated.GetCondition(commonv1.TypeReady) + synced := updated.GetCondition(commonv1.TypeSynced) + + require.Equal(t, metav1.ConditionFalse, ready.Status) + require.Equal(t, commonv1.ReasonUnavailable, ready.Reason) + require.Equal(t, metav1.ConditionFalse, synced.Status) + require.Equal(t, commonv1.ReasonReconcileError, synced.Reason) + require.Equal(t, "push failed", synced.Message) +} diff --git a/internal/controllers/repo/repo_test.go b/internal/controllers/repo/repo_test.go index 4e805a6..1337489 100644 --- a/internal/controllers/repo/repo_test.go +++ b/internal/controllers/repo/repo_test.go @@ -333,6 +333,20 @@ func createGiteaRepo(t *testing.T, name, defaultBranch string) { require.True(t, resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusConflict, "unexpected status creating repo %s: %s", name, resp.Status) } +func deleteGiteaRepo(t *testing.T, name string) { + t.Helper() + + req, err := http.NewRequest(http.MethodDelete, giteaBaseURL+"/api/v1/repos/"+giteaUsername+"/"+name, nil) + require.NoError(t, err) + req.SetBasicAuth(giteaUsername, giteaPassword) + + resp, err := newInsecureHTTPClient().Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusNotFound, "unexpected status deleting repo %s: %s", name, resp.Status) +} + func cloneGiteaRepo(t *testing.T, repoName, branch string) *gitclient.Repo { t.Helper() @@ -783,6 +797,128 @@ func TestController(t *testing.T) { require.Equal(t, "Hello GoTemplate!\n", readRemoteFile(t, "dst-tc09", "main", "content/template.txt")) }, }, + { + name: "TC10-EnableUpdateOverrideFalse", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc10", "main") + createGiteaRepo(t, "dst-tc10", "main") + commitFilesToRepo(t, "src-tc10", "main", "seed source", map[string]string{ + "content/existing.txt": "source version v1\n", + "content/shared.txt": "shared v1\n", + }) + commitFilesToRepo(t, "dst-tc10", "main", "seed target", map[string]string{ + "content/existing.txt": "target protected\n", + }) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc10-update-override-false", "src-tc10", "main", "dst-tc10", "main") + repo.Spec.EnableUpdate = true + repo.Spec.Override = false + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + current, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + + initialOrigin := current.Status.OriginCommitId + initialTarget := current.Status.TargetCommitId + + updatedOrigin := commitFilesToRepo(t, "src-tc10", "main", "update source", map[string]string{ + "content/existing.txt": "source version v2\n", + "content/shared.txt": "shared v2\n", + "content/new-after.txt": "arrived later\n", + }) + + updated, err := waitForRepo(ctx, r, repoName, 2*time.Minute, func(repo *repov1alpha1.Repo) bool { + return repo.GetCondition(commonv1.TypeReady).Status == metav1.ConditionTrue && + repo.Status.OriginCommitId == updatedOrigin && + repo.Status.TargetCommitId != "" && + repo.Status.TargetCommitId != initialTarget && + repo.Status.OriginCommitId != initialOrigin + }) + require.NoError(t, err) + require.NotEqual(t, initialTarget, updated.Status.TargetCommitId) + require.Equal(t, "target protected\n", readRemoteFile(t, "dst-tc10", "main", "content/existing.txt")) + require.Equal(t, "shared v1\n", readRemoteFile(t, "dst-tc10", "main", "content/shared.txt")) + require.Equal(t, "arrived later\n", readRemoteFile(t, "dst-tc10", "main", "content/new-after.txt")) + }, + }, + { + name: "TC11-DisableUpdateOverrideTrue", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc11", "main") + createGiteaRepo(t, "dst-tc11", "main") + commitFilesToRepo(t, "src-tc11", "main", "seed source", map[string]string{ + "content/app.txt": "v1\n", + }) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc11-disable-update-override-true", "src-tc11", "main", "dst-tc11", "main") + repo.Spec.Override = true + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + current, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + + initialOrigin := current.Status.OriginCommitId + initialTarget := current.Status.TargetCommitId + initialRemoteTarget := latestRemoteCommit(t, "dst-tc11", "main") + + updatedOrigin := commitFilesToRepo(t, "src-tc11", "main", "update source", map[string]string{ + "content/app.txt": "v2\n", + "content/extra.txt": "should not sync\n", + }) + require.NotEqual(t, initialOrigin, updatedOrigin) + + later, err := waitForRepo(ctx, r, repoName, 45*time.Second, func(repo *repov1alpha1.Repo) bool { + synced := repo.GetCondition(commonv1.TypeSynced) + return synced.Status == metav1.ConditionFalse && + synced.Reason == commonv1.ReasonReconcileError + }) + require.NoError(t, err) + require.Equal(t, initialOrigin, later.Status.OriginCommitId) + require.Equal(t, initialTarget, later.Status.TargetCommitId) + require.Equal(t, initialRemoteTarget, latestRemoteCommit(t, "dst-tc11", "main")) + require.Contains(t, later.GetCondition(commonv1.TypeSynced).Message, "enableUpdate is false") + require.Equal(t, "v1\n", readRemoteFile(t, "dst-tc11", "main", "content/app.txt")) + assertRemoteFileAbsent(t, "dst-tc11", "main", "content/extra.txt") + }, + }, + { + name: "TC12-DisableUpdateDetectsTargetDrift", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc12", "main") + createGiteaRepo(t, "dst-tc12", "main") + commitFilesToRepo(t, "src-tc12", "main", "seed source", map[string]string{ + "content/app.txt": "baseline\n", + }) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc12-detect-target-drift", "src-tc12", "main", "dst-tc12", "main") + repo.Spec.Override = true + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + current, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + require.NotEmpty(t, current.Status.TargetCommitId) + + deleteGiteaRepo(t, "dst-tc12") + createGiteaRepo(t, "dst-tc12", "main") + + updated, err := waitForRepo(ctx, r, repoName, 45*time.Second, func(repo *repov1alpha1.Repo) bool { + ready := repo.GetCondition(commonv1.TypeReady) + synced := repo.GetCondition(commonv1.TypeSynced) + return ready.Status == metav1.ConditionFalse && + synced.Status == metav1.ConditionFalse && + synced.Reason == commonv1.ReasonReconcileError + }) + require.NoError(t, err) + require.Equal(t, commonv1.ReasonUnavailable, updated.GetCondition(commonv1.TypeReady).Reason) + require.Contains(t, updated.GetCondition(commonv1.TypeSynced).Message, "target commit") + }, + }, } f := features.New("RepoControllerFeatures"). From 1d477a6905503a4fc768eef35c351cd545c9d3b3 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Fri, 8 May 2026 12:07:59 +0200 Subject: [PATCH 03/13] fix: update Go version in Dockerfile and refactor error handling in repo controller --- Dockerfile | 2 +- internal/controllers/repo/repo.go | 7 +-- internal/controllers/repo/repo_status_test.go | 45 ++++++++++++++++++ internal/controllers/repo/repo_test.go | 46 +++++++++++++++++++ 4 files changed, 94 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 02ad6e3..0ea25b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Build environment # ----------------- -FROM golang:1.25.0-bookworm AS builder +FROM golang:1.25.6-bookworm AS builder LABEL stage=builder ARG DEBIAN_FRONTEND=noninteractive diff --git a/internal/controllers/repo/repo.go b/internal/controllers/repo/repo.go index 010d338..cb71b0c 100644 --- a/internal/controllers/repo/repo.go +++ b/internal/controllers/repo/repo.go @@ -284,10 +284,6 @@ func (e *external) failSync(ctx context.Context, cr *repov1alpha1.Repo, err erro } cr.Status.SetConditions(commonv1.Unavailable(), commonv1.ReconcileError(err)) - if updateErr := e.kube.Status().Update(ctx, cr); updateErr != nil { - return fmt.Errorf("%w; unable to update status after failure: %v", err, updateErr) - } - return err } @@ -414,7 +410,6 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM FromPath: fromPath, ToPath: toPath, }) - toRepoCommitId := toRepoCommitIdObj.String() if err == git.NoErrAlreadyUpToDate { toRepoCommitId, err := toRepo.GetLatestCommit(toRepo.CurrentBranch()) if err != nil { @@ -437,6 +432,8 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM } else if err != nil { return e.failSync(ctx, cr, fmt.Errorf("unable to commit target repo: %w", err)) } + toRepoCommitId := toRepoCommitIdObj.String() + e.log.Info("Target repo committed", "branch", toRepo.CurrentBranch(), "commitId", toRepoCommitId) e.rec.Event(cr, plumbingevent.Normal("RepoCommitSuccess", "Reconciling", fmt.Sprintf("Target repo committed on branch %s", toRepo.CurrentBranch()))) diff --git a/internal/controllers/repo/repo_status_test.go b/internal/controllers/repo/repo_status_test.go index b56ae51..c3fce14 100644 --- a/internal/controllers/repo/repo_status_test.go +++ b/internal/controllers/repo/repo_status_test.go @@ -48,3 +48,48 @@ func TestFailSyncMarksResourceUnavailableAndSyncedFalse(t *testing.T) { require.Equal(t, commonv1.ReasonReconcileError, synced.Reason) require.Equal(t, "push failed", synced.Message) } + +// errorStatusWriter simula un fallimento durante la scrittura dello status +type errorStatusWriter struct { + ctrlclient.StatusWriter +} + +func (w *errorStatusWriter) Update(ctx context.Context, obj ctrlclient.Object, opts ...ctrlclient.SubResourceUpdateOption) error { + return errors.New("simulated update conflict error") +} + +// errorClient fa da wrapper a un client mockato e restituisce lo StatusWriter fallato +type errorClient struct { + ctrlclient.Client +} + +func (c *errorClient) Status() ctrlclient.StatusWriter { + return &errorStatusWriter{c.Client.Status()} +} + +func TestFailSyncWithUpdateConflictCausesNestedError(t *testing.T) { + require.NoError(t, apis.AddToScheme(clientsetscheme.Scheme)) + + cr := &repov1alpha1.Repo{ + ObjectMeta: metav1.ObjectMeta{Name: "sample-conflict", Namespace: "test-system"}, + } + + kubeClient := fake.NewClientBuilder(). + WithScheme(clientsetscheme.Scheme). + WithStatusSubresource(cr). + WithObjects(cr). + Build() + + // Inseriamo il client malevolo + e := &external{kube: &errorClient{Client: kubeClient}} + + inputErr := errors.New("primary reconcile failure") + + // Eseguiamo failSync. + returnedErr := e.failSync(context.Background(), cr, inputErr) + + // Verifichiamo che il fallimento originale venga restituito intatto, + // senza errori di update annidati. + require.Equal(t, inputErr, returnedErr) + require.NotContains(t, returnedErr.Error(), "simulated update conflict error") +} diff --git a/internal/controllers/repo/repo_test.go b/internal/controllers/repo/repo_test.go index 1337489..23301a8 100644 --- a/internal/controllers/repo/repo_test.go +++ b/internal/controllers/repo/repo_test.go @@ -919,6 +919,52 @@ func TestController(t *testing.T) { require.Contains(t, updated.GetCondition(commonv1.TypeSynced).Message, "target commit") }, }, + { + name: "TC13-AutoHealAfterEnableUpdateFalse", + setup: func(ctx context.Context, t *testing.T, r *resources.Resources) { + createGiteaRepo(t, "src-tc13", "main") + createGiteaRepo(t, "dst-tc13", "main") + commitFilesToRepo(t, "src-tc13", "main", "seed source", map[string]string{ + "content/app.txt": "v1\n", + }) + }, + repo: func() *repov1alpha1.Repo { + repo := newRepoResource("tc13-autoheal", "src-tc13", "main", "dst-tc13", "main") + repo.Spec.Override = true + repo.Spec.EnableUpdate = false // Parte disabilitato + return repo + }(), + verify: func(ctx context.Context, t *testing.T, r *resources.Resources, repoName string) { + // Il primo sync (fase di Create) avviene a prescindere da EnableUpdate + _, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) + require.NoError(t, err) + + // Simuliamo un aggiornamento nel repo sorgente + updatedOrigin := commitFilesToRepo(t, "src-tc13", "main", "update source", map[string]string{ + "content/app.txt": "v2\n", + }) + + // Essendo EnableUpdate = false, il Reconcile va in errore + failed, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeSynced, metav1.ConditionFalse, 45*time.Second) + require.NoError(t, err) + require.Equal(t, commonv1.ReasonReconcileError, failed.GetCondition(commonv1.TypeSynced).Reason) + + // AUTO-HEAL: L'utente riabilita l'update + err = r.Get(ctx, repoName, namespace, failed) + require.NoError(t, err) + failed.Spec.EnableUpdate = true + err = r.Update(ctx, failed) + require.NoError(t, err) + + // Verifichiamo che il controller guarisca automaticamente e completi l'allineamento + _, err = waitForRepo(ctx, r, repoName, 90*time.Second, func(repo *repov1alpha1.Repo) bool { + return repo.GetCondition(commonv1.TypeReady).Status == metav1.ConditionTrue && + repo.Status.OriginCommitId == updatedOrigin + }) + require.NoError(t, err) + require.Equal(t, "v2\n", readRemoteFile(t, "dst-tc13", "main", "content/app.txt")) + }, + }, } f := features.New("RepoControllerFeatures"). From c49f95076f9277dbac965bb21210de529f2a90c6 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Fri, 8 May 2026 15:43:06 +0200 Subject: [PATCH 04/13] fix: replace debug logs with warning logs for better error visibility and improve error handling on file closure --- internal/clients/git/git.go | 3 ++- internal/controllers/repo/repo.go | 6 +++--- internal/tools/copier/copier.go | 6 +++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/internal/clients/git/git.go b/internal/clients/git/git.go index f01c637..4eeb822 100644 --- a/internal/clients/git/git.go +++ b/internal/clients/git/git.go @@ -16,6 +16,7 @@ import ( contexttools "github.com/krateoplatformops/provider-runtime/pkg/context" "github.com/krateoplatformops/provider-runtime/pkg/logging" + "github.com/rs/zerolog/log" "github.com/go-git/go-git/v5/plumbing/cache" gitclient "github.com/go-git/go-git/v5/plumbing/transport/client" @@ -257,7 +258,7 @@ func IsInGitCommitHistory(opts ListOptions, hash string) (bool, error) { res.repo, err = git.Clone(res.storer, res.fs, &cloneOpts) if err != nil { if strings.Contains(err.Error(), "couldn't find remote ref") { - fmt.Println("Branch not found in remote repository") + log.Warn("Branch not found in remote repository", "branch", opts.Branch, "url", opts.URL) return false, nil } return false, fmt.Errorf("failed to clone repository: %v", err) diff --git a/internal/controllers/repo/repo.go b/internal/controllers/repo/repo.go index cb71b0c..99a2024 100644 --- a/internal/controllers/repo/repo.go +++ b/internal/controllers/repo/repo.go @@ -262,7 +262,7 @@ func (e *external) loadValuesFromConfigMap(ctx context.Context, ref *commonv1.Co js, err := resource.GetConfigMapValue(ctx, e.kube, ref) if err != nil { - e.log.Debug(err.Error(), "name", ref.Name, "key", ref.Key, "namespace", ref.Namespace) + e.log.Warn(err.Error(), "name", ref.Name, "key", ref.Key, "namespace", ref.Namespace) return nil, err } @@ -271,7 +271,7 @@ func (e *external) loadValuesFromConfigMap(ctx context.Context, ref *commonv1.Co err = json.Unmarshal([]byte(js), &res) if err != nil { - e.log.Debug(err.Error(), "json", js) + e.log.Warn(err.Error(), "json", js) return nil, err } @@ -350,7 +350,7 @@ func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitM if spec.ConfigMapKeyRef != nil { values, err = e.loadValuesFromConfigMap(ctx, spec.ConfigMapKeyRef) if err != nil { - e.log.Debug("Unable to load configmap with template data", "msg", err.Error()) + e.log.Warn("Unable to load configmap with template data", "msg", err.Error()) e.rec.Event(cr, plumbingevent.Warning("CannotLoadConfigMap", "Reconciling", fmt.Errorf("Unable to load configmap with template data: %s", err.Error()))) } diff --git a/internal/tools/copier/copier.go b/internal/tools/copier/copier.go index 6d17747..7ec370b 100644 --- a/internal/tools/copier/copier.go +++ b/internal/tools/copier/copier.go @@ -283,7 +283,11 @@ func (co *Copier) copyFile(src, dst string, doNotRender bool) (err error) { defer func() { if e := out.Close(); e != nil { - err = e + if err != nil { + err = fmt.Errorf("%v; additionally failed to close file: %w", err, e) + } else { + err = fmt.Errorf("failed to close file: %w", e) + } } }() From bb13ccd8f77f118e45f8f69e34206fd54a367624 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Fri, 8 May 2026 16:46:39 +0200 Subject: [PATCH 05/13] fix: update IsInGitCommitHistory function signature to include context and adjust usage in repo controller --- internal/clients/git/git.go | 5 +++-- internal/controllers/repo/repo.go | 2 +- internal/controllers/repo/repo_test.go | 12 +++++++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/internal/clients/git/git.go b/internal/clients/git/git.go index 4eeb822..1ae8261 100644 --- a/internal/clients/git/git.go +++ b/internal/clients/git/git.go @@ -16,7 +16,6 @@ import ( contexttools "github.com/krateoplatformops/provider-runtime/pkg/context" "github.com/krateoplatformops/provider-runtime/pkg/logging" - "github.com/rs/zerolog/log" "github.com/go-git/go-git/v5/plumbing/cache" gitclient "github.com/go-git/go-git/v5/plumbing/transport/client" @@ -202,7 +201,9 @@ func restoreUnsupportedCapabilities(oldUnsupportedCaps []capability.Capability) transport.UnsupportedCapabilities = oldUnsupportedCaps } -func IsInGitCommitHistory(opts ListOptions, hash string) (bool, error) { +func IsInGitCommitHistory(ctx context.Context, opts ListOptions, hash string) (bool, error) { + log := contexttools.LoggerFromCtx(ctx, logging.NewNopLogger()) + tmpDir, err := os.MkdirTemp(opts.HomeDir, "git-provider-history-*") if err != nil { return false, fmt.Errorf("failed to create temporary directory: %w", err) diff --git a/internal/controllers/repo/repo.go b/internal/controllers/repo/repo.go index 99a2024..489a1b0 100644 --- a/internal/controllers/repo/repo.go +++ b/internal/controllers/repo/repo.go @@ -163,7 +163,7 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (reconciler return reconciler.ExternalObservation{}, err } - isTargetRepoSynced, err := git.IsInGitCommitHistory(git.ListOptions{ + isTargetRepoSynced, err := git.IsInGitCommitHistory(ctx, git.ListOptions{ URL: cr.Spec.ToRepo.Url, Auth: e.cfg.ToRepoCreds, Insecure: e.cfg.Insecure, diff --git a/internal/controllers/repo/repo_test.go b/internal/controllers/repo/repo_test.go index 23301a8..b63df26 100644 --- a/internal/controllers/repo/repo_test.go +++ b/internal/controllers/repo/repo_test.go @@ -969,7 +969,10 @@ func TestController(t *testing.T) { f := features.New("RepoControllerFeatures"). Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { - ctx, err := setupController(ctx, cfg) + mgrCtx, mgrCancel := context.WithCancel(context.Background()) + ctx = context.WithValue(ctx, "mgrCancel", mgrCancel) + + _, err := setupController(mgrCtx, cfg) require.NoError(t, err) r, err := resources.New(cfg.Client().RESTConfig()) @@ -991,6 +994,13 @@ func TestController(t *testing.T) { }, })) + return ctx + }). + Teardown(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { + if cancel, ok := ctx.Value("mgrCancel").(context.CancelFunc); ok { + cancel() + time.Sleep(1 * time.Second) // Give manager time to stop + } return ctx }) From f454dd50f54a6dc224fcb91464e3294cd74a8ab1 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Fri, 8 May 2026 17:50:22 +0200 Subject: [PATCH 06/13] fix: enhance error handling for branch not found and refactor IsInGitCommitHistory usage --- internal/clients/git/git.go | 42 ++++++++++++++++--------------- internal/clients/git/git_test.go | 14 +++++++++++ internal/controllers/repo/repo.go | 2 +- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/internal/clients/git/git.go b/internal/clients/git/git.go index 1ae8261..30c1245 100644 --- a/internal/clients/git/git.go +++ b/internal/clients/git/git.go @@ -3,6 +3,7 @@ package git import ( "bytes" "context" + "errors" "fmt" "net/http" "net/http/cookiejar" @@ -45,6 +46,7 @@ var ( ErrEmptyRemoteRepository = fmt.Errorf("remote repository is empty: %w", transport.ErrEmptyRemoteRepository) ErrAuthenticationRequired = fmt.Errorf("authentication required: %w", transport.ErrAuthenticationRequired) ErrAuthorizationFailed = fmt.Errorf("authorization failed: %w", transport.ErrAuthorizationFailed) + ErrBranchNotFound = errors.New("branch not found") NoErrAlreadyUpToDate = git.NoErrAlreadyUpToDate ) @@ -194,14 +196,14 @@ func GetLatestCommitRemote(opts ListOptions) (*string, error) { } } - return nil, fmt.Errorf("Branch %s reference %s not found on remote %s", opts.Branch, repoRef, opts.URL) + return nil, fmt.Errorf("%w: branch %s reference %s not found on remote %s", ErrBranchNotFound, opts.Branch, repoRef, opts.URL) } func restoreUnsupportedCapabilities(oldUnsupportedCaps []capability.Capability) { transport.UnsupportedCapabilities = oldUnsupportedCaps } -func IsInGitCommitHistory(ctx context.Context, opts ListOptions, hash string) (bool, error) { +func isInGitCommitHistory(ctx context.Context, opts ListOptions, hash string) (bool, error) { log := contexttools.LoggerFromCtx(ctx, logging.NewNopLogger()) tmpDir, err := os.MkdirTemp(opts.HomeDir, "git-provider-history-*") @@ -262,7 +264,7 @@ func IsInGitCommitHistory(ctx context.Context, opts ListOptions, hash string) (b log.Warn("Branch not found in remote repository", "branch", opts.Branch, "url", opts.URL) return false, nil } - return false, fmt.Errorf("failed to clone repository: %v", err) + return false, fmt.Errorf("failed to clone repository: %w", err) } head, err := res.repo.Head() if err != nil { @@ -351,7 +353,7 @@ func IsFuncInGitCommitHistory(ctx context.Context, opts ListOptions, f func(comm log.Warn("Branch not found in remote repository", "branch", opts.Branch, "url", opts.URL) return plumbing.Hash{}, nil } - return plumbing.Hash{}, fmt.Errorf("failed to clone repository: %v", err) + return plumbing.Hash{}, fmt.Errorf("failed to clone repository: %w", err) } head, err := res.repo.Head() if err != nil { @@ -433,6 +435,10 @@ func (s *Repo) UpdateIndex(idx *IndexOptions) error { return nil } func Clone(opts CloneOptions) (*Repo, error) { + return clone(context.Background(), opts) +} + +func clone(ctx context.Context, opts CloneOptions) (*Repo, error) { tmpDir, err := os.MkdirTemp(opts.HomeDir, "git-provider-clone-*") if err != nil { return nil, fmt.Errorf("failed to create temporary directory: %w", err) @@ -495,6 +501,9 @@ func Clone(opts CloneOptions) (*Repo, error) { GitCookies: opts.GitCookies, }) if err != nil { + if !errors.Is(err, ErrBranchNotFound) { + return nil, fmt.Errorf("failed to inspect remote branch: %w", err) + } cloneOpts = git.CloneOptions{ RemoteName: "origin", URL: opts.URL, @@ -515,22 +524,7 @@ func Clone(opts CloneOptions) (*Repo, error) { } res.repo, err = git.Clone(res.storer, res.fs, &cloneOpts) if err != nil { - if utils.IsErr(ErrRepositoryNotFound, err) { - return nil, ErrRepositoryNotFound - } - - if utils.IsErr(ErrEmptyRemoteRepository, err) { - return nil, ErrEmptyRemoteRepository - } - - if utils.IsErr(ErrAuthenticationRequired, err) { - return nil, ErrAuthenticationRequired - } - - if utils.IsErr(ErrAuthorizationFailed, err) { - return nil, ErrAuthorizationFailed - } - return nil, err + return nil, fmt.Errorf("failed to clone repository: %w", err) } err = res.Branch(opts.Branch, &CreateOpt{ @@ -542,6 +536,14 @@ func Clone(opts CloneOptions) (*Repo, error) { return res, err } +func IsInGitCommitHistory(opts ListOptions, hash string) (bool, error) { + return isInGitCommitHistory(context.Background(), opts, hash) +} + +func IsInGitCommitHistoryContext(ctx context.Context, opts ListOptions, hash string) (bool, error) { + return isInGitCommitHistory(ctx, opts, hash) +} + func (s *Repo) Exists(path string) (bool, error) { if err := s.setCustomHTTPSClientWithCookieJar(); err != nil { return false, err diff --git a/internal/clients/git/git_test.go b/internal/clients/git/git_test.go index 92dbb94..7ba2afe 100644 --- a/internal/clients/git/git_test.go +++ b/internal/clients/git/git_test.go @@ -2,6 +2,7 @@ package git import ( "context" + "errors" "fmt" "os" "strings" @@ -65,6 +66,19 @@ func TestGetLatestCommitRemote(t *testing.T) { assert.Equal(t, expected, *commit) } +func TestGetLatestCommitRemoteBranchNotFound(t *testing.T) { + baseRepo := BaseSuite{} + baseRepo.BuildBasicRepository() + + _, err := GetLatestCommitRemote(ListOptions{ + URL: baseRepo.GetBasicLocalRepositoryURL(), + Branch: "missing-branch", + }) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrBranchNotFound)) + assert.Contains(t, err.Error(), "missing-branch") +} + func TestPull(t *testing.T) { baseRepo := BaseSuite{} baseRepo.BuildBasicRepository() diff --git a/internal/controllers/repo/repo.go b/internal/controllers/repo/repo.go index 489a1b0..728dcf6 100644 --- a/internal/controllers/repo/repo.go +++ b/internal/controllers/repo/repo.go @@ -163,7 +163,7 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (reconciler return reconciler.ExternalObservation{}, err } - isTargetRepoSynced, err := git.IsInGitCommitHistory(ctx, git.ListOptions{ + isTargetRepoSynced, err := git.IsInGitCommitHistoryContext(ctx, git.ListOptions{ URL: cr.Spec.ToRepo.Url, Auth: e.cfg.ToRepoCreds, Insecure: e.cfg.Insecure, From f12ed18f253f5f15bac979043c9a8c8402fe25a5 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Mon, 11 May 2026 14:10:08 +0200 Subject: [PATCH 07/13] fix: improve error normalization and handling in Git operations --- internal/clients/git/git.go | 36 ++++++++++++++++++--- internal/clients/git/git_test.go | 10 ++++++ internal/controllers/repo/repo.go | 22 ++++++------- internal/controllers/repo/repo_test.go | 45 ++++++++++++++++++-------- 4 files changed, 84 insertions(+), 29 deletions(-) diff --git a/internal/clients/git/git.go b/internal/clients/git/git.go index 30c1245..38d2b40 100644 --- a/internal/clients/git/git.go +++ b/internal/clients/git/git.go @@ -50,6 +50,32 @@ var ( NoErrAlreadyUpToDate = git.NoErrAlreadyUpToDate ) +type normalizedError struct { + err error + msg string +} + +func (e normalizedError) Error() string { + return e.msg +} + +func (e normalizedError) Unwrap() error { + return e.err +} + +func normalizeEmptyReasonError(err error) error { + if err == nil { + return nil + } + + msg := err.Error() + if strings.HasSuffix(msg, ": ") { + return normalizedError{err: err, msg: strings.TrimSuffix(msg, ": ")} + } + + return err +} + var clientMutex sync.Mutex type Repo struct { @@ -187,7 +213,7 @@ func GetLatestCommitRemote(opts ListOptions) (*string, error) { InsecureSkipTLS: opts.Insecure, }) if err != nil { - return nil, err + return nil, normalizeEmptyReasonError(err) } repoRef := plumbing.NewBranchReferenceName(opts.Branch) for _, ref := range refs { @@ -264,7 +290,7 @@ func isInGitCommitHistory(ctx context.Context, opts ListOptions, hash string) (b log.Warn("Branch not found in remote repository", "branch", opts.Branch, "url", opts.URL) return false, nil } - return false, fmt.Errorf("failed to clone repository: %w", err) + return false, fmt.Errorf("failed to clone repository: %w", normalizeEmptyReasonError(err)) } head, err := res.repo.Head() if err != nil { @@ -353,7 +379,7 @@ func IsFuncInGitCommitHistory(ctx context.Context, opts ListOptions, f func(comm log.Warn("Branch not found in remote repository", "branch", opts.Branch, "url", opts.URL) return plumbing.Hash{}, nil } - return plumbing.Hash{}, fmt.Errorf("failed to clone repository: %w", err) + return plumbing.Hash{}, fmt.Errorf("failed to clone repository: %w", normalizeEmptyReasonError(err)) } head, err := res.repo.Head() if err != nil { @@ -502,7 +528,7 @@ func clone(ctx context.Context, opts CloneOptions) (*Repo, error) { }) if err != nil { if !errors.Is(err, ErrBranchNotFound) { - return nil, fmt.Errorf("failed to inspect remote branch: %w", err) + return nil, fmt.Errorf("failed to inspect remote branch: %w", normalizeEmptyReasonError(err)) } cloneOpts = git.CloneOptions{ RemoteName: "origin", @@ -524,7 +550,7 @@ func clone(ctx context.Context, opts CloneOptions) (*Repo, error) { } res.repo, err = git.Clone(res.storer, res.fs, &cloneOpts) if err != nil { - return nil, fmt.Errorf("failed to clone repository: %w", err) + return nil, fmt.Errorf("failed to clone repository: %w", normalizeEmptyReasonError(err)) } err = res.Branch(opts.Branch, &CreateOpt{ diff --git a/internal/clients/git/git_test.go b/internal/clients/git/git_test.go index 7ba2afe..dccb46c 100644 --- a/internal/clients/git/git_test.go +++ b/internal/clients/git/git_test.go @@ -79,6 +79,16 @@ func TestGetLatestCommitRemoteBranchNotFound(t *testing.T) { assert.Contains(t, err.Error(), "missing-branch") } +func TestNormalizeEmptyReasonError(t *testing.T) { + sentinel := errors.New("authentication required") + err := fmt.Errorf("%w: ", sentinel) + + normalized := normalizeEmptyReasonError(err) + + require.Equal(t, "authentication required", normalized.Error()) + assert.True(t, errors.Is(normalized, sentinel)) +} + func TestPull(t *testing.T) { baseRepo := BaseSuite{} baseRepo.BuildBasicRepository() diff --git a/internal/controllers/repo/repo.go b/internal/controllers/repo/repo.go index 728dcf6..7ba0f65 100644 --- a/internal/controllers/repo/repo.go +++ b/internal/controllers/repo/repo.go @@ -176,17 +176,6 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (reconciler return reconciler.ExternalObservation{}, err } - if ptr.Deref(latestCommit, "") != cr.Status.OriginCommitId { - e.log.Debug("Origin commit not found in origin remote repository", "commitId", cr.Status.OriginCommitId, "branch", cr.Status.OriginBranch) - if !cr.Spec.EnableUpdate { - return reconciler.ExternalObservation{}, e.failSync(ctx, cr, fmt.Errorf("origin commit %s is no longer the latest commit on branch %s while enableUpdate is false", cr.Status.OriginCommitId, cr.Status.OriginBranch)) - } - return reconciler.ExternalObservation{ - ResourceExists: true, - ResourceUpToDate: false, - }, nil - } - if !isTargetRepoSynced { e.log.Debug("Target commit not found in target remote repository", "commitId", cr.Status.TargetCommitId, "branch", cr.Status.TargetBranch) if !cr.Spec.EnableUpdate { @@ -198,6 +187,17 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (reconciler }, nil } + if ptr.Deref(latestCommit, "") != cr.Status.OriginCommitId { + e.log.Debug("Origin commit is no longer the latest commit on origin repository", "commitId", cr.Status.OriginCommitId, "branch", cr.Status.OriginBranch) + if cr.Spec.EnableUpdate { + return reconciler.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: false, + }, nil + } + e.log.Debug("Origin commit changed but enableUpdate is false, keeping the target repository unchanged") + } + cr.Status.SetConditions(commonv1.Available()) return reconciler.ExternalObservation{ diff --git a/internal/controllers/repo/repo_test.go b/internal/controllers/repo/repo_test.go index b63df26..39d6e31 100644 --- a/internal/controllers/repo/repo_test.go +++ b/internal/controllers/repo/repo_test.go @@ -871,16 +871,23 @@ func TestController(t *testing.T) { }) require.NotEqual(t, initialOrigin, updatedOrigin) - later, err := waitForRepo(ctx, r, repoName, 45*time.Second, func(repo *repov1alpha1.Repo) bool { - synced := repo.GetCondition(commonv1.TypeSynced) - return synced.Status == metav1.ConditionFalse && - synced.Reason == commonv1.ReasonReconcileError - }) + require.Never(t, func() bool { + current := &repov1alpha1.Repo{} + if err := r.Get(ctx, repoName, namespace, current); err != nil { + return false + } + synced := current.GetCondition(commonv1.TypeSynced) + return synced.Status == metav1.ConditionFalse && synced.Reason == commonv1.ReasonReconcileError + }, 15*time.Second, 2*time.Second) + + later := &repov1alpha1.Repo{} + err = r.Get(ctx, repoName, namespace, later) require.NoError(t, err) require.Equal(t, initialOrigin, later.Status.OriginCommitId) require.Equal(t, initialTarget, later.Status.TargetCommitId) require.Equal(t, initialRemoteTarget, latestRemoteCommit(t, "dst-tc11", "main")) - require.Contains(t, later.GetCondition(commonv1.TypeSynced).Message, "enableUpdate is false") + require.Equal(t, metav1.ConditionTrue, later.GetCondition(commonv1.TypeReady).Status) + require.Equal(t, metav1.ConditionTrue, later.GetCondition(commonv1.TypeSynced).Status) require.Equal(t, "v1\n", readRemoteFile(t, "dst-tc11", "main", "content/app.txt")) assertRemoteFileAbsent(t, "dst-tc11", "main", "content/extra.txt") }, @@ -938,22 +945,34 @@ func TestController(t *testing.T) { // Il primo sync (fase di Create) avviene a prescindere da EnableUpdate _, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeReady, metav1.ConditionTrue, 90*time.Second) require.NoError(t, err) + initialTarget := latestRemoteCommit(t, "dst-tc13", "main") // Simuliamo un aggiornamento nel repo sorgente updatedOrigin := commitFilesToRepo(t, "src-tc13", "main", "update source", map[string]string{ "content/app.txt": "v2\n", }) - // Essendo EnableUpdate = false, il Reconcile va in errore - failed, err := waitForRepoCondition(ctx, r, repoName, commonv1.TypeSynced, metav1.ConditionFalse, 45*time.Second) + // Essendo EnableUpdate = false, il controller resta passivo ma non deve segnalare errori. + require.Never(t, func() bool { + current := &repov1alpha1.Repo{} + if err := r.Get(ctx, repoName, namespace, current); err != nil { + return false + } + synced := current.GetCondition(commonv1.TypeSynced) + return synced.Status == metav1.ConditionFalse && synced.Reason == commonv1.ReasonReconcileError + }, 15*time.Second, 2*time.Second) + + current := &repov1alpha1.Repo{} + err = r.Get(ctx, repoName, namespace, current) require.NoError(t, err) - require.Equal(t, commonv1.ReasonReconcileError, failed.GetCondition(commonv1.TypeSynced).Reason) + require.Equal(t, initialTarget, current.Status.TargetCommitId) + require.Equal(t, initialTarget, latestRemoteCommit(t, "dst-tc13", "main")) + require.Equal(t, metav1.ConditionTrue, current.GetCondition(commonv1.TypeReady).Status) + require.Equal(t, metav1.ConditionTrue, current.GetCondition(commonv1.TypeSynced).Status) // AUTO-HEAL: L'utente riabilita l'update - err = r.Get(ctx, repoName, namespace, failed) - require.NoError(t, err) - failed.Spec.EnableUpdate = true - err = r.Update(ctx, failed) + current.Spec.EnableUpdate = true + err = r.Update(ctx, current) require.NoError(t, err) // Verifichiamo che il controller guarisca automaticamente e completi l'allineamento From cc32b594ddac1849af7c03c9b6ba93c75788be83 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Mon, 11 May 2026 14:24:45 +0200 Subject: [PATCH 08/13] test: add Mustache template rendering test for file names and content --- internal/tools/copier/copier_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/tools/copier/copier_test.go b/internal/tools/copier/copier_test.go index 72111c0..590f6c8 100644 --- a/internal/tools/copier/copier_test.go +++ b/internal/tools/copier/copier_test.go @@ -63,6 +63,27 @@ func TestRenderFileNamesAndContent(t *testing.T) { } } +func TestMustacheRenderFileNamesAndContent(t *testing.T) { + from := memfs.New() + to := memfs.New() + + // source file with templated name and content + writeFile(t, from, "/src/file_{{name}}.txt", "hello {{name}}") + + co, err := NewCopier(from, to, WithOriginCopyPath("/src"), WithTargetCopyPath("/dst"), WithIgnorePath("/"), WithMustacheTemplate(map[string]string{"name": "world"})) + if err != nil { + t.Fatalf("failed to create copier: %v", err) + } + if err := co.Copy(true); err != nil { + t.Fatalf("copy failed: %v", err) + } + + got := readFile(t, to, "/dst/file_world.txt") + if got != "hello world" { + t.Fatalf("unexpected content: %q", got) + } +} + func TestMustacheRendering(t *testing.T) { from := memfs.New() to := memfs.New() From e5eadb7126056b1c3bcd512032b440b5248e0949 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Mon, 11 May 2026 14:51:25 +0200 Subject: [PATCH 09/13] fix: update README and add documentation for LocalResource and Repo CRs --- README.md | 101 ++++-------------------- docs/local-resource.md | 173 +++++++++++++++++++++++++++++++++++++++++ docs/repo.md | 128 ++++++++++++++++++++++++++++++ 3 files changed, 318 insertions(+), 84 deletions(-) create mode 100644 docs/local-resource.md create mode 100644 docs/repo.md diff --git a/README.md b/README.md index 45cb4bc..f669806 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,17 @@ # Git Provider -This is a [Krateo](https://krateo.io) Provider that clones git repositories (eventually applying templates). - -## Summary - -- [Summary](#summary) -- [Overview](#overview) -- [Examples](#examples) -- [Configuration](#configuration) - +This is a [Krateo](https://krateo.io) Provider that enables Git operations natively from your Kubernetes cluster. ## Overview -Git Provider clones git repositories and may apply [Mustache templates](https://mustache.github.io). It then pushes the cloned and modified repository to a different location. The templating values are retrieved in a configmap referenced in the custom resource. -It provides automatic reconciliation when changes are retrieved from the original repository. +The `git-provider` leverages Krateo [provider-runtime](https://docs.krateo.io/key-concepts/kco/#provider-runtime), a production-grade version of the controller-runtime, to provide automatic reconciliation and Git interactions. -Git Provider leverages Krateo [provider-runtime](https://docs.krateo.io/key-concepts/kco/#provider-runtime) a production-grade version of the controller-runtime. +It exposes two distinct Custom Resources (CRs) to handle different use cases: -## Examples +* **[Repo](docs/repo.md):** Designed for **Git-to-Git** workflows. It clones an existing Git repository, optionally applies [Mustache templates](https://mustache.github.io) to the files using values from a `ConfigMap`, and pushes the result to a destination repository. +* **[LocalResource](docs/local-resource.md):** Designed for **K8s-to-Git** workflows. It takes a local source (such as an embedded Kubernetes manifest, a reference to an existing cluster resource, or a raw string), optionally applies placeholder replacements, and commits the result directly to a destination Git repository. -### Provider Installation +## Installation ```bash $ helm repo add krateo https://charts.krateo.io @@ -27,88 +19,29 @@ $ helm repo update krateo $ helm install git-provider krateo/git-provider ``` -### Manifest Application - -As a first step, you need to create a [`kind: Repo` Manifest](#repo-manifest) as shown below and a [ConfigMap](#configmap-manifest) which will contain the templating values. +## Documentation -### File Templating -`git-provider` uses the Mustache library ([see custom delimiter reference](https://github.com/janl/mustache.js/?tab=readme-ov-file#setting-in-templates)) to apply templating. Therefore, you need to specify the custom delimiter you want to use in the first line of the file you want to template. You can see an example [here](https://github.com/krateoplatformops/krateo-v2-template-fireworksapp/blob/5dee9fe1d2de3785eb7e6374ad50e3f8e7b12907/skeleton/chart/values.yaml#L1C1-L1C14). - -### File Name Templating -If you need to template the filename of a file, you can only use the delimiters `{{ }}` (e.g., `{{ your-prop }}.yaml`). - -#### Repo Manifest -```yaml -apiVersion: git.krateo.io/v1alpha1 -kind: Repo -metadata: - name: test-repo -spec: - enableUpdate: false - configMapKeyRef: - key: values - name: filename-replace-values - namespace: default - fromRepo: - authMethod: generic - branch: main - path: skeleton - usernameRef: - key: username - name: git-username - namespace: default - secretRef: - key: token - name: git-secret - namespace: default - url: https://github.com/your-organization/fromRepo - toRepo: - authMethod: generic - branch: main - cloneFromBranch: main - path: / - secretRef: - key: token - name: git-secret - namespace: default - usernameRef: - key: username - name: git-username - namespace: default - url: https://github.com/your-organization/toRepo - unsupportedCapabilities: true -``` - -#### Configmap Manifest -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: filename-replace-values -data: - values: | - { - "organizationName": "krateo", - "repositoryName": "testfilename", - "serviceType": "type", - "servicePort": "8080", - "testTemplate": "tplKrateo" - } -``` +For detailed configuration, templating rules, and synchronization behavior, please refer to the specific documentation for each Custom Resource: +* 📖 [**Repo CR Documentation**](docs/repo.md) +* 📖 [**LocalResource CR Documentation**](docs/local-resource.md) ## Environment Variables +The provider controller can be configured using the following environment variables: + | Environment Variable | Type | Default Value | Description | |---------------------|------|---------------|-------------| | `GIT_PROVIDER_DEBUG` | bool | `false` | Run with debug logging | | `GIT_PROVIDER_SYNC_PERIOD` | duration | `1h` | Controller manager sync period (e.g., 300ms, 1.5h, or 2h45m) | -| `GIT_PROVIDER_POLL_INTERVAL` | duration | `2m` | Poll interval controls how often an individual resource should be checked for drift | +| `GIT_PROVIDER_POLL_INTERVAL` | duration | `3m` | Poll interval controls how often an individual resource should be checked for drift | | `GIT_PROVIDER_MAX_RECONCILE_RATE` | int | `5` | The number of concurrent reconciles for each controller. Maximum number of resources that can be reconciled at the same time | | `GIT_PROVIDER_LEADER_ELECTION` | bool | `false` | Use leader election for the controller manager | | `GIT_PROVIDER_MAX_ERROR_RETRY_INTERVAL` | duration | `1m` | The maximum interval between retries when an error occurs. Should be less than half of the poll interval | | `GIT_PROVIDER_MIN_ERROR_RETRY_INTERVAL` | duration | `1s` | The minimum interval between retries when an error occurs. Should be less than max-error-retry-interval | | `GIT_PROVIDER_TIMEOUT` | duration | `4m` | The timeout time for each action. | +| `GIT_PROVIDER_GIT_COMMIT_AUTHOR_NAME` | string | `krateo-git-provider` | The name to use for git commits. | +| `GIT_PROVIDER_GIT_COMMIT_AUTHOR_EMAIL` | string | `contact@krateo.io` | The email to use for git commits. | -## Configuration -To view the CR configuration visit [this link](https://doc.crds.dev/github.com/krateoplatformops/git-provider). \ No newline at end of file +## CRD Reference +To view the generated CR configuration schema, visit [this link](https://doc.crds.dev/github.com/krateoplatformops/git-provider). \ No newline at end of file diff --git a/docs/local-resource.md b/docs/local-resource.md new file mode 100644 index 0000000..0cbbc5e --- /dev/null +++ b/docs/local-resource.md @@ -0,0 +1,173 @@ +# LocalResource + +The `LocalResource` Custom Resource (CR) allows you to export content to a Git repository. The source content can be a Kubernetes manifest (either as a raw string or an embedded object) or a reference to an existing resource within the cluster. + +## Overview + +Unlike the `Repo` CR, which handles Git-to-Git cloning and templating, `LocalResource` focuses on the **K8s-to-Git** (or String-to-Git) workflow. It takes a defined source, optionally applies placeholder substitutions, and commits the result to a specified branch in a target Git repository. + +**Why use it?** +* **GitOps Backup/Export:** Automatically backup specific Kubernetes resources (like a `ConfigMap` or a dynamically generated `Secret`) directly into a Git repository. +* **Dynamic Documentation/Files:** Generate a `README.md`, an environment-specific configuration file, or any arbitrary file on the fly using the `fromString` capability and push it to a repository, injecting real-time cluster data via placeholders. + +## Sources (`FromResource`) + +You must define exactly one source type within the `spec.fromResource` block. + +### 1. `FromYaml` +Provides a valid Kubernetes manifest directly embedded in the CR. It is considered valid if it contains `kind`, `apiVersion`, and either `name` or `generateName`. + +```yaml +fromResource: + fromYaml: + apiVersion: v1 + kind: ConfigMap + metadata: + name: my-config + data: + key: value +``` + +### 2. `FromRef` +References an existing resource within the same cluster to copy from. + +```yaml +fromResource: + fromRef: + apiVersion: v1 + resource: configmaps + name: my-existing-config + namespace: default # Optional, omit for cluster-scoped resources +``` + +### 3. `FromString` +Contains a string representation of a K8s manifest or any other text content. Unlike `fromYaml`, this is not strictly validated as a K8s object, preserving the exact string content and ordering. + +> [!IMPORTANT] +> When using `fromString`, you **must** also provide a `fileName`. + +```yaml +fromResource: + fileName: custom-file.txt + fromString: | + This is some custom text content. + It can be anything. +``` + +### File Name Generation +If `fileName` is not explicitly provided in `spec.fromResource` (and you are not using `fromString`), the provider automatically generates a filename based on the resource metadata: +`{kind}_{metadata.name}_{metadata.namespace}.yaml` (namespace is omitted if empty). + +## Templating + +`LocalResource` supports a simple placeholder replacement mechanism. You can define a list of `placeholdersToOverride` in the spec. The provider will look for occurrences of `{{ .placeholderName }}` in the source content and replace them with the corresponding value. + +```yaml +spec: + placeholdersToOverride: + - name: environment + value: production + - name: replicaCount + value: "3" +``` +In your source content, you would use `{{ .environment }}` and `{{ .replicaCount }}`. + +## Custom Commits + +You can customize the commit messages used when the provider pushes changes to the target repository. A description indicating the CR that triggered the commit will be automatically appended to these messages. + +* `createCommitMessage`: Used when creating new files (Default: `"chore: add files to remote repository"`). +* `updateCommitMessage`: Used when updating existing files (Default: `"chore: update files in remote repository"`). + +> [!NOTE] +> You cannot include the substring `"Managed by git-provider LocalResource:"` in your custom messages, as this is reserved for the provider's internal tracking. + +## Synchronization & Overrides + +The behavior of how `LocalResource` interacts with the target repository over time is controlled by two key flags: `syncEnabled` and `override`. + +### `syncEnabled` +* **`false` (Default):** The provider performs a "one-shot" execution. It will process the source and push it to the target repository once. Subsequent changes to the source resource (e.g., modifying the `ConfigMap` referenced by `fromRef`) will **not** trigger a new commit. Furthermore, the source definitions (`fromYaml`, `fromString`, `placeholdersToOverride`) become immutable. +* **`true`:** The provider continuously monitors the source resource. If the source changes (or if you update the `fromYaml`/`fromString` in the CR), the provider will generate a new commit in the target repository to reflect those changes. + +### `override` +* **`false` (Default - Additive):** The provider will only add new files or update the specific file generated by this CR in the destination repository. It will leave other pre-existing files in the target path untouched. +* **`true` (Destructive):** The provider will override the existing files in the destination repository's specified path with the files from the source. + +> [!CAUTION] +> Avoid using `override: true` with `path: "/"` as it will overwrite repository service folders like `.git`, `.github`, `.gitignore`, etc. + +### Behavior Matrix + +| `syncEnabled` | `override` | Behavior | +| :--- | :--- | :--- | +| `false` | `false` | **One-shot Add/Update:** Pushes the file once. Leaves other files in the directory intact. Source fields become immutable. | +| `false` | `true` | **One-shot Replace:** Pushes the file once. **Deletes all other files** in the target `path`. Source fields become immutable. | +| `true` | `false` | **Continuous Sync Add/Update:** Pushes the file and updates it whenever the source changes. Leaves other files in the directory intact. | +| `true` | `true` | **Continuous Sync Replace:** Pushes the file and updates it whenever the source changes. **Deletes all other files** in the target `path` on every sync. | + +## Examples + +### Exporting a ConfigMap (fromYaml) +This example pushes a ConfigMap directly to a Git repository. + +```yaml +apiVersion: git.krateo.io/v1alpha1 +kind: LocalResource +metadata: + name: export-cm +spec: + syncEnabled: false + override: false + fromResource: + fromYaml: + apiVersion: v1 + kind: ConfigMap + metadata: + name: my-app-config + data: + logLevel: debug + toRepo: + url: https://github.com/your-org/target-repo + branch: main + path: /configs + credentials: + authMethod: basic + secretRef: + name: git-credentials + namespace: default + key: token + usernameRef: + name: git-credentials + namespace: default + key: username +``` + +### Dynamic Documentation (fromString) +This example dynamically creates a text file using a string and templating, and keeps it synchronized if you update the CR. + +```yaml +apiVersion: git.krateo.io/v1alpha1 +kind: LocalResource +metadata: + name: export-string +spec: + syncEnabled: true + placeholdersToOverride: + - name: teamName + value: platform-ops + fromResource: + fileName: team-info.txt + fromString: | + This repository is managed by the {{ .teamName }} team. + toRepo: + url: https://github.com/your-org/target-repo + branch: main + path: /docs + credentials: + authMethod: basic + secretRef: + name: git-credentials + namespace: default + key: token +``` \ No newline at end of file diff --git a/docs/repo.md b/docs/repo.md new file mode 100644 index 0000000..246f3df --- /dev/null +++ b/docs/repo.md @@ -0,0 +1,128 @@ +# Repo + +The `Repo` Custom Resource (CR) manages Git-to-Git operations. It clones a source repository, optionally applies templates to its contents, and pushes the result to a destination repository. + +## Overview + +`Repo` is designed for scenarios where you need to copy an entire repository (or a specific path within it) to another location. + +**Why use it?** +A common use case is **project bootstrapping**. You might have a "golden path" or "skeleton" repository containing standard boilerplate code, CI/CD pipelines, and configuration files. When a developer requests a new project, you can use the `Repo` CR to clone this skeleton, inject project-specific values (like project name, ports, or namespaces) via templating, and push the customized result to a brand new Git repository for the developer to use. + +## Templating + +The `git-provider` supports two templating engines to customize files copied from the source repository: **Mustache** (default) and **Go Templates**. + +### Providing Values +Templating values are provided via a referenced `ConfigMap` defined in `spec.configMapKeyRef`. The referenced key in the `ConfigMap` must contain a valid JSON string representing the key-value pairs for the templates. + +```yaml +spec: + configMapKeyRef: + name: my-template-values + namespace: default + key: values # This key in the ConfigMap contains the JSON string +``` + +### Mustache (Default) +By default, the provider uses the [Mustache](https://mustache.github.io) templating engine with `{{ }}` delimiters. + +> [!TIP] +> If you are templating files where `{{ }}` conflicts with the file's native syntax (e.g., Helm charts or GitHub Actions), you can specify custom delimiters in the **first line** of the file you want to template. +> +> For example, to change the delimiters to `<% %>`, add this as the first line: +> ```text +> {{=<% %>=}} +> ``` +> Then use `<% myValue %>` in the rest of the file. [See custom delimiter reference](https://github.com/janl/mustache.js/?tab=readme-ov-file#setting-in-templates). + +### Go Templates +If you prefer [Go templates](https://pkg.go.dev/text/template), you can enable it by adding a specific annotation to your `Repo` CR: + +```yaml +apiVersion: git.krateo.io/v1alpha1 +kind: Repo +metadata: + name: test-repo + annotations: + krateo.io/templating-engine: "gotemplate" +``` + +> [!NOTE] +> The Go template engine includes the [Sprig function library](http://masterminds.github.io/sprig/), providing a wide variety of template functions (e.g., string manipulation, math, default values). + +### File Name Templating +If you need to template the filename itself, you can only use the default `{{ }}` delimiters in the filename string (e.g., `{{ your-prop }}.yaml`), regardless of the engine chosen. + +## Synchronization & Overrides + +The behavior of how the `Repo` CR interacts with the destination repository over time is controlled by two key flags: `enableUpdate` and `override`. + +### `enableUpdate` +* **`false` (Default):** The provider performs a "one-shot" execution. It clones the source, templates it, and pushes it to the destination once. Subsequent commits to the source repository will **not** trigger a new push to the destination. +* **`true`:** The provider continuously monitors the source repository. When newer commits are retrieved from the `fromRepo`, the provider performs updates on the repository specified in `toRepo`, re-applying templates and pushing the changes. + +### `override` +* **`false` (Default - Additive):** The provider will only add new files and update existing templated files in the destination repository. It will leave other pre-existing files in the destination path untouched. +* **`true` (Destructive):** The provider will override the existing files in the destination repository with the files from the source repository. + +> [!WARNING] +> Avoid using `override: true` if both `fromRepo.path` and `toRepo.path` are `/` (the root). This will override and potentially delete service folders like `.git`, `.github`, `.gitignore`, etc., in the destination repository! + +### Behavior Matrix + +| `enableUpdate` | `override` | Behavior | +| :--- | :--- | :--- | +| `false` | `false` | **One-shot Add/Update:** Copies and templates files once. Leaves other files in the destination directory intact. | +| `false` | `true` | **One-shot Replace:** Copies and templates files once. **Deletes all other files** in the destination `path`. | +| `true` | `false` | **Continuous Sync Add/Update:** Re-syncs whenever the source repo gets new commits. Leaves other files in the destination directory intact. | +| `true` | `true` | **Continuous Sync Replace:** Re-syncs whenever the source repo gets new commits. **Deletes all other files** in the destination `path` on every sync. | + +## Examples + +### Bootstrapping a Project with Go Templates + +#### 1. ConfigMap Manifest +Create a ConfigMap containing the values for your templates: +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: project-values +data: + values: | + { + "projectName": "my-awesome-api", + "servicePort": "8080" + } +``` + +#### 2. Repo Manifest +Create the `Repo` CR to initiate the project bootstrap: +```yaml +apiVersion: git.krateo.io/v1alpha1 +kind: Repo +metadata: + name: bootstrap-api + annotations: + krateo.io/templating-engine: "gotemplate" +spec: + enableUpdate: false # We only want to bootstrap it once + override: false + configMapKeyRef: + key: values + name: project-values + namespace: default + fromRepo: + authMethod: generic + branch: main + path: templates/go-api-skeleton + url: https://github.com/my-org/golden-paths + # ... secretRefs omitted for brevity ... + toRepo: + authMethod: generic + branch: main + path: / + url: https://github.com/my-org/my-awesome-api + # ... secretRefs omitted for brevity ... +``` \ No newline at end of file From e440a42adc1b690ba131bf21098d925de2aea33e Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Mon, 11 May 2026 15:11:32 +0200 Subject: [PATCH 10/13] fix: add waitForLocalResourceDeletion function and enhance deletion handling in tests --- .../localresource/localresource_test.go | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/internal/controllers/localresource/localresource_test.go b/internal/controllers/localresource/localresource_test.go index 85095d3..b26def9 100644 --- a/internal/controllers/localresource/localresource_test.go +++ b/internal/controllers/localresource/localresource_test.go @@ -33,6 +33,7 @@ import ( "github.com/krateoplatformops/provider-runtime/pkg/logging" "github.com/krateoplatformops/provider-runtime/pkg/ratelimiter" v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -825,7 +826,22 @@ spec: } } - time.Sleep(30 * time.Second) // wait for the controller to process deletions + for _, test := range toDeleteAndPatch { + var res v1alpha1.LocalResource + err := decoder.DecodeFile( + os.DirFS(filepath.Join(testdataPath)), test.filename, + &res, + decoder.MutateNamespace(namespace), + ) + if err != nil { + t.Fatal(err) + } + + err = waitForLocalResourceDeletion(ctx, r, res.GetName(), res.GetNamespace(), 90*time.Second) + if err != nil { + t.Fatalf("Failed waiting for LocalResource %s deletion: %v", res.Name, err) + } + } // Now we recreate them with different specs and we check if values are overritten or not according to the specs for _, test := range toDeleteAndPatch { @@ -1014,3 +1030,21 @@ func waitForGitea(ctx context.Context) error { return fmt.Errorf("Gitea failed to become ready after %v attempts", maxAttempts) } + +func waitForLocalResourceDeletion(ctx context.Context, r *resources.Resources, name, namespace string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + current := &v1alpha1.LocalResource{} + err := r.Get(ctx, name, namespace, current) + if err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return err + } + + time.Sleep(2 * time.Second) + } + + return fmt.Errorf("timed out waiting for LocalResource %s/%s deletion", namespace, name) +} From 4fa9ba05d11a2a5990863d1a37c826a4530a67fc Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Mon, 11 May 2026 15:16:07 +0200 Subject: [PATCH 11/13] fix: simplify TestFailSync by removing unnecessary kubeClient setup and unused tests --- internal/controllers/repo/repo_status_test.go | 66 +------------------ 1 file changed, 3 insertions(+), 63 deletions(-) diff --git a/internal/controllers/repo/repo_status_test.go b/internal/controllers/repo/repo_status_test.go index c3fce14..af5fe29 100644 --- a/internal/controllers/repo/repo_status_test.go +++ b/internal/controllers/repo/repo_status_test.go @@ -5,42 +5,27 @@ import ( "errors" "testing" - "github.com/krateoplatformops/git-provider/apis" repov1alpha1 "github.com/krateoplatformops/git-provider/apis/repo/v1alpha1" commonv1 "github.com/krateoplatformops/provider-runtime/apis/common/v1" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clientsetscheme "k8s.io/client-go/kubernetes/scheme" - ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestFailSyncMarksResourceUnavailableAndSyncedFalse(t *testing.T) { - require.NoError(t, apis.AddToScheme(clientsetscheme.Scheme)) - cr := &repov1alpha1.Repo{ ObjectMeta: metav1.ObjectMeta{Name: "sample", Namespace: "test-system"}, } cr.Status.SetConditions(commonv1.Available()) - kubeClient := fake.NewClientBuilder(). - WithScheme(clientsetscheme.Scheme). - WithStatusSubresource(cr). - WithObjects(cr). - Build() - - e := &external{kube: kubeClient} + e := &external{} inputErr := errors.New("push failed") returnedErr := e.failSync(context.Background(), cr, inputErr) require.ErrorIs(t, returnedErr, inputErr) - updated := &repov1alpha1.Repo{} - require.NoError(t, kubeClient.Get(context.Background(), ctrlclient.ObjectKey{Name: "sample", Namespace: "test-system"}, updated)) - - ready := updated.GetCondition(commonv1.TypeReady) - synced := updated.GetCondition(commonv1.TypeSynced) + ready := cr.GetCondition(commonv1.TypeReady) + synced := cr.GetCondition(commonv1.TypeSynced) require.Equal(t, metav1.ConditionFalse, ready.Status) require.Equal(t, commonv1.ReasonUnavailable, ready.Reason) @@ -48,48 +33,3 @@ func TestFailSyncMarksResourceUnavailableAndSyncedFalse(t *testing.T) { require.Equal(t, commonv1.ReasonReconcileError, synced.Reason) require.Equal(t, "push failed", synced.Message) } - -// errorStatusWriter simula un fallimento durante la scrittura dello status -type errorStatusWriter struct { - ctrlclient.StatusWriter -} - -func (w *errorStatusWriter) Update(ctx context.Context, obj ctrlclient.Object, opts ...ctrlclient.SubResourceUpdateOption) error { - return errors.New("simulated update conflict error") -} - -// errorClient fa da wrapper a un client mockato e restituisce lo StatusWriter fallato -type errorClient struct { - ctrlclient.Client -} - -func (c *errorClient) Status() ctrlclient.StatusWriter { - return &errorStatusWriter{c.Client.Status()} -} - -func TestFailSyncWithUpdateConflictCausesNestedError(t *testing.T) { - require.NoError(t, apis.AddToScheme(clientsetscheme.Scheme)) - - cr := &repov1alpha1.Repo{ - ObjectMeta: metav1.ObjectMeta{Name: "sample-conflict", Namespace: "test-system"}, - } - - kubeClient := fake.NewClientBuilder(). - WithScheme(clientsetscheme.Scheme). - WithStatusSubresource(cr). - WithObjects(cr). - Build() - - // Inseriamo il client malevolo - e := &external{kube: &errorClient{Client: kubeClient}} - - inputErr := errors.New("primary reconcile failure") - - // Eseguiamo failSync. - returnedErr := e.failSync(context.Background(), cr, inputErr) - - // Verifichiamo che il fallimento originale venga restituito intatto, - // senza errori di update annidati. - require.Equal(t, inputErr, returnedErr) - require.NotContains(t, returnedErr.Error(), "simulated update conflict error") -} From ff467e40c627a92dbfa640ae5136ee7cc585f159 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Mon, 11 May 2026 15:31:05 +0200 Subject: [PATCH 12/13] fix: update Repo CR documentation to clarify template options --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f669806..29c0856 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The `git-provider` leverages Krateo [provider-runtime](https://docs.krateo.io/ke It exposes two distinct Custom Resources (CRs) to handle different use cases: -* **[Repo](docs/repo.md):** Designed for **Git-to-Git** workflows. It clones an existing Git repository, optionally applies [Mustache templates](https://mustache.github.io) to the files using values from a `ConfigMap`, and pushes the result to a destination repository. +* **[Repo](docs/repo.md):** Designed for **Git-to-Git** workflows. It clones an existing Git repository, optionally applies templates ([Mustache](https://mustache.github.io) or [Go templates](https://pkg.go.dev/text/template)) to the files using values from a `ConfigMap`, and pushes the result to a destination repository. * **[LocalResource](docs/local-resource.md):** Designed for **K8s-to-Git** workflows. It takes a local source (such as an embedded Kubernetes manifest, a reference to an existing cluster resource, or a raw string), optionally applies placeholder replacements, and commits the result directly to a destination Git repository. ## Installation From f67d39c9f0a8a842178ac4e908ec0ac40005f959 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Mon, 11 May 2026 15:51:01 +0200 Subject: [PATCH 13/13] fix: enhance documentation for LocalResource and Repo CRs with advanced configuration details --- docs/local-resource.md | 31 +++++++++++++++++++++++++++++++ docs/repo.md | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/docs/local-resource.md b/docs/local-resource.md index 0cbbc5e..d4c7435 100644 --- a/docs/local-resource.md +++ b/docs/local-resource.md @@ -106,6 +106,37 @@ The behavior of how `LocalResource` interacts with the target repository over ti | `true` | `false` | **Continuous Sync Add/Update:** Pushes the file and updates it whenever the source changes. Leaves other files in the directory intact. | | `true` | `true` | **Continuous Sync Replace:** Pushes the file and updates it whenever the source changes. **Deletes all other files** in the target `path` on every sync. | +## Advanced Configuration + +The `LocalResource` CR provides several advanced flags to handle complex Git environments. + +### Authentication Methods +The `authMethod` field dictates how the provider authenticates with the destination Git server (`toRepo`). Supported values are: +* **`basic` (Default):** Basic authentication. Requires both `secretRef` (for the token/password) and `usernameRef`. +* **`bearer`:** Token-based authentication. Requires only `secretRef`. `usernameRef` is ignored. +* **`cookiefile`:** Authentication via a Git cookie file. Requires `secretRef` (containing the file contents). `usernameRef` is ignored. + +### Branch Creation & Orphan Branches +When pushing to the destination repository (`toRepo`), if the target `branch` does not exist, the provider will create it. The behavior is controlled by the `cloneFromBranch` field: +* **If `cloneFromBranch` is set:** The new branch will be derived from the specified parent branch (e.g., `main`). +* **If `cloneFromBranch` is omitted:** The provider creates an **orphan branch** (a completely empty branch with no commit history). + +### Azure DevOps Compatibility +Azure DevOps requires specific Git transport capabilities (`multi_ack` and `multi_ack_detailed`) that the underlying `go-git` library does not natively implement. +If the `toRepo.url` contains `dev.azure.com`, you **must** set the following flag to `true` (otherwise the CR validation will fail): +```yaml +spec: + unsupportedCapabilities: true +``` +This forces the library to bypass the capability check, allowing successful communication with Azure. + +### Bypassing SSL Verification +If you are interacting with Git servers using self-signed or invalid SSL certificates (e.g., in a local or air-gapped environment), you can disable certificate validation: +```yaml +spec: + insecure: true +``` + ## Examples ### Exporting a ConfigMap (fromYaml) diff --git a/docs/repo.md b/docs/repo.md index 246f3df..501b8f1 100644 --- a/docs/repo.md +++ b/docs/repo.md @@ -78,6 +78,46 @@ The behavior of how the `Repo` CR interacts with the destination repository over | `true` | `false` | **Continuous Sync Add/Update:** Re-syncs whenever the source repo gets new commits. Leaves other files in the destination directory intact. | | `true` | `true` | **Continuous Sync Replace:** Re-syncs whenever the source repo gets new commits. **Deletes all other files** in the destination `path` on every sync. | +## Advanced Configuration + +The `Repo` CR provides several advanced flags to handle complex Git environments. + +### Authentication Methods +The `authMethod` field dictates how the provider authenticates with the remote Git servers (`fromRepo` and `toRepo`). Supported values are: +* **`generic` (Default):** Basic authentication. Requires both `secretRef` (for the token/password) and `usernameRef`. +* **`bearer`:** Token-based authentication. Requires only `secretRef`. `usernameRef` is ignored. +* **`cookiefile`:** Authentication via a Git cookie file. Requires `secretRef` (containing the file contents). `usernameRef` is ignored. + +### Branch Creation & Orphan Branches +When pushing to the destination repository (`toRepo`), if the target `branch` does not exist, the provider will create it. The behavior is controlled by the `cloneFromBranch` field: +* **If `cloneFromBranch` is set:** The new branch will be derived from the specified parent branch (e.g., `main`). +* **If `cloneFromBranch` is omitted:** The provider creates an **orphan branch** (a completely empty branch with no commit history). + +### Azure DevOps Compatibility +Azure DevOps requires specific Git transport capabilities (`multi_ack` and `multi_ack_detailed`) that the underlying `go-git` library does not natively implement. +To clone or push to Azure DevOps, you **must** set the following flag to `true`: +```yaml +spec: + unsupportedCapabilities: true +``` +This forces the library to bypass the capability check, allowing successful communication with Azure. + +### Bypassing SSL Verification +If you are interacting with Git servers using self-signed or invalid SSL certificates (e.g., in a local or air-gapped environment), you can disable certificate validation: +```yaml +spec: + insecure: true +``` + +### Excluding Files (.krateoignore) +You can prevent specific files or directories from being copied from the source repository by defining a "krateo ignore" file (which uses the exact same syntax as `.gitignore`). +Specify the path to this file within the source repository using `krateoIgnorePath`. If not set, it defaults to looking for a file at `/` (the root). +```yaml +spec: + fromRepo: + krateoIgnorePath: .krateoignore +``` + ## Examples ### Bootstrapping a Project with Go Templates