Repository: google/ko
Pull request #259 (2020-12-07 22:01:07)

Allow comma-separated list of platforms
Fixes https://github.com/google/ko/issues/253

Fixes https://github.com/google/ko/issues/274

```
$ ko publish -B --platform=linux/amd64,linux/arm64 ./cmd/ko/
2020/12/07 13:57:20 Using base gcr.io/distroless/static:nonroot for github.com/google/ko/cmd/ko
2020/12/07 13:57:21 Building github.com/google/ko/cmd/ko for linux/amd64
2020/12/07 13:57:25 Building github.com/google/ko/cmd/ko for linux/arm64
2020/12/07 13:57:29 Publishing gcr.io/jonjohnson-test/ko/ko:latest
2020/12/07 13:57:30 existing blob: sha256:72164b581b02b1eb297b403bcc8fc1bfa245cb52e103a3a525a0835a58ff58e2
2020/12/07 13:57:30 existing blob: sha256:e59bd8947ac7af2c8f4403b183326986ab554bbc262739cf5d9d596c7c7a3aca
2020/12/07 13:57:31 pushed blob: sha256:314bd22f324d73c81d23dd94a14e3a4df6a591381e39b9164163208115c23ae4
2020/12/07 13:57:33 pushed blob: sha256:dd714425e64113dddc2cf7a21a1b7f47c20ae29dc2072284b64fe1e7b82a5307
2020/12/07 13:57:33 gcr.io/jonjohnson-test/ko/ko@sha256:cb6a0c6cb99e6c7b1d7dfd598484d0d25b74e15a69837f3d87c08c58ee7f88e2: digest: sha256:cb6a0c6cb99e6c7b1d7dfd598484d0d25b74e15a69837f3d87c08c58ee7f88e2 size: 751
2020/12/07 13:57:34 existing blob: sha256:72164b581b02b1eb297b403bcc8fc1bfa245cb52e103a3a525a0835a58ff58e2
2020/12/07 13:57:34 existing blob: sha256:d34b09c140e4b67f2c6214e24f26a68bc6b17c28d42f0dda89db38ad03488416
2020/12/07 13:57:35 pushed blob: sha256:99af2cd1b289d3a47aba52934e2ac8c4037e92563e8a4d119b5d0e040f5b9ed3
2020/12/07 13:57:37 pushed blob: sha256:052d3194d06e9194d60e7dcdce4c7d8e91df952f34dfddd461ce181732342db2
2020/12/07 13:57:37 gcr.io/jonjohnson-test/ko/ko@sha256:ca19f1aa2adc983e55c1eb7d6af1ed8574991ff6762eccec8ef7b5e5d965d1ce: digest: sha256:ca19f1aa2adc983e55c1eb7d6af1ed8574991ff6762eccec8ef7b5e5d965d1ce size: 751
2020/12/07 13:57:38 gcr.io/jonjohnson-test/ko/ko:latest: digest: sha256:03ed41dd8676e9988fb4958366cf8d38fbb39e576d5b09bb1cc108c937e6d7ff size: 529
2020/12/07 13:57:38 Published gcr.io/jonjohnson-test/ko/ko@sha256:03ed41dd8676e9988fb4958366cf8d38fbb39e576d5b09bb1cc108c937e6d7ff
gcr.io/jonjohnson-test/ko/ko@sha256:03ed41dd8676e9988fb4958366cf8d38fbb39e576d5b09bb1cc108c937e6d7ff
```

