Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ toolchain go1.27.2

require (
github.com/CycloneDX/cyclonedx-go v0.12.0
github.com/google/go-containerregistry v0.21.7
github.com/in-toto/attestation v1.2.0
github.com/sigstore/sigstore-go v1.3.0
github.com/stretchr/testify v1.12.1
Expand All @@ -23,6 +24,8 @@ require (
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
github.com/docker/cli v29.5.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.25.5 // indirect
Expand Down Expand Up @@ -50,12 +53,13 @@ require (
github.com/go-openapi/validate v0.26.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/google/certificate-transparency-go v1.3.3 // indirect
github.com/google/go-containerregistry v0.21.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/in-toto/in-toto-golang v0.11.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
Expand All @@ -64,6 +68,7 @@ require (
github.com/sigstore/rekor-tiles/v2 v2.3.0 // indirect
github.com/sigstore/sigstore v1.10.8 // indirect
github.com/sigstore/timestamp-authority/v2 v2.1.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/theupdateframework/go-tuf/v2 v2.4.2 // indirect
github.com/transparency-dev/formats v0.1.1 // indirect
github.com/transparency-dev/merkle v0.0.2 // indirect
Expand All @@ -83,5 +88,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
google.golang.org/grpc v1.82.1 // indirect
gotest.tools/v3 v3.5.2 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
)
16 changes: 14 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,19 @@ github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNv
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc=
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE=
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc=
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I=
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y=
github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs=
github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
Expand Down Expand Up @@ -216,6 +220,8 @@ github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7P
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU=
github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/letsencrypt/boulder v0.20260309.0 h1:kZynrxK3QfqLGx6hhoz+Rfs3hgltJs1p9Mp+4+VwnY0=
Expand All @@ -230,6 +236,8 @@ github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
Expand Down Expand Up @@ -269,6 +277,8 @@ github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8 h1:1DGe4/clcdO
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8/go.mod h1:6IDFhpgxtzqbnzrFkyegbj7RfWwKeRrb3/+xAD1Wp+Y=
github.com/sigstore/timestamp-authority/v2 v2.1.3 h1:Fc+LjCTfik1lh3YLkaosENfkXa3R2Y1nswiUKutBdFA=
github.com/sigstore/timestamp-authority/v2 v2.1.3/go.mod h1:myoFOKJB/u5vNTFwvBBJVkG3NnOBeIJevbfjNeasLjo=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
Expand Down Expand Up @@ -377,6 +387,8 @@ google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fw
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
Expand Down
68 changes: 43 additions & 25 deletions versions/upstream/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ package upstream
import (
"crypto/sha256"
"encoding/hex"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/google/go-containerregistry/pkg/name"
ggcrregistry "github.com/google/go-containerregistry/pkg/registry"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/go-containerregistry/pkg/v1/remote"

"github.com/spin-stack/go-tools/versions"
)

Expand Down Expand Up @@ -81,32 +87,46 @@ func TestADownloadsPinIsTheSumAtTheVersionAsked(t *testing.T) {
}
}

// docker is a stand-in for the docker CLI that prints the digest in digestFile, or fails when
// there is none.
func docker(t *testing.T) (digestFile string) {
// registry is a registry in this process, whose host a localhost reference names over plain HTTP.
func registry(t *testing.T) string {
t.Helper()
srv := httptest.NewServer(ggcrregistry.New(ggcrregistry.Logger(log.New(io.Discard, "", 0))))
t.Cleanup(srv.Close)
return strings.TrimPrefix(srv.URL, "http://")
}

// push tags a multi-platform image as ref, and returns its index's digest.
func push(t *testing.T, ref string) string {
t.Helper()
dir := t.TempDir()
digestFile = filepath.Join(dir, "digest")
script := "#!/bin/sh\nexec cat " + digestFile + "\n"
if err := os.WriteFile(filepath.Join(dir, "docker"), []byte(script), 0o755); err != nil { //nolint:gosec // an executable is the point
idx, err := random.Index(64, 1, 2)
if err != nil {
t.Fatal(err)
}
r, err := name.ParseReference(ref)
if err != nil {
t.Fatal(err)
}
if err := remote.WriteIndex(r, idx, remote.WithContext(t.Context())); err != nil {
t.Fatal(err)
}
d, err := idx.Digest()
if err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
return digestFile
return d.String()
}

// An image's pin is the digest its tag names now, as docker reads it.
// An image's pin is the digest of what its tag names now: the index, which every platform's
// manifest is reached from, and nothing but the digest.
func TestAnImagesPinIsItsDigest(t *testing.T) {
digestFile := docker(t)
e := versions.Entry{Name: "go", Kind: versions.Image, Source: "golang"}
e := versions.Entry{Name: "go", Kind: versions.Image, Source: registry(t) + "/golang"}
if got, err := resolve(t.Context(), e, "1.27"); err == nil {
t.Errorf("docker failed and the pin is %q", got)
}
if err := os.WriteFile(digestFile, []byte("sha256:abc\n"), 0o600); err != nil {
t.Fatal(err)
t.Errorf("the tag is not there and the pin is %q", got)
}
if got, err := resolve(t.Context(), e, "1.27"); err != nil || got != "sha256:abc" {
t.Errorf("pin %q, %v", got, err)
want := push(t, e.Source+":1.27")
push(t, e.Source+":1.28")
if got, err := resolve(t.Context(), e, "1.27"); err != nil || got != want {
t.Errorf("pin %q, %v; want %s", got, err, want)
}
}

Expand Down Expand Up @@ -154,17 +174,15 @@ func TestCheckSaysWhereEachEntryStands(t *testing.T) {
sum := sha256.Sum256([]byte("v1.2.0's file"))
missing := httptest.NewServer(http.NotFoundHandler())
t.Cleanup(missing.Close)
digestFile := docker(t)
if err := os.WriteFile(digestFile, []byte("sha256:new\n"), 0o600); err != nil {
t.Fatal(err)
}
golang := registry(t) + "/golang"
now := push(t, golang+":1.27")

git := versions.Entry{Name: "g", Kind: versions.Git, Source: r.dir, Version: "master", Track: "branch"}
moved, gone := git, git
moved.Name, moved.Pin = "moved", r.second
gone.Name, gone.Source = "gone", filepath.Join(r.dir, "nothing")
git.Pin = r.head
image := versions.Entry{Name: "i", Kind: versions.Image, Source: "golang", Version: "1.27", Pin: "sha256:old", Track: "digest"}
image := versions.Entry{Name: "i", Kind: versions.Image, Source: golang, Version: "1.27", Pin: "sha256:old", Track: "digest"}
// Behind by its version, and its file at that version is the same bytes: still behind, since
// only a file tracked by a commit is excused.
download := versions.Entry{Name: "d", Kind: versions.Download, Source: srv.URL + "/{version}", Version: "v1.0.0",
Expand All @@ -191,7 +209,7 @@ func TestCheckSaysWhereEachEntryStands(t *testing.T) {
{"master", false, ""},
{"master", true, "master is now " + r.head},
{"master", false, "ls-remote"},
{"1.27", true, "1.27 is now sha256:new"},
{"1.27", true, "1.27 is now " + now},
{"v1.2.0", true, ""},
{"v1.2.0", false, ""},
{r.head, false, ""},
Expand Down
13 changes: 9 additions & 4 deletions versions/upstream/upstream.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Package upstream is where each entry of a versions.yaml stands against what it pins: the newest
// its track finds, the pin of a version, a bump, and a download taken only as the bytes it pins. It is the half of versions that reaches the
// network and runs git and docker, so that a program which embeds its versions.yaml links only
// network and runs git, so that a program which embeds its versions.yaml links only
// the half that reads it.
package upstream

Expand All @@ -21,6 +21,9 @@ import (
"strings"
"time"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane"

"github.com/spin-stack/go-tools/internal/fetch"
"github.com/spin-stack/go-tools/versions"
)
Expand Down Expand Up @@ -88,11 +91,13 @@ func resolve(ctx context.Context, e versions.Entry, version string) (string, err
e.Version = version
switch e.Kind {
case versions.Image:
out, err := run(ctx, "docker", "buildx", "imagetools", "inspect", e.Source+":"+version, "--format", "{{.Manifest.Digest}}")
// Asked of the registry, not of docker: the buildx a CI builder installs printed its whole
// listing for `imagetools inspect --format`, and that was written as the pin.
pin, err := crane.Digest(e.Source+":"+version, crane.WithContext(ctx), crane.WithAuthFromKeychain(authn.DefaultKeychain))
if err != nil {
return "", err
return "", fmt.Errorf("versions: %s:%s: %w", e.Source, version, err)
}
return strings.TrimSpace(out), nil
return pin, nil
case versions.Git:
out, err := run(ctx, "git", "ls-remote", e.Source,
"refs/tags/"+version, "refs/tags/"+version+"^{}", "refs/heads/"+version)
Expand Down