Repository: GoogleContainerTools/skaffold
Pull request #7788 (2022-08-19 13:56:33)

fix: Fill IMAGE_TAG,etc on Docker builds
Fixes: #4295

**Description**
Docs have long stated that these fields are available when using the local docker builder. This makes is to they actually are.

aaron-prindle: Thanks for the PR here!  Can you rebase this PR, I think recently one of the issues in the tests:
```
panic: runtime error: index out of range [0] with length 0 [recovered]
	panic: runtime error: index out of range [0] with length 0
```
was fixed - 
https://github.com/GoogleContainerTools/skaffold/issues/7774

Additionally I believe there are some legitimate test failures where some expected values in tests might have to be changed in relation to the change made here.  To run the full set of unit tests locally you can use `make test`.  After we get these tests green we should be able to merge this!



Patch:
diff --git a/cmd/skaffold/app/cmd/flags.go b/cmd/skaffold/app/cmd/flags.go
index 5a802ea10..af238350d 100644
--- a/cmd/skaffold/app/cmd/flags.go
+++ b/cmd/skaffold/app/cmd/flags.go
@@ -104,7 +104,7 @@ var flagRegistry = []Flag{
 		Value:         &opts.Profiles,
 		DefValue:      []string{},
 		FlagAddMethod: "StringSliceVar",
-		DefinedOn:     []string{"dev", "run", "debug", "deploy", "render", "build", "delete", "diagnose", "apply", "test"},
+		DefinedOn:     []string{"dev", "run", "debug", "deploy", "render", "build", "delete", "diagnose", "apply", "test", "verify"},
 	},
 	{
 		Name:          "namespace",
@@ -421,7 +421,7 @@ var flagRegistry = []Flag{
 		Value:         &opts.ProfileAutoActivation,
 		DefValue:      true,
 		FlagAddMethod: "BoolVar",
-		DefinedOn:     []string{"dev", "run", "debug", "deploy", "render", "build", "delete", "diagnose", "test"},
+		DefinedOn:     []string{"dev", "run", "debug", "deploy", "render", "build", "delete", "diagnose", "test", "verify"},
 		IsEnum:        true,
 	},
 	{
@@ -430,7 +430,7 @@ var flagRegistry = []Flag{
 		Value:         &opts.PropagateProfiles,
 		DefValue:      true,
 		FlagAddMethod: "BoolVar",
-		DefinedOn:     []string{"dev", "run", "debug", "deploy", "render", "build", "delete", "diagnose", "test"},
+		DefinedOn:     []string{"dev", "run", "debug", "deploy", "render", "build", "delete", "diagnose", "test", "verify"},
 		IsEnum:        true,
 	},
 	{
diff --git a/docs/content/en/docs/references/cli/_index.md b/docs/content/en/docs/references/cli/_index.md
index 5d89b0d02..a4f3934de 100644
--- a/docs/content/en/docs/references/cli/_index.md
+++ b/docs/content/en/docs/references/cli/_index.md
@@ -1287,6 +1287,9 @@ Options:
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
   -m, --module=[]: Filter Skaffold configs to only the provided named modules
       --port-forward=off: Port-forward exposes service ports and container ports within pods and other resources (off, user, services, debug, pods)
+  -p, --profile=[]: Activate profiles by name (prefixed with `-` to disable a profile)
+      --profile-auto-activation=true: Set to false to disable profile auto activation
+      --propagate-profiles=true: Setting '--propagate-profiles=false' disables propagating profiles set by the '--profile' flag across config dependencies. This mean that only profiles defined directly in the target 'skaffold.yaml' file are activated.
       --remote-cache-dir='': Specify the location of the git repositories cache (default $HOME/.skaffold/repos)
       --rpc-http-port=: tcp port to expose the Skaffold API over HTTP REST
       --rpc-port=: tcp port to expose the Skaffold API over gRPC
@@ -1309,6 +1312,9 @@ Env vars:
 * `SKAFFOLD_FILENAME` (same as `--filename`)
 * `SKAFFOLD_MODULE` (same as `--module`)
 * `SKAFFOLD_PORT_FORWARD` (same as `--port-forward`)
+* `SKAFFOLD_PROFILE` (same as `--profile`)
+* `SKAFFOLD_PROFILE_AUTO_ACTIVATION` (same as `--profile-auto-activation`)
+* `SKAFFOLD_PROPAGATE_PROFILES` (same as `--propagate-profiles`)
 * `SKAFFOLD_REMOTE_CACHE_DIR` (same as `--remote-cache-dir`)
 * `SKAFFOLD_RPC_HTTP_PORT` (same as `--rpc-http-port`)
 * `SKAFFOLD_RPC_PORT` (same as `--rpc-port`)
diff --git a/integration/binpack/gcp.go b/integration/binpack/gcp.go
index 1704e8ea0..3ae226fe2 100644
--- a/integration/binpack/gcp.go
+++ b/integration/binpack/gcp.go
@@ -21,6 +21,7 @@ package binpack
 // GCPTimings contains the timings for tests marked with NeedsGcp
 var GCPTimings = []Timing{
 	{"TestBuildKanikoInsecureRegistry", 10.00},
+	{"TestBuildWithMultiPlatforms", 20.00},
 	{"TestBuildKanikoWithExplicitRepo", 10.00},
 	{"TestBuildInCluster", 10.00},
 	{"TestBuildGCBWithExplicitRepo", 10.00},
diff --git a/pkg/skaffold/build/docker/docker.go b/pkg/skaffold/build/docker/docker.go
index 07367d3ee..d7f17472a 100644
--- a/pkg/skaffold/build/docker/docker.go
+++ b/pkg/skaffold/build/docker/docker.go
@@ -93,7 +93,16 @@ func (b *Builder) Build(ctx context.Context, out io.Writer, a *latest.Artifact,
 
 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)
+	imgRef, err := docker.ParseReference(opts.Tag)
+	if err != nil {
+		return "", fmt.Errorf("couldn't parse image tag: %w", err)
+	}
+	imageInfoEnv := map[string]string{
+		"IMAGE_REPO": imgRef.Repo,
+		"IMAGE_NAME": imgRef.Name,
+		"IMAGE_TAG":  imgRef.Tag,
+	}
+	ba, err := docker.EvalBuildArgsWithEnv(b.cfg.Mode(), workspace, a.DockerfilePath, a.BuildArgs, opts.ExtraBuildArgs, imageInfoEnv)
 	if err != nil {
 		return "", fmt.Errorf("unable to evaluate build args: %w", err)
 	}
diff --git a/pkg/skaffold/docker/image.go b/pkg/skaffold/docker/image.go
index 9e179326d..836543ce5 100644
--- a/pkg/skaffold/docker/image.go
+++ b/pkg/skaffold/docker/image.go
@@ -312,7 +312,16 @@ func (l *localDaemon) Build(ctx context.Context, out io.Writer, workspace string
 	if err := l.CheckCompatible(a); err != nil {
 		return "", err
 	}
-	buildArgs, err := EvalBuildArgs(opts.Mode, workspace, a.DockerfilePath, a.BuildArgs, opts.ExtraBuildArgs)
+	imgRef, err := ParseReference(opts.Tag)
+	if err != nil {
+		return "", fmt.Errorf("couldn't parse image tag: %w", err)
+	}
+	imageInfoEnv := map[string]string{
+		"IMAGE_REPO": imgRef.Repo,
+		"IMAGE_NAME": imgRef.Name,
+		"IMAGE_TAG":  imgRef.Tag,
+	}
+	buildArgs, err := EvalBuildArgsWithEnv(opts.Mode, workspace, a.DockerfilePath, a.BuildArgs, opts.ExtraBuildArgs, imageInfoEnv)
 	if err != nil {
 		return "", fmt.Errorf("unable to evaluate build args: %w", err)
 	}
diff --git a/pkg/skaffold/docker/manifest_list.go b/pkg/skaffold/docker/manifest_list.go
index 27de3b1f3..c826390fe 100644
--- a/pkg/skaffold/docker/manifest_list.go
+++ b/pkg/skaffold/docker/manifest_list.go
@@ -55,7 +55,7 @@ func CreateManifestList(ctx context.Context, images []SinglePlatformImage, targe
 			return "", err
 		}
 
-		img, err := remoteImage(ref)
+		img, err := remoteImage(ref, remote.WithAuthFromKeychain(primaryKeychain))
 		if err != nil {
 			return "", err
 		}
diff --git a/pkg/skaffold/instrumentation/export.go b/pkg/skaffold/instrumentation/export.go
index 42240bf79..e5c29f761 100644
--- a/pkg/skaffold/instrumentation/export.go
+++ b/pkg/skaffold/instrumentation/export.go
@@ -182,7 +182,7 @@ func createMetrics(ctx context.Context, meter skaffoldMeter) {
 		sharedLabels = append(sharedLabels, attribute.String("user", meter.User))
 	}
 	labels = append(labels, sharedLabels...)
-
+	platformLabel := attribute.String("host_os_arch", fmt.Sprintf("%s/%s", meter.OS, meter.Arch))
 	runCounter := metric.Must(m).NewInt64ValueRecorder("launches", metric.WithDescription("Skaffold Invocations"))
 	runCounter.Record(ctx, 1, labels...)
 
@@ -194,7 +194,7 @@ func createMetrics(ctx context.Context, meter skaffoldMeter) {
 		flagMetrics(ctx, meter, m, randLabel)
 		hooksMetrics(ctx, meter, m, labels...)
 		if doesBuild.Contains(meter.Command) {
-			builderMetrics(ctx, meter, m, sharedLabels...)
+			builderMetrics(ctx, meter, m, platformLabel, sharedLabels...)
 		}
 		if doesDeploy.Contains(meter.Command) {
 			deployerMetrics(ctx, meter, m, sharedLabels...)
@@ -205,7 +205,7 @@ func createMetrics(ctx context.Context, meter skaffoldMeter) {
 	}
 
 	if meter.ErrorCode != 0 {
-		errorMetrics(ctx, meter, m, sharedLabels...)
+		errorMetrics(ctx, meter, m, append(sharedLabels, platformLabel)...)
 	}
 }
 
@@ -274,7 +274,7 @@ func resourceSelectorMetrics(ctx context.Context, meter skaffoldMeter, m metric.
 	}
 }
 
-func builderMetrics(ctx context.Context, meter skaffoldMeter, m metric.Meter, labels ...attribute.KeyValue) {
+func builderMetrics(ctx context.Context, meter skaffoldMeter, m metric.Meter, platformLabel attribute.KeyValue, labels ...attribute.KeyValue) {
 	builderCounter := metric.Must(m).NewInt64ValueRecorder("builders", metric.WithDescription("Builders used"))
 	artifactCounter := metric.Must(m).NewInt64ValueRecorder("artifacts", metric.WithDescription("Number of artifacts used"))
 	dependenciesCounter := metric.Must(m).NewInt64ValueRecorder("artifact-dependencies", metric.WithDescription("Number of artifacts with dependencies"))
@@ -284,24 +284,24 @@ func builderMetrics(ctx context.Context, meter skaffoldMeter, m metric.Meter, la
 		builderCounter.Record(ctx, 1, append(labels, bLabel)...)
 		artifactCounter.Record(ctx, int64(count), append(labels, bLabel)...)
 		dependenciesCounter.Record(ctx, int64(meter.BuildDependencies[builder]), append(labels, bLabel)...)
-		platformsCounter.Record(ctx, int64(meter.BuildWithPlatforms[builder]), append(labels, bLabel)...)
+		platformsCounter.Record(ctx, int64(meter.BuildWithPlatforms[builder]), append(labels, platformLabel, bLabel)...)
 	}
 
 	if len(meter.ResolvedBuildTargetPlatforms) > 0 {
 		platforms := metric.Must(m).NewInt64ValueRecorder("build-platforms", metric.WithDescription("The resolved build target platforms for each run"))
 		for _, buildPlatform := range meter.ResolvedBuildTargetPlatforms {
-			platforms.Record(ctx, 1, append(labels, attribute.String("os_arch", buildPlatform))...)
+			platforms.Record(ctx, 1, append(labels, platformLabel, attribute.String("os_arch", buildPlatform))...)
 		}
 	}
 
 	if len(meter.CliBuildTargetPlatforms) > 0 {
 		platforms := metric.Must(m).NewInt64ValueRecorder("cli-platforms", metric.WithDescription("The build target platforms specified via CLI flag --platform"))
-		platforms.Record(ctx, 1, append(labels, attribute.String("os_arch", meter.CliBuildTargetPlatforms))...)
+		platforms.Record(ctx, 1, append(labels, platformLabel, attribute.String("os_arch", meter.CliBuildTargetPlatforms))...)
 	}
 
 	if len(meter.DeployNodePlatforms) > 0 {
 		platforms := metric.Must(m).NewInt64ValueRecorder("node-platforms", metric.WithDescription("The kubernetes cluster node platforms"))
-		platforms.Record(ctx, 1, append(labels, attribute.String("os_arch", meter.DeployNodePlatforms))...)
+		platforms.Record(ctx, 1, append(labels, platformLabel, attribute.String("os_arch", meter.DeployNodePlatforms))...)
 	}
 }
 
diff --git a/pkg/skaffold/platform/platform.go b/pkg/skaffold/platform/platform.go
index 99a228121..1e41e901b 100644
--- a/pkg/skaffold/platform/platform.go
+++ b/pkg/skaffold/platform/platform.go
@@ -18,6 +18,7 @@ package platform
 
 import (
 	"context"
+	"fmt"
 	"strings"
 
 	"github.com/containerd/containerd/platforms"
@@ -109,9 +110,42 @@ func Parse(ps []string) (Matcher, error) {
 		if err != nil {
 			return Matcher{}, UnknownPlatformCLIFlag(p, err)
 		}
+		if err := isKnownPlatform(platform); err != nil {
+			return Matcher{}, UnknownPlatformCLIFlag(p, err)
+		}
 		sl = append(sl, platform)
 	}
 	return Matcher{Platforms: sl}, nil
 }
 
+func isKnownPlatform(p specs.Platform) error {
+	if err := isKnownOS(p.OS); err != nil {
+		return err
+	}
+	if err := isKnownArch(p.Architecture); err != nil {
+		return err
+	}
+	return nil
+}
+
+// isKnownOS returns an error if we don't know about the operating system.
+// Unexported function copied from "github.com/containerd/containerd/platforms"
+func isKnownOS(os string) error {
+	switch os {
+	case "aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos", "js", "linux", "nacl", "netbsd", "openbsd", "plan9", "solaris", "windows", "zos":
+		return nil
+	}
+	return fmt.Errorf("unknown operating system %q", os)
+}
+
+// isKnownArch returns an error if we don't know about the architecture.
+// Unexported function copied from "github.com/containerd/containerd/platforms"
+func isKnownArch(arch string) error {
+	switch arch {
+	case "386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", "ppc64", "ppc64le", "mips", "mipsle", "mips64", "mips64le", "mips64p32", "mips64p32le", "ppc", "riscv", "riscv64", "s390", "s390x", "sparc", "sparc64", "wasm":
+		return nil
+	}
+	return fmt.Errorf("unknown architecture %q", arch)
+}
+
 func Format(pl specs.Platform) string { return platforms.Format(pl) }
diff --git a/pkg/skaffold/schema/v2beta29/upgrade.go b/pkg/skaffold/schema/v2beta29/upgrade.go
index 53efb1068..60a9b11d8 100644
--- a/pkg/skaffold/schema/v2beta29/upgrade.go
+++ b/pkg/skaffold/schema/v2beta29/upgrade.go
@@ -17,15 +17,25 @@ limitations under the License.
 package v2beta29
 
 import (
+	"context"
 	"encoding/json"
+	"strings"
 
 	"github.com/pkg/errors"
 
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
 	next "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/util"
 	pkgutil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
 )
 
+var migrations = map[string]string{
+	"/deploy/kubectl":             "/manifests/rawYaml",
+	"/deploy/kustomize/paths":     "/manifests/kustomize/paths",
+	"/deploy/kustomize/buildArgs": "/manifests/kustomize/buildArgs",
+	"/deploy/helm":                "/manifests/helm",
+}
+
 // Upgrade upgrades a configuration to the next version.
 // v2beta29 is the last config version for skaffold v1, and future version will
 // follow the naming scheme of v3*
@@ -35,7 +45,24 @@ func (c *SkaffoldConfig) Upgrade() (util.VersionedConfig, error) {
 	newConfig.APIVersion = next.Version
 
 	err := util.UpgradePipelines(c, &newConfig, upgradeOnePipeline)
-	return &newConfig, err
+	if err != nil {
+		return &newConfig, err
+	}
+
+	var newProfiles []next.Profile
+	// seed with existing Profiles
+	if c.Profiles != nil {
+		pkgutil.CloneThroughJSON(newConfig.Profiles, &newProfiles)
+	}
+
+	// Update profiles patches
+	for i, p := range c.Profiles {
+		upgradePatches(p.Patches, newProfiles[i].Patches)
+	}
+
+	newConfig.Profiles = newProfiles
+
+	return &newConfig, nil
 }
 
 func upgradeOnePipeline(oldPipeline, newPipeline interface{}) error {
@@ -56,12 +83,8 @@ func upgradeOnePipeline(oldPipeline, newPipeline interface{}) error {
 		if len(newPL.Render.Kustomize.Paths) == 0 {
 			newPL.Render.Kustomize.Paths = append(newPL.Render.Kustomize.Paths, ".")
 		}
-		if len(oldPL.Deploy.KustomizeDeploy.BuildArgs) != 0 {
-			return errors.New("converting deploy.kustomize.buildArgs isn't currently supported")
-		}
 	}
 
-	// TODO(marlongamez): what should happen when migrating v2?
 	// Copy Kpt deploy config to render config
 	if oldPL.Deploy.KptDeploy != nil {
 		return errors.New("converting deploy.kpt isn't currently supported")
@@ -124,6 +147,18 @@ func upgradeOnePipeline(oldPipeline, newPipeline interface{}) error {
 		newPL.Deploy.LegacyHelmDeploy = &next.LegacyHelmDeploy{}
 		newPL.Deploy.LegacyHelmDeploy.LifecycleHooks = newHelm.LifecycleHooks
 	}
-
 	return nil
 }
+
+func upgradePatches(olds []JSONPatch, news []next.JSONPatch) {
+	for i, old := range olds {
+		for str, repStr := range migrations {
+			if strings.Contains(old.Path, str) {
+				news[i].Path = strings.ReplaceAll(old.Path, str, repStr)
+			}
+			if strings.Contains(old.Path, "/deploy/kpt") {
+				log.Entry(context.TODO()).Warn("skip migrating kpt deploy sections. Please migrate these over manually")
+			}
+		}
+	}
+}


Test patch:
diff --git a/integration/build_test.go b/integration/build_test.go
index 22e315cf7..0dee0d98f 100644
--- a/integration/build_test.go
+++ b/integration/build_test.go
@@ -25,13 +25,16 @@ import (
 	"time"
 
 	"4d63.com/tz"
+	v1 "github.com/opencontainers/image-spec/specs-go/v1"
 
+	"github.com/GoogleContainerTools/skaffold/cmd/skaffold/app/flags"
 	"github.com/GoogleContainerTools/skaffold/integration/skaffold"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/jib"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
 	kubectx "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/context"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
+	"github.com/GoogleContainerTools/skaffold/testutil"
 )
 
 const imageName = "gcr.io/k8s-skaffold/simple-build:"
@@ -109,9 +112,79 @@ func TestBuild(t *testing.T) {
 	}
 }
 
+func TestBuildWithWithPlatform(t *testing.T) {
+	MarkIntegrationTest(t, CanRunWithoutGcp)
+
+	tests := []struct {
+		description       string
+		dir               string
+		args              []string
+		image             string
+		expectedPlatforms []v1.Platform
+	}{
+		{
+			description:       "docker build linux/amd64",
+			dir:               "testdata/build/docker-with-platform-amd",
+			args:              []string{"--platform", "linux/amd64"},
+			expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "amd64"}},
+		},
+		{
+			description:       "docker build linux/arm64",
+			dir:               "testdata/build/docker-with-platform-arm",
+			args:              []string{"--platform", "linux/arm64"},
+			expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.description, func(t *testing.T) {
+			tmpfile := testutil.TempFile(t, "", []byte{})
+			args := append(test.args, "--file-output", tmpfile)
+			skaffold.Build(args...).InDir(test.dir).RunOrFail(t)
+			bytes, err := os.ReadFile(tmpfile)
+			failNowIfError(t, err)
+			buildArtifacts, err := flags.ParseBuildOutput(bytes)
+			failNowIfError(t, err)
+			checkLocalImagePlatforms(t, buildArtifacts.Builds[0].Tag, test.expectedPlatforms)
+		})
+	}
+}
+
+func TestBuildWithMultiPlatforms(t *testing.T) {
+	MarkIntegrationTest(t, NeedsGcp)
+
+	tests := []struct {
+		description       string
+		dir               string
+		args              []string
+		image             string
+		expectedPlatforms []v1.Platform
+	}{
+		{
+			description:       "build cross platform images with gcb",
+			dir:               "testdata/build/gcb-with-platform",
+			args:              []string{"--platform", "linux/arm64,linux/amd64"},
+			expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}, {OS: "linux", Architecture: "amd64"}},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.description, func(t *testing.T) {
+			tmpfile := testutil.TempFile(t, "", []byte{})
+			args := append(test.args, "--file-output", tmpfile)
+			skaffold.Build(args...).InDir(test.dir).RunOrFail(t)
+			bytes, err := os.ReadFile(tmpfile)
+			failNowIfError(t, err)
+			buildArtifacts, err := flags.ParseBuildOutput(bytes)
+			failNowIfError(t, err)
+			checkRemoteImagePlatforms(t, buildArtifacts.Builds[0].Tag, test.expectedPlatforms)
+		})
+	}
+}
+
 // TestExpectedBuildFailures verifies that `skaffold build` fails in expected ways
 func TestExpectedBuildFailures(t *testing.T) {
-	MarkIntegrationTest(t, CanRunWithoutGcp)
+	MarkIntegrationTest(t, NeedsGcp)
 	if !jib.JVMFound(context.Background()) {
 		t.Fatal("test requires Java VM")
 	}
@@ -142,6 +215,59 @@ func TestExpectedBuildFailures(t *testing.T) {
 	}
 }
 
+func checkLocalImagePlatforms(t *testing.T, image string, expected []v1.Platform) {
+	if expected == nil {
+		return
+	}
+	t.Helper()
+
+	cfg, err := kubectx.CurrentConfig()
+	failNowIfError(t, err)
+
+	client, err := docker.NewAPIClient(context.Background(), &runcontext.RunContext{
+		KubeContext: cfg.CurrentContext,
+	})
+	failNowIfError(t, err)
+	inspect, _, err := client.ImageInspectWithRaw(context.Background(), image)
+	failNowIfError(t, err)
+
+	actual := []v1.Platform{{Architecture: inspect.Architecture, OS: inspect.Os}}
+	checkPlatformsEqual(t, actual, expected)
+}
+
+func checkRemoteImagePlatforms(t *testing.T, image string, expected []v1.Platform) {
+	if expected == nil {
+		return
+	}
+	t.Helper()
+	actual, err := docker.GetPlatforms(image)
+	if err != nil {
+		t.Error(err)
+	}
+	checkPlatformsEqual(t, actual, expected)
+}
+
+func checkPlatformsEqual(t *testing.T, actual, expected []v1.Platform) {
+	expectedLen := len(expected)
+	actualLen := len(actual)
+
+	if expectedLen != actualLen {
+		t.Fatalf("length of the slices differ: Expected %d, but was %d", expectedLen, actualLen)
+	}
+	wmap := make(map[string]int)
+	for _, elem := range expected {
+		wmap[elem.OS+"/"+elem.Architecture]++
+	}
+	for _, elem := range actual {
+		wmap[elem.OS+"/"+elem.Architecture]--
+	}
+	for _, v := range wmap {
+		if v != 0 {
+			t.Fatalf("elements are missing (negative integers) or excess (positive integers): %#v", wmap)
+		}
+	}
+}
+
 // checkImageExists asserts that the given image is present
 func checkImageExists(t *testing.T, image string) {
 	t.Helper()
diff --git a/integration/testdata/build/docker-with-platform-amd/Dockerfile b/integration/testdata/build/docker-with-platform-amd/Dockerfile
new file mode 100644
index 000000000..8f6762484
--- /dev/null
+++ b/integration/testdata/build/docker-with-platform-amd/Dockerfile
@@ -0,0 +1,7 @@
+FROM golang:1.15-alpine as builder
+COPY main.go .
+RUN go build -o /app main.go
+
+FROM alpine:3
+CMD ["./app"]
+COPY --from=builder /app .
diff --git a/integration/testdata/build/docker-with-platform-amd/k8s/deployment.yaml b/integration/testdata/build/docker-with-platform-amd/k8s/deployment.yaml
new file mode 100644
index 000000000..f1c71fd8b
--- /dev/null
+++ b/integration/testdata/build/docker-with-platform-amd/k8s/deployment.yaml
@@ -0,0 +1,18 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: getting-started
+  labels:
+    app: getting-started
+spec:
+  selector:
+    matchLabels:
+      app: getting-started
+  template:
+    metadata:
+      labels:
+        app: getting-started
+    spec:
+      containers:
+      - name: getting-started
+        image: skaffold-hello
diff --git a/integration/testdata/build/docker-with-platform-amd/k8s/pod.yaml b/integration/testdata/build/docker-with-platform-amd/k8s/pod.yaml
new file mode 100644
index 000000000..f80ddcdf6
--- /dev/null
+++ b/integration/testdata/build/docker-with-platform-amd/k8s/pod.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Pod
+metadata:
+  name: getting-started
+spec:
+  containers:
+  - name: getting-started
+    image: skaffold-hello
diff --git a/integration/testdata/build/docker-with-platform-amd/main.go b/integration/testdata/build/docker-with-platform-amd/main.go
new file mode 100644
index 000000000..8c3e45267
--- /dev/null
+++ b/integration/testdata/build/docker-with-platform-amd/main.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	for counter := 0; ; counter++ {
+		fmt.Println("Hello world!", counter)
+
+		time.Sleep(time.Second * 1)
+	}
+}
diff --git a/integration/testdata/build/docker-with-platform-amd/skaffold.yaml b/integration/testdata/build/docker-with-platform-amd/skaffold.yaml
new file mode 100644
index 000000000..8ed4fda2d
--- /dev/null
+++ b/integration/testdata/build/docker-with-platform-amd/skaffold.yaml
@@ -0,0 +1,15 @@
+apiVersion: skaffold/v3alpha1
+kind: Config
+build:
+  artifacts:
+  - image: docker-with-platform-amd
+
+profiles:
+  - name: pod
+    manifests:
+      rawYaml:
+      - k8s/pod.yaml
+  - name: deployment
+    manifests:
+      rawYaml:
+      - k8s/deployment.yaml
\ No newline at end of file
diff --git a/integration/testdata/build/docker-with-platform-arm/Dockerfile b/integration/testdata/build/docker-with-platform-arm/Dockerfile
new file mode 100644
index 000000000..8f6762484
--- /dev/null
+++ b/integration/testdata/build/docker-with-platform-arm/Dockerfile
@@ -0,0 +1,7 @@
+FROM golang:1.15-alpine as builder
+COPY main.go .
+RUN go build -o /app main.go
+
+FROM alpine:3
+CMD ["./app"]
+COPY --from=builder /app .
diff --git a/integration/testdata/build/docker-with-platform-arm/k8s/deployment.yaml b/integration/testdata/build/docker-with-platform-arm/k8s/deployment.yaml
new file mode 100644
index 000000000..f1c71fd8b
--- /dev/null
+++ b/integration/testdata/build/docker-with-platform-arm/k8s/deployment.yaml
@@ -0,0 +1,18 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: getting-started
+  labels:
+    app: getting-started
+spec:
+  selector:
+    matchLabels:
+      app: getting-started
+  template:
+    metadata:
+      labels:
+        app: getting-started
+    spec:
+      containers:
+      - name: getting-started
+        image: skaffold-hello
diff --git a/integration/testdata/build/docker-with-platform-arm/k8s/pod.yaml b/integration/testdata/build/docker-with-platform-arm/k8s/pod.yaml
new file mode 100644
index 000000000..f80ddcdf6
--- /dev/null
+++ b/integration/testdata/build/docker-with-platform-arm/k8s/pod.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Pod
+metadata:
+  name: getting-started
+spec:
+  containers:
+  - name: getting-started
+    image: skaffold-hello
diff --git a/integration/testdata/build/docker-with-platform-arm/main.go b/integration/testdata/build/docker-with-platform-arm/main.go
new file mode 100644
index 000000000..8c3e45267
--- /dev/null
+++ b/integration/testdata/build/docker-with-platform-arm/main.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	for counter := 0; ; counter++ {
+		fmt.Println("Hello world!", counter)
+
+		time.Sleep(time.Second * 1)
+	}
+}
diff --git a/integration/testdata/build/docker-with-platform-arm/skaffold.yaml b/integration/testdata/build/docker-with-platform-arm/skaffold.yaml
new file mode 100644
index 000000000..2baaaf30d
--- /dev/null
+++ b/integration/testdata/build/docker-with-platform-arm/skaffold.yaml
@@ -0,0 +1,15 @@
+apiVersion: skaffold/v3alpha1
+kind: Config
+build:
+  artifacts:
+  - image: docker-with-platform-arm
+
+profiles:
+  - name: pod
+    manifests:
+      rawYaml:
+      - k8s/pod.yaml
+  - name: deployment
+    manifests:
+      rawYaml:
+      - k8s/deployment.yaml
\ No newline at end of file
diff --git a/integration/testdata/build/gcb-with-platform/Dockerfile b/integration/testdata/build/gcb-with-platform/Dockerfile
new file mode 100644
index 000000000..67f8c3cbb
--- /dev/null
+++ b/integration/testdata/build/gcb-with-platform/Dockerfile
@@ -0,0 +1,14 @@
+FROM golang:1.18 as builder
+WORKDIR /code
+COPY main.go .
+COPY go.mod .
+# `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
+ARG SKAFFOLD_GO_GCFLAGS
+RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -trimpath -o /app main.go
+
+FROM alpine:3
+# Define GOTRACEBACK to mark this container as using the Go language runtime
+# for `skaffold debug` (https://skaffold.dev/docs/workflows/debug/).
+ENV GOTRACEBACK=single
+CMD ["./app"]
+COPY --from=builder /app .
diff --git a/integration/testdata/build/gcb-with-platform/go.mod b/integration/testdata/build/gcb-with-platform/go.mod
new file mode 100644
index 000000000..adee4fb0a
--- /dev/null
+++ b/integration/testdata/build/gcb-with-platform/go.mod
@@ -0,0 +1,3 @@
+module github.com/GoogleContainerTools/skaffold/examples/google-cloud-build
+
+go 1.18
diff --git a/integration/testdata/build/gcb-with-platform/k8s-pod.yaml b/integration/testdata/build/gcb-with-platform/k8s-pod.yaml
new file mode 100644
index 000000000..e8e559450
--- /dev/null
+++ b/integration/testdata/build/gcb-with-platform/k8s-pod.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Pod
+metadata:
+  name: getting-started
+spec:
+  containers:
+  - name: getting-started
+    image: gcr.io/k8s-skaffold/skaffold-example-with-location
diff --git a/integration/testdata/build/gcb-with-platform/main.go b/integration/testdata/build/gcb-with-platform/main.go
new file mode 100644
index 000000000..593721cfe
--- /dev/null
+++ b/integration/testdata/build/gcb-with-platform/main.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	for {
+		fmt.Println("Hello world!")
+
+		time.Sleep(time.Second * 1)
+	}
+}
diff --git a/integration/testdata/build/gcb-with-platform/skaffold.yaml b/integration/testdata/build/gcb-with-platform/skaffold.yaml
new file mode 100644
index 000000000..79658f19a
--- /dev/null
+++ b/integration/testdata/build/gcb-with-platform/skaffold.yaml
@@ -0,0 +1,11 @@
+apiVersion: skaffold/v3alpha1
+kind: Config
+build:
+  artifacts:
+    - image: gcr.io/k8s-skaffold/gcb-with-platform
+  googleCloudBuild:
+    projectId: k8s-skaffold
+    region: asia-east1
+manifests:
+  rawYaml:
+    - k8s-*
\ No newline at end of file
diff --git a/pkg/skaffold/build/cache/retrieve_test.go b/pkg/skaffold/build/cache/retrieve_test.go
index 4d4ae6730..125aeb881 100644
--- a/pkg/skaffold/build/cache/retrieve_test.go
+++ b/pkg/skaffold/build/cache/retrieve_test.go
@@ -142,6 +142,9 @@ func TestCacheBuildLocal(t *testing.T) {
 		t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
 			return args, nil
 		})