codecov-io: # [Codecov](https://codecov.io/gh/google/ko/pull/259?src=pr&el=h1) Report
> Merging [#259](https://codecov.io/gh/google/ko/pull/259?src=pr&el=desc) (a97ab38) into [master](https://codecov.io/gh/google/ko/commit/5b08b051745eb93a001789f5f1c5a0da0f007f84?el=desc) (5b08b05) will **increase** coverage by `0.75%`.
> The diff coverage is `59.45%`.

[![Impacted file tree graph](https://codecov.io/gh/google/ko/pull/259/graphs/tree.svg?width=650&height=150&src=pr&token=UhqMbLrA3r)](https://codecov.io/gh/google/ko/pull/259?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #259      +/-   ##
==========================================
+ Coverage   37.06%   37.81%   +0.75%     
==========================================
  Files          33       33              
  Lines        1465     1494      +29     
==========================================
+ Hits          543      565      +22     
- Misses        833      838       +5     
- Partials       89       91       +2     
```


| [Impacted Files](https://codecov.io/gh/google/ko/pull/259?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/commands/config.go](https://codecov.io/gh/google/ko/pull/259/diff?src=pr&el=tree#diff-cGtnL2NvbW1hbmRzL2NvbmZpZy5nbw==) | `16.17% <0.00%> (+0.89%)` | :arrow_up: |
| [pkg/commands/resolver.go](https://codecov.io/gh/google/ko/pull/259/diff?src=pr&el=tree#diff-cGtnL2NvbW1hbmRzL3Jlc29sdmVyLmdv) | `11.93% <0.00%> (-0.50%)` | :arrow_down: |
| [pkg/build/gobuild.go](https://codecov.io/gh/google/ko/pull/259/diff?src=pr&el=tree#diff-cGtnL2J1aWxkL2dvYnVpbGQuZ28=) | `59.93% <82.60%> (+1.71%)` | :arrow_up: |
| [pkg/build/options.go](https://codecov.io/gh/google/ko/pull/259/diff?src=pr&el=tree#diff-cGtnL2J1aWxkL29wdGlvbnMuZ28=) | `85.71% <100.00%> (+2.38%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/google/ko/pull/259?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/google/ko/pull/259?src=pr&el=footer). Last update [5b08b05...a97ab38](https://codecov.io/gh/google/ko/pull/259?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

ImJasonH: Can you include README.md in this change?

In particular, what happens if I pass `ko resolve --platform=A,B` and some of the images built in the process support both platforms while some only support one or the other (or neither).
jonjohnsonjr: Will do -- also want to fix the TODO in there, but wanted to hack something together to play with the UX.
jonjohnsonjr: PTAL, cc @mattmoor 
google-cla[bot]: All (the pull request submitter and all commit authors) CLAs are signed, **but** one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project.  Please have them confirm that by leaving a comment that contains only `@googlebot I consent.` in this pull request.

*Note to project maintainer:* There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent.  In those cases, you can manually confirm consent of the commit author(s), and set the `cla` label to `yes` (if enabled on your project).

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogle%2Fko%2Fpull%2F259) for more info**.

<!-- need_author_consent -->
google-cla[bot]: All (the pull request submitter and all commit authors) CLAs are signed, **but** one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project.  Please have them confirm that by leaving a comment that contains only `@googlebot I consent.` in this pull request.

*Note to project maintainer:* There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent.  In those cases, you can manually confirm consent of the commit author(s), and set the `cla` label to `yes` (if enabled on your project).

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogle%2Fko%2Fpull%2F259) for more info**.

<!-- need_author_consent -->
mattmoor: @googlebot I consent!

google-cla[bot]: All (the pull request submitter and all commit authors) CLAs are signed, **but** one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project.  Please have them confirm that by leaving a comment that contains only `@googlebot I consent.` in this pull request.

*Note to project maintainer:* There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent.  In those cases, you can manually confirm consent of the commit author(s), and set the `cla` label to `yes` (if enabled on your project).

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogle%2Fko%2Fpull%2F259) for more info**.

<!-- need_author_consent -->
mattmoor: @googlebot I consent.
google-cla[bot]: All (the pull request submitter and all commit authors) CLAs are signed, **but** one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project.  Please have them confirm that by leaving a comment that contains only `@googlebot I consent.` in this pull request.

*Note to project maintainer:* There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent.  In those cases, you can manually confirm consent of the commit author(s), and set the `cla` label to `yes` (if enabled on your project).

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogle%2Fko%2Fpull%2F259) for more info**.

<!-- need_author_consent -->
jonjohnsonjr: @googlebot I consent.
google-cla[bot]: All (the pull request submitter and all commit authors) CLAs are signed, **but** one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project.  Please have them confirm that by leaving a comment that contains only `@googlebot I consent.` in this pull request.

*Note to project maintainer:* There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent.  In those cases, you can manually confirm consent of the commit author(s), and set the `cla` label to `yes` (if enabled on your project).

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogle%2Fko%2Fpull%2F259) for more info**.

