diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad16451 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist/ +/go-cache-plugin +/go-cache-plugin-* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3bd7df7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-merge-conflict + - id: mixed-line-ending + args: [--fix=lf] + + - repo: https://github.com/dnephin/pre-commit-golang + rev: v0.5.1 + hooks: + - id: go-fmt diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml new file mode 100644 index 0000000..53dd544 --- /dev/null +++ b/.woodpecker/build.yaml @@ -0,0 +1,18 @@ +when: + - event: pull_request + +steps: + - name: build + image: docker.io/golang:1.26 + commands: + - make build + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 2Gi + cpu: 2 + limits: + memory: 6Gi + cpu: 4 diff --git a/.woodpecker/pre-commit.yaml b/.woodpecker/pre-commit.yaml new file mode 100644 index 0000000..d57b508 --- /dev/null +++ b/.woodpecker/pre-commit.yaml @@ -0,0 +1,18 @@ +when: + - event: pull_request + +steps: + - name: pre-commit + image: git.unkin.net/unkin/almalinux9-gobuilder:20260606 + commands: + - uvx pre-commit run --all-files + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 512Mi + cpu: 1 + limits: + memory: 2Gi + cpu: 2 diff --git a/.woodpecker/release.yaml b/.woodpecker/release.yaml new file mode 100644 index 0000000..9357bc8 --- /dev/null +++ b/.woodpecker/release.yaml @@ -0,0 +1,69 @@ +when: + - event: tag + +steps: + - name: test + image: docker.io/golang:1.26 + commands: + - make test + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 2Gi + cpu: 2 + limits: + memory: 6Gi + cpu: 4 + + # Static binaries so CI images can curl one and run it regardless of libc. + - name: build + image: docker.io/golang:1.26 + commands: + - | + for arch in amd64 arm64; do + make build OS=linux ARCH="$$arch" + mv dist/go-cache-plugin "go-cache-plugin-linux-$$arch" + done + - make upstream > upstream-version.txt + depends_on: [test] + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 2Gi + cpu: 2 + limits: + memory: 6Gi + cpu: 4 + + - name: release + image: git.unkin.net/unkin/almalinux9-base:20260606 + environment: + RELEASER_TOKEN: + from_secret: RELEASER_TOKEN + commands: + # $$ escapes shell vars so Woodpecker leaves them for the shell; + # ${CI_COMMIT_TAG}/${CI_REPO} are real Woodpecker vars. + - | + curl -f -o /usr/local/bin/tea https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/gitea-dl/tea/0.12.0/tea-0.12.0-linux-amd64 + chmod +x /usr/local/bin/tea + tea logins add --name gitea --url https://git.unkin.net --token "$${RELEASER_TOKEN}" --no-version-check + ASSETS="go-cache-plugin-linux-amd64 go-cache-plugin-linux-arm64" + sha256sum $$ASSETS > sha256sums.txt + NOTES="Built from github.com/tailscale/go-cache-plugin $$(cat upstream-version.txt)" + tea releases create --tag "${CI_COMMIT_TAG}" --title "${CI_COMMIT_TAG}" --note "$$NOTES" --login gitea --repo "${CI_REPO}" + tea releases assets create "${CI_COMMIT_TAG}" $$ASSETS sha256sums.txt --login gitea --repo "${CI_REPO}" + depends_on: [build] + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 512Mi + cpu: 500m diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml new file mode 100644 index 0000000..32454ef --- /dev/null +++ b/.woodpecker/test.yaml @@ -0,0 +1,37 @@ +when: + - event: pull_request + +steps: + # go vet rather than golangci-lint: the repo has no source of its own beyond + # the build test, and the lint image lags the Go version upstream requires. + - name: vet + image: docker.io/golang:1.26 + commands: + - go vet ./... + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 2Gi + cpu: 2 + limits: + memory: 6Gi + cpu: 4 + + # Builds the pinned binary and drives a real `go build` through it. + - name: test + image: docker.io/golang:1.26 + commands: + - make test + depends_on: [vet] + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 2Gi + cpu: 2 + limits: + memory: 6Gi + cpu: 4 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1f3b585 --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +BINARY := go-cache-plugin +# Upstream has no releases; the shipped binary is built from the module version +# pinned by the tool directive in go.mod. +PKG := github.com/tailscale/go-cache-plugin/cmd/go-cache-plugin +DIST := dist +UPSTREAM := $(shell go list -m -f '{{.Version}}' github.com/tailscale/go-cache-plugin) +GOFLAGS := -ldflags="-s -w" +OS ?= $(shell go env GOOS) +ARCH ?= $(shell go env GOARCH) + +.PHONY: all build test lint fmt pre-commit clean upstream patch minor major _tag + +all: build + +# Static so the released binary runs on any image in the estate, Alpine included. +build: + CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build $(GOFLAGS) -o $(DIST)/$(BINARY) $(PKG) + +test: + go test -v ./... + +lint: + golangci-lint run ./... + +fmt: + gofmt -w . + +pre-commit: + uvx pre-commit run --all-files + +upstream: + @echo $(UPSTREAM) + +clean: + rm -rf $(DIST) + +# Bump helpers -- reads the latest semver tag and creates the next one. +# If no tag exists yet, starts from v0.0.0. +_LATEST := $(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -1) +_BASE := $(if $(_LATEST),$(_LATEST),v0.0.0) +_MAJ := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f1) +_MIN := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f2) +_PAT := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f3) + +patch: + @NEW=v$(_MAJ).$(_MIN).$(shell expr $(_PAT) + 1); \ + git tag $$NEW && echo "Tagged $$NEW" && $(MAKE) _tag TAG=$$NEW + +minor: + @NEW=v$(_MAJ).$(shell expr $(_MIN) + 1).0; \ + git tag $$NEW && echo "Tagged $$NEW" && $(MAKE) _tag TAG=$$NEW + +major: + @NEW=v$(shell expr $(_MAJ) + 1).0.0; \ + git tag $$NEW && echo "Tagged $$NEW" && $(MAKE) _tag TAG=$$NEW + +_tag: + git push origin $(TAG) diff --git a/README.md b/README.md index 5d501af..669a068 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,58 @@ # go-cache-plugin -Build repo that compiles the tailscale/go-cache-plugin GOCACHEPROG S3 Go build cache from a pinned upstream module version and publishes static binaries as Gitea releases. \ No newline at end of file +Build repo for [`tailscale/go-cache-plugin`](https://github.com/tailscale/go-cache-plugin), +a `GOCACHEPROG` implementation that backs the Go build cache with S3. Upstream +publishes no releases, so this repo pins a commit via the `tool` directive in +`go.mod` and ships the static binary as a Gitea release. + +No source of its own. To move to a newer upstream commit: + +```sh +go get -tool github.com/tailscale/go-cache-plugin/cmd/go-cache-plugin@ +make test +``` + +## Build + +```sh +make build # dist/go-cache-plugin, CGO_ENABLED=0, stripped +make test # builds the binary, then drives a real `go build` through it +``` + +## Use + +Point it at the estate's Ceph RGW, which needs both a custom endpoint and +path-style addressing: + +```sh +export GOCACHEPROG="go-cache-plugin \ + --cache-dir=$HOME/.cache/go-cache-plugin \ + --bucket=go-build-cache \ + --region=default \ + --s3-endpoint-url=https://s3.ceph.unkin.net \ + --s3-path-style" +go build ./... +``` + +Credentials come from the usual `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` +environment. Every flag also has a `GOCACHE_*` environment equivalent — see +`go-cache-plugin help environment`. Add `--metrics` to print cache hit rates at +exit, and `--expiry` to age entries out. + +For a shared cache daemon, run `go-cache-plugin serve --plugin ` once and +have each build use `go-cache-plugin connect ` as its `GOCACHEPROG`. + +## CI + +Woodpecker steps fetch the released binary: + +```yaml +commands: + - curl -fsSLo /usr/local/bin/go-cache-plugin https://git.unkin.net/unkin/go-cache-plugin/releases/download/v0.1.0/go-cache-plugin-linux-amd64 + - chmod +x /usr/local/bin/go-cache-plugin +``` + +## Release + +`make patch|minor|major` tags and pushes; the tag pipeline builds +`linux/amd64` and `linux/arm64` and attaches them with `sha256sums.txt`. diff --git a/build_test.go b/build_test.go new file mode 100644 index 0000000..e9ef0ea --- /dev/null +++ b/build_test.go @@ -0,0 +1,191 @@ +// Package gocacheplugin has no source of its own: this repo exists only to +// build the pinned upstream go-cache-plugin and ship it as a release. The tests +// therefore exercise the built binary rather than any local code. +package gocacheplugin + +import ( + "debug/buildinfo" + "debug/elf" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "sync/atomic" + "testing" +) + +const ( + upstreamPkg = "github.com/tailscale/go-cache-plugin/cmd/go-cache-plugin" + upstreamModule = "github.com/tailscale/go-cache-plugin" +) + +var pluginBin string + +func TestMain(m *testing.M) { + dir, err := os.MkdirTemp("", "go-cache-plugin-test") + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + pluginBin = filepath.Join(dir, "go-cache-plugin") + build := exec.Command("go", "build", "-ldflags=-s -w", "-o", pluginBin, upstreamPkg) + build.Env = append(os.Environ(), "CGO_ENABLED=0") + if out, err := build.CombinedOutput(); err != nil { + fmt.Fprintf(os.Stderr, "building %s: %v\n%s", upstreamPkg, err, out) + _ = os.RemoveAll(dir) + os.Exit(1) + } + code := m.Run() + _ = os.RemoveAll(dir) + os.Exit(code) +} + +// A dynamically linked binary will not run on the estate's Alpine-based CI +// images, so CGO_ENABLED=0 must actually take effect. +func TestBinaryIsStatic(t *testing.T) { + fi, err := os.Stat(pluginBin) + if err != nil { + t.Fatal(err) + } + if fi.Size() == 0 { + t.Fatal("binary is empty") + } + f, err := elf.Open(pluginBin) + if err != nil { + t.Fatal(err) + } + defer func() { _ = f.Close() }() + if s := f.Section(".interp"); s != nil { + t.Error("binary has a .interp section, so it needs a dynamic loader") + } + libs, err := f.ImportedLibraries() + if err != nil { + t.Fatal(err) + } + if len(libs) != 0 { + t.Errorf("binary links shared libraries: %v", libs) + } +} + +// The binary must come from the version go.mod pins, not from whatever the +// toolchain would resolve on its own. +func TestBuiltFromPinnedVersion(t *testing.T) { + out, err := exec.Command("go", "list", "-m", "-f", "{{.Version}}", upstreamModule).Output() + if err != nil { + t.Fatal(err) + } + want := strings.TrimSpace(string(out)) + if want == "" { + t.Fatalf("%s is not in the module graph", upstreamModule) + } + + info, err := buildinfo.ReadFile(pluginBin) + if err != nil { + t.Fatal(err) + } + got := info.Main.Version + for _, dep := range info.Deps { + if dep.Path == upstreamModule { + got = dep.Version + } + } + if got != want { + t.Errorf("binary built from %s %q, go.mod pins %q", upstreamModule, got, want) + } +} + +// Sanity check that the real entry point runs and parses flags: without a +// bucket the cache server must refuse to start. +func TestRequiresBucket(t *testing.T) { + cmd := exec.Command(pluginBin, "--cache-dir="+t.TempDir()) + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatalf("expected a non-zero exit, got success:\n%s", out) + } + if !strings.Contains(string(out), "--bucket") { + t.Errorf("expected a usage error naming --bucket, got:\n%s", out) + } +} + +// End to end: drive a real `go build` through the binary as a GOCACHEPROG +// plugin, pointed at a stub S3 endpoint that misses every read. This is the +// only thing the repo ships, so it has to actually speak the protocol. +func TestServesGoCacheProg(t *testing.T) { + var reqs atomic.Int64 + s3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqs.Add(1) + if r.Method == http.MethodPut || r.Method == http.MethodPost { + _, _ = io.Copy(io.Discard, r.Body) + w.WriteHeader(http.StatusOK) + return + } + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(http.StatusNotFound) + _, _ = io.WriteString(w, `NoSuchKeyno such key`) + })) + defer s3.Close() + + cacheDir := t.TempDir() + src := t.TempDir() + write(t, filepath.Join(src, "go.mod"), "module example.test\n\ngo 1.26.1\n") + write(t, filepath.Join(src, "main.go"), "package main\n\nfunc main() { println(\"ok\") }\n") + + prog := strings.Join([]string{ + pluginBin, + "--cache-dir=" + cacheDir, + "--bucket=go-cache-test", + "--region=us-east-1", + "--s3-endpoint-url=" + s3.URL, + "--s3-path-style", + }, " ") + + build := exec.Command("go", "build", "-o", filepath.Join(src, "out"), ".") + build.Dir = src + build.Env = append(os.Environ(), + "GOFLAGS=", + "GOCACHEPROG="+prog, + "AWS_ACCESS_KEY_ID=test", + "AWS_SECRET_ACCESS_KEY=test", + ) + if out, err := build.CombinedOutput(); err != nil { + t.Fatalf("go build under GOCACHEPROG failed: %v\n%s", err, out) + } + if _, err := os.Stat(filepath.Join(src, "out")); err != nil { + t.Fatalf("build produced no binary: %v", err) + } + if n := countFiles(t, cacheDir); n == 0 { + t.Error("plugin staged nothing in the local cache directory") + } + if reqs.Load() == 0 { + t.Error("plugin made no S3 requests") + } +} + +func write(t *testing.T, path, content string) { + t.Helper() + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } +} + +func countFiles(t *testing.T, dir string) int { + t.Helper() + var n int + err := filepath.WalkDir(dir, func(_ string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() { + n++ + } + return nil + }) + if err != nil { + t.Fatal(err) + } + return n +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c74610e --- /dev/null +++ b/go.mod @@ -0,0 +1,47 @@ +module git.unkin.net/unkin/go-cache-plugin + +go 1.26.1 + +tool github.com/tailscale/go-cache-plugin/cmd/go-cache-plugin + +require ( + github.com/aws/aws-sdk-go-v2 v1.41.6 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.16 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.15 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 // indirect + github.com/aws/smithy-go v1.25.0 // indirect + github.com/creachadair/atomicfile v0.4.2 // indirect + github.com/creachadair/command v0.2.6 // indirect + github.com/creachadair/flax v0.0.6 // indirect + github.com/creachadair/gocache v0.0.0-20260710224145-042386578e2a // indirect + github.com/creachadair/mds v0.29.2 // indirect + github.com/creachadair/mhttp v0.0.0-20260407161248-6866af4c556b // indirect + github.com/creachadair/msync v0.8.1 // indirect + github.com/creachadair/scheddle v0.0.0-20260418161627-87a4a0c853c4 // indirect + github.com/creachadair/taskgroup v0.14.4 // indirect + github.com/creachadair/tlsutil v0.0.0-20260218173745-49b0059fedaf // indirect + github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect + github.com/goproxy/goproxy v0.21.0 // indirect + github.com/tailscale/go-cache-plugin v0.1.2-0.20260723040932-3031b5d01c50 // indirect + go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + tailscale.com v1.96.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b0c00c4 --- /dev/null +++ b/go.sum @@ -0,0 +1,78 @@ +github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg= +github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 h1:adBsCIIpLbLmYnkQU+nAChU5yhVTvu5PerROm+/Kq2A= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9/go.mod h1:uOYhgfgThm/ZyAuJGNQ5YgNyOlYfqnGpTHXvk3cpykg= +github.com/aws/aws-sdk-go-v2/config v1.32.16 h1:Q0iQ7quUgJP0F/SCRTieScnaMdXr9h/2+wze1u3cNeM= +github.com/aws/aws-sdk-go-v2/config v1.32.16/go.mod h1:duCCnJEFqpt2RC6no1iK6q+8HpwOAkiUua0pY507dQc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTtEVmAVyo+NFLHBhH4= +github.com/aws/aws-sdk-go-v2/credentials v1.19.15/go.mod h1:gJiYyMOjNg8OEdRWOf3CrFQxM2a98qmrtjx1zuiQfB8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 h1:IOGsJ1xVWhsi+ZO7/NW8OuZZBtMJLZbk4P5HDjJO0jQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22/go.mod h1:b+hYdbU+jGKfXE8kKM6g1+h+L/Go3vMvzlxBsiuGsxg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22/go.mod h1:6sW9iWm9DK9YRpRGga/qzrzNLgKpT2cIxb7Vo2eNOp0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22/go.mod h1:KIpEUx0JuRZLO7U6cbV204cWAEco2iC3l061IxlwLtI= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 h1:FPXsW9+gMuIeKmz7j6ENWcWtBGTe1kH8r9thNt5Uxx4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23/go.mod h1:7J8iGMdRKk6lw2C+cMIphgAnT8uTwBwNOsGkyOCm80U= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 h1:HtOTYcbVcGABLOVuPYaIihj6IlkqubBwFj10K5fxRek= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8/go.mod h1:VsK9abqQeGlzPgUr+isNWzPlK2vKe9INMLWnY65f5Xs= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 h1:xnvDEnw+pnj5mctWiYuFbigrEzSm35x7k4KS/ZkCANg= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14/go.mod h1:yS5rNogD8e0Wu9+l3MUwr6eENBzEeGejvINpN5PAYfY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 h1:PUmZeJU6Y1Lbvt9WFuJ0ugUK2xn6hIWUBBbKuOWF30s= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22/go.mod h1:nO6egFBoAaoXze24a2C0NjQCvdpk8OueRoYimvEB9jo= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 h1:SE+aQ4DEqG53RRCAIHlCf//B2ycxGH7jFkpnAh/kKPM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22/go.mod h1:ES3ynECd7fYeJIL6+oax+uIEljmfps0S70BaQzbMd/o= +github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1 h1:kU/eBN5+MWNo/LcbNa4hWDdN76hdcd7hocU5kvu7IsU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1/go.mod h1:Fw9aqhJicIVee1VytBBjH+l+5ov6/PhbtIK/u3rt/ls= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 h1:a1Fq/KXn75wSzoJaPQTgZO0wHGqE9mjFnylnqEPTchA= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.10/go.mod h1:p6+MXNxW7IA6dMgHfTAzljuwSKD0NCm/4lbS4t6+7vI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 h1:x6bKbmDhsgSZwv6q19wY/u3rLk/3FGjJWyqKcIRufpE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.16/go.mod h1:CudnEVKRtLn0+3uMV0yEXZ+YZOKnAtUJ5DmDhilVnIw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 h1:oK/njaL8GtyEihkWMD4k3VgHCT64RQKkZwh0DG5j8ak= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20/go.mod h1:JHs8/y1f3zY7U5WcuzoJ/yAYGYtNIVPKLIbp61euvmg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 h1:ks8KBcZPh3PYISr5dAiXCM5/Thcuxk8l+PG4+A0exds= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.0/go.mod h1:pFw33T0WLvXU3rw1WBkpMlkgIn54eCB5FYLhjDc9Foo= +github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U= +github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/creachadair/atomicfile v0.4.2 h1:pSSsKxmUefqCTg+QgyCG2bw1KmsB76apBL4lhJjichI= +github.com/creachadair/atomicfile v0.4.2/go.mod h1:ts9VunJluQvSXtkrl0YlDn9ckZaS+eMkBW2g20gB5II= +github.com/creachadair/command v0.2.6 h1:+d4xUSZza5nqRVVsVRf5jDbix/0uKxVqFrc30F4uyRI= +github.com/creachadair/command v0.2.6/go.mod h1:HWvS3zEpVakKlNXxT/j1U8tNoDaY7By7ORYQ+m1ha2U= +github.com/creachadair/flax v0.0.6 h1:IFUdRdfKynNTyR5SRlrHMUGnxxMZZen8RVpwcDq/ftk= +github.com/creachadair/flax v0.0.6/go.mod h1:F1PML0JZLXSNDMNiRGK2yjm5f+L9QCHchyHBldFymj8= +github.com/creachadair/gocache v0.0.0-20260710224145-042386578e2a h1:cYAmYaIvlHjbnpM2DmH+kNaOC3qYQcpg8iYXNaMrcMg= +github.com/creachadair/gocache v0.0.0-20260710224145-042386578e2a/go.mod h1:f7A5Y4U3AWm53Dh6diUnOxDRBfuFNLqusf07sLd6cc4= +github.com/creachadair/mds v0.29.2 h1:1IBhfyolZUCA53twI1hXembOdpIe4bJ4OGJELBbGrN4= +github.com/creachadair/mds v0.29.2/go.mod h1:dMBTCSy3iS3dwh4Rb1zxeZz2d7K8+N24GCTsayWtQRI= +github.com/creachadair/mhttp v0.0.0-20260407161248-6866af4c556b h1:bIvrALyE2ixkCvz9LADfOIuhkUMYWMBsvQ29xe70nI0= +github.com/creachadair/mhttp v0.0.0-20260407161248-6866af4c556b/go.mod h1:eV+igunEfCBbgpM0gkDK/25/ukdeNYXH9PZuuEWZTDo= +github.com/creachadair/msync v0.8.1 h1:QRd8si3qZ2Q4TaDL7tS/MG/lFE3YND7U7J9fy42eAFM= +github.com/creachadair/msync v0.8.1/go.mod h1:dt0bscS09J8Ie3AdccK9JpCb7LfStaDGlAmDLukOlY4= +github.com/creachadair/scheddle v0.0.0-20260418161627-87a4a0c853c4 h1:uYu6k39F6Sz4Y5Zb+R2ZnhE1S5ehCw4lknwrt4bm7NI= +github.com/creachadair/scheddle v0.0.0-20260418161627-87a4a0c853c4/go.mod h1:PE4owEX+08lTR1VNomiSFFh6DzkmmzlBgysEOq8zwdI= +github.com/creachadair/taskgroup v0.14.4 h1:ttR8StLWmYA1O6x96YlTpQLXjKyRpO4RBo+OcO6W6C0= +github.com/creachadair/taskgroup v0.14.4/go.mod h1:uhCtIEsa7zpeMAFixddhCYjbJi7e4JMyU7OXUygkSsg= +github.com/creachadair/tlsutil v0.0.0-20260218173745-49b0059fedaf h1:6ZqacBCoIdOSlwh03zr3TMqA5VyuFpzUS7E2yzfjq7w= +github.com/creachadair/tlsutil v0.0.0-20260218173745-49b0059fedaf/go.mod h1:YUng4wUBh60nfswe0FwLeBOpoIsrW6ReXt9Zr9k55Sg= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/goproxy/goproxy v0.21.0 h1:S3JPCp/WLqLl7X40eo5FFiAbBZwD8S6TzGPBITwfHlo= +github.com/goproxy/goproxy v0.21.0/go.mod h1:4j3iRV76B133PK4sOzVYWSBs/SsE5ovbZsdceoC5f+w= +github.com/tailscale/go-cache-plugin v0.1.2-0.20260723040932-3031b5d01c50 h1:iF7+CaT5ro+WYnIZH6a9EbGPQ3KRWnF7VnyA+57sFKY= +github.com/tailscale/go-cache-plugin v0.1.2-0.20260723040932-3031b5d01c50/go.mod h1:HiHE7c3OvFT6JfCq+UZCupZN1aohtpYaY6MdhOpBPys= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA= +tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=