1
16
17 package replace
18
19 import (
20 "fmt"
21 "net/url"
22 "os"
23 "path/filepath"
24 "strings"
25 "time"
26
27 "github.com/spf13/cobra"
28
29 "k8s.io/klog/v2"
30
31 "k8s.io/apimachinery/pkg/api/errors"
32 "k8s.io/apimachinery/pkg/runtime"
33 "k8s.io/apimachinery/pkg/util/wait"
34 "k8s.io/cli-runtime/pkg/genericclioptions"
35 "k8s.io/cli-runtime/pkg/genericiooptions"
36 "k8s.io/cli-runtime/pkg/resource"
37 "k8s.io/kubectl/pkg/cmd/delete"
38 cmdutil "k8s.io/kubectl/pkg/cmd/util"
39 "k8s.io/kubectl/pkg/rawhttp"
40 "k8s.io/kubectl/pkg/scheme"
41 "k8s.io/kubectl/pkg/util"
42 "k8s.io/kubectl/pkg/util/i18n"
43 "k8s.io/kubectl/pkg/util/slice"
44 "k8s.io/kubectl/pkg/util/templates"
45 "k8s.io/kubectl/pkg/validation"
46 )
47
48 var (
49 replaceLong = templates.LongDesc(i18n.T(`
50 Replace a resource by file name or stdin.
51
52 JSON and YAML formats are accepted. If replacing an existing resource, the
53 complete resource spec must be provided. This can be obtained by
54
55 $ kubectl get TYPE NAME -o yaml`))
56
57 replaceExample = templates.Examples(i18n.T(`
58 # Replace a pod using the data in pod.json
59 kubectl replace -f ./pod.json
60
61 # Replace a pod based on the JSON passed into stdin
62 cat pod.json | kubectl replace -f -
63
64 # Update a single-container pod's image version (tag) to v4
65 kubectl get pod mypod -o yaml | sed 's/\(image: myimage\):.*$/\1:v4/' | kubectl replace -f -
66
67 # Force replace, delete and then re-create the resource
68 kubectl replace --force -f ./pod.json`))
69 )
70
71 var supportedSubresources = []string{"status", "scale"}
72
73 type ReplaceOptions struct {
74 PrintFlags *genericclioptions.PrintFlags
75 RecordFlags *genericclioptions.RecordFlags
76
77 DeleteFlags *delete.DeleteFlags
78 DeleteOptions *delete.DeleteOptions
79
80 DryRunStrategy cmdutil.DryRunStrategy
81 validationDirective string
82
83 PrintObj func(obj runtime.Object) error
84
85 createAnnotation bool
86
87 Schema validation.Schema
88 Builder func() *resource.Builder
89 BuilderArgs []string
90
91 Namespace string
92 EnforceNamespace bool
93 Raw string
94
95 Recorder genericclioptions.Recorder
96
97 Subresource string
98
99 genericiooptions.IOStreams
100
101 fieldManager string
102 }
103
104 func NewReplaceOptions(streams genericiooptions.IOStreams) *ReplaceOptions {
105 return &ReplaceOptions{
106 PrintFlags: genericclioptions.NewPrintFlags("replaced"),
107 DeleteFlags: delete.NewDeleteFlags("The files that contain the configurations to replace."),
108
109 IOStreams: streams,
110 }
111 }
112
113 func NewCmdReplace(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
114 o := NewReplaceOptions(streams)
115
116 cmd := &cobra.Command{
117 Use: "replace -f FILENAME",
118 DisableFlagsInUseLine: true,
119 Short: i18n.T("Replace a resource by file name or stdin"),
120 Long: replaceLong,
121 Example: replaceExample,
122 Run: func(cmd *cobra.Command, args []string) {
123 cmdutil.CheckErr(o.Complete(f, cmd, args))
124 cmdutil.CheckErr(o.Validate())
125 cmdutil.CheckErr(o.Run(f))
126 },
127 }
128
129 o.PrintFlags.AddFlags(cmd)
130 o.DeleteFlags.AddFlags(cmd)
131 o.RecordFlags.AddFlags(cmd)
132
133 cmdutil.AddValidateFlags(cmd)
134 cmdutil.AddApplyAnnotationFlags(cmd)
135 cmdutil.AddDryRunFlag(cmd)
136
137 cmd.Flags().StringVar(&o.Raw, "raw", o.Raw, "Raw URI to PUT to the server. Uses the transport specified by the kubeconfig file.")
138 cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-replace")
139 cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, replace will operate on the subresource of the requested object.", supportedSubresources...)
140
141 return cmd
142 }
143
144 func (o *ReplaceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
145 var err error
146
147 o.RecordFlags.Complete(cmd)
148 o.Recorder, err = o.RecordFlags.ToRecorder()
149 if err != nil {
150 return err
151 }
152
153 o.validationDirective, err = cmdutil.GetValidationDirective(cmd)
154 if err != nil {
155 return err
156 }
157 o.createAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
158
159 o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
160 if err != nil {
161 return err
162 }
163 dynamicClient, err := f.DynamicClient()
164 if err != nil {
165 return err
166 }
167 cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
168
169 printer, err := o.PrintFlags.ToPrinter()
170 if err != nil {
171 return err
172 }
173 o.PrintObj = func(obj runtime.Object) error {
174 return printer.PrintObj(obj, o.Out)
175 }
176
177 deleteOpts, err := o.DeleteFlags.ToOptions(dynamicClient, o.IOStreams)
178 if err != nil {
179 return err
180 }
181
182
183 deleteOpts.IgnoreNotFound = true
184 if o.PrintFlags.OutputFormat != nil {
185 deleteOpts.Output = *o.PrintFlags.OutputFormat
186 }
187 if deleteOpts.GracePeriod == 0 {
188
189
190 deleteOpts.GracePeriod = 1
191 deleteOpts.WaitForDeletion = true
192 }
193 o.DeleteOptions = deleteOpts
194
195 err = o.DeleteOptions.FilenameOptions.RequireFilenameOrKustomize()
196 if err != nil {
197 return err
198 }
199
200 schema, err := f.Validator(o.validationDirective)
201 if err != nil {
202 return err
203 }
204
205 o.Schema = schema
206 o.Builder = f.NewBuilder
207 o.BuilderArgs = args
208
209 o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
210 if err != nil {
211 return err
212 }
213
214 return nil
215 }
216
217 func (o *ReplaceOptions) Validate() error {
218 if o.DeleteOptions.GracePeriod >= 0 && !o.DeleteOptions.ForceDeletion {
219 return fmt.Errorf("--grace-period must have --force specified")
220 }
221
222 if o.DeleteOptions.Timeout != 0 && !o.DeleteOptions.ForceDeletion {
223 return fmt.Errorf("--timeout must have --force specified")
224 }
225
226 if o.DeleteOptions.ForceDeletion && o.DryRunStrategy != cmdutil.DryRunNone {
227 return fmt.Errorf("--dry-run can not be used when --force is set")
228 }
229
230 if cmdutil.IsFilenameSliceEmpty(o.DeleteOptions.FilenameOptions.Filenames, o.DeleteOptions.FilenameOptions.Kustomize) {
231 return fmt.Errorf("must specify --filename to replace")
232 }
233
234 if len(o.Raw) > 0 {
235 if len(o.DeleteOptions.FilenameOptions.Filenames) != 1 {
236 return fmt.Errorf("--raw can only use a single local file or stdin")
237 }
238 if strings.Index(o.DeleteOptions.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.DeleteOptions.FilenameOptions.Filenames[0], "https://") == 0 {
239 return fmt.Errorf("--raw cannot read from a url")
240 }
241 if o.DeleteOptions.FilenameOptions.Recursive {
242 return fmt.Errorf("--raw and --recursive are mutually exclusive")
243 }
244 if o.PrintFlags.OutputFormat != nil && len(*o.PrintFlags.OutputFormat) > 0 {
245 return fmt.Errorf("--raw and --output are mutually exclusive")
246 }
247 if _, err := url.ParseRequestURI(o.Raw); err != nil {
248 return fmt.Errorf("--raw must be a valid URL path: %v", err)
249 }
250 }
251
252 if len(o.Subresource) > 0 && !slice.ContainsString(supportedSubresources, o.Subresource, nil) {
253 return fmt.Errorf("invalid subresource value: %q. Must be one of %v", o.Subresource, supportedSubresources)
254 }
255
256 return nil
257 }
258
259 func (o *ReplaceOptions) Run(f cmdutil.Factory) error {
260
261
262 if len(o.Raw) > 0 {
263 restClient, err := f.RESTClient()
264 if err != nil {
265 return err
266 }
267 return rawhttp.RawPut(restClient, o.IOStreams, o.Raw, o.DeleteOptions.Filenames[0])
268 }
269
270 if o.DeleteOptions.ForceDeletion {
271 return o.forceReplace()
272 }
273
274 r := o.Builder().
275 Unstructured().
276 Schema(o.Schema).
277 ContinueOnError().
278 NamespaceParam(o.Namespace).DefaultNamespace().
279 FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
280 Subresource(o.Subresource).
281 Flatten().
282 Do()
283 if err := r.Err(); err != nil {
284 return err
285 }
286
287 return r.Visit(func(info *resource.Info, err error) error {
288 if err != nil {
289 return err
290 }
291
292 if err := util.CreateOrUpdateAnnotation(o.createAnnotation, info.Object, scheme.DefaultJSONEncoder()); err != nil {
293 return cmdutil.AddSourceToErr("replacing", info.Source, err)
294 }
295
296 if err := o.Recorder.Record(info.Object); err != nil {
297 klog.V(4).Infof("error recording current command: %v", err)
298 }
299
300 if o.DryRunStrategy == cmdutil.DryRunClient {
301 return o.PrintObj(info.Object)
302 }
303
304
305 obj, err := resource.
306 NewHelper(info.Client, info.Mapping).
307 DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
308 WithFieldManager(o.fieldManager).
309 WithFieldValidation(o.validationDirective).
310 WithSubresource(o.Subresource).
311 Replace(info.Namespace, info.Name, true, info.Object)
312 if err != nil {
313 return cmdutil.AddSourceToErr("replacing", info.Source, err)
314 }
315
316 info.Refresh(obj, true)
317 return o.PrintObj(info.Object)
318 })
319 }
320
321 func (o *ReplaceOptions) forceReplace() error {
322 stdinInUse := false
323 for i, filename := range o.DeleteOptions.FilenameOptions.Filenames {
324 if filename == "-" {
325 tempDir, err := os.MkdirTemp("", "kubectl_replace_")
326 if err != nil {
327 return err
328 }
329 defer os.RemoveAll(tempDir)
330 tempFilename := filepath.Join(tempDir, "resource.stdin")
331 err = cmdutil.DumpReaderToFile(os.Stdin, tempFilename)
332 if err != nil {
333 return err
334 }
335 o.DeleteOptions.FilenameOptions.Filenames[i] = tempFilename
336 stdinInUse = true
337 }
338 }
339
340 b := o.Builder().
341 Unstructured().
342 ContinueOnError().
343 NamespaceParam(o.Namespace).DefaultNamespace().
344 ResourceTypeOrNameArgs(false, o.BuilderArgs...).RequireObject(false).
345 FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
346 Subresource(o.Subresource).
347 Flatten()
348 if stdinInUse {
349 b = b.StdinInUse()
350 }
351 r := b.Do()
352 if err := r.Err(); err != nil {
353 return err
354 }
355
356 if err := o.DeleteOptions.DeleteResult(r); err != nil {
357 return err
358 }
359
360 timeout := o.DeleteOptions.Timeout
361 if timeout == 0 {
362 timeout = 5 * time.Minute
363 }
364 err := r.Visit(func(info *resource.Info, err error) error {
365 if err != nil {
366 return err
367 }
368
369 return wait.PollImmediate(1*time.Second, timeout, func() (bool, error) {
370 if err := info.Get(); !errors.IsNotFound(err) {
371 return false, err
372 }
373 return true, nil
374 })
375 })
376 if err != nil {
377 return err
378 }
379
380 b = o.Builder().
381 Unstructured().
382 Schema(o.Schema).
383 ContinueOnError().
384 NamespaceParam(o.Namespace).DefaultNamespace().
385 FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
386 Subresource(o.Subresource).
387 Flatten()
388 if stdinInUse {
389 b = b.StdinInUse()
390 }
391 r = b.Do()
392 err = r.Err()
393 if err != nil {
394 return err
395 }
396
397 count := 0
398 err = r.Visit(func(info *resource.Info, err error) error {
399 if err != nil {
400 return err
401 }
402
403 if err := util.CreateOrUpdateAnnotation(o.createAnnotation, info.Object, scheme.DefaultJSONEncoder()); err != nil {
404 return err
405 }
406
407 if err := o.Recorder.Record(info.Object); err != nil {
408 klog.V(4).Infof("error recording current command: %v", err)
409 }
410
411 obj, err := resource.NewHelper(info.Client, info.Mapping).
412 WithFieldManager(o.fieldManager).
413 WithFieldValidation(o.validationDirective).
414 Create(info.Namespace, true, info.Object)
415 if err != nil {
416 return err
417 }
418
419 count++
420 info.Refresh(obj, true)
421 return o.PrintObj(info.Object)
422 })
423 if err != nil {
424 return err
425 }
426 if count == 0 {
427 return fmt.Errorf("no objects passed to replace")
428 }
429 return nil
430 }
431
View as plain text