github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/deploy/kubectl/cli.go (about)

     1  /*
     2  Copyright 2019 The Skaffold Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package kubectl
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
    28  	deployerr "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/error"
    29  	deploy "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/types"
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubectl"
    32  	kloader "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/loader"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest"
    34  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/portforward"
    35  	kstatus "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/status"
    36  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    37  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    38  )
    39  
    40  // CLI holds parameters to run kubectl.
    41  type CLI struct {
    42  	*kubectl.CLI
    43  	Flags latest.KubectlFlags
    44  
    45  	forceDeploy      bool
    46  	waitForDeletions config.WaitForDeletions
    47  	previousApply    manifest.ManifestList
    48  }
    49  
    50  type Config interface {
    51  	kubectl.Config
    52  	kstatus.Config
    53  	kloader.Config
    54  	portforward.Config
    55  	deploy.Config
    56  	ForceDeploy() bool
    57  	WaitForDeletions() config.WaitForDeletions
    58  	Mode() config.RunMode
    59  	HydratedManifests() []string
    60  	DefaultPipeline() latest.Pipeline
    61  	Tail() bool
    62  	PipelineForImage(imageName string) (latest.Pipeline, bool)
    63  	JSONParseConfig() latest.JSONParseConfig
    64  }
    65  
    66  func NewCLI(cfg Config, flags latest.KubectlFlags, defaultNamespace string) CLI {
    67  	return CLI{
    68  		CLI:              kubectl.NewCLI(cfg, defaultNamespace),
    69  		Flags:            flags,
    70  		forceDeploy:      cfg.ForceDeploy(),
    71  		waitForDeletions: cfg.WaitForDeletions(),
    72  	}
    73  }
    74  
    75  // Delete runs `kubectl delete` on a list of manifests.
    76  func (c *CLI) Delete(ctx context.Context, out io.Writer, manifests manifest.ManifestList) error {
    77  	args := c.args(c.Flags.Delete, "--ignore-not-found=true", "--wait=false", "-f", "-")
    78  	if err := c.Run(ctx, manifests.Reader(), out, "delete", args...); err != nil {
    79  		return deployerr.CleanupErr(fmt.Errorf("kubectl delete: %w", err))
    80  	}
    81  
    82  	return nil
    83  }
    84  
    85  // Apply runs `kubectl apply` on a list of manifests.
    86  func (c *CLI) Apply(ctx context.Context, out io.Writer, manifests manifest.ManifestList) error {
    87  	ctx, endTrace := instrumentation.StartTrace(ctx, "Apply", map[string]string{
    88  		"AppliedBy": "kubectl",
    89  	})
    90  	defer endTrace()
    91  	// Only redeploy modified or new manifests
    92  	// TODO(dgageot): should we delete a manifest that was deployed and is not anymore?
    93  	updated := c.previousApply.Diff(manifests)
    94  	log.Entry(ctx).Debugf("%d manifests to deploy. %d are updated or new", len(manifests), len(updated))
    95  	c.previousApply = manifests
    96  	if len(updated) == 0 {
    97  		return nil
    98  	}
    99  
   100  	args := []string{"-f", "-"}
   101  	if c.forceDeploy {
   102  		args = append(args, "--force", "--grace-period=0")
   103  	}
   104  
   105  	if c.Flags.DisableValidation {
   106  		args = append(args, "--validate=false")
   107  	}
   108  
   109  	if err := c.Run(ctx, updated.Reader(), out, "apply", c.args(c.Flags.Apply, args...)...); err != nil {
   110  		endTrace(instrumentation.TraceEndError(err))
   111  		return userErr(fmt.Errorf("kubectl apply: %w", err))
   112  	}
   113  
   114  	return nil
   115  }
   116  
   117  // Kustomize runs `kubectl kustomize` with the provided args
   118  func (c *CLI) Kustomize(ctx context.Context, args []string) ([]byte, error) {
   119  	return c.RunOut(ctx, "kustomize", c.args(nil, args...)...)
   120  }
   121  
   122  type getResult struct {
   123  	Items []struct {
   124  		Metadata struct {
   125  			Name              string `json:"name"`
   126  			DeletionTimestamp string `json:"deletionTimestamp"`
   127  		} `json:"metadata"`
   128  	} `json:"items"`
   129  }
   130  
   131  // WaitForDeletions waits for resource marked for deletion to complete their deletion.
   132  func (c *CLI) WaitForDeletions(ctx context.Context, out io.Writer, manifests manifest.ManifestList) error {
   133  	if !c.waitForDeletions.Enabled {
   134  		return nil
   135  	}
   136  
   137  	ctx, cancel := context.WithTimeout(ctx, c.waitForDeletions.Max)
   138  	defer cancel()
   139  
   140  	previousList := ""
   141  	previousCount := 0
   142  
   143  	for {
   144  		select {
   145  		case <-ctx.Done():
   146  			return waitForDeletionErr(fmt.Errorf("%d resources failed to complete their deletion before a new deployment: %s", previousCount, previousList))
   147  		default:
   148  			// List resources in json format.
   149  			buf, err := c.RunOutInput(ctx, manifests.Reader(), "get", c.args(nil, "-f", "-", "--ignore-not-found", "-ojson")...)
   150  			if err != nil {
   151  				return waitForDeletionErr(err)
   152  			}
   153  
   154  			// No resource found.
   155  			if len(buf) == 0 {
   156  				return nil
   157  			}
   158  
   159  			// Find which ones are marked for deletion. They have a `metadata.deletionTimestamp` field.
   160  			var result getResult
   161  			if err := json.Unmarshal(buf, &result); err != nil {
   162  				return waitForDeletionErr(err)
   163  			}
   164  
   165  			var marked []string
   166  			for _, item := range result.Items {
   167  				if item.Metadata.DeletionTimestamp != "" {
   168  					marked = append(marked, item.Metadata.Name)
   169  				}
   170  			}
   171  			if len(marked) == 0 {
   172  				return nil
   173  			}
   174  
   175  			list := `"` + strings.Join(marked, `", "`) + `"`
   176  			log.Entry(ctx).Debug("Resources are marked for deletion: ", list)
   177  			if list != previousList {
   178  				if len(marked) == 1 {
   179  					fmt.Fprintf(out, "%s is marked for deletion, waiting for completion\n", list)
   180  				} else {
   181  					fmt.Fprintf(out, "%d resources are marked for deletion, waiting for completion: %s\n", len(marked), list)
   182  				}
   183  
   184  				previousList = list
   185  				previousCount = len(marked)
   186  			}
   187  
   188  			select {
   189  			case <-ctx.Done():
   190  			case <-time.After(c.waitForDeletions.Delay):
   191  			}
   192  		}
   193  	}
   194  }
   195  
   196  // ReadManifests reads a list of manifests in yaml format.
   197  func (c *CLI) ReadManifests(ctx context.Context, manifests []string) (manifest.ManifestList, error) {
   198  	var list []string
   199  	for _, manifest := range manifests {
   200  		list = append(list, "-f", manifest)
   201  	}
   202  
   203  	var dryRun = "--dry-run"
   204  	compTo1_18, err := c.CLI.CompareVersionTo(ctx, 1, 18)
   205  	if err != nil {
   206  		return nil, versionGetErr(err)
   207  	}
   208  	if compTo1_18 >= 0 {
   209  		dryRun += "=client"
   210  	}
   211  
   212  	args := c.args([]string{dryRun, "-oyaml"}, list...)
   213  	if c.Flags.DisableValidation {
   214  		args = append(args, "--validate=false")
   215  	}
   216  
   217  	buf, err := c.RunOut(ctx, "create", args...)
   218  	if err != nil {
   219  		return nil, readManifestErr(fmt.Errorf("kubectl create: %w", err))
   220  	}
   221  
   222  	var manifestList manifest.ManifestList
   223  	manifestList.Append(buf)
   224  
   225  	return manifestList, nil
   226  }
   227  
   228  func (c *CLI) args(commandFlags []string, additionalArgs ...string) []string {
   229  	args := make([]string, 0, len(c.Flags.Global)+len(commandFlags)+len(additionalArgs))
   230  
   231  	args = append(args, c.Flags.Global...)
   232  	args = append(args, commandFlags...)
   233  	args = append(args, additionalArgs...)
   234  
   235  	return args
   236  }