diff --git a/cmd/skaffold/app/cmd/apply.go b/cmd/skaffold/app/cmd/apply.go new file mode 100644 index 00000000000..bb1a512ba9d --- /dev/null +++ b/cmd/skaffold/app/cmd/apply.go @@ -0,0 +1,74 @@ +/* +Copyright 2021 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 cmd + +import ( + "context" + "errors" + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" +) + +// NewCmdApply describes the CLI command to apply manifests to a cluster. +func NewCmdApply() *cobra.Command { + return NewCmd("apply"). + WithDescription("Apply hydrated manifests to a cluster"). + WithExample("Hydrate Kubernetes pod manifest first", "render --output rendered-pod.yaml"). + WithExample("Then create resources on your cluster from that hydrated manifest", "apply rendered-pod.yaml"). + WithCommonFlags(). + WithHouseKeepingMessages(). + WithArgs(func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return errors.New("`apply` requires at least one manifest argument") + } + return nil + }, doApply) +} + +func doApply(ctx context.Context, out io.Writer, args []string) error { + // force set apply boolean to select default options in runner creation + opts.Apply = true + opts.HydratedManifests = args + if err := validateManifests(args); err != nil { + return err + } + return withRunner(ctx, out, func(r runner.Runner, configs []*latest.SkaffoldConfig) error { + return r.Apply(ctx, out) + }) +} + +func validateManifests(manifests []string) error { + for _, m := range manifests { + if _, err := os.Open(m); err != nil { + if errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("cannot find provided file %s", m) + } + return fmt.Errorf("unable to open provided file %s", m) + } + if !kubernetes.IsKubernetesManifest(m) { + return fmt.Errorf("%s is not a valid Kubernetes manifest", m) + } + } + return nil +} diff --git a/cmd/skaffold/app/cmd/cmd.go b/cmd/skaffold/app/cmd/cmd.go index 23e2e56626b..0612d39463d 100644 --- a/cmd/skaffold/app/cmd/cmd.go +++ b/cmd/skaffold/app/cmd/cmd.go @@ -172,6 +172,7 @@ func NewSkaffoldCommand(out, err io.Writer) *cobra.Command { NewCmdDeploy(), NewCmdDelete(), NewCmdRender(), + NewCmdApply(), }, }, { diff --git a/cmd/skaffold/app/cmd/commands.go b/cmd/skaffold/app/cmd/commands.go index 788b60b54b5..73f68f127d6 100644 --- a/cmd/skaffold/app/cmd/commands.go +++ b/cmd/skaffold/app/cmd/commands.go @@ -29,6 +29,7 @@ import ( // Builder is used to build cobra commands. type Builder interface { + WithArgs(cobra.PositionalArgs, func(context.Context, io.Writer, []string) error) *cobra.Command WithDescription(description string) Builder WithLongDescription(long string) Builder WithExample(comment, command string) Builder @@ -132,6 +133,20 @@ func (b *builder) NoArgs(action func(context.Context, io.Writer) error) *cobra.C return &b.cmd } +func (b *builder) WithArgs(f cobra.PositionalArgs, action func(context.Context, io.Writer, []string) error) *cobra.Command { + b.cmd.Args = f + b.cmd.RunE = func(_ *cobra.Command, args []string) error { + err := handleWellKnownErrors(action(b.cmd.Context(), b.cmd.OutOrStdout(), args)) + // clean up server at end of the execution since post run hooks are only executed if + // RunE is successful + if shutdownAPIServer != nil { + shutdownAPIServer() + } + return err + } + return &b.cmd +} + func handleWellKnownErrors(err error) error { if err == nil { return err diff --git a/cmd/skaffold/app/cmd/flags.go b/cmd/skaffold/app/cmd/flags.go index d0f28d7a340..c207455ccfe 100644 --- a/cmd/skaffold/app/cmd/flags.go +++ b/cmd/skaffold/app/cmd/flags.go @@ -83,7 +83,7 @@ var flagRegistry = []Flag{ Value: &opts.Profiles, DefValue: []string{}, FlagAddMethod: "StringSliceVar", - DefinedOn: []string{"dev", "run", "debug", "deploy", "render", "build", "delete", "diagnose"}, + DefinedOn: []string{"dev", "run", "debug", "deploy", "render", "build", "delete", "diagnose", "apply"}, }, { Name: "namespace", @@ -92,7 +92,7 @@ var flagRegistry = []Flag{ Value: &opts.Namespace, DefValue: "", FlagAddMethod: "StringVar", - DefinedOn: []string{"dev", "run", "debug", "deploy", "render", "build", "delete"}, + DefinedOn: []string{"dev", "run", "debug", "deploy", "render", "build", "delete", "apply"}, }, { Name: "default-repo", @@ -201,7 +201,7 @@ var flagRegistry = []Flag{ "debug": true, }, FlagAddMethod: "BoolVar", - DefinedOn: []string{"dev", "run", "debug", "deploy"}, + DefinedOn: []string{"dev", "run", "debug", "deploy", "apply"}, IsEnum: true, }, { @@ -210,7 +210,7 @@ var flagRegistry = []Flag{ Value: &opts.Force, DefValue: false, FlagAddMethod: "BoolVar", - DefinedOn: []string{"deploy", "dev", "run", "debug"}, + DefinedOn: []string{"deploy", "dev", "run", "debug", "apply"}, IsEnum: true, }, { @@ -264,7 +264,7 @@ var flagRegistry = []Flag{ Value: &opts.StatusCheck, DefValue: true, FlagAddMethod: "BoolVar", - DefinedOn: []string{"dev", "debug", "deploy", "run"}, + DefinedOn: []string{"dev", "debug", "deploy", "run", "apply"}, IsEnum: true, }, { @@ -291,7 +291,7 @@ var flagRegistry = []Flag{ Value: &opts.GlobalConfig, DefValue: "", FlagAddMethod: "StringVar", - DefinedOn: []string{"run", "dev", "debug", "build", "deploy", "delete", "diagnose"}, + DefinedOn: []string{"run", "dev", "debug", "build", "deploy", "delete", "diagnose", "apply"}, }, { Name: "kube-context", @@ -299,7 +299,7 @@ var flagRegistry = []Flag{ Value: &opts.KubeContext, DefValue: "", FlagAddMethod: "StringVar", - DefinedOn: []string{"build", "debug", "delete", "deploy", "dev", "run", "filter"}, + DefinedOn: []string{"build", "debug", "delete", "deploy", "dev", "run", "filter", "apply"}, }, { Name: "kubeconfig", @@ -307,7 +307,7 @@ var flagRegistry = []Flag{ Value: &opts.KubeConfig, DefValue: "", FlagAddMethod: "StringVar", - DefinedOn: []string{"build", "debug", "delete", "deploy", "dev", "run", "filter"}, + DefinedOn: []string{"build", "debug", "delete", "deploy", "dev", "run", "filter", "apply"}, }, { Name: "tag", diff --git a/docs/content/en/docs/references/cli/_index.md b/docs/content/en/docs/references/cli/_index.md index a5242b12c0c..00f634de4c7 100644 --- a/docs/content/en/docs/references/cli/_index.md +++ b/docs/content/en/docs/references/cli/_index.md @@ -77,6 +77,7 @@ Pipeline building blocks for CI/CD: deploy Deploy pre-built artifacts delete Delete the deployed application render [alpha] Perform all image builds, and output rendered Kubernetes manifests + apply Apply hydrated manifests to a cluster Getting started with a new project: init [alpha] Generate configuration for deploying an application @@ -104,6 +105,54 @@ Env vars: * `SKAFFOLD_UPDATE_CHECK` (same as `--update-check`) * `SKAFFOLD_VERBOSITY` (same as `--verbosity`) +### skaffold apply + +Apply hydrated manifests to a cluster + +``` + + +Examples: + # Hydrate Kubernetes pod manifest first + skaffold render --output rendered-pod.yaml + + # Then create resources on your cluster from that hydrated manifest + skaffold apply rendered-pod.yaml + +Options: + -c, --config='': File for global configurations (defaults to $HOME/.skaffold/config) + -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file + --force=false: Recreate Kubernetes resources if necessary for deployment, warning: might cause downtime! + --kube-context='': Deploy to this Kubernetes context + --kubeconfig='': Path to the kubeconfig file to use for CLI requests. + -m, --module=[]: Filter Skaffold configs to only the provided named modules + -n, --namespace='': Run deployments in the specified namespace + -p, --profile=[]: Activate profiles by name (prefixed with `-` to disable a profile) + --remote-cache-dir='': Specify the location of the git repositories cache (default $HOME/.skaffold/repos) + --status-check=true: Wait for deployed resources to stabilize + --tail=false: Stream logs from deployed objects + +Usage: + skaffold apply [options] + +Use "skaffold options" for a list of global command-line options (applies to all commands). + + +``` +Env vars: + +* `SKAFFOLD_CONFIG` (same as `--config`) +* `SKAFFOLD_FILENAME` (same as `--filename`) +* `SKAFFOLD_FORCE` (same as `--force`) +* `SKAFFOLD_KUBE_CONTEXT` (same as `--kube-context`) +* `SKAFFOLD_KUBECONFIG` (same as `--kubeconfig`) +* `SKAFFOLD_MODULE` (same as `--module`) +* `SKAFFOLD_NAMESPACE` (same as `--namespace`) +* `SKAFFOLD_PROFILE` (same as `--profile`) +* `SKAFFOLD_REMOTE_CACHE_DIR` (same as `--remote-cache-dir`) +* `SKAFFOLD_STATUS_CHECK` (same as `--status-check`) +* `SKAFFOLD_TAIL` (same as `--tail`) + ### skaffold build Build the artifacts diff --git a/docs/content/en/docs/workflows/ci-cd.md b/docs/content/en/docs/workflows/ci-cd.md index 2a308d27ce8..d68987fe605 100644 --- a/docs/content/en/docs/workflows/ci-cd.md +++ b/docs/content/en/docs/workflows/ci-cd.md @@ -4,20 +4,26 @@ linkTitle: "Continuous Delivery" weight: 40 --- -Skaffold offers several sub-commands for its workflows that make it quite flexible when integrating with CI/CD pipelines. +Skaffold provides several features and sub-command "building blocks" that make it very useful for integrating with (or creating entirely new) CI/CD pipelines. +The ability to use the same `skaffold.yaml` for iterative development and continuous delivery eases handing off an application from a development team to an ops team. +Let's start with the simplest use case: a single, full deployment of your application. ## `skaffold run` +{{< maturity "run" >}} -`skaffold run` is a single command for a one-off deployment. It includes all the following phases as it builds, tags, deploys and waits for the deployment to succeed if specified. -We recommend `skaffold run` for a simple Continuous Delivery setup, where it is sufficient to have a single step that deploys from version control to a cluster. -For more sophisticated Continuous Delivery pipelines, Skaffold offers building blocks that are described next: +`skaffold run` is a single command for a one-off deployment. It runs through every major phase of the Skaffold lifecycle: building your application images, tagging these images (and optionally pushing them to a remote registry), deploying your application to the target cluster, and monitoring the created resources for readiness. + +We recommend `skaffold run` for the simplest Continuous Delivery setup, where it is sufficient to have a single step that deploys from version control to a cluster. + +For more sophisticated Continuous Delivery pipelines, Skaffold offers building blocks: - [healthcheck]({{}}) - wait for `deployments` to stabilize and succeed only if all deployments are successful - [`skaffold build`]({{}}) - build, tag and push artifacts to a registry - [`skaffold deploy`]({{}}) - deploy built artifacts to a cluster -- [`skaffold render`]({{}}) - export the transformed Kubernetes manifests for GitOps workflows +- [`skaffold render`]({{}}) - export the transformed Kubernetes manifests for GitOps workflows +- [`skaffold apply`]({{}}) - send hydrated Kubernetes manifests to the API server to create resources on the target cluster ## Waiting for Skaffold deployments using `healthcheck` {{< maturity "deploy.status_check" >}} @@ -106,7 +112,7 @@ Waiting for deployments to stabilize FATA[0006] 1/1 deployment(s) failed ``` -## `skaffold build | skaffold deploy` +## Traditional continuous delivery: `skaffold build | skaffold deploy` `skaffold build` will build your project's artifacts, and push the build images to the specified registry. If your project is already configured to run with Skaffold, `skaffold build` can be a very lightweight way of setting up builds for your CI pipeline. Passing the `--file-output` flag to Skaffold build will also write out your built artifacts in JSON format to a file on disk, which can then by passed to `skaffold deploy` later on. This is a great way of "committing" your artifacts when they have reached a state that you're comfortable with, especially for projects with multiple artifacts for multiple services. @@ -147,15 +153,39 @@ Starting deploy... ``` -## `skaffold render` -{{< maturity "render" >}} +## GitOps-style continuous delivery: `skaffold render` | `skaffold apply` +{{< maturity "apply" >}} + +GitOps-based CD pipelines traditionally see fully-hydrated Kubernetes manifests committed to a configuration Git repository (separate from the application source), which triggers a deployment pipeline that applies the changes to resources on the cluster. Skaffold has two built-in commands that enable easy GitOps pipeline workflows - `skaffold render` and `skaffold apply`. + +`skaffold render` builds all application images from your artifacts, templates the newly-generated image tags into your Kubernetes manifests (based on your project's deployment configuration), and then prints out the final hydrated manifests to a file or your terminal. This allows you to capture the full, declarative state of your application in configuration rather than actually applying changes to your cluster, and use this configuration in a GitOps pipeline by committing it to a separate Git repository. + +`skaffold apply` consumes one or more fully-hydrated Kubernetes manifests, and then sends the results directly to the Kubernetes control plane via `kubectl` to create resources on the target cluster. After creating the resources on your cluster, `skaffold apply` uses Skaffold's built-in health checking to monitor the created resources for readiness. See [resource health checks]({{}}) for more information on how Skaffold's resource health checking works. + +*Note: `skaffold apply` always uses `kubectl` to deploy resources to a target cluster, regardless of deployment configuration in the provided skaffold.yaml. Only a small subset of deploy configuration is honored when running `skaffold apply`:* +* deploy.statusCheckDeadlineSeconds +* deploy.kubeContext +* deploy.logs.prefix +* deploy.kubectl.flags +* deploy.kubectl.defaultNamespace +* deploy.kustomize.flags +* deploy.kustomize.defaultNamespace + +{{}} +`skaffold apply` attempts to honor the deployment configuration mentioned above. But when conflicting configuration is detected in a multi-configuration project, `skaffold apply` will not work. +{{}} + +`skaffold apply` works with any arbitrary Kubernetes YAML, whether it was generated by Skaffold or not, making it an ideal counterpart to `skaffold render`. -Skaffold also has another built-in command, `skaffold render`, that will perform builds on all artifacts in your project, template the newly built image tags into your Kubernetes deployment configuration files (based on your configured deployer), and instead of sending these through the deployment process, print out the final deployment artifacts. This allows you to snapshot your project's builds, but also integrate those builds into your deployment configs to snapshot your deployment as well. This can be very useful when integrating with GitOps based workflows: these templated deployment configurations can be committed to a Git repository as a way to deploy using GitOps. +### Example: Hydrating Kubernetes resources using `skaffold render`, then sending them to the cluster using `skaffold apply`: -Example of running `skaffold render` to render Kubernetes manifests, then sending them directly to `kubectl`: +First, use `skaffold render` to hydrate the Kubernetes resource file with a newly-built image tag: -Running `skaffold render --output render.txt && cat render.txt` outputs: +```code +$ skaffold render --output render.yaml +``` ```yaml +# render.yaml apiVersion: v1 kind: Pod metadata: @@ -163,26 +193,17 @@ metadata: namespace: default spec: containers: - - image: gcr.io/k8s-skaffold/skaffold-example:v0.41.0-57-gbee90013@sha256:eeffb639f53368c4039b02a4d337bde44e3acc728b309a84353d4857ee95c369 + - image: gcr.io/k8s-skaffold/skaffold-example:v1.19.0-89-gdbedd2a20-dirty name: getting-started ``` -We can then pipe this yaml to kubectl: -```code -cat render.txt | kubectl apply -f - -``` -which shows -``` -pod/getting-started configured -``` - -Or, if we want to skip the file writing altogether: +Then, we can apply this output directly to the cluster: ```code -skaffold render | kubectl apply -f - -``` +$ skaffold apply render.yaml -gives us the one line output telling us the only thing we need to know: -```code -pod/getting-started configured +Starting deploy... + - pod/getting-started created +Waiting for deployments to stabilize... +Deployments stabilized in 49.277055ms ``` diff --git a/docs/data/maturity.json b/docs/data/maturity.json index 33104cf9d35..b79a14078ef 100644 --- a/docs/data/maturity.json +++ b/docs/data/maturity.json @@ -7,6 +7,13 @@ "description": "Control API, Event API and State API", "url": "/docs/design/api" }, + "apply": { + "deploy": "x", + "area": "Deploy", + "feature": "Apply", + "maturity": "alpha", + "description": "Use hydrated Kubernetes manifests to create resources on the cluster" + }, "build.buildpacks": { "build": "x", "area": "Build", diff --git a/integration/apply_test.go b/integration/apply_test.go new file mode 100644 index 00000000000..975c281dc76 --- /dev/null +++ b/integration/apply_test.go @@ -0,0 +1,55 @@ +/* +Copyright 2021 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 integration + +import ( + "testing" + + "github.com/GoogleContainerTools/skaffold/integration/skaffold" + "github.com/GoogleContainerTools/skaffold/testutil" +) + +func TestDiagnoseRenderApply(t *testing.T) { + // This test verifies that `skaffold apply` can consume the output of both `skaffold render` and `skaffold diagnose`. + + // 1. Run `skaffold diagnose --yaml-only` to resolve and combine skaffold configs for a multi-config project + // 2. Run `skaffold render` using this config to hydrate manifests + // 3. Run `skaffold apply` using the config from `diagnose` and the manifests from `render` to create resources on the cluster. + + testutil.Run(t, "DiagnoseRenderApply", func(t *testutil.T) { + MarkIntegrationTest(t.T, NeedsGcp) + ns, client := SetupNamespace(t.T) + + out := skaffold.Diagnose("--yaml-only").InDir("examples/multi-config-microservices").RunOrFailOutput(t.T) + + tmpDir := testutil.NewTempDir(t.T) + tmpDir.Chdir() + + tmpDir.Write("skaffold-diagnose.yaml", string(out)) + + out = skaffold.Render("--add-skaffold-labels=false", "-f", "skaffold-diagnose.yaml").InNs(ns.Name).RunOrFailOutput(t.T) + tmpDir.Write("render.yaml", string(out)) + + skaffold.Apply("render.yaml", "-f", "skaffold-diagnose.yaml").InNs(ns.Name).RunOrFail(t.T) + + depApp := client.GetDeployment("leeroy-app") + t.CheckNotNil(depApp) + + depWeb := client.GetDeployment("leeroy-web") + t.CheckNotNil(depWeb) + }) +} diff --git a/integration/skaffold/helper.go b/integration/skaffold/helper.go index 9efb3af0580..126c911443c 100644 --- a/integration/skaffold/helper.go +++ b/integration/skaffold/helper.go @@ -47,6 +47,11 @@ type RunBuilder struct { stdin []byte } +// Apply runs `skaffold apply` with the given arguments. +func Apply(args ...string) *RunBuilder { + return withDefaults("apply", args) +} + // Dev runs `skaffold dev` with the given arguments. func Dev(args ...string) *RunBuilder { return withDefaults("dev", args) diff --git a/pkg/skaffold/config/options.go b/pkg/skaffold/config/options.go index 47ebe636f23..a10be488cd6 100644 --- a/pkg/skaffold/config/options.go +++ b/pkg/skaffold/config/options.go @@ -42,8 +42,11 @@ type WaitForDeletions struct { type SkaffoldOptions struct { ConfigurationFile string ConfigurationFilter []string + HydratedManifests []string GlobalConfig string EventLogFile string + RenderOutput string + Apply bool Cleanup bool Notification bool Tail bool @@ -60,7 +63,6 @@ type SkaffoldOptions struct { RenderOnly bool AutoCreateConfig bool AssumeYes bool - RenderOutput string ProfileAutoActivation bool DryRun bool SkipRender bool diff --git a/pkg/skaffold/deploy/kubectl/cli.go b/pkg/skaffold/deploy/kubectl/cli.go index 34b94e1b862..cb89b1eedc8 100644 --- a/pkg/skaffold/deploy/kubectl/cli.go +++ b/pkg/skaffold/deploy/kubectl/cli.go @@ -50,6 +50,7 @@ type Config interface { ForceDeploy() bool WaitForDeletions() config.WaitForDeletions Mode() config.RunMode + HydratedManifests() []string } func NewCLI(cfg Config, flags latest.KubectlFlags, defaultNameSpace string) CLI { diff --git a/pkg/skaffold/deploy/kubectl/kubectl.go b/pkg/skaffold/deploy/kubectl/kubectl.go index 8600d37d3c1..b25810fbdcc 100644 --- a/pkg/skaffold/deploy/kubectl/kubectl.go +++ b/pkg/skaffold/deploy/kubectl/kubectl.go @@ -45,6 +45,7 @@ type Deployer struct { *latest.KubectlDeploy originalImages []build.Artifact + hydratedManifests []string workingDir string globalConfig string gcsManifestDir string @@ -76,6 +77,7 @@ func NewDeployer(cfg Config, labels map[string]string, d *latest.KubectlDeploy) insecureRegistries: cfg.GetInsecureRegistries(), skipRender: cfg.SkipRender(), labels: labels, + hydratedManifests: cfg.HydratedManifests(), }, nil } @@ -86,11 +88,21 @@ func (k *Deployer) Deploy(ctx context.Context, out io.Writer, builds []build.Art manifests manifest.ManifestList err error ) - if k.skipRender { + // if any hydrated manifests are passed to `skaffold apply`, only deploy these + // also, manually set the labels to ensure the runID is added + switch { + case len(k.hydratedManifests) > 0: + manifests, err = createManifestList(k.hydratedManifests) + if err != nil { + return nil, err + } + manifests, err = manifests.SetLabels(k.labels) + case k.skipRender: manifests, err = k.readManifests(ctx, false) - } else { + default: manifests, err = k.renderManifests(ctx, out, builds, false) } + if err != nil { return nil, err } @@ -195,7 +207,10 @@ func (k *Deployer) readManifests(ctx context.Context, offline bool) (manifest.Ma if hasURLManifest { return nil, offlineModeErr() } + return createManifestList(manifests) +} +func createManifestList(manifests []string) (manifest.ManifestList, error) { var manifestList manifest.ManifestList for _, manifestFilePath := range manifests { manifestFileContent, err := ioutil.ReadFile(manifestFilePath) diff --git a/pkg/skaffold/instrumentation/meter.go b/pkg/skaffold/instrumentation/meter.go index 4bdb76597c8..56d18ccbd56 100644 --- a/pkg/skaffold/instrumentation/meter.go +++ b/pkg/skaffold/instrumentation/meter.go @@ -54,9 +54,9 @@ var ( ) func init() { - MeteredCommands.Insert("build", "delete", "deploy", "dev", "debug", "filter", "generate_pipeline", "render", "run", "test") + MeteredCommands.Insert("apply", "build", "delete", "deploy", "dev", "debug", "filter", "generate_pipeline", "render", "run", "test") doesBuild.Insert("build", "render", "dev", "debug", "run") - doesDeploy.Insert("deploy", "dev", "debug", "run") + doesDeploy.Insert("apply", "deploy", "dev", "debug", "run") } // SetOnlineStatus issues a GET request to see if the user is online. diff --git a/pkg/skaffold/runner/apply.go b/pkg/skaffold/runner/apply.go new file mode 100644 index 00000000000..2c94388e429 --- /dev/null +++ b/pkg/skaffold/runner/apply.go @@ -0,0 +1,77 @@ +/* +Copyright 2021 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 runner + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build" + deployutil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/util" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/event" +) + +// Apply sends Kubernetes manifests to the cluster. +func (r *SkaffoldRunner) Apply(ctx context.Context, out io.Writer) error { + if err := r.applyResources(ctx, out, nil, nil); err != nil { + return err + } + + statusCheckOut, postStatusCheckFn, err := deployutil.WithStatusCheckLogFile(time.Now().Format(deployutil.TimeFormat)+".log", out, r.runCtx.Muted()) + postStatusCheckFn() + if err != nil { + return err + } + sErr := r.performStatusCheck(ctx, statusCheckOut) + return sErr +} + +func (r *SkaffoldRunner) applyResources(ctx context.Context, out io.Writer, artifacts, localImages []build.Artifact) error { + // Check that the cluster is reachable. + // This gives a better error message when the cluster can't + // be reached. + if err := failIfClusterIsNotReachable(); err != nil { + return fmt.Errorf("unable to connect to Kubernetes: %w", err) + } + + if len(localImages) > 0 && r.runCtx.Cluster.LoadImages { + err := r.loadImagesIntoCluster(ctx, out, localImages) + if err != nil { + return err + } + } + + deployOut, postDeployFn, err := deployutil.WithLogFile(time.Now().Format(deployutil.TimeFormat)+".log", out, r.runCtx.Muted()) + if err != nil { + return err + } + + event.DeployInProgress() + namespaces, err := r.deployer.Deploy(ctx, deployOut, artifacts) + postDeployFn() + if err != nil { + event.DeployFailed(err) + return err + } + + r.hasDeployed = true + event.DeployComplete() + r.runCtx.UpdateNamespaces(namespaces) + return nil +} diff --git a/pkg/skaffold/runner/new.go b/pkg/skaffold/runner/new.go index cae18752ff4..69277c9f0d2 100644 --- a/pkg/skaffold/runner/new.go +++ b/pkg/skaffold/runner/new.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "strconv" "github.com/sirupsen/logrus" @@ -317,7 +318,7 @@ func getDefaultDeployer(runCtx *runcontext.RunContext, labels map[string]string) func validateKubectlFlags(flags *latest.KubectlFlags, additional latest.KubectlFlags) error { errStr := "conflicting sets of kubectl deploy flags not supported in `skaffold apply` (flag: %s)" if additional.DisableValidation != flags.DisableValidation { - return fmt.Errorf(errStr, additional.DisableValidation) + return fmt.Errorf(errStr, strconv.FormatBool(additional.DisableValidation)) } for _, flag := range additional.Apply { if !util.StrSliceContains(flags.Apply, flag) { diff --git a/pkg/skaffold/runner/runcontext/context.go b/pkg/skaffold/runner/runcontext/context.go index 58ff7afdc1a..5693414976c 100644 --- a/pkg/skaffold/runner/runcontext/context.go +++ b/pkg/skaffold/runner/runcontext/context.go @@ -169,6 +169,7 @@ func (rc *RunContext) ForceDeploy() bool { return rc.Opt func (rc *RunContext) GetKubeConfig() string { return rc.Opts.KubeConfig } func (rc *RunContext) GetKubeNamespace() string { return rc.Opts.Namespace } func (rc *RunContext) GlobalConfig() string { return rc.Opts.GlobalConfig } +func (rc *RunContext) HydratedManifests() []string { return rc.Opts.HydratedManifests } func (rc *RunContext) MinikubeProfile() string { return rc.Opts.MinikubeProfile } func (rc *RunContext) Muted() config.Muted { return rc.Opts.Muted } func (rc *RunContext) NoPruneChildren() bool { return rc.Opts.NoPruneChildren } diff --git a/pkg/skaffold/runner/runner.go b/pkg/skaffold/runner/runner.go index c3a483870e6..3e9fba194c1 100644 --- a/pkg/skaffold/runner/runner.go +++ b/pkg/skaffold/runner/runner.go @@ -43,17 +43,18 @@ const ( // Runner is responsible for running the skaffold build, test and deploy config. type Runner interface { - Dev(context.Context, io.Writer, []*latest.Artifact) error + Apply(context.Context, io.Writer) error ApplyDefaultRepo(tag string) (string, error) Build(context.Context, io.Writer, []*latest.Artifact) ([]build.Artifact, error) - Test(context.Context, io.Writer, []build.Artifact) error + Cleanup(context.Context, io.Writer) error DeployAndLog(context.Context, io.Writer, []build.Artifact) error + Dev(context.Context, io.Writer, []*latest.Artifact) error GeneratePipeline(context.Context, io.Writer, []*latest.SkaffoldConfig, []string, string) error - Render(context.Context, io.Writer, []build.Artifact, bool, string) error - Cleanup(context.Context, io.Writer) error - Prune(context.Context, io.Writer) error - HasDeployed() bool HasBuilt() bool + HasDeployed() bool + Prune(context.Context, io.Writer) error + Render(context.Context, io.Writer, []build.Artifact, bool, string) error + Test(context.Context, io.Writer, []build.Artifact) error } // SkaffoldRunner is responsible for running the skaffold build, test and deploy config.