diff --git a/client/client_test.go b/client/client_test.go index 4fed74ad92cd..e98014f96abe 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -50,6 +50,7 @@ import ( binfotypes "github.com/moby/buildkit/util/buildinfo/types" "github.com/moby/buildkit/util/contentutil" "github.com/moby/buildkit/util/entitlements" + "github.com/moby/buildkit/util/purl" "github.com/moby/buildkit/util/testutil" "github.com/moby/buildkit/util/testutil/echoserver" "github.com/moby/buildkit/util/testutil/httpserver" @@ -6527,13 +6528,16 @@ func testExportAttestations(t *testing.T, sb integration.Sandbox) { return res, nil } - target := registry + "/buildkit/testattestations:latest" + targets := []string{ + registry + "/buildkit/testattestationsfoo:latest", + registry + "/buildkit/testattestationsbar:latest", + } _, err = c.Build(sb.Context(), SolveOpt{ Exports: []ExportEntry{ { Type: ExporterImage, Attrs: map[string]string{ - "name": target, + "name": strings.Join(targets, ","), "push": "true", }, }, @@ -6541,7 +6545,7 @@ func testExportAttestations(t *testing.T, sb integration.Sandbox) { }, "", frontend, nil) require.NoError(t, err) - desc, provider, err := contentutil.ProviderFromRef(target) + desc, provider, err := contentutil.ProviderFromRef(targets[0]) require.NoError(t, err) imgs, err := testutil.ReadImages(sb.Context(), provider, desc) @@ -6574,15 +6578,29 @@ func testExportAttestations(t *testing.T, sb integration.Sandbox) { var attest intoto.Statement require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + purls := map[string]string{} + for _, k := range targets { + p, _ := purl.RefToPURL(k, &ps[i]) + purls[k] = p + } + require.Equal(t, "https://in-toto.io/Statement/v0.1", attest.Type) require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) require.Equal(t, map[string]interface{}{"success": true}, attest.Predicate) - subjects := []intoto.Subject{{ - Name: "_", - Digest: map[string]string{ - "sha256": bases[i].Desc.Digest.Encoded(), + subjects := []intoto.Subject{ + { + Name: purls[targets[0]], + Digest: map[string]string{ + "sha256": bases[i].Desc.Digest.Encoded(), + }, }, - }} + { + Name: purls[targets[1]], + Digest: map[string]string{ + "sha256": bases[i].Desc.Digest.Encoded(), + }, + }, + } require.Equal(t, subjects, attest.Subject) var attest2 intoto.Statement @@ -6609,9 +6627,10 @@ func testExportAttestations(t *testing.T, sb integration.Sandbox) { defer client.Close() ctx := namespaces.WithNamespace(sb.Context(), "buildkit") - err = client.ImageService().Delete(ctx, target, images.SynchronousDelete()) - require.NoError(t, err) - + for _, target := range targets { + err = client.ImageService().Delete(ctx, target, images.SynchronousDelete()) + require.NoError(t, err) + } checkAllReleasable(t, c, sb, true) } @@ -6737,8 +6756,11 @@ func testAttestationDefaultSubject(t *testing.T, sb integration.Sandbox) { require.Equal(t, "https://in-toto.io/Statement/v0.1", attest.Type) require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) require.Equal(t, map[string]interface{}{"success": true}, attest.Predicate) + + name, _ := purl.RefToPURL(target, &ps[0]) + subjects := []intoto.Subject{{ - Name: "_", + Name: name, Digest: map[string]string{ "sha256": bases[i].Desc.Digest.Encoded(), }, diff --git a/exporter/containerimage/writer.go b/exporter/containerimage/writer.go index a6520b544652..734d1683b183 100644 --- a/exporter/containerimage/writer.go +++ b/exporter/containerimage/writer.go @@ -30,6 +30,7 @@ import ( binfotypes "github.com/moby/buildkit/util/buildinfo/types" "github.com/moby/buildkit/util/compression" "github.com/moby/buildkit/util/progress" + "github.com/moby/buildkit/util/purl" "github.com/moby/buildkit/util/system" "github.com/moby/buildkit/util/tracing" digest "github.com/opencontainers/go-digest" @@ -85,7 +86,7 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, session } } - mfstDesc, configDesc, err := ic.commitDistributionManifest(ctx, inp.Ref, inp.Metadata[exptypes.ExporterImageConfigKey], &remotes[0], opts.Annotations.Platform(nil), opts.OCITypes, inp.Metadata[exptypes.ExporterInlineCache], dtbi) + mfstDesc, configDesc, err := ic.commitDistributionManifest(ctx, opts, inp.Ref, inp.Metadata[exptypes.ExporterImageConfigKey], &remotes[0], opts.Annotations.Platform(nil), inp.Metadata[exptypes.ExporterInlineCache], dtbi) if err != nil { return nil, err } @@ -165,7 +166,7 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, session } } - desc, _, err := ic.commitDistributionManifest(ctx, r, config, &remotes[remotesMap[p.ID]], opts.Annotations.Platform(&p.Platform), opts.OCITypes, inlineCache, dtbi) + desc, _, err := ic.commitDistributionManifest(ctx, opts, r, config, &remotes[remotesMap[p.ID]], opts.Annotations.Platform(&p.Platform), inlineCache, dtbi) if err != nil { return nil, err } @@ -176,12 +177,12 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, session labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = desc.Digest.String() if attestations, ok := inp.Attestations[p.ID]; ok { - inTotos, err := ic.extractAttestations(ctx, session.NewGroup(sessionID), desc, inp.Refs, attestations) + inTotos, err := ic.extractAttestations(ctx, opts, session.NewGroup(sessionID), desc, inp.Refs, attestations) if err != nil { return nil, err } - desc, err := ic.commitAttestationsManifest(ctx, p, desc.Digest.String(), opts.OCITypes, inTotos) + desc, err := ic.commitAttestationsManifest(ctx, opts, p, desc.Digest.String(), inTotos) if err != nil { return nil, err } @@ -254,7 +255,7 @@ func (ic *ImageWriter) exportLayers(ctx context.Context, refCfg cacheconfig.RefC return out, err } -func (ic *ImageWriter) extractAttestations(ctx context.Context, s session.Group, desc *ocispecs.Descriptor, refs map[string]cache.ImmutableRef, attestations []result.Attestation) ([]intoto.Statement, error) { +func (ic *ImageWriter) extractAttestations(ctx context.Context, opts *ImageCommitOpts, s session.Group, desc *ocispecs.Descriptor, refs map[string]cache.ImmutableRef, attestations []result.Attestation) ([]intoto.Statement, error) { eg, ctx := errgroup.WithContext(ctx) statements := make([]intoto.Statement, len(attestations)) @@ -290,13 +291,6 @@ func (ic *ImageWriter) extractAttestations(ctx context.Context, s session.Group, if len(predicate) == 0 { predicate = nil } - statements[i] = intoto.Statement{ - StatementHeader: intoto.StatementHeader{ - Type: intoto.StatementInTotoV01, - PredicateType: att.InToto.PredicateType, - }, - Predicate: json.RawMessage(predicate), - } if len(att.InToto.Subjects) == 0 { att.InToto.Subjects = []result.InTotoSubject{{ @@ -304,21 +298,50 @@ func (ic *ImageWriter) extractAttestations(ctx context.Context, s session.Group, }} } - statements[i].Subject = make([]intoto.Subject, len(att.InToto.Subjects)) - for j, subject := range att.InToto.Subjects { - statements[i].Subject[j].Name = "_" + subjects := make([]intoto.Subject, 0, len(att.InToto.Subjects)) + for _, subject := range att.InToto.Subjects { + name := "_" if subject.Name != "" { - statements[i].Subject[j].Name = subject.Name + name = subject.Name } switch subject.Kind { case gatewaypb.InTotoSubjectKindSelf: - statements[i].Subject[j].Digest = result.DigestMap(desc.Digest) + var names []string + if opts.ImageName != "" { + for _, name := range strings.Split(opts.ImageName, ",") { + name, err := purl.RefToPURL(name, desc.Platform) + if err != nil { + return err + } + names = append(names, name) + } + } else { + names = []string{name} + } + for _, name := range names { + subjects = append(subjects, intoto.Subject{ + Name: name, + Digest: result.DigestMap(desc.Digest), + }) + } case gatewaypb.InTotoSubjectKindRaw: - statements[i].Subject[j].Digest = result.DigestMap(subject.Digest...) + subjects = append(subjects, intoto.Subject{ + Name: name, + Digest: result.DigestMap(subject.Digest...), + }) + default: return errors.Errorf("unknown attestation subject kind %q", subject.Kind) } } + statements[i] = intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + Type: intoto.StatementInTotoV01, + PredicateType: att.InToto.PredicateType, + Subject: subjects, + }, + Predicate: json.RawMessage(predicate), + } } return nil }) @@ -330,7 +353,7 @@ func (ic *ImageWriter) extractAttestations(ctx context.Context, s session.Group, return statements, nil } -func (ic *ImageWriter) commitDistributionManifest(ctx context.Context, ref cache.ImmutableRef, config []byte, remote *solver.Remote, annotations *Annotations, oci bool, inlineCache []byte, buildInfo []byte) (*ocispecs.Descriptor, *ocispecs.Descriptor, error) { +func (ic *ImageWriter) commitDistributionManifest(ctx context.Context, opts *ImageCommitOpts, ref cache.ImmutableRef, config []byte, remote *solver.Remote, annotations *Annotations, inlineCache []byte, buildInfo []byte) (*ocispecs.Descriptor, *ocispecs.Descriptor, error) { if len(config) == 0 { var err error config, err = defaultImageConfig() @@ -350,7 +373,7 @@ func (ic *ImageWriter) commitDistributionManifest(ctx context.Context, ref cache return nil, nil, err } - remote, history = normalizeLayersAndHistory(ctx, remote, history, ref, oci) + remote, history = normalizeLayersAndHistory(ctx, remote, history, ref, opts.OCITypes) config, err = patchImageConfig(config, remote.Descriptors, history, inlineCache, buildInfo) if err != nil { @@ -364,7 +387,7 @@ func (ic *ImageWriter) commitDistributionManifest(ctx context.Context, ref cache ) // Use docker media types for older Docker versions and registries - if !oci { + if !opts.OCITypes { manifestType = images.MediaTypeDockerSchema2Manifest configType = images.MediaTypeDockerSchema2Config } @@ -395,7 +418,7 @@ func (ic *ImageWriter) commitDistributionManifest(ctx context.Context, ref cache } for i, desc := range remote.Descriptors { - removeInternalLayerAnnotations(&desc, oci) + removeInternalLayerAnnotations(&desc, opts.OCITypes) mfst.Layers = append(mfst.Layers, desc) labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = desc.Digest.String() } @@ -437,12 +460,12 @@ func (ic *ImageWriter) commitDistributionManifest(ctx context.Context, ref cache }, &configDesc, nil } -func (ic *ImageWriter) commitAttestationsManifest(ctx context.Context, p exptypes.Platform, target string, oci bool, statements []intoto.Statement) (*ocispecs.Descriptor, error) { +func (ic *ImageWriter) commitAttestationsManifest(ctx context.Context, opts *ImageCommitOpts, p exptypes.Platform, target string, statements []intoto.Statement) (*ocispecs.Descriptor, error) { var ( manifestType = ocispecs.MediaTypeImageManifest configType = ocispecs.MediaTypeImageConfig ) - if !oci { + if !opts.OCITypes { manifestType = images.MediaTypeDockerSchema2Manifest configType = images.MediaTypeDockerSchema2Config } @@ -507,7 +530,7 @@ func (ic *ImageWriter) commitAttestationsManifest(ctx context.Context, p exptype "containerd.io/gc.ref.content.0": configDigest.String(), } for i, desc := range layers { - removeInternalLayerAnnotations(&desc, oci) + removeInternalLayerAnnotations(&desc, opts.OCITypes) mfst.Layers = append(mfst.Layers, desc) labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = desc.Digest.String() } diff --git a/go.mod b/go.mod index dc05fb681bba..d1cf36f59b26 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( github.com/opencontainers/runc v1.1.3 github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 github.com/opencontainers/selinux v1.10.1 + github.com/package-url/packageurl-go v0.1.0 github.com/pelletier/go-toml v1.9.4 github.com/pkg/errors v0.9.1 github.com/pkg/profile v1.5.0 diff --git a/go.sum b/go.sum index d96d61ab9937..bdfeda97c874 100644 --- a/go.sum +++ b/go.sum @@ -1123,6 +1123,8 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/package-url/packageurl-go v0.1.0 h1:efWBc98O/dBZRg1pw2xiDzovnlMjCa9NPnfaiBduh8I= +github.com/package-url/packageurl-go v0.1.0/go.mod h1:C/ApiuWpmbpni4DIOECf6WCjFUZV7O1Fx7VAzrZHgBw= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= diff --git a/util/purl/image.go b/util/purl/image.go new file mode 100644 index 000000000000..b3364ba4cecb --- /dev/null +++ b/util/purl/image.go @@ -0,0 +1,117 @@ +package purl + +import ( + "strings" + + "github.com/containerd/containerd/platforms" + "github.com/docker/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(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("docker", 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 + } + 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 +} diff --git a/util/purl/image_test.go b/util/purl/image_test.go new file mode 100644 index 000000000000..621de37bff16 --- /dev/null +++ b/util/purl/image_test.go @@ -0,0 +1,158 @@ +package purl + +import ( + "net/url" + "testing" + + "github.com/containerd/containerd/platforms" + digest "github.com/opencontainers/go-digest" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/require" +) + +func TestRefToPURL(t *testing.T) { + testDgst := digest.FromBytes([]byte("test")).String() + p := platforms.DefaultSpec() + testPlatform := &p + + expPlatform := url.QueryEscape(platforms.Format(platforms.Normalize(p))) + + tcases := []struct { + ref string + platform *ocispecs.Platform + expected string + err bool + }{ + { + ref: "alpine", + expected: "pkg:docker/alpine@latest", + }, + { + ref: "library/alpine:3.15", + expected: "pkg:docker/alpine@3.15", + }, + { + ref: "docker.io/library/alpine:latest", + expected: "pkg:docker/alpine@latest", + }, + { + ref: "docker.io/library/alpine:latest@" + testDgst, + expected: "pkg:docker/alpine@latest?digest=" + testDgst, + }, + { + ref: "docker.io/library/alpine@" + testDgst, + expected: "pkg:docker/alpine?digest=" + testDgst, + }, + { + ref: "user/test:v2", + expected: "pkg:docker/user/test@v2", + }, + { + ref: "ghcr.io/foo/bar:v2", + expected: "pkg:docker/ghcr.io/foo/bar@v2", + }, + { + ref: "ghcr.io/foo/bar", + expected: "pkg:docker/ghcr.io/foo/bar@latest", + }, + { + ref: "busybox", + platform: testPlatform, + expected: "pkg:docker/busybox@latest?platform=" + expPlatform, + }, + { + ref: "busybox@" + testDgst, + platform: testPlatform, + expected: "pkg:docker/busybox?digest=" + testDgst + "&platform=" + expPlatform, + }, + { + ref: "inv:al:id", + err: true, + }, + } + + for _, tc := range tcases { + tc := tc + t.Run(tc.ref, func(t *testing.T) { + purl, err := RefToPURL(tc.ref, tc.platform) + if tc.err { + require.Error(t, err) + return + } + if err != nil { + require.NoError(t, err) + } + require.Equal(t, tc.expected, purl) + }) + } +} + +func TestPURLToRef(t *testing.T) { + testDgst := digest.FromBytes([]byte("test")).String() + p := platforms.Normalize(platforms.DefaultSpec()) + p.OSVersion = "" // OSVersion is not supported in PURL + testPlatform := &p + + encPlatform := url.QueryEscape(platforms.Format(platforms.Normalize(p))) + + tcases := []struct { + purl string + err bool + expected string + platform *ocispecs.Platform + }{ + { + purl: "pkg:docker/alpine@latest", + expected: "docker.io/library/alpine:latest", + }, + { + purl: "pkg:docker/alpine", + expected: "docker.io/library/alpine:latest", + }, + { + purl: "pkg:docker/alpine?digest=" + testDgst, + expected: "docker.io/library/alpine@" + testDgst, + }, + { + purl: "pkg:docker/library/alpine@3.15?digest=" + testDgst, + expected: "docker.io/library/alpine:3.15@" + testDgst, + }, + { + purl: "pkg:docker/ghcr.io/foo/bar@v2", + expected: "ghcr.io/foo/bar:v2", + }, + { + purl: "pkg:docker/ghcr.io/foo/bar@v2", + expected: "ghcr.io/foo/bar:v2", + }, + { + purl: "pkg:docker/busybox@latest?platform=" + encPlatform, + expected: "docker.io/library/busybox:latest", + platform: testPlatform, + }, + { + purl: "pkg:busybox@latest", + err: true, + }, + } + + for _, tc := range tcases { + tc := tc + t.Run(tc.purl, func(t *testing.T) { + ref, platform, err := PURLToRef(tc.purl) + if tc.err { + require.Error(t, err) + return + } + if err != nil { + require.NoError(t, err) + } + require.Equal(t, tc.expected, ref) + if platform == nil { + require.Nil(t, tc.platform) + } else { + require.Equal(t, *tc.platform, *platform) + } + }) + } +} diff --git a/vendor/github.com/package-url/packageurl-go/.gitignore b/vendor/github.com/package-url/packageurl-go/.gitignore new file mode 100644 index 000000000000..d373807a9374 --- /dev/null +++ b/vendor/github.com/package-url/packageurl-go/.gitignore @@ -0,0 +1,16 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +testdata/*json + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ diff --git a/vendor/github.com/package-url/packageurl-go/.travis.yml b/vendor/github.com/package-url/packageurl-go/.travis.yml new file mode 100644 index 000000000000..1bb07d03a3af --- /dev/null +++ b/vendor/github.com/package-url/packageurl-go/.travis.yml @@ -0,0 +1,19 @@ +language: go + +go: + - 1.12 + - tip + +install: true + +matrix: + allow_failures: + - go: tip + fast_finish: true + +script: + - make lint + - make test + +notifications: + email: false diff --git a/vendor/github.com/package-url/packageurl-go/Makefile b/vendor/github.com/package-url/packageurl-go/Makefile new file mode 100644 index 000000000000..f6e71425f759 --- /dev/null +++ b/vendor/github.com/package-url/packageurl-go/Makefile @@ -0,0 +1,12 @@ +.PHONY: test clean lint + +test: + curl -L https://raw.githubusercontent.com/package-url/purl-spec/master/test-suite-data.json -o testdata/test-suite-data.json + go test -v -cover ./... + +clean: + find . -name "test-suite-data.json" | xargs rm -f + +lint: + go get -u golang.org/x/lint/golint + golint -set_exit_status diff --git a/vendor/github.com/package-url/packageurl-go/README.md b/vendor/github.com/package-url/packageurl-go/README.md new file mode 100644 index 000000000000..68b42ac18e07 --- /dev/null +++ b/vendor/github.com/package-url/packageurl-go/README.md @@ -0,0 +1,74 @@ +# packageurl-go + +Go implementation of the package url spec + +[![Build Status](https://travis-ci.com/package-url/packageurl-go.svg)](https://travis-ci.com/package-url/packageurl-go) + + +## Install +``` +go get -u github.com/package-url/packageurl-go +``` + +## Versioning + +The versions will follow the spec. So if the spec is released at ``1.0``. Then all versions in the ``1.x.y`` will follow the ``1.x`` spec. + + +## Usage + +### Create from parts +```go +package main + +import ( + "fmt" + + "github.com/package-url/packageurl-go" +) + +func main() { + instance := packageurl.NewPackageURL("test", "ok", "name", "version", nil, "") + fmt.Printf("%s", instance.ToString()) +} +``` + +### Parse from string +```go +package main + +import ( + "fmt" + + "github.com/package-url/packageurl-go" +) + +func main() { + instance, err := packageurl.FromString("test:ok/name@version") + if err != nil { + panic(err) + } + fmt.Printf("%#v", instance) +} + +``` + + +## Test +Testing using the normal ``go test`` command. Using ``make test`` will pull down the test fixtures shared between all package-url projects and then execute the tests. + +``` +$ make test +curl -L https://raw.githubusercontent.com/package-url/purl-test-suite/master/test-suite-data.json -o testdata/test-suite-data.json + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 7181 100 7181 0 0 1202 0 0:00:05 0:00:05 --:--:-- 1611 +go test -v -cover ./... +=== RUN TestFromStringExamples +--- PASS: TestFromStringExamples (0.00s) +=== RUN TestToStringExamples +--- PASS: TestToStringExamples (0.00s) +PASS +coverage: 94.7% of statements +ok github.com/package-url/packageurl-go 0.002s +``` diff --git a/vendor/github.com/package-url/packageurl-go/VERSION b/vendor/github.com/package-url/packageurl-go/VERSION new file mode 100644 index 000000000000..77d6f4ca2371 --- /dev/null +++ b/vendor/github.com/package-url/packageurl-go/VERSION @@ -0,0 +1 @@ +0.0.0 diff --git a/vendor/github.com/package-url/packageurl-go/mit.LICENSE b/vendor/github.com/package-url/packageurl-go/mit.LICENSE new file mode 100644 index 000000000000..0b5633b5de5b --- /dev/null +++ b/vendor/github.com/package-url/packageurl-go/mit.LICENSE @@ -0,0 +1,18 @@ +Copyright (c) the purl authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/package-url/packageurl-go/packageurl.go b/vendor/github.com/package-url/packageurl-go/packageurl.go new file mode 100644 index 000000000000..b521429f58d2 --- /dev/null +++ b/vendor/github.com/package-url/packageurl-go/packageurl.go @@ -0,0 +1,336 @@ +/* +Copyright (c) the purl authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Package packageurl implements the package-url spec +package packageurl + +import ( + "errors" + "fmt" + "net/url" + "regexp" + "sort" + "strings" +) + +var ( + // QualifierKeyPattern describes a valid qualifier key: + // + // - The key must be composed only of ASCII letters and numbers, '.', + // '-' and '_' (period, dash and underscore). + // - A key cannot start with a number. + QualifierKeyPattern = regexp.MustCompile(`^[A-Za-z\.\-_][0-9A-Za-z\.\-_]*$`) +) + +// These are the known purl types as defined in the spec. Some of these require +// special treatment during parsing. +// https://github.com/package-url/purl-spec#known-purl-types +var ( + // TypeBitbucket is a pkg:bitbucket purl. + TypeBitbucket = "bitbucket" + // TypeComposer is a pkg:composer purl. + TypeComposer = "composer" + // TypeDebian is a pkg:deb purl. + TypeDebian = "debian" + // TypeDocker is a pkg:docker purl. + TypeDocker = "docker" + // TypeGem is a pkg:gem purl. + TypeGem = "gem" + // TypeGeneric is a pkg:generic purl. + TypeGeneric = "generic" + // TypeGithub is a pkg:github purl. + TypeGithub = "github" + // TypeGolang is a pkg:golang purl. + TypeGolang = "golang" + // TypeMaven is a pkg:maven purl. + TypeMaven = "maven" + // TypeNPM is a pkg:npm purl. + TypeNPM = "npm" + // TypeNuget is a pkg:nuget purl. + TypeNuget = "nuget" + // TypePyPi is a pkg:pypi purl. + TypePyPi = "pypi" + // TypeRPM is a pkg:rpm purl. + TypeRPM = "rpm" +) + +// Qualifier represents a single key=value qualifier in the package url +type Qualifier struct { + Key string + Value string +} + +func (q Qualifier) String() string { + // A value must be must be a percent-encoded string + return fmt.Sprintf("%s=%s", q.Key, url.PathEscape(q.Value)) +} + +// Qualifiers is a slice of key=value pairs, with order preserved as it appears +// in the package URL. +type Qualifiers []Qualifier + +// QualifiersFromMap constructs a Qualifiers slice from a string map. To get a +// deterministic qualifier order (despite maps not providing any iteration order +// guarantees) the returned Qualifiers are sorted in increasing order of key. +func QualifiersFromMap(mm map[string]string) Qualifiers { + q := Qualifiers{} + + for k, v := range mm { + q = append(q, Qualifier{Key: k, Value: v}) + } + + // sort for deterministic qualifier order + sort.Slice(q, func(i int, j int) bool { return q[i].Key < q[j].Key }) + + return q +} + +// Map converts a Qualifiers struct to a string map. +func (qq Qualifiers) Map() map[string]string { + m := make(map[string]string, 0) + + for i := 0; i < len(qq); i++ { + k := qq[i].Key + v := qq[i].Value + m[k] = v + } + + return m +} + +func (qq Qualifiers) String() string { + var kvPairs []string + for _, q := range qq { + kvPairs = append(kvPairs, q.String()) + } + return strings.Join(kvPairs, "&") +} + +// PackageURL is the struct representation of the parts that make a package url +type PackageURL struct { + Type string + Namespace string + Name string + Version string + Qualifiers Qualifiers + Subpath string +} + +// NewPackageURL creates a new PackageURL struct instance based on input +func NewPackageURL(purlType, namespace, name, version string, + qualifiers Qualifiers, subpath string) *PackageURL { + + return &PackageURL{ + Type: purlType, + Namespace: namespace, + Name: name, + Version: version, + Qualifiers: qualifiers, + Subpath: subpath, + } +} + +// ToString returns the human readable instance of the PackageURL structure. +// This is the literal purl as defined by the spec. +func (p *PackageURL) ToString() string { + // Start with the type and a colon + purl := fmt.Sprintf("pkg:%s/", p.Type) + // Add namespaces if provided + if p.Namespace != "" { + ns := []string{} + for _, item := range strings.Split(p.Namespace, "/") { + ns = append(ns, url.QueryEscape(item)) + } + purl = purl + strings.Join(ns, "/") + "/" + } + // The name is always required and must be a percent-encoded string + purl = purl + url.PathEscape(p.Name) + // If a version is provided, add it after the at symbol + if p.Version != "" { + // A name must be a percent-encoded string + purl = purl + "@" + url.PathEscape(p.Version) + } + + // Iterate over qualifiers and make groups of key=value + var qualifiers []string + for _, q := range p.Qualifiers { + qualifiers = append(qualifiers, q.String()) + } + // If there one or more key=value pairs then append on the package url + if len(qualifiers) != 0 { + purl = purl + "?" + strings.Join(qualifiers, "&") + } + // Add a subpath if available + if p.Subpath != "" { + purl = purl + "#" + p.Subpath + } + return purl +} + +func (p *PackageURL) String() string { + return p.ToString() +} + +// FromString parses a valid package url string into a PackageURL structure +func FromString(purl string) (PackageURL, error) { + initialIndex := strings.Index(purl, "#") + // Start with purl being stored in the remainder + remainder := purl + substring := "" + if initialIndex != -1 { + initialSplit := strings.SplitN(purl, "#", 2) + remainder = initialSplit[0] + rightSide := initialSplit[1] + rightSide = strings.TrimLeft(rightSide, "/") + rightSide = strings.TrimRight(rightSide, "/") + var rightSides []string + + for _, item := range strings.Split(rightSide, "/") { + item = strings.Replace(item, ".", "", -1) + item = strings.Replace(item, "..", "", -1) + if item != "" { + i, err := url.PathUnescape(item) + if err != nil { + return PackageURL{}, fmt.Errorf("failed to unescape path: %s", err) + } + rightSides = append(rightSides, i) + } + } + substring = strings.Join(rightSides, "/") + } + qualifiers := Qualifiers{} + index := strings.LastIndex(remainder, "?") + // If we don't have anything to split then return an empty result + if index != -1 { + qualifier := remainder[index+1:] + for _, item := range strings.Split(qualifier, "&") { + kv := strings.Split(item, "=") + key := strings.ToLower(kv[0]) + key, err := url.PathUnescape(key) + if err != nil { + return PackageURL{}, fmt.Errorf("failed to unescape qualifier key: %s", err) + } + if !validQualifierKey(key) { + return PackageURL{}, fmt.Errorf("invalid qualifier key: '%s'", key) + } + // TODO + // - If the `key` is `checksums`, split the `value` on ',' to create + // a list of `checksums` + if kv[1] == "" { + continue + } + value, err := url.PathUnescape(kv[1]) + if err != nil { + return PackageURL{}, fmt.Errorf("failed to unescape qualifier value: %s", err) + } + qualifiers = append(qualifiers, Qualifier{key, value}) + } + remainder = remainder[:index] + } + + nextSplit := strings.SplitN(remainder, ":", 2) + if len(nextSplit) != 2 || nextSplit[0] != "pkg" { + return PackageURL{}, errors.New("scheme is missing") + } + // leading slashes after pkg: are to be ignored (pkg://maven is + // equivalent to pkg:maven) + remainder = strings.TrimLeft(nextSplit[1], "/") + + nextSplit = strings.SplitN(remainder, "/", 2) + if len(nextSplit) != 2 { + return PackageURL{}, errors.New("type is missing") + } + // purl type is case-insensitive, canonical form is lower-case + purlType := strings.ToLower(nextSplit[0]) + remainder = nextSplit[1] + + index = strings.LastIndex(remainder, "/") + name := typeAdjustName(purlType, remainder[index+1:]) + version := "" + + atIndex := strings.Index(name, "@") + if atIndex != -1 { + v, err := url.PathUnescape(name[atIndex+1:]) + if err != nil { + return PackageURL{}, fmt.Errorf("failed to unescape purl version: %s", err) + } + version = v + name = name[:atIndex] + } + namespaces := []string{} + + if index != -1 { + remainder = remainder[:index] + + for _, item := range strings.Split(remainder, "/") { + if item != "" { + unescaped, err := url.PathUnescape(item) + if err != nil { + return PackageURL{}, fmt.Errorf("failed to unescape path: %s", err) + } + namespaces = append(namespaces, unescaped) + } + } + } + namespace := strings.Join(namespaces, "/") + namespace = typeAdjustNamespace(purlType, namespace) + + // Fail if name is empty at this point + if name == "" { + return PackageURL{}, errors.New("name is required") + } + + return PackageURL{ + Type: purlType, + Namespace: namespace, + Name: name, + Version: version, + Qualifiers: qualifiers, + Subpath: substring, + }, nil +} + +// Make any purl type-specific adjustments to the parsed namespace. +// See https://github.com/package-url/purl-spec#known-purl-types +func typeAdjustNamespace(purlType, ns string) string { + switch purlType { + case TypeBitbucket, TypeDebian, TypeGithub, TypeGolang, TypeNPM, TypeRPM: + return strings.ToLower(ns) + } + return ns +} + +// Make any purl type-specific adjustments to the parsed name. +// See https://github.com/package-url/purl-spec#known-purl-types +func typeAdjustName(purlType, name string) string { + switch purlType { + case TypeBitbucket, TypeDebian, TypeGithub, TypeGolang, TypeNPM: + return strings.ToLower(name) + case TypePyPi: + return strings.ToLower(strings.ReplaceAll(name, "_", "-")) + } + return name +} + +func validQualifierKey(key string) bool { + return QualifierKeyPattern.MatchString(key) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index debab3d3927a..88e8bfe7aea6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -580,6 +580,9 @@ github.com/opencontainers/selinux/go-selinux github.com/opencontainers/selinux/go-selinux/label github.com/opencontainers/selinux/pkg/pwalk github.com/opencontainers/selinux/pkg/pwalkdir +# github.com/package-url/packageurl-go v0.1.0 +## explicit; go 1.12 +github.com/package-url/packageurl-go # github.com/pelletier/go-toml v1.9.4 ## explicit; go 1.12 github.com/pelletier/go-toml