diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1966964..13013e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: go-version: 1.22.4 - name: Import GPG Key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v6.1.0 + uses: crazy-max/ghaction-import-gpg@v6.2.0 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 39c50f2..28189fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: - run: go mod download - run: go build -v . - name: Run linters - uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 + uses: golangci/golangci-lint-action@2e788936b09dd82dc280e845628a40d2ba6b204c # v6.3.1 with: version: latest @@ -38,6 +38,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: "1.9.*" + terraform_wrapper: false - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: go-version-file: "go.mod" @@ -70,7 +74,7 @@ jobs: with: go-version-file: "go.mod" cache: true - - uses: hashicorp/setup-terraform@651471c36a6092792c552e8b1bef71e592b462d8 # v3.1.1 + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 with: terraform_version: ${{ matrix.terraform }} terraform_wrapper: false diff --git a/.golangci.yml b/.golangci.yml index 223cf95..29ddfe8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,19 +9,18 @@ linters: enable: - durationcheck - errcheck - - exportloopref - forcetypeassert - godot - gofmt - gosimple + - govet - ineffassign - makezero - misspell - nilerr - predeclared - staticcheck - - tenv - unconvert - unparam - unused - - vet \ No newline at end of file + - usetesting diff --git a/GNUmakefile b/GNUmakefile index 800fcb9..0fc2494 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -41,15 +41,20 @@ test-registry-container: .registry-cache # Pulls images referenced in integration tests and pushes them to the local cache. .PHONY: test-images-push -test-images-push: .registry-cache/docker/registry/v2/repositories/test-ubuntu +test-images-push: .registry-cache/docker/registry/v2/repositories/test-ubuntu .registry-cache/docker/registry/v2/repositories/envbuilder .PHONY: test-images-pull test-images-pull: docker pull ubuntu:latest docker tag ubuntu:latest localhost:5000/test-ubuntu:latest + docker pull ghcr.io/coder/envbuilder-preview:latest + docker tag ghcr.io/coder/envbuilder-preview:latest localhost:5000/envbuilder:latest .registry-cache: mkdir -p .registry-cache && chmod -R ag+w .registry-cache .registry-cache/docker/registry/v2/repositories/test-ubuntu: docker push localhost:5000/test-ubuntu:latest + +.registry-cache/docker/registry/v2/repositories/envbuilder: + docker push localhost:5000/envbuilder:latest \ No newline at end of file diff --git a/README.md b/README.md index 62589ff..92434e5 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,17 @@ The `terraform-provider-envbuilder` is a Terraform provider that acts as a helpe It is used to determine if a pre-built image of a dev container built from a given Git repository is present in a given Docker registry. If it is found that building a particular dev container would produce the same image that is already present in the remote registry, then that image can be used to start the container instead, skipping over the build phase. +> **Note:** currently, this provider can only be run on Linux platforms. We are [investigating support](https://github.com/coder/terraform-provider-envbuilder/issues/26) for other platforms. + ## Usage Take a look at the [`envbuilder_cached_image_resource.tf`](./examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf) example for a detailed usage example. For use with [Coder](https://github.com/coder/coder), see the [Dev Containers documentation](https://coder.com/docs/templates/dev-containers) and check out the example templates: -- [Docker](https://github.com/coder/coder/tree/main/examples/templates/devcontainer-docker) -- [Kubernetes](https://github.com/coder/coder/tree/main/examples/templates/devcontainer-kuberntes) -- [AWS VM](https://github.com/coder/coder/tree/main/examples/templates/devcontainer-aws-vm) -- [GCP VM](https://github.com/coder/coder/tree/main/examples/templates/devcontainer-gcp-vm) +- [Docker](https://github.com/coder/coder/tree/main/examples/templates/docker-devcontainer) +- [Kubernetes](https://github.com/coder/coder/tree/main/examples/templates/kubernetes-devcontainer) +- [AWS VM](https://github.com/coder/coder/tree/main/examples/templates/aws-devcontainer) +- [GCP VM](https://github.com/coder/coder/tree/main/examples/templates/gcp-devcontainer) ## Requirements @@ -30,10 +32,6 @@ For use with [Coder](https://github.com/coder/coder), see the [Dev Containers do go install ``` -## Using the provider - -Fill this in for each provider - ## Developing the Provider If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (see [Requirements](#requirements) above). diff --git a/docs/index.md b/docs/index.md index 8148773..8bedaaa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,12 +3,14 @@ page_title: "envbuilder Provider" subcategory: "" description: |- - + The Envbuilder provider can be used to check for the presence of a container image previously built by Envbuilder https://github.com/coder/envbuilder. + This allows re-using a previously built image pushed to a container registry without having to rebuild it. --- # envbuilder Provider - +The Envbuilder provider can be used to check for the presence of a container image previously built by [Envbuilder](https://github.com/coder/envbuilder). +This allows re-using a previously built image pushed to a container registry without having to rebuild it. ## Example Usage diff --git a/docs/resources/cached_image.md b/docs/resources/cached_image.md index 8498720..842d806 100644 --- a/docs/resources/cached_image.md +++ b/docs/resources/cached_image.md @@ -25,6 +25,7 @@ The cached image resource can be used to retrieve a cached image produced by env - `base_image_cache_dir` (String) (Envbuilder option) The path to a directory where the base image can be found. This should be a read-only directory solely mounted for the purpose of caching the base image. - `build_context_path` (String) (Envbuilder option) Can be specified when a DockerfilePath is specified outside the base WorkspaceFolder. This path MUST be relative to the WorkspaceFolder path into which the repo is cloned. +- `build_secrets` (Map of String) The secrets to use for the build. This is a map of key-value pairs. - `cache_ttl_days` (Number) (Envbuilder option) The number of days to use cached layers before expiring them. Defaults to 7 days. - `devcontainer_dir` (String) (Envbuilder option) The path to the folder containing the devcontainer.json file that will be used to build the workspace and can either be an absolute path or a path relative to the workspace folder. If not provided, defaults to `.devcontainer`. - `devcontainer_json_path` (String) (Envbuilder option) The path to a devcontainer.json file that is either an absolute path or a path relative to DevcontainerDir. This can be used in cases where one wants to substitute an edited devcontainer.json file for the one that exists in the repo. @@ -37,6 +38,7 @@ The cached image resource can be used to retrieve a cached image produced by env - `git_clone_single_branch` (Boolean) (Envbuilder option) Clone only a single branch of the Git repository. - `git_http_proxy_url` (String) (Envbuilder option) The URL for the HTTP proxy. This is optional. - `git_password` (String, Sensitive) (Envbuilder option) The password to use for Git authentication. This is optional. +- `git_ssh_private_key_base64` (String, Sensitive) (Envbuilder option) Base64 encoded SSH private key to be used for Git authentication. - `git_ssh_private_key_path` (String) (Envbuilder option) Path to an SSH private key to be used for Git authentication. - `git_username` (String) (Envbuilder option) The username to use for Git authentication. This is optional. - `ignore_paths` (List of String) (Envbuilder option) The comma separated list of paths to ignore when building the workspace. @@ -48,7 +50,8 @@ The cached image resource can be used to retrieve a cached image produced by env ### Read-Only -- `env` (List of String, Sensitive) Computed envbuilder configuration to be set for the container. May contain secrets. +- `env` (List of String, Sensitive) Computed envbuilder configuration to be set for the container in the form of a list of strings of `key=value`. May contain secrets. +- `env_map` (Map of String, Sensitive) Computed envbuilder configuration to be set for the container in the form of a key-value map. May contain secrets. - `exists` (Boolean) Whether the cached image was exists or not for the given config. - `id` (String) Cached image identifier. This will generally be the image's SHA256 digest. - `image` (String) Outputs the cached image repo@digest if it exists, and builder image otherwise. diff --git a/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf b/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf index 304aea4..af90219 100644 --- a/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf +++ b/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf @@ -66,10 +66,11 @@ resource "envbuilder_cached_image" "example" { builder_image = var.builder_image git_url = var.repo_url cache_repo = local.cache_repo + insecure = true extra_env = { "ENVBUILDER_VERBOSE" : "true" "ENVBUILDER_INSECURE" : "true" # due to local registry - "ENVBUILDER_INIT_SCRIPT" : "sleep infinity" + "ENVBUILDER_INIT_SCRIPT" : "#!/usr/bin/env bash\necho Hello && sleep infinity" "ENVBUILDER_PUSH_IMAGE" : "true" } depends_on = [docker_container.registry] @@ -77,8 +78,8 @@ resource "envbuilder_cached_image" "example" { // Run the cached image. Depending on the contents of // the cache repo, this will either be var.builder_image -// or a previously built image pusehd to var.cache_repo. -// Running `terraform apply` once (assuming empty cache) +// or a previously built image pushed to var.cache_repo. +// Running `terraform apply` once (assuming empty cache) // will result in the builder image running, and the built // image being pushed to the cache repo. // Running `terraform apply` again will result in the @@ -105,4 +106,3 @@ output "id" { output "image" { value = envbuilder_cached_image.example.image } - diff --git a/go.mod b/go.mod index 1f68bb3..fdd5454 100644 --- a/go.mod +++ b/go.mod @@ -1,28 +1,32 @@ module github.com/coder/terraform-provider-envbuilder -go 1.22.4 +go 1.23.0 + +toolchain go1.23.7 // We use our own Kaniko fork. -replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20240807142221-ffc5e60fca41 +replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20241120132148-131d6094d781 // Required to import codersdk due to gvisor dependency. replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240702054557-aa558fbe5374 require ( github.com/GoogleContainerTools/kaniko v1.9.2 - github.com/coder/envbuilder v1.0.0-rc.0.0.20240807151028-6e5bfa5faa29 - github.com/docker/docker v26.1.4+incompatible - github.com/gliderlabs/ssh v0.3.7 - github.com/go-git/go-billy/v5 v5.5.0 - github.com/go-git/go-git/v5 v5.12.0 + github.com/coder/envbuilder v1.1.0 + github.com/coder/serpent v0.8.0 + github.com/docker/docker v27.3.1+incompatible + github.com/gliderlabs/ssh v0.3.8 + github.com/go-git/go-billy/v5 v5.6.1 + github.com/go-git/go-git/v5 v5.13.1 github.com/google/go-containerregistry v0.20.2 github.com/google/uuid v1.6.0 - github.com/hashicorp/terraform-plugin-docs v0.19.4 + github.com/hashicorp/terraform-plugin-docs v0.21.0 github.com/hashicorp/terraform-plugin-framework v1.11.0 github.com/hashicorp/terraform-plugin-go v0.23.0 github.com/hashicorp/terraform-plugin-log v0.9.0 - github.com/hashicorp/terraform-plugin-testing v1.9.0 - github.com/stretchr/testify v1.9.0 + github.com/hashicorp/terraform-plugin-testing v1.10.0 + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.10.0 ) require ( @@ -55,9 +59,9 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/Microsoft/hcsshim v0.11.4 // indirect - github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.11.7 // indirect + github.com/ProtonMail/go-crypto v1.1.3 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect @@ -84,41 +88,40 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect - github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chainguard-dev/git-urls v1.0.2 // indirect github.com/charmbracelet/lipgloss v0.8.0 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect - github.com/cilium/ebpf v0.12.3 // indirect - github.com/cloudflare/circl v1.3.7 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352 // indirect github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 // indirect github.com/coder/quartz v0.1.0 // indirect github.com/coder/retry v1.5.1 // indirect - github.com/coder/serpent v0.7.0 // indirect github.com/coder/terraform-provider-coder v0.23.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect - github.com/containerd/cgroups/v3 v3.0.2 // indirect - github.com/containerd/containerd v1.7.15 // indirect + github.com/containerd/containerd v1.7.21 // indirect + github.com/containerd/containerd/api v1.7.19 // indirect github.com/containerd/continuity v0.4.3 // indirect + github.com/containerd/errdefs v0.1.0 // indirect github.com/containerd/fifo v1.1.0 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect - github.com/containerd/ttrpc v1.2.3 // indirect - github.com/containerd/typeurl/v2 v2.1.1 // indirect + github.com/containerd/ttrpc v1.2.5 // indirect + github.com/containerd/typeurl/v2 v2.2.0 // indirect github.com/coreos/go-iptables v0.6.0 // indirect github.com/coreos/go-oidc/v3 v3.10.0 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/distribution/distribution/v3 v3.0.0-alpha.1 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v27.1.1+incompatible // indirect + github.com/docker/cli v27.2.1+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.1 // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-metrics v0.0.1 // indirect @@ -132,23 +135,23 @@ require ( github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-chi/chi/v5 v5.0.10 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/nftables v0.2.0 // indirect - github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect + github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect - github.com/hashicorp/cli v1.1.6 // indirect + github.com/hashicorp/cli v1.1.7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -158,16 +161,17 @@ require ( github.com/hashicorp/go-memdb v1.3.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect - github.com/hashicorp/hc-install v0.7.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hc-install v0.9.1 // indirect github.com/hashicorp/hcl/v2 v2.21.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-exec v0.21.0 // indirect - github.com/hashicorp/terraform-json v0.22.1 // indirect + github.com/hashicorp/terraform-exec v0.22.0 // indirect + github.com/hashicorp/terraform-json v0.24.0 // indirect github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect @@ -181,13 +185,12 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect github.com/jsimonetti/rtnetlink v1.3.5 // indirect - github.com/karrick/godirwalk v1.16.1 // 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.17.9 // indirect github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect @@ -203,17 +206,18 @@ require ( github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/buildkit v0.13.1 // indirect + github.com/moby/buildkit v0.16.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/swarmkit/v2 v2.0.0-20230315203717-e28e8ba9bc83 // indirect github.com/moby/sys/mount v0.3.3 // indirect - github.com/moby/sys/mountinfo v0.7.1 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/sequential v0.5.0 // indirect - github.com/moby/sys/signal v0.7.0 // indirect + github.com/moby/sys/signal v0.7.1 // indirect github.com/moby/sys/symlink v0.2.0 // indirect - github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/muesli/reflow v0.3.0 // indirect @@ -221,7 +225,7 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runtime-spec v1.1.0 // indirect + github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/opencontainers/selinux v1.11.0 // indirect github.com/otiai10/copy v1.14.0 // indirect github.com/outcaste-io/ristretto v0.2.3 // indirect @@ -236,7 +240,7 @@ require ( github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.0 // indirect github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.15.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect github.com/redis/go-redis/v9 v9.1.0 // indirect @@ -247,11 +251,10 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.2.2 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d // indirect github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect @@ -260,6 +263,8 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect + github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect + github.com/twpayne/go-vfs/v5 v5.0.4 // indirect github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a // indirect github.com/valyala/fasthttp v1.55.0 // indirect github.com/vbatts/tar-split v0.11.5 // indirect @@ -270,9 +275,9 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - github.com/yuin/goldmark v1.7.1 // indirect + github.com/yuin/goldmark v1.7.7 // indirect github.com/yuin/goldmark-meta v1.1.0 // indirect - github.com/zclconf/go-cty v1.14.4 // indirect + github.com/zclconf/go-cty v1.16.2 // indirect github.com/zeebo/errs v1.3.0 // indirect go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect go.etcd.io/etcd/raft/v3 v3.5.6 // indirect @@ -289,17 +294,17 @@ require ( go.uber.org/atomic v1.11.0 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 // indirect - golang.org/x/crypto v0.25.0 // indirect - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/crypto v0.35.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.36.0 // indirect golang.org/x/oauth2 v0.20.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/tools v0.23.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect diff --git a/go.sum b/go.sum index 995dc1e..fe3c583 100644 --- a/go.sum +++ b/go.sum @@ -77,12 +77,12 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= -github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= -github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= -github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= +github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= +github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= +github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= @@ -144,8 +144,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= -github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= +github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= @@ -178,59 +178,61 @@ github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb2 github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352 h1:L/EjCuZxs5tOcqqCaASj/nu65TRYEFcTt8qRQfHZXX0= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352/go.mod h1:P1KoQSgnKEAG6Mnd3YlGzAophty+yKA9VV48LpfNRvo= -github.com/coder/envbuilder v1.0.0-rc.0.0.20240807151028-6e5bfa5faa29 h1:PhJBofIrh6NGuTQ93nW7/KWcYX6Ju0PGoE/BbNvf87o= -github.com/coder/envbuilder v1.0.0-rc.0.0.20240807151028-6e5bfa5faa29/go.mod h1:lm33s3+chqnl7lB4avNFfDH5gXDYSunYD7/y4bJ/LMA= -github.com/coder/kaniko v0.0.0-20240807142221-ffc5e60fca41 h1:1Ye7AcLnuT5IDv6il7Fxo+aqpzlWfedkpraCCwx8Lyo= -github.com/coder/kaniko v0.0.0-20240807142221-ffc5e60fca41/go.mod h1:YMK7BlxerzLlMwihGxNWUaFoN9LXCij4P+w/8/fNlcM= +github.com/coder/envbuilder v1.1.0 h1:OcICg3FzwFHzBDw+60tW7dgCxfkTxt/C6faz/cAfwDE= +github.com/coder/envbuilder v1.1.0/go.mod h1:WgqCgSz6XzXSoTGMMIuBf+0D38iofTqAwQCIlEmohX0= +github.com/coder/kaniko v0.0.0-20241120132148-131d6094d781 h1:/4SMdrjLQL1BseLSnMd9nYQSI+E63CXcyFGC7ZHHj8I= +github.com/coder/kaniko v0.0.0-20241120132148-131d6094d781/go.mod h1:3rM/KOQ4LgF8mE+O1P6pLDa/E57mzxIxNdUOMKi1qpg= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/quartz v0.1.0 h1:cLL+0g5l7xTf6ordRnUMMiZtRE8Sq5LxpghS63vEXrQ= github.com/coder/quartz v0.1.0/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA= github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc= github.com/coder/retry v1.5.1/go.mod h1:blHMk9vs6LkoRT9ZHyuZo360cufXEhrxqvEzeMtRGoY= -github.com/coder/serpent v0.7.0 h1:zGpD2GlF3lKIVkMjNGKbkip88qzd5r/TRcc30X/SrT0= -github.com/coder/serpent v0.7.0/go.mod h1:REkJ5ZFHQUWFTPLExhXYZ1CaHFjxvGNRlLXLdsI08YA= +github.com/coder/serpent v0.8.0 h1:6OR+k6fekhSeEDmwwzBgnSjaa7FfGGrMlc3GoAEH9dg= +github.com/coder/serpent v0.8.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q= github.com/coder/tailscale v1.1.1-0.20240702054557-aa558fbe5374 h1:a5Eg7D5e2oAc0tN56ee4yxtiTo76ztpRlk6geljaZp8= github.com/coder/tailscale v1.1.1-0.20240702054557-aa558fbe5374/go.mod h1:rp6BIJxCp127/hvvDWNkHC9MxAlKvQfoOtBr8s5sCqo= github.com/coder/terraform-provider-coder v0.23.0 h1:DuNLWxhnGlXyG0g+OCAZRI6xd8+bJjIEnE4F3hYgA4E= github.com/coder/terraform-provider-coder v0.23.0/go.mod h1:wMun9UZ9HT2CzF6qPPBup1odzBpVUc0/xSFoXgdI3tk= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0= -github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= -github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes= -github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY= +github.com/containerd/containerd v1.7.21 h1:USGXRK1eOC/SX0L195YgxTHb0a00anxajOzgfN0qrCA= +github.com/containerd/containerd v1.7.21/go.mod h1:e3Jz1rYRUZ2Lt51YrH9Rz0zPyJBOlSvB3ghr2jbVD8g= +github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA= +github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= +github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= -github.com/containerd/ttrpc v1.2.3 h1:4jlhbXIGvijRtNC8F/5CpuJZ7yKOBFGFOOXg1bkISz0= -github.com/containerd/ttrpc v1.2.3/go.mod h1:ieWsXucbb8Mj9PH0rXCw1i8IunRbbAiDkpXkbfflWBM= -github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= -github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= +github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU= +github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= +github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= +github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -246,14 +248,14 @@ github.com/distribution/distribution/v3 v3.0.0-alpha.1 h1:jn7I1gvjOvmLztH1+1cLiU github.com/distribution/distribution/v3 v3.0.0-alpha.1/go.mod h1:LCp4JZp1ZalYg0W/TN05jarCQu+h4w7xc7ZfQF4Y/cY= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= -github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v27.2.1+incompatible h1:U5BPtiD0viUzjGAjV1p0MGB8eVA3L3cbIrnyWmSJI70= +github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU= -github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo= -github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= @@ -269,8 +271,8 @@ github.com/ePirat/docker-credential-gitlabci v1.0.0 h1:YRkUSvkON6rT88vtscClAmPEY github.com/ePirat/docker-credential-gitlabci v1.0.0/go.mod h1:Ptmh+D0lzBQtgb6+QHjXl9HqOn3T1P8fKUHldiSQQGA= github.com/ebitengine/purego v0.6.0-alpha.5 h1:EYID3JOAdmQ4SNZYJHu9V6IqOeRQDBYxqKAg9PyoHFY= github.com/ebitengine/purego v0.6.0-alpha.5/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= +github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= 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= @@ -297,20 +299,20 @@ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= -github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= -github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= +github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= -github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= -github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= +github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -354,8 +356,9 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= @@ -402,8 +405,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/nftables v0.2.0 h1:PbJwaBmbVLzpeldoeUKGkE2RjstrjPKMl6oLrfEJ6/8= github.com/google/nftables v0.2.0/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= -github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b h1:h9U78+dx9a4BKdQkBBos92HalKpaGKHrp+3Uo6yTodo= -github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -417,8 +420,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= -github.com/hashicorp/cli v1.1.6 h1:CMOV+/LJfL1tXCOKrgAX0uRKnzjj/mpmqNXloRSy2K8= -github.com/hashicorp/cli v1.1.6/go.mod h1:MPon5QYlgjjo0BSoAiN0ESeT5fRzDjVRp+uioJ0piz4= +github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU= +github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -441,6 +444,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -452,20 +457,20 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= -github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= -github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC1659aBk= -github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hc-install v0.9.1 h1:gkqTfE3vVbafGQo6VZXcy2v5yoz2bE0+nhZXruCuODQ= +github.com/hashicorp/hc-install v0.9.1/go.mod h1:pWWvN/IrfeBK4XPeXXYkL6EjMufHkCK5DvwxeLKuBf0= github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= -github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= -github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= -github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= -github.com/hashicorp/terraform-plugin-docs v0.19.4 h1:G3Bgo7J22OMtegIgn8Cd/CaSeyEljqjH3G39w28JK4c= -github.com/hashicorp/terraform-plugin-docs v0.19.4/go.mod h1:4pLASsatTmRynVzsjEhbXZ6s7xBlUw/2Kt0zfrq8HxA= +github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8l+uRmZHj64= +github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ= +github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= +github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= +github.com/hashicorp/terraform-plugin-docs v0.21.0 h1:yoyA/Y719z9WdFJAhpUkI1jRbKP/nteVNBaI3hW7iQ8= +github.com/hashicorp/terraform-plugin-docs v0.21.0/go.mod h1:J4Wott1J2XBKZPp/NkQv7LMShJYOcrqhQ2myXBcu64s= github.com/hashicorp/terraform-plugin-framework v1.11.0 h1:M7+9zBArexHFXDx/pKTxjE6n/2UCXY6b8FIq9ZYhwfE= github.com/hashicorp/terraform-plugin-framework v1.11.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= @@ -474,8 +479,8 @@ github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9T github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= -github.com/hashicorp/terraform-plugin-testing v1.9.0 h1:xOsQRqqlHKXpFq6etTxih3ubdK3HVDtfE1IY7Rpd37o= -github.com/hashicorp/terraform-plugin-testing v1.9.0/go.mod h1:fhhVx/8+XNJZTD5o3b4stfZ6+q7z9+lIWigIYdT6/44= +github.com/hashicorp/terraform-plugin-testing v1.10.0 h1:2+tmRNhvnfE4Bs8rB6v58S/VpqzGC6RCh9Y8ujdn+aw= +github.com/hashicorp/terraform-plugin-testing v1.10.0/go.mod h1:iWRW3+loP33WMch2P/TEyCxxct/ZEcCGMquSLSCVsrc= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -516,8 +521,6 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u 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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= -github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -548,11 +551,10 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -587,8 +589,8 @@ github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1 github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/buildkit v0.13.1 h1:L8afOFhPq2RPJJSr/VyzbufwID7jquZVB7oFHbPRcPE= -github.com/moby/buildkit v0.13.1/go.mod h1:aNmNQKLBFYAOFuzQjR3VA27/FijlvtBD1pjNwTSN37k= +github.com/moby/buildkit v0.16.0 h1:wOVBj1o5YNVad/txPQNXUXdelm7Hs/i0PUFjzbK0VKE= +github.com/moby/buildkit v0.16.0/go.mod h1:Xqx/5GlrqE1yIRORk0NSCVDFpQAU1WjlT6KHYZdisIQ= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= @@ -600,16 +602,18 @@ github.com/moby/swarmkit/v2 v2.0.0-20230315203717-e28e8ba9bc83/go.mod h1:GvjR7mC github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= github.com/moby/sys/mount v0.3.3/go.mod h1:PBaEorSNTLG5t/+4EgukEQVlAvVEc6ZjTySwKdqp5K0= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= -github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= -github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= -github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= -github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= +github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc= github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= -github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= -github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -626,16 +630,18 @@ github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKt github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= -github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= @@ -686,8 +692,8 @@ github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5E github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek= -github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= @@ -703,8 +709,8 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg= github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= @@ -716,8 +722,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -744,8 +750,8 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d h1:K3j02b5j2Iw1xoggN9B2DIEkhWGheqFOeDkdJdBrJI8= @@ -764,8 +770,12 @@ github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ0 github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/twpayne/go-vfs/v5 v5.0.4 h1:/ne3h+rW7f5YOyOFguz+3ztfUwzOLR0Vts3y0mMAitg= +github.com/twpayne/go-vfs/v5 v5.0.4/go.mod h1:zTPFJUbgsEMFNSWnWQlLq9wh4AN83edZzx3VXbxrS1w= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnGJiUF0TrBpNs+iyYocu6h0og= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= @@ -803,12 +813,12 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= -github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.7 h1:5m9rrB1sW3JUMToKFQfb+FGt1U7r57IHu5GrYrG2nqU= +github.com/yuin/goldmark v1.7.7/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= -github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= -github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70= +github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -874,11 +884,11 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/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= @@ -887,8 +897,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -908,8 +918,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -922,8 +932,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -955,23 +965,22 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 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= @@ -981,8 +990,8 @@ 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.5.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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= @@ -998,8 +1007,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/imgutil/imgutil.go b/internal/imgutil/imgutil.go new file mode 100644 index 0000000..5c04441 --- /dev/null +++ b/internal/imgutil/imgutil.go @@ -0,0 +1,106 @@ +package imgutil + +import ( + "archive/tar" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + eboptions "github.com/coder/envbuilder/options" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// GetRemoteImage fetches the image manifest of the image. +func GetRemoteImage(imgRef string) (v1.Image, error) { + ref, err := name.ParseReference(imgRef) + if err != nil { + return nil, fmt.Errorf("parse reference: %w", err) + } + + img, err := remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return nil, fmt.Errorf("check remote image: %w", err) + } + + return img, nil +} + +// ExtractEnvbuilderFromImage reads the image located at imgRef and extracts +// MagicBinaryLocation to destPath. +func ExtractEnvbuilderFromImage(ctx context.Context, imgRef, destPath string) error { + var o eboptions.Options + o.SetDefaults() + needle := strings.TrimPrefix(o.BinaryPath, "/") + img, err := GetRemoteImage(imgRef) + if err != nil { + return fmt.Errorf("check remote image: %w", err) + } + + layers, err := img.Layers() + if err != nil { + return fmt.Errorf("get image layers: %w", err) + } + + // Check the layers in reverse order. The last layers are more likely to + // include the binary. + for i := len(layers) - 1; i >= 0; i-- { + ul, err := layers[i].Uncompressed() + if err != nil { + return fmt.Errorf("get uncompressed layer: %w", err) + } + + tr := tar.NewReader(ul) + for { + th, err := tr.Next() + if err == io.EOF { + break + } + + if err != nil { + return fmt.Errorf("read tar header: %w", err) + } + + name := filepath.Clean(th.Name) + if th.Typeflag != tar.TypeReg { + tflog.Debug(ctx, "skip non-regular file", map[string]any{"name": name, "layer_idx": i + 1}) + continue + } + + if name != needle { + tflog.Debug(ctx, "skip file", map[string]any{"name": name, "layer_idx": i + 1}) + continue + } + + tflog.Debug(ctx, "found file", map[string]any{"name": name, "layer_idx": i + 1}) + if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil { + return fmt.Errorf("create parent directories: %w", err) + } + destF, err := os.Create(destPath) + if err != nil { + return fmt.Errorf("create dest file for writing: %w", err) + } + defer destF.Close() + _, err = io.Copy(destF, tr) + if err != nil { + return fmt.Errorf("copy dest file from image: %w", err) + } + if err := destF.Close(); err != nil { + return fmt.Errorf("close dest file: %w", err) + } + + if err := os.Chmod(destPath, 0o755); err != nil { + return fmt.Errorf("chmod file: %w", err) + } + return nil + } + } + + return fmt.Errorf("extract envbuilder binary from image %q: %w", imgRef, os.ErrNotExist) +} diff --git a/internal/provider/cached_image_resource.go b/internal/provider/cached_image_resource.go index 4cbdfce..986d628 100644 --- a/internal/provider/cached_image_resource.go +++ b/internal/provider/cached_image_resource.go @@ -1,10 +1,8 @@ package provider import ( - "archive/tar" "context" "fmt" - "io" "net/http" "os" "path/filepath" @@ -12,16 +10,14 @@ import ( kconfig "github.com/GoogleContainerTools/kaniko/pkg/config" "github.com/coder/envbuilder" - "github.com/coder/envbuilder/constants" - eblog "github.com/coder/envbuilder/log" eboptions "github.com/coder/envbuilder/options" + "github.com/coder/terraform-provider-envbuilder/internal/imgutil" + "github.com/coder/terraform-provider-envbuilder/internal/tfutil" "github.com/go-git/go-billy/v5/osfs" - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -30,6 +26,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" ) @@ -52,30 +49,33 @@ type CachedImageResourceModel struct { CacheRepo types.String `tfsdk:"cache_repo"` GitURL types.String `tfsdk:"git_url"` // Optional "inputs". - BaseImageCacheDir types.String `tfsdk:"base_image_cache_dir"` - BuildContextPath types.String `tfsdk:"build_context_path"` - CacheTTLDays types.Int64 `tfsdk:"cache_ttl_days"` - DevcontainerDir types.String `tfsdk:"devcontainer_dir"` - DevcontainerJSONPath types.String `tfsdk:"devcontainer_json_path"` - DockerfilePath types.String `tfsdk:"dockerfile_path"` - DockerConfigBase64 types.String `tfsdk:"docker_config_base64"` - ExitOnBuildFailure types.Bool `tfsdk:"exit_on_build_failure"` - ExtraEnv types.Map `tfsdk:"extra_env"` - FallbackImage types.String `tfsdk:"fallback_image"` - GitCloneDepth types.Int64 `tfsdk:"git_clone_depth"` - GitCloneSingleBranch types.Bool `tfsdk:"git_clone_single_branch"` - GitHTTPProxyURL types.String `tfsdk:"git_http_proxy_url"` - GitPassword types.String `tfsdk:"git_password"` - GitSSHPrivateKeyPath types.String `tfsdk:"git_ssh_private_key_path"` - GitUsername types.String `tfsdk:"git_username"` - IgnorePaths types.List `tfsdk:"ignore_paths"` - Insecure types.Bool `tfsdk:"insecure"` - RemoteRepoBuildMode types.Bool `tfsdk:"remote_repo_build_mode"` - SSLCertBase64 types.String `tfsdk:"ssl_cert_base64"` - Verbose types.Bool `tfsdk:"verbose"` - WorkspaceFolder types.String `tfsdk:"workspace_folder"` + BaseImageCacheDir types.String `tfsdk:"base_image_cache_dir"` + BuildContextPath types.String `tfsdk:"build_context_path"` + BuildSecrets types.Map `tfsdk:"build_secrets"` + CacheTTLDays types.Int64 `tfsdk:"cache_ttl_days"` + DevcontainerDir types.String `tfsdk:"devcontainer_dir"` + DevcontainerJSONPath types.String `tfsdk:"devcontainer_json_path"` + DockerfilePath types.String `tfsdk:"dockerfile_path"` + DockerConfigBase64 types.String `tfsdk:"docker_config_base64"` + ExitOnBuildFailure types.Bool `tfsdk:"exit_on_build_failure"` + ExtraEnv types.Map `tfsdk:"extra_env"` + FallbackImage types.String `tfsdk:"fallback_image"` + GitCloneDepth types.Int64 `tfsdk:"git_clone_depth"` + GitCloneSingleBranch types.Bool `tfsdk:"git_clone_single_branch"` + GitHTTPProxyURL types.String `tfsdk:"git_http_proxy_url"` + GitPassword types.String `tfsdk:"git_password"` + GitSSHPrivateKeyPath types.String `tfsdk:"git_ssh_private_key_path"` + GitSSHPrivateKeyBase64 types.String `tfsdk:"git_ssh_private_key_base64"` + GitUsername types.String `tfsdk:"git_username"` + IgnorePaths types.List `tfsdk:"ignore_paths"` + Insecure types.Bool `tfsdk:"insecure"` + RemoteRepoBuildMode types.Bool `tfsdk:"remote_repo_build_mode"` + SSLCertBase64 types.String `tfsdk:"ssl_cert_base64"` + Verbose types.Bool `tfsdk:"verbose"` + WorkspaceFolder types.String `tfsdk:"workspace_folder"` // Computed "outputs". Env types.List `tfsdk:"env"` + EnvMap types.Map `tfsdk:"env_map"` Exists types.Bool `tfsdk:"exists"` ID types.String `tfsdk:"id"` Image types.String `tfsdk:"image"` @@ -122,6 +122,11 @@ func (r *CachedImageResource) Schema(ctx context.Context, req resource.SchemaReq MarkdownDescription: "(Envbuilder option) Can be specified when a DockerfilePath is specified outside the base WorkspaceFolder. This path MUST be relative to the WorkspaceFolder path into which the repo is cloned.", Optional: true, }, + "build_secrets": schema.MapAttribute{ + MarkdownDescription: "The secrets to use for the build. This is a map of key-value pairs.", + ElementType: types.StringType, + Optional: true, + }, "cache_ttl_days": schema.Int64Attribute{ MarkdownDescription: "(Envbuilder option) The number of days to use cached layers before expiring them. Defaults to 7 days.", Optional: true, @@ -155,7 +160,6 @@ func (r *CachedImageResource) Schema(ctx context.Context, req resource.SchemaReq MarkdownDescription: "(Envbuilder option) Terminates upon a build failure. This is handy when preferring the FALLBACK_IMAGE in cases where no devcontainer.json or image is provided. However, it ensures that the container stops if the build process encounters an error.", Optional: true, }, - // TODO(mafredri): Map vs List? Support both? "extra_env": schema.MapAttribute{ MarkdownDescription: "Extra environment variables to set for the container. This may include envbuilder options.", ElementType: types.StringType, @@ -189,6 +193,11 @@ func (r *CachedImageResource) Schema(ctx context.Context, req resource.SchemaReq MarkdownDescription: "(Envbuilder option) Path to an SSH private key to be used for Git authentication.", Optional: true, }, + "git_ssh_private_key_base64": schema.StringAttribute{ + MarkdownDescription: "(Envbuilder option) Base64 encoded SSH private key to be used for Git authentication.", + Optional: true, + Sensitive: true, + }, "git_username": schema.StringAttribute{ MarkdownDescription: "(Envbuilder option) The username to use for Git authentication. This is optional.", Optional: true, @@ -225,9 +234,8 @@ func (r *CachedImageResource) Schema(ctx context.Context, req resource.SchemaReq }, // Computed "outputs". - // TODO(mafredri): Map vs List? Support both? "env": schema.ListAttribute{ - MarkdownDescription: "Computed envbuilder configuration to be set for the container. May contain secrets.", + MarkdownDescription: "Computed envbuilder configuration to be set for the container in the form of a list of strings of `key=value`. May contain secrets.", ElementType: types.StringType, Computed: true, Sensitive: true, @@ -235,6 +243,15 @@ func (r *CachedImageResource) Schema(ctx context.Context, req resource.SchemaReq listplanmodifier.RequiresReplace(), }, }, + "env_map": schema.MapAttribute{ + MarkdownDescription: "Computed envbuilder configuration to be set for the container in the form of a key-value map. May contain secrets.", + ElementType: types.StringType, + Computed: true, + Sensitive: true, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.RequiresReplace(), + }, + }, "exists": schema.BoolAttribute{ MarkdownDescription: "Whether the cached image was exists or not for the given config.", Computed: true, @@ -280,6 +297,17 @@ func (r *CachedImageResource) Configure(ctx context.Context, req resource.Config r.client = client } +// setComputedEnv sets data.Env and data.EnvMap based on the values of the +// other fields in the model. +func (data *CachedImageResourceModel) setComputedEnv(ctx context.Context, env map[string]string) diag.Diagnostics { + var diag, ds diag.Diagnostics + data.EnvMap, ds = basetypes.NewMapValueFrom(ctx, types.StringType, env) + diag = append(diag, ds...) + data.Env, ds = basetypes.NewListValueFrom(ctx, types.StringType, tfutil.DockerEnv(env)) + diag = append(diag, ds...) + return diag +} + func (r *CachedImageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data CachedImageResourceModel @@ -289,24 +317,48 @@ func (r *CachedImageResource) Read(ctx context.Context, req resource.ReadRequest return } + // Get the options from the data model. + opts, diags := optionsFromDataModel(data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Set the expected environment variables. + computedEnv := computeEnvFromOptions(opts, tfutil.TFMapToStringMap(data.ExtraEnv)) + resp.Diagnostics.Append(data.setComputedEnv(ctx, computedEnv)...) + // If the previous state is that Image == BuilderImage, then we previously did // not find the image. We will need to run another cache probe. if data.Image.Equal(data.BuilderImage) { - tflog.Debug(ctx, "Image previously not found. Recreating.", map[string]any{"ref": data.Image.ValueString()}) + resp.Diagnostics.AddWarning( + "Re-running cache probe due to previous miss.", + fmt.Sprintf(`The previous state specifies image == builder_image %q, which indicates a previous cache miss.`, + data.Image.ValueString(), + )) resp.State.RemoveResource(ctx) return } // Check the remote registry for the image we previously found. - img, err := getRemoteImage(data.Image.ValueString()) + img, err := imgutil.GetRemoteImage(data.Image.ValueString()) if err != nil { if !strings.Contains(err.Error(), "MANIFEST_UNKNOWN") { - resp.Diagnostics.AddError("Error checking remote image", err.Error()) + // Explicitly not making this an error diag. + resp.Diagnostics.AddWarning("Unable to check remote image.", + fmt.Sprintf("The repository %q returned the following error while checking for a cached image %q: %q", + data.CacheRepo.ValueString(), + data.Image.ValueString(), + err.Error(), + )) return } // Image does not exist any longer! Remove the resource so we can re-create // it next time. - tflog.Debug(ctx, "Remote image does not exist any longer. Recreating.", map[string]any{"ref": data.Image.ValueString()}) + resp.Diagnostics.AddWarning("Previously built image not found, recreating.", + fmt.Sprintf("The repository %q does not contain the cached image %q. It will be rebuilt in the next apply.", + data.CacheRepo.ValueString(), + data.Image.ValueString(), + )) resp.State.RemoveResource(ctx) return } @@ -322,29 +374,6 @@ func (r *CachedImageResource) Read(ctx context.Context, req resource.ReadRequest data.Image = types.StringValue(fmt.Sprintf("%s@%s", data.CacheRepo.ValueString(), digest)) data.Exists = types.BoolValue(true) - // Set the expected environment variables. - for key, elem := range data.ExtraEnv.Elements() { - data.Env = appendKnownEnvToList(data.Env, key, elem) - } - - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_CACHE_REPO", data.CacheRepo) - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_URL", data.GitURL) - if !data.CacheTTLDays.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_CACHE_TTL_DAYS", data.CacheTTLDays) - } - if !data.GitUsername.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_USERNAME", data.GitUsername) - } - if !data.GitPassword.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_PASSWORD", data.GitPassword) - } - // Default to remote build mode. - if data.RemoteRepoBuildMode.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_REMOTE_REPO_BUILD_MODE", types.BoolValue(true)) - } else { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_REMOTE_REPO_BUILD_MODE", data.RemoteRepoBuildMode) - } - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -358,14 +387,29 @@ func (r *CachedImageResource) Create(ctx context.Context, req resource.CreateReq return } - cachedImg, err := r.runCacheProbe(ctx, data) + // Get the options from the data model. + opts, diags := optionsFromDataModel(data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Set the expected environment variables. + computedEnv := computeEnvFromOptions(opts, tfutil.TFMapToStringMap(data.ExtraEnv)) + resp.Diagnostics.Append(data.setComputedEnv(ctx, computedEnv)...) + + cachedImg, err := runCacheProbe(ctx, data.BuilderImage.ValueString(), opts) data.ID = types.StringValue(uuid.Nil.String()) data.Exists = types.BoolValue(err == nil) if err != nil { // FIXME: there are legit errors that can crop up here. // We should add a sentinel error in Kaniko for uncached layers, and check // it here. - tflog.Info(ctx, "cached image not found", map[string]any{"err": err.Error()}) + resp.Diagnostics.AddWarning("Cached image not found.", fmt.Sprintf( + "Failed to find cached image in repository %q. It will be rebuilt in the next apply. Error: %s", + data.CacheRepo.ValueString(), + err.Error(), + )) data.Image = data.BuilderImage } else if digest, err := cachedImg.Digest(); err != nil { // There's something seriously up with this image! @@ -376,29 +420,6 @@ func (r *CachedImageResource) Create(ctx context.Context, req resource.CreateReq data.Image = types.StringValue(fmt.Sprintf("%s@%s", data.CacheRepo.ValueString(), digest)) data.ID = types.StringValue(digest.String()) } - // Compute the env attribute from the config map. - // TODO(mafredri): Convert any other relevant attributes given via schema. - for key, elem := range data.ExtraEnv.Elements() { - data.Env = appendKnownEnvToList(data.Env, key, elem) - } - - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_CACHE_REPO", data.CacheRepo) - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_URL", data.GitURL) - if !data.CacheTTLDays.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_CACHE_TTL_DAYS", data.CacheTTLDays) - } - if !data.GitUsername.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_USERNAME", data.GitUsername) - } - if !data.GitPassword.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_PASSWORD", data.GitPassword) - } - // Default to remote build mode. - if data.RemoteRepoBuildMode.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_REMOTE_REPO_BUILD_MODE", types.BoolValue(true)) - } else { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_REMOTE_REPO_BUILD_MODE", data.RemoteRepoBuildMode) - } // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -433,7 +454,7 @@ func (r *CachedImageResource) Delete(ctx context.Context, req resource.DeleteReq // runCacheProbe performs a 'fake build' of the requested image and ensures that // all of the resulting layers of the image are present in the configured cache // repo. Otherwise, returns an error. -func (r *CachedImageResource) runCacheProbe(ctx context.Context, data CachedImageResourceModel) (v1.Image, error) { +func runCacheProbe(ctx context.Context, builderImage string, opts eboptions.Options) (v1.Image, error) { tmpDir, err := os.MkdirTemp(os.TempDir(), "envbuilder-provider-cached-image-data-source") if err != nil { return nil, fmt.Errorf("unable to create temp directory: %s", err.Error()) @@ -445,7 +466,7 @@ func (r *CachedImageResource) runCacheProbe(ctx context.Context, data CachedImag }() oldKanikoDir := kconfig.KanikoDir - tmpKanikoDir := filepath.Join(tmpDir, constants.MagicDir) + tmpKanikoDir := filepath.Join(tmpDir, ".envbuilder") // Normally you would set the KANIKO_DIR environment variable, but we are importing kaniko directly. kconfig.KanikoDir = tmpKanikoDir tflog.Info(ctx, "set kaniko dir to "+tmpKanikoDir) @@ -457,210 +478,52 @@ func (r *CachedImageResource) runCacheProbe(ctx context.Context, data CachedImag if err := os.MkdirAll(tmpKanikoDir, 0o755); err != nil { return nil, fmt.Errorf("failed to create kaniko dir: %w", err) } + // Use the temporary directory as our 'magic dir'. + opts.WorkingDirBase = tmpKanikoDir // In order to correctly reproduce the final layer of the cached image, we // need the envbuilder binary used to originally build the image! envbuilderPath := filepath.Join(tmpDir, "envbuilder") - if err := extractEnvbuilderFromImage(ctx, data.BuilderImage.ValueString(), envbuilderPath); err != nil { + if err := imgutil.ExtractEnvbuilderFromImage(ctx, builderImage, envbuilderPath); err != nil { tflog.Error(ctx, "failed to fetch envbuilder binary from builder image", map[string]any{"err": err}) return nil, fmt.Errorf("failed to fetch the envbuilder binary from the builder image: %s", err.Error()) } - - workspaceFolder := data.WorkspaceFolder.ValueString() - if workspaceFolder == "" { - workspaceFolder = filepath.Join(tmpDir, "workspace") - tflog.Debug(ctx, "workspace_folder not specified, using temp dir", map[string]any{"workspace_folder": workspaceFolder}) - } - - opts := eboptions.Options{ - // These options are always required - CacheRepo: data.CacheRepo.ValueString(), - Filesystem: osfs.New("/"), - ForceSafe: false, // This should never be set to true, as this may be running outside of a container! - GetCachedImage: true, // always! - Logger: tfLogFunc(ctx), - Verbose: data.Verbose.ValueBool(), - WorkspaceFolder: workspaceFolder, - - // Options related to compiling the devcontainer - BuildContextPath: data.BuildContextPath.ValueString(), - DevcontainerDir: data.DevcontainerDir.ValueString(), - DevcontainerJSONPath: data.DevcontainerJSONPath.ValueString(), - DockerfilePath: data.DockerfilePath.ValueString(), - DockerConfigBase64: data.DockerConfigBase64.ValueString(), - FallbackImage: data.FallbackImage.ValueString(), - - // These options are required for cloning the Git repo - CacheTTLDays: data.CacheTTLDays.ValueInt64(), - GitURL: data.GitURL.ValueString(), - GitCloneDepth: data.GitCloneDepth.ValueInt64(), - GitCloneSingleBranch: data.GitCloneSingleBranch.ValueBool(), - GitUsername: data.GitUsername.ValueString(), - GitPassword: data.GitPassword.ValueString(), - GitSSHPrivateKeyPath: data.GitSSHPrivateKeyPath.ValueString(), - GitHTTPProxyURL: data.GitHTTPProxyURL.ValueString(), - RemoteRepoBuildMode: data.RemoteRepoBuildMode.ValueBool(), - RemoteRepoDir: filepath.Join(tmpDir, "repo"), - SSLCertBase64: data.SSLCertBase64.ValueString(), - - // Other options - BaseImageCacheDir: data.BaseImageCacheDir.ValueString(), - BinaryPath: envbuilderPath, // needed to reproduce the final layer. - ExitOnBuildFailure: data.ExitOnBuildFailure.ValueBool(), // may wish to do this instead of fallback image? - Insecure: data.Insecure.ValueBool(), // might have internal CAs? - IgnorePaths: tfListToStringSlice(data.IgnorePaths), // may need to be specified? - // The below options are not relevant and are set to their zero value explicitly. - CoderAgentSubsystem: nil, - CoderAgentToken: "", - CoderAgentURL: "", - ExportEnvFile: "", - InitArgs: "", - InitCommand: "", - InitScript: "", - LayerCacheDir: "", - PostStartScriptPath: "", - PushImage: false, // This is only relevant when building. - SetupScript: "", - SkipRebuild: false, - } - - return envbuilder.RunCacheProbe(ctx, opts) -} - -// getRemoteImage fetches the image manifest of the image. -func getRemoteImage(imgRef string) (v1.Image, error) { - ref, err := name.ParseReference(imgRef) - if err != nil { - return nil, fmt.Errorf("parse reference: %w", err) - } - - img, err := remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) - if err != nil { - return nil, fmt.Errorf("check remote image: %w", err) - } - - return img, nil -} - -// extractEnvbuilderFromImage reads the image located at imgRef and extracts -// MagicBinaryLocation to destPath. -func extractEnvbuilderFromImage(ctx context.Context, imgRef, destPath string) error { - needle := filepath.Clean(constants.MagicBinaryLocation)[1:] // skip leading '/' - img, err := getRemoteImage(imgRef) - if err != nil { - return fmt.Errorf("check remote image: %w", err) - } - - layers, err := img.Layers() - if err != nil { - return fmt.Errorf("get image layers: %w", err) - } - - // Check the layers in reverse order. The last layers are more likely to - // include the binary. - for i := len(layers) - 1; i >= 0; i-- { - ul, err := layers[i].Uncompressed() - if err != nil { - return fmt.Errorf("get uncompressed layer: %w", err) - } - - tr := tar.NewReader(ul) - for { - th, err := tr.Next() - if err == io.EOF { - break - } - - if err != nil { - return fmt.Errorf("read tar header: %w", err) - } - - name := filepath.Clean(th.Name) - if th.Typeflag != tar.TypeReg { - tflog.Debug(ctx, "skip non-regular file", map[string]any{"name": name, "layer_idx": i + 1}) - continue - } - - if name != needle { - tflog.Debug(ctx, "skip file", map[string]any{"name": name, "layer_idx": i + 1}) - continue - } - - tflog.Debug(ctx, "found file", map[string]any{"name": name, "layer_idx": i + 1}) - if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil { - return fmt.Errorf("create parent directories: %w", err) - } - destF, err := os.Create(destPath) - if err != nil { - return fmt.Errorf("create dest file for writing: %w", err) - } - defer destF.Close() - _, err = io.Copy(destF, tr) - if err != nil { - return fmt.Errorf("copy dest file from image: %w", err) - } - if err := destF.Close(); err != nil { - return fmt.Errorf("close dest file: %w", err) - } - - if err := os.Chmod(destPath, 0o755); err != nil { - return fmt.Errorf("chmod file: %w", err) - } - return nil - } - } - - return fmt.Errorf("extract envbuilder binary from image %q: %w", imgRef, os.ErrNotExist) -} - -// NOTE: the String() method of Terraform values will evalue to `` if unknown. -// Check IsUnknown() first before calling String(). -type stringable interface { - IsUnknown() bool - IsNull() bool - String() string -} - -func appendKnownEnvToList(list types.List, key string, value stringable) types.List { - if value.IsUnknown() || value.IsNull() { - return list - } - val := strings.Trim(value.String(), `"`) - elem := types.StringValue(fmt.Sprintf("%s=%s", key, val)) - list, _ = types.ListValue(types.StringType, append(list.Elements(), elem)) - return list -} - -func tfListToStringSlice(l types.List) []string { - var ss []string - for _, el := range l.Elements() { - if sv, ok := el.(stringable); !ok { - panic(fmt.Sprintf("developer error: element %+v must be stringable", el)) - } else if sv.IsUnknown() { - ss = append(ss, "") - } else { - ss = append(ss, sv.String()) + opts.BinaryPath = envbuilderPath + + // We need a filesystem to work with. + opts.Filesystem = osfs.New("/") + // This should never be set to true, as this may be running outside of a container! + opts.ForceSafe = false + // We always want to get the cached image. + opts.GetCachedImage = true + // Log to the Terraform logger. + opts.Logger = tfutil.TFLogFunc(ctx) + + // We don't require users to set a workspace folder, but maybe there's a + // reason someone may need to. + if opts.WorkspaceFolder == "" { + opts.WorkspaceFolder = filepath.Join(tmpDir, "workspace") + if err := os.MkdirAll(opts.WorkspaceFolder, 0o755); err != nil { + return nil, fmt.Errorf("failed to create workspace folder: %w", err) } - } - return ss -} + tflog.Debug(ctx, "workspace_folder not specified, using temp dir", map[string]any{"workspace_folder": opts.WorkspaceFolder}) + } + + // The below options are not relevant and are set to their zero value + // explicitly. + // They must be set by extra_env to be used in the final builder image. + opts.CoderAgentSubsystem = nil + opts.CoderAgentToken = "" + opts.CoderAgentURL = "" + opts.ExportEnvFile = "" + opts.InitArgs = "" + opts.InitCommand = "" + opts.InitScript = "" + opts.LayerCacheDir = "" + opts.PostStartScriptPath = "" + opts.PushImage = false + opts.SetupScript = "" + opts.SkipRebuild = false -// tfLogFunc is an adapter to envbuilder/log.Func. -func tfLogFunc(ctx context.Context) eblog.Func { - return func(level eblog.Level, format string, args ...any) { - var logFn func(context.Context, string, ...map[string]interface{}) - switch level { - case eblog.LevelTrace: - logFn = tflog.Trace - case eblog.LevelDebug: - logFn = tflog.Debug - case eblog.LevelWarn: - logFn = tflog.Warn - case eblog.LevelError: - logFn = tflog.Error - default: - logFn = tflog.Info - } - logFn(ctx, fmt.Sprintf(format, args...)) - } + return envbuilder.RunCacheProbe(ctx, opts) } diff --git a/internal/provider/cached_image_resource_test.go b/internal/provider/cached_image_resource_test.go index e57ce8a..86dd581 100644 --- a/internal/provider/cached_image_resource_test.go +++ b/internal/provider/cached_image_resource_test.go @@ -10,105 +10,315 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) -func TestAccCachedImageDataSource(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) +// testEnvValue is a multi-line environment variable value that we use in +// tests to ensure that we can handle multi-line values correctly. +var testEnvValue = `bar +baz` + +func TestAccCachedImageResource(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - files := map[string]string{ - ".devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`, - ".devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest - RUN date > /date.txt`, - } - deps := setup(ctx, t, files) - deps.ExtraEnv["FOO"] = "bar" - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - Steps: []resource.TestStep{ - // Initial state: cache has not been seeded. - { - Config: deps.Config(t), - PlanOnly: true, - ExpectNonEmptyPlan: true, + for _, tc := range []struct { + name string + files map[string]string + extraEnv map[string]string + assertEnv func(t *testing.T, deps testDependencies) resource.TestCheckFunc + }{ + { + // This test case is the simplest possible case: a devcontainer.json. + // However, it also makes sure we are able to generate a Dockerfile + // from the devcontainer.json. + name: "devcontainer only", + files: map[string]string{ + ".devcontainer/devcontainer.json": `{"image": "localhost:5000/test-ubuntu:latest"}`, }, - // Should detect that no cached image is present and plan to create the resource. - { - Config: deps.Config(t), - Check: resource.ComposeAggregateTestCheckFunc( - // Computed values MUST be present. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "id", uuid.Nil.String()), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "false"), - resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "env.0"), - // Cached image should be set to the builder image. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), - // Inputs should still be present. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar"), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), - // Should be empty - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), - ), - ExpectNonEmptyPlan: true, // TODO: check the plan. + extraEnv: map[string]string{ + "CODER_AGENT_TOKEN": "some-token", + "CODER_AGENT_URL": "https://coder.example.com", + "ENVBUILDER_GIT_URL": "https://not.the.real.git/url", + "ENVBUILDER_CACHE_REPO": "not-the-real-cache-repo", + "FOO": testEnvValue, }, - // Re-running plan should have the same effect. - { - Config: deps.Config(t), - Check: resource.ComposeAggregateTestCheckFunc( - // Computed values MUST be present. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "id", uuid.Nil.String()), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "false"), - resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "env.0"), - // Cached image should be set to the builder image. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), - // Inputs should still be present. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar"), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), - // Should be empty - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), - ), - ExpectNonEmptyPlan: true, // TODO: check the plan. + assertEnv: func(t *testing.T, deps testDependencies) resource.TestCheckFunc { + return resource.ComposeAggregateTestCheckFunc( + assertEnv(t, + "CODER_AGENT_TOKEN", "some-token", + "CODER_AGENT_URL", "https://coder.example.com", + "ENVBUILDER_CACHE_REPO", deps.CacheRepo, + "ENVBUILDER_DOCKER_CONFIG_BASE64", deps.DockerConfigBase64, + "ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key, + "ENVBUILDER_GIT_URL", deps.Repo.URL, + "ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true", + "ENVBUILDER_VERBOSE", "true", + "FOO", "bar\nbaz", + ), + ) }, - // Now, seed the cache and re-run. We should now successfully create the cached image resource. - { - PreConfig: func() { - seedCache(ctx, t, deps) - }, - Config: deps.Config(t), - Check: resource.ComposeAggregateTestCheckFunc( - // Inputs should still be present. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar"), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), - // Should be empty - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), - // Computed - resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "id", quotedPrefix("sha256:")), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "true"), - resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "image"), - resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "image", quotedPrefix(deps.CacheRepo)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.0", "FOO=bar"), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.1", fmt.Sprintf("ENVBUILDER_CACHE_REPO=%s", deps.CacheRepo)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.2", fmt.Sprintf("ENVBUILDER_GIT_URL=%s", deps.Repo.URL)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.3", "ENVBUILDER_REMOTE_REPO_BUILD_MODE=true"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "env.4"), - ), + }, + { + // This test case includes a Dockerfile in addition to the devcontainer.json. + // The Dockerfile writes the current date to a file. This is currently not checked but + // illustrates that a RUN instruction is cached. + name: "devcontainer and Dockerfile", + files: map[string]string{ + ".devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`, + ".devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest +RUN date > /date.txt`, + }, + extraEnv: map[string]string{ + "CODER_AGENT_TOKEN": "some-token", + "CODER_AGENT_URL": "https://coder.example.com", + "FOO": testEnvValue, + "ENVBUILDER_GIT_URL": "https://not.the.real.git/url", + "ENVBUILDER_CACHE_REPO": "not-the-real-cache-repo", + }, + assertEnv: func(t *testing.T, deps testDependencies) resource.TestCheckFunc { + return resource.ComposeAggregateTestCheckFunc( + assertEnv(t, + "CODER_AGENT_TOKEN", "some-token", + "CODER_AGENT_URL", "https://coder.example.com", + "ENVBUILDER_CACHE_REPO", deps.CacheRepo, + "ENVBUILDER_DOCKER_CONFIG_BASE64", deps.DockerConfigBase64, + "ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key, + "ENVBUILDER_GIT_URL", deps.Repo.URL, + "ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true", + "ENVBUILDER_VERBOSE", "true", + "FOO", "bar\nbaz", + ), + ) + }, + }, + { + // This test case ensures that overriding the devcontainer directory works. + name: "different_dir", + files: map[string]string{ + "path/to/.devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`, + "path/to/.devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest + RUN date > /date.txt`, + }, + extraEnv: map[string]string{ + "CODER_AGENT_TOKEN": "some-token", + "CODER_AGENT_URL": "https://coder.example.com", + "FOO": testEnvValue, + "ENVBUILDER_GIT_URL": "https://not.the.real.git/url", + "ENVBUILDER_CACHE_REPO": "not-the-real-cache-repo", + "ENVBUILDER_DEVCONTAINER_DIR": "path/to/.devcontainer", + "ENVBUILDER_DEVCONTAINER_JSON_PATH": "path/to/.devcontainer/devcontainer.json", + "ENVBUILDER_DOCKERFILE_PATH": "path/to/.devcontainer/Dockerfile", + }, + assertEnv: func(t *testing.T, deps testDependencies) resource.TestCheckFunc { + return resource.ComposeAggregateTestCheckFunc( + assertEnv(t, + "CODER_AGENT_TOKEN", "some-token", + "CODER_AGENT_URL", "https://coder.example.com", + "ENVBUILDER_CACHE_REPO", deps.CacheRepo, + "ENVBUILDER_DEVCONTAINER_DIR", "path/to/.devcontainer", + "ENVBUILDER_DEVCONTAINER_JSON_PATH", "path/to/.devcontainer/devcontainer.json", + "ENVBUILDER_DOCKERFILE_PATH", "path/to/.devcontainer/Dockerfile", + "ENVBUILDER_DOCKER_CONFIG_BASE64", deps.DockerConfigBase64, + "ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key, + "ENVBUILDER_GIT_URL", deps.Repo.URL, + "ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true", + "ENVBUILDER_VERBOSE", "true", + "FOO", "bar\nbaz", + ), + ) + }, + }, + { + // This tests that a multi-stage build works correctly. + name: "multistage_run_copy", + files: map[string]string{ + "Dockerfile": ` + FROM localhost:5000/test-ubuntu:latest AS a + RUN date > /date.txt + FROM localhost:5000/test-ubuntu:latest + COPY --from=a /date.txt /date.txt`, + }, + extraEnv: map[string]string{ + "CODER_AGENT_TOKEN": "some-token", + "CODER_AGENT_URL": "https://coder.example.com", + "FOO": testEnvValue, + "ENVBUILDER_GIT_URL": "https://not.the.real.git/url", + "ENVBUILDER_CACHE_REPO": "not-the-real-cache-repo", + "ENVBUILDER_DOCKERFILE_PATH": "Dockerfile", + }, + assertEnv: func(t *testing.T, deps testDependencies) resource.TestCheckFunc { + return resource.ComposeAggregateTestCheckFunc( + assertEnv(t, + "CODER_AGENT_TOKEN", "some-token", + "CODER_AGENT_URL", "https://coder.example.com", + "ENVBUILDER_CACHE_REPO", deps.CacheRepo, + "ENVBUILDER_DOCKERFILE_PATH", "Dockerfile", + "ENVBUILDER_DOCKER_CONFIG_BASE64", deps.DockerConfigBase64, + "ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key, + "ENVBUILDER_GIT_URL", deps.Repo.URL, + "ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true", + "ENVBUILDER_VERBOSE", "true", + "FOO", "bar\nbaz", + ), + ) }, - // Should produce an empty plan after apply - { - Config: deps.Config(t), - PlanOnly: true, + }, + { + // This tests correct handling of the difference in permissions between + // the provider and the image when running a COPY instruction. + // Added to verify fix for coder/terraform-provider-envbuilder#43 + name: "copy_perms", + files: map[string]string{ + "Dockerfile": ` + FROM localhost:5000/test-ubuntu:latest AS a + COPY date.txt /date.txt + FROM localhost:5000/test-ubuntu:latest + COPY --from=a /date.txt /date.txt`, + "date.txt": fmt.Sprintf("%d", time.Now().Unix()), + }, + extraEnv: map[string]string{ + "CODER_AGENT_TOKEN": "some-token", + "CODER_AGENT_URL": "https://coder.example.com", + "FOO": testEnvValue, + "ENVBUILDER_GIT_URL": "https://not.the.real.git/url", + "ENVBUILDER_CACHE_REPO": "not-the-real-cache-repo", + "ENVBUILDER_DOCKERFILE_PATH": "Dockerfile", }, - // Ensure idempotence in this state! - { - Config: deps.Config(t), - PlanOnly: true, + assertEnv: func(t *testing.T, deps testDependencies) resource.TestCheckFunc { + return resource.ComposeAggregateTestCheckFunc( + assertEnv(t, + "CODER_AGENT_TOKEN", "some-token", + "CODER_AGENT_URL", "https://coder.example.com", + "ENVBUILDER_CACHE_REPO", deps.CacheRepo, + "ENVBUILDER_DOCKERFILE_PATH", "Dockerfile", + "ENVBUILDER_DOCKER_CONFIG_BASE64", deps.DockerConfigBase64, + "ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key, + "ENVBUILDER_GIT_URL", deps.Repo.URL, + "ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true", + "ENVBUILDER_VERBOSE", "true", + "FOO", "bar\nbaz", + ), + ) }, }, - }) + } { + t.Run(tc.name, func(t *testing.T) { + //nolint: paralleltest + deps := setup(ctx, t, tc.extraEnv, tc.files) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // 1) Initial state: cache has not been seeded. + { + Config: deps.Config(t), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + // 2) Should detect that no cached image is present and plan to create the resource. + { + Config: deps.Config(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Computed values MUST be present. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "id", uuid.Nil.String()), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "false"), + // Cached image should be set to the builder image. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), + // Inputs should still be present. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), + // Should be empty + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), + // Environment variables + tc.assertEnv(t, deps), + ), + ExpectNonEmptyPlan: true, // TODO: check the plan. + }, + // 3) Re-running plan should have the same effect. + { + Config: deps.Config(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Computed values MUST be present. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "id", uuid.Nil.String()), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "false"), + // Cached image should be set to the builder image. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), + // Inputs should still be present. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), + // Should be empty + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), + // Environment variables + tc.assertEnv(t, deps), + ), + ExpectNonEmptyPlan: true, // TODO: check the plan. + }, + // 4) Now, seed the cache and re-run. We should now successfully create the cached image resource. + { + PreConfig: func() { + seedCache(ctx, t, deps) + }, + Config: deps.Config(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Inputs should still be present. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), + // Should be empty + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), + // Computed + resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "id", quotedPrefix("sha256:")), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "true"), + resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "image"), + resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "image", quotedPrefix(deps.CacheRepo)), + // Environment variables + tc.assertEnv(t, deps), + ), + }, + // 5) Should produce an empty plan after apply + { + Config: deps.Config(t), + PlanOnly: true, + }, + // 6) Ensure idempotence in this state! + { + Config: deps.Config(t), + PlanOnly: true, + }, + }, + }) + }) + } +} + +// assertEnv is a test helper that checks the environment variables, in order, +// on both the env and env_map attributes of the cached image resource. +func assertEnv(t *testing.T, kvs ...string) resource.TestCheckFunc { + t.Helper() + if len(kvs)%2 != 0 { + t.Fatalf("assertEnv: expected an even number of key-value pairs, got %d", len(kvs)) + } + + funcs := make([]resource.TestCheckFunc, 0) + for i := 0; i < len(kvs); i += 2 { + resKey := fmt.Sprintf("env.%d", len(funcs)) + resVal := fmt.Sprintf("%s=%s", kvs[i], kvs[i+1]) + fn := resource.TestCheckResourceAttr("envbuilder_cached_image.test", resKey, resVal) + funcs = append(funcs, fn) + } + + lastKey := fmt.Sprintf("env.%d", len(funcs)) + lastFn := resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", lastKey) + funcs = append(funcs, lastFn) + + for i := 0; i < len(kvs); i += 2 { + resKey := fmt.Sprintf("env_map.%s", kvs[i]) + fn := resource.TestCheckResourceAttr("envbuilder_cached_image.test", resKey, kvs[i+1]) + funcs = append(funcs, fn) + } + + return resource.ComposeAggregateTestCheckFunc(funcs...) } diff --git a/internal/provider/git_test.go b/internal/provider/git_test.go index a512a85..86131fe 100644 --- a/internal/provider/git_test.go +++ b/internal/provider/git_test.go @@ -135,17 +135,19 @@ func startSSHServer(ctx context.Context, t testing.TB) string { _, _ = io.Copy(in, s) _ = in.Close() }() + outCopyDone := make(chan struct{}) go func() { _, _ = io.Copy(s, out) _ = out.Close() - _ = s.CloseWrite() + close(outCopyDone) }() err = cmd.Wait() if err != nil { t.Logf("command failed: %s", err) } + <-outCopyDone - t.Logf("session ended: %s", s.RawCommand()) + t.Logf("session ended (cmd=%q, code=%d)", s.RawCommand(), cmd.ProcessState.ExitCode()) err = s.Exit(cmd.ProcessState.ExitCode()) if err != nil { diff --git a/internal/provider/helpers.go b/internal/provider/helpers.go new file mode 100644 index 0000000..acc69f2 --- /dev/null +++ b/internal/provider/helpers.go @@ -0,0 +1,282 @@ +package provider + +import ( + "fmt" + "slices" + "strings" + + eboptions "github.com/coder/envbuilder/options" + "github.com/coder/serpent" + "github.com/coder/terraform-provider-envbuilder/internal/tfutil" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/spf13/pflag" +) + +const ( + envbuilderOptionPrefix = "ENVBUILDER_" +) + +// nonOverrideOptions are options that cannot be overridden by extra_env. +var nonOverrideOptions = map[string]bool{ + "ENVBUILDER_CACHE_REPO": true, + "ENVBUILDER_GIT_URL": true, +} + +// optionsFromDataModel converts a CachedImageResourceModel into a corresponding set of +// Envbuilder options. It returns the options and any diagnostics encountered. +func optionsFromDataModel(data CachedImageResourceModel) (eboptions.Options, diag.Diagnostics) { + var diags diag.Diagnostics + var opts eboptions.Options + + // Required options. Cannot be overridden by extra_env. + opts.CacheRepo = data.CacheRepo.ValueString() + opts.GitURL = data.GitURL.ValueString() + + // Other options can be overridden by extra_env, with a warning. + // Keep track of which options are set from the data model so we + // can check if they are being overridden. + providerOpts := make(map[string]bool) + + if !data.BaseImageCacheDir.IsNull() { + providerOpts["ENVBUILDER_BASE_IMAGE_CACHE_DIR"] = true + opts.BaseImageCacheDir = data.BaseImageCacheDir.ValueString() + } + + if !data.BuildContextPath.IsNull() { + providerOpts["ENVBUILDER_BUILD_CONTEXT_PATH"] = true + opts.BuildContextPath = data.BuildContextPath.ValueString() + } + + if !data.BuildSecrets.IsNull() { + providerOpts["ENVBUILDER_BUILD_SECRETS"] = true + + // Depending on use case, users might want to provide build secrets as a map or a list of strings. + // The string list option is supported by extra_env, so we support the map option here. Envbuilder + // expects a list of strings, so we convert the map to a list of strings here. + buildSecretMap := tfutil.TFMapToStringMap(data.BuildSecrets) + buildSecretSlice := make([]string, 0, len(buildSecretMap)) + for k, v := range buildSecretMap { + buildSecretSlice = append(buildSecretSlice, fmt.Sprintf("%s=%s", k, v)) + } + slices.Sort(buildSecretSlice) + + opts.BuildSecrets = buildSecretSlice + } + + if !data.CacheTTLDays.IsNull() { + providerOpts["ENVBUILDER_CACHE_TTL_DAYS"] = true + opts.CacheTTLDays = data.CacheTTLDays.ValueInt64() + } + + if !data.DevcontainerDir.IsNull() { + providerOpts["ENVBUILDER_DEVCONTAINER_DIR"] = true + opts.DevcontainerDir = data.DevcontainerDir.ValueString() + } + + if !data.DevcontainerJSONPath.IsNull() { + providerOpts["ENVBUILDER_DEVCONTAINER_JSON_PATH"] = true + opts.DevcontainerJSONPath = data.DevcontainerJSONPath.ValueString() + } + + if !data.DockerfilePath.IsNull() { + providerOpts["ENVBUILDER_DOCKERFILE_PATH"] = true + opts.DockerfilePath = data.DockerfilePath.ValueString() + } + + if !data.DockerConfigBase64.IsNull() { + providerOpts["ENVBUILDER_DOCKER_CONFIG_BASE64"] = true + opts.DockerConfigBase64 = data.DockerConfigBase64.ValueString() + } + + if !data.ExitOnBuildFailure.IsNull() { + providerOpts["ENVBUILDER_EXIT_ON_BUILD_FAILURE"] = true + opts.ExitOnBuildFailure = data.ExitOnBuildFailure.ValueBool() + } + + if !data.FallbackImage.IsNull() { + providerOpts["ENVBUILDER_FALLBACK_IMAGE"] = true + opts.FallbackImage = data.FallbackImage.ValueString() + } + + if !data.GitCloneDepth.IsNull() { + providerOpts["ENVBUILDER_GIT_CLONE_DEPTH"] = true + opts.GitCloneDepth = data.GitCloneDepth.ValueInt64() + } + + if !data.GitCloneSingleBranch.IsNull() { + providerOpts["ENVBUILDER_GIT_CLONE_SINGLE_BRANCH"] = true + opts.GitCloneSingleBranch = data.GitCloneSingleBranch.ValueBool() + } + + if !data.GitHTTPProxyURL.IsNull() { + providerOpts["ENVBUILDER_GIT_HTTP_PROXY_URL"] = true + opts.GitHTTPProxyURL = data.GitHTTPProxyURL.ValueString() + } + + if !data.GitSSHPrivateKeyPath.IsNull() { + providerOpts["ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH"] = true + opts.GitSSHPrivateKeyPath = data.GitSSHPrivateKeyPath.ValueString() + } + + if !data.GitSSHPrivateKeyBase64.IsNull() { + providerOpts["ENVBUILDER_GIT_SSH_PRIVATE_KEY_BASE64"] = true + opts.GitSSHPrivateKeyBase64 = data.GitSSHPrivateKeyBase64.ValueString() + } + + if !data.GitUsername.IsNull() { + providerOpts["ENVBUILDER_GIT_USERNAME"] = true + opts.GitUsername = data.GitUsername.ValueString() + } + + if !data.GitPassword.IsNull() { + providerOpts["ENVBUILDER_GIT_PASSWORD"] = true + opts.GitPassword = data.GitPassword.ValueString() + } + + if !data.IgnorePaths.IsNull() { + providerOpts["ENVBUILDER_IGNORE_PATHS"] = true + opts.IgnorePaths = tfutil.TFListToStringSlice(data.IgnorePaths) + } + + if !data.Insecure.IsNull() { + providerOpts["ENVBUILDER_INSECURE"] = true + opts.Insecure = data.Insecure.ValueBool() + } + + if data.RemoteRepoBuildMode.IsNull() { + opts.RemoteRepoBuildMode = true + } else { + providerOpts["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = true + opts.RemoteRepoBuildMode = data.RemoteRepoBuildMode.ValueBool() + } + + if !data.SSLCertBase64.IsNull() { + providerOpts["ENVBUILDER_SSL_CERT_BASE64"] = true + opts.SSLCertBase64 = data.SSLCertBase64.ValueString() + } + + if !data.Verbose.IsNull() { + providerOpts["ENVBUILDER_VERBOSE"] = true + opts.Verbose = data.Verbose.ValueBool() + } + + if !data.WorkspaceFolder.IsNull() { + providerOpts["ENVBUILDER_WORKSPACE_FOLDER"] = true + opts.WorkspaceFolder = data.WorkspaceFolder.ValueString() + } + + // convert extraEnv to a map for ease of use. + extraEnv := make(map[string]string) + for k, v := range data.ExtraEnv.Elements() { + extraEnv[k] = tfutil.TFValueToString(v) + } + diags = append(diags, overrideOptionsFromExtraEnv(&opts, extraEnv, providerOpts)...) + + if opts.GitSSHPrivateKeyPath != "" && opts.GitSSHPrivateKeyBase64 != "" { + diags.AddError("Cannot set more than one git ssh private key option", + "Both ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH and ENVBUILDER_GIT_SSH_PRIVATE_KEY_BASE64 have been set.") + } + + return opts, diags +} + +// overrideOptionsFromExtraEnv overrides the options in opts with values from extraEnv. +// It returns any diagnostics encountered. +// It will not override certain options, such as ENVBUILDER_CACHE_REPO and ENVBUILDER_GIT_URL. +func overrideOptionsFromExtraEnv(opts *eboptions.Options, extraEnv map[string]string, providerOpts map[string]bool) diag.Diagnostics { + var diags diag.Diagnostics + // Make a map of the options for easy lookup. + optsMap := make(map[string]pflag.Value) + for _, opt := range opts.CLI() { + optsMap[opt.Env] = opt.Value + } + for key, val := range extraEnv { + opt, found := optsMap[key] + if !found { + // ignore unknown keys + continue + } + + if nonOverrideOptions[key] { + diags.AddAttributeWarning(path.Root("extra_env"), + "Cannot override required environment variable", + fmt.Sprintf("The key %q in extra_env cannot be overridden.", key), + ) + continue + } + + // Check if the option was set on the provider data model and generate a warning if so. + if providerOpts[key] { + diags.AddAttributeWarning(path.Root("extra_env"), + "Overriding provider environment variable", + fmt.Sprintf("The key %q in extra_env overrides an option set on the provider.", key), + ) + } + + // XXX: workaround for serpent behaviour where calling Set() on a + // string slice will append instead of replace: set to empty first. + if _, ok := optsMap[key].(*serpent.StringArray); ok { + _ = optsMap[key].Set("") + } + + if err := opt.Set(val); err != nil { + diags.AddAttributeError(path.Root("extra_env"), + "Invalid value for environment variable", + fmt.Sprintf("The key %q in extra_env has an invalid value: %s", key, err), + ) + } + } + return diags +} + +// computeEnvFromOptions computes the environment variables to set based on the +// options in opts and the extra environment variables in extraEnv. +// It returns the computed environment variables as a map. +// It will not set certain options, such as ENVBUILDER_CACHE_REPO and ENVBUILDER_GIT_URL. +// It will also not handle legacy Envbuilder options (i.e. those not prefixed with ENVBUILDER_). +func computeEnvFromOptions(opts eboptions.Options, extraEnv map[string]string) map[string]string { + for _, opt := range opts.CLI() { + if opt.Env == "" { + continue + } + } + + computed := make(map[string]string) + for _, opt := range opts.CLI() { + if opt.Env == "" { + continue + } + // TODO: remove this check once support for legacy options is removed. + // Only set the environment variables from opts that are not legacy options. + // Legacy options are those that are not prefixed with ENVBUILDER_. + // While we can detect when a legacy option is set, overriding it becomes + // problematic. Erring on the side of caution, we will not override legacy options. + if !strings.HasPrefix(opt.Env, envbuilderOptionPrefix) { + continue + } + var val string + if sa, ok := opt.Value.(*serpent.StringArray); ok { + val = strings.Join(sa.GetSlice(), ",") + } else { + val = opt.Value.String() + } + + switch val { + case "", "false", "0": + // Skip zero values. + continue + } + computed[opt.Env] = val + } + + // Merge in extraEnv, which may override values from opts. + // Skip any keys that are envbuilder options. + for key, val := range extraEnv { + if strings.HasPrefix(key, envbuilderOptionPrefix) { + continue + } + computed[key] = val + } + return computed +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index d444c23..cfc0c61 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -36,6 +36,9 @@ func (p *EnvbuilderProvider) Metadata(ctx context.Context, req provider.Metadata func (p *EnvbuilderProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{}, + MarkdownDescription: ` +The Envbuilder provider can be used to check for the presence of a container image previously built by [Envbuilder](https://github.com/coder/envbuilder). +This allows re-using a previously built image pushed to a container registry without having to rebuild it.`, } } diff --git a/internal/provider/provider_internal_test.go b/internal/provider/provider_internal_test.go new file mode 100644 index 0000000..c1ae983 --- /dev/null +++ b/internal/provider/provider_internal_test.go @@ -0,0 +1,421 @@ +package provider + +import ( + "testing" + + eboptions "github.com/coder/envbuilder/options" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/stretchr/testify/assert" +) + +func Test_optionsFromDataModel(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + data CachedImageResourceModel + expectOpts eboptions.Options + expectNumErrorDiags int + expectNumWarningDiags int + }{ + { + name: "required only", + data: CachedImageResourceModel{ + BuilderImage: basetypes.NewStringValue("envbuilder:latest"), + CacheRepo: basetypes.NewStringValue("localhost:5000/cache"), + GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"), + }, + expectOpts: eboptions.Options{ + CacheRepo: "localhost:5000/cache", + GitURL: "git@git.local/devcontainer.git", + RemoteRepoBuildMode: true, + }, + }, + { + name: "all options without extra_env", + data: CachedImageResourceModel{ + BuilderImage: basetypes.NewStringValue("envbuilder:latest"), + CacheRepo: basetypes.NewStringValue("localhost:5000/cache"), + GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"), + BaseImageCacheDir: basetypes.NewStringValue("/tmp/cache"), + BuildContextPath: basetypes.NewStringValue("."), + BuildSecrets: basetypes.NewMapValueMust(basetypes.StringType{}, map[string]attr.Value{ + "FOO": basetypes.NewStringValue("bar"), + "BAZ": basetypes.NewStringValue("qux"), + }), + CacheTTLDays: basetypes.NewInt64Value(7), + DevcontainerDir: basetypes.NewStringValue(".devcontainer"), + DevcontainerJSONPath: basetypes.NewStringValue(".devcontainer/devcontainer.json"), + DockerfilePath: basetypes.NewStringValue("Dockerfile"), + DockerConfigBase64: basetypes.NewStringValue("some base64"), + ExitOnBuildFailure: basetypes.NewBoolValue(true), + // ExtraEnv: map[string]basetypes.Value{}, + FallbackImage: basetypes.NewStringValue("fallback"), + GitCloneDepth: basetypes.NewInt64Value(1), + GitCloneSingleBranch: basetypes.NewBoolValue(true), + GitHTTPProxyURL: basetypes.NewStringValue("http://proxy"), + GitPassword: basetypes.NewStringValue("password"), + GitSSHPrivateKeyPath: basetypes.NewStringValue("/tmp/id_rsa"), + GitUsername: basetypes.NewStringValue("user"), + IgnorePaths: listValue("ignore", "paths"), + Insecure: basetypes.NewBoolValue(true), + RemoteRepoBuildMode: basetypes.NewBoolValue(false), + SSLCertBase64: basetypes.NewStringValue("cert"), + Verbose: basetypes.NewBoolValue(true), + WorkspaceFolder: basetypes.NewStringValue("workspace"), + }, + expectOpts: eboptions.Options{ + CacheRepo: "localhost:5000/cache", + GitURL: "git@git.local/devcontainer.git", + BaseImageCacheDir: "/tmp/cache", + BuildContextPath: ".", + BuildSecrets: []string{"BAZ=qux", "FOO=bar"}, // Sorted + CacheTTLDays: 7, + DevcontainerDir: ".devcontainer", + DevcontainerJSONPath: ".devcontainer/devcontainer.json", + DockerfilePath: "Dockerfile", + DockerConfigBase64: "some base64", + ExitOnBuildFailure: true, + FallbackImage: "fallback", + GitCloneDepth: 1, + GitCloneSingleBranch: true, + GitHTTPProxyURL: "http://proxy", + GitPassword: "password", + GitSSHPrivateKeyPath: "/tmp/id_rsa", + GitUsername: "user", + IgnorePaths: []string{"ignore", "paths"}, + Insecure: true, + RemoteRepoBuildMode: false, + SSLCertBase64: "cert", + Verbose: true, + WorkspaceFolder: "workspace", + }, + }, + { + name: "extra env override", + data: CachedImageResourceModel{ + BuilderImage: basetypes.NewStringValue("envbuilder:latest"), + CacheRepo: basetypes.NewStringValue("localhost:5000/cache"), + GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"), + ExtraEnv: extraEnvMap(t, + "CODER_AGENT_TOKEN", "token", + "CODER_AGENT_URL", "http://coder", + "FOO", "bar", + "ENVBUILDER_BUILD_SECRETS", "FOO=bar,BAZ=qux", + ), + }, + expectOpts: eboptions.Options{ + BuildSecrets: []string{"FOO=bar", "BAZ=qux"}, + CacheRepo: "localhost:5000/cache", + GitURL: "git@git.local/devcontainer.git", + RemoteRepoBuildMode: true, + CoderAgentToken: "token", + CoderAgentURL: "http://coder", + }, + }, + { + name: "extra_env override warnings", + data: CachedImageResourceModel{ + BuilderImage: basetypes.NewStringValue("envbuilder:latest"), + CacheRepo: basetypes.NewStringValue("localhost:5000/cache"), + GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"), + BaseImageCacheDir: basetypes.NewStringValue("/tmp/cache"), + BuildContextPath: basetypes.NewStringValue("."), + BuildSecrets: basetypes.NewMapValueMust(basetypes.StringType{}, map[string]attr.Value{ + "FOO": basetypes.NewStringValue("bar"), + }), + CacheTTLDays: basetypes.NewInt64Value(7), + DevcontainerDir: basetypes.NewStringValue(".devcontainer"), + DevcontainerJSONPath: basetypes.NewStringValue(".devcontainer/devcontainer.json"), + DockerfilePath: basetypes.NewStringValue("Dockerfile"), + DockerConfigBase64: basetypes.NewStringValue("some base64"), + ExitOnBuildFailure: basetypes.NewBoolValue(true), + // ExtraEnv: map[string]basetypes.Value{}, + FallbackImage: basetypes.NewStringValue("fallback"), + GitCloneDepth: basetypes.NewInt64Value(1), + GitCloneSingleBranch: basetypes.NewBoolValue(true), + GitHTTPProxyURL: basetypes.NewStringValue("http://proxy"), + GitPassword: basetypes.NewStringValue("password"), + GitSSHPrivateKeyPath: basetypes.NewStringValue("/tmp/id_rsa"), + GitUsername: basetypes.NewStringValue("user"), + IgnorePaths: listValue("ignore", "paths"), + Insecure: basetypes.NewBoolValue(true), + RemoteRepoBuildMode: basetypes.NewBoolValue(false), + SSLCertBase64: basetypes.NewStringValue("cert"), + Verbose: basetypes.NewBoolValue(true), + WorkspaceFolder: basetypes.NewStringValue("workspace"), + ExtraEnv: extraEnvMap(t, + "ENVBUILDER_BUILD_SECRETS", "FOO=bar,BAZ=qux", + "ENVBUILDER_CACHE_REPO", "override", + "ENVBUILDER_GIT_URL", "override", + "ENVBUILDER_BASE_IMAGE_CACHE_DIR", "override", + "ENVBUILDER_BUILD_CONTEXT_PATH", "override", + "ENVBUILDER_CACHE_TTL_DAYS", "8", + "ENVBUILDER_DEVCONTAINER_DIR", "override", + "ENVBUILDER_DEVCONTAINER_JSON_PATH", "override", + "ENVBUILDER_DOCKERFILE_PATH", "override", + "ENVBUILDER_DOCKER_CONFIG_BASE64", "override", + "ENVBUILDER_EXIT_ON_BUILD_FAILURE", "false", + "ENVBUILDER_FALLBACK_IMAGE", "override", + "ENVBUILDER_GIT_CLONE_DEPTH", "2", + "ENVBUILDER_GIT_CLONE_SINGLE_BRANCH", "false", + "ENVBUILDER_GIT_HTTP_PROXY_URL", "override", + "ENVBUILDER_GIT_PASSWORD", "override", + "ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", "override", + "ENVBUILDER_GIT_USERNAME", "override", + "ENVBUILDER_IGNORE_PATHS", "override", + "ENVBUILDER_INSECURE", "false", + "ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true", + "ENVBUILDER_SSL_CERT_BASE64", "override", + "ENVBUILDER_VERBOSE", "false", + "ENVBUILDER_WORKSPACE_FOLDER", "override", + "FOO", "bar", + ), + }, + expectOpts: eboptions.Options{ + // not overridden + CacheRepo: "localhost:5000/cache", + GitURL: "git@git.local/devcontainer.git", + // overridden + BaseImageCacheDir: "override", + BuildContextPath: "override", + BuildSecrets: []string{"FOO=bar", "BAZ=qux"}, + CacheTTLDays: 8, + DevcontainerDir: "override", + DevcontainerJSONPath: "override", + DockerfilePath: "override", + DockerConfigBase64: "override", + ExitOnBuildFailure: false, + FallbackImage: "override", + GitCloneDepth: 2, + GitCloneSingleBranch: false, + GitHTTPProxyURL: "override", + GitPassword: "override", + GitSSHPrivateKeyPath: "override", + GitUsername: "override", + IgnorePaths: []string{"override"}, + Insecure: false, + RemoteRepoBuildMode: true, + SSLCertBase64: "override", + Verbose: false, + WorkspaceFolder: "override", + }, + expectNumWarningDiags: 24, + }, + { + name: "extra_env override errors", + data: CachedImageResourceModel{ + BuilderImage: basetypes.NewStringValue("envbuilder:latest"), + CacheRepo: basetypes.NewStringValue("localhost:5000/cache"), + GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"), + ExtraEnv: extraEnvMap(t, + "ENVBUILDER_CACHE_TTL_DAYS", "not a number", + "ENVBUILDER_VERBOSE", "not a bool", + "FOO", "bar", + ), + }, + expectOpts: eboptions.Options{ + // not overridden + CacheRepo: "localhost:5000/cache", + GitURL: "git@git.local/devcontainer.git", + RemoteRepoBuildMode: true, + }, + expectNumErrorDiags: 2, + }, + { + name: "errors when git ssh private key path and base64 are set", + data: CachedImageResourceModel{ + BuilderImage: basetypes.NewStringValue("envbuilder:latest"), + CacheRepo: basetypes.NewStringValue("localhost:5000/cache"), + GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"), + GitSSHPrivateKeyPath: basetypes.NewStringValue("/tmp/id_rsa"), + GitSSHPrivateKeyBase64: basetypes.NewStringValue("cHJpdmF0ZUtleQo="), + }, + expectOpts: eboptions.Options{ + CacheRepo: "localhost:5000/cache", + GitURL: "git@git.local/devcontainer.git", + RemoteRepoBuildMode: true, + GitSSHPrivateKeyPath: "/tmp/id_rsa", + GitSSHPrivateKeyBase64: "cHJpdmF0ZUtleQo=", + }, + expectNumErrorDiags: 1, + }, + { + name: "extra_env override errors when git ssh private key path and base64 are set", + data: CachedImageResourceModel{ + BuilderImage: basetypes.NewStringValue("envbuilder:latest"), + CacheRepo: basetypes.NewStringValue("localhost:5000/cache"), + GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"), + GitSSHPrivateKeyBase64: basetypes.NewStringValue("cHJpdmF0ZUtleQo="), + ExtraEnv: extraEnvMap(t, + "ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", "/tmp/id_rsa", + ), + }, + expectOpts: eboptions.Options{ + CacheRepo: "localhost:5000/cache", + GitURL: "git@git.local/devcontainer.git", + RemoteRepoBuildMode: true, + GitSSHPrivateKeyPath: "/tmp/id_rsa", + GitSSHPrivateKeyBase64: "cHJpdmF0ZUtleQo=", + }, + expectNumErrorDiags: 1, + }, + { + name: "required only with base64 ssh key", + data: CachedImageResourceModel{ + BuilderImage: basetypes.NewStringValue("envbuilder:latest"), + CacheRepo: basetypes.NewStringValue("localhost:5000/cache"), + GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"), + GitSSHPrivateKeyBase64: basetypes.NewStringValue("cHJpdmF0ZUtleQo="), + }, + expectOpts: eboptions.Options{ + CacheRepo: "localhost:5000/cache", + GitURL: "git@git.local/devcontainer.git", + RemoteRepoBuildMode: true, + GitSSHPrivateKeyBase64: "cHJpdmF0ZUtleQo=", + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + actual, diags := optionsFromDataModel(tc.data) + assert.Equal(t, tc.expectNumErrorDiags, diags.ErrorsCount()) + assert.Equal(t, tc.expectNumWarningDiags, diags.WarningsCount()) + assert.EqualValues(t, tc.expectOpts, actual) + }) + } +} + +func Test_computeEnvFromOptions(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + opts eboptions.Options + extraEnv map[string]string + expectEnv map[string]string + }{ + { + name: "empty", + opts: eboptions.Options{}, + expectEnv: map[string]string{}, + }, + { + name: "all options", + opts: eboptions.Options{ + BaseImageCacheDir: "string", + BinaryPath: "string", + BuildContextPath: "string", + BuildSecrets: []string{"FOO=bar", "BAZ=qux"}, + CacheRepo: "string", + CacheTTLDays: 1, + CoderAgentSubsystem: []string{"one", "two"}, + CoderAgentToken: "string", + CoderAgentURL: "string", + DevcontainerDir: "string", + DevcontainerJSONPath: "string", + DockerConfigBase64: "string", + DockerfilePath: "string", + ExitOnBuildFailure: true, + ExportEnvFile: "string", + FallbackImage: "string", + ForceSafe: true, + GetCachedImage: true, + GitCloneDepth: 1, + GitCloneSingleBranch: true, + GitHTTPProxyURL: "string", + GitPassword: "string", + GitSSHPrivateKeyPath: "string", + GitURL: "string", + GitUsername: "string", + IgnorePaths: []string{"one", "two"}, + InitArgs: "string", + InitCommand: "string", + InitScript: "string", + Insecure: true, + LayerCacheDir: "string", + PostStartScriptPath: "string", + PushImage: true, + RemoteRepoBuildMode: true, + SetupScript: "string", + SkipRebuild: true, + SSLCertBase64: "string", + Verbose: true, + WorkspaceFolder: "string", + }, + extraEnv: map[string]string{ + "ENVBUILDER_SOMETHING": "string", // should be ignored + "FOO": "bar", // should be included + }, + expectEnv: map[string]string{ + "ENVBUILDER_BASE_IMAGE_CACHE_DIR": "string", + "ENVBUILDER_BINARY_PATH": "string", + "ENVBUILDER_BUILD_CONTEXT_PATH": "string", + "ENVBUILDER_BUILD_SECRETS": "FOO=bar,BAZ=qux", + "ENVBUILDER_CACHE_REPO": "string", + "ENVBUILDER_CACHE_TTL_DAYS": "1", + "ENVBUILDER_DEVCONTAINER_DIR": "string", + "ENVBUILDER_DEVCONTAINER_JSON_PATH": "string", + "ENVBUILDER_DOCKER_CONFIG_BASE64": "string", + "ENVBUILDER_DOCKERFILE_PATH": "string", + "ENVBUILDER_EXIT_ON_BUILD_FAILURE": "true", + "ENVBUILDER_EXPORT_ENV_FILE": "string", + "ENVBUILDER_FALLBACK_IMAGE": "string", + "ENVBUILDER_FORCE_SAFE": "true", + "ENVBUILDER_GET_CACHED_IMAGE": "true", + "ENVBUILDER_GIT_CLONE_DEPTH": "1", + "ENVBUILDER_GIT_CLONE_SINGLE_BRANCH": "true", + "ENVBUILDER_GIT_HTTP_PROXY_URL": "string", + "ENVBUILDER_GIT_PASSWORD": "string", + "ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH": "string", + "ENVBUILDER_GIT_URL": "string", + "ENVBUILDER_GIT_USERNAME": "string", + "ENVBUILDER_IGNORE_PATHS": "one,two", + "ENVBUILDER_INIT_ARGS": "string", + "ENVBUILDER_INIT_COMMAND": "string", + "ENVBUILDER_INIT_SCRIPT": "string", + "ENVBUILDER_INSECURE": "true", + "ENVBUILDER_LAYER_CACHE_DIR": "string", + "ENVBUILDER_POST_START_SCRIPT_PATH": "string", + "ENVBUILDER_PUSH_IMAGE": "true", + "ENVBUILDER_REMOTE_REPO_BUILD_MODE": "true", + "ENVBUILDER_SETUP_SCRIPT": "string", + "ENVBUILDER_SKIP_REBUILD": "true", + "ENVBUILDER_SSL_CERT_BASE64": "string", + "ENVBUILDER_VERBOSE": "true", + "ENVBUILDER_WORKSPACE_FOLDER": "string", + "FOO": "bar", + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.extraEnv == nil { + tc.extraEnv = map[string]string{} + } + actual := computeEnvFromOptions(tc.opts, tc.extraEnv) + assert.EqualValues(t, tc.expectEnv, actual) + }) + } +} + +func listValue(vs ...string) basetypes.ListValue { + vals := make([]attr.Value, len(vs)) + for i, s := range vs { + vals[i] = basetypes.NewStringValue(s) + } + return basetypes.NewListValueMust(basetypes.StringType{}, vals) +} + +func extraEnvMap(t *testing.T, kvs ...string) basetypes.MapValue { + t.Helper() + if len(kvs)%2 != 0 { + t.Fatalf("extraEnvMap: expected even number of key-value pairs, got %d", len(kvs)) + } + vals := make(map[string]attr.Value) + for i := 0; i < len(kvs); i += 2 { + vals[kvs[i]] = basetypes.NewStringValue(kvs[i+1]) + } + return basetypes.NewMapValueMust(basetypes.StringType{}, vals) +} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 59f21ed..1df4490 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -3,6 +3,7 @@ package provider import ( "bufio" "context" + "encoding/base64" "fmt" "io" "os" @@ -35,10 +36,11 @@ var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServe // testDependencies contain information about stuff the test depends on. type testDependencies struct { - BuilderImage string - CacheRepo string - ExtraEnv map[string]string - Repo testGitRepoSSH + BuilderImage string + CacheRepo string + DockerConfigBase64 string + ExtraEnv map[string]string + Repo testGitRepoSSH } // Config generates a valid Terraform config file from the dependencies. @@ -47,16 +49,17 @@ func (d *testDependencies) Config(t testing.TB) string { tpl := `provider envbuilder {} resource "envbuilder_cached_image" "test" { - builder_image = {{ quote .BuilderImage }} + builder_image = {{ quote .BuilderImage }} cache_repo = {{ quote .CacheRepo }} + docker_config_base64 = {{ quote .DockerConfigBase64 }} + git_url = {{ quote .Repo.URL }} extra_env = { + "ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH": {{ quote .Repo.Key }} + "ENVBUILDER_VERBOSE": true {{ range $k, $v := .ExtraEnv }} {{ quote $k }}: {{ quote $v }} {{ end }} } - git_url = {{ quote .Repo.URL }} - git_ssh_private_key_path = {{ quote .Repo.Key }} - verbose = true }` fm := template.FuncMap{"quote": quote} @@ -71,26 +74,36 @@ func quote(s string) string { return fmt.Sprintf("%q", s) } -func setup(ctx context.Context, t testing.TB, files map[string]string) testDependencies { +func setup(ctx context.Context, t testing.TB, extraEnv, files map[string]string) testDependencies { t.Helper() - envbuilderImage := getEnvOrDefault("ENVBUILDER_IMAGE", "ghcr.io/coder/envbuilder-preview") + envbuilderImage := getEnvOrDefault("ENVBUILDER_IMAGE", "localhost:5000/envbuilder") envbuilderVersion := getEnvOrDefault("ENVBUILDER_VERSION", "latest") envbuilderImageRef := envbuilderImage + ":" + envbuilderVersion - // TODO: envbuilder creates /.envbuilder/bin/envbuilder owned by root:root which we are unable to clean up. - // This causes tests to fail. + testUsername := "testuser" + testPassword := "testpassword" + testAuthBase64 := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", testUsername, testPassword))) regDir := t.TempDir() - reg := registrytest.New(t, regDir) + reg := registrytest.New(t, regDir, registrytest.BasicAuthMW(t, testUsername, testPassword)) repoDir := setupGitRepo(t, files) gitRepo := serveGitRepoSSH(ctx, t, repoDir) + dockerConfigJSON := fmt.Sprintf(`{ + "auths": { + "%s": { + "auth": "%s", + } + } + }`, reg, testAuthBase64) + dockerConfigJSONBase64 := base64.StdEncoding.EncodeToString([]byte(dockerConfigJSON)) return testDependencies{ - BuilderImage: envbuilderImageRef, - CacheRepo: reg + "/test", - ExtraEnv: make(map[string]string), - Repo: gitRepo, + BuilderImage: envbuilderImageRef, + CacheRepo: reg + "/test", + ExtraEnv: extraEnv, + Repo: gitRepo, + DockerConfigBase64: dockerConfigJSONBase64, } } @@ -106,18 +119,39 @@ func seedCache(ctx context.Context, t testing.TB, deps testDependencies) { ensureImage(ctx, t, cli, deps.BuilderImage) + // Set up env for envbuilder + seedEnv := map[string]string{ + "ENVBUILDER_CACHE_REPO": deps.CacheRepo, + "ENVBUILDER_EXIT_ON_BUILD_FAILURE": "true", + "ENVBUILDER_INIT_SCRIPT": "exit", + "ENVBUILDER_PUSH_IMAGE": "true", + "ENVBUILDER_VERBOSE": "true", + "ENVBUILDER_GIT_URL": deps.Repo.URL, + "ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH": "/id_ed25519", + "ENVBUILDER_DOCKER_CONFIG_BASE64": deps.DockerConfigBase64, + } + + for k, v := range deps.ExtraEnv { + if !strings.HasPrefix(k, envbuilderOptionPrefix) { + continue + } + if _, ok := seedEnv[k]; ok { + continue + } + seedEnv[k] = v + } + + seedDockerEnv := make([]string, 0) + for k, v := range seedEnv { + seedDockerEnv = append(seedDockerEnv, k+"="+v) + } + + t.Logf("running envbuilder to seed cache with args: %v", seedDockerEnv) + // Run envbuilder using this dir as a local layer cache ctr, err := cli.ContainerCreate(ctx, &container.Config{ Image: deps.BuilderImage, - Env: []string{ - "ENVBUILDER_CACHE_REPO=" + deps.CacheRepo, - "ENVBUILDER_EXIT_ON_BUILD_FAILURE=true", - "ENVBUILDER_INIT_SCRIPT=exit", - "ENVBUILDER_PUSH_IMAGE=true", - "ENVBUILDER_VERBOSE=true", - "ENVBUILDER_GIT_URL=" + deps.Repo.URL, - "ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH=/id_ed25519", - }, + Env: seedDockerEnv, Labels: map[string]string{ testContainerLabel: "true", }, @@ -160,7 +194,7 @@ SCANLOGS: } log := scanner.Text() t.Logf("envbuilder: %s", log) - if strings.Contains(log, "=== Running the init command") { + if strings.Contains(log, "=== Running init command") { break SCANLOGS } } diff --git a/internal/tfutil/tfutil.go b/internal/tfutil/tfutil.go new file mode 100644 index 0000000..3366b6f --- /dev/null +++ b/internal/tfutil/tfutil.go @@ -0,0 +1,92 @@ +package tfutil + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/coder/envbuilder/log" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// TFValueToString converts an attr.Value to its string representation +// based on its Terraform type. This is needed because the String() +// method on an attr.Value creates a 'human-readable' version of the type, which +// leads to quotes, escaped characters, and other assorted sadness. +func TFValueToString(val attr.Value) string { + if val.IsUnknown() || val.IsNull() { + return "" + } + if vs, ok := val.(interface{ ValueString() string }); ok { + return vs.ValueString() + } + if vb, ok := val.(interface{ ValueBool() bool }); ok { + return fmt.Sprintf("%t", vb.ValueBool()) + } + if vi, ok := val.(interface{ ValueInt64() int64 }); ok { + return fmt.Sprintf("%d", vi.ValueInt64()) + } + panic(fmt.Errorf("tfValueToString: value %T is not a supported type", val)) +} + +// TFListToStringSlice converts a types.List to a []string by calling +// tfValueToString on each element. +func TFListToStringSlice(l types.List) []string { + els := l.Elements() + ss := make([]string, len(els)) + for idx, el := range els { + ss[idx] = TFValueToString(el) + } + return ss +} + +// TFMapToStringMap converts a types.Map to a map[string]string by calling +// tfValueToString on each element. +func TFMapToStringMap(m types.Map) map[string]string { + els := m.Elements() + res := make(map[string]string, len(els)) + for k, v := range els { + res[k] = TFValueToString(v) + } + return res +} + +// TFLogFunc is an adapter to envbuilder/log.Func. +func TFLogFunc(ctx context.Context) log.Func { + return func(level log.Level, format string, args ...any) { + var logFn func(context.Context, string, ...map[string]interface{}) + switch level { + case log.LevelTrace: + logFn = tflog.Trace + case log.LevelDebug: + logFn = tflog.Debug + case log.LevelWarn: + logFn = tflog.Warn + case log.LevelError: + logFn = tflog.Error + default: + logFn = tflog.Info + } + logFn(ctx, fmt.Sprintf(format, args...)) + } +} + +// DockerEnv returns the keys and values of the map in the form "key=value" +// sorted by key in lexicographical order. This is the format expected by +// Docker and some other tools that consume environment variables. +func DockerEnv(m map[string]string) []string { + pairs := make([]string, 0, len(m)) + var sb strings.Builder + for k := range m { + _, _ = sb.WriteString(k) + _, _ = sb.WriteRune('=') + _, _ = sb.WriteString(m[k]) + pairs = append(pairs, sb.String()) + sb.Reset() + } + sort.Strings(pairs) + return pairs +} diff --git a/testutil/registrytest/registrytest.go b/testutil/registrytest/registrytest.go index e18043a..be8d40d 100644 --- a/testutil/registrytest/registrytest.go +++ b/testutil/registrytest/registrytest.go @@ -2,6 +2,7 @@ package registrytest import ( "fmt" + "net/http" "net/http/httptest" "net/url" "testing" @@ -13,12 +14,31 @@ import ( // New starts a new Docker registry listening on localhost. // It will automatically shut down when the test finishes. // It will store data in dir. -func New(t testing.TB, dir string) string { +func New(t testing.TB, dir string, mws ...func(http.Handler) http.Handler) string { t.Helper() regHandler := registry.New(registry.WithBlobHandler(registry.NewDiskBlobHandler(dir))) + for _, mw := range mws { + regHandler = mw(regHandler) + } regSrv := httptest.NewServer(regHandler) t.Cleanup(func() { regSrv.Close() }) regSrvURL, err := url.Parse(regSrv.URL) require.NoError(t, err) return fmt.Sprintf("localhost:%s", regSrvURL.Port()) } + +func BasicAuthMW(t testing.TB, username, password string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if username != "" || password != "" { + authUser, authPass, ok := r.BasicAuth() + if !ok || username != authUser || password != authPass { + t.Logf("basic auth failed: got user %q, pass %q", authUser, authPass) + w.WriteHeader(http.StatusUnauthorized) + return + } + } + next.ServeHTTP(w, r) + }) + } +}