From 2242480835cfcf34e5d7855e9626e58d75937bd1 Mon Sep 17 00:00:00 2001 From: Nick Kubala Date: Tue, 16 Jun 2020 18:21:03 -0700 Subject: [PATCH] 'skaffold init' for Kustomize projects generates profiles for each overlay --- cmd/skaffold/app/cmd/init.go | 9 +- docs/content/en/docs/pipeline-stages/init.md | 74 ++++++-- docs/content/en/docs/references/cli/_index.md | 4 +- .../getting-started-kustomize/deployment.yaml | 1 - .../{ => app}/Dockerfile | 0 .../{ => app}/main.go | 0 .../{ => base}/deployment.yaml | 6 +- .../base/kustomization.yaml | 5 + .../kustomization.yaml | 4 - .../overlays/dev/deployment.yaml | 6 + .../overlays/dev/kustomization.yaml | 11 ++ .../overlays/prod/deployment.yaml | 8 + .../overlays/prod/kustomization.yaml | 11 ++ .../overlays/staging/deployment.yaml | 6 + .../overlays/staging/kustomization.yaml | 11 ++ .../getting-started-kustomize/patch.yaml | 10 -- .../getting-started-kustomize/skaffold.yaml | 18 +- pkg/skaffold/deploy/kustomize.go | 10 +- pkg/skaffold/initializer/analyze/analyze.go | 4 + pkg/skaffold/initializer/analyze/kustomize.go | 12 +- pkg/skaffold/initializer/config.go | 5 +- pkg/skaffold/initializer/config/config.go | 3 +- pkg/skaffold/initializer/config_test.go | 9 +- pkg/skaffold/initializer/deploy/init.go | 16 +- pkg/skaffold/initializer/deploy/kubectl.go | 4 +- .../initializer/deploy/kubectl_test.go | 17 +- pkg/skaffold/initializer/deploy/kustomize.go | 94 ++++++++-- .../initializer/deploy/kustomize_test.go | 166 ++++++++++++++++++ pkg/skaffold/initializer/init.go | 2 +- 29 files changed, 445 insertions(+), 81 deletions(-) rename integration/examples/getting-started-kustomize/{ => app}/Dockerfile (100%) rename integration/examples/getting-started-kustomize/{ => app}/main.go (100%) rename integration/examples/getting-started-kustomize/{ => base}/deployment.yaml (72%) create mode 100644 integration/examples/getting-started-kustomize/base/kustomization.yaml delete mode 100644 integration/examples/getting-started-kustomize/kustomization.yaml create mode 100644 integration/examples/getting-started-kustomize/overlays/dev/deployment.yaml create mode 100644 integration/examples/getting-started-kustomize/overlays/dev/kustomization.yaml create mode 100644 integration/examples/getting-started-kustomize/overlays/prod/deployment.yaml create mode 100644 integration/examples/getting-started-kustomize/overlays/prod/kustomization.yaml create mode 100644 integration/examples/getting-started-kustomize/overlays/staging/deployment.yaml create mode 100644 integration/examples/getting-started-kustomize/overlays/staging/kustomization.yaml delete mode 100644 integration/examples/getting-started-kustomize/patch.yaml create mode 100644 pkg/skaffold/initializer/deploy/kustomize_test.go diff --git a/cmd/skaffold/app/cmd/init.go b/cmd/skaffold/app/cmd/init.go index ea1b11dd92e..d0d8812cd94 100644 --- a/cmd/skaffold/app/cmd/init.go +++ b/cmd/skaffold/app/cmd/init.go @@ -31,6 +31,8 @@ const maxFileSize = 1024 * 1024 * 512 var ( composeFile string + buildpacksBuilder string + defaultKustomization string cliArtifacts []string cliKubernetesManifests []string skipBuild bool @@ -42,7 +44,6 @@ var ( enableBuildpacksInit bool enableNewInitFormat bool enableManifestGeneration bool - buildpacksBuilder string ) // for testing @@ -59,8 +60,9 @@ func NewCmdInit() *cobra.Command { f.MarkHidden("skip-deploy") f.BoolVar(&force, "force", false, "Force the generation of the Skaffold config") f.StringVar(&composeFile, "compose-file", "", "Initialize from a docker-compose file") + f.StringVar(&defaultKustomization, "default-kustomization", "", "Default Kustomization overlay path (others will be added as profiles)") f.StringArrayVarP(&cliArtifacts, "artifact", "a", nil, "'='-delimited Dockerfile/image pair, or JSON string, to generate build artifact\n(example: --artifact='{\"builder\":\"Docker\",\"payload\":{\"path\":\"/web/Dockerfile.web\"},\"image\":\"gcr.io/web-project/image\"}')") - f.StringArrayVarP(&cliKubernetesManifests, "kubernetes-manifest", "k", nil, "a path or a glob pattern to kubernetes manifests (can be non-existent) to be added to the kubectl deployer (overrides detection of kubernetes manifests). Repeat the flag for multiple entries. E.g.: skaffold init -k pod.yaml -k k8s/*.yml") + f.StringArrayVarP(&cliKubernetesManifests, "kubernetes-manifest", "k", nil, "A path or a glob pattern to kubernetes manifests (can be non-existent) to be added to the kubectl deployer (overrides detection of kubernetes manifests). Repeat the flag for multiple entries. E.g.: skaffold init -k pod.yaml -k k8s/*.yml") f.BoolVar(&analyze, "analyze", false, "Print all discoverable Dockerfiles and images in JSON format to stdout") f.BoolVar(&enableNewInitFormat, "XXenableNewInitFormat", false, "") f.MarkHidden("XXenableNewInitFormat") @@ -80,7 +82,9 @@ func NewCmdInit() *cobra.Command { func doInit(ctx context.Context, out io.Writer) error { return initEntrypoint(ctx, out, config.Config{ + BuildpacksBuilder: buildpacksBuilder, ComposeFile: composeFile, + DefaultKustomization: defaultKustomization, CliArtifacts: cliArtifacts, CliKubernetesManifests: cliKubernetesManifests, SkipBuild: skipBuild, @@ -92,7 +96,6 @@ func doInit(ctx context.Context, out io.Writer) error { EnableBuildpacksInit: enableBuildpacksInit, EnableNewInitFormat: enableNewInitFormat || enableBuildpacksInit || enableJibInit, EnableManifestGeneration: enableManifestGeneration, - BuildpacksBuilder: buildpacksBuilder, Opts: opts, MaxFileSize: maxFileSize, }) diff --git a/docs/content/en/docs/pipeline-stages/init.md b/docs/content/en/docs/pipeline-stages/init.md index f5e62cfb3af..46db94da299 100644 --- a/docs/content/en/docs/pipeline-stages/init.md +++ b/docs/content/en/docs/pipeline-stages/init.md @@ -11,7 +11,7 @@ Skaffold auto-generates `build` and `deploy` config for supported builders and d ## Build Config Initialization -`skaffold init` currently supports build detection for two builders. +`skaffold init` currently supports build detection for two builders: 1. [Docker]({{}}) 2. [Jib]({{}}) @@ -19,10 +19,10 @@ Skaffold auto-generates `build` and `deploy` config for supported builders and d `skaffold init` will walk your project directory and look for any `Dockerfiles` or `build.gradle/pom.xml`. Please note, `skaffold init` skips files that are larger than 500MB. -If you have multiple `Dockerfile` or `build.gradle/pom.xml` files, Skaffold will provide an option -to pair an image with one of the file. +If you have multiple `Dockerfile` or `build.gradle/pom.xml` files, Skaffold will prompt you to +pair your build config files with any images detected in your deploy configuration. -E.g. For a multi-services [microservices example](https://github.com/GoogleContainerTools/skaffold/tree/master/examples/microservices) +E.g. For an application with [two microservices] (https://github.com/GoogleContainerTools/skaffold/tree/master/examples/microservices): ```bash skaffold init @@ -31,15 +31,15 @@ skaffold init {{< alert title="Note" >}} -You can choose None (image not built from these sources) in case none of the suggested -options are correct.
-You will have to manually set up build config for this artifact +You can choose None (image not built from these sources) if none of the suggested +options are correct, or this image is not built by any of your source code.
+If this image is one you want Skaffold to build, you'll need to manually set up the build configuration for this artifact. {{}} -`skaffold` init also recognizes a maven or gradle project and will auto-suggest [`jib`]({{}}) builder. -Currently `jib` artifact detection is disabled by default, you can turn it on using the flag `--XXenableJibInit`. +`skaffold` init also recognizes Maven and Gradle projects, and will auto-suggest the [`jib`]({{}}) builder. +Currently `jib` artifact detection is disabled by default, but can be enabled using the flag `--XXenableJibInit`. -You can try it this out on example [jib project](https://github.com/GoogleContainerTools/skaffold/tree/master/examples/jib-multimodule) +You can try this out on our example [jib project](https://github.com/GoogleContainerTools/skaffold/tree/master/examples/jib-multimodule) ```bash skaffold init --XXenableJibInit @@ -48,11 +48,10 @@ skaffold init --XXenableJibInit ![jib-multimodule](/images/jib-multimodule-init-flow.png) -In case you want to configure build artifacts on your own, use `--skip-build` flag. - ## Deploy Config Initialization -`skaffold init` currently supports only [`kubectl` deployer]({{}}) -Skaffold will walk through all the `yaml` files in your project and find valid kubernetes manifest files. +`skaffold init` support bootstrapping projects set up to deploy with [`kubectl`]({{}}) +or [`kustomize`]({{}}). +For projects deploying straight through `kubectl`, Skaffold will walk through all the `yaml` files in your project and find valid Kubernetes manifest files. These files will be added to `deploy` config in `skaffold.yaml`. @@ -64,15 +63,60 @@ deploy: - leeroy-web/kubernetes/deployment.yaml ``` +For projects deploying with `kustomize`, Skaffold will scan your project and look for `kustomization.yaml`s as well as Kubernetes manifests. +It will attempt to infer the project structure based on the recommended project structure from the Kustomize project: thus, +**it is highly recommended to match your project structure to the recommended base/ and overlay/ structure from Kustomize!** + +This generally looks like this: + +``` +app/ <- application source code, along with build configuration + main.go + Dockerfile +... +base/ <- base deploy configuration + kustomization.yaml + deployment.yaml +overlays/ <- one or more nested directories, each with modified environment configuration + dev/ + deployment.yaml + kustomization.yaml + prod/ +... +``` + +When overlay directories are found, these will be listed in the generated Skaffold config as `paths` in the `kustomize` deploy stanza. However, it generally does not make sense to have multiple overlays applied at the same time, so **Skaffold will attempt to choose a default overlay, and put each other overlay into its own profile**. This can be specified by the user through the flag `--default-kustomization`; otherwise, Skaffold will use the following heuristic: + +1) Any overlay with the name `dev` +2) If none present, the **first** overlay that isn't named `prod` + +*Note: order is guaranteed, since Skaffold's directory parsing is always deterministic.* ## Init API -`skaffold init` also exposes an api which tools like IDEs can integrate with via flags. +`skaffold init` also exposes an AP:I which tools like IDEs can integrate with via flags. This API can be used to 1. Analyze a project workspace and discover all build definitions (e.g. `Dockerfile`s) and artifacts (image names from the Kubernetes manifests) - this then provides an ability for tools to ask the user to pair the artifacts with Dockerfiles interactively. 2. Given a pairing between the image names (artifacts) and build definitions (e.g. Dockerfiles), generate Skaffold `build` config for a given artifact. +The resulting `skaffold.yaml` will look something like this: + +```yaml +apiVersion: skaffold/v2beta5 +... +deploy: + kustomize: + paths: + - overlays/dev +profiles: +- name: prod + deploy: + kustomize: + paths: + - overlays/prod +``` + **Init API contract** | API | flag | input/output | diff --git a/docs/content/en/docs/references/cli/_index.md b/docs/content/en/docs/references/cli/_index.md index 60def6eb3ca..e333c2f5bac 100644 --- a/docs/content/en/docs/references/cli/_index.md +++ b/docs/content/en/docs/references/cli/_index.md @@ -663,9 +663,10 @@ Options: -a, --artifact=[]: '='-delimited Dockerfile/image pair, or JSON string, to generate build artifact (example: --artifact='{"builder":"Docker","payload":{"path":"/web/Dockerfile.web"},"image":"gcr.io/web-project/image"}') --compose-file='': Initialize from a docker-compose file + --default-kustomization='': Default Kustomization overlay path (others will be added as profiles) -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file --force=false: Force the generation of the Skaffold config - -k, --kubernetes-manifest=[]: a path or a glob pattern to kubernetes manifests (can be non-existent) to be added to the kubectl deployer (overrides detection of kubernetes manifests). Repeat the flag for multiple entries. E.g.: skaffold init -k pod.yaml -k k8s/*.yml + -k, --kubernetes-manifest=[]: A path or a glob pattern to kubernetes manifests (can be non-existent) to be added to the kubectl deployer (overrides detection of kubernetes manifests). Repeat the flag for multiple entries. E.g.: skaffold init -k pod.yaml -k k8s/*.yml --skip-build=false: Skip generating build artifacts in Skaffold config Usage: @@ -680,6 +681,7 @@ Env vars: * `SKAFFOLD_ANALYZE` (same as `--analyze`) * `SKAFFOLD_ARTIFACT` (same as `--artifact`) * `SKAFFOLD_COMPOSE_FILE` (same as `--compose-file`) +* `SKAFFOLD_DEFAULT_KUSTOMIZATION` (same as `--default-kustomization`) * `SKAFFOLD_FILENAME` (same as `--filename`) * `SKAFFOLD_FORCE` (same as `--force`) * `SKAFFOLD_KUBERNETES_MANIFEST` (same as `--kubernetes-manifest`) diff --git a/examples/getting-started-kustomize/deployment.yaml b/examples/getting-started-kustomize/deployment.yaml index 5a0b005c556..b0ae0251d77 100644 --- a/examples/getting-started-kustomize/deployment.yaml +++ b/examples/getting-started-kustomize/deployment.yaml @@ -13,4 +13,3 @@ spec: metadata: labels: app: skaffold-kustomize - diff --git a/integration/examples/getting-started-kustomize/Dockerfile b/integration/examples/getting-started-kustomize/app/Dockerfile similarity index 100% rename from integration/examples/getting-started-kustomize/Dockerfile rename to integration/examples/getting-started-kustomize/app/Dockerfile diff --git a/integration/examples/getting-started-kustomize/main.go b/integration/examples/getting-started-kustomize/app/main.go similarity index 100% rename from integration/examples/getting-started-kustomize/main.go rename to integration/examples/getting-started-kustomize/app/main.go diff --git a/integration/examples/getting-started-kustomize/deployment.yaml b/integration/examples/getting-started-kustomize/base/deployment.yaml similarity index 72% rename from integration/examples/getting-started-kustomize/deployment.yaml rename to integration/examples/getting-started-kustomize/base/deployment.yaml index 5a0b005c556..87e074e8608 100644 --- a/integration/examples/getting-started-kustomize/deployment.yaml +++ b/integration/examples/getting-started-kustomize/base/deployment.yaml @@ -5,7 +5,6 @@ metadata: labels: app: skaffold-kustomize spec: - replicas: 1 selector: matchLabels: app: skaffold-kustomize @@ -13,4 +12,7 @@ spec: metadata: labels: app: skaffold-kustomize - + spec: + containers: + - name: skaffold-kustomize + image: skaffold-kustomize diff --git a/integration/examples/getting-started-kustomize/base/kustomization.yaml b/integration/examples/getting-started-kustomize/base/kustomization.yaml new file mode 100644 index 00000000000..88a04b54175 --- /dev/null +++ b/integration/examples/getting-started-kustomize/base/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml diff --git a/integration/examples/getting-started-kustomize/kustomization.yaml b/integration/examples/getting-started-kustomize/kustomization.yaml deleted file mode 100644 index 88b1320b5b9..00000000000 --- a/integration/examples/getting-started-kustomize/kustomization.yaml +++ /dev/null @@ -1,4 +0,0 @@ -resources: - - deployment.yaml -patches: - - patch.yaml diff --git a/integration/examples/getting-started-kustomize/overlays/dev/deployment.yaml b/integration/examples/getting-started-kustomize/overlays/dev/deployment.yaml new file mode 100644 index 00000000000..3cff8ae795d --- /dev/null +++ b/integration/examples/getting-started-kustomize/overlays/dev/deployment.yaml @@ -0,0 +1,6 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: skaffold-kustomize + labels: + env: dev diff --git a/integration/examples/getting-started-kustomize/overlays/dev/kustomization.yaml b/integration/examples/getting-started-kustomize/overlays/dev/kustomization.yaml new file mode 100644 index 00000000000..de4920f6d09 --- /dev/null +++ b/integration/examples/getting-started-kustomize/overlays/dev/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# namespace: dev +nameSuffix: -dev + +patchesStrategicMerge: +- deployment.yaml + +resources: +- ../../base diff --git a/integration/examples/getting-started-kustomize/overlays/prod/deployment.yaml b/integration/examples/getting-started-kustomize/overlays/prod/deployment.yaml new file mode 100644 index 00000000000..fceed536666 --- /dev/null +++ b/integration/examples/getting-started-kustomize/overlays/prod/deployment.yaml @@ -0,0 +1,8 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: skaffold-kustomize + labels: + env: prod +spec: + replicas: 3 diff --git a/integration/examples/getting-started-kustomize/overlays/prod/kustomization.yaml b/integration/examples/getting-started-kustomize/overlays/prod/kustomization.yaml new file mode 100644 index 00000000000..a3913c642ea --- /dev/null +++ b/integration/examples/getting-started-kustomize/overlays/prod/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# namespace: prod +nameSuffix: -prod + +patchesStrategicMerge: +- deployment.yaml + +resources: +- ../../base diff --git a/integration/examples/getting-started-kustomize/overlays/staging/deployment.yaml b/integration/examples/getting-started-kustomize/overlays/staging/deployment.yaml new file mode 100644 index 00000000000..53ae35bdc64 --- /dev/null +++ b/integration/examples/getting-started-kustomize/overlays/staging/deployment.yaml @@ -0,0 +1,6 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: skaffold-kustomize + labels: + env: staging diff --git a/integration/examples/getting-started-kustomize/overlays/staging/kustomization.yaml b/integration/examples/getting-started-kustomize/overlays/staging/kustomization.yaml new file mode 100644 index 00000000000..72dcce3eb54 --- /dev/null +++ b/integration/examples/getting-started-kustomize/overlays/staging/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# namespace: staging +nameSuffix: -staging + +patchesStrategicMerge: +- deployment.yaml + +resources: +- ../../base diff --git a/integration/examples/getting-started-kustomize/patch.yaml b/integration/examples/getting-started-kustomize/patch.yaml deleted file mode 100644 index 92a6b8b7223..00000000000 --- a/integration/examples/getting-started-kustomize/patch.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: skaffold-kustomize -spec: - template: - spec: - containers: - - name: skaffold-kustomize - image: hello-world diff --git a/integration/examples/getting-started-kustomize/skaffold.yaml b/integration/examples/getting-started-kustomize/skaffold.yaml index 46fdd59384f..0ff31b7edae 100644 --- a/integration/examples/getting-started-kustomize/skaffold.yaml +++ b/integration/examples/getting-started-kustomize/skaffold.yaml @@ -4,6 +4,20 @@ metadata: name: getting-started-kustomize build: artifacts: - - image: hello-world + - image: skaffold-kustomize + context: app deploy: - kustomize: {} + kustomize: + paths: + - overlays/dev +profiles: +- name: prod + deploy: + kustomize: + paths: + - overlays/prod +- name: staging + deploy: + kustomize: + paths: + - overlays/staging diff --git a/pkg/skaffold/deploy/kustomize.go b/pkg/skaffold/deploy/kustomize.go index 2bbbf26d4f8..794dca49eb1 100644 --- a/pkg/skaffold/deploy/kustomize.go +++ b/pkg/skaffold/deploy/kustomize.go @@ -45,6 +45,7 @@ import ( var ( DefaultKustomizePath = "." kustomizeFilePaths = []string{"kustomization.yaml", "kustomization.yml", "Kustomization"} + basePath = "base" ) type patchPath struct { @@ -261,7 +262,7 @@ func dependenciesForKustomization(dir string) ([]string, error) { candidates := append(content.Bases, content.Resources...) for _, candidate := range candidates { - // If the file doesn't exist locally, we can assume it's a remote file and + // If the file doesn't exist locally, we can assume it's a remote file and // skip it, since we can't monitor remote files. Kustomize itself will // handle invalid/missing files. local, mode := pathExistsLocally(candidate, dir) @@ -313,8 +314,7 @@ func dependenciesForKustomization(dir string) ([]string, error) { // A Kustomization config must be at the root of the directory. Kustomize will // error if more than one of these files exists so order doesn't matter. func findKustomizationConfig(dir string) (string, error) { - candidates := []string{"kustomization.yaml", "kustomization.yml", "Kustomization"} - for _, candidate := range candidates { + for _, candidate := range kustomizeFilePaths { if local, _ := pathExistsLocally(candidate, dir); local { return filepath.Join(dir, candidate), nil } @@ -368,6 +368,10 @@ func buildCommandArgs(buildArgs []string, kustomizePath string) []string { return args } +func IsKustomizationBase(path string) bool { + return filepath.Dir(path) == basePath +} + func IsKustomizationPath(path string) bool { filename := filepath.Base(path) for _, candidate := range kustomizeFilePaths { diff --git a/pkg/skaffold/initializer/analyze/analyze.go b/pkg/skaffold/initializer/analyze/analyze.go index 8f6f22489dc..33e8b732719 100644 --- a/pkg/skaffold/initializer/analyze/analyze.go +++ b/pkg/skaffold/initializer/analyze/analyze.go @@ -59,6 +59,10 @@ func (a *ProjectAnalysis) KustomizePaths() []string { return a.kustomizeAnalyzer.kustomizePaths } +func (a *ProjectAnalysis) KustomizeBases() []string { + return a.kustomizeAnalyzer.bases +} + func (a *ProjectAnalysis) analyzers() []analyzer { return []analyzer{ a.kubeAnalyzer, diff --git a/pkg/skaffold/initializer/analyze/kustomize.go b/pkg/skaffold/initializer/analyze/kustomize.go index b457f6a9673..bb156f10cbe 100644 --- a/pkg/skaffold/initializer/analyze/kustomize.go +++ b/pkg/skaffold/initializer/analyze/kustomize.go @@ -26,12 +26,18 @@ import ( // kustomizeAnalyzer is a Visitor during the directory analysis that finds kustomize files type kustomizeAnalyzer struct { directoryAnalyzer + + bases []string kustomizePaths []string } -func (k *kustomizeAnalyzer) analyzeFile(filePath string) error { - if !schema.IsSkaffoldConfig(filePath) && deploy.IsKustomizationPath(filePath) { - k.kustomizePaths = append(k.kustomizePaths, filepath.Dir(filePath)) +func (k *kustomizeAnalyzer) analyzeFile(path string) error { + switch { + case schema.IsSkaffoldConfig(path): + case deploy.IsKustomizationBase(path): + k.bases = append(k.bases, filepath.Dir(path)) + case deploy.IsKustomizationPath(path): + k.kustomizePaths = append(k.kustomizePaths, filepath.Dir(path)) } return nil } diff --git a/pkg/skaffold/initializer/config.go b/pkg/skaffold/initializer/config.go index 6675171d79a..952e42f2352 100644 --- a/pkg/skaffold/initializer/config.go +++ b/pkg/skaffold/initializer/config.go @@ -45,6 +45,8 @@ func generateSkaffoldConfig(b build.Initializer, d deploy.Initializer) *latest.S warnings.Printf("Couldn't generate default config name: %s", err.Error()) } + deploy, profiles := d.DeployConfig() + return &latest.SkaffoldConfig{ APIVersion: latest.Version, Kind: "Config", @@ -53,8 +55,9 @@ func generateSkaffoldConfig(b build.Initializer, d deploy.Initializer) *latest.S }, Pipeline: latest.Pipeline{ Build: b.BuildConfig(), - Deploy: d.DeployConfig(), + Deploy: deploy, }, + Profiles: profiles, } } diff --git a/pkg/skaffold/initializer/config/config.go b/pkg/skaffold/initializer/config/config.go index b43c9dd7a56..28eb5435a22 100644 --- a/pkg/skaffold/initializer/config/config.go +++ b/pkg/skaffold/initializer/config/config.go @@ -20,7 +20,9 @@ import "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" // Config contains all the parameters for the initializer package type Config struct { + BuildpacksBuilder string ComposeFile string + DefaultKustomization string CliArtifacts []string CliKubernetesManifests []string SkipBuild bool @@ -32,7 +34,6 @@ type Config struct { EnableBuildpacksInit bool EnableNewInitFormat bool EnableManifestGeneration bool - BuildpacksBuilder string Opts config.SkaffoldOptions MaxFileSize int64 } diff --git a/pkg/skaffold/initializer/config_test.go b/pkg/skaffold/initializer/config_test.go index c350f0c7ddf..b09b0600f9a 100644 --- a/pkg/skaffold/initializer/config_test.go +++ b/pkg/skaffold/initializer/config_test.go @@ -29,11 +29,12 @@ import ( ) type stubDeploymentInitializer struct { - config latest.DeployConfig + config latest.DeployConfig + profiles []latest.Profile } -func (s stubDeploymentInitializer) DeployConfig() latest.DeployConfig { - return s.config +func (s stubDeploymentInitializer) DeployConfig() (latest.DeployConfig, []latest.Profile) { + return s.config, s.profiles } func (s stubDeploymentInitializer) GetImages() []string { @@ -75,6 +76,7 @@ func TestGenerateSkaffoldConfig(t *testing.T) { name string expectedSkaffoldConfig *latest.SkaffoldConfig deployConfig latest.DeployConfig + profiles []latest.Profile builderConfigPairs []build.BuilderImagePair getWd func() (string, error) }{ @@ -144,6 +146,7 @@ func TestGenerateSkaffoldConfig(t *testing.T) { testutil.Run(t, test.name, func(t *testutil.T) { deploymentInitializer := stubDeploymentInitializer{ test.deployConfig, + test.profiles, } buildInitializer := stubBuildInitializer{ test.builderConfigPairs, diff --git a/pkg/skaffold/initializer/deploy/init.go b/pkg/skaffold/initializer/deploy/init.go index 37dadb9cf58..5d40720cad8 100644 --- a/pkg/skaffold/initializer/deploy/init.go +++ b/pkg/skaffold/initializer/deploy/init.go @@ -25,12 +25,12 @@ type Error string func (e Error) Error() string { return string(e) } -const NoManifest = Error("one or more Kubernetes manifests are required to run skaffold") +const NoManifest = Error("one or more valid Kubernetes manifests are required to run skaffold") // Initializer detects a deployment type and is able to extract image names from it type Initializer interface { // deployConfig generates Deploy Config for skaffold configuration. - DeployConfig() latest.DeployConfig + DeployConfig() (latest.DeployConfig, []latest.Profile) // GetImages fetches all the images defined in the manifest files. GetImages() []string // Validate ensures preconditions are met before generating a skaffold config @@ -43,14 +43,14 @@ type cliDeployInit struct { cliKubernetesManifests []string } -func (c *cliDeployInit) DeployConfig() latest.DeployConfig { +func (c *cliDeployInit) DeployConfig() (latest.DeployConfig, []latest.Profile) { return latest.DeployConfig{ DeployType: latest.DeployType{ KubectlDeploy: &latest.KubectlDeploy{ Manifests: c.cliKubernetesManifests, }, }, - } + }, nil } func (c *cliDeployInit) GetImages() []string { @@ -69,8 +69,8 @@ func (c *cliDeployInit) AddManifestForImage(string, string) {} type emptyDeployInit struct { } -func (e *emptyDeployInit) DeployConfig() latest.DeployConfig { - return latest.DeployConfig{} +func (e *emptyDeployInit) DeployConfig() (latest.DeployConfig, []latest.Profile) { + return latest.DeployConfig{}, nil } func (e *emptyDeployInit) GetImages() []string { @@ -86,14 +86,14 @@ func (e *emptyDeployInit) AddManifestForImage(string, string) {} // if any CLI manifests are provided, we always use those as part of a kubectl deploy first // if not, then if a kustomization yaml is found, we use that next // otherwise, default to a kubectl deploy. -func NewInitializer(manifests []string, kustomizations []string, c config.Config) Initializer { +func NewInitializer(manifests, bases, kustomizations []string, c config.Config) Initializer { switch { case c.SkipDeploy: return &emptyDeployInit{} case len(c.CliKubernetesManifests) > 0: return &cliDeployInit{c.CliKubernetesManifests} case len(kustomizations) > 0: - return newKustomizeInitializer(kustomizations, manifests) + return newKustomizeInitializer(c.DefaultKustomization, bases, kustomizations, manifests) default: return newKubectlInitializer(manifests) } diff --git a/pkg/skaffold/initializer/deploy/kubectl.go b/pkg/skaffold/initializer/deploy/kubectl.go index 369cc936cbc..beebc1d6c41 100644 --- a/pkg/skaffold/initializer/deploy/kubectl.go +++ b/pkg/skaffold/initializer/deploy/kubectl.go @@ -48,14 +48,14 @@ func newKubectlInitializer(potentialConfigs []string) *kubectl { // deployConfig implements the Initializer interface and generates // skaffold kubectl deployment config. -func (k *kubectl) DeployConfig() latest.DeployConfig { +func (k *kubectl) DeployConfig() (latest.DeployConfig, []latest.Profile) { return latest.DeployConfig{ DeployType: latest.DeployType{ KubectlDeploy: &latest.KubectlDeploy{ Manifests: k.configs, }, }, - } + }, nil } // GetImages implements the Initializer interface and lists all the diff --git a/pkg/skaffold/initializer/deploy/kubectl_test.go b/pkg/skaffold/initializer/deploy/kubectl_test.go index ba558e9c48e..5c378f10561 100644 --- a/pkg/skaffold/initializer/deploy/kubectl_test.go +++ b/pkg/skaffold/initializer/deploy/kubectl_test.go @@ -47,7 +47,8 @@ spec: }, }, } - testutil.CheckDeepEqual(t, expectedConfig, k.DeployConfig()) + deployConfig, _ := k.DeployConfig() + testutil.CheckDeepEqual(t, expectedConfig, deployConfig) } func TestParseImagesFromKubernetesYaml(t *testing.T) { @@ -58,14 +59,14 @@ func TestParseImagesFromKubernetesYaml(t *testing.T) { shouldErr bool }{ { - description: "incorrect k8 yaml", + description: "incorrect k8s yaml", contents: `no apiVersion: t kind: Pod`, images: nil, shouldErr: true, }, { - description: "correct k8 yaml", + description: "correct k8s yaml", contents: `apiVersion: v1 kind: Pod metadata: @@ -129,31 +130,31 @@ func TestIsKubernetesManifest(t *testing.T) { expected bool }{ { - description: "valid k8 yaml filename format", + description: "valid k8s yaml filename format", filename: "test1.yaml", content: "apiVersion: v1\nkind: Service\nmetadata:\n name: test\n", expected: true, }, { - description: "valid k8 json filename format", + description: "valid k8s json filename format", filename: "test1.json", content: `{"apiVersion":"v1","kind":"Service","metadata":{"name": "test"}}`, expected: true, }, { - description: "valid k8 yaml filename format", + description: "valid k8s yaml filename format", filename: "test1.yml", content: "apiVersion: v1\nkind: Service\nmetadata:\n name: test\n", expected: true, }, { - description: "invalid k8 yaml", + description: "invalid k8s yaml", filename: "test1.yaml", content: "key: value", expected: false, }, { - description: "invalid k8 json", + description: "invalid k8s json", filename: "test1.json", content: `{}`, expected: false, diff --git a/pkg/skaffold/initializer/deploy/kustomize.go b/pkg/skaffold/initializer/deploy/kustomize.go index fa6711a8baa..6bbe647bba3 100644 --- a/pkg/skaffold/initializer/deploy/kustomize.go +++ b/pkg/skaffold/initializer/deploy/kustomize.go @@ -17,6 +17,10 @@ limitations under the License. package deploy import ( + "path/filepath" + + "github.com/sirupsen/logrus" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" @@ -24,12 +28,14 @@ import ( // kustomize implements deploymentInitializer for the kustomize deployer. type kustomize struct { - kustomizations []string - images []string + defaultKustomization string + kustomizations []string + bases []string + images []string } // newKustomizeInitializer returns a kustomize config generator. -func newKustomizeInitializer(kustomizations []string, potentialConfigs []string) *kustomize { +func newKustomizeInitializer(defaultKustomization string, bases, kustomizations, potentialConfigs []string) *kustomize { var images []string for _, file := range potentialConfigs { imgs, err := kubernetes.ParseImagesFromKubernetesYaml(file) @@ -38,28 +44,90 @@ func newKustomizeInitializer(kustomizations []string, potentialConfigs []string) } } return &kustomize{ - images: images, - kustomizations: kustomizations, + defaultKustomization: defaultKustomization, + images: images, + bases: bases, + kustomizations: kustomizations, } } // deployConfig implements the Initializer interface and generates // a kustomize deployment config. -func (k *kustomize) DeployConfig() latest.DeployConfig { +func (k *kustomize) DeployConfig() (latest.DeployConfig, []latest.Profile) { var kustomizeConfig *latest.KustomizeDeploy - // if we only have the default path, leave the config empty - it's cleaner - if len(k.kustomizations) == 1 && k.kustomizations[0] == deploy.DefaultKustomizePath { - kustomizeConfig = &latest.KustomizeDeploy{} - } else { - kustomizeConfig = &latest.KustomizeDeploy{ - KustomizePaths: k.kustomizations, + var profiles []latest.Profile + + // if there's only one kustomize path, either leave it blank (if it's the default path), + // or generate a config with that single path and return it + if len(k.kustomizations) == 1 { + if k.kustomizations[0] == deploy.DefaultKustomizePath { + kustomizeConfig = &latest.KustomizeDeploy{} + } else { + kustomizeConfig = &latest.KustomizeDeploy{ + KustomizePaths: k.kustomizations, + } } + return latest.DeployConfig{ + DeployType: latest.DeployType{ + KustomizeDeploy: kustomizeConfig, + }, + }, nil } + + // if there are multiple paths, generate a config that chooses a default + // kustomization based on our heuristic, and creates separate profiles + // for all other overlays in the project + defaultKustomization := k.defaultKustomization + if defaultKustomization == "" { + // either choose one that's called "dev", or else the first one that isn't called "prod" + dev, prod := -1, -1 + for i, kustomization := range k.kustomizations { + switch filepath.Base(kustomization) { + case "dev": + dev = i + case "prod": + prod = i + default: + } + } + + switch { + case dev != -1: + defaultKustomization = k.kustomizations[dev] + case prod == 0: + defaultKustomization = k.kustomizations[1] + default: + defaultKustomization = k.kustomizations[0] + } + logrus.Warnf("multiple kustomizations found but no default provided - defaulting to %s", defaultKustomization) + } + + for _, kustomization := range k.kustomizations { + if kustomization == defaultKustomization { + kustomizeConfig = &latest.KustomizeDeploy{ + KustomizePaths: []string{defaultKustomization}, + } + } else { + profiles = append(profiles, latest.Profile{ + Name: filepath.Base(kustomization), + Pipeline: latest.Pipeline{ + Deploy: latest.DeployConfig{ + DeployType: latest.DeployType{ + KustomizeDeploy: &latest.KustomizeDeploy{ + KustomizePaths: []string{kustomization}, + }, + }, + }, + }, + }) + } + } + return latest.DeployConfig{ DeployType: latest.DeployType{ KustomizeDeploy: kustomizeConfig, }, - } + }, profiles } // GetImages implements the Initializer interface and lists all the diff --git a/pkg/skaffold/initializer/deploy/kustomize_test.go b/pkg/skaffold/initializer/deploy/kustomize_test.go new file mode 100644 index 00000000000..9117ed93bb9 --- /dev/null +++ b/pkg/skaffold/initializer/deploy/kustomize_test.go @@ -0,0 +1,166 @@ +/* +Copyright 2020 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 deploy + +import ( + "path/filepath" + "testing" + + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" + "github.com/GoogleContainerTools/skaffold/testutil" +) + +var ( + baseDeployment = `apiVersion: v1 +kind: Pod +metadata: + name: getting-started +spec: + containers: + - name: getting-started + image: skaffold-example` + + baseKustomization = `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml` + + overlayDeployment = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: skaffold-kustomize + labels: + env: overlay` + + overlayKustomization = `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +nameSuffix: -overlay + +patchesStrategicMerge: +- deployment.yaml + +resources: +- ../../base` +) + +func TestGenerateKustomizePipeline(t *testing.T) { + tests := []struct { + description string + base string + baseKustomization string + overlays map[string]string + overlayKustomizations map[string]string + expectedConfig latest.SkaffoldConfig + }{ + { + description: "single overlay", + base: baseDeployment, + baseKustomization: baseKustomization, + overlays: map[string]string{"dev": overlayDeployment}, + overlayKustomizations: map[string]string{"dev": overlayKustomization}, + expectedConfig: latest.SkaffoldConfig{ + Pipeline: latest.Pipeline{ + Deploy: latest.DeployConfig{ + DeployType: latest.DeployType{ + KustomizeDeploy: &latest.KustomizeDeploy{ + KustomizePaths: []string{filepath.Join("overlays", "dev")}, + }, + }, + }, + }, + }, + }, + { + description: "three overlays", + base: baseDeployment, + baseKustomization: baseKustomization, + overlays: map[string]string{ + "foo": overlayDeployment, + "bar": overlayDeployment, + "baz": overlayDeployment, + }, + overlayKustomizations: map[string]string{ + "foo": overlayKustomization, + "bar": overlayKustomization, + "baz": overlayKustomization, + }, + expectedConfig: latest.SkaffoldConfig{ + Pipeline: latest.Pipeline{ + Deploy: latest.DeployConfig{ + DeployType: latest.DeployType{ + KustomizeDeploy: &latest.KustomizeDeploy{ + KustomizePaths: []string{filepath.Join("overlays", "foo")}, + }, + }, + }, + }, + Profiles: []latest.Profile{ + { + Name: "bar", + Pipeline: latest.Pipeline{ + Deploy: latest.DeployConfig{ + DeployType: latest.DeployType{ + KustomizeDeploy: &latest.KustomizeDeploy{ + KustomizePaths: []string{filepath.Join("overlays", "bar")}, + }, + }, + }, + }, + }, + { + Name: "baz", + Pipeline: latest.Pipeline{ + Deploy: latest.DeployConfig{ + DeployType: latest.DeployType{ + KustomizeDeploy: &latest.KustomizeDeploy{ + KustomizePaths: []string{filepath.Join("overlays", "baz")}, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + testutil.Run(t, test.description, func(tt *testutil.T) { + tmpDir := testutil.NewTempDir(t) + + var overlays []string + manifests := []string{filepath.Join("base", "deployment.yaml")} + + tmpDir.Write(filepath.Join("base", "deployment.yaml"), test.base) + tmpDir.Write(filepath.Join("base", "kustomization.yaml"), test.baseKustomization) + for name, overlay := range test.overlays { + overlays = append(overlays, filepath.Join("overlays", name)) + manifests = append(manifests, filepath.Join("overlays", name, "deployment.yaml")) + tmpDir.Write(filepath.Join("overlays", name, "deployment.yaml"), overlay) + tmpDir.Write(filepath.Join("overlays", name, "kustomization.yaml"), test.overlayKustomizations[name]) + } + + k := newKustomizeInitializer("", []string{test.base}, overlays, manifests) + + deployConfig, profiles := k.DeployConfig() + testutil.CheckDeepEqual(t, test.expectedConfig.Pipeline.Deploy, deployConfig) + testutil.CheckDeepEqual(t, test.expectedConfig.Profiles, profiles) + }) + } +} diff --git a/pkg/skaffold/initializer/init.go b/pkg/skaffold/initializer/init.go index d92f162b6e8..1d469793d09 100644 --- a/pkg/skaffold/initializer/init.go +++ b/pkg/skaffold/initializer/init.go @@ -44,7 +44,7 @@ func DoInit(ctx context.Context, out io.Writer, c config.Config) error { return err } - deployInitializer := deploy.NewInitializer(a.Manifests(), a.KustomizePaths(), c) + deployInitializer := deploy.NewInitializer(a.Manifests(), a.KustomizeBases(), a.KustomizePaths(), c) images := deployInitializer.GetImages() buildInitializer := build.NewInitializer(a.Builders(), c)