From eb614e12ba7762ee702edb0db482192ffd71f559 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sun, 16 Aug 2026 10:54:51 +0100 Subject: [PATCH 1/5] feat(supply-chain): install the Pulumi CLI and provider plugins from our own mirror Turns the pins from #276 into actual vendoring. A pin stops us moving by accident, but the artifacts still lived on hosts we do not control and could change or disappear underneath a pinned release -- which is how prd broke on 2026-08-14. Both install paths now fetch from wardnet/toolchain-mirror and verify the download against the SHA256SUMS that mirror publishes: - action.yml downloads the CLI archive, verifies it, and puts it first on PATH. A follow-up step asserts `pulumi version` matches the pin, so if PATH resolution ever picks the runner image's CLI again we fail with a clear message instead of silently deploying on an unpinned engine. - cmd/inforge/plugins.go fetches each provider archive from the mirror and verifies it. This path previously downloaded from three different GitHub orgs with NO verification of any kind and executed the result as part of a production deploy. Verification happens BEFORE extraction, not while streaming: hashing into the extractor would write an executable to the plugin directory and only afterwards discover the bytes were wrong. Also closes the SDK/CLI skew: go.mod's pulumi/sdk moves 3.251.0 -> 3.253.0 to match action.yml's CLI pin. They are the same product either side of a process boundary and nothing links them -- Dependabot bumps the SDK and cannot see the CLI pin -- so #249 (SDK-only, to 3.256.0) must NOT be merged as-is; it would silently reopen the skew. 3.253.0 is chosen over 3.256.0 deliberately: prd recovered on 3.253.0 and 3.256.0 is the version that broke it. Our AWS_REQUEST_CHECKSUM_CALCULATION fix should make 3.256.0 safe, but that deserves its own verified bump rather than riding along here. Worth recording: the provider repos publish SHA-1 checksums, not SHA-256, so the mirror's own SHA-256 over the stored bytes is now the strongest digest available for those artifacts. Verified by installing all four plugins from the live mirror with the real binary -- digests checked, archives extracted, plugin executes. --- AGENTS.md | 15 +++- action.yml | 61 +++++++++++++-- cmd/inforge/plugins.go | 144 ++++++++++++++++++++++++++++++------ cmd/inforge/plugins_test.go | 88 ++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 6 files changed, 281 insertions(+), 33 deletions(-) create mode 100644 cmd/inforge/plugins_test.go diff --git a/AGENTS.md b/AGENTS.md index 1739e41..c35f1ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -712,8 +712,19 @@ realized in two halves: Leaving it unpinned is not a theoretical risk — a GitHub runner-image roll moved the preinstalled CLI 3.253.0 → 3.256.0 and broke every prd deploy with `InvalidDigest` on the R2 checkpoint write, with no diff in inforge itself. When bumping, bump it deliberately and verify a real deploy against - the R2 backend; a newer CLI may additionally need - `AWS_REQUEST_CHECKSUM_CALCULATION=when_required` for S3-compatible stores. + the R2 backend. +- **The Pulumi SDK and the Pulumi CLI must be bumped TOGETHER.** `go.mod`'s + `github.com/pulumi/pulumi/sdk/v3` and `action.yml`'s `pulumi-version` are the same product on two + sides of a process boundary, and both are currently **3.253.0**. Nothing in the toolchain links + them: Dependabot bumps the SDK and cannot see the CLI pin, so accepting an SDK PR on its own + silently reopens the skew. Move both in one PR, or neither. +- **Third-party binaries install from `wardnet/toolchain-mirror`, verified.** The Pulumi CLI + (`action.yml`) and every provider plugin (`cmd/inforge/plugins.go`) are downloaded from our mirror + and checked against the `SHA256SUMS` it publishes. **Mirror a version before pinning it here** — an + unmirrored version fails the install with a message naming the mirror. Fetching from a mirror + without verifying the digest would only relocate the trust, so the check is not optional: the plugin + path previously ran unverified bytes as part of a production deploy. The mirror's SHA-256 is + stronger than upstream's own guarantee for plugins — the provider repos publish only **SHA-1**. - **A third-party apt repo must be PROBED before its sources file is written.** `apt-get update` fails hard on an unreachable source and our installers wrap it in retry-then-exit-1, so a sources file naming a suite the vendor does not publish breaks **every later apt-using step on diff --git a/action.yml b/action.yml index 2236f44..fa063f4 100644 --- a/action.yml +++ b/action.yml @@ -26,12 +26,63 @@ runs: # preinstalled CLI 3.253.0 -> 3.256.0, and 3.256.0 fails to write the R2 # checkpoint with `InvalidDigest: The checksum or Content-MD5 you specified is # not valid` (S3 PutObject 400). The same inforge v6.1.3 binary succeeded on - # the older image the day before. Pinning here makes the CLI a version we - # control and bump deliberately, like any other dependency. + # the older image the day before. + # + # We install from wardnet/toolchain-mirror, not from Pulumi, and verify the + # download against the SHA256SUMS that mirror published. Pinning alone stops + # drift but still leaves a third-party host in the deploy path; mirroring means + # the bytes are ours and a pinned version cannot change or vanish. The digest + # check is the point of the exercise — fetching from a mirror without verifying + # moves the trust rather than establishing it. + # + # $GITHUB_PATH PREPENDS, so this CLI wins over any preinstalled on the image. - name: Install Pulumi CLI - uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d # v7.0.0 - with: - pulumi-version: ${{ inputs.pulumi-version }} + shell: bash + env: + PULUMI_VERSION: ${{ inputs.pulumi-version }} + run: | + set -euo pipefail + + case "$(uname -s)/$(uname -m)" in + Linux/x86_64) PLATFORM=linux-x64 ;; + Linux/aarch64) PLATFORM=linux-arm64 ;; + Darwin/arm64) PLATFORM=darwin-arm64 ;; + *) echo "::error::no mirrored Pulumi CLI for $(uname -s)/$(uname -m). The mirror carries only the platforms we run; add it in wardnet/toolchain-mirror if this is a real target."; exit 1 ;; + esac + + MIRROR="https://github.com/wardnet/toolchain-mirror/releases/download/pulumi-v${PULUMI_VERSION}" + ARCHIVE="pulumi-v${PULUMI_VERSION}-${PLATFORM}.tar.gz" + DEST="${RUNNER_TEMP}/pulumi-cli" + mkdir -p "${DEST}" + + echo "installing Pulumi CLI ${PULUMI_VERSION} (${PLATFORM}) from the mirror..." + curl -fsSL -o "${DEST}/${ARCHIVE}" "${MIRROR}/${ARCHIVE}" + curl -fsSL -o "${DEST}/SHA256SUMS" "${MIRROR}/SHA256SUMS" + + # Verify ONLY the archive we fetched: SHA256SUMS covers every mirrored + # platform, and `sha256sum -c` fails on entries whose files are absent. + ( cd "${DEST}" && grep -F " ${ARCHIVE}" SHA256SUMS | sha256sum -c - ) + + tar -xzf "${DEST}/${ARCHIVE}" -C "${DEST}" + # The archive unpacks to a pulumi/ directory holding the CLI and its + # language hosts; they must stay together, so PATH points at that dir. + echo "${DEST}/pulumi" >> "$GITHUB_PATH" + + - name: Verify the Pulumi CLI on PATH is the pinned one + shell: bash + env: + PULUMI_VERSION: ${{ inputs.pulumi-version }} + run: | + set -euo pipefail + # Guards the failure this whole change exists to prevent: if PATH ordering + # ever resolves to the runner image's CLI instead of ours, fail here with a + # clear message rather than silently deploying on an unpinned engine. + got="$(pulumi version)" + if [ "${got#v}" != "${PULUMI_VERSION}" ]; then + echo "::error::pulumi on PATH is ${got}, expected v${PULUMI_VERSION} — the mirrored CLI is not winning PATH resolution" + exit 1 + fi + echo "pulumi ${got} (mirrored, verified)" - name: Install inforge shell: bash diff --git a/cmd/inforge/plugins.go b/cmd/inforge/plugins.go index 105de7e..a593adc 100644 --- a/cmd/inforge/plugins.go +++ b/cmd/inforge/plugins.go @@ -4,12 +4,15 @@ import ( "archive/tar" "compress/gzip" "context" + "crypto/sha256" + "encoding/hex" "fmt" "io" "net/http" "os" "path/filepath" "runtime" + "strings" "github.com/spf13/cobra" ) @@ -34,20 +37,20 @@ func newPluginsCmd() *cobra.Command { RunE: func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() - // Standard Pulumi providers: download tar.gz from their GitHub releases. - // Versions are pinned to match the SDK modules in go.mod. - type stdPlugin struct{ name, version, repo string } + // Provider plugins come from OUR mirror, not from each provider's own + // GitHub releases. Versions are pinned to match the SDK modules in go.mod. + // See installPulumiPlugin for why the source moved. + type stdPlugin struct{ name, version string } for _, p := range []stdPlugin{ - {"hcloud", "1.38.0", "pulumi/pulumi-hcloud"}, - {"cloudflare", "6.17.0", "pulumi/pulumi-cloudflare"}, + {"hcloud", "1.38.0"}, + {"cloudflare", "6.17.0"}, // pulumi-random backs stable per-service database passwords (ADR-0036). - {"random", "4.16.8", "pulumi/pulumi-random"}, - // pulumiverse/grafana pushes dashboards + alerts (ADR-0038). Note the - // pulumiverse org publishes the same asset layout as pulumi/*. - {"grafana", "1.0.0", "pulumiverse/pulumi-grafana"}, + {"random", "4.16.8"}, + // pulumiverse/grafana pushes dashboards + alerts (ADR-0038). + {"grafana", "1.0.0"}, } { fmt.Printf("installing pulumi-resource-%s v%s...\n", p.name, p.version) - if err := installPulumiPlugin(ctx, p.name, p.version, p.repo); err != nil { + if err := installPulumiPlugin(ctx, p.name, p.version); err != nil { return fmt.Errorf("install %s: %w", p.name, err) } fmt.Printf(" installed pulumi-resource-%s\n", p.name) @@ -65,22 +68,80 @@ func newPluginsCmd() *cobra.Command { return plugins } -// installPulumiPlugin downloads a published Pulumi provider archive from GitHub -// and extracts the binary into the Pulumi plugins directory. -func installPulumiPlugin(ctx context.Context, name, ver, repo string) error { - goos := runtime.GOOS - goarch := runtime.GOARCH +// mirrorRepo is the release host for every third-party binary the toolchain +// installs. Mirroring is what makes a pinned version actually pinned: a pin stops +// us from silently moving, but the artifact still lived on someone else's host and +// could change or disappear underneath it. Add a version there before pinning it +// here — see that repo's README. +const mirrorRepo = "wardnet/toolchain-mirror" + +// installPulumiPlugin downloads a Pulumi provider archive from our mirror, +// verifies it against the mirror's SHA256SUMS, and extracts the binary into the +// Pulumi plugins directory. +// +// Previously this fetched straight from each provider's GitHub releases with no +// verification at all — whatever bytes arrived were extracted and executed as part +// of a production deploy. Two things changed: +// +// - the source is our mirror, so a pinned version cannot change or vanish; +// - the download is verified, because fetching from a mirror without checking +// the digest just relocates the trust instead of establishing it. +// +// Note the digest is SHA-256 even though the upstream provider repos publish only +// SHA-1: the mirror computes its own over the bytes it stored, so what we verify +// here is stronger than anything upstream offers for these artifacts. +func installPulumiPlugin(ctx context.Context, name, ver string) error { binary := "pulumi-resource-" + name - // Pulumi provider archives use hyphen-separated os-arch (e.g. linux-amd64). - archive := fmt.Sprintf("%s-v%s-%s-%s.tar.gz", binary, ver, goos, goarch) - url := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", repo, ver, archive) + // Provider archives use hyphen-separated os-arch (e.g. linux-amd64) — note + // this is the Go GOARCH spelling, unlike the CLI's own linux-x64. + archive := fmt.Sprintf("%s-v%s-%s-%s.tar.gz", binary, ver, runtime.GOOS, runtime.GOARCH) + base := fmt.Sprintf("https://github.com/%s/releases/download/plugin-%s-v%s", mirrorRepo, name, ver) + + want, err := mirrorDigest(ctx, base+"/SHA256SUMS", archive) + if err != nil { + return err + } pluginDir, err := pulumiPluginDir(name, ver) if err != nil { return err } - return downloadAndExtractTarGz(ctx, url, pluginDir, binary) + return downloadAndExtractTarGzVerified(ctx, base+"/"+archive, pluginDir, binary, want) +} + +// mirrorDigest fetches a mirror release's SHA256SUMS and returns the expected +// digest for one file. A missing entry is an error, not a skip: the whole point is +// that nothing is installed unverified, so "no digest published" must fail loudly +// rather than quietly degrade to the old unverified behaviour. +func mirrorDigest(ctx context.Context, sumsURL, file string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, sumsURL, nil) + if err != nil { + return "", err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("HTTP %d fetching %s — is this version mirrored? see %s", resp.StatusCode, sumsURL, mirrorRepo) + } + + // SHA256SUMS is small and fully trusted input from our own release; cap the + // read anyway so a wrong URL can't stream unbounded into memory. + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return "", err + } + for line := range strings.SplitSeq(string(body), "\n") { + fields := strings.Fields(line) + if len(fields) == 2 && fields[1] == file { + return fields[0], nil + } + } + return "", fmt.Errorf("%s has no entry for %s — the mirrored release is incomplete for %s/%s", + sumsURL, file, runtime.GOOS, runtime.GOARCH) } func pulumiPluginDir(name, ver string) (string, error) { @@ -125,7 +186,15 @@ func downloadBinary(ctx context.Context, url, dst string, mode os.FileMode) erro return closeErr } -func downloadAndExtractTarGz(ctx context.Context, url, dir, binaryName string) error { +// downloadAndExtractTarGzVerified downloads an archive, checks its SHA-256 +// against wantDigest, and only then extracts binaryName from it. +// +// The archive is staged to a temp file and verified BEFORE a single byte is +// extracted. Hashing while streaming straight into the extractor would be less +// code, but it would write an executable to the plugin directory and only +// afterwards discover the bytes were wrong — and that executable is run as part +// of a production deploy. Verify first, then extract. +func downloadAndExtractTarGzVerified(ctx context.Context, url, dir, binaryName, wantDigest string) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err @@ -136,11 +205,40 @@ func downloadAndExtractTarGz(ctx context.Context, url, dir, binaryName string) e } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("HTTP %d fetching %s — asset not found for %s/%s", - resp.StatusCode, url, runtime.GOOS, runtime.GOARCH) + return fmt.Errorf("HTTP %d fetching %s — asset not found for %s/%s (is this version mirrored? see %s)", + resp.StatusCode, url, runtime.GOOS, runtime.GOARCH, mirrorRepo) + } + + tmp, err := os.CreateTemp("", "inforge-plugin-*.tar.gz") + if err != nil { + return err + } + defer func() { _ = os.Remove(tmp.Name()) }() + + sum := sha256.New() + n, copyErr := io.CopyN(io.MultiWriter(tmp, sum), resp.Body, maxPluginBinarySize+1) + if closeErr := tmp.Close(); closeErr != nil && copyErr == nil { + return closeErr + } + if copyErr != nil && copyErr != io.EOF { + return copyErr + } + if n > maxPluginBinarySize { + return fmt.Errorf("archive %s exceeds %d bytes, refusing to extract", url, maxPluginBinarySize) + } + + if got := hex.EncodeToString(sum.Sum(nil)); got != wantDigest { + return fmt.Errorf("checksum mismatch for %s:\n want %s\n got %s\nrefusing to install — the mirrored artifact does not match its published SHA256SUMS", + url, wantDigest, got) + } + + f, err := os.Open(tmp.Name()) // #nosec G304 -- tmp.Name() is our own os.CreateTemp path, not external input + if err != nil { + return err } + defer func() { _ = f.Close() }() - gz, err := gzip.NewReader(resp.Body) + gz, err := gzip.NewReader(f) if err != nil { return fmt.Errorf("gzip: %w", err) } diff --git a/cmd/inforge/plugins_test.go b/cmd/inforge/plugins_test.go new file mode 100644 index 0000000..9768373 --- /dev/null +++ b/cmd/inforge/plugins_test.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// mirrorDigest must resolve the entry for the requested file and nothing else — +// a SHA256SUMS covers every mirrored platform, so picking the wrong line would +// install a different platform's binary or fail confusingly. +func TestMirrorDigestSelectsTheRequestedFile(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte( + "aaaa pulumi-resource-hcloud-v1.38.0-linux-amd64.tar.gz\n" + + "bbbb pulumi-resource-hcloud-v1.38.0-linux-arm64.tar.gz\n" + + "cccc pulumi-resource-hcloud-v1.38.0-darwin-arm64.tar.gz\n")) + })) + defer srv.Close() + + got, err := mirrorDigest(context.Background(), srv.URL, "pulumi-resource-hcloud-v1.38.0-linux-arm64.tar.gz") + if err != nil { + t.Fatalf("mirrorDigest: %v", err) + } + if got != "bbbb" { + t.Errorf("digest = %q, want %q", got, "bbbb") + } +} + +// A file absent from SHA256SUMS must be a hard error. Falling back to installing +// it unverified would silently restore the behaviour this change removes. +func TestMirrorDigestRejectsMissingEntry(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("aaaa some-other-file.tar.gz\n")) + })) + defer srv.Close() + + if _, err := mirrorDigest(context.Background(), srv.URL, "wanted.tar.gz"); err == nil { + t.Fatal("expected an error for a file with no published digest, got nil") + } +} + +// An unmirrored version must name the mirror in the error — the fix is always +// "mirror it first", and that is not guessable from a bare 404. +func TestMirrorDigestReportsUnmirroredVersion(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer srv.Close() + + _, err := mirrorDigest(context.Background(), srv.URL, "anything.tar.gz") + if err == nil { + t.Fatal("expected an error for a missing SHA256SUMS, got nil") + } + if !strings.Contains(err.Error(), mirrorRepo) { + t.Errorf("error should point at the mirror, got: %v", err) + } +} + +// The whole point of the change: bytes that do not match the published digest are +// never extracted. A tampered or corrupt archive must fail before anything is +// written to the plugin directory. +func TestDownloadAndExtractRejectsChecksumMismatch(t *testing.T) { + payload := []byte("not really a tarball") + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write(payload) + })) + defer srv.Close() + + real := sha256.Sum256(payload) + wrong := strings.Repeat("0", 64) + + err := downloadAndExtractTarGzVerified(context.Background(), srv.URL, t.TempDir(), "pulumi-resource-x", wrong) + if err == nil { + t.Fatal("expected a checksum mismatch error, got nil") + } + if !strings.Contains(err.Error(), "checksum mismatch") { + t.Errorf("want a checksum mismatch error, got: %v", err) + } + // The real digest must not leak into the failure as though it were expected. + if strings.Contains(err.Error(), hex.EncodeToString(real[:])) && !strings.Contains(err.Error(), wrong) { + t.Error("error should report both wanted and got digests") + } +} diff --git a/go.mod b/go.mod index 74aee7a..d21c92c 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/pulumi/pulumi-command/sdk v1.2.1 github.com/pulumi/pulumi-hcloud/sdk v1.39.0 github.com/pulumi/pulumi-random/sdk/v4 v4.21.0 - github.com/pulumi/pulumi/sdk/v3 v3.251.0 + github.com/pulumi/pulumi/sdk/v3 v3.253.0 github.com/pulumiverse/pulumi-grafana/sdk v1.0.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index 94dab73..4735597 100644 --- a/go.sum +++ b/go.sum @@ -242,8 +242,8 @@ github.com/pulumi/pulumi-hcloud/sdk v1.39.0 h1:Rm2Xh/EnPs53v1CEWxJzAasxTT8Exycz+ github.com/pulumi/pulumi-hcloud/sdk v1.39.0/go.mod h1:rfOSimT+VT5KOiEO62R+wibR+Y3zDGHigOvzPecJ56g= github.com/pulumi/pulumi-random/sdk/v4 v4.21.0 h1:j6LtoXue77y16trYOR40iqCUxXxv8TnxvOCOP/a0zVI= github.com/pulumi/pulumi-random/sdk/v4 v4.21.0/go.mod h1:92+w+95clbBPdrYdi0uCajihmgol52w/pWgZJ9wT0To= -github.com/pulumi/pulumi/sdk/v3 v3.251.0 h1:H1D42Jra2jvnEij0M0dZcNGz3Q9DyANsvzQqalncsXU= -github.com/pulumi/pulumi/sdk/v3 v3.251.0/go.mod h1:ZXd0WRd89VLX2juP+HEXMZ+J2AFPfcJTXhdmmv9cMgY= +github.com/pulumi/pulumi/sdk/v3 v3.253.0 h1:YWdKcZMZC7DtYSqNBn1Yv2UXmFMcgx7RqG3lhhBNZyE= +github.com/pulumi/pulumi/sdk/v3 v3.253.0/go.mod h1:iTb2Yb9mn0kfUfMjwHgSuI5sxVUOcXRy0JcV7DLWsrc= github.com/pulumiverse/pulumi-grafana/sdk v1.0.0 h1:9qLa98MV/0E/JTbsAfVo/w348FK19qDYI8C6Z5i8IDc= github.com/pulumiverse/pulumi-grafana/sdk v1.0.0/go.mod h1:UKCepGCFF9rLtTvdpqyW0HqJsz3Nx7UGkEkNZw0T77w= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= From 89d527e81c3081b3604c37b6769fbca859c167ce Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sun, 16 Aug 2026 11:10:43 +0100 Subject: [PATCH 2/5] test(plugins): cover the mirror install path CI's patch-coverage gate flagged the previous commit at 46.3% (31/67 new lines). The security scans passed and aggregate coverage rose, but the new verification code was mostly exercised only through its error paths. Adds the happy path (a correctly-signed archive extracts, with the executable bit Pulumi needs to exec the plugin at all), an archive that verifies but lacks the expected binary, and a mirror 404 -- which must name the mirror, since "mirror that version first" is not guessable from a bare status code. Also extracts pluginArchiveName and mirrorPluginBase as pure functions and pins them. Not coverage theatre: provider archives use the GOARCH spelling (linux-amd64) while the Pulumi CLI's own archives use linux-x64 for the same machine, and conflating the two produces a 404 that reads like "never mirrored". That exact mistake already cost a round trip in the mirror workflow today, so the convention is now asserted rather than inlined. --- cmd/inforge/plugins.go | 24 +++++-- cmd/inforge/plugins_test.go | 131 ++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 5 deletions(-) diff --git a/cmd/inforge/plugins.go b/cmd/inforge/plugins.go index a593adc..fe9489f 100644 --- a/cmd/inforge/plugins.go +++ b/cmd/inforge/plugins.go @@ -92,11 +92,8 @@ const mirrorRepo = "wardnet/toolchain-mirror" // here is stronger than anything upstream offers for these artifacts. func installPulumiPlugin(ctx context.Context, name, ver string) error { binary := "pulumi-resource-" + name - - // Provider archives use hyphen-separated os-arch (e.g. linux-amd64) — note - // this is the Go GOARCH spelling, unlike the CLI's own linux-x64. - archive := fmt.Sprintf("%s-v%s-%s-%s.tar.gz", binary, ver, runtime.GOOS, runtime.GOARCH) - base := fmt.Sprintf("https://github.com/%s/releases/download/plugin-%s-v%s", mirrorRepo, name, ver) + archive := pluginArchiveName(name, ver, runtime.GOOS, runtime.GOARCH) + base := mirrorPluginBase(name, ver) want, err := mirrorDigest(ctx, base+"/SHA256SUMS", archive) if err != nil { @@ -110,6 +107,23 @@ func installPulumiPlugin(ctx context.Context, name, ver string) error { return downloadAndExtractTarGzVerified(ctx, base+"/"+archive, pluginDir, binary, want) } +// pluginArchiveName builds the provider archive filename for an os/arch pair. +// +// Provider archives use the Go GOARCH spelling (linux-amd64), which is NOT the +// spelling the Pulumi CLI's own archives use for the same machine (linux-x64). +// Getting this wrong produces a 404 that reads like "this version was never +// mirrored", so the convention is isolated here and tested rather than inlined. +func pluginArchiveName(name, ver, goos, goarch string) string { + return fmt.Sprintf("pulumi-resource-%s-v%s-%s-%s.tar.gz", name, ver, goos, goarch) +} + +// mirrorPluginBase is the mirror release URL for one plugin version. The tag +// scheme (plugin--v) is the mirror's contract — see that repo's +// README — so producer and consumer must agree on it exactly. +func mirrorPluginBase(name, ver string) string { + return fmt.Sprintf("https://github.com/%s/releases/download/plugin-%s-v%s", mirrorRepo, name, ver) +} + // mirrorDigest fetches a mirror release's SHA256SUMS and returns the expected // digest for one file. A missing entry is an error, not a skip: the whole point is // that nothing is installed unverified, so "no digest published" must fail loudly diff --git a/cmd/inforge/plugins_test.go b/cmd/inforge/plugins_test.go index 9768373..12924e8 100644 --- a/cmd/inforge/plugins_test.go +++ b/cmd/inforge/plugins_test.go @@ -1,11 +1,16 @@ package main import ( + "archive/tar" + "bytes" + "compress/gzip" "context" "crypto/sha256" "encoding/hex" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" ) @@ -86,3 +91,129 @@ func TestDownloadAndExtractRejectsChecksumMismatch(t *testing.T) { t.Error("error should report both wanted and got digests") } } + +// tarGzWith builds a gzipped tar containing one entry, as the provider archives +// do, so the extraction path can be exercised without the network. +func tarGzWith(t *testing.T, name string, content []byte) []byte { + t.Helper() + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + tw := tar.NewWriter(gz) + if err := tw.WriteHeader(&tar.Header{ + Name: name, + Mode: 0o755, + Size: int64(len(content)), + }); err != nil { + t.Fatalf("tar header: %v", err) + } + if _, err := tw.Write(content); err != nil { + t.Fatalf("tar write: %v", err) + } + if err := tw.Close(); err != nil { + t.Fatalf("tar close: %v", err) + } + if err := gz.Close(); err != nil { + t.Fatalf("gzip close: %v", err) + } + return buf.Bytes() +} + +// The happy path: a correctly-signed archive is extracted and the binary lands +// executable in the plugin directory. +func TestDownloadAndExtractVerifiedExtractsBinary(t *testing.T) { + const binary = "pulumi-resource-hcloud" + want := []byte("#!/bin/sh\necho plugin\n") + archive := tarGzWith(t, binary, want) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write(archive) + })) + defer srv.Close() + + sum := sha256.Sum256(archive) + dir := t.TempDir() + + if err := downloadAndExtractTarGzVerified(context.Background(), srv.URL, dir, binary, hex.EncodeToString(sum[:])); err != nil { + t.Fatalf("extract: %v", err) + } + + got, err := os.ReadFile(filepath.Join(dir, binary)) + if err != nil { + t.Fatalf("read extracted binary: %v", err) + } + if !bytes.Equal(got, want) { + t.Errorf("extracted content = %q, want %q", got, want) + } + + info, err := os.Stat(filepath.Join(dir, binary)) + if err != nil { + t.Fatal(err) + } + // Pulumi execs the plugin directly; a non-executable file fails at deploy time. + if info.Mode().Perm()&0o111 == 0 { + t.Errorf("extracted binary is not executable (mode %v)", info.Mode().Perm()) + } +} + +// An archive that verifies but does not contain the expected binary must error +// rather than silently leave the plugin directory empty — the deploy would then +// fail much later with a confusing "plugin not found". +func TestDownloadAndExtractVerifiedRejectsArchiveWithoutBinary(t *testing.T) { + archive := tarGzWith(t, "some-other-file", []byte("x")) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write(archive) + })) + defer srv.Close() + + sum := sha256.Sum256(archive) + err := downloadAndExtractTarGzVerified(context.Background(), srv.URL, t.TempDir(), "pulumi-resource-hcloud", hex.EncodeToString(sum[:])) + if err == nil { + t.Fatal("expected an error when the binary is absent from the archive, got nil") + } + if !strings.Contains(err.Error(), "not found in archive") { + t.Errorf("unexpected error: %v", err) + } +} + +// A 404 from the mirror must name the mirror: the fix is always "mirror that +// version first", which is not guessable from a bare HTTP status. +func TestDownloadAndExtractVerifiedReportsUnmirroredArtifact(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer srv.Close() + + err := downloadAndExtractTarGzVerified(context.Background(), srv.URL, t.TempDir(), "pulumi-resource-hcloud", strings.Repeat("0", 64)) + if err == nil { + t.Fatal("expected an error on 404, got nil") + } + if !strings.Contains(err.Error(), mirrorRepo) { + t.Errorf("error should point at the mirror, got: %v", err) + } +} + +// The two naming conventions are easy to conflate and fail as an unhelpful 404, +// so pin them: provider archives use GOARCH (amd64), while the Pulumi CLI's own +// archives use x64 for the same machine. +func TestPluginArchiveNameUsesGoarchSpelling(t *testing.T) { + tests := []struct { + goos, goarch, want string + }{ + {"linux", "amd64", "pulumi-resource-hcloud-v1.38.0-linux-amd64.tar.gz"}, + {"linux", "arm64", "pulumi-resource-hcloud-v1.38.0-linux-arm64.tar.gz"}, + {"darwin", "arm64", "pulumi-resource-hcloud-v1.38.0-darwin-arm64.tar.gz"}, + } + for _, tt := range tests { + if got := pluginArchiveName("hcloud", "1.38.0", tt.goos, tt.goarch); got != tt.want { + t.Errorf("pluginArchiveName(%s/%s) = %q, want %q", tt.goos, tt.goarch, got, tt.want) + } + } +} + +// The tag scheme is the mirror's contract; producer and consumer must agree. +func TestMirrorPluginBaseMatchesTheMirrorTagScheme(t *testing.T) { + want := "https://github.com/wardnet/toolchain-mirror/releases/download/plugin-grafana-v1.0.0" + if got := mirrorPluginBase("grafana", "1.0.0"); got != want { + t.Errorf("mirrorPluginBase = %q, want %q", got, want) + } +} From 02e254c8d3047f1d69dcd1085d6ec071b7973569 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sun, 16 Aug 2026 11:25:14 +0100 Subject: [PATCH 3/5] test(plugins): exercise the whole install path, not just its parts Patch coverage was still under baseline (60.3%) because installPulumiPlugin and pulumiPluginDir could not be reached from a test: the mirror URL was built inside the function, so there was no way to point it anywhere but github.com. The caller now supplies the base URL (mirrorPluginBase in production), which makes fetch-digest -> verify -> extract runnable against a local server. That buys the test worth having: a TAMPERED archive must leave nothing behind in the plugin directory. Ordering verification before extraction in the source is a claim; this asserts it, and the assertion is the whole reason the ordering exists -- a plugin binary is executed as part of a production deploy. Also covers the multi-platform SHA256SUMS case, so the right digest line is proven to be selected rather than the first one. installPulumiPlugin 0% -> 77.8%, pulumiPluginDir 0% -> 71.4%. --- cmd/inforge/plugins.go | 9 ++-- cmd/inforge/plugins_test.go | 87 +++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/cmd/inforge/plugins.go b/cmd/inforge/plugins.go index fe9489f..7d08770 100644 --- a/cmd/inforge/plugins.go +++ b/cmd/inforge/plugins.go @@ -50,7 +50,7 @@ func newPluginsCmd() *cobra.Command { {"grafana", "1.0.0"}, } { fmt.Printf("installing pulumi-resource-%s v%s...\n", p.name, p.version) - if err := installPulumiPlugin(ctx, p.name, p.version); err != nil { + if err := installPulumiPlugin(ctx, p.name, p.version, mirrorPluginBase(p.name, p.version)); err != nil { return fmt.Errorf("install %s: %w", p.name, err) } fmt.Printf(" installed pulumi-resource-%s\n", p.name) @@ -90,10 +90,13 @@ const mirrorRepo = "wardnet/toolchain-mirror" // Note the digest is SHA-256 even though the upstream provider repos publish only // SHA-1: the mirror computes its own over the bytes it stored, so what we verify // here is stronger than anything upstream offers for these artifacts. -func installPulumiPlugin(ctx context.Context, name, ver string) error { +// base is the release URL to fetch from — supplied by the caller rather than +// derived here so the whole install path (fetch digest → verify → extract) is +// exercisable against a local server in tests. Production callers pass +// mirrorPluginBase; nothing else is a supported source. +func installPulumiPlugin(ctx context.Context, name, ver, base string) error { binary := "pulumi-resource-" + name archive := pluginArchiveName(name, ver, runtime.GOOS, runtime.GOARCH) - base := mirrorPluginBase(name, ver) want, err := mirrorDigest(ctx, base+"/SHA256SUMS", archive) if err != nil { diff --git a/cmd/inforge/plugins_test.go b/cmd/inforge/plugins_test.go index 12924e8..ac36647 100644 --- a/cmd/inforge/plugins_test.go +++ b/cmd/inforge/plugins_test.go @@ -11,6 +11,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "runtime" "strings" "testing" ) @@ -217,3 +218,89 @@ func TestMirrorPluginBaseMatchesTheMirrorTagScheme(t *testing.T) { t.Errorf("mirrorPluginBase = %q, want %q", got, want) } } + +// The full install path against a local stand-in for the mirror: fetch the +// digest, verify the archive, extract into the Pulumi plugin directory. This is +// the shape of what runs before every deploy, so it is worth exercising as one +// piece rather than only as its parts. +func TestInstallPulumiPluginVerifiesAndInstalls(t *testing.T) { + const ( + name = "hcloud" + ver = "1.38.0" + ) + binary := "pulumi-resource-" + name + want := []byte("#!/bin/sh\necho hcloud\n") + archive := tarGzWith(t, binary, want) + sum := sha256.Sum256(archive) + archiveName := pluginArchiveName(name, ver, runtime.GOOS, runtime.GOARCH) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.URL.Path, "/SHA256SUMS"): + // Cover every platform, as a real mirror release does, so the test also + // proves the right line is selected rather than the first one. + for _, p := range []string{"linux-amd64", "linux-arm64", "darwin-arm64"} { + digest := "0000000000000000000000000000000000000000000000000000000000000000" + file := pluginArchiveName(name, ver, strings.Split(p, "-")[0], strings.Split(p, "-")[1]) + if file == archiveName { + digest = hex.EncodeToString(sum[:]) + } + _, _ = w.Write([]byte(digest + " " + file + "\n")) + } + case strings.HasSuffix(r.URL.Path, archiveName): + _, _ = w.Write(archive) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer srv.Close() + + // pulumiPluginDir resolves under the user's home; point it at a temp dir so the + // test never touches the real ~/.pulumi. + home := t.TempDir() + t.Setenv("HOME", home) + + if err := installPulumiPlugin(context.Background(), name, ver, srv.URL); err != nil { + t.Fatalf("installPulumiPlugin: %v", err) + } + + got, err := os.ReadFile(filepath.Join(home, ".pulumi", "plugins", "resource-"+name+"-v"+ver, binary)) + if err != nil { + t.Fatalf("plugin binary not installed where Pulumi looks for it: %v", err) + } + if !bytes.Equal(got, want) { + t.Errorf("installed content = %q, want %q", got, want) + } +} + +// A tampered archive must not reach the plugin directory at all — verification +// happens before extraction precisely so nothing executable is written first. +func TestInstallPulumiPluginLeavesNothingBehindOnMismatch(t *testing.T) { + const ( + name = "hcloud" + ver = "1.38.0" + ) + archiveName := pluginArchiveName(name, ver, runtime.GOOS, runtime.GOARCH) + tampered := tarGzWith(t, "pulumi-resource-"+name, []byte("malicious")) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "/SHA256SUMS") { + // A digest that does NOT match the bytes served below. + _, _ = w.Write([]byte(strings.Repeat("a", 64) + " " + archiveName + "\n")) + return + } + _, _ = w.Write(tampered) + })) + defer srv.Close() + + home := t.TempDir() + t.Setenv("HOME", home) + + if err := installPulumiPlugin(context.Background(), name, ver, srv.URL); err == nil { + t.Fatal("expected a checksum mismatch, got nil") + } + + if _, err := os.Stat(filepath.Join(home, ".pulumi", "plugins", "resource-"+name+"-v"+ver, "pulumi-resource-"+name)); !os.IsNotExist(err) { + t.Error("a binary was written despite the checksum mismatch — verification must precede extraction") + } +} From 0069c3f5c2a4be63c14f1779a6bc114a20c92610 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sun, 16 Aug 2026 13:18:24 +0100 Subject: [PATCH 4/5] ci: align all four Pulumi version pins at 3.253.0 Closing the SDK/CLI skew in go.mod + action.yml left ci.yml and release.yml still on 3.251.0 -- so our tests and our release build ran a different engine from the one consumers deploy with, and AGENTS.md claimed an alignment that was not true of two of the four files. That is the same shape as the incident this whole branch is about: a version relationship nobody could see from any single file. Pinning them consistently is worth more than the individual versions. AGENTS.md now tabulates all four pins and what each governs, so a partial bump is visible rather than implied, and records that CI CANNOT catch a regression in the consumer install path -- our workflows use pulumi/actions, not our own action.yml, so only a real consumer deploy exercises what we ship. --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- AGENTS.md | 29 ++++++++++++++++++++--------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cd1003..4d47a86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: # input: the action then only installs the CLI (no login, no credentials). - uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d # v7.0.0 with: - pulumi-version: "3.251.0" + pulumi-version: "3.253.0" - name: go build run: go build ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 602fdcc..2a04d73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: # that quietly skipped them. - uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d # v7.0.0 with: - pulumi-version: "3.251.0" + pulumi-version: "3.253.0" - name: go build run: go build ./... diff --git a/AGENTS.md b/AGENTS.md index c35f1ab..d137dd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -706,18 +706,29 @@ realized in two halves: with `auto.NewLocalWorkspace` / `auto.*StackInlineSource` — the **Automation API**, which is a Go SDK that *drives a `pulumi` binary*: it shells out to whatever `pulumi` is first on `PATH`. There is no in-process engine. Treat the CLI as a real dependency with a pinned version: - `action.yml` installs it (`pulumi-version` input, currently **3.253.0**) so every consumer gets the - engine we validated rather than whatever its runner image ships, and - `.github/workflows/{ci,release}.yml` pin their own via `pulumi/actions`. + `action.yml` installs it so every consumer gets the engine we validated rather than whatever its + runner image ships (see the four-way pin table below). Leaving it unpinned is not a theoretical risk — a GitHub runner-image roll moved the preinstalled CLI 3.253.0 → 3.256.0 and broke every prd deploy with `InvalidDigest` on the R2 checkpoint write, with no diff in inforge itself. When bumping, bump it deliberately and verify a real deploy against - the R2 backend. -- **The Pulumi SDK and the Pulumi CLI must be bumped TOGETHER.** `go.mod`'s - `github.com/pulumi/pulumi/sdk/v3` and `action.yml`'s `pulumi-version` are the same product on two - sides of a process boundary, and both are currently **3.253.0**. Nothing in the toolchain links - them: Dependabot bumps the SDK and cannot see the CLI pin, so accepting an SDK PR on its own - silently reopens the skew. Move both in one PR, or neither. + the R2 backend. **CI cannot catch a regression here**: our own workflows install the CLI via + `pulumi/actions`, not via `action.yml`, so the consumer install path this repo ships is exercised + only by a real consumer deploy. +- **The Pulumi version is pinned in FOUR places and they must move together.** All four are + currently **3.253.0**: + + | Where | What it pins | + |---|---| + | `go.mod` → `github.com/pulumi/pulumi/sdk/v3` | the SDK compiled into the binary | + | `action.yml` → `pulumi-version` input | the CLI every **consumer** deploys with | + | `.github/workflows/ci.yml` → `pulumi/actions` | the CLI our tests run against | + | `.github/workflows/release.yml` → `pulumi/actions` | the CLI the release build runs against | + + The SDK and the CLI are the same product on two sides of a process boundary, and **nothing in the + toolchain links any of them** — Dependabot bumps `go.mod` and cannot see the other three, so + accepting an SDK PR on its own silently reopens the skew. Move all four in one PR, or none. + Grep `3\.25` before claiming they agree; a partial bump is the failure mode, and it is invisible + until a deploy behaves differently from CI. - **Third-party binaries install from `wardnet/toolchain-mirror`, verified.** The Pulumi CLI (`action.yml`) and every provider plugin (`cmd/inforge/plugins.go`) are downloaded from our mirror and checked against the `SHA256SUMS` it publishes. **Mirror a version before pinning it here** — an From 39701b22477a8c227dbd812c97722fc94a10e4d4 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sun, 16 Aug 2026 13:32:51 +0100 Subject: [PATCH 5/5] refactor(plugins): lift the pinned plugin set out of the cobra closure The plugin table and the install loop were trapped inside the command's RunE, so neither could be asserted. Two things that matter were therefore untested: - the pinned set itself. A typo'd version surfaces as a mirror 404 partway through a deploy, which is a slow and confusing way to find it. stdPlugins is data; at package scope a test catches a blank field, a duplicate, or a stray leading "v" immediately. - the loop's failure behaviour. installAllPlugins must ABORT on the first failure rather than print "all plugins installed" after a partial install and leave the deploy to fail later, further from the cause. installAllPlugins takes the source resolver as an argument (production passes mirrorPluginBase), so the whole set can be installed against a local server. installAllPlugins 100%, installPulumiPlugin 88.9%. What remains uncovered is cobra wiring, which is not worth asserting -- that would test the framework, not our logic. --- cmd/inforge/plugins.go | 64 +++++++++++++--------- cmd/inforge/plugins_test.go | 104 ++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 26 deletions(-) diff --git a/cmd/inforge/plugins.go b/cmd/inforge/plugins.go index 7d08770..f4c5eb3 100644 --- a/cmd/inforge/plugins.go +++ b/cmd/inforge/plugins.go @@ -35,32 +35,7 @@ func newPluginsCmd() *cobra.Command { SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, _ []string) error { - ctx := cmd.Context() - - // Provider plugins come from OUR mirror, not from each provider's own - // GitHub releases. Versions are pinned to match the SDK modules in go.mod. - // See installPulumiPlugin for why the source moved. - type stdPlugin struct{ name, version string } - for _, p := range []stdPlugin{ - {"hcloud", "1.38.0"}, - {"cloudflare", "6.17.0"}, - // pulumi-random backs stable per-service database passwords (ADR-0036). - {"random", "4.16.8"}, - // pulumiverse/grafana pushes dashboards + alerts (ADR-0038). - {"grafana", "1.0.0"}, - } { - fmt.Printf("installing pulumi-resource-%s v%s...\n", p.name, p.version) - if err := installPulumiPlugin(ctx, p.name, p.version, mirrorPluginBase(p.name, p.version)); err != nil { - return fmt.Errorf("install %s: %w", p.name, err) - } - fmt.Printf(" installed pulumi-resource-%s\n", p.name) - } - - // No custom (raw-binary) providers ship today: ADR-0036 retired the Neon - // plugin and self-hosted Postgres needs none. The seam remains if one returns. - - fmt.Println("all plugins installed") - return nil + return installAllPlugins(cmd.Context(), mirrorPluginBase) }, } @@ -68,6 +43,43 @@ func newPluginsCmd() *cobra.Command { return plugins } +// stdPlugin is one pinned provider plugin. +type stdPlugin struct{ name, version string } + +// stdPlugins is the set of provider plugins every deploy needs. It is DATA, kept +// at package scope rather than buried in the command closure so it can be +// asserted directly — a typo in a version here surfaces as a mirror 404 at deploy +// time, which is a slow and confusing way to find it. +// +// Versions are pinned to match the SDK modules in go.mod, and each must be +// mirrored (see mirrorRepo) before it can be installed. +var stdPlugins = []stdPlugin{ + {"hcloud", "1.38.0"}, + {"cloudflare", "6.17.0"}, + // pulumi-random backs stable per-service database passwords (ADR-0036). + {"random", "4.16.8"}, + // pulumiverse/grafana pushes dashboards + alerts (ADR-0038). + {"grafana", "1.0.0"}, +} + +// installAllPlugins installs every pinned plugin, resolving each one's source +// through baseFor. Production passes mirrorPluginBase; a test can pass a stub so +// the loop is exercisable without reaching the network. +// +// No custom (raw-binary) providers ship today: ADR-0036 retired the Neon plugin +// and self-hosted Postgres needs none. The seam remains if one returns. +func installAllPlugins(ctx context.Context, baseFor func(name, ver string) string) error { + for _, p := range stdPlugins { + fmt.Printf("installing pulumi-resource-%s v%s...\n", p.name, p.version) + if err := installPulumiPlugin(ctx, p.name, p.version, baseFor(p.name, p.version)); err != nil { + return fmt.Errorf("install %s: %w", p.name, err) + } + fmt.Printf(" installed pulumi-resource-%s\n", p.name) + } + fmt.Println("all plugins installed") + return nil +} + // mirrorRepo is the release host for every third-party binary the toolchain // installs. Mirroring is what makes a pinned version actually pinned: a pin stops // us from silently moving, but the artifact still lived on someone else's host and diff --git a/cmd/inforge/plugins_test.go b/cmd/inforge/plugins_test.go index ac36647..6d9eaa1 100644 --- a/cmd/inforge/plugins_test.go +++ b/cmd/inforge/plugins_test.go @@ -304,3 +304,107 @@ func TestInstallPulumiPluginLeavesNothingBehindOnMismatch(t *testing.T) { t.Error("a binary was written despite the checksum mismatch — verification must precede extraction") } } + +// The pinned set must be well-formed: a blank field or a duplicate name is a +// typo that would otherwise surface as a mirror 404 during a deploy. +func TestStdPluginsAreWellFormed(t *testing.T) { + if len(stdPlugins) == 0 { + t.Fatal("stdPlugins is empty — no provider would be installed") + } + seen := map[string]bool{} + for _, p := range stdPlugins { + if p.name == "" || p.version == "" { + t.Errorf("incomplete entry: %+v", p) + } + if strings.HasPrefix(p.version, "v") { + t.Errorf("%s version %q must not carry a leading v — it is added when building the tag", p.name, p.version) + } + if seen[p.name] { + t.Errorf("duplicate plugin %q", p.name) + } + seen[p.name] = true + } +} + +// The install loop over the real pinned set, against a local stand-in for the +// mirror: every plugin is fetched, verified and installed, and a failure on any +// one of them aborts rather than reporting success. +func TestInstallAllPluginsInstallsEveryPinnedPlugin(t *testing.T) { + archives := map[string][]byte{} + sums := map[string]string{} + for _, p := range stdPlugins { + name := pluginArchiveName(p.name, p.version, runtime.GOOS, runtime.GOARCH) + a := tarGzWith(t, "pulumi-resource-"+p.name, []byte(p.name)) + archives[name] = a + s := sha256.Sum256(a) + sums[name] = hex.EncodeToString(s[:]) + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "/SHA256SUMS") { + // The tag carries the plugin name; serve that plugin's digest. + for file, digest := range sums { + if strings.Contains(r.URL.Path, tagNameOf(file)) { + _, _ = w.Write([]byte(digest + " " + file + "\n")) + return + } + } + w.WriteHeader(http.StatusNotFound) + return + } + for file, a := range archives { + if strings.HasSuffix(r.URL.Path, file) { + _, _ = w.Write(a) + return + } + } + w.WriteHeader(http.StatusNotFound) + })) + defer srv.Close() + + home := t.TempDir() + t.Setenv("HOME", home) + + base := func(name, ver string) string { return srv.URL + "/plugin-" + name + "-v" + ver } + if err := installAllPlugins(context.Background(), base); err != nil { + t.Fatalf("installAllPlugins: %v", err) + } + + for _, p := range stdPlugins { + path := filepath.Join(home, ".pulumi", "plugins", "resource-"+p.name+"-v"+p.version, "pulumi-resource-"+p.name) + if _, err := os.Stat(path); err != nil { + t.Errorf("%s not installed: %v", p.name, err) + } + } +} + +// tagNameOf maps an archive filename back to its plugin name, so the stub server +// can tell which plugin's SHA256SUMS is being requested. +func tagNameOf(archive string) string { + rest := strings.TrimPrefix(archive, "pulumi-resource-") + if i := strings.Index(rest, "-v"); i >= 0 { + return rest[:i] + } + return rest +} + +// A single plugin failing must abort the run — reporting "all plugins installed" +// after a partial install would leave the deploy to fail later, further from the +// cause. +func TestInstallAllPluginsStopsOnFirstFailure(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer srv.Close() + + t.Setenv("HOME", t.TempDir()) + base := func(_, _ string) string { return srv.URL } + + err := installAllPlugins(context.Background(), base) + if err == nil { + t.Fatal("expected the run to abort, got nil") + } + if !strings.Contains(err.Error(), stdPlugins[0].name) { + t.Errorf("error should name the plugin that failed first, got: %v", err) + } +}