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/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/README.md b/README.md index 45cb4bc..29c0856 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 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. -### 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..d4c7435 --- /dev/null +++ b/docs/local-resource.md @@ -0,0 +1,204 @@ +# 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. | + +## 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) +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..501b8f1 --- /dev/null +++ b/docs/repo.md @@ -0,0 +1,168 @@ +# 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. | + +## 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 + +#### 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 diff --git a/go.mod b/go.mod index 85b78f5..009c51b 100644 --- a/go.mod +++ b/go.mod @@ -1,63 +1,61 @@ module github.com/krateoplatformops/git-provider -go 1.25.0 +go 1.25.6 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 + 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.10.0 - 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 + github.com/stretchr/testify v1.11.1 + 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.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 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/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 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 @@ -73,6 +71,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 +79,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,55 +87,55 @@ 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/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.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.41.0 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.43.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/time v0.11.0 // indirect - golang.org/x/tools v0.36.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.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.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 c504a25..adc5af2 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,18 @@ 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= 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,14 +49,14 @@ 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= 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= @@ -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= @@ -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,10 +135,10 @@ 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= +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= @@ -149,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= @@ -165,8 +163,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= @@ -189,33 +185,33 @@ 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= 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= 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.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,15 +220,15 @@ 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= -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= @@ -247,124 +243,98 @@ 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= 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.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/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/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/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.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= 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/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/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/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +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-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= 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/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/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.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/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +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.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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= -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 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.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/clients/git/git.go b/internal/clients/git/git.go index bfe2bac..38d2b40 100644 --- a/internal/clients/git/git.go +++ b/internal/clients/git/git.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - "io/fs" "net/http" "net/http/cookiejar" "net/url" @@ -33,6 +32,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,13 +42,40 @@ 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) + ErrBranchNotFound = errors.New("branch not found") 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 { @@ -186,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 { @@ -195,14 +222,16 @@ 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(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) @@ -258,10 +287,10 @@ 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) + return false, fmt.Errorf("failed to clone repository: %w", normalizeEmptyReasonError(err)) } head, err := res.repo.Head() if err != nil { @@ -350,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: %v", err) + return plumbing.Hash{}, fmt.Errorf("failed to clone repository: %w", normalizeEmptyReasonError(err)) } head, err := res.repo.Head() if err != nil { @@ -432,6 +461,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) @@ -494,13 +527,16 @@ 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", normalizeEmptyReasonError(err)) + } cloneOpts = git.CloneOptions{ RemoteName: "origin", URL: opts.URL, 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,22 +550,7 @@ 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) { - return nil, ErrRepositoryNotFound - } - - if errors.Is(err, transport.ErrEmptyRemoteRepository) { - return nil, ErrEmptyRemoteRepository - } - - if errors.Is(err, transport.ErrAuthenticationRequired) { - return nil, ErrAuthenticationRequired - } - - if errors.Is(err, transport.ErrAuthorizationFailed) { - return nil, ErrAuthorizationFailed - } - return nil, err + return nil, fmt.Errorf("failed to clone repository: %w", normalizeEmptyReasonError(err)) } err = res.Branch(opts.Branch, &CreateOpt{ @@ -541,6 +562,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 @@ -548,8 +577,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 +790,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/clients/git/git_test.go b/internal/clients/git/git_test.go index 92dbb94..dccb46c 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,29 @@ 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 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/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/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) +} diff --git a/internal/controllers/repo/repo.go b/internal/controllers/repo/repo.go index 86691ea..7ba0f65 100644 --- a/internal/controllers/repo/repo.go +++ b/internal/controllers/repo/repo.go @@ -7,15 +7,11 @@ 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" - "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" @@ -27,22 +23,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 +57,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), ) @@ -73,7 +77,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 +92,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 +111,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") @@ -132,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) } @@ -165,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.IsInGitCommitHistoryContext(ctx, git.ListOptions{ URL: cr.Spec.ToRepo.Url, Auth: e.cfg.ToRepoCreds, Insecure: e.cfg.Insecure, @@ -178,22 +176,28 @@ 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) - 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 { + 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, }, 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{ @@ -205,7 +209,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 +223,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 +243,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.") @@ -258,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 } @@ -267,35 +271,48 @@ 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 } 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)) + return err +} + func (e *external) SyncRepos(ctx context.Context, cr *repov1alpha1.Repo, commitMessage string) error { 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 }) if err != nil { - return fmt.Errorf("cloning toRepo: %w", err) + return e.failSync(ctx, cr, fmt.Errorf("cloning toRepo: %w", err)) } 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{ @@ -308,17 +325,16 @@ 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) - 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()) if err != nil { - return err + return e.failSync(ctx, cr, err) } fromPath := spec.FromRepo.Path @@ -334,9 +350,8 @@ 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.rec.Eventf(cr, corev1.EventTypeWarning, "CannotLoadConfigMap", - "Unable to load configmap with template data: %s", 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()))) } e.log.Debug("Loaded values from config map", @@ -346,25 +361,41 @@ 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) + 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") 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.") } } 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", @@ -372,23 +403,20 @@ 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, FromPath: fromPath, ToPath: toPath, }) - toRepoCommitId := toRepoCommitIdObj.String() 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.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 @@ -398,23 +426,23 @@ 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)) } + toRepoCommitId := toRepoCommitIdObj.String() + 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) + 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.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 @@ -423,7 +451,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..af5fe29 --- /dev/null +++ b/internal/controllers/repo/repo_status_test.go @@ -0,0 +1,35 @@ +package repo + +import ( + "context" + "errors" + "testing" + + 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" +) + +func TestFailSyncMarksResourceUnavailableAndSyncedFalse(t *testing.T) { + cr := &repov1alpha1.Repo{ + ObjectMeta: metav1.ObjectMeta{Name: "sample", Namespace: "test-system"}, + } + cr.Status.SetConditions(commonv1.Available()) + + e := &external{} + + inputErr := errors.New("push failed") + returnedErr := e.failSync(context.Background(), cr, inputErr) + + require.ErrorIs(t, returnedErr, inputErr) + + ready := cr.GetCondition(commonv1.TypeReady) + synced := cr.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 new file mode 100644 index 0000000..39d6e31 --- /dev/null +++ b/internal/controllers/repo/repo_test.go @@ -0,0 +1,1047 @@ +//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 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() + + 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")) + }, + }, + { + 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) + + 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.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") + }, + }, + { + 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") + }, + }, + { + 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) + 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 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, 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 + current.Spec.EnableUpdate = true + err = r.Update(ctx, current) + 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"). + Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { + 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()) + 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 + }). + 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 + }) + + 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..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) + } } }() @@ -316,7 +320,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..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() @@ -115,11 +136,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 +}