Repository: GoogleContainerTools/skaffold
Pull request #7650 (2022-07-15 08:02:23)

feat(ko): Google Cloud Build remote builds for the ko builder
Enables remote builds on Google Cloud Build when using the Skaffold `ko` builder.

Since Skaffold embeds ko as a module, the Cloud Build step uses the `skaffold` binary to build and push the image. Since the `gcr.io/k8s-skaffold/skaffold` image contains the Go distribution, we can use this image on Cloud Build. This means that we don't need to create and maintain a separate `ko` builder image for Cloud Build.

Tracking: #7131
Related: #7258


codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7650?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) Report
> Merging [#7650](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7650?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (715b585) 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.89%`.
> The diff coverage is `53.95%`.

```diff
@@            Coverage Diff             @@
##             main    #7650      +/-   ##
==========================================
- Coverage   70.48%   66.58%   -3.90%     
==========================================
  Files         515      585      +70     
  Lines       23150    28007    +4857     
==========================================
+ Hits        16317    18649    +2332     
- Misses       5776     7994    +2218     
- Partials     1057     1364     +307     
```


| [Impacted Files](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7650?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/7650/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/7650/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/7650/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/7650/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/7650/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/7650/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/7650/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/7650/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/7650/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZml4Lmdv) | `55.93% <38.88%> (-20.54%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/verify.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7650/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 [324 more](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7650/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/docs-v2/content/en/docs/pipeline-stages/builders/_index.md b/docs-v2/content/en/docs/pipeline-stages/builders/_index.md
index 9be23b86f..76c7f6bf4 100644
--- a/docs-v2/content/en/docs/pipeline-stages/builders/_index.md
+++ b/docs-v2/content/en/docs/pipeline-stages/builders/_index.md
@@ -15,7 +15,7 @@ Skaffold supports different tools for building images:
 | **Jib Maven and Gradle** | [Yes]({{< relref "/docs/pipeline-stages/builders/jib#jib-maven-and-gradle-locally" >}}) | - | [Yes]({{< relref "/docs/pipeline-stages/builders/jib#remotely-with-google-cloud-build" >}}) |
 | **Cloud Native Buildpacks** | [Yes]({{< relref "/docs/pipeline-stages/builders/buildpacks" >}}) | - | [Yes]({{< relref "/docs/pipeline-stages/builders/buildpacks" >}}) |
 | **Bazel** | [Yes]({{< relref "/docs/pipeline-stages/builders/bazel" >}}) | - | - |
-| **ko** | [Yes]({{< relref "/docs/pipeline-stages/builders/ko" >}}) | - | - |
+| **ko** | [Yes]({{< relref "/docs/pipeline-stages/builders/ko" >}}) | - | [Yes]({{< relref "/docs/pipeline-stages/builders/ko#remote-builds" >}}) |
 | **Custom Script** | [Yes]({{<relref "/docs/pipeline-stages/builders/custom#custom-build-script-locally" >}}) | [Yes]({{<relref "/docs/pipeline-stages/builders/custom#custom-build-script-in-cluster" >}}) | - |
 
 **Configuration**
@@ -184,13 +184,13 @@ The final list of target platforms need to ultimately be supported by the target
 
 |    | Local Build | In Cluster Build | Remote on Google Cloud Build |
 |----|:-----------:|:----------------:|:----------------------------:|
-| **Dockerfile** | Cross platform supported | Cross platform supported but platform should match cluster node running the pod. | Cross platform supported |
-| **Jib Maven and Gradle** | Cross platform supported | - | Cross platform supported |
+| **Dockerfile** | Cross-platform supported | Cross-platform supported but platform should match cluster node running the pod. | Cross-platform supported |
+| **Jib Maven and Gradle** | Cross-platform and multi-platform supported | - | Cross-platform and multi-platform supported |
 | **Cloud Native Buildpacks** | Only supports `linux/amd64` | - | Only supports `linux/amd64` |
-| **Bazel** | Cross platform supported but requires explicit platform specific rules. Not yet implemented | - | - |
-| **ko** | Cross platform supported | - | - |
-| **Custom Script** | Cross platform supported but requires user to implement it in the build script | Cross platform supported but requires user to implement it in the build script | - |
+| **Bazel** | Cross-platform supported but requires explicit platform specific rules. Not yet implemented | - | - |
+| **ko** | Cross-platform and multi-platform supported | - | Cross-platform and multi-platform supported |
+| **Custom Script** | Cross-platform and multi-platform supported but requires user to implement it in the build script | Cross-platform and multi-platform supported but requires user to implement it in the build script | - |
 
 {{< alert title="Note" >}}
-Multi-arch image build is not yet supported for any builders other than the [jib builder]({{<relref "/docs/pipeline-stages/builders/jib" >}}), the [ko builder]({{<relref "/docs/pipeline-stages/builders/ko">}}) and the [custom builder]({{<relref "/docs/pipeline-stages/builders/custom" >}}) in Skaffold. 
-{{< /alert >}}
\ No newline at end of file
+Skaffold supports multi-platform image builds using the [jib builder]({{<relref "/docs/pipeline-stages/builders/jib" >}}), the [ko builder]({{<relref "/docs/pipeline-stages/builders/ko">}}) and the [custom builder]({{<relref "/docs/pipeline-stages/builders/custom" >}}).
+{{< /alert >}}
diff --git a/docs-v2/content/en/docs/pipeline-stages/builders/ko.md b/docs-v2/content/en/docs/pipeline-stages/builders/ko.md
index e9ded288e..af0994be8 100644
--- a/docs-v2/content/en/docs/pipeline-stages/builders/ko.md
+++ b/docs-v2/content/en/docs/pipeline-stages/builders/ko.md
@@ -411,9 +411,10 @@ To learn more about how Skaffold debugs Go applications, read the
 
 File `sync` is not supported while the ko builder feature is in Alpha.
 
-### Remote builders
+### Remote builds
 
-Only `local` builds are supported while the ko builder feature is in Alpha.
+The `ko` builder supports remote builds on Google Cloud Build. See the
+[example](https://github.com/GoogleContainerTools/skaffold/tree/main/examples/ko).
 
 ### Using the `custom` builder
 
diff --git a/docs/content/en/docs/pipeline-stages/builders/_index.md b/docs/content/en/docs/pipeline-stages/builders/_index.md
index 9be23b86f..76c7f6bf4 100644
--- a/docs/content/en/docs/pipeline-stages/builders/_index.md
+++ b/docs/content/en/docs/pipeline-stages/builders/_index.md
@@ -15,7 +15,7 @@ Skaffold supports different tools for building images:
 | **Jib Maven and Gradle** | [Yes]({{< relref "/docs/pipeline-stages/builders/jib#jib-maven-and-gradle-locally" >}}) | - | [Yes]({{< relref "/docs/pipeline-stages/builders/jib#remotely-with-google-cloud-build" >}}) |
 | **Cloud Native Buildpacks** | [Yes]({{< relref "/docs/pipeline-stages/builders/buildpacks" >}}) | - | [Yes]({{< relref "/docs/pipeline-stages/builders/buildpacks" >}}) |
 | **Bazel** | [Yes]({{< relref "/docs/pipeline-stages/builders/bazel" >}}) | - | - |
-| **ko** | [Yes]({{< relref "/docs/pipeline-stages/builders/ko" >}}) | - | - |
+| **ko** | [Yes]({{< relref "/docs/pipeline-stages/builders/ko" >}}) | - | [Yes]({{< relref "/docs/pipeline-stages/builders/ko#remote-builds" >}}) |
 | **Custom Script** | [Yes]({{<relref "/docs/pipeline-stages/builders/custom#custom-build-script-locally" >}}) | [Yes]({{<relref "/docs/pipeline-stages/builders/custom#custom-build-script-in-cluster" >}}) | - |
 
 **Configuration**
@@ -184,13 +184,13 @@ The final list of target platforms need to ultimately be supported by the target
 
 |    | Local Build | In Cluster Build | Remote on Google Cloud Build |
 |----|:-----------:|:----------------:|:----------------------------:|
-| **Dockerfile** | Cross platform supported | Cross platform supported but platform should match cluster node running the pod. | Cross platform supported |
-| **Jib Maven and Gradle** | Cross platform supported | - | Cross platform supported |
+| **Dockerfile** | Cross-platform supported | Cross-platform supported but platform should match cluster node running the pod. | Cross-platform supported |
+| **Jib Maven and Gradle** | Cross-platform and multi-platform supported | - | Cross-platform and multi-platform supported |
 | **Cloud Native Buildpacks** | Only supports `linux/amd64` | - | Only supports `linux/amd64` |
-| **Bazel** | Cross platform supported but requires explicit platform specific rules. Not yet implemented | - | - |
-| **ko** | Cross platform supported | - | - |
-| **Custom Script** | Cross platform supported but requires user to implement it in the build script | Cross platform supported but requires user to implement it in the build script | - |
+| **Bazel** | Cross-platform supported but requires explicit platform specific rules. Not yet implemented | - | - |
+| **ko** | Cross-platform and multi-platform supported | - | Cross-platform and multi-platform supported |
+| **Custom Script** | Cross-platform and multi-platform supported but requires user to implement it in the build script | Cross-platform and multi-platform supported but requires user to implement it in the build script | - |
 
 {{< alert title="Note" >}}
-Multi-arch image build is not yet supported for any builders other than the [jib builder]({{<relref "/docs/pipeline-stages/builders/jib" >}}), the [ko builder]({{<relref "/docs/pipeline-stages/builders/ko">}}) and the [custom builder]({{<relref "/docs/pipeline-stages/builders/custom" >}}) in Skaffold. 
-{{< /alert >}}
\ No newline at end of file
+Skaffold supports multi-platform image builds using the [jib builder]({{<relref "/docs/pipeline-stages/builders/jib" >}}), the [ko builder]({{<relref "/docs/pipeline-stages/builders/ko">}}) and the [custom builder]({{<relref "/docs/pipeline-stages/builders/custom" >}}).
+{{< /alert >}}
diff --git a/docs/content/en/docs/pipeline-stages/builders/ko.md b/docs/content/en/docs/pipeline-stages/builders/ko.md
index e9ded288e..af0994be8 100644
--- a/docs/content/en/docs/pipeline-stages/builders/ko.md
+++ b/docs/content/en/docs/pipeline-stages/builders/ko.md
@@ -411,9 +411,10 @@ To learn more about how Skaffold debugs Go applications, read the
 
 File `sync` is not supported while the ko builder feature is in Alpha.
 
-### Remote builders
+### Remote builds
 
-Only `local` builds are supported while the ko builder feature is in Alpha.
+The `ko` builder supports remote builds on Google Cloud Build. See the
+[example](https://github.com/GoogleContainerTools/skaffold/tree/main/examples/ko).
 
 ### Using the `custom` builder
 
diff --git a/integration/examples/ko/skaffold.yaml b/integration/examples/ko/skaffold.yaml
index f4e3ac231..49c5a8f35 100644
--- a/integration/examples/ko/skaffold.yaml
+++ b/integration/examples/ko/skaffold.yaml
@@ -17,4 +17,13 @@ kind: Config
 build:
   artifacts:
   - image: skaffold-ko
-    ko: {}
+    ko:
+      dependencies:
+        paths:
+        - "**/*.go"
+        - go.*
+profiles:
+- name: gcb
+  build:
+    googleCloudBuild:
+      koImage: gcr.io/k8s-skaffold/skaffold:v1.37.2-lts
diff --git a/pkg/skaffold/build/gcb/ko.go b/pkg/skaffold/build/gcb/ko.go
new file mode 100644
index 000000000..7348d3d03
--- /dev/null
+++ b/pkg/skaffold/build/gcb/ko.go
@@ -0,0 +1,143 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package gcb
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"google.golang.org/api/cloudbuild/v1"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
+	schema "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/v2beta28"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/yaml"
+)
+
+// koBuildSpec creates a Cloud Build configuration using the `ko` builder.
+//
+// Because Skaffold embeds ko as a library, the `skaffold` binary builds the container image on Cloud Build.
+//
+// The method uses the artifact input argument to generate a Skaffold Config manifest.
+func (b *Builder) koBuildSpec(ctx context.Context, artifact *latest.Artifact, tag string, platforms platform.Matcher) (cloudbuild.Build, error) {
+	imageName, imageTag := splitImageNameAndTag(tag)
+	insecureRegistries := getKeys(b.cfg.GetInsecureRegistries())
+	skaffoldConfig := createSkaffoldConfig(artifact, imageName, platforms.Array(), insecureRegistries)
+	skaffoldYaml, err := yaml.Marshal(skaffoldConfig)
+	if err != nil {
+		return cloudbuild.Build{}, fmt.Errorf("marshalling Skaffold Config YAML for %s: %w", tag, err)
+	}
+	log.Entry(ctx).Debugf("Skaffold config for Cloud Build:\n%s\n", skaffoldYaml)
+	verbosity := log.GetLevel().String()
+	return cloudBuildConfig(b.KoImage, skaffoldYaml, imageTag, verbosity, artifact.KoArtifact.Env...), nil
+}
+
+func splitImageNameAndTag(imageNameWithTag string) (string, string) {
+	nameAndTag := strings.Split(imageNameWithTag, ":")
+	imageName := nameAndTag[0]
+	imageTag := "latest"
+	if len(nameAndTag) > 1 {
+		imageTag = nameAndTag[1]
+	}
+	return imageName, imageTag
+}
+
+func getKeys(in map[string]bool) []string {
+	var keys []string
+	for v := range in {
+		keys = append(keys, v)
+	}
+	return keys
+}
+
+// createSkaffoldConfig creates a Skaffold Config manifest for use in a build step on Cloud Build.
+//
+// The manifest uses a specific schema version that is known to be supported by a public Skaffold image on GCR/AR.
+// The schema version must be a version supported by a public Skaffold image.
+// The latest schema version is not used for two reasons:
+//
+// 1. There could be a mismatch between the versions of Skaffold used locally and on Cloud Build.
+//
+//	For instance, a team could choose to use an LTS image for builds on Cloud Build.
+//
+// 2. The local version of Skaffold may not be available as a public image on GCR/AR.
+//
+// The manifest does not include artifact fields that are irrelevant for a remote build, such as Dependencies, LifecycleHooks, and Sync.
+func createSkaffoldConfig(artifact *latest.Artifact, imageName string, platforms []string, insecureRegistries []string) *schema.SkaffoldConfig {
+	return &schema.SkaffoldConfig{
+		APIVersion: schema.Version,
+		Kind:       "Config",
+		Pipeline: schema.Pipeline{
+			Build: schema.BuildConfig{
+				Artifacts: []*schema.Artifact{{
+					// Replace `ImageName` since we need the fully resolved name (with the Skaffold default repo).
+					ImageName: imageName,
+					// Copy values from the `artifact` function argument.
+					ArtifactType: schema.ArtifactType{
+						KoArtifact: &schema.KoArtifact{
+							BaseImage: artifact.KoArtifact.BaseImage,
+							Dir:       artifact.KoArtifact.Dir,
+							Env:       artifact.KoArtifact.Env,
+							Flags:     artifact.KoArtifact.Flags,
+							Labels:    artifact.KoArtifact.Labels,
+							Ldflags:   artifact.KoArtifact.Ldflags,
+							Main:      artifact.KoArtifact.Main,
+						},
+					},
+					Platforms: artifact.Platforms, // platforms defined for the artifact
+					Workspace: artifact.Workspace,
+				}},
+				InsecureRegistries: insecureRegistries,
+				Platforms:          platforms, // platforms provided via command-line flag, or envvar
+			},
+		},
+	}
+}
+
+// cloudBuildConfig creates a single step build configuraration using the provided image and Skaffold config.
+//
+// The build step writes out the generated Skaffold Config manifest to a temporary file.
+// Skaffold uses this Config to build and push the image.
+func cloudBuildConfig(koImage string, skaffoldYaml []byte, imageTag string, verbosity string, env ...string) cloudbuild.Build {
+	return cloudbuild.Build{
+		Steps: []*cloudbuild.BuildStep{{
+			Name:       koImage,
+			Entrypoint: "sh",
+			Args: []string{"-c", strings.Join(
+				[]string{
+					"skaffoldConfigFile=$(mktemp)",
+					// here document with quoted end marker to ensure no subsitution or expansion
+					fmt.Sprintf("cat << 'EOF' > $skaffoldConfigFile\n%s\nEOF", skaffoldYaml),
+					fmt.Sprintf("skaffold build --filename $skaffoldConfigFile --tag %s --verbosity %s", imageTag, verbosity),
+				},
+				"\n",
+			)},
+			Env: skaffoldGCBEnv(env...),
+		}},
+	}
+}
+
+func skaffoldGCBEnv(env ...string) []string {
+	defaultEnv := []string{
+		"SKAFFOLD_DETECT_MINIKUBE=false",
+		"SKAFFOLD_INTERACTIVE=false",
+		"SKAFFOLD_UPDATE_CHECK=false",
+	}
+	return append(defaultEnv, env...)
+}
diff --git a/pkg/skaffold/build/gcb/spec.go b/pkg/skaffold/build/gcb/spec.go
index cf51f9f96..b13d29267 100644
--- a/pkg/skaffold/build/gcb/spec.go
+++ b/pkg/skaffold/build/gcb/spec.go
@@ -76,6 +76,9 @@ func (b *Builder) buildSpecForArtifact(ctx context.Context, a *latest.Artifact,
 		}
 		return b.buildpackBuildSpec(a.BuildpackArtifact, tag, a.Dependencies)
 
+	case a.KoArtifact != nil:
+		return b.koBuildSpec(ctx, a, tag, platforms)
+
 	default:
 		return cloudbuild.Build{}, fmt.Errorf("unexpected type %q for gcb artifact:\n%s", misc.ArtifactType(a), misc.FormatArtifact(a))
 	}
diff --git a/pkg/skaffold/build/misc/artifact_type.go b/pkg/skaffold/build/misc/artifact_type.go
index 0bdd701d5..5ebe69df0 100644
--- a/pkg/skaffold/build/misc/artifact_type.go
+++ b/pkg/skaffold/build/misc/artifact_type.go
@@ -31,6 +31,7 @@ const (
 	Jib       = "jib"
 	Custom    = "custom"
 	Buildpack = "buildpack"
+	Ko        = "ko"
 )
 
 // ArtifactType returns a string representing the type found in an artifact. Used for error messages.
@@ -49,6 +50,8 @@ func ArtifactType(a *latest.Artifact) string {
 		return Custom
 	case a.BuildpackArtifact != nil:
 		return Buildpack
+	case a.KoArtifact != nil:
+		return Ko
 	default:
 		return ""
 	}
diff --git a/pkg/skaffold/schema/defaults/defaults.go b/pkg/skaffold/schema/defaults/defaults.go
index 4cbb0e86d..17b9fff0d 100644
--- a/pkg/skaffold/schema/defaults/defaults.go
+++ b/pkg/skaffold/schema/defaults/defaults.go
@@ -38,6 +38,7 @@ const (
 	defaultCloudBuildGradleImage = "gcr.io/cloud-builders/gradle"
 	defaultCloudBuildKanikoImage = kaniko.DefaultImage
 	defaultCloudBuildPackImage   = "gcr.io/k8s-skaffold/pack"
+	defaultCloudBuildKoImage     = "gcr.io/k8s-skaffold/skaffold"
 )
 
 // Set makes sure default values are set on a SkaffoldConfig.
@@ -88,6 +89,7 @@ func Set(c *latest.SkaffoldConfig) error {
 		setDefaultCloudBuildGradleImage,
 		setDefaultCloudBuildKanikoImage,
 		setDefaultCloudBuildPackImage,
+		setDefaultCloudBuildKoImage,
 	)
 
 	if err := withClusterConfig(c,
@@ -193,6 +195,10 @@ func setDefaultCloudBuildPackImage(gcb *latest.GoogleCloudBuild) {
 	gcb.PackImage = valueOrDefault(gcb.PackImage, defaultCloudBuildPackImage)
 }
 
+func setDefaultCloudBuildKoImage(gcb *latest.GoogleCloudBuild) {
+	gcb.KoImage = valueOrDefault(gcb.KoImage, defaultCloudBuildKoImage)
+}
+
 func setDefaultTagger(c *latest.SkaffoldConfig) {
 	if c.Build.TagPolicy != (latest.TagPolicy{}) {
 		return
diff --git a/pkg/skaffold/schema/validation/validation.go b/pkg/skaffold/schema/validation/validation.go
index 25e70ecbd..57bc0e5ac 100644
--- a/pkg/skaffold/schema/validation/validation.go
+++ b/pkg/skaffold/schema/validation/validation.go
@@ -612,7 +612,7 @@ func validateArtifactTypes(cfg *parser.SkaffoldConfigEntry, bc latest.BuildConfi
 	case bc.GoogleCloudBuild != nil:
 		for i, a := range bc.Artifacts {
 			at := misc.ArtifactType(a)
-			if at != misc.Kaniko && at != misc.Docker && at != misc.Jib && at != misc.Buildpack {
+			if at != misc.Kaniko && at != misc.Docker && at != misc.Jib && at != misc.Buildpack && at != misc.Ko {
 				cfgErrs = append(cfgErrs, ErrorWithLocation{
 					Error:    fmt.Errorf("found a '%s' artifact, which is incompatible with the 'gcb' builder:\n\n%s\n\nTo use the '%s' builder, remove the 'googleCloudBuild' stanza from the 'build' section of your configuration. For information, see https://skaffold.dev/docs/pipeline-stages/builders/", misc.ArtifactType(a), misc.FormatArtifact(a), misc.ArtifactType(a)),
 					Location: cfg.YAMLInfos.Locate(&cfg.Build.Artifacts[i].ArtifactType),


Test patch:
diff --git a/pkg/skaffold/build/gcb/ko_test.go b/pkg/skaffold/build/gcb/ko_test.go
new file mode 100644
index 000000000..df3e6254f
--- /dev/null
+++ b/pkg/skaffold/build/gcb/ko_test.go
@@ -0,0 +1,182 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package gcb
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
+	schema "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/v2beta28"
+	"github.com/GoogleContainerTools/skaffold/testutil"
+)
+
+func TestCloudBuildConfig(t *testing.T) {
+	tests := []struct {
+		description  string
+		koImage      string
+		skaffoldYaml string
+		imageTag     string
+		verbosity    string
+		env          []string
+	}{
+		{
+			description: "ensure Skaffold Config manifest is created in build step",
+			koImage:     "gcr.io/k8s-skaffold/skaffold:v1.37.2-lts@sha256:0bde2b09928ce891f4e1bfb8d957648bbece9987ec6ef3678c6542196e64e71a",
+			skaffoldYaml: `apiVersion: skaffold/v2beta28
+kind: Config
+build:
+  artifacts:
+  - image: skaffold-ko
+    ko: {}
+`,
+			imageTag:  "mytag",
+			verbosity: "info",
+			env:       []string{"GOTRACEBACK=2"},
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			config := cloudBuildConfig(test.koImage, []byte(test.skaffoldYaml), test.imageTag, test.verbosity, test.env...)
+			t.CheckEmpty(config.Images)
+			step := config.Steps[0]
+			t.CheckDeepEqual(test.koImage, step.Name)
+			t.CheckDeepEqual("sh", step.Entrypoint)
+			t.CheckContains(test.skaffoldYaml, step.Args[1])
+			t.CheckContains("--tag "+test.imageTag, step.Args[1])
+			t.CheckContains("--verbosity "+test.verbosity, step.Args[1])
+			for _, envvar := range test.env {
+				t.CheckContains(envvar, strings.Join(step.Env, " "))
+			}
+		})
+	}
+}
+
+func TestCreateSkaffoldConfig(t *testing.T) {
+	tests := []struct {
+		description            string
+		artifact               *latest.Artifact
+		imageName              string
+		insecureRegistries     []string
+		platforms              []string
+		expectedSkaffoldConfig *schema.SkaffoldConfig
+	}{
+		{
+			description: "all fields",
+			artifact: &latest.Artifact{
+				ImageName: "myimage",
+				ArtifactType: latest.ArtifactType{
+					KoArtifact: &latest.KoArtifact{
+						BaseImage: "baseImage",
+						Dir:       "./dir",
+						Env:       []string{"GOTRACEBACK=2"},
+						Flags:     []string{"-tags", "netgo"},
+						Labels: map[string]string{
+							"org.opencontainers.image.source": "https://github.com/GoogleContainerTools/skaffold.git",
+						},
+						Ldflags: []string{"-s", "-w"},
+						Main:    "./main",
+						Dependencies: &latest.KoDependencies{
+							Paths:  []string{"unused-koartifact-paths"},
+							Ignore: []string{"unused-koartifact-ingore"},
+						},
+					},
+				},
+				Platforms: []string{"linux/amd64"},
+				Workspace: "./workspace",
+				Dependencies: []*latest.ArtifactDependency{{
+					ImageName: "unused-dependency-image-name",
+					Alias:     "unused-dependency-alias",
+				}},
+				LifecycleHooks: latest.BuildHooks{
+					PreHooks: []latest.HostHook{{
+						Command: []string{"unused-lifecycle-hook-command"},
+					}},
+				},
+				Sync: &latest.Sync{
+					Infer: []string{"unused-sync-infer"},
+				},
+			},
+			imageName:          "gcr.io/project-id/myimage",
+			insecureRegistries: []string{"insecure.example.com:5000"},
+			platforms:          []string{"linux/amd64", "linux/arm64"},
+			expectedSkaffoldConfig: &schema.SkaffoldConfig{
+				APIVersion: schema.Version,
+				Kind:       "Config",
+				Pipeline: schema.Pipeline{
+					Build: schema.BuildConfig{
+						Artifacts: []*schema.Artifact{{
+							ImageName: "gcr.io/project-id/myimage",
+							ArtifactType: schema.ArtifactType{
+								KoArtifact: &schema.KoArtifact{
+									BaseImage: "baseImage",
+									Dir:       "./dir",
+									Env:       []string{"GOTRACEBACK=2"},
+									Flags:     []string{"-tags", "netgo"},
+									Labels: map[string]string{
+										"org.opencontainers.image.source": "https://github.com/GoogleContainerTools/skaffold.git",
+									},
+									Ldflags: []string{"-s", "-w"},
+									Main:    "./main",
+								},
+							},
+							Platforms: []string{"linux/amd64"},
+							Workspace: "./workspace",
+						}},
+						InsecureRegistries: []string{"insecure.example.com:5000"},
+						Platforms:          []string{"linux/amd64", "linux/arm64"},
+					},
+				},
+			},
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			skaffoldConfig := createSkaffoldConfig(test.artifact, test.imageName, test.platforms, test.insecureRegistries)
+			t.CheckDeepEqual(test.expectedSkaffoldConfig, skaffoldConfig)
+		})
+	}
+}
+
+func TestSplitImageNameAndTag(t *testing.T) {
+	tests := []struct {
+		description  string
+		input        string
+		expectedName string
+		expectedTag  string
+	}{
+		{
+			description:  "image name with tag",
+			input:        "gcr.io/project-id/myimage:mytag",
+			expectedName: "gcr.io/project-id/myimage",
+			expectedTag:  "mytag",
+		},
+		{
+			description:  "image name without tag defaults to latest",
+			input:        "gcr.io/project-id/myimage",
+			expectedName: "gcr.io/project-id/myimage",
+			expectedTag:  "latest",
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			name, tag := splitImageNameAndTag(test.input)
+			t.CheckDeepEqual(test.expectedName, name)
+			t.CheckDeepEqual(test.expectedTag, tag)
+		})
+	}
+}
diff --git a/pkg/skaffold/schema/latest/config.go b/pkg/skaffold/schema/latest/config.go
index 2e91325a3..4f317d3e0 100644
--- a/pkg/skaffold/schema/latest/config.go
+++ b/pkg/skaffold/schema/latest/config.go
@@ -384,6 +384,12 @@ type GoogleCloudBuild struct {
 	// Defaults to `gcr.io/k8s-skaffold/pack`.
 	PackImage string `yaml:"packImage,omitempty"`
 
+	// KoImage is the image that runs a ko build.
+	// The image must contain Skaffold, Go, and a shell (runnable as `sh`) that supports here documents.
+	// See [Cloud Builders](https://cloud.google.com/cloud-build/docs/cloud-builders).
+	// Defaults to `gcr.io/k8s-skaffold/skaffold`.
+	KoImage string `yaml:"koImage,omitempty"`
+
 	// Concurrency is how many artifacts can be built concurrently. 0 means "no-limit".
 	// Defaults to `0`.
 	Concurrency int `yaml:"concurrency,omitempty"`
diff --git a/pkg/skaffold/schema/profiles_test.go b/pkg/skaffold/schema/profiles_test.go
index d511cbd88..16dc9bbf8 100644
--- a/pkg/skaffold/schema/profiles_test.go
+++ b/pkg/skaffold/schema/profiles_test.go
@@ -136,6 +136,7 @@ func TestApplyProfiles(t *testing.T) {
 									GradleImage: "gcr.io/cloud-builders/gradle",
 									KanikoImage: kaniko.DefaultImage,
 									PackImage:   "gcr.io/k8s-skaffold/pack",
+									KoImage:     "gcr.io/k8s-skaffold/skaffold",
 								},
 							},
 						},
diff --git a/pkg/skaffold/schema/versions_test.go b/pkg/skaffold/schema/versions_test.go
index 6a8ef662c..cd0b991d2 100644
--- a/pkg/skaffold/schema/versions_test.go
+++ b/pkg/skaffold/schema/versions_test.go
@@ -499,6 +499,7 @@ func withGoogleCloudBuild(id string, ops ...func(*latest.BuildConfig)) func(*lat
 			GradleImage: "gcr.io/cloud-builders/gradle",
 			KanikoImage: kaniko.DefaultImage,
 			PackImage:   "gcr.io/k8s-skaffold/pack",
+			KoImage:     "gcr.io/k8s-skaffold/skaffold",
 		}}}
 		for _, op := range ops {
 			op(&b)