From f19c801d70117c62feac93f885350177033b5e32 Mon Sep 17 00:00:00 2001 From: Halvard Skogsrud Date: Wed, 20 Jul 2022 23:24:55 +0000 Subject: [PATCH] feat(ko): Init support for the ko builder Adds the ko builder as an option when running `skaffold init` in a directory containing Go code. The ko builder will show up as an option for directories containing a `go.mod` file. The feature can be disabled by adding the `--XXenableKoInit=false` flag. Tracking: #7131 --- cmd/skaffold/app/cmd/init.go | 5 +- cmd/skaffold/app/cmd/init_test.go | 38 +++++++++- .../en/docs/pipeline-stages/builders/ko.md | 28 ++------ .../content/en/docs/pipeline-stages/init.md | 5 +- .../en/docs/pipeline-stages/builders/ko.md | 28 ++------ docs/content/en/docs/pipeline-stages/init.md | 5 +- pkg/skaffold/build/ko/init/init.go | 70 +++++++++++++++++++ pkg/skaffold/build/ko/init/init_test.go | 52 ++++++++++++++ pkg/skaffold/initializer/analyze/analyze.go | 1 + pkg/skaffold/initializer/analyze/builder.go | 10 +++ pkg/skaffold/initializer/build/resolve.go | 8 ++- pkg/skaffold/initializer/config/config.go | 1 + pkg/skaffold/initializer/errors/errors.go | 6 +- 13 files changed, 196 insertions(+), 61 deletions(-) create mode 100644 pkg/skaffold/build/ko/init/init.go create mode 100644 pkg/skaffold/build/ko/init/init_test.go diff --git a/cmd/skaffold/app/cmd/init.go b/cmd/skaffold/app/cmd/init.go index f0a0660a447..686e96ccaf0 100644 --- a/cmd/skaffold/app/cmd/init.go +++ b/cmd/skaffold/app/cmd/init.go @@ -40,6 +40,7 @@ var ( analyze bool enableJibInit bool enableJibGradleInit bool + enableKoInit bool enableBuildpacksInit bool enableNewInitFormat bool enableManifestGeneration bool @@ -65,6 +66,7 @@ func NewCmdInit() *cobra.Command { {Value: &enableNewInitFormat, Name: "XXenableNewInitFormat", DefValue: false, Usage: "", Hidden: true, IsEnum: true}, {Value: &enableJibInit, Name: "XXenableJibInit", DefValue: true, Usage: "", Hidden: true, IsEnum: true}, {Value: &enableJibGradleInit, Name: "XXenableJibGradleInit", DefValue: false, Usage: "", Hidden: true, IsEnum: true}, + {Value: &enableKoInit, Name: "XXenableKoInit", DefValue: true, Usage: "", Hidden: true, IsEnum: true}, {Value: &enableBuildpacksInit, Name: "XXenableBuildpacksInit", DefValue: true, Usage: "", Hidden: true, IsEnum: true}, {Value: &buildpacksBuilder, Name: "XXdefaultBuildpacksBuilder", DefValue: "gcr.io/buildpacks/builder:v1", Usage: "", Hidden: true}, {Value: &enableManifestGeneration, Name: "generate-manifests", DefValue: false, Usage: "Allows skaffold to try and generate basic kubernetes resources to get your project started", IsEnum: true}, @@ -85,8 +87,9 @@ func doInit(ctx context.Context, out io.Writer) error { Analyze: analyze, EnableJibInit: enableJibInit, EnableJibGradleInit: enableJibGradleInit, + EnableKoInit: enableKoInit, EnableBuildpacksInit: enableBuildpacksInit, - EnableNewInitFormat: enableNewInitFormat || enableBuildpacksInit || enableJibInit, + EnableNewInitFormat: enableNewInitFormat || enableBuildpacksInit || enableJibInit || enableKoInit, EnableManifestGeneration: enableManifestGeneration, Opts: opts, MaxFileSize: maxFileSize, diff --git a/cmd/skaffold/app/cmd/init_test.go b/cmd/skaffold/app/cmd/init_test.go index abee6f58402..d82143848a7 100644 --- a/cmd/skaffold/app/cmd/init_test.go +++ b/cmd/skaffold/app/cmd/init_test.go @@ -54,6 +54,7 @@ func TestFlagsToConfigVersion(t *testing.T) { Analyze: false, EnableJibInit: true, EnableJibGradleInit: false, + EnableKoInit: true, EnableBuildpacksInit: true, EnableNewInitFormat: true, BuildpacksBuilder: "gcr.io/buildpacks/builder:v1", @@ -77,6 +78,7 @@ func TestFlagsToConfigVersion(t *testing.T) { "--analyze", "--XXenableJibInit", "--XXenableJibGradleInit", + "--XXenableKoInit=false", "--XXenableBuildpacksInit", "--XXenableNewInitFormat", "--XXdefaultBuildpacksBuilder", "buildpacks/builder", @@ -91,6 +93,7 @@ func TestFlagsToConfigVersion(t *testing.T) { Analyze: true, EnableJibInit: true, EnableJibGradleInit: true, + EnableKoInit: false, EnableBuildpacksInit: true, EnableNewInitFormat: true, BuildpacksBuilder: "buildpacks/builder", @@ -104,6 +107,8 @@ func TestFlagsToConfigVersion(t *testing.T) { args: []string{ "init", "--XXenableJibInit", + "--XXenableKoInit=false", + "--XXenableBuildpacksInit=false", }, expectedConfig: config.Config{ ComposeFile: "", @@ -114,7 +119,33 @@ func TestFlagsToConfigVersion(t *testing.T) { Force: false, Analyze: false, EnableJibInit: true, - EnableBuildpacksInit: true, + EnableKoInit: false, + EnableBuildpacksInit: false, + EnableNewInitFormat: true, + BuildpacksBuilder: "gcr.io/buildpacks/builder:v1", + Opts: opts, + MaxFileSize: maxFileSize, + }, + }, + { + name: "enableKoInit implies enableNewInitFormat", + args: []string{ + "init", + "--XXenableJibInit=false", + "--XXenableKoInit", + "--XXenableBuildpacksInit=false", + }, + expectedConfig: config.Config{ + ComposeFile: "", + CliArtifacts: []string{}, + CliKubernetesManifests: []string{}, + SkipBuild: false, + SkipDeploy: false, + Force: false, + Analyze: false, + EnableJibInit: false, + EnableKoInit: true, + EnableBuildpacksInit: false, EnableNewInitFormat: true, BuildpacksBuilder: "gcr.io/buildpacks/builder:v1", Opts: opts, @@ -125,6 +156,8 @@ func TestFlagsToConfigVersion(t *testing.T) { name: "enableBuildpackInit implies enableNewInitFormat", args: []string{ "init", + "--XXenableJibInit=false", + "--XXenableKoInit=false", "--XXenableBuildpacksInit", }, expectedConfig: config.Config{ @@ -135,7 +168,8 @@ func TestFlagsToConfigVersion(t *testing.T) { SkipDeploy: false, Force: false, Analyze: false, - EnableJibInit: true, + EnableJibInit: false, + EnableKoInit: false, EnableBuildpacksInit: true, EnableNewInitFormat: true, BuildpacksBuilder: "gcr.io/buildpacks/builder:v1", 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 af0994be80b..5fd824dd2df 100644 --- a/docs-v2/content/en/docs/pipeline-stages/builders/ko.md +++ b/docs-v2/content/en/docs/pipeline-stages/builders/ko.md @@ -254,30 +254,10 @@ Useful tips for existing `ko` users: specify these fields in `.ko.yaml`, you do not need to repeat them in `skaffold.yaml`. -- Future Skaffold releases will include support for generating `skaffold.yaml` - files by examining an existing code base. For now, you can generate a starter - `skaffold.yaml` file by searching your existing manifests for image - references starting with `ko://` by using this snippet: - - ```shell - cat << EOF > skaffold.yaml - apiVersion: skaffold/v2beta26 - kind: Config - build: - artifacts: - EOF - grep -rho "ko://$(go list -m)[^\"]*" ./config/ | sort | uniq | xargs -Iimg echo -e " - image: img\n ko: {}" >> skaffold.yaml - cat << EOF >> skaffold.yaml - local: - concurrency: 0 - deploy: - kubectl: - manifests: - - ./config/** - EOF - ``` - - Replace `./config/` with the path to your Kubernetes manifest files. +- You can generate `skaffold.yaml` files by examining an existing code base, + using the command + [`skaffold init`]({{< relref "/docs/pipeline-stages/init" >}}). + Select the Ko builder for your images when prompted. ### `ko` commands and workflows in Skaffold diff --git a/docs-v2/content/en/docs/pipeline-stages/init.md b/docs-v2/content/en/docs/pipeline-stages/init.md index a719db9c8fe..0fdf0c2e9f8 100644 --- a/docs-v2/content/en/docs/pipeline-stages/init.md +++ b/docs-v2/content/en/docs/pipeline-stages/init.md @@ -17,7 +17,8 @@ and [deploy](#deploy-config-initialization) config. 1. [Docker]({{}}) 2. [Jib]({{}}) -2. [Buildpacks]({{}}) +3. [Ko]({{}}) +4. [Buildpacks]({{}}) `skaffold init` walks your project directory and looks for any build configuration files such as `Dockerfile`, `build.gradle/pom.xml`, `package.json`, `requirements.txt` or `go.mod`. `init` skips files that are larger @@ -108,7 +109,7 @@ If bringing a project to skaffold that has no kubernetes manifests yet, it may b ## `--force` Flag `skaffold init` allows for use of a `--force` flag, which removes the prompts from vanilla `skaffold init`, and allows skaffold to make a best effort attempt to automatically generate a config for your project. -In a situation where one image is detected, but multiple possible builders are detected, skaffold will choose a builder as follows: Docker > Jib > Bazel > Buildpacks. +In a situation where one image is detected, but multiple possible builders are detected, skaffold will choose a builder as follows: Docker > Jib > Ko > Bazel > Buildpacks. *Note: This feature is still under development, and doesn't currently support use cases such as multiple images in a project.* diff --git a/docs/content/en/docs/pipeline-stages/builders/ko.md b/docs/content/en/docs/pipeline-stages/builders/ko.md index af0994be80b..5fd824dd2df 100644 --- a/docs/content/en/docs/pipeline-stages/builders/ko.md +++ b/docs/content/en/docs/pipeline-stages/builders/ko.md @@ -254,30 +254,10 @@ Useful tips for existing `ko` users: specify these fields in `.ko.yaml`, you do not need to repeat them in `skaffold.yaml`. -- Future Skaffold releases will include support for generating `skaffold.yaml` - files by examining an existing code base. For now, you can generate a starter - `skaffold.yaml` file by searching your existing manifests for image - references starting with `ko://` by using this snippet: - - ```shell - cat << EOF > skaffold.yaml - apiVersion: skaffold/v2beta26 - kind: Config - build: - artifacts: - EOF - grep -rho "ko://$(go list -m)[^\"]*" ./config/ | sort | uniq | xargs -Iimg echo -e " - image: img\n ko: {}" >> skaffold.yaml - cat << EOF >> skaffold.yaml - local: - concurrency: 0 - deploy: - kubectl: - manifests: - - ./config/** - EOF - ``` - - Replace `./config/` with the path to your Kubernetes manifest files. +- You can generate `skaffold.yaml` files by examining an existing code base, + using the command + [`skaffold init`]({{< relref "/docs/pipeline-stages/init" >}}). + Select the Ko builder for your images when prompted. ### `ko` commands and workflows in Skaffold diff --git a/docs/content/en/docs/pipeline-stages/init.md b/docs/content/en/docs/pipeline-stages/init.md index a719db9c8fe..0fdf0c2e9f8 100644 --- a/docs/content/en/docs/pipeline-stages/init.md +++ b/docs/content/en/docs/pipeline-stages/init.md @@ -17,7 +17,8 @@ and [deploy](#deploy-config-initialization) config. 1. [Docker]({{}}) 2. [Jib]({{}}) -2. [Buildpacks]({{}}) +3. [Ko]({{}}) +4. [Buildpacks]({{}}) `skaffold init` walks your project directory and looks for any build configuration files such as `Dockerfile`, `build.gradle/pom.xml`, `package.json`, `requirements.txt` or `go.mod`. `init` skips files that are larger @@ -108,7 +109,7 @@ If bringing a project to skaffold that has no kubernetes manifests yet, it may b ## `--force` Flag `skaffold init` allows for use of a `--force` flag, which removes the prompts from vanilla `skaffold init`, and allows skaffold to make a best effort attempt to automatically generate a config for your project. -In a situation where one image is detected, but multiple possible builders are detected, skaffold will choose a builder as follows: Docker > Jib > Bazel > Buildpacks. +In a situation where one image is detected, but multiple possible builders are detected, skaffold will choose a builder as follows: Docker > Jib > Ko > Bazel > Buildpacks. *Note: This feature is still under development, and doesn't currently support use cases such as multiple images in a project.* diff --git a/pkg/skaffold/build/ko/init/init.go b/pkg/skaffold/build/ko/init/init.go new file mode 100644 index 00000000000..62eac917a02 --- /dev/null +++ b/pkg/skaffold/build/ko/init/init.go @@ -0,0 +1,70 @@ +/* +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 init + +import ( + "fmt" + "path/filepath" + + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/initializer/build" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" +) + +// ArtifactConfig holds information about a Ko project +type ArtifactConfig struct { + File string `json:"path,omitempty"` +} + +var _ build.InitBuilder = &ArtifactConfig{} + +// Validate checks if the file is a Go module file. +func Validate(path string) bool { + return filepath.Base(path) == "go.mod" +} + +// Name returns the name of the builder. +func (c ArtifactConfig) Name() string { + return "Ko" +} + +// Describe returns the initBuilder's string representation. +// This representation is used when prompting the user to choose a builder. +func (c ArtifactConfig) Describe() string { + return fmt.Sprintf("%s (%s)", c.Name(), c.File) +} + +// ArtifactType returns a definition of the artifact to be built. +func (c ArtifactConfig) ArtifactType(_ string) latest.ArtifactType { + return latest.ArtifactType{ + KoArtifact: &latest.KoArtifact{ + Dependencies: &latest.KoDependencies{ + Paths: []string{"**/*.go", "go.mod"}, + }, + }, + } +} + +// ConfiguredImage returns the target image configured by the builder, or empty string if no image is configured. +func (c ArtifactConfig) ConfiguredImage() string { + // Target image is not configured in Ko. + return "" +} + +// Path returns the path to the build definition. +func (c ArtifactConfig) Path() string { + return c.File +} diff --git a/pkg/skaffold/build/ko/init/init_test.go b/pkg/skaffold/build/ko/init/init_test.go new file mode 100644 index 00000000000..1138db4af9b --- /dev/null +++ b/pkg/skaffold/build/ko/init/init_test.go @@ -0,0 +1,52 @@ +/* +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 init + +import ( + "testing" + + "github.com/GoogleContainerTools/skaffold/testutil" +) + +func TestValidate(t *testing.T) { + tests := []struct { + description string + path string + expect bool + }{ + { + description: "go.mod in current directory", + path: "go.mod", + expect: true, + }, + { + description: "go.mod in subdirectory", + path: "foo/go.mod", + expect: true, + }, + { + description: "not go.mod", + path: "Gopkg.toml", + expect: false, + }, + } + for _, test := range tests { + testutil.Run(t, test.description, func(t *testutil.T) { + t.CheckDeepEqual(test.expect, Validate(test.path)) + }) + } +} diff --git a/pkg/skaffold/initializer/analyze/analyze.go b/pkg/skaffold/initializer/analyze/analyze.go index 8aa55aa01d8..07e03f662a2 100644 --- a/pkg/skaffold/initializer/analyze/analyze.go +++ b/pkg/skaffold/initializer/analyze/analyze.go @@ -105,6 +105,7 @@ func NewAnalyzer(c config.Config) *ProjectAnalysis { findBuilders: !c.SkipBuild, enableJibInit: c.EnableJibInit, enableJibGradleInit: c.EnableJibGradleInit, + enableKoInit: c.EnableKoInit, enableBuildpacksInit: c.EnableBuildpacksInit, buildpacksBuilder: c.BuildpacksBuilder, }, diff --git a/pkg/skaffold/initializer/analyze/builder.go b/pkg/skaffold/initializer/analyze/builder.go index 8db7df081a0..aa4b1cb8bf1 100644 --- a/pkg/skaffold/initializer/analyze/builder.go +++ b/pkg/skaffold/initializer/analyze/builder.go @@ -23,6 +23,7 @@ import ( "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/buildpacks" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/jib" + koinit "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/ko/init" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/initializer/build" ) @@ -31,6 +32,7 @@ type builderAnalyzer struct { directoryAnalyzer enableJibInit bool enableJibGradleInit bool + enableKoInit bool enableBuildpacksInit bool findBuilders bool buildpacksBuilder string @@ -86,6 +88,14 @@ func (a *builderAnalyzer) detectBuilders(ctx context.Context, path string, detec } } + if a.enableKoInit { + if koinit.Validate(path) { + results = append(results, koinit.ArtifactConfig{ + File: path, + }) + } + } + // TODO: Remove backwards compatibility if statement (not entire block) if a.enableBuildpacksInit { // Check for buildpacks diff --git a/pkg/skaffold/initializer/build/resolve.go b/pkg/skaffold/initializer/build/resolve.go index 567807a8f02..ab81a4c9f2d 100644 --- a/pkg/skaffold/initializer/build/resolve.go +++ b/pkg/skaffold/initializer/build/resolve.go @@ -85,13 +85,15 @@ func builderRank(builder InitBuilder) int { return 1 case a.JibArtifact != nil: return 2 - case a.BazelArtifact != nil: + case a.KoArtifact != nil: return 3 - case a.BuildpackArtifact != nil: + case a.BazelArtifact != nil: return 4 + case a.BuildpackArtifact != nil: + return 5 } - return 5 + return 6 } func (d *defaultBuildInitializer) resolveBuilderImagesInteractively() error { diff --git a/pkg/skaffold/initializer/config/config.go b/pkg/skaffold/initializer/config/config.go index 28eb5435a22..1bc1cf553d6 100644 --- a/pkg/skaffold/initializer/config/config.go +++ b/pkg/skaffold/initializer/config/config.go @@ -31,6 +31,7 @@ type Config struct { Analyze bool EnableJibInit bool // TODO: Remove this parameter EnableJibGradleInit bool + EnableKoInit bool EnableBuildpacksInit bool EnableNewInitFormat bool EnableManifestGeneration bool diff --git a/pkg/skaffold/initializer/errors/errors.go b/pkg/skaffold/initializer/errors/errors.go index d03b06456ea..fa48390ddfd 100644 --- a/pkg/skaffold/initializer/errors/errors.go +++ b/pkg/skaffold/initializer/errors/errors.go @@ -23,7 +23,7 @@ type NoBuilderErr struct{} func (e NoBuilderErr) ExitCode() int { return 101 } func (e NoBuilderErr) Error() string { - return "one or more valid builder configuration (Dockerfile or Jib configuration) must be present to build images with skaffold; please provide at least one build config and try again or run `skaffold init --skip-build`" + return "one or more valid build configuration must be present to build images with Skaffold; please provide at least one build config and try again, or run `skaffold init --skip-build`" } // NoManifestErr is an error returned by `skaffold init` when no valid Kubernetes manifest is found. @@ -34,7 +34,7 @@ func (e NoManifestErr) Error() string { return "one or more valid Kubernetes manifests are required to run skaffold" } -// PreExistingConfigErr is an error returned by `skaffold init` when a skaffold config file already exists. +// PreExistingConfigErr is an error returned by `skaffold init` when a Skaffold config file already exists. type PreExistingConfigErr struct { Path string } @@ -57,5 +57,5 @@ type NoHelmChartsErr struct{} func (e NoHelmChartsErr) ExitCode() int { return 105 } func (e NoHelmChartsErr) Error() string { - return "one or more valid helm charts or kubernetes manifests are required to run skaffold" + return "one or more valid Helm charts or Kubernetes manifests are required to run Skaffold" }