<!-- need_author_consent -->
googlebot: All (the pull request submitter and all commit authors) CLAs are signed, **but** one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project.  Please have them confirm that by leaving a comment that contains only `@googlebot I consent.` in this pull request.

*Note to project maintainer:* There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent.  In those cases, you can manually confirm consent of the commit author(s), and set the `cla` label to `yes` (if enabled on your project).

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogle%2Fko%2Fpull%2F259) for more info**.

<!-- need_author_consent -->
google-cla[bot]: A Googler has manually verified that the CLAs look good.

(Googler, please make sure the reason for overriding the CLA status is clearly documented in these comments.)

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogle%2Fko%2Fpull%2F259) for more info**.

<!-- cla_yes -->

Patch:
diff --git a/.travis.yml b/.travis.yml
index 5fde9318..b723baeb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -18,14 +18,9 @@ script:
   - go install -mod=vendor ./cmd/ko
   # Try with all, and GOOS/GOARCH set.
   - |
-    GOOS=${GOOS} GOARCH=${GOARCH} KO_DOCKER_REPO=ko.local ko publish --platform=all -B ./cmd/ko/test
-    OUTPUT=$(docker run -i ko.local/test -wait=false 2>&1)
-    if [[ ! "${OUTPUT}" =~ "$(cat ./cmd/ko/test/kodata/kenobi)" ]]; then
-      echo Mismatched output: ${OUTPUT}, wanted: $(cat ./cmd/ko/test/kodata/kenobi)
-      exit 1
-    fi
-    if [[ ! "${OUTPUT}" =~ "$(cat ./cmd/ko/test/kodata/HEAD)" ]]; then
-      echo Mismatched output: ${OUTPUT}, wanted: $(cat ./cmd/ko/test/kodata/HEAD)
+    OUTPUT=$(GOOS=${GOOS} GOARCH=${GOARCH} KO_DOCKER_REPO=ko.local ko publish --platform=all -B ./cmd/ko/test 2>&1)
+    if [[ ! "${OUTPUT}" =~ "cannot use --platform with GOOS=\"${GOOS}\"" ]]; then
+      echo Mismatched output: ${OUTPUT}, wanted: "cannot use --platform with GOOS=\"${GOOS}\""
       exit 1
     fi
 
diff --git a/README.md b/README.md
index 9f131194..010132c2 100644
--- a/README.md
+++ b/README.md
@@ -454,13 +454,28 @@ a multi-architecture
 (aka "manifest list"), with support for each OS and architecture pair supported
 by the base image.
 