+		t.Override(&docker.EvalBuildArgsWithEnv, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string, _ map[string]string) (map[string]*string, error) {
+			return args, nil
+		})
 
 		// Create cache
 		cfg := &mockConfig{
@@ -239,6 +242,9 @@ func TestCacheBuildRemote(t *testing.T) {
 		t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
 			return args, nil
 		})
+		t.Override(&docker.EvalBuildArgsWithEnv, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string, _ map[string]string) (map[string]*string, error) {
+			return args, nil
+		})
 
 		// Create cache
 		cfg := &mockConfig{
@@ -324,6 +330,9 @@ func TestCacheFindMissing(t *testing.T) {
 		t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
 			return args, nil
 		})
+		t.Override(&docker.EvalBuildArgsWithEnv, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string, _ map[string]string) (map[string]*string, error) {
+			return args, nil
+		})
 
 		// Create cache
 		cfg := &mockConfig{
diff --git a/pkg/skaffold/build/docker/docker_test.go b/pkg/skaffold/build/docker/docker_test.go
index b46212cc2..a7c4ec1cd 100644
--- a/pkg/skaffold/build/docker/docker_test.go
+++ b/pkg/skaffold/build/docker/docker_test.go
@@ -134,7 +134,7 @@ func TestDockerCLIBuild(t *testing.T) {
 		testutil.Run(t, test.description, func(t *testutil.T) {
 			t.NewTempDir().Touch("Dockerfile").Chdir()
 			dockerfilePath, _ := filepath.Abs("Dockerfile")
-			t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
+			t.Override(&docker.EvalBuildArgsWithEnv, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string, _ map[string]string) (map[string]*string, error) {
 				return args, nil
 			})
 			t.Override(&docker.DefaultAuthHelper, stubAuth{})
@@ -233,7 +233,7 @@ func TestDockerCLICheckCacheFromArgs(t *testing.T) {
 			a.Workspace = "."
 			a.DockerArtifact.DockerfilePath = dockerfilePath
 			t.Override(&docker.DefaultAuthHelper, stubAuth{})
-			t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
+			t.Override(&docker.EvalBuildArgsWithEnv, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string, _ map[string]string) (map[string]*string, error) {
 				return args, nil
 			})
 
diff --git a/pkg/skaffold/build/docker/errors_test.go b/pkg/skaffold/build/docker/errors_test.go
index ccc694698..6f92bdad2 100644
--- a/pkg/skaffold/build/docker/errors_test.go
+++ b/pkg/skaffold/build/docker/errors_test.go
@@ -53,7 +53,7 @@ Refer https://skaffold.dev/docs/references/yaml/#build-artifacts-docker for deta
 		testutil.Run(t, test.description, func(t *testutil.T) {
 			t.NewTempDir().Touch("Dockerfile").Chdir()
 			dockerfilePath, _ := filepath.Abs("Dockerfile")
-			t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
+			t.Override(&docker.EvalBuildArgsWithEnv, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string, _ map[string]string) (map[string]*string, error) {
 				return args, nil
 			})
 			t.Override(&util.DefaultExecCommand, testutil.CmdRun(
diff --git a/pkg/skaffold/build/local/local_test.go b/pkg/skaffold/build/local/local_test.go
index 8cd330e73..b9bc45b17 100644
--- a/pkg/skaffold/build/local/local_test.go
+++ b/pkg/skaffold/build/local/local_test.go
@@ -228,7 +228,7 @@ func TestLocalRun(t *testing.T) {
 			t.Override(&docker.NewAPIClient, func(context.Context, docker.Config) (docker.LocalDaemon, error) {
 				return fakeLocalDaemon(test.api), nil
 			})
-			t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
+			t.Override(&docker.EvalBuildArgsWithEnv, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string, _ map[string]string) (map[string]*string, error) {
 				return args, nil
 			})
 			testEvent.InitializeState([]latest.Pipeline{{
@@ -402,7 +402,7 @@ func TestGetArtifactBuilder(t *testing.T) {
 			t.Override(&docker.NewAPIClient, func(context.Context, docker.Config) (docker.LocalDaemon, error) {
 				return fakeLocalDaemon(&testutil.FakeAPIClient{}), nil
 			})
-			t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
+			t.Override(&docker.EvalBuildArgsWithEnv, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string, _ map[string]string) (map[string]*string, error) {
 				return args, nil
 			})
 
diff --git a/pkg/skaffold/docker/image_test.go b/pkg/skaffold/docker/image_test.go
index 7467a86d9..8a6e4090d 100644
--- a/pkg/skaffold/docker/image_test.go
+++ b/pkg/skaffold/docker/image_test.go
@@ -195,7 +195,7 @@ func TestBuild(t *testing.T) {
 	for _, test := range tests {
 		testutil.Run(t, test.description, func(t *testutil.T) {
 			t.Override(&DefaultAuthHelper, testAuthHelper{})
-			t.Override(&EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
+			t.Override(&EvalBuildArgsWithEnv, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string, _ map[string]string) (map[string]*string, error) {
 				return util.EvaluateEnvTemplateMap(args)
 			})
 			t.SetEnvs(test.env)
diff --git a/pkg/skaffold/platform/platform_test.go b/pkg/skaffold/platform/platform_test.go
index d3c90dfb7..df60cb9bc 100644
--- a/pkg/skaffold/platform/platform_test.go
+++ b/pkg/skaffold/platform/platform_test.go
@@ -253,3 +253,33 @@ func TestIntersect(t *testing.T) {
 		})
 	}
 }
+
+func TestParse(t *testing.T) {
+	tests := []struct {
+		description string
+		input       []string
+		shouldErr   bool
+	}{
+		{
+			description: "known platforms",
+			input:       []string{"linux/arm64", "darwin/amd64", "freebsd/arm"},
+			shouldErr:   false,
+		},
+		{
+			description: "unknown os",
+			input:       []string{"foo/arm64", "darwin/amd64", "freebsd/arm"},
+			shouldErr:   true,
+		},
+		{
+			description: "unknown arch",
+			input:       []string{"linux/arm64", "darwin/foo", "freebsd/arm"},
+			shouldErr:   true,
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			_, err := Parse(test.input)
+			t.CheckError(test.shouldErr, err)
+		})
+	}
+}
diff --git a/pkg/skaffold/schema/v2beta29/upgrade_test.go b/pkg/skaffold/schema/v2beta29/upgrade_test.go
index dbc5dbff7..3e3c79a34 100644
--- a/pkg/skaffold/schema/v2beta29/upgrade_test.go
+++ b/pkg/skaffold/schema/v2beta29/upgrade_test.go
@@ -223,3 +223,57 @@ func verifyUpgrade(t *testing.T, input, output string) {
 
 	testutil.CheckErrorAndDeepEqual(t, false, err, expected, upgraded)
 }
+
+func TestUpgradePatches(t *testing.T) {
+	yaml := `apiVersion: skaffold/v2beta29
+kind: Config
+build:
+  artifacts:
+  - image: skaffold-debug-java
+    context: java
+deploy:
+  kubectl:
+    manifests:
+    - java/k8s/web.yaml
+  helm:
+   releases:
+   - name: test
+profiles:
+- name: kustomize
+  patches:
+  - op: remove
+    path: /deploy/kubectl
+  - op: remove
+    path: /deploy/helm
+  deploy:
+    kustomize: {}
+`
+	expected := `apiVersion: skaffold/v3alpha1
+kind: Config
+build:
+  artifacts:
+  - image: skaffold-debug-java
+    context: java
+manifests:
+  rawYaml:
+    - java/k8s/web.yaml
+  helm:
+   releases:
+   - name: test
+deploy:
+    helm: {}
+    kubectl: {}
+profiles:
+- name: kustomize
+  patches:
+  - op: remove
+    path: /manifests/rawYaml
+  - op: remove
+    path: /manifests/helm
+  manifests:
+    kustomize:
+      paths:
+      - "."
+`
+	verifyUpgrade(t, yaml, expected)
+}