diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 4c682d64f..1d85e1f01 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -423,7 +423,7 @@ jobs: requires-secret: false - label: up-provider-podman-rootless-basic - runner: ubuntu-latest + runner: ubuntu-24.04 free-disk-space: false install-kind: false requires-secret: false @@ -433,7 +433,7 @@ jobs: flake-attempts: 2 - label: up-provider-podman-rootless-exec - runner: ubuntu-latest + runner: ubuntu-24.04 free-disk-space: false install-kind: false requires-secret: false @@ -443,7 +443,7 @@ jobs: flake-attempts: 2 - label: up-provider-podman-rootless-lifecycle - runner: ubuntu-latest + runner: ubuntu-24.04 free-disk-space: false install-kind: false requires-secret: false @@ -453,7 +453,7 @@ jobs: flake-attempts: 2 - label: up-provider-podman-rootless-config - runner: ubuntu-latest + runner: ubuntu-24.04 free-disk-space: false install-kind: false requires-secret: false @@ -463,7 +463,7 @@ jobs: flake-attempts: 2 - label: up-provider-podman-rootless-features - runner: ubuntu-latest + runner: ubuntu-24.04 free-disk-space: false install-kind: false requires-secret: false @@ -473,7 +473,7 @@ jobs: flake-attempts: 2 - label: up-provider-podman-rootful-basic - runner: ubuntu-latest + runner: ubuntu-24.04 free-disk-space: false install-kind: false requires-secret: false @@ -483,7 +483,7 @@ jobs: flake-attempts: 2 - label: up-provider-podman-rootful-lifecycle - runner: ubuntu-latest + runner: ubuntu-24.04 free-disk-space: false install-kind: false requires-secret: false @@ -493,7 +493,7 @@ jobs: flake-attempts: 2 - label: up-provider-podman-rootful-lifecycle-2 - runner: ubuntu-latest + runner: ubuntu-24.04 free-disk-space: false install-kind: false requires-secret: false @@ -503,7 +503,7 @@ jobs: flake-attempts: 2 - label: up-provider-podman-rootful-config - runner: ubuntu-latest + runner: ubuntu-24.04 free-disk-space: false install-kind: false requires-secret: false @@ -513,7 +513,7 @@ jobs: flake-attempts: 2 - label: up-provider-podman-rootful-features - runner: ubuntu-latest + runner: ubuntu-24.04 free-disk-space: false install-kind: false requires-secret: false @@ -781,26 +781,17 @@ jobs: command -v newuidmap >/dev/null || sudo apt-get install -y uidmap - - name: Install Podman (Linux rootless) + - name: Install Podman (Linux) if: matrix.install-podman == 'rootless' && runner.os == 'Linux' + shell: bash run: | - podman info - podman run --rm busybox@sha256:fd8d9aa63ba2f0982b5304e1ee8d3b90a210bc1ffb5314d980eb6962f1a9715d echo "podman runtime preflight OK" + ./hack/ci/setup-podman-linux.sh rootless - name: Install Podman (Linux rootful) if: matrix.install-podman == 'rootful' && runner.os == 'Linux' + shell: bash run: | - sudo systemctl daemon-reload - sudo systemctl enable --now podman.socket - if ! timeout 30 bash -c 'until sudo podman --remote --url unix:///run/podman/podman.sock info >/dev/null 2>&1; do sleep 1; done'; then - echo "::error::podman service did not become ready within 30s" - sudo systemctl status podman.socket --no-pager || true - sudo journalctl -u podman.socket --no-pager -n 100 || true - exit 1 - fi - echo "DOCKER_HOST=unix:///run/podman/podman.sock" >> "$GITHUB_ENV" - sudo podman --remote --url unix:///run/podman/podman.sock info - sudo podman run --rm busybox@sha256:fd8d9aa63ba2f0982b5304e1ee8d3b90a210bc1ffb5314d980eb6962f1a9715d echo "podman runtime preflight OK" + ./hack/ci/setup-podman-linux.sh rootful - name: get microsandbox latest version if: matrix.install-microsandbox == true && runner.os == 'Linux' diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index 2e80c698f..20781ea40 100644 --- a/THIRD_PARTY_LICENSES.md +++ b/THIRD_PARTY_LICENSES.md @@ -14,50 +14,75 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | Dependency | Version | License | | ---------- | ------- | ------- | | [al.essio.dev/pkg/shellescape](https://al.essio.dev/pkg/shellescape) | `v1.6.0` | MIT | -| [cel.dev/expr](https://cel.dev/expr) | `v0.25.1` | Apache-2.0 | +| [cel.dev/expr](https://cel.dev/expr) | `v0.25.2` | Apache-2.0 | | [charm.land/bubbles/v2](https://charm.land/bubbles/v2) | `v2.0.0` | MIT | | [charm.land/bubbletea/v2](https://charm.land/bubbletea/v2) | `v2.0.2` | MIT | | [charm.land/huh/v2](https://charm.land/huh/v2) | `v2.0.3` | MIT | -| [charm.land/lipgloss/v2](https://charm.land/lipgloss/v2) | `v2.0.3` | MIT | +| [charm.land/lipgloss/v2](https://charm.land/lipgloss/v2) | `v2.0.5` | MIT | +| [cloud.google.com/go](https://cloud.google.com/go) | `v0.123.0` | Apache-2.0 | +| [cloud.google.com/go/auth](https://cloud.google.com/go/auth) | `v0.22.0` | Apache-2.0 | +| [cloud.google.com/go/auth/oauth2adapt](https://cloud.google.com/go/auth/oauth2adapt) | `v0.2.8` | Apache-2.0 | | [cloud.google.com/go/compute/metadata](https://cloud.google.com/go/compute/metadata) | `v0.9.0` | Apache-2.0 | -| [code.gitea.io/sdk/gitea](https://code.gitea.io/sdk/gitea) | `v0.22.1` | MIT | -| [filippo.io/edwards25519](https://filippo.io/edwards25519) | `v1.1.0` | BSD-3-Clause | -| [github.com/42wim/httpsig](https://github.com/42wim/httpsig) | `v1.2.3` | BSD-3-Clause | +| [cloud.google.com/go/iam](https://cloud.google.com/go/iam) | `v1.12.0` | Apache-2.0 | +| [cloud.google.com/go/kms](https://cloud.google.com/go/kms) | `v1.32.0` | Apache-2.0 | +| [cloud.google.com/go/longrunning](https://cloud.google.com/go/longrunning) | `v1.2.0` | Apache-2.0 | +| [cloud.google.com/go/monitoring](https://cloud.google.com/go/monitoring) | `v1.30.0` | Apache-2.0 | +| [cloud.google.com/go/storage](https://cloud.google.com/go/storage) | `v1.63.1` | Apache-2.0 | +| [code.gitea.io/sdk/gitea](https://code.gitea.io/sdk/gitea) | `v0.25.1` | MIT | +| [filippo.io/age](https://filippo.io/age) | `v1.3.1` | BSD-3-Clause | +| [filippo.io/edwards25519](https://filippo.io/edwards25519) | `v1.2.0` | BSD-3-Clause | +| [filippo.io/hpke](https://filippo.io/hpke) | `v0.4.0` | BSD-3-Clause | +| [github.com/42wim/httpsig](https://github.com/42wim/httpsig) | `v1.2.4` | BSD-3-Clause | | [github.com/AlecAivazis/survey/v2](https://github.com/AlecAivazis/survey) | `v2.3.7` | MIT | -| [github.com/Azure/azure-sdk-for-go/sdk/azcore](https://github.com/Azure/azure-sdk-for-go) | `v1.21.0` | MIT | -| [github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://github.com/Azure/azure-sdk-for-go) | `v1.13.1` | MIT | -| [github.com/Azure/azure-sdk-for-go/sdk/internal](https://github.com/Azure/azure-sdk-for-go) | `v1.11.2` | MIT | +| [github.com/Azure/azure-sdk-for-go/sdk/azcore](https://github.com/Azure/azure-sdk-for-go) | `v1.22.0` | MIT | +| [github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://github.com/Azure/azure-sdk-for-go) | `v1.14.0` | MIT | +| [github.com/Azure/azure-sdk-for-go/sdk/internal](https://github.com/Azure/azure-sdk-for-go) | `v1.12.0` | MIT | +| [github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys](https://github.com/Azure/azure-sdk-for-go) | `v1.5.0` | MIT | +| [github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal](https://github.com/Azure/azure-sdk-for-go) | `v1.2.0` | MIT | | [github.com/Azure/go-ansiterm](https://github.com/Azure/go-ansiterm) | `v0.0.0-20250102033503-faa5f7b0171c` | MIT | -| [github.com/AzureAD/microsoft-authentication-library-for-go](https://github.com/AzureAD/microsoft-authentication-library-for-go) | `v1.6.0` | MIT | -| [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) | `v1.5.0` | MIT | -| [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) | `v3.4.0` | MIT | -| [github.com/Microsoft/go-winio](https://github.com/Microsoft/go-winio) | `v0.6.2` | MIT | +| [github.com/AzureAD/microsoft-authentication-library-for-go](https://github.com/AzureAD/microsoft-authentication-library-for-go) | `v1.7.2` | MIT | +| [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) | `v1.6.0` | MIT | +| [github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go) | `v1.34.0` | Apache-2.0 | +| [github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go) | `v0.58.0` | Apache-2.0 | +| [github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go) | `v0.58.0` | Apache-2.0 | +| [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) | `v3.5.0` | MIT | +| [github.com/Microsoft/go-winio](https://github.com/Microsoft/go-winio) | `v0.6.3-0.20260107145400-75610162e7da` | MIT | | [github.com/NYTimes/gziphandler](https://github.com/NYTimes/gziphandler) | `v1.1.1` | Apache-2.0 | +| [github.com/ProtonMail/go-crypto](https://github.com/ProtonMail/go-crypto) | `v1.4.1` | BSD-3-Clause | | [github.com/agext/levenshtein](https://github.com/agext/levenshtein) | `v1.2.3` | Apache-2.0 | | [github.com/alexbrainman/sspi](https://github.com/alexbrainman/sspi) | `v0.0.0-20231016080023-1a75b4708caa` | BSD-3-Clause | +| [github.com/andybalholm/brotli](https://github.com/andybalholm/brotli) | `v1.1.1` | MIT | | [github.com/antlr4-go/antlr/v4](https://github.com/antlr4-go/antlr) | `v4.13.1` | BSD-3-Clause | | [github.com/atotto/clipboard](https://github.com/atotto/clipboard) | `v0.1.4` | BSD-3-Clause | -| [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `v1.41.4` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) | `v1.32.12` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) | `v1.19.12` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://github.com/aws/aws-sdk-go-v2) | `v1.18.20` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/internal/configsources](https://github.com/aws/aws-sdk-go-v2) | `v1.4.20` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://github.com/aws/aws-sdk-go-v2) | `v2.7.20` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/internal/ini](https://github.com/aws/aws-sdk-go-v2) | `v1.8.6` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/ecr](https://github.com/aws/aws-sdk-go-v2) | `v1.55.3` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/ecrpublic](https://github.com/aws/aws-sdk-go-v2) | `v1.38.10` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding](https://github.com/aws/aws-sdk-go-v2) | `v1.13.7` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://github.com/aws/aws-sdk-go-v2) | `v1.13.20` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/signin](https://github.com/aws/aws-sdk-go-v2) | `v1.0.8` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/sso](https://github.com/aws/aws-sdk-go-v2) | `v1.30.13` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://github.com/aws/aws-sdk-go-v2) | `v1.35.17` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/sts](https://github.com/aws/aws-sdk-go-v2) | `v1.41.9` | Apache-2.0 | -| [github.com/aws/smithy-go](https://github.com/aws/smithy-go) | `v1.24.2` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `v1.43.0` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream](https://github.com/aws/aws-sdk-go-v2) | `v1.7.14` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) | `v1.32.31` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) | `v1.19.30` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://github.com/aws/aws-sdk-go-v2) | `v1.18.31` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) | `v1.22.34` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/internal/configsources](https://github.com/aws/aws-sdk-go-v2) | `v1.4.31` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://github.com/aws/aws-sdk-go-v2) | `v2.7.31` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/internal/v4a](https://github.com/aws/aws-sdk-go-v2) | `v1.4.32` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/ecr](https://github.com/aws/aws-sdk-go-v2) | `v1.56.0` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/ecrpublic](https://github.com/aws/aws-sdk-go-v2) | `v1.38.11` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding](https://github.com/aws/aws-sdk-go-v2) | `v1.13.13` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/internal/checksum](https://github.com/aws/aws-sdk-go-v2) | `v1.9.24` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://github.com/aws/aws-sdk-go-v2) | `v1.13.31` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/internal/s3shared](https://github.com/aws/aws-sdk-go-v2) | `v1.19.32` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/kms](https://github.com/aws/aws-sdk-go-v2) | `v1.54.1` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) | `v1.106.0` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/signin](https://github.com/aws/aws-sdk-go-v2) | `v1.5.0` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/sso](https://github.com/aws/aws-sdk-go-v2) | `v1.33.0` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://github.com/aws/aws-sdk-go-v2) | `v1.38.0` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/sts](https://github.com/aws/aws-sdk-go-v2) | `v1.45.0` | Apache-2.0 | +| [github.com/aws/smithy-go](https://github.com/aws/smithy-go) | `v1.27.4` | Apache-2.0 | | [github.com/awslabs/amazon-ecr-credential-helper/ecr-login](https://github.com/awslabs/amazon-ecr-credential-helper) | `v0.12.0` | Apache-2.0 | | [github.com/beorn7/perks](https://github.com/beorn7/perks) | `v1.0.1` | MIT | +| [github.com/blang/semver](https://github.com/blang/semver) | `v3.5.1+incompatible` | MIT | | [github.com/blang/semver/v4](https://github.com/blang/semver) | `v4.0.0` | MIT | | [github.com/bmatcuk/doublestar/v4](https://github.com/bmatcuk/doublestar) | `v4.10.0` | MIT | | [github.com/catppuccin/go](https://github.com/catppuccin/go) | `v0.3.0` | MIT | +| [github.com/cenkalti/backoff/v4](https://github.com/cenkalti/backoff) | `v4.3.0` | MIT | | [github.com/cenkalti/backoff/v5](https://github.com/cenkalti/backoff) | `v5.0.3` | MIT | | [github.com/cespare/xxhash/v2](https://github.com/cespare/xxhash) | `v2.3.0` | MIT | | [github.com/charmbracelet/colorprofile](https://github.com/charmbracelet/colorprofile) | `v0.4.3` | MIT | @@ -70,111 +95,137 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/charmbracelet/x/windows](https://github.com/charmbracelet/x) | `v0.2.2` | MIT | | [github.com/clipperhouse/displaywidth](https://github.com/clipperhouse/displaywidth) | `v0.11.0` | MIT | | [github.com/clipperhouse/uax29/v2](https://github.com/clipperhouse/uax29) | `v2.7.0` | MIT | -| [github.com/coder/websocket](https://github.com/coder/websocket) | `v1.8.12` | ISC | -| [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) | `v2.10.2` | Apache-2.0 | +| [github.com/cloudflare/circl](https://github.com/cloudflare/circl) | `v1.6.4` | BSD-3-Clause | +| [github.com/cncf/xds/go](https://github.com/cncf/xds) | `v0.0.0-20260202195803-dba9d589def2` | Apache-2.0 | +| [github.com/coder/websocket](https://github.com/coder/websocket) | `v1.8.14` | ISC | +| [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) | `v2.14.0` | Apache-2.0 | | [github.com/containerd/console](https://github.com/containerd/console) | `v1.0.5` | Apache-2.0 | -| [github.com/containerd/containerd/api](https://github.com/containerd/containerd) | `v1.10.0` | Apache-2.0 | -| [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) | `v2.2.2` | Apache-2.0 | +| [github.com/containerd/containerd/api](https://github.com/containerd/containerd) | `v1.11.1` | Apache-2.0 | +| [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) | `v2.3.3` | Apache-2.0 | | [github.com/containerd/continuity](https://github.com/containerd/continuity) | `v0.5.0` | Apache-2.0 | | [github.com/containerd/errdefs](https://github.com/containerd/errdefs) | `v1.0.0` | Apache-2.0 | | [github.com/containerd/errdefs/pkg](https://github.com/containerd/errdefs) | `v0.3.0` | Apache-2.0 | | [github.com/containerd/log](https://github.com/containerd/log) | `v0.1.0` | Apache-2.0 | -| [github.com/containerd/platforms](https://github.com/containerd/platforms) | `v1.0.0-rc.2` | Apache-2.0 | -| [github.com/containerd/stargz-snapshotter/estargz](https://github.com/containerd/stargz-snapshotter) | `v0.18.2` | Apache-2.0 | -| [github.com/containerd/ttrpc](https://github.com/containerd/ttrpc) | `v1.2.8` | Apache-2.0 | -| [github.com/containerd/typeurl/v2](https://github.com/containerd/typeurl) | `v2.2.3` | Apache-2.0 | +| [github.com/containerd/platforms](https://github.com/containerd/platforms) | `v1.0.0-rc.4` | Apache-2.0 | +| [github.com/containerd/ttrpc](https://github.com/containerd/ttrpc) | `v1.2.9` | Apache-2.0 | +| [github.com/containerd/typeurl/v2](https://github.com/containerd/typeurl) | `v2.3.0` | Apache-2.0 | | [github.com/containers/image/v5](https://github.com/containers/image) | `v5.36.2` | Apache-2.0 | | [github.com/containers/storage](https://github.com/containers/storage) | `v1.59.1` | Apache-2.0 | | [github.com/coreos/go-semver](https://github.com/coreos/go-semver) | `v0.3.1` | Apache-2.0 | | [github.com/coreos/go-systemd/v22](https://github.com/coreos/go-systemd) | `v22.7.0` | Apache-2.0 | -| [github.com/creachadair/msync](https://github.com/creachadair/msync) | `v0.7.1` | BSD-3-Clause | +| [github.com/cpuguy83/go-md2man/v2](https://github.com/cpuguy83/go-md2man) | `v2.0.7` | MIT | +| [github.com/creachadair/msync](https://github.com/creachadair/msync) | `v0.8.1` | BSD-3-Clause | | [github.com/creack/pty](https://github.com/creack/pty) | `v1.1.24` | MIT | -| [github.com/creativeprojects/go-selfupdate](https://github.com/creativeprojects/go-selfupdate) | `v1.5.2` | MIT | +| [github.com/creativeprojects/go-selfupdate](https://github.com/creativeprojects/go-selfupdate) | `v1.6.0` | MIT | +| [github.com/danieljoos/wincred](https://github.com/danieljoos/wincred) | `v1.2.3` | MIT | | [github.com/davecgh/go-spew](https://github.com/davecgh/go-spew) | `v1.1.2-0.20180830191138-d8f796af33cc` | ISC | | [github.com/davidmz/go-pageant](https://github.com/davidmz/go-pageant) | `v1.0.2` | MIT | | [github.com/dblohm7/wingoes](https://github.com/dblohm7/wingoes) | `v0.0.0-20240119213807-a09d6be7affa` | BSD-3-Clause | | [github.com/devsy-org/admin-apis](https://github.com/devsy-org/admin-apis) | `v1.2.0` | MPL-2.0 | | [github.com/devsy-org/agentapi](https://github.com/devsy-org/agentapi) | `v1.0.1` | MPL-2.0 | | [github.com/devsy-org/api](https://github.com/devsy-org/api) | `v1.1.0` | MPL-2.0 | -| [github.com/devsy-org/apiserver](https://github.com/devsy-org/apiserver) | `v1.5.0` | Apache-2.0 | -| [github.com/devsy-org/ssh](https://github.com/devsy-org/ssh) | `v1.2.0` | BSD-3-Clause | +| [github.com/devsy-org/apiserver](https://github.com/devsy-org/apiserver) | `v1.5.3` | Apache-2.0 | +| [github.com/devsy-org/ssh](https://github.com/devsy-org/ssh) | `v1.2.5` | BSD-3-Clause | | [github.com/distribution/reference](https://github.com/distribution/reference) | `v0.6.0` | Apache-2.0 | -| [github.com/docker/cli](https://github.com/docker/cli) | `v29.6.0+incompatible` | Apache-2.0 | +| [github.com/docker/cli](https://github.com/docker/cli) | `v29.7.1+incompatible` | Apache-2.0 | | [github.com/docker/docker](https://github.com/docker/docker) | `v28.5.2+incompatible` | Apache-2.0 | -| [github.com/docker/docker-credential-helpers](https://github.com/docker/docker-credential-helpers) | `v0.9.5` | MIT | -| [github.com/docker/go-connections](https://github.com/docker/go-connections) | `v0.6.0` | Apache-2.0 | +| [github.com/docker/docker-credential-helpers](https://github.com/docker/docker-credential-helpers) | `v0.9.8` | MIT | +| [github.com/docker/go-connections](https://github.com/docker/go-connections) | `v0.8.1` | Apache-2.0 | | [github.com/docker/go-units](https://github.com/docker/go-units) | `v0.5.0` | Apache-2.0 | | [github.com/dustin/go-humanize](https://github.com/dustin/go-humanize) | `v1.0.1` | MIT | -| [github.com/ebitengine/purego](https://github.com/ebitengine/purego) | `v0.10.0` | Apache-2.0 | +| [github.com/ebitengine/purego](https://github.com/ebitengine/purego) | `v0.10.2` | Apache-2.0 | | [github.com/emicklei/go-restful/v3](https://github.com/emicklei/go-restful) | `v3.13.0` | MIT | +| [github.com/envoyproxy/go-control-plane/envoy](https://github.com/envoyproxy/go-control-plane) | `v1.37.0` | Apache-2.0 | +| [github.com/envoyproxy/protoc-gen-validate](https://github.com/envoyproxy/protoc-gen-validate) | `v1.3.3` | Apache-2.0 | | [github.com/evanphx/json-patch/v5](https://github.com/evanphx/json-patch) | `v5.9.11` | BSD-3-Clause | -| [github.com/felixge/httpsnoop](https://github.com/felixge/httpsnoop) | `v1.0.4` | MIT | -| [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify) | `v1.9.0` | BSD-3-Clause | +| [github.com/fatih/color](https://github.com/fatih/color) | `v1.19.0` | MIT | +| [github.com/felixge/httpsnoop](https://github.com/felixge/httpsnoop) | `v1.1.0` | MIT | +| [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify) | `v1.10.1` | BSD-3-Clause | | [github.com/fxamacker/cbor/v2](https://github.com/fxamacker/cbor) | `v2.9.0` | MIT | -| [github.com/gaissmai/bart](https://github.com/gaissmai/bart) | `v0.18.0` | MIT | +| [github.com/gaissmai/bart](https://github.com/gaissmai/bart) | `v0.26.1` | MIT | +| [github.com/getsops/gopgagent](https://github.com/getsops/gopgagent) | `v0.0.0-20241224165529-7044f28e491e` | Apache-2.0 | +| [github.com/getsops/sops/v3](https://github.com/getsops/sops) | `v3.13.3` | MPL-2.0 | | [github.com/go-fed/httpsig](https://github.com/go-fed/httpsig) | `v1.1.0` | BSD-3-Clause | -| [github.com/go-json-experiment/json](https://github.com/go-json-experiment/json) | `v0.0.0-20250813024750-ebf49471dced` | BSD-3-Clause | -| [github.com/go-logr/logr](https://github.com/go-logr/logr) | `v1.4.3` | Apache-2.0 | +| [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) | `v4.1.4` | Apache-2.0 | +| [github.com/go-json-experiment/json](https://github.com/go-json-experiment/json) | `v0.0.0-20260623181947-01eb4420fa68` | BSD-3-Clause | +| [github.com/go-logr/logr](https://github.com/go-logr/logr) | `v1.4.4` | Apache-2.0 | | [github.com/go-logr/stdr](https://github.com/go-logr/stdr) | `v1.2.2` | Apache-2.0 | | [github.com/go-logr/zapr](https://github.com/go-logr/zapr) | `v1.3.0` | Apache-2.0 | | [github.com/go-ole/go-ole](https://github.com/go-ole/go-ole) | `v1.3.0` | MIT | -| [github.com/go-openapi/jsonpointer](https://github.com/go-openapi/jsonpointer) | `v0.22.5` | Apache-2.0 | -| [github.com/go-openapi/jsonreference](https://github.com/go-openapi/jsonreference) | `v0.21.5` | Apache-2.0 | -| [github.com/go-openapi/swag](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | -| [github.com/go-openapi/swag/cmdutils](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | -| [github.com/go-openapi/swag/conv](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | -| [github.com/go-openapi/swag/fileutils](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | -| [github.com/go-openapi/swag/jsonname](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | -| [github.com/go-openapi/swag/jsonutils](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | -| [github.com/go-openapi/swag/loading](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | -| [github.com/go-openapi/swag/mangling](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | -| [github.com/go-openapi/swag/netutils](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | -| [github.com/go-openapi/swag/stringutils](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | -| [github.com/go-openapi/swag/typeutils](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | -| [github.com/go-openapi/swag/yamlutils](https://github.com/go-openapi/swag) | `v0.25.5` | Apache-2.0 | +| [github.com/go-openapi/jsonpointer](https://github.com/go-openapi/jsonpointer) | `v0.23.1` | Apache-2.0 | +| [github.com/go-openapi/jsonreference](https://github.com/go-openapi/jsonreference) | `v0.21.6` | Apache-2.0 | +| [github.com/go-openapi/swag](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | +| [github.com/go-openapi/swag/cmdutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | +| [github.com/go-openapi/swag/conv](https://github.com/go-openapi/swag) | `v0.27.0` | Apache-2.0 | +| [github.com/go-openapi/swag/fileutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | +| [github.com/go-openapi/swag/jsonname](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | +| [github.com/go-openapi/swag/jsonutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | +| [github.com/go-openapi/swag/loading](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | +| [github.com/go-openapi/swag/mangling](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | +| [github.com/go-openapi/swag/netutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | +| [github.com/go-openapi/swag/stringutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | +| [github.com/go-openapi/swag/typeutils](https://github.com/go-openapi/swag) | `v0.27.0` | Apache-2.0 | +| [github.com/go-openapi/swag/yamlutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | | [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) | `v2.5.0` | MIT | -| [github.com/goccy/go-json](https://github.com/goccy/go-json) | `v0.10.5` | MIT | -| [github.com/godbus/dbus/v5](https://github.com/godbus/dbus) | `v5.2.0` | BSD-2-Clause | +| [github.com/goccy/go-json](https://github.com/goccy/go-json) | `v0.10.6` | MIT | +| [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) | `v1.19.2` | MIT | +| [github.com/godbus/dbus/v5](https://github.com/godbus/dbus) | `v5.2.2` | BSD-2-Clause | | [github.com/gofrs/flock](https://github.com/gofrs/flock) | `v0.13.0` | BSD-3-Clause | | [github.com/gogo/protobuf](https://github.com/gogo/protobuf) | `v1.3.2` | BSD-3-Clause | | [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) | `v5.3.1` | MIT | | [github.com/golang/groupcache](https://github.com/golang/groupcache) | `v0.0.0-20241129210726-2c02b8208cf8` | Apache-2.0 | | [github.com/golang/protobuf](https://github.com/golang/protobuf) | `v1.5.4` | BSD-3-Clause | | [github.com/google/btree](https://github.com/google/btree) | `v1.1.3` | Apache-2.0 | -| [github.com/google/cel-go](https://github.com/google/cel-go) | `v0.26.1` | Apache-2.0 | +| [github.com/google/cel-go](https://github.com/google/cel-go) | `v0.27.0` | Apache-2.0 | | [github.com/google/gnostic-models](https://github.com/google/gnostic-models) | `v0.7.1` | Apache-2.0 | | [github.com/google/go-cmp](https://github.com/google/go-cmp) | `v0.7.0` | BSD-3-Clause | -| [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) | `v0.21.5` | Apache-2.0 | -| [github.com/google/go-containerregistry/pkg/authn/kubernetes](https://github.com/google/go-containerregistry) | `v0.0.0-20260624172245-e8b2b9e51df6` | Apache-2.0 | -| [github.com/google/go-github/v74](https://github.com/google/go-github) | `v74.0.0` | BSD-3-Clause | -| [github.com/google/go-querystring](https://github.com/google/go-querystring) | `v1.1.0` | BSD-3-Clause | +| [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) | `v0.21.8` | Apache-2.0 | +| [github.com/google/go-containerregistry/pkg/authn/kubernetes](https://github.com/google/go-containerregistry) | `v0.0.0-20260731170845-2ea098f4b134` | Apache-2.0 | +| [github.com/google/go-github/v86](https://github.com/google/go-github) | `v86.0.0` | BSD-3-Clause | +| [github.com/google/go-github/v90](https://github.com/google/go-github) | `v90.0.0` | BSD-3-Clause | +| [github.com/google/go-querystring](https://github.com/google/go-querystring) | `v1.2.0` | BSD-3-Clause | | [github.com/google/jsonschema-go](https://github.com/google/jsonschema-go) | `v0.4.3` | MIT | +| [github.com/google/s2a-go](https://github.com/google/s2a-go) | `v0.1.9` | Apache-2.0 | | [github.com/google/shlex](https://github.com/google/shlex) | `v0.0.0-20191202100458-e7afc7fbc510` | Apache-2.0 | | [github.com/google/uuid](https://github.com/google/uuid) | `v1.6.0` | BSD-3-Clause | +| [github.com/googleapis/enterprise-certificate-proxy](https://github.com/googleapis/enterprise-certificate-proxy) | `v0.3.18` | Apache-2.0 | +| [github.com/googleapis/gax-go/v2](https://github.com/googleapis/gax-go) | `v2.23.0` | BSD-3-Clause | | [github.com/gorilla/handlers](https://github.com/gorilla/handlers) | `v1.5.2` | BSD-3-Clause | | [github.com/gorilla/websocket](https://github.com/gorilla/websocket) | `v1.5.4-0.20250319132907-e064f32e3674` | BSD-2-Clause | +| [github.com/goware/prefixer](https://github.com/goware/prefixer) | `v0.0.0-20160118172347-395022866408` | MIT | | [github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus](https://github.com/grpc-ecosystem/go-grpc-middleware) | `v1.1.0` | Apache-2.0 | | [github.com/grpc-ecosystem/go-grpc-middleware/v2](https://github.com/grpc-ecosystem/go-grpc-middleware) | `v2.3.3` | Apache-2.0 | -| [github.com/grpc-ecosystem/grpc-gateway/v2](https://github.com/grpc-ecosystem/grpc-gateway) | `v2.27.7` | BSD-3-Clause | +| [github.com/grpc-ecosystem/grpc-gateway/v2](https://github.com/grpc-ecosystem/grpc-gateway) | `v2.29.0` | BSD-3-Clause | +| [github.com/hashicorp/errwrap](https://github.com/hashicorp/errwrap) | `v1.1.0` | MPL-2.0 | | [github.com/hashicorp/go-cleanhttp](https://github.com/hashicorp/go-cleanhttp) | `v0.5.2` | MPL-2.0 | +| [github.com/hashicorp/go-multierror](https://github.com/hashicorp/go-multierror) | `v1.1.1` | MPL-2.0 | | [github.com/hashicorp/go-retryablehttp](https://github.com/hashicorp/go-retryablehttp) | `v0.7.8` | MPL-2.0 | -| [github.com/hashicorp/go-version](https://github.com/hashicorp/go-version) | `v1.8.0` | MPL-2.0 | +| [github.com/hashicorp/go-rootcerts](https://github.com/hashicorp/go-rootcerts) | `v1.0.2` | MPL-2.0 | +| [github.com/hashicorp/go-secure-stdlib/parseutil](https://github.com/hashicorp/go-secure-stdlib) | `v0.2.0` | MPL-2.0 | +| [github.com/hashicorp/go-secure-stdlib/strutil](https://github.com/hashicorp/go-secure-stdlib) | `v0.1.2` | MPL-2.0 | +| [github.com/hashicorp/go-sockaddr](https://github.com/hashicorp/go-sockaddr) | `v1.0.7` | MPL-2.0 | +| [github.com/hashicorp/go-version](https://github.com/hashicorp/go-version) | `v1.9.0` | MPL-2.0 | | [github.com/hashicorp/golang-lru/v2](https://github.com/hashicorp/golang-lru) | `v2.0.7` | MPL-2.0 | +| [github.com/hashicorp/hcl](https://github.com/hashicorp/hcl) | `v1.0.1-vault-7` | MPL-2.0 | +| [github.com/hashicorp/vault/api](https://github.com/hashicorp/vault) | `v1.23.0` | MPL-2.0 | | [github.com/hdevalence/ed25519consensus](https://github.com/hdevalence/ed25519consensus) | `v0.2.0` | BSD-3-Clause | -| [github.com/in-toto/attestation](https://github.com/in-toto/attestation) | `v1.1.2` | Apache-2.0 | -| [github.com/in-toto/in-toto-golang](https://github.com/in-toto/in-toto-golang) | `v0.10.0` | Apache-2.0 | +| [github.com/huaweicloud/huaweicloud-sdk-go-v3](https://github.com/huaweicloud/huaweicloud-sdk-go-v3) | `v0.1.207` | BSD-2-Clause | +| [github.com/huin/goupnp](https://github.com/huin/goupnp) | `v1.3.0` | BSD-2-Clause | +| [github.com/in-toto/attestation](https://github.com/in-toto/attestation) | `v1.2.0` | Apache-2.0 | +| [github.com/in-toto/in-toto-golang](https://github.com/in-toto/in-toto-golang) | `v0.11.0` | Apache-2.0 | | [github.com/inconshreveable/mousetrap](https://github.com/inconshreveable/mousetrap) | `v1.1.0` | Apache-2.0 | | [github.com/joho/godotenv](https://github.com/joho/godotenv) | `v1.5.1` | MIT | -| [github.com/jsimonetti/rtnetlink](https://github.com/jsimonetti/rtnetlink) | `v1.4.0` | MIT | -| [github.com/json-iterator/go](https://github.com/json-iterator/go) | `v1.1.12` | MIT | +| [github.com/jsimonetti/rtnetlink](https://github.com/jsimonetti/rtnetlink) | `v1.4.1` | MIT | +| [github.com/json-iterator/go](https://github.com/json-iterator/go) | `v1.1.13-0.20220915233716-71ac16282d12` | MIT | | [github.com/kballard/go-shellquote](https://github.com/kballard/go-shellquote) | `v0.0.0-20180428030007-95032a82bc51` | MIT | -| [github.com/klauspost/compress](https://github.com/klauspost/compress) | `v1.18.5` | Apache-2.0 | +| [github.com/klauspost/compress](https://github.com/klauspost/compress) | `v1.19.1` | Apache-2.0 | | [github.com/kr/fs](https://github.com/kr/fs) | `v0.1.0` | BSD-3-Clause | | [github.com/kylelemons/godebug](https://github.com/kylelemons/godebug) | `v1.1.0` | Apache-2.0 | +| [github.com/lib/pq](https://github.com/lib/pq) | `v1.12.3` | MIT | | [github.com/liggitt/tabwriter](https://github.com/liggitt/tabwriter) | `v0.0.0-20181228230101-89fcab3d43de` | BSD-3-Clause | | [github.com/lucasb-eyer/go-colorful](https://github.com/lucasb-eyer/go-colorful) | `v1.4.0` | MIT | -| [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable) | `v0.1.14` | MIT | -| [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) | `v0.0.20` | MIT | +| [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable) | `v0.1.15` | MIT | +| [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) | `v0.0.23` | MIT | | [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) | `v0.0.23` | MIT | | [github.com/mattn/go-shellwords](https://github.com/mattn/go-shellwords) | `v1.0.12` | MIT | | [github.com/mdlayher/netlink](https://github.com/mdlayher/netlink) | `v1.7.3-0.20250113171957-fbb4dce95f42` | MIT | @@ -184,7 +235,8 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/mitchellh/go-ps](https://github.com/mitchellh/go-ps) | `v1.0.0` | MIT | | [github.com/mitchellh/go-wordwrap](https://github.com/mitchellh/go-wordwrap) | `v1.0.1` | MIT | | [github.com/mitchellh/hashstructure/v2](https://github.com/mitchellh/hashstructure) | `v2.0.2` | MIT | -| [github.com/moby/buildkit](https://github.com/moby/buildkit) | `v0.29.0` | Apache-2.0 | +| [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) | `v1.5.1-0.20231216201459-8508981c8b6c` | MIT | +| [github.com/moby/buildkit](https://github.com/moby/buildkit) | `v0.32.2` | Apache-2.0 | | [github.com/moby/docker-image-spec](https://github.com/moby/docker-image-spec) | `v1.3.1` | Apache-2.0 | | [github.com/moby/locker](https://github.com/moby/locker) | `v1.0.1` | Apache-2.0 | | [github.com/moby/patternmatcher](https://github.com/moby/patternmatcher) | `v0.6.1` | Apache-2.0 | @@ -192,133 +244,142 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/moby/sys/capability](https://github.com/moby/sys) | `v0.4.0` | BSD-2-Clause | | [github.com/moby/sys/mountinfo](https://github.com/moby/sys) | `v0.7.2` | Apache-2.0 | | [github.com/moby/sys/signal](https://github.com/moby/sys) | `v0.7.1` | Apache-2.0 | -| [github.com/moby/sys/user](https://github.com/moby/sys) | `v0.4.0` | Apache-2.0 | +| [github.com/moby/sys/user](https://github.com/moby/sys) | `v0.4.1` | Apache-2.0 | | [github.com/moby/term](https://github.com/moby/term) | `v0.5.2` | Apache-2.0 | -| [github.com/modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) | `v1.6.1` | Apache-2.0 | +| [github.com/modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) | `v1.7.0` | Apache-2.0 | | [github.com/modern-go/concurrent](https://github.com/modern-go/concurrent) | `v0.0.0-20180306012644-bacd9c7ef1dd` | Apache-2.0 | | [github.com/modern-go/reflect2](https://github.com/modern-go/reflect2) | `v1.0.3-0.20250322232337-35a7c28c31ee` | Apache-2.0 | | [github.com/morikuni/aec](https://github.com/morikuni/aec) | `v1.1.0` | MIT | | [github.com/muesli/cancelreader](https://github.com/muesli/cancelreader) | `v0.2.2` | MIT | | [github.com/munnerz/goautoneg](https://github.com/munnerz/goautoneg) | `v0.0.0-20191010083416-a7dc8b61c822` | BSD-3-Clause | -| [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) | `v2.28.1` | MIT | -| [github.com/onsi/gomega](https://github.com/onsi/gomega) | `v1.39.1` | MIT | +| [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) | `v2.32.0` | MIT | +| [github.com/onsi/gomega](https://github.com/onsi/gomega) | `v1.42.1` | MIT | | [github.com/opencontainers/go-digest](https://github.com/opencontainers/go-digest) | `v1.0.0` | Apache-2.0 | | [github.com/opencontainers/image-spec](https://github.com/opencontainers/image-spec) | `v1.1.1` | Apache-2.0 | | [github.com/opencontainers/runtime-spec](https://github.com/opencontainers/runtime-spec) | `v1.3.0` | Apache-2.0 | | [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) | `v0.8.1` | Apache-2.0 | | [github.com/pkg/browser](https://github.com/pkg/browser) | `v0.0.0-20240102092130-5ac0b6a4141c` | BSD-2-Clause | | [github.com/pkg/errors](https://github.com/pkg/errors) | `v0.9.1` | BSD-2-Clause | -| [github.com/pkg/sftp](https://github.com/pkg/sftp) | `v1.13.10` | BSD-2-Clause | +| [github.com/pkg/sftp](https://github.com/pkg/sftp) | `v1.13.11` | BSD-2-Clause | | [github.com/planetscale/vtprotobuf](https://github.com/planetscale/vtprotobuf) | `v0.6.1-0.20240319094008-0393e58bdf10` | BSD-3-Clause | | [github.com/pmezard/go-difflib](https://github.com/pmezard/go-difflib) | `v1.0.1-0.20181226105442-5d4384ee4fb2` | BSD-3-Clause | -| [github.com/posthog/posthog-go](https://github.com/posthog/posthog-go) | `v1.12.5` | MIT | -| [github.com/prometheus-community/pro-bing](https://github.com/prometheus-community/pro-bing) | `v0.4.0` | MIT | +| [github.com/posthog/posthog-go](https://github.com/posthog/posthog-go) | `v1.22.0` | MIT | | [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) | `v1.23.2` | Apache-2.0 | | [github.com/prometheus/client_model](https://github.com/prometheus/client_model) | `v0.6.2` | Apache-2.0 | -| [github.com/prometheus/common](https://github.com/prometheus/common) | `v0.67.5` | Apache-2.0 | -| [github.com/prometheus/procfs](https://github.com/prometheus/procfs) | `v0.19.2` | Apache-2.0 | +| [github.com/prometheus/common](https://github.com/prometheus/common) | `v0.69.0` | Apache-2.0 | +| [github.com/prometheus/procfs](https://github.com/prometheus/procfs) | `v0.20.1` | Apache-2.0 | | [github.com/rivo/uniseg](https://github.com/rivo/uniseg) | `v0.4.7` | MIT | +| [github.com/russross/blackfriday/v2](https://github.com/russross/blackfriday) | `v2.1.0` | BSD-2-Clause | +| [github.com/ryanuber/go-glob](https://github.com/ryanuber/go-glob) | `v1.0.0` | MIT | | [github.com/safchain/ethtool](https://github.com/safchain/ethtool) | `v0.3.0` | Apache-2.0 | | [github.com/santhosh-tekuri/jsonschema/v6](https://github.com/santhosh-tekuri/jsonschema) | `v6.0.2` | Apache-2.0 | -| [github.com/secure-systems-lab/go-securesystemslib](https://github.com/secure-systems-lab/go-securesystemslib) | `v0.10.0` | MIT | -| [github.com/segmentio/asm](https://github.com/segmentio/asm) | `v1.1.3` | MIT | +| [github.com/secure-systems-lab/go-securesystemslib](https://github.com/secure-systems-lab/go-securesystemslib) | `v0.11.0` | MIT | +| [github.com/segmentio/asm](https://github.com/segmentio/asm) | `v1.2.1` | MIT-0 | | [github.com/segmentio/encoding](https://github.com/segmentio/encoding) | `v0.5.4` | MIT | | [github.com/shibumi/go-pathspec](https://github.com/shibumi/go-pathspec) | `v1.3.0` | Apache-2.0 | -| [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) | `v4.26.4` | BSD-3-Clause | +| [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) | `v4.26.7` | BSD-3-Clause | | [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) | `v1.9.4` | MIT | | [github.com/skratchdot/open-golang](https://github.com/skratchdot/open-golang) | `v0.0.0-20200116055534-eef842397966` | MIT | | [github.com/spf13/cobra](https://github.com/spf13/cobra) | `v1.10.2` | Apache-2.0 | | [github.com/spf13/pflag](https://github.com/spf13/pflag) | `v1.0.10` | BSD-3-Clause | -| [github.com/stoewer/go-strcase](https://github.com/stoewer/go-strcase) | `v1.3.1` | MIT | -| [github.com/stretchr/testify](https://github.com/stretchr/testify) | `v1.11.1` | MIT | -| [github.com/tailscale/certstore](https://github.com/tailscale/certstore) | `v0.1.1-0.20231202035212-d3fa0460f47e` | MIT | +| [github.com/spiffe/go-spiffe/v2](https://github.com/spiffe/go-spiffe) | `v2.8.1` | Apache-2.0 | +| [github.com/stretchr/testify](https://github.com/stretchr/testify) | `v1.12.0` | MIT | +| [github.com/tailscale/certstore](https://github.com/tailscale/certstore) | `v0.1.1-0.20260409135935-3638fb84b77d` | MIT | | [github.com/tailscale/go-winio](https://github.com/tailscale/go-winio) | `v0.0.0-20231025203758-c4f33415bf55` | MIT | -| [github.com/tailscale/goupnp](https://github.com/tailscale/goupnp) | `v1.0.1-0.20210804011211-c64d0f06ea05` | BSD-2-Clause | -| [github.com/tailscale/hujson](https://github.com/tailscale/hujson) | `v0.0.0-20260302212456-ecc657c15afd` | BSD-3-Clause | +| [github.com/tailscale/hujson](https://github.com/tailscale/hujson) | `v0.0.0-20260727124030-b80ff77dac4f` | BSD-3-Clause | | [github.com/tailscale/peercred](https://github.com/tailscale/peercred) | `v0.0.0-20250107143737-35a0c7bd7edc` | BSD-3-Clause | | [github.com/tailscale/web-client-prebuilt](https://github.com/tailscale/web-client-prebuilt) | `v0.0.0-20250124233751-d4cd19a26976` | BSD-3-Clause | -| [github.com/tailscale/wireguard-go](https://github.com/tailscale/wireguard-go) | `v0.0.0-20250716170648-1d0488a3d7da` | MIT | -| [github.com/tidwall/gjson](https://github.com/tidwall/gjson) | `v1.18.0` | MIT | +| [github.com/tailscale/wireguard-go](https://github.com/tailscale/wireguard-go) | `v0.0.0-20260730222847-4affce44577c` | MIT | +| [github.com/tidwall/gjson](https://github.com/tidwall/gjson) | `v1.19.0` | MIT | | [github.com/tidwall/match](https://github.com/tidwall/match) | `v1.1.1` | MIT | | [github.com/tidwall/pretty](https://github.com/tidwall/pretty) | `v1.2.1` | MIT | +| [github.com/tjfoc/gmsm](https://github.com/tjfoc/gmsm) | `v1.4.1` | Apache-2.0 | | [github.com/tklauser/go-sysconf](https://github.com/tklauser/go-sysconf) | `v0.3.16` | BSD-3-Clause | | [github.com/tklauser/numcpus](https://github.com/tklauser/numcpus) | `v0.11.0` | Apache-2.0 | -| [github.com/tonistiigi/fsutil](https://github.com/tonistiigi/fsutil) | `v0.0.0-20260609174605-b61e79c0c046` | MIT | +| [github.com/tonistiigi/fsutil](https://github.com/tonistiigi/fsutil) | `v0.0.0-20260717003753-6d9dc2ebad62` | MIT | | [github.com/tonistiigi/go-csvvalue](https://github.com/tonistiigi/go-csvvalue) | `v0.0.0-20240814133006-030d3b2625d0` | MIT | | [github.com/tonistiigi/units](https://github.com/tonistiigi/units) | `v0.0.0-20180711220420-6950e57a87ea` | MIT | | [github.com/tonistiigi/vt100](https://github.com/tonistiigi/vt100) | `v0.0.0-20240514184818-90bafcd6abab` | MIT | | [github.com/u-root/u-root](https://github.com/u-root/u-root) | `v0.16.0` | BSD-3-Clause | -| [github.com/ulikunitz/xz](https://github.com/ulikunitz/xz) | `v0.5.15` | BSD-3-Clause | -| [github.com/vbatts/tar-split](https://github.com/vbatts/tar-split) | `v0.12.2` | BSD-3-Clause | +| [github.com/ulikunitz/xz](https://github.com/ulikunitz/xz) | `v0.5.16` | BSD-3-Clause | +| [github.com/urfave/cli](https://github.com/urfave/cli) | `v1.22.17` | MIT | | [github.com/x448/float16](https://github.com/x448/float16) | `v0.8.4` | MIT | | [github.com/xhit/go-str2duration/v2](https://github.com/xhit/go-str2duration) | `v2.1.0` | BSD-3-Clause | | [github.com/xo/terminfo](https://github.com/xo/terminfo) | `v0.0.0-20220910002029-abceb7e1c41e` | MIT | | [github.com/yosida95/uritemplate/v3](https://github.com/yosida95/uritemplate) | `v3.0.2` | BSD-3-Clause | | [github.com/yusufpapurcu/wmi](https://github.com/yusufpapurcu/wmi) | `v1.2.4` | MIT | -| [gitlab.com/gitlab-org/api/client-go](https://gitlab.com/gitlab-org/api/client-go) | `v1.9.1` | Apache-2.0 | +| [github.com/zalando/go-keyring](https://github.com/zalando/go-keyring) | `v0.2.8` | MIT | +| [gitlab.com/gitlab-org/api/client-go](https://gitlab.com/gitlab-org/api/client-go) | `v1.46.0` | Apache-2.0 | | [go.etcd.io/etcd/api/v3](https://go.etcd.io/etcd/api/v3) | `v3.6.8` | Apache-2.0 | | [go.etcd.io/etcd/client/pkg/v3](https://go.etcd.io/etcd/client/pkg/v3) | `v3.6.8` | Apache-2.0 | | [go.etcd.io/etcd/client/v3](https://go.etcd.io/etcd/client/v3) | `v3.6.8` | Apache-2.0 | +| [go.mongodb.org/mongo-driver](https://go.mongodb.org/mongo-driver) | `v1.17.9` | Apache-2.0 | | [go.opentelemetry.io/auto/sdk](https://go.opentelemetry.io/auto/sdk) | `v1.2.1` | Apache-2.0 | -| [go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc](https://go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc) | `v0.65.0` | Apache-2.0 | -| [go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace](https://go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace) | `v0.63.0` | Apache-2.0 | -| [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp) | `v0.65.0` | Apache-2.0 | -| [go.opentelemetry.io/otel](https://go.opentelemetry.io/otel) | `v1.41.0` | Apache-2.0 | -| [go.opentelemetry.io/otel/exporters/otlp/otlptrace](https://go.opentelemetry.io/otel/exporters/otlp/otlptrace) | `v1.40.0` | Apache-2.0 | -| [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc) | `v1.40.0` | Apache-2.0 | -| [go.opentelemetry.io/otel/metric](https://go.opentelemetry.io/otel/metric) | `v1.41.0` | Apache-2.0 | -| [go.opentelemetry.io/otel/sdk](https://go.opentelemetry.io/otel/sdk) | `v1.40.0` | Apache-2.0 | -| [go.opentelemetry.io/otel/trace](https://go.opentelemetry.io/otel/trace) | `v1.41.0` | Apache-2.0 | -| [go.opentelemetry.io/proto/otlp](https://go.opentelemetry.io/proto/otlp) | `v1.9.0` | Apache-2.0 | +| [go.opentelemetry.io/contrib/detectors/gcp](https://go.opentelemetry.io/contrib/detectors/gcp) | `v1.44.0` | Apache-2.0 | +| [go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc](https://go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc) | `v0.69.0` | Apache-2.0 | +| [go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace](https://go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace) | `v0.69.0` | Apache-2.0 | +| [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp) | `v0.69.0` | Apache-2.0 | +| [go.opentelemetry.io/otel](https://go.opentelemetry.io/otel) | `v1.44.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/exporters/otlp/otlptrace](https://go.opentelemetry.io/otel/exporters/otlp/otlptrace) | `v1.44.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc) | `v1.44.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/metric](https://go.opentelemetry.io/otel/metric) | `v1.44.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/sdk](https://go.opentelemetry.io/otel/sdk) | `v1.44.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/sdk/metric](https://go.opentelemetry.io/otel/sdk/metric) | `v1.44.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/trace](https://go.opentelemetry.io/otel/trace) | `v1.44.0` | Apache-2.0 | +| [go.opentelemetry.io/proto/otlp](https://go.opentelemetry.io/proto/otlp) | `v1.10.0` | Apache-2.0 | | [go.uber.org/atomic](https://go.uber.org/atomic) | `v1.11.0` | MIT | +| [go.uber.org/goleak](https://go.uber.org/goleak) | `v1.3.0` | MIT | | [go.uber.org/multierr](https://go.uber.org/multierr) | `v1.11.0` | MIT | -| [go.uber.org/zap](https://go.uber.org/zap) | `v1.27.1` | MIT | -| [go.yaml.in/yaml/v2](https://go.yaml.in/yaml/v2) | `v2.4.3` | Apache-2.0 | +| [go.uber.org/zap](https://go.uber.org/zap) | `v1.28.0` | MIT | +| [go.yaml.in/yaml/v2](https://go.yaml.in/yaml/v2) | `v2.4.4` | Apache-2.0 | | [go.yaml.in/yaml/v3](https://go.yaml.in/yaml/v3) | `v3.0.4` | MIT | -| [go.yaml.in/yaml/v4](https://go.yaml.in/yaml/v4) | `v4.0.0-rc.4` | Apache-2.0 | +| [go.yaml.in/yaml/v4](https://go.yaml.in/yaml/v4) | `v4.0.0-rc.6` | Apache-2.0 | | [go4.org/mem](https://go4.org/mem) | `v0.0.0-20240501181205-ae6ca9944745` | Apache-2.0 | | [go4.org/netipx](https://go4.org/netipx) | `v0.0.0-20231129151722-fdeea329fbba` | BSD-3-Clause | -| [golang.org/x/crypto](https://golang.org/x/crypto) | `v0.50.0` | BSD-3-Clause | -| [golang.org/x/exp](https://golang.org/x/exp) | `v0.0.0-20251219203646-944ab1f22d93` | BSD-3-Clause | -| [golang.org/x/mod](https://golang.org/x/mod) | `v0.35.0` | BSD-3-Clause | -| [golang.org/x/net](https://golang.org/x/net) | `v0.53.0` | BSD-3-Clause | +| [golang.org/x/crypto](https://golang.org/x/crypto) | `v0.55.0` | BSD-3-Clause | +| [golang.org/x/exp](https://golang.org/x/exp) | `v0.0.0-20260603202125-055de637280b` | BSD-3-Clause | +| [golang.org/x/mod](https://golang.org/x/mod) | `v0.38.0` | BSD-3-Clause | +| [golang.org/x/net](https://golang.org/x/net) | `v0.57.1-0.20260729233039-99c3b0a8f463` | BSD-3-Clause | | [golang.org/x/oauth2](https://golang.org/x/oauth2) | `v0.36.0` | BSD-3-Clause | -| [golang.org/x/sync](https://golang.org/x/sync) | `v0.20.0` | BSD-3-Clause | -| [golang.org/x/sys](https://golang.org/x/sys) | `v0.46.0` | BSD-3-Clause | -| [golang.org/x/term](https://golang.org/x/term) | `v0.42.0` | BSD-3-Clause | -| [golang.org/x/text](https://golang.org/x/text) | `v0.36.0` | BSD-3-Clause | -| [golang.org/x/time](https://golang.org/x/time) | `v0.14.0` | BSD-3-Clause | -| [golang.org/x/tools](https://golang.org/x/tools) | `v0.44.0` | BSD-3-Clause | +| [golang.org/x/sync](https://golang.org/x/sync) | `v0.22.0` | BSD-3-Clause | +| [golang.org/x/sys](https://golang.org/x/sys) | `v0.47.0` | BSD-3-Clause | +| [golang.org/x/term](https://golang.org/x/term) | `v0.45.0` | BSD-3-Clause | +| [golang.org/x/text](https://golang.org/x/text) | `v0.41.0` | BSD-3-Clause | +| [golang.org/x/time](https://golang.org/x/time) | `v0.15.0` | BSD-3-Clause | +| [golang.org/x/tools](https://golang.org/x/tools) | `v0.48.0` | BSD-3-Clause | | [golang.zx2c4.com/wintun](https://golang.zx2c4.com/wintun) | `v0.0.0-20230126152724-0fa3db229ce2` | MIT | | [golang.zx2c4.com/wireguard/windows](https://golang.zx2c4.com/wireguard/windows) | `v0.5.3` | MIT | | [gomodules.xyz/jsonpatch/v2](https://gomodules.xyz/jsonpatch/v2) | `v2.4.0` | Apache-2.0 | -| [google.golang.org/genproto/googleapis/api](https://google.golang.org/genproto/googleapis/api) | `v0.0.0-20260128011058-8636f8732409` | Apache-2.0 | -| [google.golang.org/genproto/googleapis/rpc](https://google.golang.org/genproto/googleapis/rpc) | `v0.0.0-20260128011058-8636f8732409` | Apache-2.0 | -| [google.golang.org/grpc](https://google.golang.org/grpc) | `v1.80.0` | Apache-2.0 | +| [google.golang.org/api](https://google.golang.org/api) | `v0.289.0` | BSD-3-Clause | +| [google.golang.org/genproto](https://google.golang.org/genproto) | `v0.0.0-20260720171339-e059f2f05d78` | Apache-2.0 | +| [google.golang.org/genproto/googleapis/api](https://google.golang.org/genproto/googleapis/api) | `v0.0.0-20260720171339-e059f2f05d78` | Apache-2.0 | +| [google.golang.org/genproto/googleapis/rpc](https://google.golang.org/genproto/googleapis/rpc) | `v0.0.0-20260720171339-e059f2f05d78` | Apache-2.0 | +| [google.golang.org/grpc](https://google.golang.org/grpc) | `v1.83.0` | Apache-2.0 | | [google.golang.org/protobuf](https://google.golang.org/protobuf) | `v1.36.12-0.20260120151049-f2248ac996af` | BSD-3-Clause | | [gopkg.in/evanphx/json-patch.v4](https://gopkg.in/evanphx/json-patch.v4) | `v4.13.0` | BSD-3-Clause | | [gopkg.in/inf.v0](https://gopkg.in/inf.v0) | `v0.9.1` | BSD-3-Clause | +| [gopkg.in/ini.v1](https://gopkg.in/ini.v1) | `v1.67.3` | Apache-2.0 | | [gopkg.in/yaml.v3](https://gopkg.in/yaml.v3) | `v3.0.1` | MIT | -| [gvisor.dev/gvisor](https://gvisor.dev/gvisor) | `v0.0.0-20250205023644-9414b50a5633` | Apache-2.0 | -| [k8s.io/api](https://github.com/kubernetes/api) | `v0.36.2` | Apache-2.0 | +| [gvisor.dev/gvisor](https://gvisor.dev/gvisor) | `v0.0.0-20260224225140-573d5e7127a8` | Apache-2.0 | +| [k8s.io/api](https://github.com/kubernetes/api) | `v0.36.3` | Apache-2.0 | | [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver) | `v0.36.2` | Apache-2.0 | -| [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) | `v0.36.2` | Apache-2.0 | -| [k8s.io/apiserver](https://github.com/kubernetes/apiserver) | `v0.36.2` | Apache-2.0 | -| [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) | `v0.36.2` | Apache-2.0 | -| [k8s.io/client-go](https://github.com/kubernetes/client-go) | `v0.36.2` | Apache-2.0 | -| [k8s.io/component-base](https://github.com/kubernetes/component-base) | `v0.36.2` | Apache-2.0 | +| [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) | `v0.36.3` | Apache-2.0 | +| [k8s.io/apiserver](https://github.com/kubernetes/apiserver) | `v0.36.3` | Apache-2.0 | +| [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) | `v0.36.3` | Apache-2.0 | +| [k8s.io/client-go](https://github.com/kubernetes/client-go) | `v0.36.3` | Apache-2.0 | +| [k8s.io/component-base](https://github.com/kubernetes/component-base) | `v0.36.3` | Apache-2.0 | | [k8s.io/klog/v2](https://github.com/kubernetes/klog) | `v2.140.0` | Apache-2.0 | -| [k8s.io/kube-aggregator](https://github.com/kubernetes/kube-aggregator) | `v0.36.2` | Apache-2.0 | -| [k8s.io/kube-openapi](https://github.com/kubernetes/kube-openapi) | `v0.0.0-20260330154417-16be699c7b31` | Apache-2.0 | -| [k8s.io/kubectl](https://github.com/kubernetes/kubectl) | `v0.36.2` | Apache-2.0 | -| [k8s.io/metrics](https://github.com/kubernetes/metrics) | `v0.36.2` | Apache-2.0 | -| [k8s.io/streaming](https://github.com/kubernetes/streaming) | `v0.36.2` | Apache-2.0 | -| [k8s.io/utils](https://github.com/kubernetes/utils) | `v0.0.0-20260319190234-28399d86e0b5` | Apache-2.0 | +| [k8s.io/kube-aggregator](https://github.com/kubernetes/kube-aggregator) | `v0.36.3` | Apache-2.0 | +| [k8s.io/kube-openapi](https://github.com/kubernetes/kube-openapi) | `v0.0.0-20260624041617-8f3fa4921821` | Apache-2.0 | +| [k8s.io/kubectl](https://github.com/kubernetes/kubectl) | `v0.36.3` | Apache-2.0 | +| [k8s.io/metrics](https://github.com/kubernetes/metrics) | `v0.36.3` | Apache-2.0 | +| [k8s.io/streaming](https://github.com/kubernetes/streaming) | `v0.36.3` | Apache-2.0 | +| [k8s.io/utils](https://github.com/kubernetes/utils) | `v0.0.0-20260707023825-cf1189d6abe3` | Apache-2.0 | | [mvdan.cc/sh/v3](https://mvdan.cc/sh/v3) | `v3.13.1` | BSD-3-Clause | | [sigs.k8s.io/apiserver-network-proxy/konnectivity-client](https://sigs.k8s.io/apiserver-network-proxy/konnectivity-client) | `v0.34.0` | Apache-2.0 | | [sigs.k8s.io/controller-runtime](https://sigs.k8s.io/controller-runtime) | `v0.24.1` | Apache-2.0 | | [sigs.k8s.io/json](https://sigs.k8s.io/json) | `v0.0.0-20250730193827-2d320260d730` | Apache-2.0 | | [sigs.k8s.io/randfill](https://sigs.k8s.io/randfill) | `v1.0.0` | Apache-2.0 | -| [sigs.k8s.io/structured-merge-diff/v6](https://sigs.k8s.io/structured-merge-diff/v6) | `v6.3.2` | Apache-2.0 | +| [sigs.k8s.io/structured-merge-diff/v6](https://sigs.k8s.io/structured-merge-diff/v6) | `v6.3.3` | Apache-2.0 | | [sigs.k8s.io/yaml](https://sigs.k8s.io/yaml) | `v1.6.0` | Apache-2.0 | -| [tailscale.com](https://tailscale.com) | `v1.96.5` | BSD-3-Clause | +| [tailscale.com](https://tailscale.com) | `v1.102.2` | BSD-3-Clause | diff --git a/cmd/ci/ci.go b/cmd/ci/ci.go index 4c5c55949..81b32dc87 100644 --- a/cmd/ci/ci.go +++ b/cmd/ci/ci.go @@ -139,16 +139,21 @@ func (cmd *CICmd) registerSecretsFlags(ciCmd *cobra.Command) { cliflags.String(&cmd.FeatureSecretsFile, names.FeatureSecretsFile, "", "Path to a JSON file containing secret values for features, format: "+ `{"featureId": {"optionName": "value"}}`), - cliflags.StringArray(&cmd.Secrets, names.Secret, nil, - "Stored Devsy secret to inject, as NAME[,type=env|mount][,target=X]; "+ - "type=env (default) sets an env var, type=mount writes /run/secrets/. Repeatable"), + cliflags.StringArray( + &cmd.Secrets, + names.Secret, + nil, + "Secret to inject: NAME or source-qualified TYPE:SOURCE/NAME[,type=env|mount][,target=X]; "+ + "type=env (default) sets an env var, type=mount writes /run/secrets/. Repeatable", + ), cliflags.StringArray(&cmd.EnvVars, names.Env, nil, "Stored Devsy env var to inject into the workspace as NAME[=TARGET]. Repeatable"), cliflags.StringArray(&cmd.BuildSecretNames, names.BuildSecret, nil, - "Stored Devsy secret exposed to the build via BuildKit "+ + "Secret to expose to the build via BuildKit; accepts NAME or TYPE:SOURCE/NAME "+ "(RUN --mount=type=secret,id=NAME). Repeatable"), cliflags.String(&cmd.GitTokenSecret, names.GitToken, "", - "Stored Devsy secret holding an access token for cloning a private HTTP repository"), + "Secret holding an access token for cloning a private HTTP repository; "+ + "accepts NAME or TYPE:SOURCE/NAME; repository-owned sources cannot bootstrap their own clone"), cliflags.String(&cmd.GitTokenUsername, names.GitTokenUsername, "", "Username for --git-token (default inferred from the repo host)"), ) diff --git a/cmd/secrets/bind.go b/cmd/secrets/bind.go index d11be92c4..09e812776 100644 --- a/cmd/secrets/bind.go +++ b/cmd/secrets/bind.go @@ -20,7 +20,7 @@ func NewAttachCmd(flags *flags.GlobalFlags) *cobra.Command { cmd := &AttachCmd{GlobalFlags: flags} attachCmd := &cobra.Command{ Use: "attach NAME", - Short: "Bind a secret to the active context so it is injected automatically on up", + Short: "Bind a secret reference to the active context so it is injected automatically on up", Args: cobra.ExactArgs(1), RunE: func(cobraCmd *cobra.Command, args []string) error { return cmd.Run(cobraCmd.Context(), args[0]) @@ -30,7 +30,7 @@ func NewAttachCmd(flags *flags.GlobalFlags) *cobra.Command { return attachCmd } -func verifySensitive(devsyConfig *config.Config, contextName, name string) error { +func verifyLocalSensitive(devsyConfig *config.Config, contextName, name string) error { store, err := secrets.NewStoreForConfig(devsyConfig) if err != nil { return err @@ -45,10 +45,35 @@ func verifySensitive(devsyConfig *config.Config, contextName, name string) error return nil } -func (cmd *AttachCmd) Run(_ context.Context, name string) error { - if err := secrets.ValidateName(name); err != nil { +func verifySecretReference( + ctx context.Context, + devsyConfig *config.Config, + ref secrets.SecretRef, +) error { + if ref.Source == secrets.LocalSourceName && + (ref.Type == "" || ref.Type == secrets.LocalSourceName) { + return verifyLocalSensitive(devsyConfig, devsyConfig.DefaultContext, ref.Name) + } + resolver, err := secrets.NewResolverForConfig(devsyConfig) + if err != nil { + return err + } + resolved, err := resolver.Resolve(ctx, ref) + if err != nil { + return fmt.Errorf("cannot attach secret %q: %w", ref.String(), err) + } + if !resolved.Sensitive { + return fmt.Errorf("%q is not a sensitive secret", ref.String()) + } + return nil +} + +func (cmd *AttachCmd) Run(ctx context.Context, name string) error { + ref, err := secrets.ParseRef(name) + if err != nil { return err } + canonical := ref.String() devsyConfig, err := config.LoadConfig(cmd.Context, cmd.Provider) if err != nil { @@ -56,7 +81,7 @@ func (cmd *AttachCmd) Run(_ context.Context, name string) error { } contextName := devsyConfig.DefaultContext - if err := verifySensitive(devsyConfig, contextName, name); err != nil { + if err := verifySecretReference(ctx, devsyConfig, ref); err != nil { return err } @@ -64,17 +89,17 @@ func (cmd *AttachCmd) Run(_ context.Context, name string) error { if ctxConfig == nil { return fmt.Errorf("context %q doesn't exist", contextName) } - if slices.Contains(ctxConfig.Secrets, name) { - log.Infof("secret %q already attached to context %q", name, contextName) + if slices.Contains(ctxConfig.Secrets, canonical) { + log.Infof("secret %q already attached to context %q", canonical, contextName) return nil } - ctxConfig.Secrets = append(ctxConfig.Secrets, name) + ctxConfig.Secrets = append(ctxConfig.Secrets, canonical) if err := config.SaveConfig(devsyConfig); err != nil { return fmt.Errorf("save config: %w", err) } - log.Infof("secret %q attached to context %q", name, contextName) + log.Infof("secret %q attached to context %q", canonical, contextName) return nil } @@ -86,7 +111,7 @@ func NewDetachCmd(flags *flags.GlobalFlags) *cobra.Command { cmd := &DetachCmd{GlobalFlags: flags} detachCmd := &cobra.Command{ Use: "detach NAME", - Short: "Unbind a secret from the active context", + Short: "Unbind a secret reference from the active context", Args: cobra.ExactArgs(1), RunE: func(cobraCmd *cobra.Command, args []string) error { return cmd.Run(cobraCmd.Context(), args[0]) @@ -97,6 +122,11 @@ func NewDetachCmd(flags *flags.GlobalFlags) *cobra.Command { } func (cmd *DetachCmd) Run(_ context.Context, name string) error { + ref, err := secrets.ParseRef(name) + if err != nil { + return err + } + canonical := ref.String() devsyConfig, err := config.LoadConfig(cmd.Context, cmd.Provider) if err != nil { return err @@ -108,9 +138,9 @@ func (cmd *DetachCmd) Run(_ context.Context, name string) error { return fmt.Errorf("context %q doesn't exist", contextName) } - idx := slices.Index(ctxConfig.Secrets, name) + idx := slices.Index(ctxConfig.Secrets, canonical) if idx < 0 { - log.Infof("secret %q is not attached to context %q", name, contextName) + log.Infof("secret %q is not attached to context %q", canonical, contextName) return nil } ctxConfig.Secrets = slices.Delete(ctxConfig.Secrets, idx, idx+1) @@ -119,6 +149,6 @@ func (cmd *DetachCmd) Run(_ context.Context, name string) error { return fmt.Errorf("save config: %w", err) } - log.Infof("secret %q detached from context %q", name, contextName) + log.Infof("secret %q detached from context %q", canonical, contextName) return nil } diff --git a/cmd/secrets/secrets.go b/cmd/secrets/secrets.go index 0a8acf3ed..d6c12cfe8 100644 --- a/cmd/secrets/secrets.go +++ b/cmd/secrets/secrets.go @@ -10,12 +10,13 @@ import ( func NewSecretsCmd(flags *flags.GlobalFlags) *cobra.Command { secretsCmd := &cobra.Command{ Use: "secret", - Short: "Manage secrets", - Long: `Manage named secrets stored locally and injected into workspaces. + Short: "Manage secrets and external secret sources", + Long: `Manage secrets and external secret sources used by Devsy workspaces. -Secret values are kept in the OS keyring (macOS Keychain, Windows Credential -Manager, or libsecret) when available, and in an age-encrypted file otherwise. -Only non-sensitive metadata is stored in the Devsy config directory.`, +Managed secret values are kept in the OS keyring (macOS Keychain, +Windows Credential Manager, or libsecret) when available, and in an +age-encrypted file otherwise. External sources such as SOPS remain owned by +their encrypted source file and are resolved only when needed.`, } secretsCmd.AddCommand(NewSetCmd(flags)) @@ -24,6 +25,7 @@ Only non-sensitive metadata is stored in the Devsy config directory.`, secretsCmd.AddCommand(NewDeleteCmd(flags)) secretsCmd.AddCommand(NewAttachCmd(flags)) secretsCmd.AddCommand(NewDetachCmd(flags)) + secretsCmd.AddCommand(NewSourceCmd(flags)) return secretsCmd } diff --git a/cmd/secrets/source.go b/cmd/secrets/source.go new file mode 100644 index 000000000..2a5206831 --- /dev/null +++ b/cmd/secrets/source.go @@ -0,0 +1,221 @@ +package secrets + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "sort" + + "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/config" + secrets2 "github.com/devsy-org/devsy/pkg/secrets" + "github.com/spf13/cobra" +) + +type addSOPSSourceOptions struct { + out io.Writer + globalFlags *flags.GlobalFlags + name string + filePath string + format string +} + +func NewSourceCmd(globalFlags *flags.GlobalFlags) *cobra.Command { + cmd := &cobra.Command{ + Use: "source", + Short: "Manage external secret sources", + } + cmd.AddCommand(newSourceAddCmd(globalFlags)) + cmd.AddCommand(newSourceListCmd(globalFlags)) + cmd.AddCommand(newSourceRemoveCmd(globalFlags)) + return cmd +} + +func newSourceAddCmd(globalFlags *flags.GlobalFlags) *cobra.Command { + var format string + cmd := &cobra.Command{ + Use: "add sops NAME PATH", + Short: "Register a SOPS-encrypted file as a secret source", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + if args[0] != secrets2.SOPSFormatter { + return fmt.Errorf("unsupported secret source type %q", args[0]) + } + return addSOPSSource(cmd.Context(), addSOPSSourceOptions{ + out: cmd.OutOrStdout(), + globalFlags: globalFlags, + name: args[1], + filePath: args[2], + format: format, + }) + }, + } + cmd.Flags().StringVar( + &format, + "format", + "", + "SOPS document format override (yaml, json, dotenv)", + ) + return cmd +} + +func addSOPSSource(ctx context.Context, opts addSOPSSourceOptions) error { + if err := secrets2.ValidateSourceName(opts.name); err != nil { + return err + } + if opts.name == secrets2.LocalSourceName { + return fmt.Errorf("secret source name %q is reserved", opts.name) + } + absolutePath, err := resolveSOPSFile(opts.filePath) + if err != nil { + return err + } + devsyConfig, err := config.LoadConfig(opts.globalFlags.Context, opts.globalFlags.Provider) + if err != nil { + return err + } + if err := secrets2.NewSOPSSource(opts.name, absolutePath, opts.format). + Validate(ctx); err != nil { + return fmt.Errorf("cannot add SOPS source %q: %w", opts.name, err) + } + if err := persistSOPSSource(devsyConfig, opts.name, absolutePath, opts.format); err != nil { + return err + } + _, err = fmt.Fprintf(opts.out, "Added SOPS secret source %q: %s\n", opts.name, absolutePath) + return err +} + +func resolveSOPSFile(filePath string) (string, error) { + absolutePath, err := filepath.Abs(filePath) + if err != nil { + return "", err + } + absolutePath, err = filepath.EvalSymlinks(absolutePath) + if err != nil { + return "", fmt.Errorf("resolve SOPS source path: %w", err) + } + info, err := os.Stat(absolutePath) + if err != nil { + return "", fmt.Errorf("stat SOPS source path: %w", err) + } + if !info.Mode().IsRegular() { + return "", fmt.Errorf("SOPS source path %q is not a regular file", filePath) + } + return absolutePath, nil +} + +func persistSOPSSource(devsyConfig *config.Config, name, filePath, format string) error { + return secrets2.ModifySourceConfigs( + devsyConfig, + func(sources []secrets2.SourceConfig) ([]secrets2.SourceConfig, error) { + return secrets2.AddSourceConfig(sources, secrets2.SourceConfig{ + Name: name, + Type: secrets2.SOPSFormatter, + Path: filePath, + Format: format, + }) + }, + ) +} + +func newSourceListCmd(globalFlags *flags.GlobalFlags) *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List configured external secret sources", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + devsyConfig, err := config.LoadConfig(globalFlags.Context, globalFlags.Provider) + if err != nil { + return err + } + sources, err := secrets2.LoadSourceConfigs(devsyConfig) + if err != nil { + return err + } + sort.Slice(sources, func(i, j int) bool { return sources[i].Name < sources[j].Name }) + return writeSourceList(cmd.OutOrStdout(), sources) + }, + } +} + +func writeSourceList(out io.Writer, sources []secrets2.SourceConfig) error { + if len(sources) == 0 { + _, err := fmt.Fprintln(out, "No external secret sources configured.") + return err + } + if _, err := fmt.Fprintln(out, "NAME\tTYPE\tLOCATION"); err != nil { + return err + } + for _, source := range sources { + if _, err := fmt.Fprintf( + out, + "%s\t%s\t%s\n", + source.Name, + source.Type, + source.Path, + ); err != nil { + return err + } + } + return nil +} + +func newSourceRemoveCmd(globalFlags *flags.GlobalFlags) *cobra.Command { + return &cobra.Command{ + Use: "remove NAME", + Short: "Remove an external secret source reference", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return removeSource(cmd.OutOrStdout(), globalFlags, args[0]) + }, + } +} + +func removeSource(out io.Writer, globalFlags *flags.GlobalFlags, name string) error { + devsyConfig, err := config.LoadConfig(globalFlags.Context, globalFlags.Provider) + if err != nil { + return err + } + if references := attachedSourceReferences(devsyConfig, name); len(references) > 0 { + return fmt.Errorf( + "cannot remove secret source %q: attached secrets still reference it: %v", + name, + references, + ) + } + if err := removeSourceConfig(devsyConfig, name); err != nil { + return err + } + _, err = fmt.Fprintf(out, "Removed secret source %q\n", name) + return err +} + +func attachedSourceReferences(devsyConfig *config.Config, name string) []string { + current := devsyConfig.Current() + if current == nil { + return nil + } + var references []string + for _, bound := range current.Secrets { + ref, err := secrets2.ParseRef(bound) + if err == nil && ref.Source == name { + references = append(references, bound) + } + } + return references +} + +func removeSourceConfig(devsyConfig *config.Config, name string) error { + return secrets2.ModifySourceConfigs( + devsyConfig, + func(sources []secrets2.SourceConfig) ([]secrets2.SourceConfig, error) { + sources, removed := secrets2.RemoveSourceConfig(sources, name) + if !removed { + return nil, fmt.Errorf("secret source %q is not configured", name) + } + return sources, nil + }, + ) +} diff --git a/cmd/workspace/up/project_secrets.go b/cmd/workspace/up/project_secrets.go new file mode 100644 index 000000000..d0f128e25 --- /dev/null +++ b/cmd/workspace/up/project_secrets.go @@ -0,0 +1,244 @@ +package up + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "path/filepath" + "strconv" + "strings" + + gitpkg "github.com/devsy-org/devsy/pkg/git" + provider2 "github.com/devsy-org/devsy/pkg/provider" + "github.com/devsy-org/devsy/pkg/secrets" +) + +type projectSecretContext struct { + config *secrets.ProjectConfig + sources map[string]secrets.Source +} + +func (p *projectSecretContext) attachedSecrets() []string { + if p == nil || p.config == nil { + return nil + } + return p.config.Secrets +} + +func (p *projectSecretContext) register(resolver *secrets.Resolver) error { + if p == nil { + return nil + } + for _, sourceConfig := range p.config.SecretSources { + source := p.sources[sourceConfig.Name] + if source == nil { + return fmt.Errorf("project secret source %q was not loaded", sourceConfig.Name) + } + if err := resolver.Register(sourceConfig.Name, sourceConfig.Type, source); err != nil { + return fmt.Errorf("register project secret source %q: %w", sourceConfig.Name, err) + } + } + return nil +} + +func (cmd *UpCmd) discoverProjectSecrets( + ctx context.Context, + source *provider2.WorkspaceSource, +) (*projectSecretContext, error) { + if source == nil { + return nil, nil + } + if source.LocalFolder != "" { + folder := source.LocalFolder + if source.GitSubPath != "" { + cleanSubPath, err := secrets.CleanProjectSourcePath(source.GitSubPath) + if err != nil { + return nil, fmt.Errorf("invalid subpath %q: %w", source.GitSubPath, err) + } + folder = filepath.Join(folder, filepath.FromSlash(cleanSubPath)) + } + return cmd.discoverLocalProjectSecrets(folder) + } + if source.GitRepository == "" { + return nil, nil + } + return cmd.discoverRemoteProjectSecrets(ctx, source) +} + +func (cmd *UpCmd) discoverLocalProjectSecrets(root string) (*projectSecretContext, error) { + root, err := filepath.Abs(root) + if err != nil { + return nil, err + } + cfg, found, err := secrets.LoadProjectConfigFromRootWithOptions( + root, + cmd.DevContainerPath, + cmd.DevContainerID, + ) + if err != nil || !found { + return nil, err + } + project := &projectSecretContext{config: cfg, sources: map[string]secrets.Source{}} + for _, sourceConfig := range cfg.SecretSources { + resolvedPath, err := secrets.ResolveProjectSourcePath(root, sourceConfig.Path) + if err != nil { + return nil, fmt.Errorf("load project secret source %q: %w", sourceConfig.Name, err) + } + project.sources[sourceConfig.Name] = secrets.NewSOPSSource( + sourceConfig.Name, + resolvedPath, + sourceConfig.Format, + ) + } + return project, nil +} + +func (cmd *UpCmd) discoverRemoteProjectSecrets( + ctx context.Context, + source *provider2.WorkspaceSource, +) (*projectSecretContext, error) { + info := &gitpkg.GitInfo{ + Repository: source.GitRepository, + Branch: source.GitBranch, + Commit: source.GitCommit, + PR: source.GitPRReference, + SubPath: source.GitSubPath, + } + inspection, err := gitpkg.InspectRemote(ctx, info, cmd.gitInspectionEnv()) + if err != nil { + return nil, err + } + defer func() { _ = inspection.Close() }() + if inspection.Revision() != "" { + source.GitCommit = inspection.Revision() + } + + cfg, err := cmd.readRemoteProjectConfig(ctx, inspection) + if err != nil || cfg == nil { + return nil, err + } + + sources, err := loadRemoteProjectSources(ctx, inspection, cfg) + if err != nil { + return nil, err + } + return &projectSecretContext{config: cfg, sources: sources}, nil +} + +func (cmd *UpCmd) readRemoteProjectConfig( + ctx context.Context, + inspection *gitpkg.Inspection, +) (*secrets.ProjectConfig, error) { + devContainerBytes, _, err := inspection.ReadDevContainerConfig( + ctx, + cmd.DevContainerPath, + cmd.DevContainerID, + ) + if err == nil { + cfg, parseErr := parseValidProjectConfig(devContainerBytes) + if parseErr != nil { + return nil, fmt.Errorf("parse devcontainer.json: %w", parseErr) + } + return cfg, nil + } + if !errors.Is(err, gitpkg.ErrRevisionPathNotFound) { + return nil, fmt.Errorf("discover repository devcontainer configuration: %w", err) + } + return nil, nil +} + +func parseValidProjectConfig(data []byte) (*secrets.ProjectConfig, error) { + parsed, err := secrets.ParseProjectConfig(data) + if err != nil { + return nil, err + } + if parsed != nil && (len(parsed.SecretSources) > 0 || len(parsed.Secrets) > 0) { + return parsed, nil + } + return nil, nil +} + +func loadRemoteProjectSources( + ctx context.Context, + inspection *gitpkg.Inspection, + cfg *secrets.ProjectConfig, +) (map[string]secrets.Source, error) { + sources := make(map[string]secrets.Source, len(cfg.SecretSources)) + for _, sourceConfig := range cfg.SecretSources { + cleanPath, err := secrets.CleanProjectSourcePath(sourceConfig.Path) + if err != nil { + return nil, fmt.Errorf("load project secret source %q: %w", sourceConfig.Name, err) + } + encrypted, err := inspection.ReadFile(ctx, cleanPath) + if err != nil { + return nil, fmt.Errorf( + "load project secret source %q at %q: %w", + sourceConfig.Name, + cleanPath, + err, + ) + } + sources[sourceConfig.Name] = secrets.NewSOPSDataSource( + sourceConfig.Name, + cleanPath, + sourceConfig.Format, + encrypted, + ) + } + return sources, nil +} + +func (cmd *UpCmd) gitInspectionEnv() []string { + env := gitpkg.GetDefaultExtraEnv(cmd.StrictHostKeyChecking) + if cmd.GitToken == nil || cmd.GitToken.Token == "" { + return env + } + host := cmd.GitToken.Host + if host == "" { + return env + } + username := cmd.GitToken.Username + if username == "" { + username = gitTokenUsernameForHost(host) + } + credential := base64.StdEncoding.EncodeToString([]byte(username + ":" + cmd.GitToken.Token)) + key := "http.https://" + host + "/.extraHeader" + count := 0 + outEnv := make([]string, 0, len(env)+3) + for _, entry := range env { + if val, ok := strings.CutPrefix(entry, "GIT_CONFIG_COUNT="); ok { + if n, err := strconv.Atoi(val); err == nil { + count = n + } + continue + } + outEnv = append(outEnv, entry) + } + + return append(outEnv, + fmt.Sprintf("GIT_CONFIG_COUNT=%d", count+1), + fmt.Sprintf("GIT_CONFIG_KEY_%d=%s", count, key), + fmt.Sprintf("GIT_CONFIG_VALUE_%d=Authorization: Basic %s", count, credential), + ) +} + +func validateBootstrapSecretReference( + ctx context.Context, + resolver *secrets.Resolver, + value string, +) (secrets.ResolvedSecret, error) { + ref, err := secrets.ParseRef(value) + if err != nil { + return secrets.ResolvedSecret{}, err + } + resolved, err := resolver.Resolve(ctx, ref) + if err != nil { + return secrets.ResolvedSecret{}, fmt.Errorf( + "cannot resolve bootstrap secret %q before repository acquisition: %w", + value, + err, + ) + } + return resolved, nil +} diff --git a/cmd/workspace/up/project_secrets_test.go b/cmd/workspace/up/project_secrets_test.go new file mode 100644 index 000000000..7ef2bcb5f --- /dev/null +++ b/cmd/workspace/up/project_secrets_test.go @@ -0,0 +1,292 @@ +package up + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/devsy-org/devsy/cmd/flags" + client2 "github.com/devsy-org/devsy/pkg/client" + "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/provider" + "github.com/devsy-org/devsy/pkg/workspace" + "github.com/stretchr/testify/require" +) + +type fakeWorkspaceClient struct { + config *provider.Workspace + deleted bool +} + +func (f *fakeWorkspaceClient) Provider() string { return "" } +func (f *fakeWorkspaceClient) Context() string { return "" } +func (f *fakeWorkspaceClient) RefreshOptions(context.Context, []string, bool) error { + return nil +} + +func (f *fakeWorkspaceClient) Status( + context.Context, + client2.StatusOptions, +) (client2.Status, error) { + return client2.StatusRunning, nil +} +func (f *fakeWorkspaceClient) Stop(context.Context, client2.StopOptions) error { return nil } +func (f *fakeWorkspaceClient) Delete(context.Context, client2.DeleteOptions) error { + f.deleted = true + return nil +} + +func (f *fakeWorkspaceClient) Workspace() string { return "" } + +func (f *fakeWorkspaceClient) WorkspaceConfig() *provider.Workspace { return f.config } +func (f *fakeWorkspaceClient) Lock(context.Context) error { return nil } +func (f *fakeWorkspaceClient) Unlock() {} + +const ( + // Synthetic test-only identity generated solely for this fixture. + testProjectAgeIdentity = "AGE-SECRET-KEY-12UWYSAH2MRDQ5K4EWC4253PDTCSCS32Y5EFQ8TEN2SL3QYU2GN2SG88CZX" // gitleaks:allow + testProjectPlaintext = "SUPER_SECRET_TEST_VALUE_7B91" + testProjectFixture = "testdata/sops-project-secrets.enc.yaml" +) + +func newTestProjectWorkspace(t *testing.T) *fakeWorkspaceClient { + t.Helper() + root := t.TempDir() + devcontainerDir := filepath.Join(root, ".devcontainer") + require.NoError(t, os.MkdirAll(devcontainerDir, 0o750)) + require.NoError(t, os.WriteFile( + filepath.Join(devcontainerDir, "devcontainer.json"), + []byte(`{ + "customizations": { + "devsy": { + "secretSources": [ + { + "name": "project", + "type": "sops", + "path": "secrets.enc.yaml" + } + ], + "secrets": [ + "sops:project/SOPS_E2E_SECRET" + ] + } + } +}`), + 0o600, + )) + fixture, err := os.ReadFile(testProjectFixture) + require.NoError(t, err) + require.NoError(t, os.WriteFile( // #nosec G703 -- t.TempDir()-derived path + filepath.Join(root, "secrets.enc.yaml"), + fixture, + 0o600, + )) + + return &fakeWorkspaceClient{ + config: &provider.Workspace{Source: provider.WorkspaceSource{LocalFolder: root}}, + } +} + +func TestPrepareResolvedWorkspaceSecrets_DiscoversLocalProjectSecrets(t *testing.T) { + t.Setenv("SOPS_AGE_KEY", testProjectAgeIdentity) + client := newTestProjectWorkspace(t) + + cmd := &UpCmd{} + err := cmd.prepareResolvedWorkspaceSecrets(context.Background(), testConfig(), client) + require.NoError(t, err) + + require.Contains(t, cmd.SecretsEnv, "SOPS_E2E_SECRET="+testProjectPlaintext) +} + +func newFailingProjectWorkspace(t *testing.T) *fakeWorkspaceClient { + t.Helper() + root := t.TempDir() + devcontainerDir := filepath.Join(root, ".devcontainer") + require.NoError(t, os.MkdirAll(devcontainerDir, 0o750)) + require.NoError(t, os.WriteFile( + filepath.Join(devcontainerDir, "devcontainer.json"), + []byte(`{ + "customizations": { + "devsy": { + "secretSources": [ + { + "name": "project", + "type": "sops", + "path": "nonexistent.enc.yaml" + } + ], + "secrets": [ + "sops:project/MISSING" + ] + } + } +}`), + 0o600, + )) + return &fakeWorkspaceClient{ + config: &provider.Workspace{ + ID: "my-test-workspace", + Source: provider.WorkspaceSource{LocalFolder: root}, + }, + } +} + +func TestPrepareClient_InteractiveResolutionPreservesExistingWorkspaceOnError(t *testing.T) { + client := newFailingProjectWorkspace(t) + cmd := &UpCmd{ + GlobalFlags: &flags.GlobalFlags{}, + resolveWorkspace: func( + context.Context, + *config.Config, + workspace.ResolveParams, + ) (client2.BaseWorkspaceClient, error) { + return client, nil + }, + } + + _, err := cmd.prepareClient(context.Background(), testConfig(), nil) + require.Error(t, err) + require.False( + t, + client.deleted, + "interactively resolved existing workspace must not be deleted on secret error", + ) +} + +func TestPrepareClient_NewWorkspaceCleanedUpOnError(t *testing.T) { + client := newFailingProjectWorkspace(t) + cmd := &UpCmd{ + GlobalFlags: &flags.GlobalFlags{}, + resolveWorkspace: func( + context.Context, + *config.Config, + workspace.ResolveParams, + ) (client2.BaseWorkspaceClient, error) { + return client, nil + }, + } + + _, err := cmd.prepareClient( + context.Background(), + testConfig(), + []string{filepath.Join(t.TempDir(), "new-workspace")}, + ) + require.Error(t, err) + require.True( + t, + client.deleted, + "newly created workspace must be cleaned up on secret error", + ) +} + +func setupTestGitRepoWithDevContainer( + t *testing.T, + dir string, +) (func(args ...string) string, string) { + t.Helper() + runGit := func(args ...string) string { + c := exec.Command( + "git", + args...) // #nosec G204 -- test runner helper with controlled arguments + c.Dir = dir + c.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null") + out, err := c.CombinedOutput() + require.NoError(t, err, string(out)) + return strings.TrimSpace(string(out)) + } + + runGit("init", "-b", "main") + runGit("config", "user.name", "Test") + runGit("config", "user.email", "test@example.com") + + devcontainerDir := filepath.Join(dir, ".devcontainer") + require.NoError(t, os.MkdirAll(devcontainerDir, 0o750)) + require.NoError(t, os.WriteFile( + filepath.Join(devcontainerDir, "devcontainer.json"), + []byte(`{ + "customizations": { + "devsy": { + "secretSources": [ + {"name": "project", "type": "sops", "path": "secrets.enc.yaml"} + ], + "secrets": ["sops:project/SOPS_E2E_SECRET"] + } + } +}`), + 0o600, + )) + fixture, err := os.ReadFile(testProjectFixture) + require.NoError(t, err) + require.NoError( + t, + os.WriteFile(filepath.Join(dir, "secrets.enc.yaml"), fixture, 0o600), + ) // #nosec G703 -- test temp dir + runGit("add", ".") + runGit("commit", "-m", "initial commit with secrets") + commitA := runGit("rev-parse", "HEAD") + return runGit, commitA +} + +func TestDiscoverRemoteProjectSecrets_PinsImmutableCommitSHA(t *testing.T) { + t.Setenv("SOPS_AGE_KEY", testProjectAgeIdentity) + gitDir := t.TempDir() + runGit, commitA := setupTestGitRepoWithDevContainer(t, gitDir) + + source := &provider.WorkspaceSource{ + GitRepository: gitDir, + GitBranch: "main", + } + + cmd := &UpCmd{} + projectCtx, err := cmd.discoverProjectSecrets(context.Background(), source) + require.NoError(t, err) + require.NotNil(t, projectCtx) + + // Verify immutable SHA pinning (99A.1) + require.Equal(t, commitA, source.GitCommit, "inspected source must be pinned to commit A") + + // Advance the branch to commit B + require.NoError( + t, + os.WriteFile(filepath.Join(gitDir, "dummy.txt"), []byte("advance branch"), 0o600), + ) + runGit("add", "dummy.txt") + runGit("commit", "-m", "advance branch to commit B") + commitB := runGit("rev-parse", "HEAD") + require.NotEqual(t, commitA, commitB) + + // Rediscover using the source that has GitCommit pinned to commitA + projectCtx2, err := cmd.discoverProjectSecrets(context.Background(), source) + require.NoError(t, err) + require.NotNil(t, projectCtx2) + require.Equal(t, commitA, source.GitCommit, "rediscovered source must remain pinned to commitA") +} + +func TestDiscoverProjectSecrets_RejectsAbsolutePaths(t *testing.T) { + root := t.TempDir() + devcontainerDir := filepath.Join(root, ".devcontainer") + require.NoError(t, os.MkdirAll(devcontainerDir, 0o750)) + require.NoError(t, os.WriteFile( + filepath.Join(devcontainerDir, "devcontainer.json"), + []byte(`{ + "customizations": { + "devsy": { + "secretSources": [ + {"name": "project", "type": "sops", "path": "/secrets.enc.yaml"} + ], + "secrets": ["sops:project/SOPS_E2E_SECRET"] + } + } +}`), + 0o600, + )) + + source := &provider.WorkspaceSource{LocalFolder: root} + cmd := &UpCmd{} + _, err := cmd.discoverProjectSecrets(context.Background(), source) + require.Error(t, err) + require.Contains(t, err.Error(), "must be relative to the repository root") +} diff --git a/cmd/workspace/up/secrets_test.go b/cmd/workspace/up/secrets_test.go index 1a7bb37a0..ada71fdee 100644 --- a/cmd/workspace/up/secrets_test.go +++ b/cmd/workspace/up/secrets_test.go @@ -1,10 +1,12 @@ package up import ( + "context" "sort" "testing" "github.com/devsy-org/devsy/pkg/config" + secretspkg "github.com/devsy-org/devsy/pkg/secrets" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -13,6 +15,7 @@ const ( secretAPIKey = "API_KEY" secretToken = "TOKEN" secretTLSKey = "TLS_KEY" + testNPMToken = "NPM_TOKEN" ) func testConfig(bound ...string) *config.Config { @@ -24,8 +27,16 @@ func testConfig(bound ...string) *config.Config { } } +func localRef(name string) secretspkg.SecretRef { + return secretspkg.SecretRef{ + Type: secretspkg.LocalSourceName, + Source: secretspkg.LocalSourceName, + Name: name, + } +} + func sortedRequests(reqs []secretRequest) []secretRequest { - sort.Slice(reqs, func(i, j int) bool { return reqs[i].name < reqs[j].name }) + sort.Slice(reqs, func(i, j int) bool { return reqs[i].ref.String() < reqs[j].ref.String() }) return reqs } @@ -33,82 +44,109 @@ func TestCollectSecretRequests_FlagsDefaultToEnv(t *testing.T) { got, err := collectSecretRequests( []string{secretAPIKey, "DB_PW,target=DATABASE_PASSWORD"}, testConfig(), + nil, ) require.NoError(t, err) assert.Equal(t, []secretRequest{ - {name: secretAPIKey, target: secretAPIKey, mount: false}, - {name: "DB_PW", target: "DATABASE_PASSWORD", mount: false}, + {ref: localRef(secretAPIKey), target: secretAPIKey, mount: false}, + {ref: localRef("DB_PW"), target: "DATABASE_PASSWORD", mount: false}, }, sortedRequests(got)) } -func TestCollectSecretRequests_MountType(t *testing.T) { - got, err := collectSecretRequests([]string{secretTLSKey + ",type=mount"}, testConfig()) - require.NoError(t, err) - assert.Equal(t, []secretRequest{ - {name: secretTLSKey, target: secretTLSKey, mount: true}, - }, got) -} - -func TestCollectSecretRequests_MountWithTarget(t *testing.T) { +func TestCollectSecretRequests_SourceQualified(t *testing.T) { got, err := collectSecretRequests( - []string{secretTLSKey + ",type=mount,target=tls.key"}, + []string{"sops:project/API_KEY,type=mount,target=token"}, testConfig(), + nil, ) require.NoError(t, err) + require.Len(t, got, 1) + assert.Equal(t, "sops:project/API_KEY", got[0].ref.String()) + assert.Equal(t, "token", got[0].target) + assert.True(t, got[0].mount) +} + +func TestCollectSecretRequests_ProjectBindings(t *testing.T) { + got, err := collectSecretRequests(nil, testConfig(), []string{"sops:project/API_KEY"}) + require.NoError(t, err) + require.Len(t, got, 1) + assert.Equal(t, "sops:project/API_KEY", got[0].ref.String()) + assert.Equal(t, "API_KEY", got[0].target) +} + +func TestCollectSecretRequests_MountType(t *testing.T) { + got, err := collectSecretRequests([]string{secretTLSKey + ",type=mount"}, testConfig(), nil) + require.NoError(t, err) assert.Equal(t, []secretRequest{ - {name: secretTLSKey, target: "tls.key", mount: true}, + {ref: localRef(secretTLSKey), target: secretTLSKey, mount: true}, }, got) } func TestCollectSecretRequests_ContextBindings(t *testing.T) { - got, err := collectSecretRequests(nil, testConfig(secretToken, "SECRET_TWO")) + got, err := collectSecretRequests(nil, testConfig(secretToken, "SECRET_TWO"), nil) require.NoError(t, err) assert.Equal(t, []secretRequest{ - {name: "SECRET_TWO", target: "SECRET_TWO", mount: false}, - {name: secretToken, target: secretToken, mount: false}, + {ref: localRef("SECRET_TWO"), target: "SECRET_TWO", mount: false}, + {ref: localRef(secretToken), target: secretToken, mount: false}, }, sortedRequests(got)) } func TestCollectSecretRequests_FlagOverridesBinding(t *testing.T) { - got, err := collectSecretRequests([]string{"TOKEN,target=CI_TOKEN"}, testConfig(secretToken)) + got, err := collectSecretRequests( + []string{"TOKEN,target=CI_TOKEN"}, + testConfig(secretToken), + nil, + ) require.NoError(t, err) assert.Equal(t, []secretRequest{ - {name: secretToken, target: "CI_TOKEN", mount: false}, + {ref: localRef(secretToken), target: "CI_TOKEN", mount: false}, }, got) } func TestCollectSecretRequests_Empty(t *testing.T) { - got, err := collectSecretRequests(nil, testConfig()) + got, err := collectSecretRequests(nil, testConfig(), nil) require.NoError(t, err) assert.Empty(t, got) } func TestCollectSecretRequests_InvalidType(t *testing.T) { - _, err := collectSecretRequests([]string{"A,type=bogus"}, testConfig()) + _, err := collectSecretRequests([]string{"A,type=bogus"}, testConfig(), nil) require.Error(t, err) } func TestCollectSecretRequests_InvalidOption(t *testing.T) { - _, err := collectSecretRequests([]string{"A,bogus"}, testConfig()) + _, err := collectSecretRequests([]string{"A,bogus"}, testConfig(), nil) require.Error(t, err) } func TestCollectSecretRequests_DuplicateMountTargetRejected(t *testing.T) { _, err := collectSecretRequests( - []string{"A,type=mount,target=tok", "B,type=mount,target=tok"}, + []string{"A,type=mount,target=tok", "sops:project/B,type=mount,target=tok"}, testConfig(), + nil, ) require.Error(t, err) assert.Contains(t, err.Error(), "both mount to target") } +type fixedSource struct { + values map[string]string + sensitive bool +} + +func (s fixedSource) Get(_ context.Context, name string) (secretspkg.ResolvedSecret, error) { + return secretspkg.ResolvedSecret{Name: name, Value: s.values[name], Sensitive: s.sensitive}, nil +} + func TestApplyEnvVars_RejectsSensitiveSecret(t *testing.T) { cmd := &UpCmd{} cmd.EnvVars = []string{secretAPIKey} - get := func(string) (string, error) { return "plaintext", nil } - isSensitive := func(string) (bool, error) { return true, nil } + resolver := secretspkg.NewResolver() + require.NoError(t, resolver.Register("local", "local", fixedSource{ + values: map[string]string{secretAPIKey: "plaintext"}, sensitive: true, + })) - err := cmd.applyEnvVars(get, isSensitive) + err := cmd.applyEnvVars(context.Background(), resolver) require.Error(t, err) assert.Contains(t, err.Error(), "is a secret and cannot be passed with --env") assert.Empty(t, cmd.WorkspaceEnv, "no value must reach the ps-visible WorkspaceEnv") @@ -117,10 +155,42 @@ func TestApplyEnvVars_RejectsSensitiveSecret(t *testing.T) { func TestApplyEnvVars_AllowsNonSensitive(t *testing.T) { cmd := &UpCmd{} cmd.EnvVars = []string{"LOG_LEVEL=DEBUG_TARGET"} - get := func(string) (string, error) { return "debug", nil } - isSensitive := func(string) (bool, error) { return false, nil } + resolver := secretspkg.NewResolver() + require.NoError(t, resolver.Register("local", "local", fixedSource{ + values: map[string]string{"LOG_LEVEL": "debug"}, sensitive: false, + })) - err := cmd.applyEnvVars(get, isSensitive) + err := cmd.applyEnvVars(context.Background(), resolver) require.NoError(t, err) assert.Equal(t, []string{"DEBUG_TARGET=debug"}, cmd.WorkspaceEnv) } + +func TestApplyBuildSecretsUsesUnqualifiedKeyAsID(t *testing.T) { + cmd := &UpCmd{} + cmd.BuildSecretNames = []string{"sops:project/" + testNPMToken} + resolver := secretspkg.NewResolver() + require.NoError(t, resolver.Register("project", "sops", fixedSource{ + values: map[string]string{testNPMToken: "secret"}, sensitive: true, + })) + + err := cmd.applyBuildSecrets(context.Background(), resolver) + require.NoError(t, err) + assert.Equal(t, []string{testNPMToken + "=secret"}, cmd.BuildSecrets) +} + +func TestApplyBuildSecretsRejectsDuplicateIDs(t *testing.T) { + cmd := &UpCmd{} + cmd.BuildSecretNames = []string{"sops:project/" + testNPMToken, "sops:other/" + testNPMToken} + resolver := secretspkg.NewResolver() + require.NoError(t, resolver.Register("project", "sops", fixedSource{ + values: map[string]string{testNPMToken: "secret-a"}, sensitive: true, + })) + require.NoError(t, resolver.Register("other", "sops", fixedSource{ + values: map[string]string{testNPMToken: "secret-b"}, sensitive: true, + })) + + err := cmd.applyBuildSecrets(context.Background(), resolver) + require.Error(t, err) + assert.Contains(t, err.Error(), "both use BuildKit id \""+testNPMToken+"\"") + assert.Empty(t, cmd.BuildSecrets) +} diff --git a/cmd/workspace/up/testdata/sops-project-secrets.enc.yaml b/cmd/workspace/up/testdata/sops-project-secrets.enc.yaml new file mode 100644 index 000000000..79a5fdfd8 --- /dev/null +++ b/cmd/workspace/up/testdata/sops-project-secrets.enc.yaml @@ -0,0 +1,17 @@ +SOPS_E2E_SECRET: ENC[AES256_GCM,data:7/LkTGbUbM24m2YegXZhCPwFErYDhnRC3jflQA==,iv:jox9Lg827lNean0A3dbFnjbizHq/+e5Oj41ntzFKtNc=,tag:H0XcdWPIYBmte+v0F9j47Q==,type:str] +TLS_KEY: ENC[AES256_GCM,data:7qUD3Paq3RT0XbF/qvSnfw==,iv:ClkYTnncQRPn3JhDZ9iZfuY0lNDYY71ot6HRjF2HCVY=,tag:oGoczTS56kPXL9N4/FY+nA==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwWHBIUkxaRWlsdFRidWFF + OHdHTHZGZUh2UGRzOXhVdFlxRnZIZkY3eTFnCnpoTFNrNVZKcFRtTVhjbnBUK1c1 + SGFnd2R0ZGQvOHFueWpHR0xiOGVvdTAKLS0tIDJTSVFCazNueGJkMlZDUU9vK21j + d0xPTEV4NVZGL1EweHBqM3lTSG9sb3MKJtSnyqRMtWM2WRIEJSHQPkYtBXa+wSpG + PXamDLnUGIrzsjbl6uTZxHytVSwfVERwWzagJTEOajIJpFwHIyGD/A== + -----END AGE ENCRYPTED FILE----- + recipient: age1h84qv0p8kdwfp6vrag4nvmpd6gt3mde9g6dcnj9uryh3qcl50uxq83n932 + lastmodified: "2026-09-03T05:38:40Z" + mac: ENC[AES256_GCM,data:5thaKGP8s7dbN/9VvRn54TB0R6UaRlRvtyoby0yn5gGlCAYZLvBlAXmTY8AQ0ShQII3ictP2KpXVLGd0dp0H0LXGr9PpsdXIVEO5t/WS5YO+ZhdxZ5hgWBzEL1rZ2VO9Bt0DPFmEYNNryqIpLU64lbd+AO5P/6/uaBMZZERYE0Y=,iv:7nUHdzpyZidt2DRQdvSv2W2GCF5JArFzvyX9ppVEP5k=,tag:/KTQAguMfRBpWPDumiTunA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3 diff --git a/cmd/workspace/up/up.go b/cmd/workspace/up/up.go index 53845ef6b..93d92941c 100644 --- a/cmd/workspace/up/up.go +++ b/cmd/workspace/up/up.go @@ -65,6 +65,13 @@ type UpCmd struct { Out io.Writer // Set at the start of Run; nil until then. statusReporter status.Reporter + + // resolveWorkspace overrides workspace.Resolve; nil falls back to workspace.Resolve. + resolveWorkspace func( + ctx context.Context, + devsyConfig *config.Config, + params workspace.ResolveParams, + ) (client2.BaseWorkspaceClient, error) } // Options is the structured input form of the up command. @@ -123,7 +130,22 @@ func RunHeadless( if err := cmd.validate(); err != nil { return nil, err } - if err := cmd.prepareSecrets(opts.DevsyConfig); err != nil { + var ( + project *projectSecretContext + err error + ) + if cfg := client.WorkspaceConfig(); cfg != nil { + // Bootstrap credentials must be resolved before repository-owned + // secret sources are discovered + if err := cmd.prepareBootstrapGitToken(ctx, opts.DevsyConfig, &cfg.Source); err != nil { + return nil, err + } + project, err = cmd.discoverProjectSecrets(ctx, &cfg.Source) + if err != nil { + return nil, err + } + } + if err := cmd.prepareSecretsWithProject(ctx, opts.DevsyConfig, project); err != nil { return nil, err } cmd.prepareWorkspace(client) diff --git a/cmd/workspace/up/up_client.go b/cmd/workspace/up/up_client.go index c49c22c99..6da5f81c5 100644 --- a/cmd/workspace/up/up_client.go +++ b/cmd/workspace/up/up_client.go @@ -12,6 +12,7 @@ import ( "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" config2 "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/file" "github.com/devsy-org/devsy/pkg/log" options2 "github.com/devsy-org/devsy/pkg/options" provider2 "github.com/devsy-org/devsy/pkg/provider" @@ -64,29 +65,29 @@ func (cmd *UpCmd) prepareClient( devsyConfig *config.Config, args []string, ) (client2.BaseWorkspaceClient, error) { - if err := mergeDevsyUpOptions(&cmd.CLIOptions); err != nil { - return nil, err - } - if cmd.Platform.Enabled { - log.Debug("running in platform mode") - log.Debug("using error output stream") - config.MergeContextOptions(devsyConfig.Current(), os.Environ()) - } - if err := cmd.prepareSecrets(devsyConfig); err != nil { - return nil, err - } - if err := cmd.validateFromSnapshot(ctx, args); err != nil { + if err := cmd.prepareClientEnvironment(devsyConfig, args, ctx); err != nil { return nil, err } + source, err := cmd.parseWorkspaceSource() if err != nil { return nil, err } + if err := cmd.prepareBootstrapGitToken(ctx, devsyConfig, source); err != nil { + return nil, err + } + cmd.resolveSSHConfig(devsyConfig) - args = cmd.ensureArgsForFromSnapshot(args) + args = cmd.ensureArgs(args) + + existed := cmd.checkWorkspaceExisted(ctx, devsyConfig, args) - log.Debugf("up: resolving workspace with cmd.IDE=%q ide-launch=%q", cmd.IDE, cmd.IDELaunch) - client, err := workspace2.Resolve( + log.Debugf("resolving workspace with ide=%q launch=%q", cmd.IDE, cmd.IDELaunch) + resolver := cmd.resolveWorkspace + if resolver == nil { + resolver = workspace2.Resolve + } + client, err := resolver( ctx, devsyConfig, cmd.resolveParams(args, source, devsyConfig), @@ -94,12 +95,84 @@ func (cmd *UpCmd) prepareClient( if err != nil { return nil, err } - if err := cmd.checkProviderUpdate(ctx, devsyConfig, client); err != nil { + + if err := cmd.postResolveWorkspace(ctx, devsyConfig, client, existed); err != nil { return nil, err } return client, nil } +func (cmd *UpCmd) checkWorkspaceExisted( + ctx context.Context, + devsyConfig *config.Config, + args []string, +) bool { + switch { + case cmd.ID != "": + return workspace2.Exists(ctx, devsyConfig, nil, cmd.ID, cmd.Owner) != "" + case len(args) > 0: + _, name := file.IsLocalDir(args[0]) + return workspace2.Exists(ctx, devsyConfig, nil, workspace2.ToID(name), cmd.Owner) != "" + default: + return true + } +} + +func (cmd *UpCmd) postResolveWorkspace( + ctx context.Context, + devsyConfig *config.Config, + client client2.BaseWorkspaceClient, + existed bool, +) error { + cleanupOnFailure := func(err error) error { + if !existed { + _ = client.Delete(ctx, client2.DeleteOptions{Force: true, IgnoreNotFound: true}) + } + return err + } + if err := cmd.prepareResolvedWorkspaceSecrets(ctx, devsyConfig, client); err != nil { + return cleanupOnFailure(err) + } + if err := cmd.checkProviderUpdate(ctx, devsyConfig, client); err != nil { + return cleanupOnFailure(err) + } + return nil +} + +func (cmd *UpCmd) prepareClientEnvironment( + devsyConfig *config.Config, + args []string, + ctx context.Context, +) error { + if err := mergeDevsyUpOptions(&cmd.CLIOptions); err != nil { + return err + } + if cmd.Platform.Enabled { + log.Debug("running in platform mode") + config.MergeContextOptions(devsyConfig.Current(), os.Environ()) + } + return cmd.validateFromSnapshot(ctx, args) +} + +// prepareResolvedWorkspaceSecrets discovers repository-owned project secrets +// using the workspace's resolved source and merges them with attached/explicit +// secrets. This must run after workspace2.Resolve. +func (cmd *UpCmd) prepareResolvedWorkspaceSecrets( + ctx context.Context, + devsyConfig *config.Config, + client client2.BaseWorkspaceClient, +) error { + var source *provider2.WorkspaceSource + if cfg := client.WorkspaceConfig(); cfg != nil { + source = &cfg.Source + } + projectSecrets, err := cmd.discoverProjectSecrets(ctx, source) + if err != nil { + return err + } + return cmd.prepareSecretsWithProject(ctx, devsyConfig, projectSecrets) +} + // checkProviderUpdate checks for a provider update, unless running in platform mode. func (cmd *UpCmd) checkProviderUpdate( ctx context.Context, @@ -113,22 +186,24 @@ func (cmd *UpCmd) checkProviderUpdate( return workspace2.CheckProviderUpdate(ctx, devsyConfig, proInstance) } -// ensureArgsForFromSnapshot returns args unchanged unless --from-snapshot is +// ensureArgs returns args unchanged unless either --from-snapshot or --source is // set and args is empty, in which case it synthesizes a placeholder arg. -// resolveWorkspace only takes its create-new-workspace path when args is -// non-empty (see pkg/workspace.resolveWorkspace); --from-snapshot forbids a -// positional source (validateFromSnapshot), so without this, a -// --from-snapshot restore into a workspace id that doesn't exist yet always -// fails with "doesn't exist" instead of creating it. The synthesized value -// itself is never read on this path: DesiredID and Source (both already set -// by resolveExplicitSource) take priority over it. -func (cmd *UpCmd) ensureArgsForFromSnapshot(args []string) []string { - if cmd.FromSnapshot != "" && len(args) == 0 { - return []string{cmd.FromSnapshot} +func (cmd *UpCmd) ensureArgs(args []string) []string { + if len(args) == 0 { + if cmd.FromSnapshot != "" { + return []string{cmd.FromSnapshot} + } + if cmd.Source != "" { + return []string{cmd.Source} + } } return args } +func (cmd *UpCmd) ensureArgsForFromSnapshot(args []string) []string { + return cmd.ensureArgs(args) +} + func (cmd *UpCmd) resolveParams( args []string, source *provider2.WorkspaceSource, devsyConfig *config.Config, ) workspace2.ResolveParams { @@ -154,7 +229,11 @@ func (cmd *UpCmd) resolveParams( } } -func (cmd *UpCmd) prepareSecrets(devsyConfig *config.Config) error { +func (cmd *UpCmd) prepareSecretsWithProject( + ctx context.Context, + devsyConfig *config.Config, + project *projectSecretContext, +) error { if err := mergeEnvFromFiles(&cmd.CLIOptions); err != nil { return err } @@ -169,7 +248,7 @@ func (cmd *UpCmd) prepareSecrets(devsyConfig *config.Config) error { } } - if err := cmd.resolveStoredSecrets(devsyConfig); err != nil { + if err := cmd.resolveStoredSecrets(ctx, devsyConfig, project); err != nil { return err } @@ -189,141 +268,180 @@ func (cmd *UpCmd) prepareSecrets(devsyConfig *config.Config) error { return nil } -func (cmd *UpCmd) resolveStoredSecrets(devsyConfig *config.Config) error { - requests, err := collectSecretRequests(cmd.Secrets, devsyConfig) +func (cmd *UpCmd) prepareBootstrapGitToken( + ctx context.Context, + devsyConfig *config.Config, + source *provider2.WorkspaceSource, +) error { + if cmd.GitTokenSecret == "" { + return nil + } + resolver, err := secrets.NewResolverForConfig(devsyConfig) if err != nil { return err } - if !cmd.hasStoredValues(requests) { - return nil + resolved, err := validateBootstrapSecretReference(ctx, resolver, cmd.GitTokenSecret) + if err != nil { + return err } - - store, err := secrets.NewStoreForConfig(devsyConfig) + gitToken, err := cmd.buildGitTokenForSource(resolved.Value, source) if err != nil { return err } - r := secretResolver{store: store, context: devsyConfig.DefaultContext} + cmd.GitToken = gitToken + return nil +} - if err := cmd.applyLifecycleSecrets(requests, r.get); err != nil { +func (cmd *UpCmd) resolveStoredSecrets( + ctx context.Context, + devsyConfig *config.Config, + project *projectSecretContext, +) error { + requests, err := collectSecretRequests(cmd.Secrets, devsyConfig, project.attachedSecrets()) + if err != nil { return err } - if err := cmd.applyEnvVars(r.get, r.sensitive); err != nil { + if !cmd.hasStoredValues(requests) { + return nil + } + + resolver, err := secrets.NewResolverForConfig(devsyConfig) + if err != nil { return err } - if err := cmd.applyBuildSecrets(r.get); err != nil { + if err := project.register(resolver); err != nil { return err } - return cmd.applyGitToken(r.get) -} - -type secretResolver struct { - store secrets.Store - context string -} -func (r secretResolver) get(name string) (string, error) { - value, err := r.store.Get(r.context, name) - if err != nil { - return "", fmt.Errorf("resolve secret %q in context %q: %w", name, r.context, err) + if err := cmd.applyLifecycleSecrets(ctx, requests, resolver); err != nil { + return err } - return value, nil -} - -func (r secretResolver) sensitive(name string) (bool, error) { - meta, err := r.store.Meta(r.context, name) - if err != nil { - return false, fmt.Errorf("resolve secret %q in context %q: %w", name, r.context, err) + if err := cmd.applyEnvVars(ctx, resolver); err != nil { + return err } - return meta.Sensitive(), nil + return cmd.applyBuildSecrets(ctx, resolver) } func (cmd *UpCmd) hasStoredValues(requests []secretRequest) bool { - return len(requests) > 0 || len(cmd.EnvVars) > 0 || - len(cmd.BuildSecretNames) > 0 || cmd.GitTokenSecret != "" + return len(requests) > 0 || len(cmd.EnvVars) > 0 || len(cmd.BuildSecretNames) > 0 } func (cmd *UpCmd) applyLifecycleSecrets( + ctx context.Context, requests []secretRequest, - get func(string) (string, error), + resolver *secrets.Resolver, ) error { for _, req := range requests { - value, err := get(req.name) + resolved, err := resolver.Resolve(ctx, req.ref) if err != nil { return err } if req.mount { - cmd.SecretsMount = append(cmd.SecretsMount, req.target+"="+value) + cmd.SecretsMount = append(cmd.SecretsMount, req.target+"="+resolved.Value) } else { - cmd.SecretsEnv = append(cmd.SecretsEnv, req.target+"="+value) + cmd.SecretsEnv = append(cmd.SecretsEnv, req.target+"="+resolved.Value) } } return nil } -// applyEnvVars resolves --env entries into WorkspaceEnv. A sensitive secret must -// never be routed here: WorkspaceEnv rides in the setup argv (ps-visible). -func (cmd *UpCmd) applyEnvVars( - get func(string) (string, error), - isSensitive func(string) (bool, error), -) error { +// applyEnvVars resolves --env entries from the local store. External +// sensitive sources intentionally use --secret instead: WorkspaceEnv rides in +// the setup argv and is process-list visible. +func (cmd *UpCmd) applyEnvVars(ctx context.Context, resolver *secrets.Resolver) error { for _, entry := range cmd.EnvVars { - name, target, ok := strings.Cut(entry, "=") - if !ok { - target = name - } else if target == "" { - return fmt.Errorf("invalid --env %q: target after %q= must not be empty", entry, name) - } - sensitive, err := isSensitive(name) + envVar, err := resolveEnvVarEntry(ctx, resolver, entry) if err != nil { return err } - if sensitive { - return fmt.Errorf( - "%q is a secret and cannot be passed with --env (it would be visible in the process list); use --secret %s instead", - name, - name, - ) - } - value, err := get(name) - if err != nil { - return err - } - cmd.WorkspaceEnv = append(cmd.WorkspaceEnv, target+"="+value) + cmd.WorkspaceEnv = append(cmd.WorkspaceEnv, envVar) } return nil } -func (cmd *UpCmd) applyBuildSecrets(get func(string) (string, error)) error { - for _, name := range cmd.BuildSecretNames { - value, err := get(name) - if err != nil { - return err - } - cmd.BuildSecrets = append(cmd.BuildSecrets, name+"="+value) +func resolveEnvVarEntry( + ctx context.Context, + resolver *secrets.Resolver, + entry string, +) (string, error) { + name, explicitTarget, hasTarget := strings.Cut(entry, "=") + if hasTarget && explicitTarget == "" { + return "", fmt.Errorf("invalid --env %q: target after %q= must not be empty", entry, name) + } + ref, err := secrets.ParseRef(name) + if err != nil { + return "", err } - return nil + target := ref.Name + if hasTarget { + target = explicitTarget + } + if ref.Source != secrets.LocalSourceName { + return "", fmt.Errorf( + "--env only accepts Devsy-managed values; use --secret %s instead", + name, + ) + } + resolved, err := resolver.Resolve(ctx, ref) + if err != nil { + return "", err + } + if resolved.Sensitive { + return "", fmt.Errorf( + "%q is a secret and cannot be passed with --env (it would be visible in the process list); use --secret %s instead", + name, + name, + ) + } + return target + "=" + resolved.Value, nil } -func (cmd *UpCmd) applyGitToken(get func(string) (string, error)) error { - if cmd.GitTokenSecret == "" { +func (cmd *UpCmd) applyBuildSecrets(ctx context.Context, resolver *secrets.Resolver) error { + if len(cmd.BuildSecretNames) == 0 { return nil } - token, err := get(cmd.GitTokenSecret) - if err != nil { - return err + seen := map[string]string{} + for _, existing := range cmd.BuildSecrets { + if k, _, ok := strings.Cut(existing, "="); ok { + seen[k] = existing + } } - gitToken, err := cmd.buildGitToken(token) - if err != nil { - return err + built := append([]string(nil), cmd.BuildSecrets...) + for _, value := range cmd.BuildSecretNames { + ref, err := secrets.ParseRef(value) + if err != nil { + return err + } + if other, dup := seen[ref.Name]; dup { + return fmt.Errorf( + "build secrets %q and %q both use BuildKit id %q", + other, + ref.String(), + ref.Name, + ) + } + seen[ref.Name] = ref.String() + resolved, err := resolver.Resolve(ctx, ref) + if err != nil { + return err + } + built = append(built, ref.Name+"="+resolved.Value) } - cmd.GitToken = gitToken + cmd.BuildSecrets = built return nil } -// buildGitToken host-scopes the token; it fails on an unresolvable host so the -// token is never left unscoped. -func (cmd *UpCmd) buildGitToken(token string) (*provider2.GitToken, error) { - host := gitHostFromSource(cmd.Source) +func (cmd *UpCmd) buildGitTokenForSource( + token string, + source *provider2.WorkspaceSource, +) (*provider2.GitToken, error) { + host := "" + if source != nil && source.GitRepository != "" { + host = gitHostFromSource(source.GitRepository) + } + if host == "" { + host = gitHostFromSource(cmd.Source) + } if host == "" { return nil, fmt.Errorf( "cannot use --git-token: workspace source %q has no HTTP(S) host to scope the token to", @@ -334,12 +452,11 @@ func (cmd *UpCmd) buildGitToken(token string) (*provider2.GitToken, error) { if username == "" { username = gitTokenUsernameForHost(host) } - return &provider2.GitToken{Host: host, Username: username, Token: token}, nil } // gitHostFromSource returns the host of an HTTP(S) git source, or "". A git -// token is only usable over HTTP(S), so non-HTTP(S) schemes (e.g. ssh) yield "". +// token is only usable over HTTP(S), so non-HTTP(S) schemes yield "". func gitHostFromSource(source string) string { s := strings.TrimPrefix(source, "git:") u, err := url.Parse(s) @@ -360,65 +477,112 @@ func gitTokenUsernameForHost(host string) string { } type secretRequest struct { - name string + ref secrets.SecretRef target string mount bool } -// collectSecretRequests merges context bindings with secret flags; flags override bindings. -func collectSecretRequests(flags []string, devsyConfig *config.Config) ([]secretRequest, error) { +// collectSecretRequests merges context/project bindings with secret flags; +// explicit flags override automatic bindings for the same reference. +func collectSecretRequests( + flags []string, + devsyConfig *config.Config, + projectSecrets []string, +) ([]secretRequest, error) { byName := map[string]secretRequest{} + var attached []string if ctxConfig := devsyConfig.Contexts[devsyConfig.DefaultContext]; ctxConfig != nil { - for _, name := range ctxConfig.Secrets { - byName[name] = secretRequest{name: name, target: name} + attached = ctxConfig.Secrets + } + if err := addSecretBindings(byName, attached, "attached"); err != nil { + return nil, err + } + if err := addSecretBindings(byName, projectSecrets, "project"); err != nil { + return nil, err + } + if err := addSecretFlags(byName, flags); err != nil { + return nil, err + } + + requests := sortedSecretRequests(byName) + if err := checkDuplicateTargets(requests); err != nil { + return nil, err + } + return requests, nil +} + +func addSecretBindings( + byName map[string]secretRequest, + values []string, + kind string, +) error { + for _, value := range values { + ref, err := secrets.ParseRef(value) + if err != nil { + return fmt.Errorf("invalid %s secret %q: %w", kind, value, err) } + byName[ref.String()] = secretRequest{ref: ref, target: ref.Name} } + return nil +} - for _, entry := range flags { - req, err := parseSecretFlag(entry) +func addSecretFlags(byName map[string]secretRequest, values []string) error { + for _, value := range values { + req, err := parseSecretFlag(value) if err != nil { - return nil, err + return err } - byName[req.name] = req + byName[req.ref.String()] = req } + return nil +} +func sortedSecretRequests(byName map[string]secretRequest) []secretRequest { requests := make([]secretRequest, 0, len(byName)) for _, req := range byName { requests = append(requests, req) } - sort.Slice(requests, func(i, j int) bool { return requests[i].name < requests[j].name }) - - if err := checkDuplicateMountTargets(requests); err != nil { - return nil, err - } - - return requests, nil + sort.Slice(requests, func(i, j int) bool { + return requests[i].ref.String() < requests[j].ref.String() + }) + return requests } -func checkDuplicateMountTargets(requests []secretRequest) error { - targets := map[string]string{} +func checkDuplicateTargets(requests []secretRequest) error { + envTargets := map[string]string{} + mountTargets := map[string]string{} for _, req := range requests { - if !req.mount { - continue - } - if other, dup := targets[req.target]; dup { - return fmt.Errorf( - "secrets %q and %q both mount to target %q; give one a distinct target=", - other, req.name, req.target, - ) + if req.mount { + if other, dup := mountTargets[req.target]; dup { + return fmt.Errorf( + "secrets %q and %q both mount to target %q; give one a distinct target=", + other, req.ref.String(), req.target, + ) + } + mountTargets[req.target] = req.ref.String() + } else { + if other, dup := envTargets[req.target]; dup { + return fmt.Errorf( + "secrets %q and %q both inject to environment variable %q; give one a distinct target=", + other, + req.ref.String(), + req.target, + ) + } + envTargets[req.target] = req.ref.String() } - targets[req.target] = req.name } return nil } func parseSecretFlag(entry string) (secretRequest, error) { parts := strings.Split(entry, ",") - req := secretRequest{name: parts[0]} - if req.name == "" { - return secretRequest{}, fmt.Errorf("invalid secret %q: missing name", entry) + ref, err := secrets.ParseRef(parts[0]) + if err != nil { + return secretRequest{}, err } + req := secretRequest{ref: ref} for _, opt := range parts[1:] { key, value, ok := strings.Cut(opt, "=") @@ -434,9 +598,8 @@ func parseSecretFlag(entry string) (secretRequest, error) { } if req.target == "" { - req.target = req.name + req.target = req.ref.Name } - return req, nil } @@ -459,7 +622,6 @@ func (req *secretRequest) applyOption(key, value string) error { default: return fmt.Errorf("invalid secret option %q, expected type or target", key) } - return nil } @@ -488,21 +650,6 @@ func (cmd *UpCmd) parseWorkspaceSource() (*provider2.WorkspaceSource, error) { return source, nil } -// resolveExplicitSource returns an explicit WorkspaceSource when --from-snapshot -// is set, composed identically to `devsy snapshot restore` via -// snapshot.RestoreComposition ("snapshot:" source, "image::-fs" -// DevContainerSource), taking priority over positional-arg source resolution. -// It also sets cmd.DevContainerSource so the workspace runs the snapshot's -// committed filesystem image instead of rebuilding, matching restore's -// behavior exactly. -// -// Since --from-snapshot forbids a positional source (validateFromSnapshot), -// there is no other way for the workspace ID to reach ResolveParams.DesiredID -// on this path; without defaulting it here, workspace resolution falls back -// to selecting an unrelated existing workspace (or fails confusingly in -// non-TTY contexts). So when --id wasn't given explicitly, default it from -// the snapshot ref's workspace id, mirroring `devsy snapshot restore`'s -// buildWorkspace. func (cmd *UpCmd) resolveExplicitSource() (*provider2.WorkspaceSource, error) { if cmd.FromSnapshot == "" { return nil, nil @@ -531,14 +678,6 @@ func (cmd *UpCmd) resolveExplicitSource() (*provider2.WorkspaceSource, error) { return source, nil } -// validateFromSnapshot enforces --from-snapshot's invariants before workspace -// resolution: it cannot be combined with an explicit source (positional arg -// or --source) or used in platform mode (snapshots are local-only — `devsy -// snapshot create` rejects machine-provider workspaces up front, and restore -// has no remote-registry-backed equivalent of a platform-managed container), -// and the referenced snapshot must actually exist. Mirrors `devsy snapshot -// restore`'s upfront PullManifest check so a bad or missing ref fails fast -// with a clear error instead of partway through workspace creation. func (cmd *UpCmd) validateFromSnapshot(ctx context.Context, args []string) error { if cmd.FromSnapshot == "" { return nil @@ -556,10 +695,6 @@ func (cmd *UpCmd) validateFromSnapshot(ctx context.Context, args []string) error return cmd.applyFromSnapshotOverrides(manifest) } -// applyFromSnapshotOverrides replays the create-time devcontainer.json -// settings the snapshot's manifest carries (runArgs, containerEnv, -// remoteUser) onto cmd, so the image-sourced restored container behaves like -// the original did. func (cmd *UpCmd) applyFromSnapshotOverrides(manifest *snapshotpkg.Manifest) error { runArgs, err := manifest.RunArgs() if err != nil { diff --git a/cmd/workspace/up/up_client_test.go b/cmd/workspace/up/up_client_test.go index 2edeaa72e..940c4a586 100644 --- a/cmd/workspace/up/up_client_test.go +++ b/cmd/workspace/up/up_client_test.go @@ -2,10 +2,11 @@ package up import "testing" +const testSnapshotImage = "ghcr.io/acme/snapshots:my-ws-20260731150405-abcxyz" + func TestEnsureArgsForFromSnapshot_SynthesizesPlaceholderWhenArgsEmpty(t *testing.T) { cmd := &UpCmd{} - cmd.FromSnapshot = "ghcr.io/acme/snapshots:my-ws-20260731150405-abcxyz" - + cmd.FromSnapshot = testSnapshotImage args := cmd.ensureArgsForFromSnapshot(nil) if len(args) != 1 || args[0] != cmd.FromSnapshot { @@ -19,8 +20,7 @@ func TestEnsureArgsForFromSnapshot_SynthesizesPlaceholderWhenArgsEmpty(t *testin func TestEnsureArgsForFromSnapshot_LeavesNonEmptyArgsUntouched(t *testing.T) { cmd := &UpCmd{} - cmd.FromSnapshot = "ghcr.io/acme/snapshots:my-ws-20260731150405-abcxyz" - + cmd.FromSnapshot = testSnapshotImage args := cmd.ensureArgsForFromSnapshot([]string{"already-set"}) if len(args) != 1 || args[0] != "already-set" { @@ -37,3 +37,28 @@ func TestEnsureArgsForFromSnapshot_NoOpWithoutFromSnapshot(t *testing.T) { t.Errorf("args = %v, want nil (no --from-snapshot, nothing to synthesize)", args) } } + +func TestEnsureArgs_SynthesizesPlaceholderWhenSourceProvidedAndArgsEmpty(t *testing.T) { + cmd := &UpCmd{} + cmd.Source = "https://github.com/devsy-org/devsy" + + args := cmd.ensureArgs(nil) + + if len(args) != 1 || args[0] != cmd.Source { + t.Errorf( + "args = %v, want a single placeholder arg [https://github.com/devsy-org/devsy]", + args, + ) + } +} + +func TestEnsureArgs_PrefersFromSnapshotOverSourceWhenBothEmptyArgs(t *testing.T) { + cmd := &UpCmd{} + cmd.FromSnapshot = testSnapshotImage + cmd.Source = "https://github.com/devsy-org/devsy" + args := cmd.ensureArgs(nil) + + if len(args) != 1 || args[0] != cmd.FromSnapshot { + t.Errorf("args = %v, want snapshot arg", args) + } +} diff --git a/cmd/workspace/up/up_flags.go b/cmd/workspace/up/up_flags.go index 20a8675ec..23ea87dc5 100644 --- a/cmd/workspace/up/up_flags.go +++ b/cmd/workspace/up/up_flags.go @@ -224,16 +224,21 @@ func (cmd *UpCmd) registerWorkspaceSecretsFlags(upCmd *cobra.Command) { "File of env vars for the workspace"), flags.String(&cmd.SecretsFile, names.SecretsFile, "", `JSON file ({"KEY":"value"}) of secrets for lifecycle commands`), - flags.StringArray(&cmd.Secrets, names.Secret, nil, - "Stored Devsy secret to inject, as NAME[,type=env|mount][,target=X]; "+ - "type=env (default) sets an env var, type=mount writes /run/secrets/. Repeatable"), + flags.StringArray( + &cmd.Secrets, + names.Secret, + nil, + "Secret to inject: NAME or source-qualified TYPE:SOURCE/NAME[,type=env|mount][,target=X]; "+ + "type=env (default) sets an env var, type=mount writes /run/secrets/. Repeatable", + ), flags.StringArray(&cmd.EnvVars, names.Env, nil, "Stored Devsy env var to inject into the workspace as NAME[=TARGET]. Repeatable"), flags.StringArray(&cmd.BuildSecretNames, names.BuildSecret, nil, - "Stored Devsy secret exposed to the build via BuildKit "+ + "Secret to expose to the build via BuildKit; accepts NAME or TYPE:SOURCE/NAME "+ "(RUN --mount=type=secret,id=NAME). Repeatable"), flags.String(&cmd.GitTokenSecret, names.GitToken, "", - "Stored Devsy secret holding an access token for cloning a private HTTP repository"), + "Secret holding an access token for cloning a private HTTP repository; "+ + "accepts NAME or TYPE:SOURCE/NAME; repository-owned sources cannot bootstrap their own clone"), flags.String(&cmd.GitTokenUsername, names.GitTokenUsername, "", "Username for --git-token (default inferred from the repo host)"), flags.String(&cmd.FeatureSecretsFile, names.FeatureSecretsFile, "", diff --git a/cmd/workspace/up/up_test.go b/cmd/workspace/up/up_test.go index ac0c83cde..3b59d6b70 100644 --- a/cmd/workspace/up/up_test.go +++ b/cmd/workspace/up/up_test.go @@ -18,11 +18,12 @@ import ( ) const ( - probeNone = "none" - flagNameMount = "mount" - flagMount = "--" + flagNameMount - testBindMountAB = "type=bind,source=/a,target=/b" - testSnapshotRef = "ghcr.io/acme/s:my-ws-20260731150405-abcxyz" + probeNone = "none" + flagNameMount = "mount" + flagMount = "--" + flagNameMount + testBindMountAB = "type=bind,source=/a,target=/b" + testSnapshotRef = "ghcr.io/acme/s:my-ws-20260731150405-abcxyz" + emptyIsValidName = "empty is valid" ) func TestUpCmd_NoLockfileAndFrozenLockfileMutuallyExclusive(t *testing.T) { @@ -42,7 +43,7 @@ func TestUpCmd_ValidateDefaultUserEnvProbe(t *testing.T) { value string wantErr bool }{ - {name: "empty is valid", value: "", wantErr: false}, + {name: emptyIsValidName, value: "", wantErr: false}, {name: "none", value: probeNone, wantErr: false}, {name: "loginShell", value: "loginShell", wantErr: false}, {name: "interactiveShell", value: "interactiveShell", wantErr: false}, @@ -119,7 +120,7 @@ func TestUpCmd_ValidateWorkspaceMountConsistency(t *testing.T) { value string wantErr bool }{ - {name: "empty is valid", value: "", wantErr: false}, + {name: emptyIsValidName, value: "", wantErr: false}, {name: "consistent", value: MountConsistencyConsistent, wantErr: false}, {name: "cached", value: MountConsistencyCached, wantErr: false}, {name: "delegated", value: MountConsistencyDelegated, wantErr: false}, @@ -265,7 +266,7 @@ func TestUpCmd_ValidateMounts(t *testing.T) { mounts []string wantErr bool }{ - {name: "empty is valid", mounts: []string{}}, + {name: emptyIsValidName, mounts: []string{}}, { name: "valid bind mount", mounts: []string{"type=bind,source=/host,target=/container"}, diff --git a/e2e/tests/up/sops.go b/e2e/tests/up/sops.go new file mode 100644 index 000000000..2d802d162 --- /dev/null +++ b/e2e/tests/up/sops.go @@ -0,0 +1,212 @@ +package up + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/devsy-org/devsy/e2e/framework" + docker "github.com/devsy-org/devsy/pkg/docker" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +const ( + sourceSubCmd = "source" + sopsE2EPlaintext = "SUPER_SECRET_TEST_VALUE_7B91" + sopsE2EMounted = "mounted-value-77" + sopsE2EAgeIdentity = "AGE-SECRET-KEY-12UWYSAH2MRDQ5K4EWC4253PDTCSCS32Y5EFQ8TEN2SL3QYU2GN2SG88CZX" // gitleaks:allow +) + +var _ = ginkgo.Describe( + "SOPS secret sources", + ginkgo.Label("up-provider-docker"), + func() { + var dtc *dockerTestContext + + ginkgo.BeforeEach(func(ctx context.Context) { + var err error + dtc = &dockerTestContext{} + dtc.initialDir, err = os.Getwd() + framework.ExpectNoError(err) + + dtc.dockerHelper = &docker.DockerHelper{DockerCommand: testDockerCommand} + dtc.f, err = setupDockerProvider( + filepath.Join(dtc.initialDir, "bin"), + testDockerCommand, + ) + framework.ExpectNoError(err) + }) + + ginkgo.It( + "injects a registered SOPS source as env and mounted secrets", + func(ctx context.Context) { + useSOPSAgeIdentity() + tempDir, err := setupWorkspace( + "tests/up/testdata/docker-sops-source", + dtc.initialDir, + dtc.f, + ) + framework.ExpectNoError(err) + + registerSOPSSource(ctx, dtc, "sops-e2e", filepath.Join(tempDir, "secrets.enc.yaml")) + err = dtc.f.DevsyUp( + ctx, + tempDir, + "--secret", + "sops:sops-e2e/SOPS_E2E_SECRET,target=SOPS_ALIAS", + "--secret", + "sops:sops-e2e/TLS_KEY,type=mount,target=tls_key", + ) + framework.ExpectNoError(err) + + envValue, err := dtc.execSSH(ctx, tempDir, "cat /tmp/sops-env-check.out") + framework.ExpectNoError(err) + gomega.Expect(strings.TrimSpace(envValue)).To(gomega.Equal(sopsE2EPlaintext)) + + mountValue, err := dtc.execSSH(ctx, tempDir, "cat /tmp/sops-mount-check.out") + framework.ExpectNoError(err) + gomega.Expect(strings.TrimSpace(mountValue)).To(gomega.Equal(sopsE2EMounted)) + }, + ginkgo.SpecTimeout(framework.TimeoutShort()), + ) + + ginkgo.It( + "discovers repository-owned SOPS sources and attached secrets", + func(ctx context.Context) { + useSOPSAgeIdentity() + tempDir, err := setupWorkspace( + "tests/up/testdata/docker-sops-project", + dtc.initialDir, + dtc.f, + ) + framework.ExpectNoError(err) + + err = dtc.f.DevsyUp(ctx, tempDir) + framework.ExpectNoError(err) + + value, err := dtc.execSSH(ctx, tempDir, "cat /tmp/sops-project-check.out") + framework.ExpectNoError(err) + gomega.Expect(strings.TrimSpace(value)).To(gomega.Equal(sopsE2EPlaintext)) + }, + ginkgo.SpecTimeout(framework.TimeoutShort()), + ) + ginkgo.It( + "discovers remote Git repository-owned SOPS sources from customizations.devsy", + func(ctx context.Context) { + useSOPSAgeIdentity() + tempDir, err := setupWorkspace( + "tests/up/testdata/docker-sops-project", + dtc.initialDir, + dtc.f, + ) + framework.ExpectNoError(err) + + // Initialize a real Git repository in tempDir + execGit := func(args ...string) { + c := exec.CommandContext(ctx, "git", args...) // #nosec G204 -- test helper + c.Dir = tempDir + out, gitErr := c.CombinedOutput() + framework.ExpectNoError(gitErr, string(out)) + } + execGit("init", "-b", "main") + execGit("config", "user.name", "Devsy") + execGit("config", "user.email", "devsy@example.com") + execGit("add", ".") + execGit("commit", "-m", "initial commit") + + // Start workspace pointing to the Git repository + gitURI := "file://" + filepath.ToSlash(tempDir) + err = dtc.f.DevsyUp(ctx, gitURI) + framework.ExpectNoError(err) + + value, err := dtc.execSSH(ctx, tempDir, "cat /tmp/sops-project-check.out") + framework.ExpectNoError(err) + gomega.Expect(strings.TrimSpace(value)).To(gomega.Equal(sopsE2EPlaintext)) + }, + ginkgo.SpecTimeout(framework.TimeoutShort()), + ) + + ginkgo.It( + "fails closed when SOPS credentials are unavailable without leaking plaintext", + func(ctx context.Context) { + useSOPSAgeIdentity() + tempDir, err := setupWorkspace( + "tests/up/testdata/docker-sops-source", + dtc.initialDir, + dtc.f, + ) + framework.ExpectNoError(err) + + registerSOPSSource( + ctx, + dtc, + "sops-failure", + filepath.Join(tempDir, "secrets.enc.yaml"), + ) + framework.ExpectNoError(os.Setenv("SOPS_AGE_KEY", "")) + framework.ExpectNoError( + os.Setenv("SOPS_AGE_KEY_FILE", filepath.Join(tempDir, "missing-age-key")), + ) + + stdout, stderr, upErr := dtc.f.DevsyUpStreams( + ctx, + tempDir, + "--secret", + "sops:sops-failure/SOPS_E2E_SECRET", + ) + gomega.Expect(upErr).To(gomega.HaveOccurred()) + + combined := strings.Join([]string{stdout, stderr, fmt.Sprint(upErr)}, "\n") + gomega.Expect(combined).NotTo(gomega.ContainSubstring(sopsE2EPlaintext)) + gomega.Expect(combined).NotTo(gomega.ContainSubstring(sopsE2EMounted)) + }, + ginkgo.SpecTimeout(framework.TimeoutShort()), + ) + }, +) + +func useSOPSAgeIdentity() { + setSOPSEnv("SOPS_AGE_KEY", sopsE2EAgeIdentity) + unsetSOPSEnv("SOPS_AGE_KEY_FILE") + unsetSOPSEnv("SOPS_AGE_KEY_CMD") +} + +func setSOPSEnv(name, value string) { + previous, had := os.LookupEnv(name) + framework.ExpectNoError(os.Setenv(name, value)) + ginkgo.DeferCleanup(func() { + if had { + _ = os.Setenv(name, previous) + return + } + _ = os.Unsetenv(name) + }) +} + +func unsetSOPSEnv(name string) { + previous, had := os.LookupEnv(name) + framework.ExpectNoError(os.Unsetenv(name)) + ginkgo.DeferCleanup(func() { + if had { + _ = os.Setenv(name, previous) + return + } + _ = os.Unsetenv(name) + }) +} + +func registerSOPSSource(ctx context.Context, dtc *dockerTestContext, name, filePath string) { + _, _ = dtc.f.ExecCommandOutput(ctx, []string{secretCmd, sourceSubCmd, "remove", name}) + _, err := dtc.f.ExecCommandOutput( + ctx, + []string{secretCmd, sourceSubCmd, "add", "sops", name, filePath}, + ) + framework.ExpectNoError(err) + ginkgo.DeferCleanup(func() { + _, _ = dtc.f.ExecCommandOutput(ctx, []string{secretCmd, sourceSubCmd, "remove", name}) + }) +} diff --git a/e2e/tests/up/testdata/docker-sops-project/.devcontainer.json b/e2e/tests/up/testdata/docker-sops-project/.devcontainer.json new file mode 100644 index 000000000..aa1b2d3a1 --- /dev/null +++ b/e2e/tests/up/testdata/docker-sops-project/.devcontainer.json @@ -0,0 +1,16 @@ +{ + "image": "ghcr.io/devsy-org/test-images/go:1", + "postCreateCommand": "printf '%s' \"$SOPS_E2E_SECRET\" > /tmp/sops-project-check.out", + "customizations": { + "devsy": { + "secretSources": [ + { + "name": "project", + "type": "sops", + "path": "secrets.enc.yaml" + } + ], + "secrets": ["sops:project/SOPS_E2E_SECRET"] + } + } +} diff --git a/e2e/tests/up/testdata/docker-sops-project/secrets.enc.yaml b/e2e/tests/up/testdata/docker-sops-project/secrets.enc.yaml new file mode 100644 index 000000000..79a5fdfd8 --- /dev/null +++ b/e2e/tests/up/testdata/docker-sops-project/secrets.enc.yaml @@ -0,0 +1,17 @@ +SOPS_E2E_SECRET: ENC[AES256_GCM,data:7/LkTGbUbM24m2YegXZhCPwFErYDhnRC3jflQA==,iv:jox9Lg827lNean0A3dbFnjbizHq/+e5Oj41ntzFKtNc=,tag:H0XcdWPIYBmte+v0F9j47Q==,type:str] +TLS_KEY: ENC[AES256_GCM,data:7qUD3Paq3RT0XbF/qvSnfw==,iv:ClkYTnncQRPn3JhDZ9iZfuY0lNDYY71ot6HRjF2HCVY=,tag:oGoczTS56kPXL9N4/FY+nA==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwWHBIUkxaRWlsdFRidWFF + OHdHTHZGZUh2UGRzOXhVdFlxRnZIZkY3eTFnCnpoTFNrNVZKcFRtTVhjbnBUK1c1 + SGFnd2R0ZGQvOHFueWpHR0xiOGVvdTAKLS0tIDJTSVFCazNueGJkMlZDUU9vK21j + d0xPTEV4NVZGL1EweHBqM3lTSG9sb3MKJtSnyqRMtWM2WRIEJSHQPkYtBXa+wSpG + PXamDLnUGIrzsjbl6uTZxHytVSwfVERwWzagJTEOajIJpFwHIyGD/A== + -----END AGE ENCRYPTED FILE----- + recipient: age1h84qv0p8kdwfp6vrag4nvmpd6gt3mde9g6dcnj9uryh3qcl50uxq83n932 + lastmodified: "2026-09-03T05:38:40Z" + mac: ENC[AES256_GCM,data:5thaKGP8s7dbN/9VvRn54TB0R6UaRlRvtyoby0yn5gGlCAYZLvBlAXmTY8AQ0ShQII3ictP2KpXVLGd0dp0H0LXGr9PpsdXIVEO5t/WS5YO+ZhdxZ5hgWBzEL1rZ2VO9Bt0DPFmEYNNryqIpLU64lbd+AO5P/6/uaBMZZERYE0Y=,iv:7nUHdzpyZidt2DRQdvSv2W2GCF5JArFzvyX9ppVEP5k=,tag:/KTQAguMfRBpWPDumiTunA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3 diff --git a/e2e/tests/up/testdata/docker-sops-source/.devcontainer.json b/e2e/tests/up/testdata/docker-sops-source/.devcontainer.json new file mode 100644 index 000000000..ccaca4ac3 --- /dev/null +++ b/e2e/tests/up/testdata/docker-sops-source/.devcontainer.json @@ -0,0 +1,4 @@ +{ + "image": "ghcr.io/devsy-org/test-images/go:1", + "postCreateCommand": "printf '%s' \"$SOPS_ALIAS\" > /tmp/sops-env-check.out && cat /run/secrets/tls_key > /tmp/sops-mount-check.out" +} diff --git a/e2e/tests/up/testdata/docker-sops-source/secrets.enc.yaml b/e2e/tests/up/testdata/docker-sops-source/secrets.enc.yaml new file mode 100644 index 000000000..79a5fdfd8 --- /dev/null +++ b/e2e/tests/up/testdata/docker-sops-source/secrets.enc.yaml @@ -0,0 +1,17 @@ +SOPS_E2E_SECRET: ENC[AES256_GCM,data:7/LkTGbUbM24m2YegXZhCPwFErYDhnRC3jflQA==,iv:jox9Lg827lNean0A3dbFnjbizHq/+e5Oj41ntzFKtNc=,tag:H0XcdWPIYBmte+v0F9j47Q==,type:str] +TLS_KEY: ENC[AES256_GCM,data:7qUD3Paq3RT0XbF/qvSnfw==,iv:ClkYTnncQRPn3JhDZ9iZfuY0lNDYY71ot6HRjF2HCVY=,tag:oGoczTS56kPXL9N4/FY+nA==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwWHBIUkxaRWlsdFRidWFF + OHdHTHZGZUh2UGRzOXhVdFlxRnZIZkY3eTFnCnpoTFNrNVZKcFRtTVhjbnBUK1c1 + SGFnd2R0ZGQvOHFueWpHR0xiOGVvdTAKLS0tIDJTSVFCazNueGJkMlZDUU9vK21j + d0xPTEV4NVZGL1EweHBqM3lTSG9sb3MKJtSnyqRMtWM2WRIEJSHQPkYtBXa+wSpG + PXamDLnUGIrzsjbl6uTZxHytVSwfVERwWzagJTEOajIJpFwHIyGD/A== + -----END AGE ENCRYPTED FILE----- + recipient: age1h84qv0p8kdwfp6vrag4nvmpd6gt3mde9g6dcnj9uryh3qcl50uxq83n932 + lastmodified: "2026-09-03T05:38:40Z" + mac: ENC[AES256_GCM,data:5thaKGP8s7dbN/9VvRn54TB0R6UaRlRvtyoby0yn5gGlCAYZLvBlAXmTY8AQ0ShQII3ictP2KpXVLGd0dp0H0LXGr9PpsdXIVEO5t/WS5YO+ZhdxZ5hgWBzEL1rZ2VO9Bt0DPFmEYNNryqIpLU64lbd+AO5P/6/uaBMZZERYE0Y=,iv:7nUHdzpyZidt2DRQdvSv2W2GCF5JArFzvyX9ppVEP5k=,tag:/KTQAguMfRBpWPDumiTunA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3 diff --git a/go.mod b/go.mod index a605aa684..649349709 100644 --- a/go.mod +++ b/go.mod @@ -23,13 +23,14 @@ require ( github.com/devsy-org/agentapi v1.0.1 github.com/devsy-org/api v1.1.0 github.com/devsy-org/apiserver v1.5.3 - github.com/devsy-org/ssh v1.2.7 + github.com/devsy-org/ssh v1.2.5 github.com/distribution/reference v0.6.0 github.com/docker/cli v29.7.1+incompatible github.com/docker/docker v28.5.2+incompatible github.com/docker/docker-credential-helpers v0.9.8 github.com/docker/go-connections v0.8.1 github.com/evanphx/json-patch/v5 v5.9.11 + github.com/getsops/sops/v3 v3.13.3 github.com/go-logr/logr v1.4.4 github.com/go-logr/zapr v1.3.0 github.com/gofrs/flock v0.13.0 @@ -63,7 +64,7 @@ require ( go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.28.0 - golang.org/x/crypto v0.56.0 + golang.org/x/crypto v0.55.0 golang.org/x/mod v0.38.0 golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 @@ -91,14 +92,14 @@ require ( charm.land/bubbles/v2 v2.0.0 // indirect charm.land/bubbletea/v2 v2.0.2 // indirect cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.20.0 // indirect + cloud.google.com/go/auth v0.22.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.11.0 // indirect - cloud.google.com/go/kms v1.31.0 // indirect - cloud.google.com/go/longrunning v1.0.0 // indirect - cloud.google.com/go/monitoring v1.25.0 // indirect - cloud.google.com/go/storage v1.62.2 // indirect + cloud.google.com/go/iam v1.12.0 // indirect + cloud.google.com/go/kms v1.32.0 // indirect + cloud.google.com/go/longrunning v1.2.0 // indirect + cloud.google.com/go/monitoring v1.30.0 // indirect + cloud.google.com/go/storage v1.63.1 // indirect code.gitea.io/sdk/gitea v0.25.1 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.2.0 // indirect @@ -121,9 +122,9 @@ require ( github.com/Azure/go-autorest/tracing v0.6.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect github.com/BurntSushi/toml v1.6.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.58.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.58.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect @@ -145,7 +146,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.32.31 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.30 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.34 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.3.5 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 // indirect @@ -156,7 +157,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.24 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32 // indirect - github.com/aws/aws-sdk-go-v2/service/kms v1.52.0 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.54.1 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 // indirect @@ -192,7 +193,7 @@ require ( github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/cloudflare/circl v1.6.3 // indirect + github.com/cloudflare/circl v1.6.4 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/coder/websocket v1.8.14 // indirect github.com/containerd/console v1.0.5 // indirect @@ -209,6 +210,7 @@ require ( github.com/coreos/go-oidc/v3 v3.20.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/creachadair/msync v0.8.1 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect @@ -230,11 +232,13 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gaissmai/bart v0.26.1 // indirect + github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e // indirect github.com/github/smimesign v0.2.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect @@ -271,6 +275,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.6 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect @@ -290,14 +295,15 @@ require ( github.com/google/s2a-go v0.1.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/wire v0.7.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect - github.com/googleapis/gax-go/v2 v2.22.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.18 // indirect + github.com/googleapis/gax-go/v2 v2.23.0 // indirect github.com/goreleaser/chglog v0.7.4 // indirect github.com/goreleaser/fileglob v1.4.0 // indirect github.com/goreleaser/go-shellwords v1.0.13 // indirect github.com/goreleaser/goreleaser/v2 v2.17.1 // indirect github.com/goreleaser/nfpm/v2 v2.47.0 // indirect github.com/goreleaser/quill v0.0.0-20260630015114-8310f3e9a321 // indirect + github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect @@ -305,11 +311,18 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect github.com/hashicorp/go-version v1.9.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect + github.com/hashicorp/vault/api v1.23.0 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect + github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.207 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/in-toto/attestation v1.2.0 // indirect github.com/in-toto/in-toto-golang v0.11.0 // indirect @@ -331,7 +344,7 @@ require ( github.com/jbenet/goprocess v0.1.4 // indirect github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 // indirect github.com/jsimonetti/rtnetlink v1.4.1 // indirect - github.com/json-iterator/go v1.1.12 // indirect + github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.19.1 // indirect @@ -339,6 +352,7 @@ require ( github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/fs v0.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lib/pq v1.12.3 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect @@ -356,6 +370,7 @@ require ( github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect @@ -403,6 +418,8 @@ require ( github.com/prometheus/common v0.69.0 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect @@ -429,7 +446,7 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/viper v1.21.0 // indirect - github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect @@ -440,6 +457,7 @@ require ( github.com/theupdateframework/go-tuf/v2 v2.4.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/tomnomnom/linkheader v0.0.0-20250811210735-e5fe3b51442e // indirect @@ -449,6 +467,7 @@ require ( github.com/transparency-dev/formats v0.1.1 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect github.com/ulikunitz/xz v0.5.16 // indirect + github.com/urfave/cli v1.22.17 // indirect github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 // indirect github.com/whyrusleeping/cbor-gen v0.1.3-0.20240731173018-74d74643234c // indirect github.com/x448/float16 v0.8.4 // indirect @@ -464,6 +483,7 @@ require ( go.etcd.io/etcd/api/v3 v3.6.8 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.8 // indirect go.etcd.io/etcd/client/v3 v3.6.8 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect go.mozilla.org/pkcs7 v0.9.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect @@ -496,14 +516,15 @@ require ( golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/api v0.283.0 // indirect - google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/api v0.289.0 // indirect + google.golang.org/genproto v0.0.0-20260720171339-e059f2f05d78 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260720171339-e059f2f05d78 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260720171339-e059f2f05d78 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.3 // indirect gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect diff --git a/go.sum b/go.sum index ae3ca8bb7..9d91c28e9 100644 --- a/go.sum +++ b/go.sum @@ -14,28 +14,29 @@ charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU= charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= -cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= +cloud.google.com/go/auth v0.22.0 h1:Xp9wAKkLoeaYb5pYZZoQGz4E9sdPxIbzS3gywZE3ciQ= +cloud.google.com/go/auth v0.22.0/go.mod h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM= -cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= -cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE= -cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U= -cloud.google.com/go/logging v1.14.0 h1:xpPpY8cVT6n9DgIRgrWyE+YEsGlO/994pWnbc7o5Eh4= -cloud.google.com/go/logging v1.14.0/go.mod h1:jmI+Try/fZeOTOAer3wVYOuPf9WX9PyzhlSDoBAi4HM= -cloud.google.com/go/longrunning v1.0.0 h1:lwzWEYD8+NkYV7dhexOz6kmlvajZA70+bW/xMhRVVdY= -cloud.google.com/go/longrunning v1.0.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM= -cloud.google.com/go/monitoring v1.25.0 h1:HnsTIOxTN6BCSkt1P/Im23r1m7MHTTpmSYCzPkW7NK4= -cloud.google.com/go/monitoring v1.25.0/go.mod h1:wlj6rX+JGyusw/8+2duW4cJ6kmDHGmde3zMTJuG3Jpc= -cloud.google.com/go/storage v1.62.2 h1:WgR4U9n7bIzXkkVnwPKKE8bkaKUNsHG+0MAAlh9DGU4= -cloud.google.com/go/storage v1.62.2/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= -cloud.google.com/go/trace v1.12.0 h1:XvWHYfr9q88cX4pZyou6qCcSagnuASyUq2ej1dB6NzQ= -cloud.google.com/go/trace v1.12.0/go.mod h1:TOYfyeoyCGsSH0ifXD6Aius24uQI9xV3RyvOdljFIyg= +cloud.google.com/go/iam v1.12.0 h1:Aki3bX9aHUDKPHfnRJfDcTdVedvy6quGBQcTqx3DRXk= +cloud.google.com/go/iam v1.12.0/go.mod h1:FEZ4lXpADAC2AIpQY7LANNjjwyQ2jK439CI2VaD+sLY= +cloud.google.com/go/kms v1.32.0 h1:s+rEluaaZKhLVjrIWG7uNBsnWbiitElzNzFGyp6+nIg= +cloud.google.com/go/kms v1.32.0/go.mod h1:CSGvW6GnMQbY+1nOHcIzhMtHSbExXlOmCKjWtYVjcpA= +cloud.google.com/go/logging v1.19.0 h1:NCqhdVUg3wQ8Cobdf16FDSuTGi3+6+hdSBHrY5TsR6Q= +cloud.google.com/go/logging v1.19.0/go.mod h1:i40NZCHC9Gqvod4yE+yQfDWwlgwW/SrshkkGibCHxcA= +cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM= +cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0= +cloud.google.com/go/monitoring v1.30.0 h1:r/d+JUbyKmJ8b07iznuKfzVzrIXTWxHQ3lBRm3x2LlY= +cloud.google.com/go/monitoring v1.30.0/go.mod h1:htlUR0QWVMrjFzZmN4LGnMAve9xB/eduwjmINxVZ8RM= +cloud.google.com/go/storage v1.63.1 h1:CYXILV9G4CH0C18IQ9+V0h4XiqD2LhKnMLO0o7uJWNs= +cloud.google.com/go/storage v1.63.1/go.mod h1:lWyAtwvDZHdL3k68WVKbESP6bmWaV23ZJJ/JEVw/ZaQ= +cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E= +cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0= code.gitea.io/sdk/gitea v0.25.1 h1:yywxWwoV+SdjHtbC6unBiXojWdZOtoHuGhEazEXeWuE= code.gitea.io/sdk/gitea v0.25.1/go.mod h1:uDFWYBU8dgZsgOHwe6C/6olxvf8FHguNB3wW1i83fgg= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= @@ -111,18 +112,19 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU= github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0 h1:yzIYdwuro811Z27D3T80Wkd3rqZzb0K43nner7Eh1yE= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.58.0 h1:ZYGajzJNcirVZpT1rltgf9iM+j9zZ4v8V9DrF+xKRJ8= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.58.0/go.mod h1:PDQyYBOzGtQgvshQI//UiXyzuMHCz0ndyu+4W8X82vM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.58.0 h1:IBF8BbhKJkMsON/eY+LMu3aF3XMiotCb9KvkUmEkOJo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.58.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.58.0 h1:SBZzZCiPmDrUV7NSCWY54OnKikO/oTydPCvyEyYaDDE= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.58.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -191,8 +193,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.19.30 h1:TTCvvzFU6gXa4iJecNG/0F/B0oY github.com/aws/aws-sdk-go-v2/credentials v1.19.30/go.mod h1:jKxAp2AEncnliinzpgOSZDFv6+VjvWhjw/AtbfsWT9U= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 h1:kfVL5wAunCJycL6MOQ6aNh6PlAYEymflcjuKmrWUA0o= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31/go.mod h1:nWfRNDAppujCQgOUd43lKT4yeLv9z3nJ3bw1G3BgQKo= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 h1:1hWFp+52Vq8Fevy/KUhbW/1MEApMz7uitCF/PQXRJpk= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1/go.mod h1:sIec8j802/rCkCKgZV678HFR0s7lhQUYXT77tIvlaa4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.34 h1:Pn7OsMwBLbkZ6OnCxWHAjf0L/22H8cnhxZC0uPwtMtg= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.34/go.mod h1:eToXR/Gk1uqpn04eSmdgVXwfS0WvH8aG4eBFr8ygbpU= github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.3.5 h1:7ZFdtE1XEHH+58GZU4Mbhq6SO/UbColleDApOtlv3vo= github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.3.5/go.mod h1:M/qt7xBBXqilBwNmrO1yiu0cywZwQx5aqtKpdJN9J2A= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 h1:Z8F3hfCY33IGpJjFAnv0wvtv1FIKj1GHmRDEYqy64tw= @@ -213,8 +215,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 h1:w2SIhW92 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31/go.mod h1:wAhpCQbkov+IcvjozJbd2xRCoZybUEHNkcFunssNACg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32 h1:jWXtZdCnhXa9sGFixRaU2AxT4DIVse9HS4E2f+/KwV0= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32/go.mod h1:9JS1UpfVvyD/ZPX8GsKb/Pq8scEM+7GP5fqh9SwH7po= -github.com/aws/aws-sdk-go-v2/service/kms v1.52.0 h1:QNtg+Mtj1zmepk568+UKBD5DFfqh+ESTUUqQT27JkQc= -github.com/aws/aws-sdk-go-v2/service/kms v1.52.0/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI= +github.com/aws/aws-sdk-go-v2/service/kms v1.54.1 h1:aeJAJyvWS3gQ679pJbz8ZdOh3MViD1zvEdoZMVEawbg= +github.com/aws/aws-sdk-go-v2/service/kms v1.54.1/go.mod h1:0RXNc6Yf3AvSMldGD6Lcch96Ojlw2TtGnHsqfD/L4u8= github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0 h1:7QZWVJZWzHivHWIa+5TELLaBBkbuoj0GPwQtMlJ0sqk= github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0/go.mod h1:fcvq5L7dK+5cQFicEJwpI6e6Wn8NY2i6yT5wRLYVc7s= github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 h1:OHH5iTQvVGmfHjX/5Q+vFuA/Rf2x6/95aJ/75QCQSm4= @@ -277,6 +279,7 @@ 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/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -314,12 +317,14 @@ github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb2 github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM= github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -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/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U= +github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= @@ -374,6 +379,8 @@ github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creachadair/mds v0.25.13 h1:PsSUHV6zsfPd29k4kvm1rMoee1YFia7JyNGeMPmDcPM= github.com/creachadair/mds v0.25.13/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs= github.com/creachadair/msync v0.8.1 h1:QRd8si3qZ2Q4TaDL7tS/MG/lFE3YND7U7J9fy42eAFM= @@ -407,8 +414,8 @@ github.com/devsy-org/api v1.1.0 h1:l7T9k7RVwatwN4lxeDTF3iN6EYmfGZgR3ZTJMDGha1M= github.com/devsy-org/api v1.1.0/go.mod h1:mAZklKdnywJYiXDReBLte/H+3m69z6G7RHB3n1lI53Q= github.com/devsy-org/apiserver v1.5.3 h1:tFKMgPxxfvojJ+C+wo0oqSmbE9PTJ2ur1E2yFj0eISI= github.com/devsy-org/apiserver v1.5.3/go.mod h1:m7gpbrh++Hp8iEM5jaP7vjjODPbs9Bmh3l4+iPFg1jE= -github.com/devsy-org/ssh v1.2.7 h1:tD4Sg4CrGsBwltMc5GbUWVJ8HIJp+GcI7J2WTP+j0Qk= -github.com/devsy-org/ssh v1.2.7/go.mod h1:GRrtJWdXPG2cXxcwIO1/IHM/3YnUlEqiCMdTIB4ejSs= +github.com/devsy-org/ssh v1.2.5 h1:Z7gTanYs2ZslT1swTw4leoVVuDEmuNNhQi48W2kNqMU= +github.com/devsy-org/ssh v1.2.5/go.mod h1:6r5tZ+H9JFoMl6NrxXRgltg9HhEryTd69avY831Lio8= github.com/devsy-org/tailscale v1.102.2 h1:9SB6htvO+HmG8alal8WGCshHapfHR7dUFRSMYiFIzIM= github.com/devsy-org/tailscale v1.102.2/go.mod h1:kQUA0lYb/bqCJJZzShx+gqLSxggEFgXB4VDVZDzxWc8= github.com/dghubble/go-twitter v0.0.0-20211115160449-93a8679adecb h1:7ENzkH+O3juL+yj2undESLTaAeRllHwCs/b8z6aWSfc= @@ -462,24 +469,28 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= 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= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= @@ -490,6 +501,10 @@ github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9 github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo= github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= +github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e h1:y/1nzrdF+RPds4lfoEpNhjfmzlgZtPqyO3jMzrqDQws= +github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM= +github.com/getsops/sops/v3 v3.13.3 h1:saYczbT88kD1saNChe1cAbFQe5mrRhTIfEw3TaEcmK0= +github.com/getsops/sops/v3 v3.13.3/go.mod h1:3mUuUtKnJ63IzIvU4LQoDXdp0ZvorY5s2hEc7UVNfx8= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/github/smimesign v0.2.0 h1:Hho4YcX5N1I9XNqhq0fNx0Sts8MhLonHd+HRXVGNjvk= @@ -578,6 +593,10 @@ github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAg github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-openapi/validate v0.26.0 h1:dxWzQ3F+vb1SajqUxHjwb5T4mTpSHmdrtv5Bi7+ZNhw= github.com/go-openapi/validate v0.26.0/go.mod h1:b4o00uq7fJeJA+wWhVFCJpKTctzeFwzZImGGmHsl2JA= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= @@ -599,8 +618,9 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= @@ -614,10 +634,22 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= @@ -626,6 +658,10 @@ github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -676,10 +712,10 @@ 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/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= -github.com/googleapis/enterprise-certificate-proxy v0.3.16 h1:F/VPrx0YPBdksZJQdCAp0WUsqnNmZpUZszzfYt0M5Dw= -github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE= -github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= -github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= +github.com/googleapis/enterprise-certificate-proxy v0.3.18 h1:hvVi34VucdrV1IIsiWuqYM8kutw/92MxNEFxCJZEh0k= +github.com/googleapis/enterprise-certificate-proxy v0.3.18/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k= +github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= +github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= @@ -702,6 +738,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 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/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw= +github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s= github.com/graph-gophers/graphql-go v1.9.0 h1:yu0ucKHLc5qGpRwLYKIWtr9bOoxovkWasuBrPQwlHls= github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= @@ -740,8 +778,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= -github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= -github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= +github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A= +github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= @@ -750,6 +788,8 @@ github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= 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/huaweicloud/huaweicloud-sdk-go-v3 v0.1.207 h1:lgMtpjpIWPw0gbCAko23dRKl66ZPUmeAOidjKFkub2E= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.207/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= github.com/hugelgupf/vmtest v0.0.0-20240307030256-5d9f3d34a58d h1:nP8SfQJqruIVSWYJTuYc37jLHEY1Z0fF+zKSrs3K/C8= github.com/hugelgupf/vmtest v0.0.0-20240307030256-5d9f3d34a58d/go.mod h1:B63hDJMhTupLWCHwopAyEo7wRFowx9kOc8m8j1sfOqE= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= @@ -812,8 +852,8 @@ github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/jsimonetti/rtnetlink v1.4.1 h1:JfD4jthWBqZMEffc5RjgmlzpYttAVw1sdnmiNaPO3hE= github.com/jsimonetti/rtnetlink v1.4.1/go.mod h1:xJjT7t59UIZ62GLZbv6PLLo8VFrostJMPBAheR6OM8w= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU= +github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -824,6 +864,7 @@ github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRt github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -843,8 +884,11 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/letsencrypt/boulder v0.20260309.0 h1:kZynrxK3QfqLGx6hhoz+Rfs3hgltJs1p9Mp+4+VwnY0= github.com/letsencrypt/boulder v0.20260309.0/go.mod h1:yG8lj8pNPZ8taq3oNdTpfBS+eC74IaEuiewqzVpXiWE= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= @@ -856,9 +900,11 @@ github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRH github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ= github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= @@ -941,6 +987,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -1025,6 +1072,7 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 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.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 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.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= @@ -1039,6 +1087,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR 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.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= @@ -1128,8 +1177,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4= -github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= +github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E= +github.com/spiffe/go-spiffe/v2 v2.8.1/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -1146,6 +1195,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/studio-b12/gowebdav v0.13.0 h1:OcwSg6IQHOFNdYHn3bPOHwSE8looG8N56Y5xTT1asqQ= @@ -1198,6 +1248,8 @@ github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC6 github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= @@ -1227,6 +1279,8 @@ github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17 github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0= github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= +github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= github.com/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw= github.com/vbatts/tar-split v0.12.3/go.mod h1:sQOc6OlqGCr7HkGx/IDBeKiTIvqhmj8KffNhEXG4Nq0= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= @@ -1241,6 +1295,9 @@ 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/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -1259,6 +1316,7 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= @@ -1299,6 +1357,9 @@ go.etcd.io/etcd/server/v3 v3.6.8 h1:U2strdSEy1U8qcSzRIdkYpvOPtBy/9i/IfaaCI9flZ4= go.etcd.io/etcd/server/v3 v3.6.8/go.mod h1:88dCtwUnSirkUoJbflQxxWXqtBSZa6lSG0Kuej+dois= go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= +go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= @@ -1337,8 +1398,8 @@ go.opentelemetry.io/otel/exporters/prometheus v0.66.0 h1:vkrK8PAznv2NKt2r+kdu252 go.opentelemetry.io/otel/exporters/prometheus v0.66.0/go.mod h1:V/UB6D3vMF/UBOL5igAsAYnk1nG/bzYYTzvsB16cy7o= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= @@ -1392,19 +1453,24 @@ golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= -golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q= golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -1414,10 +1480,14 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-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-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= @@ -1427,8 +1497,11 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.57.1-0.20260729233039-99c3b0a8f463 h1:56+vNxUDjzdwAJjglL/DMnp31nd8UeytlPRLbZfz55Q= golang.org/x/net v0.57.1-0.20260729233039-99c3b0a8f463/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-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= @@ -1437,11 +1510,14 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1452,6 +1528,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1472,9 +1549,11 @@ golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -1484,8 +1563,11 @@ golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1513,18 +1595,32 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.283.0 h1:0lkp8u0MPwJVHqRL+nJlMAoZVVzbmiXmFHXMOTmSPik= -google.golang.org/api v0.283.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= -google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d h1:N1Ec54vZnIPd7MnxRiYLW+oY4fDR4BOS/LrssdD9+ek= -google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:c2hJ1grtnH0xUiEKGDGkjGNTJ1Hy2LrblyKOHF0sqRM= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/api v0.289.0 h1:DmH0c6NigNFmsvsohM9bxv+MzVhag3aGHnojA5fFQjc= +google.golang.org/api v0.289.0/go.mod h1:weJZ3lldHFYI0DBFNKpJelUDNnusTt5YaOEgxvt8ci8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20260720171339-e059f2f05d78 h1:NO3LCWyMAM/f/RDLvCC8B/NEvuYqOQAP12XWoyB4os8= +google.golang.org/genproto v0.0.0-20260720171339-e059f2f05d78/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU= +google.golang.org/genproto/googleapis/api v0.0.0-20260720171339-e059f2f05d78 h1:A6tVI++lXZuQiRnz7E+iFluPQ+silVmlkbryjSO1z8c= +google.golang.org/genproto/googleapis/api v0.0.0-20260720171339-e059f2f05d78/go.mod h1:WRrQ7/7N19PypuT0fxLOL5Lq0waoiRri4FbtHDEKrGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260720171339-e059f2f05d78 h1:pRUrsnNVD/NpCD42WJ2AO3dQ2s1e2sqMxg8jOwdX2Ak= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260720171339-e059f2f05d78/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 h1:rgSNvqscFZ1JgV/4wH5GOsZFSFkR2Eua9As3KIr2LlM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2/go.mod h1:iMEtFwDlAhjDU9L5mY6U1XLwlIId/G3h+QcBHDIvrJ8= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= @@ -1539,6 +1635,9 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf 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/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw= +gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= @@ -1559,6 +1658,8 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA= gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.8.0-rc.1 h1:wqMm2kjcEXMOr+6yau+pdKqJKe6l2N1aKPkpini+Kzk= honnef.co/go/tools v0.8.0-rc.1/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks= diff --git a/hack/ci/setup-podman-linux.sh b/hack/ci/setup-podman-linux.sh new file mode 100755 index 000000000..acfbb4b33 --- /dev/null +++ b/hack/ci/setup-podman-linux.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +set -euo pipefail + +mode="${1:-}" +if [[ "$mode" != "rootless" && "$mode" != "rootful" ]]; then + echo "::error::usage: $0 " + exit 2 +fi + +export PATH="/usr/local/bin:$PATH" + +if [[ ! -x /usr/local/bin/podman ]]; then + echo "::error::Expected Podman at /usr/local/bin/podman" + exit 1 +fi +if [[ ! -x /usr/local/bin/crun ]]; then + echo "::error::Expected bundled crun at /usr/local/bin/crun" + exit 1 +fi + +sudo mkdir -p /etc/containers/containers.conf.d +sudo tee /etc/containers/containers.conf.d/99-devsy-ci.conf >/dev/null <<'EOF' +[engine] +runtime = "crun" + +[engine.runtimes] +crun = ["/usr/local/bin/crun"] +EOF + +if [[ -f /etc/apparmor.d/podman ]]; then + sudo sed -Ei \ + 's!^profile podman /usr/bin/podman !profile podman /usr/{bin,local/bin}/podman !' \ + /etc/apparmor.d/podman + sudo apparmor_parser -r /etc/apparmor.d/podman +fi + +echo "Podman executable: $(command -v podman)" +podman --version +/usr/local/bin/crun --version +cat /etc/os-release +uname -a + +if [[ "$mode" == "rootful" ]]; then + sudo systemctl daemon-reload + sudo systemctl enable --now podman.socket + if ! timeout 30 bash -c \ + 'until sudo podman --remote --url unix:///run/podman/podman.sock info >/dev/null 2>&1; do sleep 1; done'; then + echo "::error::podman service did not become ready within 30s" + sudo systemctl status podman.socket --no-pager || true + sudo systemctl status podman.service --no-pager || true + sudo journalctl -u podman.socket --no-pager -n 100 || true + sudo journalctl -u podman.service --no-pager -n 100 || true + exit 1 + fi + + runtime_path="$( + sudo podman --remote --url unix:///run/podman/podman.sock \ + info --format '{{.Host.OCIRuntime.Path}}' + )" + echo "Podman OCI runtime: $runtime_path" + if [[ "$runtime_path" != "/usr/local/bin/crun" ]]; then + echo "::error::Unexpected Podman OCI runtime: $runtime_path" + exit 1 + fi + + echo "DOCKER_HOST=unix:///run/podman/podman.sock" >>"$GITHUB_ENV" + sudo podman --remote --url unix:///run/podman/podman.sock info + sudo podman --remote --url unix:///run/podman/podman.sock run --rm \ + busybox@sha256:fd8d9aa63ba2f0982b5304e1ee8d3b90a210bc1ffb5314d980eb6962f1a9715d \ + true +else + runtime_path="$(podman info --format '{{.Host.OCIRuntime.Path}}')" + echo "Podman OCI runtime: $runtime_path" + if [[ "$runtime_path" != "/usr/local/bin/crun" ]]; then + echo "::error::Unexpected Podman OCI runtime: $runtime_path" + exit 1 + fi + + podman info + podman run --rm \ + busybox@sha256:fd8d9aa63ba2f0982b5304e1ee8d3b90a210bc1ffb5314d980eb6962f1a9715d \ + true +fi diff --git a/pkg/devcontainer/config/config.go b/pkg/devcontainer/config/config.go index 6b8c23b2e..f325d7895 100644 --- a/pkg/devcontainer/config/config.go +++ b/pkg/devcontainer/config/config.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/devsy-org/devsy/pkg/secrets" "github.com/devsy-org/devsy/pkg/types" ) @@ -561,8 +562,23 @@ func matchPortRegex(key, portStr string) bool { } type DevsyCustomizations struct { - PrebuildRepository types.StrArray `json:"prebuildRepository,omitempty"` - FeatureDownloadHTTPHeaders map[string]string `json:"featureDownloadHTTPHeaders,omitempty"` + PrebuildRepository types.StrArray `json:"prebuildRepository,omitempty"` + FeatureDownloadHTTPHeaders map[string]string `json:"featureDownloadHTTPHeaders,omitempty"` + SecretSources []secrets.SourceConfig `json:"secretSources,omitempty"` + Secrets []string `json:"secrets,omitempty"` +} + +func (d *DevsyCustomizations) ToProjectConfig() *secrets.ProjectConfig { + if d == nil { + return nil + } + if len(d.SecretSources) == 0 && len(d.Secrets) == 0 { + return nil + } + return &secrets.ProjectConfig{ + SecretSources: append([]secrets.SourceConfig(nil), d.SecretSources...), + Secrets: append([]string(nil), d.Secrets...), + } } type VSCodeCustomizations struct { diff --git a/pkg/devcontainer/config/result.go b/pkg/devcontainer/config/result.go index d1000aec6..9fdddf33b 100644 --- a/pkg/devcontainer/config/result.go +++ b/pkg/devcontainer/config/result.go @@ -121,13 +121,20 @@ func userFromContainerLabel(result *Result) string { } func GetDevsyCustomizations(parsedConfig *DevContainerConfig) *DevsyCustomizations { - if parsedConfig.Customizations == nil || - parsedConfig.Customizations[pkgconfig.BinaryName] == nil { + if parsedConfig == nil || parsedConfig.Customizations == nil { + return &DevsyCustomizations{} + } + + customization := parsedConfig.Customizations["devsy"] + if customization == nil { + customization = parsedConfig.Customizations[pkgconfig.BinaryName] + } + if customization == nil { return &DevsyCustomizations{} } devsy := &DevsyCustomizations{} - err := convert(parsedConfig.Customizations[pkgconfig.BinaryName], devsy) + err := convert(customization, devsy) if err != nil { return &DevsyCustomizations{} } diff --git a/pkg/git/config_test.go b/pkg/git/config_test.go index 7682616c9..241150308 100644 --- a/pkg/git/config_test.go +++ b/pkg/git/config_test.go @@ -48,14 +48,17 @@ func TestConfigGetWithScope(t *testing.T) { fake.lastArgs()) } +// #nosec G101 -- not a credential value; it is a git config key name. +const credentialHelperKey = "credential.helper" + func TestConfigAddSystemScope(t *testing.T) { fake := &fakeRunner{} config := At("", WithRunner(fake)).Config() - err := config.Add(context.Background(), "credential.helper", "!helper", ScopeSystem) + err := config.Add(context.Background(), credentialHelperKey, "!helper", ScopeSystem) assert.NilError(t, err) assert.DeepEqual(t, - []string{subConfig, flagSystem, "--add", "credential.helper", "!helper"}, + []string{subConfig, flagSystem, "--add", credentialHelperKey, "!helper"}, fake.lastArgs()) } @@ -74,10 +77,10 @@ func TestConfigUnsetSystemScope(t *testing.T) { fake := &fakeRunner{} config := At("", WithRunner(fake)).Config() - err := config.Unset(context.Background(), "credential.helper", ScopeSystem) + err := config.Unset(context.Background(), credentialHelperKey, ScopeSystem) assert.NilError(t, err) assert.DeepEqual(t, - []string{subConfig, flagSystem, "--unset", "credential.helper"}, + []string{subConfig, flagSystem, "--unset", credentialHelperKey}, fake.lastArgs()) } @@ -85,7 +88,7 @@ func TestConfigUnsetValueScopesToExactValue(t *testing.T) { fake := &fakeRunner{} config := At("", WithRunner(fake)).Config() - err := config.UnsetValue(context.Background(), "credential.helper", "!my-helper", ScopeSystem) + err := config.UnsetValue(context.Background(), credentialHelperKey, "!my-helper", ScopeSystem) assert.NilError(t, err) assert.DeepEqual( t, @@ -94,7 +97,7 @@ func TestConfigUnsetValueScopesToExactValue(t *testing.T) { flagSystem, "--fixed-value", "--unset-all", - "credential.helper", + credentialHelperKey, "!my-helper", }, fake.lastArgs(), @@ -105,7 +108,7 @@ func TestConfigUnsetValueNoMatchIsNotError(t *testing.T) { fake := &fakeRunner{err: &CommandError{ExitCode: 5}} config := At("", WithRunner(fake)).Config() - err := config.UnsetValue(context.Background(), "credential.helper", "!my-helper", ScopeSystem) + err := config.UnsetValue(context.Background(), credentialHelperKey, "!my-helper", ScopeSystem) assert.NilError(t, err) } @@ -113,7 +116,7 @@ func TestConfigUnsetValueRealFailurePropagates(t *testing.T) { fake := &fakeRunner{err: &CommandError{ExitCode: 128, Stderr: "fatal: bad config"}} config := At("", WithRunner(fake)).Config() - err := config.UnsetValue(context.Background(), "credential.helper", "!my-helper", ScopeSystem) + err := config.UnsetValue(context.Background(), credentialHelperKey, "!my-helper", ScopeSystem) assert.Assert(t, err != nil) var cmdErr *CommandError diff --git a/pkg/git/inspect.go b/pkg/git/inspect.go new file mode 100644 index 000000000..a965e97e3 --- /dev/null +++ b/pkg/git/inspect.go @@ -0,0 +1,373 @@ +package git + +import ( + "context" + "errors" + "fmt" + "os" + "path" + "path/filepath" + "strings" +) + +const ( + bloblessCloneFilter = "--filter=blob:none" + // inspectionHeadRev is the default revision selector used when the + // inspection carries no explicit commit or PR reference. + inspectionHeadRev = "HEAD" +) + +var ErrRevisionPathNotFound = errors.New("path not found in git revision") + +// Inspection is a blobless, no-checkout clone used to inspect a +// repository-owned configuration files before workspace build. +type Inspection struct { + repo *Repo + rev string + root string + subPath string +} + +// InspectRemote creates a temporary blobless clone and selects the exact +// revision described by info without checking out the worktree. +func InspectRemote(ctx context.Context, info *GitInfo, env []string) (*Inspection, error) { + if info == nil || info.Repository == "" { + return nil, fmt.Errorf("git repository is empty") + } + root, err := os.MkdirTemp("", "devsy-repo-inspect-*") + if err != nil { + return nil, err + } + + repo, err := cloneInspectionRepo(ctx, root, info, env) + if err != nil { + _ = os.RemoveAll(root) + return nil, err + } + rev, err := selectInspectionRevision(ctx, repo, info) + if err != nil { + _ = os.RemoveAll(root) + return nil, err + } + commitSHA, err := resolveCommitSHA(ctx, repo, rev) + if err != nil { + _ = os.RemoveAll(root) + return nil, err + } + subPath, err := cleanInspectionSubPath(info.SubPath) + if err != nil { + _ = os.RemoveAll(root) + return nil, err + } + return &Inspection{repo: repo, rev: commitSHA, root: root, subPath: subPath}, nil +} + +func resolveCommitSHA(ctx context.Context, repo *Repo, rev string) (string, error) { + revResult, err := repo.run(ctx, "rev-parse", rev+"^{commit}") + if err != nil { + return "", fmt.Errorf("resolve immutable commit sha: %w", err) + } + commitSHA := strings.TrimSpace(string(revResult.Stdout)) + if commitSHA == "" { + return "", fmt.Errorf("empty commit sha for revision %q", rev) + } + return commitSHA, nil +} + +func cloneInspectionRepo( + ctx context.Context, + root string, + info *GitInfo, + env []string, +) (*Repo, error) { + target := filepath.Join(root, "repo") + args := []string{"clone", bloblessCloneFilter, "--no-checkout"} + if info.Commit == "" { + args = append(args, "--depth=1") + } + if info.Branch != "" { + args = append(args, "--branch", info.Branch) + } + args = append(args, info.Repository, target) + + bootstrap := At("", WithEnv(env)) + if _, err := bootstrap.runner.Run(ctx, RunOptions{Env: bootstrap.env, Args: args}); err != nil { + return nil, fmt.Errorf("inspect remote repository: %w", err) + } + return At(target, WithEnv(env)), nil +} + +func selectInspectionRevision(ctx context.Context, repo *Repo, info *GitInfo) (string, error) { + if info.PR != "" { + return fetchInspectionPR(ctx, repo, info.Repository, info.PR) + } + if info.Commit != "" { + return fetchInspectionCommit(ctx, repo, info.Commit) + } + return inspectionHeadRev, nil +} + +func fetchInspectionPR( + ctx context.Context, + repo *Repo, + repository, request string, +) (string, error) { + number := prNumber(request) + if number == "" { + return "", fmt.Errorf("invalid pull/merge request reference %q", request) + } + candidates := prCandidates(repository) + if len(candidates) == 0 { + return "", fmt.Errorf("unsupported repository host for pull/merge request %q", repository) + } + var lastErr error + for _, host := range candidates { + refspec := host.Refspec(number) + _, err := repo.run(ctx, "fetch", "--depth=1", "origin", refspec) + if err == nil { + return "FETCH_HEAD", nil + } + lastErr = err + } + return "", fmt.Errorf("fetch request revision: %w", lastErr) +} + +func fetchInspectionCommit(ctx context.Context, repo *Repo, commit string) (string, error) { + if _, err := repo.run(ctx, "cat-file", "-e", commit+"^{commit}"); err == nil { + return commit, nil + } + if _, err := repo.run(ctx, "fetch", "--depth=1", "origin", commit); err == nil { + return "FETCH_HEAD", nil + } + if _, err := repo.run( + ctx, + "fetch", + "origin", + "+refs/heads/*:refs/remotes/origin/*", + ); err == nil { + if _, err := repo.run(ctx, "cat-file", "-e", commit+"^{commit}"); err == nil { + return commit, nil + } + } + if _, err := repo.run(ctx, "fetch", "--unshallow", "origin"); err == nil { + if _, err := repo.run(ctx, "cat-file", "-e", commit+"^{commit}"); err == nil { + return commit, nil + } + } + return "", fmt.Errorf("fetch commit %q: commit not found in remote repository", commit) +} + +// ReadFile returns the bytes for a path relative to the selected subpath +// project root (or the repository root when no subpath is set) at the exact +// revision selected by InspectRemote. +func (i *Inspection) ReadFile(ctx context.Context, filePath string) ([]byte, error) { + if i == nil || i.repo == nil { + return nil, fmt.Errorf("git inspection is closed") + } + cleanPath, err := cleanRepoRelativePath("file path", filePath) + if err != nil { + return nil, fmt.Errorf("read file: %w", err) + } + if cleanPath == "" { + return nil, fmt.Errorf("read file: path must not be empty") + } + if i.subPath != "" { + cleanPath = path.Join(i.subPath, cleanPath) + } + object := i.rev + ":" + cleanPath + if _, err := i.repo.run(ctx, "cat-file", "-e", object); err != nil { + return nil, fmt.Errorf("%w: %s", ErrRevisionPathNotFound, cleanPath) + } + result, err := i.repo.run(ctx, "show", object) + if err != nil { + return nil, fmt.Errorf("read %q from revision %s: %w", cleanPath, i.rev, err) + } + return append([]byte(nil), result.Stdout...), nil +} + +// cleanRepoRelativePath normalizes and validates a repository-relative path +// (either an @subpath: selector or a file path to read), rejecting anything +// absolute or that would escape the repository root once joined with +// another repository-relative path. +func cleanRepoRelativePath(kind, value string) (string, error) { + value = strings.TrimSpace(value) + if value == "" { + return "", nil + } + if err := validateRepoRelativePath(kind, value); err != nil { + return "", err + } + value = strings.TrimPrefix(strings.ReplaceAll(value, `\`, "/"), "/") + clean := path.Clean(value) + if clean == "." { + return "", nil + } + if clean == ".." || strings.HasPrefix(clean, "../") { + return "", fmt.Errorf("git %s %q escapes the repository root", kind, value) + } + return clean, nil +} + +func validateRepoRelativePath(kind, value string) error { + if filepath.VolumeName(value) != "" || isWindowsAbs(value) { + return fmt.Errorf("git %s %q must be relative to the repository root", kind, value) + } + if kind == "file path" && (strings.HasPrefix(value, "/") || strings.HasPrefix(value, `\`)) { + return fmt.Errorf("git %s %q must be relative to the repository root", kind, value) + } + return nil +} + +func isWindowsAbs(value string) bool { + if len(value) >= 2 && value[1] == ':' { + c := value[0] + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { + return true + } + } + return strings.HasPrefix(value, `\\`) || strings.HasPrefix(value, `//`) +} + +// ReadDevContainerConfig locates and reads the devcontainer.json configuration +// from the inspection repository at its pinned revision, searching explicit paths, +// standard root locations (.devcontainer/devcontainer.json, .devcontainer.json), +// and profile-specific paths (.devcontainer//devcontainer.json). +func (i *Inspection) ReadDevContainerConfig( + ctx context.Context, + devContainerPath, devContainerID string, +) ([]byte, string, error) { + if i == nil || i.repo == nil { + return nil, "", fmt.Errorf("git inspection is closed") + } + if devContainerPath != "" { + return i.readExplicitDevContainerConfig(ctx, devContainerPath) + } + if devContainerID != "" { + return i.readProfileDevContainerConfig(ctx, devContainerID) + } + if data, pathFound, err := i.readRootDevContainerConfig( + ctx, + ); err == nil || + !errors.Is(err, ErrRevisionPathNotFound) { + return data, pathFound, err + } + return i.readNestedDevContainerConfig(ctx) +} + +// cleanInspectionSubPath normalizes and validates a repository-relative +// subpath selector (from an @subpath: reference), rejecting anything that +// would escape the repository root once joined with a file path. +func cleanInspectionSubPath(value string) (string, error) { + return cleanRepoRelativePath("subpath", value) +} + +func (i *Inspection) Revision() string { + if i == nil { + return "" + } + return i.rev +} + +func (i *Inspection) Close() error { + if i == nil || i.root == "" { + return nil + } + root := i.root + i.root = "" + i.repo = nil + return os.RemoveAll(root) +} + +func (i *Inspection) readExplicitDevContainerConfig( + ctx context.Context, + devContainerPath string, +) ([]byte, string, error) { + clean, err := cleanRepoRelativePath("devcontainer path", devContainerPath) + if err != nil { + return nil, "", err + } + data, err := i.ReadFile(ctx, clean) + if err != nil { + return nil, "", err + } + return data, clean, nil +} + +func (i *Inspection) readProfileDevContainerConfig( + ctx context.Context, + devContainerID string, +) ([]byte, string, error) { + cleanID, err := cleanRepoRelativePath("devcontainer id", devContainerID) + if err != nil { + return nil, "", err + } + idPath := path.Join(".devcontainer", cleanID, "devcontainer.json") + data, err := i.ReadFile(ctx, idPath) + if err == nil { + return data, idPath, nil + } + if !errors.Is(err, ErrRevisionPathNotFound) { + return nil, "", err + } + return nil, "", fmt.Errorf("devcontainer with ID %q not found in repository", devContainerID) +} + +func (i *Inspection) readRootDevContainerConfig(ctx context.Context) ([]byte, string, error) { + for _, candidate := range []string{ + path.Join(".devcontainer", "devcontainer.json"), + ".devcontainer.json", + } { + data, err := i.ReadFile(ctx, candidate) + if err == nil { + return data, candidate, nil + } + if !errors.Is(err, ErrRevisionPathNotFound) { + return nil, "", err + } + } + return nil, "", ErrRevisionPathNotFound +} + +func (i *Inspection) readNestedDevContainerConfig(ctx context.Context) ([]byte, string, error) { + nested := i.listDevContainerConfigs(ctx) + if len(nested) == 1 { + data, err := i.ReadFile(ctx, nested[0]) + if err != nil { + return nil, "", err + } + return data, nested[0], nil + } + if len(nested) > 1 { + return nil, "", fmt.Errorf("multiple devcontainer configurations found: %v", nested) + } + return nil, "", ErrRevisionPathNotFound +} + +func (i *Inspection) listDevContainerConfigs(ctx context.Context) []string { + treePath := ".devcontainer" + if i.subPath != "" { + treePath = path.Join(i.subPath, treePath) + } + targetTree := i.rev + ":" + treePath + result, err := i.repo.run(ctx, "ls-tree", "--name-only", targetTree) + if err != nil { + return nil + } + entries := strings.Split(strings.TrimSpace(string(result.Stdout)), "\n") + var matches []string + for _, entry := range entries { + entry = strings.TrimSpace(entry) + if entry == "" { + continue + } + cand := path.Join(".devcontainer", entry, "devcontainer.json") + fullCand := cand + if i.subPath != "" { + fullCand = path.Join(i.subPath, cand) + } + if _, err := i.repo.run(ctx, "cat-file", "-e", i.rev+":"+fullCand); err == nil { + matches = append(matches, cand) + } + } + return matches +} diff --git a/pkg/git/inspect_test.go b/pkg/git/inspect_test.go new file mode 100644 index 000000000..9f6bb20af --- /dev/null +++ b/pkg/git/inspect_test.go @@ -0,0 +1,131 @@ +package git + +import ( + "context" + "testing" + + "gotest.tools/assert" +) + +const testSubPath = "apps/foo" + +func TestInspectionReadFileUsesSubPath(t *testing.T) { + runner := &fakeRunner{stdout: []byte("secret-contents")} + repo := At("/tmp/repo", WithRunner(runner)) + inspection := &Inspection{repo: repo, rev: inspectionHeadRev, subPath: testSubPath} + + out, err := inspection.ReadFile(context.Background(), ".devcontainer/devcontainer.json") + assert.NilError(t, err) + assert.Equal(t, string(out), "secret-contents") + + wantObject := inspectionHeadRev + ":" + testSubPath + "/.devcontainer/devcontainer.json" + // cat-file existence check, then show; both must target the subpath. + assert.Equal(t, len(runner.calls), 2) + assert.Equal(t, runner.calls[0].Args[len(runner.calls[0].Args)-1], wantObject) + assert.Equal(t, runner.calls[1].Args[len(runner.calls[1].Args)-1], wantObject) +} + +func TestInspectionReadFileWithoutSubPathUsesRepoRoot(t *testing.T) { + runner := &fakeRunner{stdout: []byte("secret-contents")} + repo := At("/tmp/repo", WithRunner(runner)) + inspection := &Inspection{repo: repo, rev: inspectionHeadRev} + + _, err := inspection.ReadFile(context.Background(), ".devcontainer/devcontainer.json") + assert.NilError(t, err) + wantObject := inspectionHeadRev + ":.devcontainer/devcontainer.json" + assert.Equal(t, runner.calls[0].Args[len(runner.calls[0].Args)-1], wantObject) +} + +func TestCleanInspectionSubPath(t *testing.T) { + for _, tc := range []struct { + name string + value string + want string + wantErr bool + }{ + {name: "empty", value: "", want: ""}, + {name: "dot", value: ".", want: ""}, + {name: "simple", value: testSubPath, want: testSubPath}, + {name: "trailing slash", value: testSubPath + "/", want: testSubPath}, + {name: "leading slash stripped", value: "/apps/bar", want: "apps/bar"}, + {name: "parent escape rejected", value: "../bar", wantErr: true}, + {name: "parent only rejected", value: "..", wantErr: true}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := cleanInspectionSubPath(tc.value) + if tc.wantErr { + assert.Assert(t, err != nil) + return + } + assert.NilError(t, err) + assert.Equal(t, got, tc.want) + }) + } +} + +func TestInspectionReadFileRejectsPathEscape(t *testing.T) { + runner := &fakeRunner{stdout: []byte("secret-contents")} + repo := At("/tmp/repo", WithRunner(runner)) + inspection := &Inspection{repo: repo, rev: inspectionHeadRev, subPath: testSubPath} + + _, err := inspection.ReadFile(context.Background(), "../../etc/passwd") + assert.Assert(t, err != nil) + assert.Equal(t, len(runner.calls), 0) +} + +func TestInspectionReadFileRejectsAbsolutePath(t *testing.T) { + runner := &fakeRunner{stdout: []byte("secret-contents")} + repo := At("/tmp/repo", WithRunner(runner)) + inspection := &Inspection{repo: repo, rev: inspectionHeadRev, subPath: testSubPath} + + for _, bad := range []string{ + "/secrets.enc.yaml", + "/etc/passwd", + `C:\secrets.enc.yaml`, + `\\server\share\secrets.enc.yaml`, + } { + _, err := inspection.ReadFile(context.Background(), bad) + assert.Assert(t, err != nil, bad) + } +} + +func TestInspectionReadDevContainerConfig_RootConfig(t *testing.T) { + runner := &fakeRunner{stdout: []byte(`{"customizations":{"devsy":{}}}`)} + repo := At("/tmp/repo", WithRunner(runner)) + inspection := &Inspection{repo: repo, rev: inspectionHeadRev} + + data, pathFound, err := inspection.ReadDevContainerConfig(context.Background(), "", "") + assert.NilError(t, err) + assert.Equal(t, pathFound, ".devcontainer/devcontainer.json") + assert.Assert(t, len(data) > 0) +} + +func TestInspectionReadDevContainerConfig_ExplicitPath(t *testing.T) { + runner := &fakeRunner{stdout: []byte(`{"customizations":{"devsy":{}}}`)} + repo := At("/tmp/repo", WithRunner(runner)) + inspection := &Inspection{repo: repo, rev: inspectionHeadRev} + + data, pathFound, err := inspection.ReadDevContainerConfig( + context.Background(), + "custom/devcontainer.json", + "", + ) + assert.NilError(t, err) + assert.Equal(t, pathFound, "custom/devcontainer.json") + assert.Assert(t, len(data) > 0) +} + +func TestInspectionReadDevContainerConfig_DevContainerID(t *testing.T) { + runner := &fakeRunner{stdout: []byte(`{"customizations":{"devsy":{}}}`)} + repo := At("/tmp/repo", WithRunner(runner)) + inspection := &Inspection{repo: repo, rev: inspectionHeadRev} + + data, pathFound, err := inspection.ReadDevContainerConfig( + context.Background(), + "", + "my-profile", + ) + assert.NilError(t, err) + assert.Equal(t, pathFound, ".devcontainer/my-profile/devcontainer.json") + assert.Assert(t, len(data) > 0) +} diff --git a/pkg/git/repo_test.go b/pkg/git/repo_test.go index bf0cc1dac..7b089d7dc 100644 --- a/pkg/git/repo_test.go +++ b/pkg/git/repo_test.go @@ -5,6 +5,7 @@ import ( "errors" "testing" + "github.com/devsy-org/devsy/pkg/command" "gotest.tools/assert" "gotest.tools/assert/cmp" ) @@ -55,6 +56,13 @@ func (f *fakeRunner) lastArgs() []string { return f.calls[len(f.calls)-1].Args } +func expectedCloneArgs(base ...string) []string { + if !command.Exists(binGitLFS) { + return append(append([]string{}, base...), lfsDisableFilterArgs...) + } + return append([]string{}, base...) +} + func TestRepoFetch(t *testing.T) { fake := &fakeRunner{} repo := At("/tmp/repo", WithRunner(fake)) @@ -195,10 +203,10 @@ func TestRepoCloneArgsThroughRunner(t *testing.T) { WithBranch(testBranch), ) assert.NilError(t, err) - assert.DeepEqual(t, []string{ + assert.DeepEqual(t, expectedCloneArgs( subClone, "--depth=1", flagBranch, testBranch, testRepoURL, testTarget, flagProgress, - }, fake.lastArgs()) + ), fake.lastArgs()) } func TestRepoEnvThreadedToRunner(t *testing.T) { @@ -231,10 +239,10 @@ func TestRepoCloneFromInfoBranch(t *testing.T) { assert.NilError(t, repo.CloneFromInfo(context.Background(), info, "")) // Branch becomes a clone flag; no separate checkout call. - assert.DeepEqual(t, []string{ + assert.DeepEqual(t, expectedCloneArgs( subClone, flagBranch, testBranch, testRepoURL, testTarget, flagProgress, - }, fake.calls[0].Args) + ), fake.calls[0].Args) } func TestRepoCloneFromInfoCommit(t *testing.T) { @@ -265,8 +273,8 @@ func TestRepoCloneFromInfoHelper(t *testing.T) { info := &GitInfo{Repository: "https://host/org/repo.git"} assert.NilError(t, repo.CloneFromInfo(context.Background(), info, "store")) - assert.DeepEqual(t, []string{ + assert.DeepEqual(t, expectedCloneArgs( subClone, flagConfig, "credential.helper=store", "https://host/org/repo.git", testTarget, flagProgress, - }, fake.calls[0].Args) + ), fake.calls[0].Args) } diff --git a/pkg/secrets/local_source.go b/pkg/secrets/local_source.go new file mode 100644 index 000000000..5d8de2549 --- /dev/null +++ b/pkg/secrets/local_source.go @@ -0,0 +1,36 @@ +package secrets + +import ( + "context" + "fmt" +) + +// LocalSource adapts the Devsy Store to the generic source interface. +type LocalSource struct { + store Store + context string +} + +func NewLocalSource(store Store, contextName string) *LocalSource { + return &LocalSource{store: store, context: contextName} +} + +func (s *LocalSource) Get(_ context.Context, name string) (ResolvedSecret, error) { + if s == nil || s.store == nil { + return ResolvedSecret{}, fmt.Errorf("local secret source is not configured") + } + value, err := s.store.Get(s.context, name) + if err != nil { + return ResolvedSecret{}, err + } + meta, err := s.store.Meta(s.context, name) + if err != nil { + return ResolvedSecret{}, err + } + return ResolvedSecret{ + Name: name, + Value: value, + Sensitive: meta.Sensitive(), + Source: LocalSourceName, + }, nil +} diff --git a/pkg/secrets/project_config.go b/pkg/secrets/project_config.go new file mode 100644 index 000000000..2543f3c45 --- /dev/null +++ b/pkg/secrets/project_config.go @@ -0,0 +1,317 @@ +package secrets + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path" + "path/filepath" + "strings" + + "github.com/devsy-org/devsy/pkg/config" + "github.com/tailscale/hujson" + "sigs.k8s.io/yaml" +) + +// ProjectConfig is the repository-owned subset of Devsy configuration used by +// secret discovery. +type ProjectConfig struct { + SecretSources []SourceConfig `json:"secretSources,omitempty" yaml:"secretSources,omitempty"` + Secrets []string `json:"secrets,omitempty" yaml:"secrets,omitempty"` +} + +type devContainerCustomizationsWrapper struct { + Customizations map[string]json.RawMessage `json:"customizations"` +} + +func ParseProjectConfig(data []byte) (*ProjectConfig, error) { + if cfg, handled, err := parseDevContainerCustomizations(data); handled { + return cfg, err + } + cfg := &ProjectConfig{} + if err := yaml.Unmarshal(data, cfg); err != nil { + return nil, fmt.Errorf("parse project secret configuration: %w", err) + } + if err := ValidateProjectConfig(cfg); err != nil { + return nil, err + } + return cfg, nil +} + +func parseDevContainerCustomizations(data []byte) (*ProjectConfig, bool, error) { + normalized, err := hujson.Standardize(data) + if err != nil { + return nil, false, nil + } + var wrapper devContainerCustomizationsWrapper + if err := json.Unmarshal(normalized, &wrapper); err != nil || wrapper.Customizations == nil { + return nil, false, nil + } + rawDevsy := wrapper.Customizations["devsy"] + if len(rawDevsy) == 0 { + rawDevsy = wrapper.Customizations[config.BinaryName] + } + if len(rawDevsy) == 0 { + return nil, true, nil + } + cfg := &ProjectConfig{} + if err := json.Unmarshal(rawDevsy, cfg); err != nil { + return nil, true, fmt.Errorf("parse customizations.devsy: %w", err) + } + if err := ValidateProjectConfig(cfg); err != nil { + return nil, true, err + } + return cfg, true, nil +} + +func ValidateProjectConfig(cfg *ProjectConfig) error { + if cfg == nil { + return nil + } + seen, err := validateProjectSources(cfg.SecretSources) + if err != nil { + return err + } + return validateProjectSecrets(cfg.Secrets, seen) +} + +func validateProjectSources(sources []SourceConfig) (map[string]struct{}, error) { + seen := make(map[string]struct{}, len(sources)) + for i, source := range sources { + if err := validateProjectSource(i, source, seen); err != nil { + return nil, err + } + seen[source.Name] = struct{}{} + } + return seen, nil +} + +func validateProjectSource(index int, source SourceConfig, seen map[string]struct{}) error { + if err := ValidateSourceName(source.Name); err != nil { + return fmt.Errorf("secretSources[%d]: %w", index, err) + } + if source.Name == LocalSourceName { + return fmt.Errorf("secretSources[%d]: source name %q is reserved", index, LocalSourceName) + } + if source.Type != SOPSFormatter { + return fmt.Errorf("secretSources[%d]: unsupported source type %q", index, source.Type) + } + if _, exists := seen[source.Name]; exists { + return fmt.Errorf("duplicate project secret source %q", source.Name) + } + if _, err := CleanProjectSourcePath(source.Path); err != nil { + return fmt.Errorf("secret source %q: %w", source.Name, err) + } + return nil +} + +func validateProjectSecrets(values []string, sources map[string]struct{}) error { + for _, value := range values { + if err := validateProjectSecret(value, sources); err != nil { + return err + } + } + return nil +} + +func validateProjectSecret(value string, sources map[string]struct{}) error { + ref, err := ParseRef(value) + if err != nil { + return fmt.Errorf("project secret %q: %w", value, err) + } + if ref.Source == LocalSourceName { + return fmt.Errorf("project configuration may not attach local secret %q", ref.Name) + } + if _, ok := sources[ref.Source]; !ok { + return fmt.Errorf("project secret %q references undefined source %q", value, ref.Source) + } + return nil +} + +// CleanProjectSourcePath validates a repository-controlled source path and +// returns a normalized repository-relative slash path. +func CleanProjectSourcePath(value string) (string, error) { + value = strings.TrimSpace(value) + if value == "" { + return "", fmt.Errorf("source path must not be empty") + } + if err := validateRelativePath(value); err != nil { + return "", err + } + normalized := strings.ReplaceAll(value, `\`, "/") + clean := path.Clean(normalized) + if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") { + return "", fmt.Errorf("source path %q escapes the repository root", value) + } + return strings.TrimPrefix(clean, "./"), nil +} + +func validateRelativePath(value string) error { + if filepath.VolumeName(value) != "" || isWindowsAbs(value) { + return fmt.Errorf("source path %q must be relative to the repository root", value) + } + if strings.HasPrefix(value, "/") || strings.HasPrefix(value, `\`) { + return fmt.Errorf("source path %q must be relative to the repository root", value) + } + return nil +} + +func isWindowsAbs(value string) bool { + if len(value) >= 2 && value[1] == ':' { + c := value[0] + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { + return true + } + } + return strings.HasPrefix(value, `\\`) || strings.HasPrefix(value, `//`) +} + +// LoadProjectConfigFromRoot loads repository-owned config from a local checkout. +// A missing configuration is not an error. +func LoadProjectConfigFromRoot(root string) (*ProjectConfig, bool, error) { + return LoadProjectConfigFromRootWithOptions(root, "", "") +} + +// LoadProjectConfigFromRootWithOptions loads repository-owned config from a local checkout, +// checking the specified devcontainer path, conventional root devcontainer locations, +// and profile-specific devcontainer directories for customizations.devsy. +func LoadProjectConfigFromRootWithOptions( + root, devContainerPath, devContainerID string, +) (*ProjectConfig, bool, error) { + candidates, err := devContainerCandidates(devContainerPath, devContainerID, root) + if err != nil { + return nil, false, err + } + for _, relPath := range candidates { + if cfg, found, err := loadCandidateConfig(root, relPath); found || err != nil { + return cfg, found, err + } + } + return nil, false, nil +} + +func devContainerCandidates(devContainerPath, devContainerID, root string) ([]string, error) { + if devContainerPath != "" { + clean, err := CleanProjectSourcePath(devContainerPath) + if err != nil { + return nil, err + } + return []string{clean}, nil + } + candidates := []string{ + path.Join(".devcontainer", "devcontainer.json"), + ".devcontainer.json", + } + if devContainerID != "" { + cleanID, err := CleanProjectSourcePath(devContainerID) + if err != nil { + return nil, fmt.Errorf("invalid devcontainer id %q: %w", devContainerID, err) + } + return []string{path.Join(".devcontainer", cleanID, "devcontainer.json")}, nil + } + if nested := findNestedDevContainer(root); nested != "" { + candidates = append(candidates, nested) + } + return candidates, nil +} + +func findNestedDevContainer(root string) string { + devcontainerDir := filepath.Join(root, ".devcontainer") + entries, err := os.ReadDir(devcontainerDir) + if err != nil { + return "" + } + var nested []string + for _, entry := range entries { + if !entry.IsDir() { + continue + } + cand := filepath.Join(devcontainerDir, entry.Name(), "devcontainer.json") + if _, err := os.Stat(cand); err == nil { + nested = append(nested, path.Join(".devcontainer", entry.Name(), "devcontainer.json")) + } + } + if len(nested) == 1 { + return nested[0] + } + return "" +} + +func loadCandidateConfig(root, relPath string) (*ProjectConfig, bool, error) { + fullPath := filepath.Join(root, filepath.FromSlash(relPath)) + // #nosec G304 -- fullPath is rooted under the selected repository. + data, err := os.ReadFile(fullPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, false, nil + } + return nil, false, fmt.Errorf("read %s: %w", relPath, err) + } + cfg, err := ParseProjectConfig(data) + if err != nil { + return nil, false, fmt.Errorf("parse %s: %w", relPath, err) + } + if cfg != nil && (len(cfg.SecretSources) > 0 || len(cfg.Secrets) > 0) { + return cfg, true, nil + } + return nil, false, nil +} + +// ResolveProjectSourcePath converts a repository-controlled relative path to a +// local path while enforcing containment, including after symlink resolution. +func ResolveProjectSourcePath(root, value string) (string, error) { + candidate, resolvedRoot, err := projectSourceCandidate(root, value) + if err != nil { + return "", err + } + resolved, err := filepath.EvalSymlinks(candidate) + if err != nil { + return "", fmt.Errorf("resolve secret source path %q: %w", value, err) + } + if err := ensurePathContained(resolvedRoot, resolved, value); err != nil { + return "", err + } + if err := ensureRegularFile(resolved, value); err != nil { + return "", err + } + return resolved, nil +} + +func projectSourceCandidate(root, value string) (string, string, error) { + clean, err := CleanProjectSourcePath(value) + if err != nil { + return "", "", err + } + rootAbs, err := filepath.Abs(root) + if err != nil { + return "", "", err + } + resolvedRoot, err := filepath.EvalSymlinks(rootAbs) + if err != nil { + return "", "", fmt.Errorf("resolve repository root: %w", err) + } + return filepath.Join(rootAbs, filepath.FromSlash(clean)), resolvedRoot, nil +} + +func ensurePathContained(root, candidate, original string) error { + rel, err := filepath.Rel(root, candidate) + if err != nil { + return err + } + if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return fmt.Errorf("source path %q escapes the repository root", original) + } + return nil +} + +func ensureRegularFile(filePath, original string) error { + info, err := os.Stat(filePath) + if err != nil { + return fmt.Errorf("stat secret source path %q: %w", original, err) + } + if !info.Mode().IsRegular() { + return fmt.Errorf("secret source path %q is not a regular file", original) + } + return nil +} diff --git a/pkg/secrets/project_config_test.go b/pkg/secrets/project_config_test.go new file mode 100644 index 000000000..e22636b5b --- /dev/null +++ b/pkg/secrets/project_config_test.go @@ -0,0 +1,145 @@ +package secrets + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseProjectConfig(t *testing.T) { + cfg, err := ParseProjectConfig([]byte(` +secretSources: + - name: project + type: sops + path: ./secrets.enc.yaml +secrets: + - sops:project/API_TOKEN +`)) + require.NoError(t, err) + require.Len(t, cfg.SecretSources, 1) + require.Equal(t, "project", cfg.SecretSources[0].Name) + require.Equal(t, []string{"sops:project/API_TOKEN"}, cfg.Secrets) +} + +func TestParseProjectConfig_DevContainerCustomizations(t *testing.T) { + cfg, err := ParseProjectConfig([]byte(`{ + // DevContainer with devsy customizations + "name": "my-project", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "customizations": { + "devsy": { + "secretSources": [ + { + "name": "project", + "type": "sops", + "path": "./secrets.enc.yaml", + }, + ], + "secrets": [ + "sops:project/API_KEY", + ], + }, + }, +}`)) + require.NoError(t, err) + require.NotNil(t, cfg) + require.Len(t, cfg.SecretSources, 1) + require.Equal(t, "project", cfg.SecretSources[0].Name) + require.Equal(t, []string{"sops:project/API_KEY"}, cfg.Secrets) +} + +func TestParseProjectConfig_DevContainerWithoutCustomizations(t *testing.T) { + cfg, err := ParseProjectConfig([]byte(`{ + "name": "my-project", + "image": "ubuntu", + "customizations": { + "vscode": { + "extensions": ["golang.go"] + } + } +}`)) + require.NoError(t, err) + require.Nil(t, cfg) +} + +func TestProjectConfigRejectsUndefinedSource(t *testing.T) { + _, err := ParseProjectConfig([]byte(`secrets: [sops:missing/API_TOKEN]`)) + require.ErrorContains(t, err, "undefined source") +} + +func TestCleanProjectSourcePath(t *testing.T) { + clean, err := CleanProjectSourcePath("./config/secrets.enc.yaml") + require.NoError(t, err) + require.Equal(t, "config/secrets.enc.yaml", clean) + + for _, bad := range []string{ + "", + "/config/secrets.enc.yaml", + "/secrets.enc.yaml", + "/etc/passwd", + `C:\secrets.enc.yaml`, + `\\server\share\secrets.enc.yaml`, + "../secret", + "a/../../secret", + "..", + ".", + } { + _, err := CleanProjectSourcePath(bad) + require.Error(t, err, bad) + } + for _, good := range []string{ + "./secrets.enc.yaml", + "config/secrets.enc.yaml", + "secrets.enc.yaml", + } { + clean, err := CleanProjectSourcePath(good) + require.NoError(t, err, good) + require.False(t, filepath.IsAbs(clean)) + } +} + +func TestLoadProjectConfigFromRoot_DevContainerCustomizations(t *testing.T) { + root := t.TempDir() + devcontainerDir := filepath.Join(root, ".devcontainer") + require.NoError(t, os.MkdirAll(devcontainerDir, 0o750)) + require.NoError(t, os.WriteFile( + filepath.Join(devcontainerDir, "devcontainer.json"), + []byte(`{ + "customizations": { + "devsy": { + "secretSources": [ + {"name": "project", "type": "sops", "path": "secrets.enc.yaml"} + ], + "secrets": ["sops:project/KEY"] + } + } +}`), + 0o600, + )) + + cfg, found, err := LoadProjectConfigFromRoot(root) + require.NoError(t, err) + require.True(t, found) + require.NotNil(t, cfg) + require.Len(t, cfg.SecretSources, 1) + require.Equal(t, "project", cfg.SecretSources[0].Name) + require.Equal(t, []string{"sops:project/KEY"}, cfg.Secrets) +} + +func TestResolveProjectSourcePathRejectsSymlinkEscape(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlink creation commonly requires elevated privileges on Windows") + } + root := t.TempDir() + outside := t.TempDir() + outsideSecret := filepath.Join(outside, "secret.yaml") + require.NoError(t, os.WriteFile(outsideSecret, []byte("secret"), 0o600)) + require.NoError(t, os.MkdirAll(filepath.Join(root, "config"), 0o700)) + require.NoError(t, os.Symlink(outsideSecret, filepath.Join(root, "config", "secret.yaml"))) + + _, err := ResolveProjectSourcePath(root, "config/secret.yaml") + require.ErrorContains(t, err, "escapes the repository root") +} diff --git a/pkg/secrets/reference.go b/pkg/secrets/reference.go new file mode 100644 index 000000000..ec7d1d404 --- /dev/null +++ b/pkg/secrets/reference.go @@ -0,0 +1,82 @@ +package secrets + +import ( + "fmt" + "strings" +) + +const LocalSourceName = "local" + +const qualifiedRefFormat = "TYPE:SOURCE/NAME" + +// SecretRef identifies a named secret and the source instance that owns it. +// Unqualified references resolve from the local Devsy store. Qualified +// references use TYPE:SOURCE/NAME, for example sops:project/API_TOKEN. +type SecretRef struct { + Type string + Source string + Name string +} + +func ParseRef(value string) (SecretRef, error) { + value = strings.TrimSpace(value) + if value == "" { + return SecretRef{}, fmt.Errorf("invalid secret reference: value must not be empty") + } + if !strings.Contains(value, ":") { + return parseLocalRef(value) + } + return parseQualifiedRef(value) +} + +func parseLocalRef(value string) (SecretRef, error) { + if err := ValidateName(value); err != nil { + return SecretRef{}, err + } + return SecretRef{Type: LocalSourceName, Source: LocalSourceName, Name: value}, nil +} + +func parseQualifiedRef(value string) (SecretRef, error) { + typeName, rest, err := splitQualifiedType(value) + if err != nil { + return SecretRef{}, err + } + sourceName, name, err := splitQualifiedName(value, rest) + if err != nil { + return SecretRef{}, err + } + if err := ValidateSourceName(sourceName); err != nil { + return SecretRef{}, fmt.Errorf("invalid secret reference %q: %w", value, err) + } + if err := ValidateName(name); err != nil { + return SecretRef{}, fmt.Errorf("invalid secret reference %q: %w", value, err) + } + return SecretRef{Type: typeName, Source: sourceName, Name: name}, nil +} + +func splitQualifiedType(value string) (string, string, error) { + typeName, rest, ok := strings.Cut(value, ":") + if !ok || typeName == "" || rest == "" { + return "", "", invalidQualifiedRef(value) + } + return typeName, rest, nil +} + +func splitQualifiedName(value, rest string) (string, string, error) { + sourceName, name, ok := strings.Cut(rest, "/") + if !ok || sourceName == "" || name == "" || strings.Contains(name, "/") { + return "", "", invalidQualifiedRef(value) + } + return sourceName, name, nil +} + +func invalidQualifiedRef(value string) error { + return fmt.Errorf("invalid secret reference %q: expected %s", value, qualifiedRefFormat) +} + +func (r SecretRef) String() string { + if r.Source == LocalSourceName && (r.Type == "" || r.Type == LocalSourceName) { + return r.Name + } + return r.Type + ":" + r.Source + "/" + r.Name +} diff --git a/pkg/secrets/reference_test.go b/pkg/secrets/reference_test.go new file mode 100644 index 000000000..98110babd --- /dev/null +++ b/pkg/secrets/reference_test.go @@ -0,0 +1,56 @@ +package secrets + +import "testing" + +const ( + testAPISecret = "API_TOKEN" + testProjectSource = "project" +) + +func TestParseRef(t *testing.T) { + tests := []struct { + name string + input string + want SecretRef + }{ + { + name: "local", + input: testAPISecret, + want: SecretRef{Type: LocalSourceName, Source: LocalSourceName, Name: testAPISecret}, + }, + { + name: SOPSFormatter, + input: SOPSFormatter + ":" + testProjectSource + "/" + testAPISecret, + want: SecretRef{Type: SOPSFormatter, Source: testProjectSource, Name: testAPISecret}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseRef(tt.input) + if err != nil { + t.Fatal(err) + } + if got != tt.want { + t.Fatalf("ParseRef(%q) = %#v, want %#v", tt.input, got, tt.want) + } + }) + } +} + +func TestParseRefRejectsMalformedQualifiedRefs(t *testing.T) { + for _, value := range []string{ + "", + "sops:", + "sops:project", + "sops:/TOKEN", + "sops:project/", + "sops:project/a/b", + "sops:project/API=TOKEN", + } { + t.Run(value, func(t *testing.T) { + if _, err := ParseRef(value); err == nil { + t.Fatalf("ParseRef(%q) unexpectedly succeeded", value) + } + }) + } +} diff --git a/pkg/secrets/resolver.go b/pkg/secrets/resolver.go new file mode 100644 index 000000000..2d1664b1b --- /dev/null +++ b/pkg/secrets/resolver.go @@ -0,0 +1,81 @@ +package secrets + +import ( + "context" + "fmt" + "regexp" +) + +var sourceNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`) + +type registeredSource struct { + typeName string + source Source +} + +// Resolver routes a SecretRef to an explicitly registered source instance. +type Resolver struct { + sources map[string]registeredSource +} + +func NewResolver() *Resolver { + return &Resolver{sources: map[string]registeredSource{}} +} + +func ValidateSourceName(name string) error { + if !sourceNamePattern.MatchString(name) { + return fmt.Errorf( + "invalid secret source name %q: must start with a letter and contain only letters, digits, underscores, and hyphens", + name, + ) + } + return nil +} + +func (r *Resolver) Register(name, typeName string, source Source) error { + if err := ValidateSourceName(name); err != nil { + return err + } + if source == nil { + return fmt.Errorf("secret source %q is nil", name) + } + if _, exists := r.sources[name]; exists { + return fmt.Errorf("secret source %q already exists", name) + } + if name == LocalSourceName && typeName != LocalSourceName { + return fmt.Errorf("secret source name %q is reserved", LocalSourceName) + } + r.sources[name] = registeredSource{typeName: typeName, source: source} + return nil +} + +func (r *Resolver) Resolve(ctx context.Context, ref SecretRef) (ResolvedSecret, error) { + registered, ok := r.sources[ref.Source] + if !ok { + return ResolvedSecret{}, fmt.Errorf("secret source %q is not configured", ref.Source) + } + if ref.Type != "" && registered.typeName != "" && ref.Type != registered.typeName { + return ResolvedSecret{}, fmt.Errorf( + "secret source %q is type %q, not %q", + ref.Source, + registered.typeName, + ref.Type, + ) + } + resolved, err := registered.source.Get(ctx, ref.Name) + if err != nil { + return ResolvedSecret{}, fmt.Errorf( + "resolve secret %q from source %q: %w", + ref.Name, + ref.Source, + err, + ) + } + if resolved.Source == "" { + resolved.Source = ref.Source + } + if resolved.Name == "" { + resolved.Name = ref.Name + } + return resolved, nil +} diff --git a/pkg/secrets/resolver_test.go b/pkg/secrets/resolver_test.go new file mode 100644 index 000000000..46aec6b67 --- /dev/null +++ b/pkg/secrets/resolver_test.go @@ -0,0 +1,74 @@ +package secrets + +import ( + "context" + "errors" + "testing" +) + +const testResolverToken = "TOKEN" + +type testSource struct { + value string + err error + calls int +} + +func (s *testSource) Get(_ context.Context, name string) (ResolvedSecret, error) { + s.calls++ + if s.err != nil { + return ResolvedSecret{}, s.err + } + return ResolvedSecret{Name: name, Value: s.value, Sensitive: true}, nil +} + +func TestResolverRoutesExplicitSource(t *testing.T) { + r := NewResolver() + s := &testSource{value: "secret"} + if err := r.Register(testProjectSource, SOPSFormatter, s); err != nil { + t.Fatal(err) + } + got, err := r.Resolve( + context.Background(), + SecretRef{Type: SOPSFormatter, Source: testProjectSource, Name: testResolverToken}, + ) + if err != nil { + t.Fatal(err) + } + if got.Value != "secret" || got.Source != testProjectSource || got.Name != testResolverToken { + t.Fatalf("unexpected resolved secret: %#v", got) + } +} + +func TestResolverDoesNotFallback(t *testing.T) { + r := NewResolver() + local := &testSource{value: LocalSourceName} + if err := r.Register(LocalSourceName, LocalSourceName, local); err != nil { + t.Fatal(err) + } + _, err := r.Resolve( + context.Background(), + SecretRef{Type: SOPSFormatter, Source: "missing", Name: testResolverToken}, + ) + if err == nil { + t.Fatal("expected missing source error") + } + if local.calls != 0 { + t.Fatalf("local source was called %d times", local.calls) + } +} + +func TestResolverWrapsSourceError(t *testing.T) { + r := NewResolver() + s := &testSource{err: ErrSecretNotFound} + if err := r.Register(testProjectSource, SOPSFormatter, s); err != nil { + t.Fatal(err) + } + _, err := r.Resolve( + context.Background(), + SecretRef{Type: SOPSFormatter, Source: testProjectSource, Name: testResolverToken}, + ) + if !errors.Is(err, ErrSecretNotFound) { + t.Fatalf("expected ErrSecretNotFound, got %v", err) + } +} diff --git a/pkg/secrets/sops_source.go b/pkg/secrets/sops_source.go new file mode 100644 index 000000000..fdec2a11b --- /dev/null +++ b/pkg/secrets/sops_source.go @@ -0,0 +1,244 @@ +package secrets + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/getsops/sops/v3/decrypt" + "github.com/joho/godotenv" + "gopkg.in/yaml.v3" +) + +const ( + SOPSFormatter = "sops" + SOPSFormatYAML = "yaml" + SOPSFormatJSON = "json" + SOPSFormatDotenv = "dotenv" +) + +// SOPSSource resolves values from one SOPS-encrypted document. A source is +// command-scoped: decrypted values are cached in memory for the lifetime of the +// source instance. +type SOPSSource struct { + name string + path string + format string + encrypted []byte + + once sync.Once + data map[string]string + err error + loadHook func() +} + +func NewSOPSSource(name, filePath, format string) *SOPSSource { + return &SOPSSource{name: name, path: filePath, format: format} +} + +// NewSOPSDataSource is used for repository inspection where the encrypted file +// is read directly from a Git revision without being materialized on disk. +func NewSOPSDataSource(name, logicalPath, format string, encrypted []byte) *SOPSSource { + return &SOPSSource{ + name: name, + path: logicalPath, + format: format, + encrypted: append([]byte(nil), encrypted...), + } +} + +func (s *SOPSSource) Get(ctx context.Context, name string) (ResolvedSecret, error) { + if err := ctx.Err(); err != nil { + return ResolvedSecret{}, err + } + if s == nil { + return ResolvedSecret{}, fmt.Errorf("SOPS secret source is nil") + } + s.once.Do(func() { s.data, s.err = s.load(context.Background()) }) + if err := ctx.Err(); err != nil { + return ResolvedSecret{}, err + } + if s.err != nil { + return ResolvedSecret{}, s.err + } + value, ok := s.data[name] + if !ok { + return ResolvedSecret{}, fmt.Errorf( + "secret %q was not found in SOPS source %q", + name, + s.name, + ) + } + return ResolvedSecret{Name: name, Value: value, Sensitive: true, Source: s.name}, nil +} + +// Validate forces decryption and document validation without exposing values. +func (s *SOPSSource) Validate(ctx context.Context) error { + if err := ctx.Err(); err != nil { + return err + } + s.once.Do(func() { s.data, s.err = s.load(context.Background()) }) + if err := ctx.Err(); err != nil { + return err + } + return s.err +} + +func (s *SOPSSource) load(ctx context.Context) (map[string]string, error) { + if s.loadHook != nil { + s.loadHook() + } + if err := ctx.Err(); err != nil { + return nil, err + } + format, err := normalizeSOPSFormat(s.format, s.path) + if err != nil { + return nil, err + } + plaintext, err := s.decrypt(format) + if err != nil { + return nil, err + } + values, err := parseSOPSDocument(plaintext, format) + if err != nil { + return nil, fmt.Errorf("failed to load SOPS source %q: %w", s.name, err) + } + return values, nil +} + +func (s *SOPSSource) decrypt(format string) ([]byte, error) { + var ( + plaintext []byte + err error + ) + if s.encrypted != nil { + plaintext, err = decrypt.Data(s.encrypted, format) + } else { + if s.path == "" { + return nil, fmt.Errorf("SOPS source %q has no path", s.name) + } + if _, statErr := os.Stat(s.path); statErr != nil { + return nil, fmt.Errorf("read SOPS source %q: %w", s.name, statErr) + } + plaintext, err = decrypt.File(s.path, format) + } + if err != nil { + // Error must not include encrypted/decrypted document contents. + return nil, fmt.Errorf("failed to decrypt SOPS source %q: %w", s.name, err) + } + return plaintext, nil +} + +func normalizeSOPSFormat(explicit, filePath string) (string, error) { + format := strings.ToLower(strings.TrimSpace(explicit)) + if format == "" { + return inferSOPSFormat(filePath) + } + return canonicalSOPSFormat(format) +} + +func inferSOPSFormat(filePath string) (string, error) { + switch strings.ToLower(filepath.Ext(filePath)) { + case ".yaml", ".yml": + return SOPSFormatYAML, nil + case ".json": + return SOPSFormatJSON, nil + case ".env": + return SOPSFormatDotenv, nil + default: + return "", fmt.Errorf( + "cannot determine SOPS format for %q; set format to yaml, json, or dotenv", + filePath, + ) + } +} + +func canonicalSOPSFormat(format string) (string, error) { + switch format { + case SOPSFormatYAML, SOPSFormatJSON, SOPSFormatDotenv: + return format, nil + case "env": + return SOPSFormatDotenv, nil + default: + return "", fmt.Errorf( + "unsupported SOPS format %q; expected yaml, json, or dotenv", + format, + ) + } +} + +// decodeSOPSJSON decodes a single JSON value from plaintext, preserving +// large integers via json.Number, and rejects any trailing content after +// the first value (e.g. a second top-level value or stray text) that +// json.Decoder would otherwise silently ignore. +func decodeSOPSJSON(plaintext []byte, raw *map[string]any) error { + decoder := json.NewDecoder(bytes.NewReader(plaintext)) + decoder.UseNumber() + if err := decoder.Decode(raw); err != nil { + return err + } + var extra json.RawMessage + if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) { + if err == nil { + return fmt.Errorf("unexpected trailing data after JSON document") + } + return fmt.Errorf("unexpected trailing data after JSON document: %w", err) + } + return nil +} + +func parseSOPSDocument(plaintext []byte, format string) (map[string]string, error) { + if format == SOPSFormatDotenv { + values, err := godotenv.Unmarshal(string(plaintext)) + if err != nil { + return nil, fmt.Errorf("parse dotenv document: %w", err) + } + return values, nil + } + + var raw map[string]any + var err error + if format == SOPSFormatJSON { + err = decodeSOPSJSON(plaintext, &raw) + } else { + err = yaml.Unmarshal(plaintext, &raw) + } + if err != nil { + return nil, fmt.Errorf("parse %s document: %w", format, err) + } + if raw == nil { + return map[string]string{}, nil + } + + out := make(map[string]string, len(raw)) + for key, value := range raw { + scalar, err := stringifySecretScalar(value) + if err != nil { + return nil, fmt.Errorf("secret %q: %w", key, err) + } + out[key] = scalar + } + return out, nil +} + +func stringifySecretScalar(value any) (string, error) { + switch v := value.(type) { + case string: + return v, nil + case bool, int, int8, int16, int32, int64, + uint, uint8, uint16, uint32, uint64, + float32, float64, json.Number: + return fmt.Sprint(v), nil + case nil: + return "", fmt.Errorf("value is null; only scalar non-null values are supported") + default: + return "", fmt.Errorf("value is not a scalar; nested objects and arrays are not supported") + } +} diff --git a/pkg/secrets/sops_source_test.go b/pkg/secrets/sops_source_test.go new file mode 100644 index 000000000..e8eca87ed --- /dev/null +++ b/pkg/secrets/sops_source_test.go @@ -0,0 +1,162 @@ +package secrets + +import ( + "context" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + testSOPSPlaintextMarker = "SUPER_SECRET_TEST_VALUE_7B91" + testSOPSFixturePath = "testdata/sops-age.yaml" + // Synthetic test-only identity generated solely for this fixture. gitleaks:allow. + testSOPSAgeIdentity = "AGE-SECRET-KEY-12UWYSAH2MRDQ5K4EWC4253PDTCSCS32Y5EFQ8TEN2SL3QYU2GN2SG88CZX" +) + +func encryptedSOPSTestFixture(t *testing.T) []byte { + t.Helper() + data, err := os.ReadFile(testSOPSFixturePath) + require.NoError(t, err) + return data +} + +func TestSOPSSourceDecryptsAgeFixture(t *testing.T) { + t.Setenv("SOPS_AGE_KEY", testSOPSAgeIdentity) + source := NewSOPSDataSource( + "test", + "secrets.enc.yaml", + SOPSFormatYAML, + encryptedSOPSTestFixture(t), + ) + + secret, err := source.Get(context.Background(), "SOPS_E2E_SECRET") + require.NoError(t, err) + require.Equal(t, testSOPSPlaintextMarker, secret.Value) + require.True(t, secret.Sensitive) + + mounted, err := source.Get(context.Background(), "TLS_KEY") + require.NoError(t, err) + require.Equal(t, "mounted-value-77", mounted.Value) +} + +func TestSOPSSourceDecryptFailureDoesNotLeakPlaintext(t *testing.T) { + t.Setenv("SOPS_AGE_KEY", "") + t.Setenv("SOPS_AGE_KEY_FILE", t.TempDir()+"/missing") + source := NewSOPSDataSource( + "test", + "secrets.enc.yaml", + SOPSFormatYAML, + encryptedSOPSTestFixture(t), + ) + + err := source.Validate(context.Background()) + require.Error(t, err) + require.NotContains(t, err.Error(), testSOPSPlaintextMarker) + require.NotContains(t, strings.ToLower(err.Error()), "mounted-value-77") +} + +func TestParseSOPSDocumentYAML(t *testing.T) { + values, err := parseSOPSDocument( + []byte("API_TOKEN: secret\nPORT: 5432\nDEBUG: false\n"), + SOPSFormatYAML, + ) + require.NoError(t, err) + require.Equal(t, "secret", values["API_TOKEN"]) + require.Equal(t, "5432", values["PORT"]) + require.Equal(t, "false", values["DEBUG"]) +} + +func TestParseSOPSDocumentJSON(t *testing.T) { + values, err := parseSOPSDocument( + []byte(`{"API_TOKEN":"secret","PORT":5432}`), + SOPSFormatJSON, + ) + require.NoError(t, err) + require.Equal(t, "secret", values["API_TOKEN"]) + require.Equal(t, "5432", values["PORT"]) +} + +func TestParseSOPSDocumentJSONPreservesLargeIntegers(t *testing.T) { + values, err := parseSOPSDocument( + []byte(`{"API_TOKEN":"secret","COUNT":9007199254740993}`), + SOPSFormatJSON, + ) + require.NoError(t, err) + require.Equal(t, "secret", values["API_TOKEN"]) + require.Equal(t, "9007199254740993", values["COUNT"]) +} + +func TestParseSOPSDocumentJSONRejectsTrailingData(t *testing.T) { + _, err := parseSOPSDocument( + []byte(`{"API_TOKEN":"secret"}{"extra":"value"}`), + SOPSFormatJSON, + ) + require.ErrorContains(t, err, "trailing data") + + _, err = parseSOPSDocument( + []byte(`{"API_TOKEN":"secret"} garbage`), + SOPSFormatJSON, + ) + require.ErrorContains(t, err, "trailing data") +} + +func TestParseSOPSDocumentDotenv(t *testing.T) { + values, err := parseSOPSDocument( + []byte("API_TOKEN=secret\nPORT=5432\n"), + SOPSFormatDotenv, + ) + require.NoError(t, err) + require.Equal(t, "secret", values["API_TOKEN"]) + require.Equal(t, "5432", values["PORT"]) +} + +func TestParseSOPSDocumentRejectsNestedValue(t *testing.T) { + _, err := parseSOPSDocument( + []byte("database:\n password: secret\n"), + SOPSFormatYAML, + ) + require.ErrorContains(t, err, "not a scalar") +} + +func TestNormalizeSOPSFormat(t *testing.T) { + for _, tc := range []struct { + path string + want string + }{ + {"secrets.enc.yaml", SOPSFormatYAML}, + {"secrets.enc.yml", SOPSFormatYAML}, + {"secrets.enc.json", SOPSFormatJSON}, + {"secrets.enc.env", SOPSFormatDotenv}, + } { + got, err := normalizeSOPSFormat("", tc.path) + require.NoError(t, err) + require.Equal(t, tc.want, got) + } +} + +func TestSOPSSource_CanceledFirstCallerDoesNotPoisonLaterCaller(t *testing.T) { + t.Setenv("SOPS_AGE_KEY", testSOPSAgeIdentity) + source := NewSOPSDataSource( + "test-source", + "secrets.enc.yaml", + SOPSFormatYAML, + encryptedSOPSTestFixture(t), + ) + + callerCtx, cancel := context.WithCancel(context.Background()) + // Hook cancels caller context after load execution has started inside once.Do, + // exercising the once.Do completion path rather than the pre-call guard. + source.loadHook = func() { + cancel() + } + + _, err := source.Get(callerCtx, "SOPS_E2E_SECRET") + require.ErrorIs(t, err, context.Canceled) + + resolved, err := source.Get(context.Background(), "SOPS_E2E_SECRET") + require.NoError(t, err) + require.Equal(t, testSOPSPlaintextMarker, resolved.Value) +} diff --git a/pkg/secrets/source.go b/pkg/secrets/source.go new file mode 100644 index 000000000..0ba7f7b0b --- /dev/null +++ b/pkg/secrets/source.go @@ -0,0 +1,16 @@ +package secrets + +import "context" + +// ResolvedSecret is the runtime value returned by a secret source. +type ResolvedSecret struct { + Name string + Value string + Sensitive bool + Source string +} + +// Source resolves externally or locally owned secret values. +type Source interface { + Get(ctx context.Context, name string) (ResolvedSecret, error) +} diff --git a/pkg/secrets/source_config.go b/pkg/secrets/source_config.go new file mode 100644 index 000000000..792ec3c34 --- /dev/null +++ b/pkg/secrets/source_config.go @@ -0,0 +1,213 @@ +package secrets + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/devsy-org/devsy/pkg/config" + "sigs.k8s.io/yaml" +) + +const sourceConfigFileName = "secret-sources.yaml" + +// SourceConfig describes an external secret source. It contains references +// only; secret values and decryption credentials are never persisted here. +type SourceConfig struct { + Name string `json:"name" yaml:"name"` + Type string `json:"type" yaml:"type"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` +} + +type sourceRegistryFile struct { + Contexts map[string][]SourceConfig `json:"contexts,omitempty" yaml:"contexts,omitempty"` +} + +func sourceConfigPath(devsyConfig *config.Config) (string, error) { + if devsyConfig != nil && devsyConfig.Origin != "" { + return filepath.Join(filepath.Dir(devsyConfig.Origin), sourceConfigFileName), nil + } + configPath, err := config.GetConfigPath() + if err != nil { + return "", err + } + return filepath.Join(filepath.Dir(configPath), sourceConfigFileName), nil +} + +// LoadSourceConfigs loads external sources registered for the active context. +func LoadSourceConfigs(devsyConfig *config.Config) ([]SourceConfig, error) { + filePath, err := sourceConfigPath(devsyConfig) + if err != nil { + return nil, err + } + registry, err := loadSourceRegistry(filePath) + if err != nil { + return nil, err + } + if registry.Contexts == nil || devsyConfig == nil { + return nil, nil + } + items := registry.Contexts[devsyConfig.DefaultContext] + return append([]SourceConfig(nil), items...), nil +} + +// ModifySourceConfigs serializes external source modifications across processes +// by holding the secret-sources lock while loading, mutating, and writing configuration. +func ModifySourceConfigs( + devsyConfig *config.Config, + mutate func(sources []SourceConfig) ([]SourceConfig, error), +) error { + if devsyConfig == nil { + return fmt.Errorf("devsy config is nil") + } + filePath, err := sourceConfigPath(devsyConfig) + if err != nil { + return err + } + unlock, err := acquireFlock(filepath.Dir(filePath), sourceConfigFileName+".lock") + if err != nil { + return err + } + defer unlock() + registry, err := loadSourceRegistry(filePath) + if err != nil { + return err + } + var current []SourceConfig + if registry.Contexts != nil { + current = registry.Contexts[devsyConfig.DefaultContext] + } + updated, err := mutate(append([]SourceConfig(nil), current...)) + if err != nil { + return err + } + setContextSources(®istry, devsyConfig.DefaultContext, updated) + return writeSourceRegistry(filePath, registry) +} + +// SaveSourceConfigs writes external source metadata for the active context. +func SaveSourceConfigs(devsyConfig *config.Config, sources []SourceConfig) error { + return ModifySourceConfigs(devsyConfig, func(_ []SourceConfig) ([]SourceConfig, error) { + return sources, nil + }) +} + +func loadSourceRegistry(filePath string) (sourceRegistryFile, error) { + registry := sourceRegistryFile{Contexts: map[string][]SourceConfig{}} + // #nosec G304 -- filePath is generated by sourceConfigPath under Devsy's config directory. + data, err := os.ReadFile(filePath) + if os.IsNotExist(err) { + return registry, nil + } + if err != nil { + return registry, fmt.Errorf("read secret source config: %w", err) + } + if err := yaml.Unmarshal(data, ®istry); err != nil { + return registry, fmt.Errorf("parse secret source config: %w", err) + } + if registry.Contexts == nil { + registry.Contexts = map[string][]SourceConfig{} + } + return registry, nil +} + +func setContextSources(registry *sourceRegistryFile, contextName string, sources []SourceConfig) { + if registry.Contexts == nil { + registry.Contexts = map[string][]SourceConfig{} + } + if len(sources) == 0 { + delete(registry.Contexts, contextName) + return + } + registry.Contexts[contextName] = append([]SourceConfig(nil), sources...) +} + +func writeSourceRegistry(filePath string, registry sourceRegistryFile) error { + data, err := yaml.Marshal(registry) + if err != nil { + return fmt.Errorf("marshal secret source config: %w", err) + } + dir := filepath.Dir(filePath) + if err := os.MkdirAll(dir, 0o700); err != nil { + return err + } + tmpName, err := createTempSourceRegistry(dir, data) + if err != nil { + return err + } + return replaceSourceRegistry(tmpName, filePath, dir) +} + +func createTempSourceRegistry(dir string, data []byte) (string, error) { + tmpFile, err := os.CreateTemp(dir, sourceConfigFileName+".tmp-*") + if err != nil { + return "", fmt.Errorf("create temp secret source config: %w", err) + } + tmpName := tmpFile.Name() + var success bool + defer func() { + _ = tmpFile.Close() + if !success { + _ = os.Remove(tmpName) + } + }() + if err := tmpFile.Chmod(0o600); err != nil { + return "", fmt.Errorf("chmod temp secret source config: %w", err) + } + if _, err := tmpFile.Write(data); err != nil { + return "", fmt.Errorf("write temp secret source config: %w", err) + } + if err := tmpFile.Sync(); err != nil { + return "", fmt.Errorf("sync temp secret source config: %w", err) + } + success = true + return tmpName, nil +} + +func replaceSourceRegistry(tmpName, filePath, dir string) error { + defer func() { _ = os.Remove(tmpName) }() + if err := os.Rename(tmpName, filePath); err != nil { + return fmt.Errorf("replace secret source config: %w", err) + } + // #nosec G304 -- dir is filepath.Dir(filePath) under Devsy's config directory. + if d, err := os.Open(dir); err == nil { + _ = d.Sync() + _ = d.Close() + } + return nil +} + +func FindSourceConfig(sources []SourceConfig, name string) (SourceConfig, bool) { + for _, source := range sources { + if source.Name == name { + return source, true + } + } + return SourceConfig{}, false +} + +func AddSourceConfig(sources []SourceConfig, source SourceConfig) ([]SourceConfig, error) { + if err := ValidateSourceName(source.Name); err != nil { + return nil, err + } + if source.Name == LocalSourceName { + return nil, fmt.Errorf("secret source name %q is reserved", LocalSourceName) + } + if _, exists := FindSourceConfig(sources, source.Name); exists { + return nil, fmt.Errorf("secret source %q already exists", source.Name) + } + return append(append([]SourceConfig(nil), sources...), source), nil +} + +func RemoveSourceConfig(sources []SourceConfig, name string) ([]SourceConfig, bool) { + for i, source := range sources { + if source.Name != name { + continue + } + out := append([]SourceConfig(nil), sources[:i]...) + out = append(out, sources[i+1:]...) + return out, true + } + return append([]SourceConfig(nil), sources...), false +} diff --git a/pkg/secrets/source_config_lock_test.go b/pkg/secrets/source_config_lock_test.go new file mode 100644 index 000000000..ff6cb7fe0 --- /dev/null +++ b/pkg/secrets/source_config_lock_test.go @@ -0,0 +1,84 @@ +package secrets + +import ( + "fmt" + "sync" + "testing" + + "github.com/devsy-org/devsy/pkg/config" + "github.com/stretchr/testify/require" +) + +const testSecretSourcePath = "test-secrets.enc.yaml" + +func TestSaveSourceConfigsSerializesConcurrentWriters(t *testing.T) { + devsyConfig := &config.Config{ + DefaultContext: "default", + Origin: t.TempDir() + "/config.yaml", + } + + const writers = 20 + var wg sync.WaitGroup + errs := make([]error, writers) + wantSets := make([][]SourceConfig, writers) + for i := range writers { + wantSets[i] = []SourceConfig{{ + Name: fmt.Sprintf("source-%d", i), + Type: SOPSFormatter, + Path: testSecretSourcePath, + }} + } + for i := range writers { + wg.Add(1) + go func(i int) { + defer wg.Done() + errs[i] = SaveSourceConfigs(devsyConfig, wantSets[i]) + }(i) + } + wg.Wait() + + for _, err := range errs { + require.NoError(t, err) + } + + final, err := LoadSourceConfigs(devsyConfig) + require.NoError(t, err) + require.Len(t, final, 1) + require.Contains(t, wantSets, final) +} + +func TestModifySourceConfigsSerializesConcurrentMutations(t *testing.T) { + devsyConfig := &config.Config{ + DefaultContext: "default", + Origin: t.TempDir() + "/config.yaml", + } + + const writers = 20 + var wg sync.WaitGroup + errs := make([]error, writers) + for i := range writers { + wg.Add(1) + go func(i int) { + defer wg.Done() + errs[i] = ModifySourceConfigs( + devsyConfig, + func(sources []SourceConfig) ([]SourceConfig, error) { + return AddSourceConfig(sources, SourceConfig{ + Name: fmt.Sprintf("source-%d", i), + Type: SOPSFormatter, + Path: testSecretSourcePath, + }) + }, + ) + }(i) + } + wg.Wait() + + for _, err := range errs { + require.NoError(t, err) + } + + final, err := LoadSourceConfigs(devsyConfig) + require.NoError(t, err) + require.Len(t, final, writers) +} diff --git a/pkg/secrets/source_factory.go b/pkg/secrets/source_factory.go new file mode 100644 index 000000000..66b991783 --- /dev/null +++ b/pkg/secrets/source_factory.go @@ -0,0 +1,57 @@ +package secrets + +import ( + "fmt" + + "github.com/devsy-org/devsy/pkg/config" +) + +// NewResolverForConfig constructs the local Devsy source plus all external +// sources registered in the active local context. +func NewResolverForConfig(devsyConfig *config.Config) (*Resolver, error) { + if devsyConfig == nil { + return nil, fmt.Errorf("devsy config is nil") + } + store, err := NewStoreForConfig(devsyConfig) + if err != nil { + return nil, err + } + resolver := NewResolver() + if err := resolver.Register( + LocalSourceName, + LocalSourceName, + NewLocalSource(store, devsyConfig.DefaultContext), + ); err != nil { + return nil, err + } + configs, err := LoadSourceConfigs(devsyConfig) + if err != nil { + return nil, err + } + for _, sourceConfig := range configs { + if err := RegisterConfiguredSource(resolver, sourceConfig); err != nil { + return nil, err + } + } + return resolver, nil +} + +func RegisterConfiguredSource(resolver *Resolver, sourceConfig SourceConfig) error { + if resolver == nil { + return fmt.Errorf("secret resolver is nil") + } + switch sourceConfig.Type { + case SOPSFormatter: + return resolver.Register( + sourceConfig.Name, + sourceConfig.Type, + NewSOPSSource(sourceConfig.Name, sourceConfig.Path, sourceConfig.Format), + ) + default: + return fmt.Errorf( + "secret source %q has unsupported type %q", + sourceConfig.Name, + sourceConfig.Type, + ) + } +} diff --git a/pkg/secrets/testdata/sops-age.yaml b/pkg/secrets/testdata/sops-age.yaml new file mode 100644 index 000000000..79a5fdfd8 --- /dev/null +++ b/pkg/secrets/testdata/sops-age.yaml @@ -0,0 +1,17 @@ +SOPS_E2E_SECRET: ENC[AES256_GCM,data:7/LkTGbUbM24m2YegXZhCPwFErYDhnRC3jflQA==,iv:jox9Lg827lNean0A3dbFnjbizHq/+e5Oj41ntzFKtNc=,tag:H0XcdWPIYBmte+v0F9j47Q==,type:str] +TLS_KEY: ENC[AES256_GCM,data:7qUD3Paq3RT0XbF/qvSnfw==,iv:ClkYTnncQRPn3JhDZ9iZfuY0lNDYY71ot6HRjF2HCVY=,tag:oGoczTS56kPXL9N4/FY+nA==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwWHBIUkxaRWlsdFRidWFF + OHdHTHZGZUh2UGRzOXhVdFlxRnZIZkY3eTFnCnpoTFNrNVZKcFRtTVhjbnBUK1c1 + SGFnd2R0ZGQvOHFueWpHR0xiOGVvdTAKLS0tIDJTSVFCazNueGJkMlZDUU9vK21j + d0xPTEV4NVZGL1EweHBqM3lTSG9sb3MKJtSnyqRMtWM2WRIEJSHQPkYtBXa+wSpG + PXamDLnUGIrzsjbl6uTZxHytVSwfVERwWzagJTEOajIJpFwHIyGD/A== + -----END AGE ENCRYPTED FILE----- + recipient: age1h84qv0p8kdwfp6vrag4nvmpd6gt3mde9g6dcnj9uryh3qcl50uxq83n932 + lastmodified: "2026-09-03T05:38:40Z" + mac: ENC[AES256_GCM,data:5thaKGP8s7dbN/9VvRn54TB0R6UaRlRvtyoby0yn5gGlCAYZLvBlAXmTY8AQ0ShQII3ictP2KpXVLGd0dp0H0LXGr9PpsdXIVEO5t/WS5YO+ZhdxZ5hgWBzEL1rZ2VO9Bt0DPFmEYNNryqIpLU64lbd+AO5P/6/uaBMZZERYE0Y=,iv:7nUHdzpyZidt2DRQdvSv2W2GCF5JArFzvyX9ppVEP5k=,tag:/KTQAguMfRBpWPDumiTunA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3 diff --git a/sites/docs-devsy-sh/content/docs/developing-in-workspaces/secrets.mdx b/sites/docs-devsy-sh/content/docs/developing-in-workspaces/secrets.mdx index 21727d307..8b6b83e00 100644 --- a/sites/docs-devsy-sh/content/docs/developing-in-workspaces/secrets.mdx +++ b/sites/docs-devsy-sh/content/docs/developing-in-workspaces/secrets.mdx @@ -3,175 +3,323 @@ title: Secrets in a Workspace sidebar_label: Secrets --- -## Storing Secrets +Devsy can inject sensitive values into workspaces without placing plaintext +credentials in `devcontainer.json`, command-line arguments, or ordinary dotenv +files. -Devsy can store named secrets locally and inject them into your workspace's -lifecycle commands as environment variables. This lets you keep credentials out -of your `devcontainer.json`, shell history, and dotenv files. +Secrets can come from two places: -Secret **values** are stored using the selected backend. With the default -`auto` backend, Devsy uses your operating system's keyring when available: +- **Devsy-managed secrets** — values owned by Devsy and stored in the operating + system keyring or Devsy's encrypted local file. +- **External secret sources** — values owned by another system, such as a + SOPS-encrypted file. Devsy resolves these values when they are needed and does + not import them into the local Devsy secret store. + +Both use the same protected workspace delivery paths: lifecycle environment +variables, in-memory files under `/run/secrets`, and supported build-secret +mechanisms. + +## Devsy-Managed Secrets + +With the default `auto` storage backend, Devsy stores sensitive values in your +operating system's keyring when one is available: - **macOS** — Keychain - **Windows** — Credential Manager - **Linux** — Secret Service (libsecret / GNOME Keyring / KWallet) -When no keyring is available (or the `file` backend is selected), values are -kept in an age-encrypted file in the Devsy config directory instead. Either way, -only non-sensitive metadata (the secret's name and timestamps) is written in -plaintext. Secrets are scoped to the active [context](../managing-providers/what-are-providers). +When no keyring is available, or when the `file` backend is selected, Devsy +stores values in an age-encrypted `secrets.enc` file in the Devsy config +directory. Only non-sensitive metadata such as names and timestamps is written +in plaintext. Devsy-managed secrets are scoped to the active +[context](../managing-providers/what-are-providers). ### Creating a Secret -``` +```shell +# Interactive input; the value is not echoed. devsy secret set DB_PASSWORD + +# Standard input is recommended for scripts. +printf '%s' "$MY_VALUE" | devsy secret set DB_PASSWORD --stdin + +# Or read a value from a file. +devsy secret set TLS_KEY --from-file ./tls.key ``` -You will be prompted for the value without echoing it to the terminal. You can -also supply the value non-interactively: +### Listing, Reading, and Deleting +```shell +devsy secret list +devsy secret get DB_PASSWORD +devsy secret delete DB_PASSWORD ``` -# From standard input (recommended for scripts) -printf '%s' "$MY_VALUE" | devsy secret set DB_PASSWORD --stdin -# From a file -devsy secret set TLS_KEY --from-file ./tls.key +`devsy secret list` never prints values. A value removed from the OS keyring +outside Devsy is shown as `orphaned`. + +## External Secret Sources + +An external secret source lets Devsy resolve a value at workspace startup +without making Devsy a second source of truth for that value. + +### SOPS + +Devsy supports [SOPS](https://github.com/getsops/sops)-encrypted YAML, JSON, and +dotenv files. Devsy uses the SOPS Go implementation; installing a +`sops` executable is not required. + +SOPS source documents use flat top-level key/value pairs in the initial +implementation: + +```yaml +DATABASE_PASSWORD: ENC[...] +API_TOKEN: ENC[...] ``` -Secret names may contain only letters, digits, and underscores, since they are -used directly as environment-variable names. +### Registering a Local SOPS Source -### Listing and Reading Secrets +If the encrypted file is already available on the machine running Devsy, add a +named source: +```shell +devsy secret source add sops project ./secrets.enc.yaml ``` -devsy secret list -devsy secret get DB_PASSWORD + +Devsy validates that the file can be decrypted before saving the source. The +configuration stores the source name, type, file path, and (when set via +`--format`) the document format override only. Decrypted values are not +copied into the keyring or Devsy's `secrets.enc` file. + +List or remove locally registered sources with: + +```shell +devsy secret source list +devsy secret source remove project ``` -`list` never prints values. A secret whose value has been removed from the -keyring out-of-band is shown with the `orphaned` status. +A source cannot be removed while context-level secret bindings still reference +it. + +### Repository-Owned SOPS Sources -### Deleting a Secret +A repository can declare SOPS sources in either the root-level `.devcontainer.json` or `.devcontainer/devcontainer.json` layout under `customizations.devsy.secretSources`. +```text +project/ +├── .devcontainer/ +│ └── devcontainer.json +└── secrets.enc.yaml ``` -devsy secret delete DB_PASSWORD + +Example `devcontainer.json`: + +```json +{ + "name": "My Project", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "customizations": { + "devsy": { + "secretSources": [ + { + "name": "project", + "type": "sops", + "path": "./secrets.enc.yaml" + } + ], + "secrets": [ + "sops:project/DATABASE_PASSWORD" + ] + } + } +} ``` +Repository source paths are resolved relative to the repository root. Devsy +rejects paths, including symlink targets, that escape the repository root. -## Using Secrets in a Workspace +Repository-owned sources work with a local checkout: -### Per Workspace +```shell +devsy workspace up . +``` -Reference a stored secret by name when bringing up a workspace with `--secret` -(repeatable). By default the secret is injected as an environment variable into -lifecycle commands: +and with a remote Git source: +```shell +devsy workspace up https://github.com/acme/project ``` -devsy workspace up https://github.com/example/repo --secret DB_PASSWORD + +For a remote source Devsy first acquires repository data to read the requested Git +revision, then reads `customizations.devsy` from the effective Dev Container +configuration and the referenced encrypted SOPS files from that same revision. + +### Bootstrap Authentication for Private Repositories + +Credentials needed to acquire a private repository must be available **before** +Devsy can inspect repository-owned SOPS files. + +For example, this is valid: + +```text +local Devsy secret / Git credential / SSH agent + │ + ▼ + authenticate repository clone + │ + ▼ + repository SOPS runtime secrets ``` -Each `--secret` accepts options as `NAME[,type=env|mount][,target=X]`: +A repository-owned SOPS secret cannot authenticate the clone of the same +repository that contains it. + +`--git-token` may reference a Devsy-managed secret or another external source +that is already locally available before repository acquisition. -- `type=env` (default) sets an environment variable; `target` overrides the - variable name. -- `type=mount` writes the value to a file at `/run/secrets/` on an - in-memory filesystem, matching the Docker/Podman convention. `target` - defaults to `NAME`. On Kubernetes this uses an in-memory `emptyDir`; providers - that cannot mount an in-memory filesystem reject `type=mount` with a clear - error, so use `type=env` there. +## Referencing Secrets +An unqualified name continues to mean a Devsy-managed local secret: + +```text +DB_PASSWORD ``` -# Inject under a different environment-variable name -devsy workspace up ... --secret DB_PASSWORD,target=DATABASE_PASSWORD -# Mount as a file at /run/secrets/tls.key -devsy workspace up ... --secret TLS_KEY,type=mount,target=tls.key +An external source uses a source-qualified reference: + +```text +sops:project/DB_PASSWORD ``` -### For all Workspaces +Devsy does not search other sources if a reference cannot be resolved. Explicit +source selection prevents one source from silently shadowing another. -Bind a secret to the active context so it is injected automatically on every -`up`, without repeating `--secret`: +### Lifecycle Environment Variables +`--secret` is repeatable. By default, Devsy exposes the requested value as an +environment variable to lifecycle commands: + +```shell +devsy workspace up . --secret DB_PASSWORD +devsy workspace up . --secret sops:project/DATABASE_PASSWORD ``` -devsy secret attach DB_PASSWORD -devsy secret detach DB_PASSWORD + +Use `target=` to change the environment-variable name: + +```shell +devsy workspace up . \ + --secret sops:project/DATABASE_PASSWORD,target=DB_PASSWORD ``` -Bound secret names are stored in your context configuration; deleting a context -also deletes its secrets from the keyring. +### Mounted Secret Files -If a requested or bound secret cannot be found when a workspace is created, the -`up` fails rather than silently continuing without it. +Use `type=mount` to write the secret to the existing in-memory secret mount: -## How Secrets Are Protected +```shell +devsy workspace up . \ + --secret sops:project/TLS_KEY,type=mount,target=tls.key +``` -Injected secret **values** are masked (`***`) in lifecycle-hook log output, so a -secret echoed by a `postCreateCommand` will not appear in the workspace logs. -Secret values are delivered to the workspace over Devsy's encrypted agent -tunnel, not embedded in any command line, so they are not exposed in process -listings on the host or inside the container. +The value is available at: -Secrets delivered with `type=mount` are written to an in-memory `tmpfs` at -`/run/secrets`, so they are never persisted to a container image layer or to -disk. +```text +/run/secrets/tls.key +``` -## Choosing a Storage Backend +Providers that cannot offer an in-memory secret mount reject `type=mount` with an error. -Devsy supports two backends for secret values: +### Build Secrets -- `keyring` — the OS keyring (Keychain / Credential Manager / libsecret). -- `file` — an [age](https://age-encryption.org)-encrypted file - (`secrets.enc`) stored alongside the Devsy config. +Source-qualified references can also be used with Devsy's existing build-secret +path: + +```shell +devsy workspace up . --build-secret sops:project/NPM_TOKEN +``` -By default the backend is `auto`: Devsy uses the keyring when one is available -and falls back to the encrypted file otherwise. +The build secret ID is the secret key (`NPM_TOKEN`), not the complete +source-qualified reference. Builds continue to consume it through the existing +BuildKit secret mechanism, for example: + +```dockerfile +RUN --mount=type=secret,id=NPM_TOKEN ... +``` -The `file` backend works with no configuration: Devsy generates and manages an -encryption key for you (stored in the OS keyring when available, otherwise in -`secrets.key` next to `secrets.enc`, mode `0600`). This protects secrets from -accidental disclosure and casual inspection, but a key stored in `secrets.key` -offers no protection against someone who can read your Devsy config directory -(they have both the key and the ciphertext). For stronger at-rest protection — -for example against a stolen backup — set a passphrase (see below), which is -never written to disk. +### Attaching a Secret to a Context -Set a persistent preference per context with the `SECRETS_BACKEND` option: +Attach a locally available secret reference to the active context so it is +injected on every `workspace up`: +```shell +devsy secret attach DB_PASSWORD +devsy secret attach sops:project/API_TOKEN ``` -devsy context set -o SECRETS_BACKEND=file + +Detach it with: + +```shell +devsy secret detach DB_PASSWORD +devsy secret detach sops:project/API_TOKEN ``` -Allowed values are `auto`, `keyring`, and `file`. To override the preference for -a single command, set the `DEVSY_SECRETS_BACKEND` environment variable, which -takes precedence over the context option. +Devsy stores only the reference for an external secret. Repository-owned +`customizations.devsy` can declare project-specific automatic +bindings with its `secrets` list. If any requested or attached value cannot be +resolved, `workspace up` fails rather than silently starting without it. + +## SOPS Credential Discovery -## Headless and CI Environments +Devsy delegates key and KMS credential discovery to SOPS instead of creating a +parallel credential system. -When the `file` backend is used (either by preference or because no OS keyring -is available, for example on headless Linux, containers, or CI), Devsy uses an -auto-generated key by default, so no setup is required. For at-rest protection -against a stolen backup or disk, set a passphrase — the encryption key is then -derived from it and never stored: +For age-encrypted files, normal SOPS mechanisms apply, including +`SOPS_AGE_KEY`, `SOPS_AGE_KEY_FILE`, and the standard SOPS age-key location. +For example: +```shell +export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt" +devsy workspace up . ``` -export DEVSY_SECRETS_PASSPHRASE="…" -devsy secret set DEPLOY_TOKEN --stdin < token.txt + +For AWS KMS, GCP KMS, Azure Key Vault, PGP, and other key services supported by +SOPS, use the same credentials and key configuration you would use with +SOPS itself. + +## How Secrets Are Protected + +Devsy workspaces protect sensitive values by: + +- Decrypting SOPS sources in memory at workspace startup without persisting plaintext to disk +- Injecting secrets directly as environment variables or memory-backed files under `/run/secrets` +- Redacting known secret values from logs and terminal outputs +- Excluding secret values from client-server transport and workspace metadata + +### Choosing a Devsy Storage Backend + +For Devsy-managed values, use the supported backends: + +- `keyring` — the OS keyring (Keychain / Credential Manager / libsecret). +- `file` — an [age](https://age-encryption.org)-encrypted `secrets.enc` stored + alongside the Devsy config. +- `auto` — use the keyring when available and otherwise fall back to `file`. + +Set a persistent preference per context with: + +```shell +devsy context set -o SECRETS_BACKEND=file ``` -The key source (auto-generated vs. passphrase) is recorded with your secrets. If -you later change it — for example by setting a passphrase for secrets that were -stored with an auto-generated key — Devsy reports a clear error rather than -failing to decrypt. Restore the original setting, or re-create the secrets under -the new one. +Or override it for one command with `DEVSY_SECRETS_BACKEND`. +The local `file` backend generates and manages an encryption key automatically. +For stronger at-rest protection, set a passphrase with +`DEVSY_SECRETS_PASSPHRASE`; the derived encryption key is then not stored on +disk. ## Managed Environment Variables -For non-sensitive configuration you can store managed environment variables with -`devsy env`. They share the same per-context store as secrets, but their values -are kept in plaintext in the Devsy config directory (no keyring, no encryption) -and are freely readable: +For non-sensitive configuration, `devsy env` stores managed environment +variables in plaintext Devsy configuration: -``` +```shell devsy env set LOG_LEVEL=debug devsy env set REGION --value us-east-1 devsy env list @@ -179,13 +327,12 @@ devsy env get LOG_LEVEL devsy env delete LOG_LEVEL ``` -Inject them into a workspace with `--env` (repeatable), optionally remapping the -variable name with `NAME=TARGET`: +Inject them with: -``` +```shell devsy workspace up ... --env LOG_LEVEL --env REGION=AWS_REGION ``` -Use `devsy secret` (not `devsy env`) for anything sensitive: secrets are stored -in the OS keyring or an encrypted file, masked in logs, and delivered over the -agent tunnel. +Use `devsy secret` or an external secret source for sensitive values. `--env` +is deliberately restricted to non-sensitive Devsy-managed values because that +path is not the protected secret-delivery channel.