mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 14:02:56 +09:00
feat(marketplace): accept @sha256 digest references in tap connect
parseOCIRef (CLI) and parseConnectURL (API) now accept a pinned digest reference (oci://host/org/repo@sha256:...), with an optional informational tag before it. The digest must be a full sha256:<64 hex>, the same contract the platform source's digest= reference enforces. A reference carrying only a digest no longer defaults its tag to latest, since the digest is the authoritative reference Flux resolves before any tag. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
This commit is contained in:
4 files changed
+85
-23
No files matched your search
+24
-6
@@ -65,10 +65,18 @@ type ociRef struct {
|
||||
// Repo is the last path segment.
|
||||
Repo string
|
||||
Tag string
|
||||
// Digest, when set, pins the exact content (sha256:<64 hex>). It takes
|
||||
// precedence over Tag at pull time (Flux resolves digest before tag), so a
|
||||
// digest-pinned reference never follows a moved tag.
|
||||
Digest string
|
||||
}
|
||||
|
||||
var sanitizeName = regexp.MustCompile(`[^a-z0-9-]+`)
|
||||
|
||||
// ociDigestRe matches a full OCI manifest digest (sha256:<64 hex>), the same
|
||||
// shape the platform source's "digest=" reference enforces.
|
||||
var ociDigestRe = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
||||
|
||||
// parseOCIRef splits an oci:// reference into its URL (without tag), org, repo,
|
||||
// and tag. A missing tag defaults to "latest".
|
||||
func parseOCIRef(ref string) (ociRef, error) {
|
||||
@@ -77,11 +85,20 @@ func parseOCIRef(ref string) (ociRef, error) {
|
||||
}
|
||||
body := strings.TrimPrefix(ref, "oci://")
|
||||
|
||||
tag := ""
|
||||
// Digest pins are not tag-addressable; tap is tag-based for now.
|
||||
if strings.Contains(body, "@") {
|
||||
return ociRef{}, fmt.Errorf("digest references are not supported by tap yet; use a tag (oci://host/org/repo:tag)")
|
||||
// A digest pins the exact content (oci://host/org/repo@sha256:...). An
|
||||
// optional tag may precede it and is then informational only. The digest
|
||||
// must be a full sha256:<64 hex>, the same contract the platform source's
|
||||
// "digest=" reference enforces.
|
||||
digest := ""
|
||||
if at := strings.LastIndex(body, "@"); at >= 0 {
|
||||
digest = body[at+1:]
|
||||
body = body[:at]
|
||||
if !ociDigestRe.MatchString(digest) {
|
||||
return ociRef{}, fmt.Errorf("invalid digest %q in reference %q: want sha256:<64 hex>", digest, ref)
|
||||
}
|
||||
}
|
||||
|
||||
tag := ""
|
||||
if colon := strings.LastIndex(body, ":"); colon >= 0 && !strings.Contains(body[colon:], "/") {
|
||||
tag = body[colon+1:]
|
||||
body = body[:colon]
|
||||
@@ -96,10 +113,11 @@ func parseOCIRef(ref string) (ociRef, error) {
|
||||
if len(segs) >= 3 {
|
||||
org = segs[len(segs)-2]
|
||||
}
|
||||
if tag == "" {
|
||||
// Default to latest only when neither a tag nor a digest pins the content.
|
||||
if tag == "" && digest == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
return ociRef{URL: "oci://" + body, Org: org, Repo: repo, Tag: tag}, nil
|
||||
return ociRef{URL: "oci://" + body, Org: org, Repo: repo, Tag: tag, Digest: digest}, nil
|
||||
}
|
||||
|
||||
// fluxSourceName is the RFC-1123 name of the OCIRepository tap creates. It
|
||||
|
||||
+20
-12
@@ -36,18 +36,26 @@ import (
|
||||
)
|
||||
|
||||
func TestParseOCIRef(t *testing.T) {
|
||||
const digest = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
||||
cases := []struct {
|
||||
in string
|
||||
wantURL, wantOrg, wantRepo, wantTag string
|
||||
wantErr bool
|
||||
in string
|
||||
wantURL, wantOrg, wantRepo, wantTag, wantDigest string
|
||||
wantErr bool
|
||||
}{
|
||||
{"oci://ghcr.io/foo/bar:v1", "oci://ghcr.io/foo/bar", "foo", "bar", "v1", false},
|
||||
{"oci://ghcr.io/foo/bar", "oci://ghcr.io/foo/bar", "foo", "bar", "latest", false},
|
||||
{"oci://ghcr.io/foo/group/bar:1.2.3", "oci://ghcr.io/foo/group/bar", "group", "bar", "1.2.3", false},
|
||||
{"oci://registry.example.com/solo:tag", "oci://registry.example.com/solo", "", "solo", "tag", false},
|
||||
{"https://ghcr.io/foo/bar", "", "", "", "", true},
|
||||
{"oci://ghcr.io/foo/bar@sha256:abc", "", "", "", "", true},
|
||||
{"oci://ghcr.io", "", "", "", "", true},
|
||||
{"oci://ghcr.io/foo/bar:v1", "oci://ghcr.io/foo/bar", "foo", "bar", "v1", "", false},
|
||||
{"oci://ghcr.io/foo/bar", "oci://ghcr.io/foo/bar", "foo", "bar", "latest", "", false},
|
||||
{"oci://ghcr.io/foo/group/bar:1.2.3", "oci://ghcr.io/foo/group/bar", "group", "bar", "1.2.3", "", false},
|
||||
{"oci://registry.example.com/solo:tag", "oci://registry.example.com/solo", "", "solo", "tag", "", false},
|
||||
// A digest pin is accepted; the tag is left empty (not defaulted to
|
||||
// latest) because the digest is the authoritative reference.
|
||||
{"oci://ghcr.io/foo/bar@" + digest, "oci://ghcr.io/foo/bar", "foo", "bar", "", digest, false},
|
||||
// A tag may precede the digest; the tag is then informational only.
|
||||
{"oci://ghcr.io/foo/bar:v1@" + digest, "oci://ghcr.io/foo/bar", "foo", "bar", "v1", digest, false},
|
||||
{"https://ghcr.io/foo/bar", "", "", "", "", "", true},
|
||||
// A malformed digest (not sha256:<64 hex>) is still rejected.
|
||||
{"oci://ghcr.io/foo/bar@sha256:abc", "", "", "", "", "", true},
|
||||
{"oci://ghcr.io/foo/bar@notadigest", "", "", "", "", "", true},
|
||||
{"oci://ghcr.io", "", "", "", "", "", true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := parseOCIRef(c.in)
|
||||
@@ -61,8 +69,8 @@ func TestParseOCIRef(t *testing.T) {
|
||||
t.Errorf("parseOCIRef(%q) unexpected error: %v", c.in, err)
|
||||
continue
|
||||
}
|
||||
if got.URL != c.wantURL || got.Org != c.wantOrg || got.Repo != c.wantRepo || got.Tag != c.wantTag {
|
||||
t.Errorf("parseOCIRef(%q) = %+v, want url=%q org=%q repo=%q tag=%q", c.in, got, c.wantURL, c.wantOrg, c.wantRepo, c.wantTag)
|
||||
if got.URL != c.wantURL || got.Org != c.wantOrg || got.Repo != c.wantRepo || got.Tag != c.wantTag || got.Digest != c.wantDigest {
|
||||
t.Errorf("parseOCIRef(%q) = %+v, want url=%q org=%q repo=%q tag=%q digest=%q", c.in, got, c.wantURL, c.wantOrg, c.wantRepo, c.wantTag, c.wantDigest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,18 @@ import (
|
||||
|
||||
var sanitizeDNSRe = regexp.MustCompile(`[^a-z0-9-]+`)
|
||||
|
||||
var ociDigestRe = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
||||
|
||||
// connectTarget is the parsed form of a connect request's oci:// URL.
|
||||
type connectTarget struct {
|
||||
URL string // reference without a tag
|
||||
Org string
|
||||
Repo string
|
||||
Tag string
|
||||
// Digest, when set, pins the exact content (sha256:<64 hex>); it takes
|
||||
// precedence over Tag at pull time, so a digest-pinned connect never
|
||||
// follows a moved tag. Mirrors the CLI's ociRef.Digest.
|
||||
Digest string
|
||||
// FluxSourceName is the RFC-1123 name of the tap's OCIRepository. The tapped
|
||||
// repository keeps its own declared PackageSource name(s), which are not
|
||||
// known until the artifact is materialized, so the source (this name) is the
|
||||
@@ -41,9 +47,19 @@ func parseConnectURL(url, tagOverride string) (connectTarget, error) {
|
||||
return connectTarget{}, fmt.Errorf("url %q must start with oci://", url)
|
||||
}
|
||||
body := strings.TrimPrefix(url, "oci://")
|
||||
if strings.Contains(body, "@") {
|
||||
return connectTarget{}, fmt.Errorf("digest references are not supported; use a tag")
|
||||
|
||||
// A digest pins the exact content (oci://host/org/repo@sha256:...); an
|
||||
// optional tag may precede it and is then informational only. Mirrors the
|
||||
// CLI's parseOCIRef so the API and CLI agree on digest handling.
|
||||
digest := ""
|
||||
if at := strings.LastIndex(body, "@"); at >= 0 {
|
||||
digest = body[at+1:]
|
||||
body = body[:at]
|
||||
if !ociDigestRe.MatchString(digest) {
|
||||
return connectTarget{}, fmt.Errorf("invalid digest %q in url %q: want sha256:<64 hex>", digest, url)
|
||||
}
|
||||
}
|
||||
|
||||
tag := tagOverride
|
||||
if colon := strings.LastIndex(body, ":"); colon >= 0 && !strings.Contains(body[colon:], "/") {
|
||||
if tag == "" {
|
||||
@@ -55,11 +71,12 @@ func parseConnectURL(url, tagOverride string) (connectTarget, error) {
|
||||
if len(segs) < 2 {
|
||||
return connectTarget{}, fmt.Errorf("url %q must include a host and a repository path", url)
|
||||
}
|
||||
t := connectTarget{URL: "oci://" + body, Repo: segs[len(segs)-1], Tag: tag}
|
||||
t := connectTarget{URL: "oci://" + body, Repo: segs[len(segs)-1], Tag: tag, Digest: digest}
|
||||
if len(segs) >= 3 {
|
||||
t.Org = segs[len(segs)-2]
|
||||
}
|
||||
if t.Tag == "" {
|
||||
// Default to latest only when neither a tag nor a digest pins the content.
|
||||
if t.Tag == "" && digest == "" {
|
||||
t.Tag = "latest"
|
||||
}
|
||||
fluxBase := t.Repo
|
||||
|
||||
@@ -240,8 +240,27 @@ func TestParseConnectURL(t *testing.T) {
|
||||
if _, err := parseConnectURL("ghcr.io/foo/bar", ""); err == nil {
|
||||
t.Error("expected error for non-oci url")
|
||||
}
|
||||
// A malformed digest (not sha256:<64 hex>) is rejected.
|
||||
if _, err := parseConnectURL("oci://ghcr.io/foo/bar@sha256:abc", ""); err == nil {
|
||||
t.Error("expected error for digest ref")
|
||||
t.Error("expected error for malformed digest ref")
|
||||
}
|
||||
// A valid digest pins the content: the tag is left empty (not defaulted to
|
||||
// latest), mirroring the CLI's parseOCIRef.
|
||||
const digest = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
||||
d, err := parseConnectURL("oci://ghcr.io/foo/bar@"+digest, "")
|
||||
if err != nil {
|
||||
t.Fatalf("digest ref err: %v", err)
|
||||
}
|
||||
if d.URL != "oci://ghcr.io/foo/bar" || d.Tag != "" || d.Digest != digest || d.FluxSourceName != "tap-foo-bar" {
|
||||
t.Fatalf("unexpected digest parse: %+v", d)
|
||||
}
|
||||
// A tag may precede the digest and is then informational.
|
||||
td, err := parseConnectURL("oci://ghcr.io/foo/bar:v1@"+digest, "")
|
||||
if err != nil {
|
||||
t.Fatalf("tag+digest ref err: %v", err)
|
||||
}
|
||||
if td.Tag != "v1" || td.Digest != digest {
|
||||
t.Fatalf("unexpected tag+digest parse: %+v", td)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user