diff --git a/cmd/compose/build.go b/cmd/compose/build.go index e29a9d89e..fc2e5f6f1 100644 --- a/cmd/compose/build.go +++ b/cmd/compose/build.go @@ -45,7 +45,8 @@ type buildOptions struct { deps bool print bool check bool - provenance bool + sbom string + provenance string } func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) { @@ -84,6 +85,7 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, Check: opts.check, SSHs: SSHKeys, Builder: builderName, + SBOM: opts.sbom, Provenance: opts.provenance, }, nil } @@ -125,6 +127,8 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) flags.StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)") flags.StringVar(&opts.builder, "builder", "", "Set builder to use") flags.BoolVar(&opts.deps, "with-dependencies", false, "Also build dependencies (transitively)") + flags.StringVar(&opts.provenance, "provenance", "", `Add a provenance attestation`) + flags.StringVar(&opts.sbom, "sbom", "", `Add a SBOM attestation`) flags.Bool("parallel", true, "Build images in parallel. DEPRECATED") flags.MarkHidden("parallel") //nolint:errcheck @@ -156,7 +160,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, o } apiBuildOptions, err := opts.toAPIBuildOptions(services) - apiBuildOptions.Provenance = true + apiBuildOptions.Attestations = true if err != nil { return err } diff --git a/docs/reference/compose_build.md b/docs/reference/compose_build.md index 5589a4693..af804aced 100644 --- a/docs/reference/compose_build.md +++ b/docs/reference/compose_build.md @@ -22,9 +22,11 @@ run `docker compose build` to rebuild it. | `-m`, `--memory` | `bytes` | `0` | Set memory limit for the build container. Not supported by BuildKit. | | `--no-cache` | `bool` | | Do not use cache when building the image | | `--print` | `bool` | | Print equivalent bake file | +| `--provenance` | `string` | | Add a provenance attestation | | `--pull` | `bool` | | Always attempt to pull a newer version of the image | | `--push` | `bool` | | Push service images | | `-q`, `--quiet` | `bool` | | Don't print anything to STDOUT | +| `--sbom` | `string` | | Add a SBOM attestation | | `--ssh` | `string` | | Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent) | | `--with-dependencies` | `bool` | | Also build dependencies (transitively) | diff --git a/docs/reference/docker_compose_build.yaml b/docs/reference/docker_compose_build.yaml index 6d1446a51..2efe0d9f6 100644 --- a/docs/reference/docker_compose_build.yaml +++ b/docs/reference/docker_compose_build.yaml @@ -125,6 +125,15 @@ options: experimentalcli: false kubernetes: false swarm: false + - option: provenance + value_type: string + description: Add a provenance attestation + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false - option: pull value_type: bool default_value: "false" @@ -156,6 +165,15 @@ options: experimentalcli: false kubernetes: false swarm: false + - option: sbom + value_type: string + description: Add a SBOM attestation + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false - option: ssh value_type: string description: | diff --git a/go.mod b/go.mod index 5013a28e3..d9972e346 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/buger/goterm v1.0.4 - github.com/compose-spec/compose-go/v2 v2.7.1 + github.com/compose-spec/compose-go/v2 v2.7.2-0.20250721082312-a42e7579d813 github.com/containerd/containerd/v2 v2.1.3 github.com/containerd/errdefs v1.0.0 github.com/containerd/platforms v1.0.0-rc.1 @@ -181,6 +181,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/oauth2 v0.29.0 // indirect diff --git a/go.sum b/go.sum index 50aa32db9..509c76eb6 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,8 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= -github.com/compose-spec/compose-go/v2 v2.7.1 h1:EUIbuaD0R/J1KA+FbJMNbcS9+jt/CVudbp5iHqUllSs= -github.com/compose-spec/compose-go/v2 v2.7.1/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU= +github.com/compose-spec/compose-go/v2 v2.7.2-0.20250721082312-a42e7579d813 h1:Lmtch++VWP8Oqzff+FJflVW3g6/JFtDc3wq+tvRsagE= +github.com/compose-spec/compose-go/v2 v2.7.2-0.20250721082312-a42e7579d813/go.mod h1:veko/VB7URrg/tKz3vmIAQDaz+CGiXH8vZsW79NmAww= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= @@ -539,6 +539,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/pkg/api/api.go b/pkg/api/api.go index b57a142a6..3bd197578 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -170,8 +170,12 @@ type BuildOptions struct { Print bool // Check let builder validate build configuration Check bool - // Provenance - Provenance bool + // Attestations allows to enable attestations generation + Attestations bool + // Provenance generate a provenance attestation + Provenance string + // SBOM generate a SBOM attestation + SBOM string } // Apply mutates project according to build options diff --git a/pkg/compose/build.go b/pkg/compose/build.go index d404fd0a6..c2fadee41 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "os" + "strconv" "strings" "time" @@ -397,6 +398,7 @@ func resolveAndMergeBuildArgs(dockerCli command.Cli, project *types.Project, ser return result } +//nolint:gocyclo func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) { plats, err := parsePlatforms(service) if err != nil { @@ -471,8 +473,19 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se } attests := map[string]*string{} - if !options.Provenance { - attests["provenance"] = nil + if options.Attestations { + if service.Build.Provenance != "" { + attests["provenance"] = attestation(service.Build.Provenance, "provenance") + } + if service.Build.SBOM != "" { + attests["sbom"] = attestation(service.Build.SBOM, "sbom") + } + } + if options.Provenance != "" { + attests["provenance"] = attestation(options.Provenance, "provenance") + } + if options.SBOM != "" { + attests["sbom"] = attestation(options.SBOM, "sbom") } return build.Options{ @@ -502,6 +515,16 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se }, nil } +func attestation(attest string, val string) *string { + if b, err := strconv.ParseBool(val); err == nil { + s := fmt.Sprintf("type=%s,disabled=%t", attest, b) + return &s + } else { + s := fmt.Sprintf("type=%s,%s", attest, val) + return &s + } +} + func toUlimitOpt(ulimits map[string]*types.UlimitsConfig) *cliopts.UlimitOpt { ref := map[string]*container.Ulimit{} for _, limit := range toUlimits(ulimits) {