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 }