-If `ko` is invoked with `--platform=<some-OS>/<some-platform>` (e.g.,
+If `ko` is invoked with `--platform=<some-OS>/<some-arch>` (e.g.,
 `--platform=linux/amd64` or `--platform=linux/arm64`), then it will attempt to
 build an image for that OS and architecture only, assuming the base image
 supports it.
 
-When `--platform` is not provided, `ko` builds an image with the OS and
-architecture based on the build environment's `GOOS` and `GOARCH`.
+If the base image is a manifest list with more platforms than you want to build,
+invoking `ko` with comma-separated list of platforms (e.g.
+`--platform=linux/amd64,linux/arm/v6`) will produce a manifest list
+containing only the provided platforms. Note that if the base image does not
+contain platforms that are provided by this flag, `ko` will be unable to build
+a corresponding image, and this is not an error. The resulting artifact will be
+a multi-platform image containing the intersection of platforms from the base
+image and the `--platform` flag. This is especially relevant for projects that
+use multiple base images, as you must ensure that every base image contains all
+the platforms that you'd like to build.
+
+When `--platform` is not provided, if both `GOOS` and `GOARCH` environment
+variables are set, `ko` will build an image for `${GOOS}/${GOARCH}[/v${GOARM}]`,
+otherwise `ko` will build a `linux/amd64` image.
+
+Note that using both `--platform` and GOOS/GOARCH will return an error, as it's
+unclear what platform should be used.
 
 ## Enable Autocompletion
 
diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go
index 1da4b9c2..8028f717 100644
--- a/pkg/build/gobuild.go
+++ b/pkg/build/gobuild.go
@@ -54,6 +54,11 @@ type buildContext interface {
 	Import(path string, srcDir string, mode gb.ImportMode) (*gb.Package, error)
 }
 
+type platformMatcher struct {
+	spec      string
+	platforms []v1.Platform
+}
+
 type gobuild struct {
 	getBase              GetBase
 	creationTime         v1.Time
@@ -61,6 +66,7 @@ type gobuild struct {
 	disableOptimizations bool
 	mod                  *modules
 	buildContext         buildContext
+	platformMatcher      *platformMatcher
 }
 
 // Option is a functional option for NewGo.
@@ -73,12 +79,17 @@ type gobuildOpener struct {
 	disableOptimizations bool
 	mod                  *modules
 	buildContext         buildContext
+	platform             string
 }
 
 func (gbo *gobuildOpener) Open() (Interface, error) {
 	if gbo.getBase == nil {
 		return nil, errors.New("a way of providing base images must be specified, see build.WithBaseImages")
 	}
+	matcher, err := parseSpec(gbo.platform)
+	if err != nil {
+		return nil, err
+	}
 	return &gobuild{
 		getBase:              gbo.getBase,
 		creationTime:         gbo.creationTime,
@@ -86,6 +97,7 @@ func (gbo *gobuildOpener) Open() (Interface, error) {
 		disableOptimizations: gbo.disableOptimizations,
 		mod:                  gbo.mod,
 		buildContext:         gbo.buildContext,
+		platformMatcher:      matcher,
 	}, nil
 }
 
@@ -623,6 +635,10 @@ func (g *gobuild) buildAll(ctx context.Context, s string, base v1.ImageIndex) (v
 			return nil, fmt.Errorf("%q has unexpected mediaType %q in base for %q", desc.Digest, desc.MediaType, s)
 		}
 
+		if !g.platformMatcher.matches(desc.Platform) {
+			continue
+		}
+
 		base, err := base.Image(desc.Digest)
 		if err != nil {
 			return nil, err
@@ -649,3 +665,58 @@ func (g *gobuild) buildAll(ctx context.Context, s string, base v1.ImageIndex) (v
 
 	return mutate.IndexMediaType(mutate.AppendManifests(empty.Index, adds...), baseType), nil
 }
+
+func parseSpec(spec string) (*platformMatcher, error) {
+	// Don't bother parsing "all".
+	// "" should never happen because we default to linux/amd64.
+	platforms := []v1.Platform{}
+	if spec == "all" || spec == "" {
+		return &platformMatcher{spec: spec}, nil
+	}
+
+	for _, platform := range strings.Split(spec, ",") {
+		var p v1.Platform
+		parts := strings.Split(strings.TrimSpace(platform), "/")
+		if len(parts) > 0 {
+			p.OS = parts[0]
+		}
+		if len(parts) > 1 {
+			p.Architecture = parts[1]
+		}
+		if len(parts) > 2 {
+			p.Variant = parts[2]
+		}
+		if len(parts) > 3 {
+			return nil, fmt.Errorf("too many slashes in platform spec: %s", platform)
+		}
+		platforms = append(platforms, p)
+	}
+	return &platformMatcher{spec: spec, platforms: platforms}, nil
+}
+
+func (pm *platformMatcher) matches(base *v1.Platform) bool {
+	if pm.spec == "all" {
+		return true
+	}
+
+	// Don't build anything without a platform field unless "all". Unclear what we should do here.
+	if base == nil {
+		return false
+	}
+
+	for _, p := range pm.platforms {
+		if p.OS != "" && base.OS != p.OS {
+			continue
+		}
+		if p.Architecture != "" && base.Architecture != p.Architecture {
+			continue
+		}
+		if p.Variant != "" && base.Variant != p.Variant {
+			continue
+		}
+
+		return true
+	}
+
+	return false
+}
diff --git a/pkg/build/options.go b/pkg/build/options.go
index 66ddf4ca..413bd992 100644
--- a/pkg/build/options.go
+++ b/pkg/build/options.go
@@ -45,6 +45,19 @@ func WithDisabledOptimizations() Option {
 	}
 }
 
+// WithPlatforms is a functional option for building certain platforms for
+// multi-platform base images. To build everything from the base, use "all",
+// otherwise use a comma-separated list of platform specs, i.e.:
+//
+// platform = <os>[/<arch>[/<variant>]]
+// allowed = all | platform[,platform]*
+func WithPlatforms(platforms string) Option {
+	return func(gbo *gobuildOpener) error {
+		gbo.platform = platforms
+		return nil
+	}
+}
+
 // withBuilder is a functional option for overriding the way go binaries
 // are built.
 func withBuilder(b builder) Option {
diff --git a/pkg/commands/config.go b/pkg/commands/config.go
index e9a3b9b0..954acfc1 100644
--- a/pkg/commands/config.go
+++ b/pkg/commands/config.go
@@ -20,7 +20,6 @@ import (
 	"log"
 	"os"
 	"os/signal"
-	"path"
 	"strconv"
 	"strings"
 	"syscall"
@@ -41,16 +40,6 @@ var (
 )
 
 func getBaseImage(platform string) build.GetBase {
-	// Default to linux/amd64 unless GOOS and GOARCH are set.
-	if platform == "" {
-		platform = "linux/amd64"
-
-		goos, goarch := os.Getenv("GOOS"), os.Getenv("GOARCH")
-		if goos != "" && goarch != "" {
-			platform = path.Join(goos, goarch)
-		}
-	}
-
 	return func(ctx context.Context, s string) (build.Result, error) {
 		s = strings.TrimPrefix(s, build.StrictScheme)
 		// Viper configuration file keys are case insensitive, and are
@@ -71,8 +60,12 @@ func getBaseImage(platform string) build.GetBase {
 
 		// Using --platform=all will use an image index for the base,
 		// otherwise we'll resolve it to the appropriate platform.
+		//
+		// Platforms can be comma-separated if we only want a subset of the base
+		// image.
+		multiplatform := platform == "all" || strings.Contains(platform, ",")
 		var p v1.Platform
-		if platform != "" && platform != "all" {
+		if platform != "" && !multiplatform {
 			parts := strings.Split(platform, "/")
 			if len(parts) > 0 {
 				p.OS = parts[0]
@@ -96,7 +89,7 @@ func getBaseImage(platform string) build.GetBase {
 		}
 		switch desc.MediaType {
 		case types.OCIImageIndex, types.DockerManifestList:
-			if platform == "all" {
+			if multiplatform {
 				return desc.ImageIndex()
 			}
 			return desc.Image()
diff --git a/pkg/commands/options/build.go b/pkg/commands/options/build.go
index b31da8b1..67a282c8 100644
--- a/pkg/commands/options/build.go
+++ b/pkg/commands/options/build.go
@@ -33,6 +33,5 @@ func AddBuildOptions(cmd *cobra.Command, bo *BuildOptions) {
 	cmd.Flags().BoolVar(&bo.DisableOptimizations, "disable-optimizations", bo.DisableOptimizations,
 		"Disable optimizations when building Go code. Useful when you want to interactively debug the created container.")
 	cmd.Flags().StringVar(&bo.Platform, "platform", "",
-		"Which platform to use when pulling a multi-platform base. Format: all | <os>[/<arch>[/<variant>]]")
-
+		"Which platform to use when pulling a multi-platform base. Format: all | <os>[/<arch>[/<variant>]][,platform]*")
 }
diff --git a/pkg/commands/resolver.go b/pkg/commands/resolver.go
index 79cc5cd9..4bb7a1fb 100644
--- a/pkg/commands/resolver.go
+++ b/pkg/commands/resolver.go
@@ -24,6 +24,7 @@ import (
 	"log"
 	"net/http"
 	"os"
+	"path"
 	"strings"
 	"sync"
 
@@ -69,8 +70,34 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) {
 	if err != nil {
 		return nil, err
 	}
+
+	platform := bo.Platform
+	if platform == "" {
+		platform = "linux/amd64"
+
+		goos, goarch, goarm := os.Getenv("GOOS"), os.Getenv("GOARCH"), os.Getenv("GOARM")
+
+		// Default to linux/amd64 unless GOOS and GOARCH are set.
+		if goos != "" && goarch != "" {
+			platform = path.Join(goos, goarch)
+		}
+
+		// Use GOARM for variant if it's set and GOARCH is arm.
+		if strings.Contains(goarch, "arm") && goarm != "" {
+			platform = path.Join(platform, "v"+goarm)
+		}
+	} else {
+		// Make sure these are all unset
+		for _, env := range []string{"GOOS", "GOARCH", "GOARM"} {
+			if s, ok := os.LookupEnv(env); ok {
+				return nil, fmt.Errorf("cannot use --platform with %s=%q", env, s)
+			}
+		}
+	}
+
 	opts := []build.Option{
-		build.WithBaseImages(getBaseImage(bo.Platform)),
+		build.WithBaseImages(getBaseImage(platform)),
+		build.WithPlatforms(platform),
 	}
 	if creationTime != nil {
 		opts = append(opts, build.WithCreationTime(*creationTime))
diff --git a/pkg/resolve/resolve.go b/pkg/resolve/resolve.go
index 5a441bfa..e4c66bb7 100644
--- a/pkg/resolve/resolve.go
+++ b/pkg/resolve/resolve.go
@@ -44,7 +44,7 @@ func ImageReferences(ctx context.Context, docs []*yaml.Node, strict bool, builde
 			if err := builder.IsSupportedReference(ref); err == nil {
 				refs[ref] = append(refs[ref], node)
 			} else if strict {
-				return fmt.Errorf("found strict reference but %s is not a valid import path", ref)
+				return fmt.Errorf("found strict reference but %s is not a valid import path: %v", ref, err)
 			}
 		}
 	}


Test patch:
diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go
index 486d0db9..dadf5703 100644
--- a/pkg/build/gobuild_test.go
+++ b/pkg/build/gobuild_test.go
@@ -458,6 +458,7 @@ func TestGoBuildIndex(t *testing.T) {
 		context.Background(),
 		WithCreationTime(creationTime),
 		WithBaseImages(func(context.Context, string) (Result, error) { return base, nil }),
+		WithPlatforms("all"),
 		withBuilder(writeTempFile),
 	)
 	if err != nil {
@@ -611,3 +612,80 @@ func TestGoarm(t *testing.T) {
 		}
 	}
 }
+
+func TestMatchesPlatformSpec(t *testing.T) {
+	for _, tc := range []struct {
+		platform *v1.Platform
+		spec     string
+		result   bool
+		err      bool
+	}{{
+		platform: nil,
+		spec:     "all",
+		result:   true,
+	}, {
+		platform: nil,
+		spec:     "linux/amd64",
+		result:   false,
+	}, {
+		platform: &v1.Platform{
+			Architecture: "amd64",
+			OS:           "linux",
+		},
+		spec:   "all",
+		result: true,
+	}, {
+		platform: &v1.Platform{
+			Architecture: "amd64",
+			OS:           "windows",
+		},
+		spec:   "linux",
+		result: false,
+	}, {
+		platform: &v1.Platform{
+			Architecture: "arm64",
+			OS:           "linux",
+			Variant:      "v3",
+		},
+		spec:   "linux/amd64,linux/arm64",
+		result: true,
+	}, {
+		platform: &v1.Platform{
+			Architecture: "arm64",
+			OS:           "linux",
+			Variant:      "v3",
+		},
+		spec:   "linux/amd64,linux/arm64/v4",
+		result: false,
+	}, {
+		platform: &v1.Platform{
+			Architecture: "arm64",
+			OS:           "linux",
+			Variant:      "v3",
+		},
+		spec: "linux/amd64,linux/arm64/v3/z5",
+		err:  true,
+	}, {
+		spec: "",
+		platform: &v1.Platform{
+			Architecture: "amd64",
+			OS:           "linux",
+		},
+		result: false,
+	}} {
+		pm, err := parseSpec(tc.spec)
+		if tc.err {
+			if err == nil {
+				t.Errorf("parseSpec(%v, %q) expected err", tc.platform, tc.spec)
+			}
+			continue
+		}
+		if err != nil {
+			t.Fatalf("parseSpec failed for %v %q: %v", tc.platform, tc.spec, err)
+		}
+		matches := pm.matches(tc.platform)
+		if got, want := matches, tc.result; got != want {
+			t.Errorf("wrong result for %v %q: want %t got %t", tc.platform, tc.spec, want, got)
+		}
+	}
+}