1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
|
package purl
import (
"strings"
"github.com/containerd/platforms"
"github.com/distribution/reference"
digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
packageurl "github.com/package-url/packageurl-go"
"github.com/pkg/errors"
)
// RefToPURL converts an image reference with optional platform constraint to a package URL.
// Image references are defined in https://github.com/distribution/distribution/blob/v2.8.1/reference/reference.go#L1
// Package URLs are defined in https://github.com/package-url/purl-spec
func RefToPURL(purlType string, ref string, platform *ocispecs.Platform) (string, error) {
named, err := reference.ParseNormalizedNamed(ref)
if err != nil {
return "", errors.Wrapf(err, "failed to parse ref %q", ref)
}
var qualifiers []packageurl.Qualifier
if canonical, ok := named.(reference.Canonical); ok {
qualifiers = append(qualifiers, packageurl.Qualifier{
Key: "digest",
Value: canonical.Digest().String(),
})
} else {
named = reference.TagNameOnly(named)
}
version := ""
if tagged, ok := named.(reference.Tagged); ok {
version = tagged.Tag()
}
name := reference.FamiliarName(named)
ns := ""
parts := strings.Split(name, "/")
if len(parts) > 1 {
ns = strings.Join(parts[:len(parts)-1], "/")
}
name = parts[len(parts)-1]
if platform != nil {
p := platforms.Normalize(*platform)
qualifiers = append(qualifiers, packageurl.Qualifier{
Key: "platform",
Value: platforms.Format(p),
})
}
p := packageurl.NewPackageURL(purlType, ns, name, version, qualifiers, "")
return p.ToString(), nil
}
// PURLToRef converts a package URL to an image reference and platform.
func PURLToRef(purl string) (string, *ocispecs.Platform, error) {
p, err := packageurl.FromString(purl)
if err != nil {
return "", nil, err
}
if p.Type != "docker" {
return "", nil, errors.Errorf("invalid package type %q, expecting docker", p.Type)
}
ref := p.Name
if p.Namespace != "" {
ref = p.Namespace + "/" + ref
}
dgstVersion := ""
if p.Version != "" {
dgst, err := digest.Parse(p.Version)
if err == nil {
ref = ref + "@" + dgst.String()
dgstVersion = dgst.String()
} else {
ref += ":" + p.Version
}
}
var platform *ocispecs.Platform
for _, q := range p.Qualifiers {
if q.Key == "platform" {
p, err := platforms.Parse(q.Value)
if err != nil {
return "", nil, err
}
// OS-version and OS-features are not included when serializing a
// platform as a string, however, containerd platforms.Parse appends
// missing information (including os-version) based on the host's
// platform.
//
// Given that this information is not obtained from the package-URL,
// we're resetting this information. Ideally, we'd do the same for
// "OS" and "architecture" (when not included in the URL).
//
// See:
// - https://github.com/containerd/containerd/commit/cfb30a31a8507e4417d42d38c9a99b04fc8af8a9 (https://github.com/containerd/containerd/pull/8778)
// - https://github.com/moby/buildkit/pull/4315#discussion_r1355141241
p.OSVersion = ""
p.OSFeatures = nil
platform = &p
}
if q.Key == "digest" {
if dgstVersion != "" {
if dgstVersion != q.Value {
return "", nil, errors.Errorf("digest %q does not match version %q", q.Value, dgstVersion)
}
continue
}
dgst, err := digest.Parse(q.Value)
if err != nil {
return "", nil, err
}
ref = ref + "@" + dgst.String()
dgstVersion = dgst.String()
}
}
if dgstVersion == "" && p.Version == "" {
ref += ":latest"
}
named, err := reference.ParseNormalizedNamed(ref)
if err != nil {
return "", nil, errors.Wrapf(err, "invalid image url %q", purl)
}
return named.String(), platform, nil
}
|