Repository: GoogleContainerTools/skaffold
Pull request #7706 (2022-07-31 08:25:12)

fix: specifying target platform for image pulls
<!-- Thank you for your contribution! -->

<!-- Include if applicable: -->
Fixes: #7635 <!-- tracking issues that this PR will close -->

**Description**
<!-- Describe your changes here. The more detail, the easier the review! -->
This PR makes the following changes:
* modifies the `LocalDaemon.Pull` method to allow specifying the image platform.
* disallows `tryImportMissing` for multi-arch images
* propagates the correct platform to pull single platform images.

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) Report
> Merging [#7706](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (9ac97ad) into [main](https://codecov.io/gh/GoogleContainerTools/skaffold/commit/290280e8c6c19231e9ed5b5c027f2c01d22f75be?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (290280e) will **decrease** coverage by `3.92%`.
> The diff coverage is `53.13%`.

```diff
@@            Coverage Diff             @@
##             main    #7706      +/-   ##
==========================================
- Coverage   70.48%   66.56%   -3.93%     
==========================================
  Files         515      589      +74     
  Lines       23150    28280    +5130     
==========================================
+ Hits        16317    18824    +2507     
- Misses       5776     8073    +2297     
- Partials     1057     1383     +326     
```


| [Impacted Files](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | Coverage Δ | |
|---|---|---|
| [cmd/skaffold/app/cmd/credits/export.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY3JlZGl0cy9leHBvcnQuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/deploy.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZGVwbG95Lmdv) | `40.90% <0.00%> (-12.94%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/test.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvdGVzdC5nbw==) | `44.44% <0.00%> (ø)` | |
| [cmd/skaffold/skaffold.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL3NrYWZmb2xkLmdv) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/inspect\_tests.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvaW5zcGVjdF90ZXN0cy5nbw==) | `62.50% <14.28%> (-1.14%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/render.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvcmVuZGVyLmdv) | `35.48% <18.18%> (-5.90%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/lsp.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvbHNwLmdv) | `28.12% <28.12%> (ø)` | |
| [cmd/skaffold/app/cmd/run.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvcnVuLmdv) | `64.28% <33.33%> (-9.63%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/fix.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZml4Lmdv) | `56.41% <37.50%> (-20.07%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/verify.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvdmVyaWZ5Lmdv) | `41.17% <41.17%> (ø)` | |
| ... and [340 more](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7706/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | |

:mega: Codecov can now indicate which changes are the most critical in Pull Requests. [Learn more](https://about.codecov.io/product/feature/runtime-insights/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools)


Patch:
diff --git a/cmd/skaffold/app/cmd/flags.go b/cmd/skaffold/app/cmd/flags.go
index 4a73b9960..ac6019232 100644
--- a/cmd/skaffold/app/cmd/flags.go
+++ b/cmd/skaffold/app/cmd/flags.go
@@ -652,6 +652,24 @@ var flagRegistry = []Flag{
 		FlagAddMethod: "BoolVar",
 		DefinedOn:     []string{"dev", "render", "run", "debug", "deploy"},
 	},
+	{
+		Name:          "cloud-run-project",
+		Shorthand:     "",
+		Usage:         "The GCP Project ID or Project Number to deploy for Cloud Run",
+		Value:         &opts.CloudRunProject,
+		DefValue:      "",
+		FlagAddMethod: "StringVar",
+		DefinedOn:     []string{"dev", "run", "debug", "deploy", "apply", "delete"},
+	},
+	{
+		Name:          "cloud-run-location",
+		Shorthand:     "",
+		Usage:         "The GCP Region to deploy Cloud Run services to",
+		Value:         &opts.CloudRunLocation,
+		DefValue:      "",
+		FlagAddMethod: "StringVar",
+		DefinedOn:     []string{"dev", "run", "debug", "deploy", "apply", "delete"},
+	},
 }
 
 func methodNameByType(v reflect.Value) string {
diff --git a/docs-v2/content/en/docs/pipeline-stages/deployers/_index.md b/docs-v2/content/en/docs/pipeline-stages/deployers/_index.md
index 0e1dfbd4c..4ac946ac6 100644
--- a/docs-v2/content/en/docs/pipeline-stages/deployers/_index.md
+++ b/docs-v2/content/en/docs/pipeline-stages/deployers/_index.md
@@ -7,18 +7,13 @@ aliases: [/docs/how-tos/deployers]
 no_list: true
 ---
 
-When Skaffold deploys your application to Kubernetes, it (usually) goes through these steps:
+When Skaffold deploys your application to Kubernetes, it goes through these steps:
 
 In the default case (no manifest provided using the kubectl or kpt deployer ), skaffold deploy will do the following:
 * the Skaffold renderer _renders_ the final Kubernetes manifests: Skaffold replaces untagged image names in the Kubernetes manifests with the final tagged image names.
-It also might go through the extra intermediate step of expanding templates (for helm) or calculating overlays (for kustomize).
+It also might go through the extra intermediate step of expanding templates (for helm) or calculating overlays (for kustomize).  Additionally some deployers (docker) do not render manifests as such don't use this phase.
 * the Skaffold deployer _deploys_ the final Kubernetes manifests to the cluster (or to local docker for the docker deployer)
 * the Skaffold deployer performs [status checks]({{< relref "/docs/pipeline-stages/status-check" >}}) and waits for the deployed resources to stabilize.
-
-In the case that a manifest file is supplied via the --TODO flag or the deployer does not accept manifests (eg: helm and docker) skaffold deploy will only do the following steps:
-* the Skaffold deployer _deploys_ the supplied Kubernetes manifests to the cluster (or deploys the specified container to docker for the docker deployer)
-* the Skaffold deployer performs [status checks]({{< relref "/docs/pipeline-stages/status-check" >}}) and waits for the deployed resources to stabilize
-
 ### Supported deployers
 
 Skaffold supports the following tools for deploying applications:
diff --git a/docs/content/en/docs/references/cli/_index.md b/docs/content/en/docs/references/cli/_index.md
index 44c60df29..0c843957d 100644
--- a/docs/content/en/docs/references/cli/_index.md
+++ b/docs/content/en/docs/references/cli/_index.md
@@ -122,6 +122,8 @@ Examples:
 
 Options:
       --assume-yes=false: If true, skaffold will skip yes/no confirmation from the user and default to yes
+      --cloud-run-location='': The GCP Region to deploy Cloud Run services to
+      --cloud-run-project='': The GCP Project ID or Project Number to deploy for Cloud Run
   -c, --config='': File for global configurations (defaults to $HOME/.skaffold/config)
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
       --force=false: Recreate Kubernetes resources if necessary for deployment, warning: might cause downtime!
@@ -147,6 +149,8 @@ Use "skaffold options" for a list of global command-line options (applies to all
 Env vars:
 
 * `SKAFFOLD_ASSUME_YES` (same as `--assume-yes`)
+* `SKAFFOLD_CLOUD_RUN_LOCATION` (same as `--cloud-run-location`)
+* `SKAFFOLD_CLOUD_RUN_PROJECT` (same as `--cloud-run-project`)
 * `SKAFFOLD_CONFIG` (same as `--config`)
 * `SKAFFOLD_FILENAME` (same as `--filename`)
 * `SKAFFOLD_FORCE` (same as `--force`)
@@ -405,6 +409,8 @@ Options:
       --cache-artifacts=true: Set to false to disable default caching of artifacts
       --cache-file='': Specify the location of the cache file (default $HOME/.skaffold/cache)
       --cleanup=true: Delete deployments after dev or debug mode is interrupted
+      --cloud-run-location='': The GCP Region to deploy Cloud Run services to
+      --cloud-run-project='': The GCP Project ID or Project Number to deploy for Cloud Run
   -c, --config='': File for global configurations (defaults to $HOME/.skaffold/config)
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
@@ -464,6 +470,8 @@ Env vars:
 * `SKAFFOLD_CACHE_ARTIFACTS` (same as `--cache-artifacts`)
 * `SKAFFOLD_CACHE_FILE` (same as `--cache-file`)
 * `SKAFFOLD_CLEANUP` (same as `--cleanup`)
+* `SKAFFOLD_CLOUD_RUN_LOCATION` (same as `--cloud-run-location`)
+* `SKAFFOLD_CLOUD_RUN_PROJECT` (same as `--cloud-run-project`)
 * `SKAFFOLD_CONFIG` (same as `--config`)
 * `SKAFFOLD_DEFAULT_REPO` (same as `--default-repo`)
 * `SKAFFOLD_DETECT_MINIKUBE` (same as `--detect-minikube`)
@@ -518,6 +526,8 @@ Examples:
 
 Options:
       --assume-yes=false: If true, skaffold will skip yes/no confirmation from the user and default to yes
+      --cloud-run-location='': The GCP Region to deploy Cloud Run services to
+      --cloud-run-project='': The GCP Project ID or Project Number to deploy for Cloud Run
   -c, --config='': File for global configurations (defaults to $HOME/.skaffold/config)
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
@@ -543,6 +553,8 @@ Use "skaffold options" for a list of global command-line options (applies to all
 Env vars:
 
 * `SKAFFOLD_ASSUME_YES` (same as `--assume-yes`)
+* `SKAFFOLD_CLOUD_RUN_LOCATION` (same as `--cloud-run-location`)
+* `SKAFFOLD_CLOUD_RUN_PROJECT` (same as `--cloud-run-project`)
 * `SKAFFOLD_CONFIG` (same as `--config`)
 * `SKAFFOLD_DEFAULT_REPO` (same as `--default-repo`)
 * `SKAFFOLD_DETECT_MINIKUBE` (same as `--detect-minikube`)
@@ -579,6 +591,8 @@ Options:
       --assume-yes=false: If true, skaffold will skip yes/no confirmation from the user and default to yes
   -a, --build-artifacts=: File containing build result from a previous 'skaffold build --file-output'
       --build-concurrency=-1: Number of concurrently running builds. Set to 0 to run all builds in parallel. Doesn't violate build order among dependencies.
+      --cloud-run-location='': The GCP Region to deploy Cloud Run services to
+      --cloud-run-project='': The GCP Project ID or Project Number to deploy for Cloud Run
   -c, --config='': File for global configurations (defaults to $HOME/.skaffold/config)
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
@@ -626,6 +640,8 @@ Env vars:
 * `SKAFFOLD_ASSUME_YES` (same as `--assume-yes`)
 * `SKAFFOLD_BUILD_ARTIFACTS` (same as `--build-artifacts`)
 * `SKAFFOLD_BUILD_CONCURRENCY` (same as `--build-concurrency`)
+* `SKAFFOLD_CLOUD_RUN_LOCATION` (same as `--cloud-run-location`)
+* `SKAFFOLD_CLOUD_RUN_PROJECT` (same as `--cloud-run-project`)
 * `SKAFFOLD_CONFIG` (same as `--config`)
 * `SKAFFOLD_DEFAULT_REPO` (same as `--default-repo`)
 * `SKAFFOLD_DETECT_MINIKUBE` (same as `--detect-minikube`)
@@ -678,6 +694,8 @@ Options:
       --cache-artifacts=true: Set to false to disable default caching of artifacts
       --cache-file='': Specify the location of the cache file (default $HOME/.skaffold/cache)
       --cleanup=true: Delete deployments after dev or debug mode is interrupted
+      --cloud-run-location='': The GCP Region to deploy Cloud Run services to
+      --cloud-run-project='': The GCP Project ID or Project Number to deploy for Cloud Run
   -c, --config='': File for global configurations (defaults to $HOME/.skaffold/config)
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
@@ -737,6 +755,8 @@ Env vars:
 * `SKAFFOLD_CACHE_ARTIFACTS` (same as `--cache-artifacts`)
 * `SKAFFOLD_CACHE_FILE` (same as `--cache-file`)
 * `SKAFFOLD_CLEANUP` (same as `--cleanup`)
+* `SKAFFOLD_CLOUD_RUN_LOCATION` (same as `--cloud-run-location`)
+* `SKAFFOLD_CLOUD_RUN_PROJECT` (same as `--cloud-run-project`)
 * `SKAFFOLD_CONFIG` (same as `--config`)
 * `SKAFFOLD_DEFAULT_REPO` (same as `--default-repo`)
 * `SKAFFOLD_DETECT_MINIKUBE` (same as `--detect-minikube`)
@@ -1023,6 +1043,8 @@ Options:
       --cache-artifacts=true: Set to false to disable default caching of artifacts
       --cache-file='': Specify the location of the cache file (default $HOME/.skaffold/cache)
       --cleanup=true: Delete deployments after dev or debug mode is interrupted
+      --cloud-run-location='': The GCP Region to deploy Cloud Run services to
+      --cloud-run-project='': The GCP Project ID or Project Number to deploy for Cloud Run
   -c, --config='': File for global configurations (defaults to $HOME/.skaffold/config)
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
@@ -1077,6 +1099,8 @@ Env vars:
 * `SKAFFOLD_CACHE_ARTIFACTS` (same as `--cache-artifacts`)
 * `SKAFFOLD_CACHE_FILE` (same as `--cache-file`)
 * `SKAFFOLD_CLEANUP` (same as `--cleanup`)
+* `SKAFFOLD_CLOUD_RUN_LOCATION` (same as `--cloud-run-location`)
+* `SKAFFOLD_CLOUD_RUN_PROJECT` (same as `--cloud-run-project`)
 * `SKAFFOLD_CONFIG` (same as `--config`)
 * `SKAFFOLD_DEFAULT_REPO` (same as `--default-repo`)
 * `SKAFFOLD_DETECT_MINIKUBE` (same as `--detect-minikube`)
diff --git a/examples/templated-fields/charts/values.yaml b/examples/templated-fields/charts/values.yaml
index ba42ad6e1..3e6d9d77f 100644
--- a/examples/templated-fields/charts/values.yaml
+++ b/examples/templated-fields/charts/values.yaml
@@ -2,3 +2,4 @@
 # This is a YAML-formatted file.
 # Declare variables to be passed into your templates.
 replicaCount: 2
+image: skaffold-templated
\ No newline at end of file
diff --git a/examples/templated-fields/skaffold.yaml b/examples/templated-fields/skaffold.yaml
index 8dfa78de6..e1e1c58d6 100644
--- a/examples/templated-fields/skaffold.yaml
+++ b/examples/templated-fields/skaffold.yaml
@@ -16,8 +16,6 @@ deploy:
     releases:
     - name: skaffold-templated
       chartPath: charts
-      artifactOverrides:
-        image: skaffold-templated
       setValueTemplates:
         imageRepo: "{{.IMAGE_REPO}}"
         imageTag: "{{.IMAGE_TAG}}"
diff --git a/pkg/skaffold/build/buildpacks/fetcher.go b/pkg/skaffold/build/buildpacks/fetcher.go
index 14389ec0b..93ed7322e 100644
--- a/pkg/skaffold/build/buildpacks/fetcher.go
+++ b/pkg/skaffold/build/buildpacks/fetcher.go
@@ -25,6 +25,7 @@ import (
 	"github.com/buildpacks/imgutil/local"
 	pack "github.com/buildpacks/pack/pkg/client"
 	packimg "github.com/buildpacks/pack/pkg/image"
+	v1 "github.com/google/go-containerregistry/pkg/v1"
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
 )
@@ -45,7 +46,7 @@ func newFetcher(out io.Writer, docker docker.LocalDaemon) *fetcher {
 
 func (f *fetcher) Fetch(ctx context.Context, name string, options packimg.FetchOptions) (imgutil.Image, error) {
 	if options.PullPolicy == packimg.PullAlways || (options.PullPolicy == packimg.PullIfNotPresent && !f.docker.ImageExists(ctx, name)) {
-		if err := f.docker.Pull(ctx, f.out, name); err != nil {
+		if err := f.docker.Pull(ctx, f.out, name, v1.Platform{Architecture: "amd64", OS: "linux"}); err != nil {
 			return nil, err
 		}
 	}
diff --git a/pkg/skaffold/build/cache/lookup.go b/pkg/skaffold/build/cache/lookup.go
index 1ebf062c2..369c3a6eb 100644
--- a/pkg/skaffold/build/cache/lookup.go
+++ b/pkg/skaffold/build/cache/lookup.go
@@ -22,12 +22,15 @@ import (
 	"io"
 	"sync"
 
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/tag"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
 )
 
 func (c *cache) lookupArtifacts(ctx context.Context, tags tag.ImageTags, platforms platform.Resolver, artifacts []*latest.Artifact) []cacheDetails {
@@ -67,8 +70,16 @@ func (c *cache) lookup(ctx context.Context, a *latest.Artifact, tag string, plat
 	c.cacheMutex.RLock()
 	entry, cacheHit := c.artifactCache[hash]
 	c.cacheMutex.RUnlock()
-	if !cacheHit {
-		if entry, err = c.tryImport(ctx, a, tag, hash); err != nil {
+
+	pls := platforms.GetPlatforms(a.ImageName)
+	// TODO (gaghosh): allow `tryImport` when the Docker daemon starts supporting multiarch images
+	// See https://github.com/docker/buildx/issues/1220#issuecomment-1189996403
+	if !cacheHit && !pls.IsMultiPlatform() {
+		var pl v1.Platform
+		if len(pls.Platforms) == 1 {
+			pl = util.ConvertToV1Platform(pls.Platforms[0])
+		}
+		if entry, err = c.tryImport(ctx, a, tag, hash, pl); err != nil {
 			log.Entry(ctx).Debugf("Could not import artifact from Docker, building instead (%s)", err)
 			return needsBuilding{hash: hash}
 		}
@@ -131,7 +142,7 @@ func (c *cache) lookupRemote(ctx context.Context, hash, tag string, entry ImageD
 	return needsBuilding{hash: hash}
 }
 
-func (c *cache) tryImport(ctx context.Context, a *latest.Artifact, tag string, hash string) (ImageDetails, error) {
+func (c *cache) tryImport(ctx context.Context, a *latest.Artifact, tag string, hash string, pl v1.Platform) (ImageDetails, error) {
 	entry := ImageDetails{}
 
 	if importMissing, err := c.importMissingImage(a.ImageName); err != nil {
@@ -142,7 +153,7 @@ func (c *cache) tryImport(ctx context.Context, a *latest.Artifact, tag string, h
 
 	if !c.client.ImageExists(ctx, tag) {
 		log.Entry(ctx).Debugf("Importing artifact %s from docker registry", tag)
-		err := c.client.Pull(ctx, io.Discard, tag)
+		err := c.client.Pull(ctx, io.Discard, tag, pl)
 		if err != nil {
 			return entry, err
 		}
diff --git a/pkg/skaffold/build/docker/docker.go b/pkg/skaffold/build/docker/docker.go
index 843f1bbba..e471c8498 100644
--- a/pkg/skaffold/build/docker/docker.go
+++ b/pkg/skaffold/build/docker/docker.go
@@ -23,6 +23,8 @@ import (
 	"os"
 	"os/exec"
 
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
@@ -39,6 +41,10 @@ func (b *Builder) SupportedPlatforms() platform.Matcher {
 }
 
 func (b *Builder) Build(ctx context.Context, out io.Writer, a *latest.Artifact, tag string, matcher platform.Matcher) (string, error) {
+	var pl v1.Platform
+	if len(matcher.Platforms) == 1 {
+		pl = util.ConvertToV1Platform(matcher.Platforms[0])
+	}
 	a = adjustCacheFrom(a, tag)
 	instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{
 		"BuildType":   "docker",
@@ -55,7 +61,7 @@ func (b *Builder) Build(ctx context.Context, out io.Writer, a *latest.Artifact,
 		return "", dockerfileNotFound(err, a.ImageName)
 	}
 
-	if err := b.pullCacheFromImages(ctx, out, a.ArtifactType.DockerArtifact); err != nil {
+	if err := b.pullCacheFromImages(ctx, out, a.ArtifactType.DockerArtifact, pl); err != nil {
 		return "", cacheFromPullErr(err, a.ImageName)
 	}
 	opts := docker.BuildOptions{Tag: tag, Mode: b.cfg.Mode(), ExtraBuildArgs: docker.ResolveDependencyImages(a.Dependencies, b.artifacts, true)}
@@ -66,7 +72,7 @@ func (b *Builder) Build(ctx context.Context, out io.Writer, a *latest.Artifact,
 	// we might consider a different approach in the future.
 	// use CLI for cross-platform builds
 	if b.useCLI || (b.useBuildKit != nil && *b.useBuildKit) || len(a.DockerArtifact.CliFlags) > 0 || matcher.IsNotEmpty() {
-		imageID, err = b.dockerCLIBuild(ctx, output.GetUnderlyingWriter(out), a.ImageName, a.Workspace, dockerfile, a.ArtifactType.DockerArtifact, opts, matcher)
+		imageID, err = b.dockerCLIBuild(ctx, output.GetUnderlyingWriter(out), a.ImageName, a.Workspace, dockerfile, a.ArtifactType.DockerArtifact, opts, pl)
 	} else {
 		imageID, err = b.localDocker.Build(ctx, out, a.Workspace, a.ImageName, a.ArtifactType.DockerArtifact, opts)
 	}
@@ -84,12 +90,7 @@ func (b *Builder) Build(ctx context.Context, out io.Writer, a *latest.Artifact,
 	return imageID, nil
 }
 
-func (b *Builder) dockerCLIBuild(ctx context.Context, out io.Writer, name string, workspace string, dockerfilePath string, a *latest.DockerArtifact, opts docker.BuildOptions, matcher platform.Matcher) (string, error) {
-	if matcher.IsMultiPlatform() {
-		// TODO: implement multi platform build
-		log.Entry(ctx).Warnf("multiple target platforms %q found for artifact %q. Skaffold doesn't yet support multi-platform builds for the docker builder. Consider specifying a single target platform explicitly. See https://skaffold.dev/docs/pipeline-stages/builders/#cross-platform-build-support", matcher.String(), name)
-	}
-
+func (b *Builder) dockerCLIBuild(ctx context.Context, out io.Writer, name string, workspace string, dockerfilePath string, a *latest.DockerArtifact, opts docker.BuildOptions, pl v1.Platform) (string, error) {
 	args := []string{"build", workspace, "--file", dockerfilePath, "-t", opts.Tag}
 	ba, err := docker.EvalBuildArgs(b.cfg.Mode(), workspace, a.DockerfilePath, a.BuildArgs, opts.ExtraBuildArgs)
 	if err != nil {
@@ -105,8 +106,8 @@ func (b *Builder) dockerCLIBuild(ctx context.Context, out io.Writer, name string
 		args = append(args, "--force-rm")
 	}
 
-	if len(matcher.Platforms) == 1 {
-		args = append(args, "--platform", platform.Format(matcher.Platforms[0]))
+	if pl.String() != "" {
+		args = append(args, "--platform", pl.String())
 	}
 
 	cmd := exec.CommandContext(ctx, "docker", args...)
@@ -117,8 +118,8 @@ func (b *Builder) dockerCLIBuild(ctx context.Context, out io.Writer, name string
 		} else {
 			cmd.Env = append(cmd.Env, "DOCKER_BUILDKIT=0")
 		}
-	} else if len(matcher.Platforms) == 1 { // cross-platform builds require buildkit
-		log.Entry(ctx).Debugf("setting DOCKER_BUILDKIT=1 for docker build for artifact %q since it targets platform %q", name, matcher.Platforms[0])
+	} else if pl.String() != "" { // cross-platform builds require buildkit
+		log.Entry(ctx).Debugf("setting DOCKER_BUILDKIT=1 for docker build for artifact %q since it targets platform %q", name, pl.String())
 		cmd.Env = append(cmd.Env, "DOCKER_BUILDKIT=1")
 	}
 	cmd.Stdout = out
@@ -131,7 +132,7 @@ func (b *Builder) dockerCLIBuild(ctx context.Context, out io.Writer, name string
 	return b.localDocker.ImageID(ctx, opts.Tag)
 }
 
-func (b *Builder) pullCacheFromImages(ctx context.Context, out io.Writer, a *latest.DockerArtifact) error {
+func (b *Builder) pullCacheFromImages(ctx context.Context, out io.Writer, a *latest.DockerArtifact, pl v1.Platform) error {
 	if len(a.CacheFrom) == 0 {
 		return nil
 	}
@@ -146,8 +147,8 @@ func (b *Builder) pullCacheFromImages(ctx context.Context, out io.Writer, a *lat
 			continue
 		}
 
-		if err := b.localDocker.Pull(ctx, out, image); err != nil {
-			warnings.Printf("cacheFrom image couldn't be pulled: %s\n", image)
+		if err := b.localDocker.Pull(ctx, out, image, pl); err != nil {
+			warnings.Printf("cacheFrom image %q couldn't be pulled for platform %q\n", image, pl)
 		}
 	}
 
diff --git a/pkg/skaffold/config/options.go b/pkg/skaffold/config/options.go
index 2b92a83ef..6e8988b04 100644
--- a/pkg/skaffold/config/options.go
+++ b/pkg/skaffold/config/options.go
@@ -61,6 +61,8 @@ type SkaffoldOptions struct {
 	EnablePlatformNodeAffinity bool
 	MakePathsAbsolute          *bool
 	MultiLevelRepo             *bool
+	CloudRunProject            string
+	CloudRunLocation           string
 	ConfigurationFile          string
 	HydrationDir               string
 	InventoryNamespace         string
diff --git a/pkg/skaffold/deploy/docker/deploy.go b/pkg/skaffold/deploy/docker/deploy.go
index f6dad5b82..1435df268 100644
--- a/pkg/skaffold/deploy/docker/deploy.go
+++ b/pkg/skaffold/deploy/docker/deploy.go
@@ -25,6 +25,7 @@ import (
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/mount"
 	"github.com/docker/go-connections/nat"
+	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/pkg/errors"
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/access"
@@ -229,7 +230,7 @@ func (d *Deployer) setupDebugging(ctx context.Context, out io.Writer, artifact g
 			continue
 		}
 		// pull the debug support image into the local daemon
-		if err := d.client.Pull(ctx, out, c.Image); err != nil {
+		if err := d.client.Pull(ctx, out, c.Image, v1.Platform{}); err != nil {
 			return nil, errors.Wrap(err, "pulling init container image")
 		}
 		// create the init container
diff --git a/pkg/skaffold/docker/image.go b/pkg/skaffold/docker/image.go
index f0d7f3509..6ed2fea53 100644
--- a/pkg/skaffold/docker/image.go
+++ b/pkg/skaffold/docker/image.go
@@ -86,7 +86,7 @@ type LocalDaemon interface {
 	ContainerInspect(ctx context.Context, id string) (types.ContainerJSON, error)
 	ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error)
 	Push(ctx context.Context, out io.Writer, ref string) (string, error)
-	Pull(ctx context.Context, out io.Writer, ref string) error
+	Pull(ctx context.Context, out io.Writer, ref string, platform v1.Platform) error
 	Load(ctx context.Context, out io.Writer, input io.Reader, ref string) (string, error)
 	Run(ctx context.Context, out io.Writer, opts ContainerCreateOpts) (<-chan container.ContainerWaitOKBody, <-chan error, string, error)
 	Delete(ctx context.Context, out io.Writer, id string) error
@@ -472,7 +472,7 @@ func (l *localDaemon) isAlreadyPushed(ctx context.Context, ref, registryAuth str
 }
 
 // Pull pulls an image reference from a registry.
-func (l *localDaemon) Pull(ctx context.Context, out io.Writer, ref string) error {
+func (l *localDaemon) Pull(ctx context.Context, out io.Writer, ref string, platform v1.Platform) error {
 	// We first try pulling the image with credentials.  If that fails then retry
 	// without credentials in case the image is public.
 
@@ -503,6 +503,7 @@ func (l *localDaemon) Pull(ctx context.Context, out io.Writer, ref string) error
 			//     return "" to retry as an anonymous pull.
 			return "", err
 		},
+		Platform: platform.String(),
 	})
 	if err != nil {
 		return fmt.Errorf("pulling image from repository: %w", err)
diff --git a/pkg/skaffold/helm/args.go b/pkg/skaffold/helm/args.go
index 311b2465c..46ce47267 100644
--- a/pkg/skaffold/helm/args.go
+++ b/pkg/skaffold/helm/args.go
@@ -130,5 +130,9 @@ func envVarForImage(imageName string, digest string) map[string]string {
 	} else {
 		customMap["DIGEST_HEX"] = digest
 	}
+
+	// IMAGE_DOMAIN and IMAGE_REPO_NO_DOMAIN added for v2beta* 'helm+explicitRegistry' -> v3alpha* compatibility
+	customMap["IMAGE_DOMAIN"] = ref.Domain
+	customMap["IMAGE_REPO_NO_DOMAIN"] = strings.TrimPrefix(ref.BaseName, ref.Domain+"/")
 	return customMap
 }
diff --git a/pkg/skaffold/runner/deployer.go b/pkg/skaffold/runner/deployer.go
index f1f41ab64..fef9fca4d 100644
--- a/pkg/skaffold/runner/deployer.go
+++ b/pkg/skaffold/runner/deployer.go
@@ -99,7 +99,7 @@ func GetDeployer(ctx context.Context, runCtx *runcontext.RunContext, labeller *l
 				return nil, errors.New("skaffold apply called with both Cloud Run and Kubernetes deployers. Mixing deployment targets is not allowed" +
 					" when using the Cloud Run deployer")
 			}
-			return getCloudRunDeployer(runCtx, labeller)
+			return getCloudRunDeployer(runCtx, labeller, pipelines.Deployers(), "")
 		}
 		if len(helmNamespaces) > 1 || (nonHelmDeployFound && len(helmNamespaces) == 1) {
 			return nil, errors.New("skaffold apply called with conflicting namespaces set via skaffold.yaml. This is likely due to the use of the 'deploy.helm.releases.*.namespace' field which is not supported in apply.  Remove the 'deploy.helm.releases.*.namespace' field(s) and run skaffold apply again")
@@ -185,7 +185,7 @@ func GetDeployer(ctx context.Context, runCtx *runcontext.RunContext, labeller *l
 			deployers = append(deployers, deployer)
 		}
 		if d.CloudRunDeploy != nil {
-			deployer, err := cloudrun.NewDeployer(dCtx, labeller, d.CloudRunDeploy, configName)
+			deployer, err := getCloudRunDeployer(dCtx.RunContext, labeller, runCtx.DeployConfigs(), configName)
 			if err != nil {
 				return nil, err
 			}
@@ -312,21 +312,38 @@ func validateKubectlFlags(flags *latest.KubectlFlags, additional latest.KubectlF
 }
 
 /* The Cloud Run deployer for apply. Used when Cloud Run is specified. */
-func getCloudRunDeployer(runCtx *runcontext.RunContext, labeller *label.DefaultLabeller) (*cloudrun.Deployer, error) {
+func getCloudRunDeployer(runCtx *runcontext.RunContext, labeller *label.DefaultLabeller, deployers []latest.DeployConfig, configName string) (*cloudrun.Deployer, error) {
 	var region string
+	regionFlag := false
 	var defaultProject string
-	for _, d := range runCtx.DeployConfigs() {
+	projectFlag := false
+	if runCtx.Opts.CloudRunLocation != "" {
+		region = runCtx.Opts.CloudRunLocation
+		regionFlag = true
+	}
+	if runCtx.Opts.CloudRunProject != "" {
+		defaultProject = runCtx.Opts.CloudRunProject
+		projectFlag = true
+	}
+	for _, d := range deployers {
 		if d.CloudRunDeploy != nil {
 			crDeploy := d.CloudRunDeploy
-			if region != "" && region != crDeploy.Region {
-				return nil, fmt.Errorf("expected all Cloud Run deploys to be in the same region, found deploys to %s and %s", region, crDeploy.Region)
+			if !regionFlag {
+				// No region flag was provided, so take it from the config.
+				if region != "" && region != crDeploy.Region {
+					return nil, fmt.Errorf("expected all Cloud Run deploys to be in the same region, found deploys to %s and %s", region, crDeploy.Region)
+				}
+
+				region = crDeploy.Region
 			}
-			region = crDeploy.Region
-			if defaultProject != "" && defaultProject != crDeploy.ProjectID {
-				return nil, fmt.Errorf("expected all Cloud Run deploys to use the same project, found deploys to projects %s and %s", defaultProject, crDeploy.ProjectID)
+			if !projectFlag {
+				// No project flag was specified so take it from the config.
+				if defaultProject != "" && defaultProject != crDeploy.ProjectID {
+					return nil, fmt.Errorf("expected all Cloud Run deploys to use the same project, found deploys to projects %s and %s", defaultProject, crDeploy.ProjectID)
+				}
+				defaultProject = crDeploy.ProjectID
 			}
-			defaultProject = crDeploy.ProjectID
 		}
 	}
-	return cloudrun.NewDeployer(runCtx, labeller, &latest.CloudRunDeploy{Region: region, ProjectID: defaultProject}, "")
+	return cloudrun.NewDeployer(runCtx, labeller, &latest.CloudRunDeploy{Region: region, ProjectID: defaultProject}, configName)
 }
diff --git a/pkg/skaffold/schema/v2beta29/upgrade.go b/pkg/skaffold/schema/v2beta29/upgrade.go
index f8b5b61a8..3ee98127e 100644
--- a/pkg/skaffold/schema/v2beta29/upgrade.go
+++ b/pkg/skaffold/schema/v2beta29/upgrade.go
@@ -86,6 +86,43 @@ func upgradeOnePipeline(oldPipeline, newPipeline interface{}) error {
 		newPL.Render.Helm.Releases = newHelm.Releases
 		newPL.Render.Helm.Flags = newHelm.Flags
 
+		// Copy over removed artifactOverrides & imageStrategy field as identical setValues fields
+		for i := 0; i < len(newPL.Render.Helm.Releases); i++ {
+			// need to append, not override
+			svs := map[string]string(oldPL.Deploy.HelmDeploy.Releases[i].SetValues)
+			svts := map[string]string(oldPL.Deploy.HelmDeploy.Releases[i].SetValueTemplates)
+			aos := map[string]string(oldPL.Deploy.HelmDeploy.Releases[i].ArtifactOverrides)
+			if aos != nil && svs == nil {
+				svs = map[string]string{}
+			}
+			if oldPL.Deploy.HelmDeploy.Releases[i].ImageStrategy.HelmConventionConfig != nil {
+				// is 'helm' imageStrategy
+				for k, v := range aos {
+					if k == "image" {
+						if svts == nil {
+							svts = map[string]string{}
+						}
+						svts["image.tag"] = "{{.IMAGE_TAG}}@{{.IMAGE_DIGEST}}"
+						svts["image.repository"] = "{{.IMAGE_REPO}}"
+						if oldPL.Deploy.HelmDeploy.Releases[i].ImageStrategy.HelmConventionConfig.ExplicitRegistry {
+							// is 'helm' imageStrategy + explicitRegistry
+							svts["image.registry"] = "{{.IMAGE_DOMAIN}}"
+							svts["image.repository"] = "{{.IMAGE_REPO_NO_DOMAIN}}"
+						}
+						continue
+					}
+					svs[k] = v
+				}
+			} else {
+				// is 'fqn' imageStrategy
+				for k, v := range aos {
+					svs[k] = v
+				}
+			}
+			newPL.Render.Helm.Releases[i].SetValues = svs
+			newPL.Render.Helm.Releases[i].SetValueTemplates = svts
+		}
+
 		// Copy over lifecyle hooks for helm deployer
 		newPL.Deploy.LegacyHelmDeploy = &next.LegacyHelmDeploy{}
 		newPL.Deploy.LegacyHelmDeploy.LifecycleHooks = newHelm.LifecycleHooks
diff --git a/pkg/skaffold/verify/docker/verify.go b/pkg/skaffold/verify/docker/verify.go
index 08ac787fe..cc9cc7ea5 100644
--- a/pkg/skaffold/verify/docker/verify.go
+++ b/pkg/skaffold/verify/docker/verify.go
@@ -30,6 +30,7 @@ import (
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/go-connections/nat"
 	"github.com/fatih/semgroup"
+	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/uuid"
 	"github.com/pkg/errors"
 
@@ -145,7 +146,7 @@ func (v *Verifier) Verify(ctx context.Context, out io.Writer, allbuilds []graph.
 			}
 		}
 		if !foundArtifact {
-			err = v.client.Pull(ctx, out, tc.Container.Image)
+			err = v.client.Pull(ctx, out, tc.Container.Image, v1.Platform{})
 			if err != nil {
 				return err
 			}


Test patch:
diff --git a/pkg/skaffold/build/local/local_test.go b/pkg/skaffold/build/local/local_test.go
index 8b12dfc7a..f7f45a920 100644
--- a/pkg/skaffold/build/local/local_test.go
+++ b/pkg/skaffold/build/local/local_test.go
@@ -186,7 +186,7 @@ func TestLocalRun(t *testing.T) {
 			}).Add("pull1", ""),
 			tag:              "gcr.io/test/image:tag",
 			expected:         "gcr.io/test/image:1",
-			expectedWarnings: []string{"cacheFrom image couldn't be pulled: pull1\n"},
+			expectedWarnings: []string{"cacheFrom image \"pull1\" couldn't be pulled for platform \"\"\n"},
 		},
 		{
 			description: "error checking cache-from image",
diff --git a/pkg/skaffold/runner/deployer_test.go b/pkg/skaffold/runner/deployer_test.go
index a0b415da9..a75a8822e 100644
--- a/pkg/skaffold/runner/deployer_test.go
+++ b/pkg/skaffold/runner/deployer_test.go
@@ -470,3 +470,54 @@ func TestGetDefaultDeployer(tOuter *testing.T) {
 		}
 	})
 }
+
+func TestGetCloudRunDeployer(tOuter *testing.T) {
+	tests := []struct {
+		name     string
+		opts     config.SkaffoldOptions
+		cfgs     map[string]latest.DeployType
+		expected *cloudrun.Deployer
+		haveErr  bool
+	}{
+		{
+			name: "deploy one config with no flags set",
+			cfgs: map[string]latest.DeployType{"": {
+				CloudRunDeploy: &latest.CloudRunDeploy{ProjectID: "test-project", Region: "test-region"},
+			}},
+			expected: &cloudrun.Deployer{Project: "test-project", Region: "test-region"},
+		},
+		{
+			name: "deploy with two configs and conflicting processes",
+			cfgs: map[string]latest.DeployType{"": {
+				CloudRunDeploy: &latest.CloudRunDeploy{ProjectID: "test-project", Region: "test-region"}},
+				"second": {
+					CloudRunDeploy: &latest.CloudRunDeploy{ProjectID: "test-project2", Region: "test-region"},
+				},
+			},
+			haveErr: true,
+		},
+		{
+			name: "deploy with flags set overrides config",
+			opts: config.SkaffoldOptions{CloudRunProject: "overridden-project"},
+			cfgs: map[string]latest.DeployType{"": {
+				CloudRunDeploy: &latest.CloudRunDeploy{ProjectID: "test-project", Region: "test-region"},
+			}},
+			expected: &cloudrun.Deployer{Project: "overridden-project", Region: "test-region"},
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(tOuter, test.name, func(t *testutil.T) {
+			pipelines := make(map[string]latest.Pipeline)
+			for name, config := range test.cfgs {
+				pipelines[name] = latest.Pipeline{Deploy: latest.DeployConfig{DeployType: config}}
+			}
+			rctx := &runcontext.RunContext{
+				Opts:      test.opts,
+				Pipelines: runcontext.NewPipelines(pipelines),
+			}
+			crDeployer, err := getCloudRunDeployer(rctx, &label.DefaultLabeller{}, rctx.DeployConfigs(), "")
+			t.CheckErrorAndFailNow(test.haveErr, err)
+			t.CheckDeepEqual(crDeployer, test.expected, cmpopts.IgnoreUnexported(cloudrun.Deployer{}))
+		})
+	}
+}
diff --git a/pkg/skaffold/test/structure/structure.go b/pkg/skaffold/test/structure/structure.go
index 3ece30061..86d05e80f 100644
--- a/pkg/skaffold/test/structure/structure.go
+++ b/pkg/skaffold/test/structure/structure.go
@@ -23,6 +23,8 @@ import (
 	"os"
 	"os/exec"
 
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/event"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
@@ -71,7 +73,7 @@ func (cst *Runner) runStructureTests(ctx context.Context, out io.Writer, imageTa
 		// The image is remote so we have to pull it locally.
 		// `container-structure-test` currently can't do it:
 		// https://github.com/GoogleContainerTools/container-structure-test/issues/253.
-		if err := cst.localDaemon.Pull(ctx, out, imageTag); err != nil {
+		if err := cst.localDaemon.Pull(ctx, out, imageTag, v1.Platform{}); err != nil {
 			return dockerPullImageErr(imageTag, err)
 		}
 	}