diff --git a/client/client_test.go b/client/client_test.go index 81f6186d898f..8e99247cfd9c 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -7402,7 +7402,7 @@ func testExportAttestations(t *testing.T, sb integration.Sandbox) { Type: ExporterLocal, OutputDir: dir, Attrs: map[string]string{ - "attestation-prefix": "test.", + "attestations-prefix": "test.", }, }, }, @@ -7454,7 +7454,7 @@ func testExportAttestations(t *testing.T, sb integration.Sandbox) { Type: ExporterTar, Output: fixedWriteCloser(outW), Attrs: map[string]string{ - "attestation-prefix": "test.", + "attestations-prefix": "test.", }, }, }, @@ -7469,8 +7469,9 @@ func testExportAttestations(t *testing.T, sb integration.Sandbox) { for _, p := range ps { var attest intoto.Statement - dt := m[path.Join(strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation.json")].Data - require.NoError(t, json.Unmarshal(dt, &attest)) + item := m[path.Join(strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation.json")] + require.NotNil(t, item) + require.NoError(t, json.Unmarshal(item.Data, &attest)) require.Equal(t, "https://in-toto.io/Statement/v0.1", attest.Type) require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) @@ -7482,8 +7483,9 @@ func testExportAttestations(t *testing.T, sb integration.Sandbox) { }}, attest.Subject) var attest2 intoto.Statement - dt = m[path.Join(strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation2.json")].Data - require.NoError(t, json.Unmarshal(dt, &attest2)) + item = m[path.Join(strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation2.json")] + require.NotNil(t, item) + require.NoError(t, json.Unmarshal(item.Data, &attest2)) require.Equal(t, "https://in-toto.io/Statement/v0.1", attest2.Type) require.Equal(t, "https://example.com/attestations2/v1.0", attest2.PredicateType) diff --git a/exporter/attestation/filter.go b/exporter/attestation/filter.go index 5abc234b875e..0ecc8416735b 100644 --- a/exporter/attestation/filter.go +++ b/exporter/attestation/filter.go @@ -1,45 +1,49 @@ package attestation import ( - "bytes" + "strconv" "github.com/moby/buildkit/exporter" + "github.com/moby/buildkit/solver/result" ) -func Filter(attestations []exporter.Attestation, include map[string][]byte, exclude map[string][]byte) []exporter.Attestation { - if len(include) == 0 && len(exclude) == 0 { - return attestations - } - - result := []exporter.Attestation{} +func FilterInline(attestations []exporter.Attestation) (matching []exporter.Attestation, nonMatching []exporter.Attestation) { for _, att := range attestations { - meta := att.Metadata - if meta == nil { - meta = map[string][]byte{} - } - - match := true - for k, v := range include { - if !bytes.Equal(meta[k], v) { - match = false - break + v, ok := att.Metadata[result.AttestationInlineOnlyKey] + if ok { + b, err := strconv.ParseBool(string(v)) + if b && err == nil { + matching = append(matching, att) + continue } } - if !match { - continue - } + nonMatching = append(nonMatching, att) + } + return matching, nonMatching +} - for k, v := range exclude { - if bytes.Equal(meta[k], v) { - match = false - break +func FilterReasons(attestations []exporter.Attestation, reasons []string) (matching []exporter.Attestation, nonMatching []exporter.Attestation) { + if reasons == nil { + // don't filter if no filter provided + return attestations, nil + } + + for _, att := range attestations { + target, ok := att.Metadata[result.AttestationReasonKey] + if ok { + matched := false + for _, reason := range reasons { + if string(target) == reason { + matched = true + break + } + } + if matched { + matching = append(matching, att) + continue } } - if !match { - continue - } - - result = append(result, att) + nonMatching = append(nonMatching, att) } - return result + return matching, nonMatching } diff --git a/exporter/containerimage/export.go b/exporter/containerimage/export.go index 9c5262377d3f..6762460e4633 100644 --- a/exporter/containerimage/export.go +++ b/exporter/containerimage/export.go @@ -78,7 +78,8 @@ func (e *imageExporter) Resolve(ctx context.Context, opt map[string]string) (exp RefCfg: cacheconfig.RefConfig{ Compression: compression.New(compression.Default), }, - BuildInfo: true, + BuildInfo: true, + Attestations: true, }, store: true, } diff --git a/exporter/containerimage/opts.go b/exporter/containerimage/opts.go index 057bd299e4f2..6d975bdb46f0 100644 --- a/exporter/containerimage/opts.go +++ b/exporter/containerimage/opts.go @@ -2,6 +2,7 @@ package containerimage import ( "strconv" + "strings" "time" cacheconfig "github.com/moby/buildkit/cache/config" @@ -19,6 +20,7 @@ const ( keyOCITypes = "oci-mediatypes" keyBuildInfo = "buildinfo" keyBuildInfoAttrs = "buildinfo-attrs" + keyAttestations = "attestations" // preferNondistLayersKey is an exporter option which can be used to mark a layer as non-distributable if the layer reference was // already found to use a non-distributable media type. @@ -27,13 +29,15 @@ const ( ) type ImageCommitOpts struct { - ImageName string - RefCfg cacheconfig.RefConfig - OCITypes bool - BuildInfo bool - BuildInfoAttrs bool - Annotations AnnotationsGroup - Epoch *time.Time + ImageName string + RefCfg cacheconfig.RefConfig + OCITypes bool + BuildInfo bool + BuildInfoAttrs bool + Annotations AnnotationsGroup + Epoch *time.Time + Attestations bool + AttestationsFilter []string } func (c *ImageCommitOpts) Load(opt map[string]string) (map[string]string, error) { @@ -73,6 +77,11 @@ func (c *ImageCommitOpts) Load(opt map[string]string) (map[string]string, error) err = parseBoolWithDefault(&c.BuildInfo, k, v, true) case keyBuildInfoAttrs: err = parseBoolWithDefault(&c.BuildInfoAttrs, k, v, false) + case keyAttestations: + if parseBool(&c.Attestations, k, v) != nil { + c.Attestations = true + c.AttestationsFilter = strings.Split(v, ",") + } case keyPreferNondistLayers: err = parseBool(&c.RefCfg.PreferNonDistributable, k, v) default: diff --git a/exporter/containerimage/writer.go b/exporter/containerimage/writer.go index c9b5d48b804e..f2d40cd9c305 100644 --- a/exporter/containerimage/writer.go +++ b/exporter/containerimage/writer.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "strconv" "strings" "time" @@ -69,19 +68,19 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, session return nil, err } - requiredAttestations := false + hasAttestations := false for _, p := range ps.Platforms { if atts, ok := inp.Attestations[p.ID]; ok { - atts = attestation.Filter(atts, nil, map[string][]byte{ - result.AttestationInlineOnlyKey: []byte(strconv.FormatBool(true)), - }) + _, atts = attestation.FilterInline(atts) + atts, _ = attestation.FilterReasons(atts, opts.AttestationsFilter) if len(atts) > 0 { - requiredAttestations = true + hasAttestations = true break } } } - if requiredAttestations { + hasAttestations = opts.Attestations && hasAttestations + if hasAttestations { isMap = true } @@ -108,7 +107,7 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, session if len(ps.Platforms) > 1 { return nil, errors.Errorf("cannot export multiple platforms without multi-platform enabled") } - if requiredAttestations { + if hasAttestations { return nil, errors.Errorf("cannot export attestations without multi-platform enabled") } @@ -159,7 +158,7 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, session return mfstDesc, nil } - if len(inp.Attestations) > 0 { + if hasAttestations { opts.EnableOCITypes("attestations") } @@ -238,6 +237,7 @@ 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 { + attestations, _ = attestation.FilterReasons(attestations, opts.AttestationsFilter) attestations, err := attestation.Unbundle(ctx, session.NewGroup(sessionID), attestations) if err != nil { return nil, err diff --git a/exporter/local/export.go b/exporter/local/export.go index 7d08b172e019..b70007546605 100644 --- a/exporter/local/export.go +++ b/exporter/local/export.go @@ -20,10 +20,6 @@ import ( "golang.org/x/time/rate" ) -const ( - keyAttestationPrefix = "attestation-prefix" -) - type Opt struct { SessionManager *session.Manager } @@ -39,24 +35,17 @@ func New(opt Opt) (exporter.Exporter, error) { } func (e *localExporter) Resolve(ctx context.Context, opt map[string]string) (exporter.ExporterInstance, error) { - tm, _, err := epoch.ParseExporterAttrs(opt) - if err != nil { - return nil, err - } - i := &localExporterInstance{ localExporter: e, opts: CreateFSOpts{ - Epoch: tm, + Attestations: true, }, } - - for k, v := range opt { - switch k { - case keyAttestationPrefix: - i.opts.AttestationPrefix = v - } + opt, err := i.opts.Load(opt) + if err != nil { + return nil, err } + _ = opt return i, nil } diff --git a/exporter/local/fs.go b/exporter/local/fs.go index c5a524aae32f..e9cbe38fe24a 100644 --- a/exporter/local/fs.go +++ b/exporter/local/fs.go @@ -8,6 +8,7 @@ import ( "os" "path" "strconv" + "strings" "time" "github.com/docker/docker/pkg/idtools" @@ -15,6 +16,7 @@ import ( "github.com/moby/buildkit/cache" "github.com/moby/buildkit/exporter" "github.com/moby/buildkit/exporter/attestation" + "github.com/moby/buildkit/exporter/util/epoch" "github.com/moby/buildkit/session" "github.com/moby/buildkit/snapshot" "github.com/moby/buildkit/solver/result" @@ -25,9 +27,45 @@ import ( fstypes "github.com/tonistiigi/fsutil/types" ) +const ( + keyAttestations = "attestations" + keyAttestationsPrefix = "attestations-prefix" +) + type CreateFSOpts struct { - Epoch *time.Time - AttestationPrefix string + Epoch *time.Time + Attestations bool + AttestationsFilter []string + AttestationPrefix string +} + +func (c *CreateFSOpts) Load(opt map[string]string) (map[string]string, error) { + rest := make(map[string]string) + + var err error + c.Epoch, opt, err = epoch.ParseExporterAttrs(opt) + if err != nil { + return nil, err + } + + for k, v := range opt { + switch k { + case keyAttestations: + b, err := strconv.ParseBool(v) + if err == nil { + c.Attestations = b + } else { + c.Attestations = true + c.AttestationsFilter = strings.Split(v, ",") + } + case keyAttestationsPrefix: + c.AttestationPrefix = v + default: + rest[k] = v + } + } + + return rest, nil } func CreateFS(ctx context.Context, sessionID string, k string, ref cache.ImmutableRef, attestations []exporter.Attestation, defaultTime time.Time, opt CreateFSOpts) (fsutil.FS, func() error, error) { @@ -89,14 +127,14 @@ func CreateFS(ctx context.Context, sessionID string, k string, ref cache.Immutab } outputFS := fsutil.NewFS(src, walkOpt) - attestations = attestation.Filter(attestations, nil, map[string][]byte{ - result.AttestationInlineOnlyKey: []byte(strconv.FormatBool(true)), - }) - attestations, err = attestation.Unbundle(ctx, session.NewGroup(sessionID), attestations) - if err != nil { - return nil, nil, err - } - if len(attestations) > 0 { + _, attestations = attestation.FilterInline(attestations) + attestations, _ = attestation.FilterReasons(attestations, opt.AttestationsFilter) + if opt.Attestations && len(attestations) > 0 { + attestations, err = attestation.Unbundle(ctx, session.NewGroup(sessionID), attestations) + if err != nil { + return nil, nil, err + } + subjects := []intoto.Subject{} err = outputFS.Walk(ctx, func(path string, info fs.FileInfo, err error) error { if err != nil { diff --git a/exporter/oci/export.go b/exporter/oci/export.go index 60982f4daf3c..f9ed39bbb436 100644 --- a/exporter/oci/export.go +++ b/exporter/oci/export.go @@ -67,8 +67,9 @@ func (e *imageExporter) Resolve(ctx context.Context, opt map[string]string) (exp RefCfg: cacheconfig.RefConfig{ Compression: compression.New(compression.Default), }, - BuildInfo: true, - OCITypes: e.opt.Variant == VariantOCI, + BuildInfo: true, + OCITypes: e.opt.Variant == VariantOCI, + Attestations: true, }, } diff --git a/exporter/tar/export.go b/exporter/tar/export.go index 4d136c89c1ca..758348f8be0d 100644 --- a/exporter/tar/export.go +++ b/exporter/tar/export.go @@ -3,7 +3,6 @@ package local import ( "context" "os" - "strconv" "strings" "time" @@ -20,15 +19,6 @@ import ( fstypes "github.com/tonistiigi/fsutil/types" ) -const ( - attestationPrefixKey = "attestation-prefix" - - // preferNondistLayersKey is an exporter option which can be used to mark a layer as non-distributable if the layer reference was - // already found to use a non-distributable media type. - // When this option is not set, the exporter will change the media type of the layer to a distributable one. - preferNondistLayersKey = "prefer-nondist-layers" -) - type Opt struct { SessionManager *session.Manager } @@ -44,34 +34,24 @@ func New(opt Opt) (exporter.Exporter, error) { } func (e *localExporter) Resolve(ctx context.Context, opt map[string]string) (exporter.ExporterInstance, error) { - li := &localExporterInstance{localExporter: e} - - tm, opt, err := epoch.ParseExporterAttrs(opt) + li := &localExporterInstance{ + localExporter: e, + opts: local.CreateFSOpts{ + Attestations: true, + }, + } + opt, err := li.opts.Load(opt) if err != nil { return nil, err } - li.opts.Epoch = tm - - for k, v := range opt { - switch k { - case preferNondistLayersKey: - b, err := strconv.ParseBool(v) - if err != nil { - return nil, errors.Wrapf(err, "non-bool value for %s: %s", preferNondistLayersKey, v) - } - li.preferNonDist = b - case attestationPrefixKey: - li.opts.AttestationPrefix = v - } - } + _ = opt return li, nil } type localExporterInstance struct { *localExporter - opts local.CreateFSOpts - preferNonDist bool + opts local.CreateFSOpts } func (e *localExporterInstance) Name() string {