1/*
2Copyright 2014 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package replace
18
19import (
20	"fmt"
21	"io/ioutil"
22	"net/url"
23	"os"
24	"path/filepath"
25	"strings"
26	"time"
27
28	"github.com/spf13/cobra"
29
30	"k8s.io/klog"
31
32	"k8s.io/apimachinery/pkg/api/errors"
33	"k8s.io/apimachinery/pkg/runtime"
34	"k8s.io/apimachinery/pkg/util/wait"
35	"k8s.io/cli-runtime/pkg/genericclioptions"
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/templates"
44	"k8s.io/kubectl/pkg/validation"
45)
46
47var (
48	replaceLong = templates.LongDesc(i18n.T(`
49		Replace a resource by filename or stdin.
50
51		JSON and YAML formats are accepted. If replacing an existing resource, the
52		complete resource spec must be provided. This can be obtained by
53
54		    $ kubectl get TYPE NAME -o yaml`))
55
56	replaceExample = templates.Examples(i18n.T(`
57		# Replace a pod using the data in pod.json.
58		kubectl replace -f ./pod.json
59
60		# Replace a pod based on the JSON passed into stdin.
61		cat pod.json | kubectl replace -f -
62
63		# Update a single-container pod's image version (tag) to v4
64		kubectl get pod mypod -o yaml | sed 's/\(image: myimage\):.*$/\1:v4/' | kubectl replace -f -
65
66		# Force replace, delete and then re-create the resource
67		kubectl replace --force -f ./pod.json`))
68)
69
70type ReplaceOptions struct {
71	PrintFlags  *genericclioptions.PrintFlags
72	RecordFlags *genericclioptions.RecordFlags
73
74	DeleteFlags   *delete.DeleteFlags
75	DeleteOptions *delete.DeleteOptions
76
77	DryRunStrategy cmdutil.DryRunStrategy
78	DryRunVerifier *resource.DryRunVerifier
79
80	PrintObj func(obj runtime.Object) error
81
82	createAnnotation bool
83	validate         bool
84
85	Schema      validation.Schema
86	Builder     func() *resource.Builder
87	BuilderArgs []string
88
89	Namespace        string
90	EnforceNamespace bool
91	Raw              string
92
93	Recorder genericclioptions.Recorder
94
95	genericclioptions.IOStreams
96}
97
98func NewReplaceOptions(streams genericclioptions.IOStreams) *ReplaceOptions {
99	return &ReplaceOptions{
100		PrintFlags:  genericclioptions.NewPrintFlags("replaced"),
101		DeleteFlags: delete.NewDeleteFlags("to use to replace the resource."),
102
103		IOStreams: streams,
104	}
105}
106
107func NewCmdReplace(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
108	o := NewReplaceOptions(streams)
109
110	cmd := &cobra.Command{
111		Use:                   "replace -f FILENAME",
112		DisableFlagsInUseLine: true,
113		Short:                 i18n.T("Replace a resource by filename or stdin"),
114		Long:                  replaceLong,
115		Example:               replaceExample,
116		Run: func(cmd *cobra.Command, args []string) {
117			cmdutil.CheckErr(o.Complete(f, cmd, args))
118			cmdutil.CheckErr(o.Validate(cmd))
119			cmdutil.CheckErr(o.Run(f))
120		},
121	}
122
123	o.PrintFlags.AddFlags(cmd)
124	o.DeleteFlags.AddFlags(cmd)
125	o.RecordFlags.AddFlags(cmd)
126
127	cmdutil.AddValidateFlags(cmd)
128	cmdutil.AddApplyAnnotationFlags(cmd)
129	cmdutil.AddDryRunFlag(cmd)
130
131	cmd.Flags().StringVar(&o.Raw, "raw", o.Raw, "Raw URI to PUT to the server.  Uses the transport specified by the kubeconfig file.")
132
133	return cmd
134}
135
136func (o *ReplaceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
137	var err error
138
139	o.RecordFlags.Complete(cmd)
140	o.Recorder, err = o.RecordFlags.ToRecorder()
141	if err != nil {
142		return err
143	}
144
145	o.validate = cmdutil.GetFlagBool(cmd, "validate")
146	o.createAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
147
148	o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
149	if err != nil {
150		return err
151	}
152	dynamicClient, err := f.DynamicClient()
153	if err != nil {
154		return err
155	}
156	discoveryClient, err := f.ToDiscoveryClient()
157	if err != nil {
158		return err
159	}
160	o.DryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient)
161	cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
162
163	printer, err := o.PrintFlags.ToPrinter()
164	if err != nil {
165		return err
166	}
167	o.PrintObj = func(obj runtime.Object) error {
168		return printer.PrintObj(obj, o.Out)
169	}
170
171	deleteOpts := o.DeleteFlags.ToOptions(dynamicClient, o.IOStreams)
172
173	//Replace will create a resource if it doesn't exist already, so ignore not found error
174	deleteOpts.IgnoreNotFound = true
175	if o.PrintFlags.OutputFormat != nil {
176		deleteOpts.Output = *o.PrintFlags.OutputFormat
177	}
178	if deleteOpts.GracePeriod == 0 {
179		// To preserve backwards compatibility, but prevent accidental data loss, we convert --grace-period=0
180		// into --grace-period=1 and wait until the object is successfully deleted.
181		deleteOpts.GracePeriod = 1
182		deleteOpts.WaitForDeletion = true
183	}
184	o.DeleteOptions = deleteOpts
185
186	err = o.DeleteOptions.FilenameOptions.RequireFilenameOrKustomize()
187	if err != nil {
188		return err
189	}
190
191	schema, err := f.Validator(o.validate)
192	if err != nil {
193		return err
194	}
195
196	o.Schema = schema
197	o.Builder = f.NewBuilder
198	o.BuilderArgs = args
199
200	o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
201	if err != nil {
202		return err
203	}
204
205	return nil
206}
207
208func (o *ReplaceOptions) Validate(cmd *cobra.Command) error {
209	if o.DeleteOptions.GracePeriod >= 0 && !o.DeleteOptions.ForceDeletion {
210		return fmt.Errorf("--grace-period must have --force specified")
211	}
212
213	if o.DeleteOptions.Timeout != 0 && !o.DeleteOptions.ForceDeletion {
214		return fmt.Errorf("--timeout must have --force specified")
215	}
216
217	if cmdutil.IsFilenameSliceEmpty(o.DeleteOptions.FilenameOptions.Filenames, o.DeleteOptions.FilenameOptions.Kustomize) {
218		return cmdutil.UsageErrorf(cmd, "Must specify --filename to replace")
219	}
220
221	if len(o.Raw) > 0 {
222		if len(o.DeleteOptions.FilenameOptions.Filenames) != 1 {
223			return cmdutil.UsageErrorf(cmd, "--raw can only use a single local file or stdin")
224		}
225		if strings.Index(o.DeleteOptions.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.DeleteOptions.FilenameOptions.Filenames[0], "https://") == 0 {
226			return cmdutil.UsageErrorf(cmd, "--raw cannot read from a url")
227		}
228		if o.DeleteOptions.FilenameOptions.Recursive {
229			return cmdutil.UsageErrorf(cmd, "--raw and --recursive are mutually exclusive")
230		}
231		if len(cmdutil.GetFlagString(cmd, "output")) > 0 {
232			return cmdutil.UsageErrorf(cmd, "--raw and --output are mutually exclusive")
233		}
234		if _, err := url.ParseRequestURI(o.Raw); err != nil {
235			return cmdutil.UsageErrorf(cmd, "--raw must be a valid URL path: %v", err)
236		}
237	}
238
239	return nil
240}
241
242func (o *ReplaceOptions) Run(f cmdutil.Factory) error {
243	// raw only makes sense for a single file resource multiple objects aren't likely to do what you want.
244	// the validator enforces this, so
245	if len(o.Raw) > 0 {
246		restClient, err := f.RESTClient()
247		if err != nil {
248			return err
249		}
250		return rawhttp.RawPut(restClient, o.IOStreams, o.Raw, o.DeleteOptions.Filenames[0])
251	}
252
253	if o.DeleteOptions.ForceDeletion {
254		return o.forceReplace()
255	}
256
257	r := o.Builder().
258		Unstructured().
259		Schema(o.Schema).
260		ContinueOnError().
261		NamespaceParam(o.Namespace).DefaultNamespace().
262		FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
263		Flatten().
264		Do()
265	if err := r.Err(); err != nil {
266		return err
267	}
268
269	return r.Visit(func(info *resource.Info, err error) error {
270		if err != nil {
271			return err
272		}
273
274		if err := util.CreateOrUpdateAnnotation(o.createAnnotation, info.Object, scheme.DefaultJSONEncoder()); err != nil {
275			return cmdutil.AddSourceToErr("replacing", info.Source, err)
276		}
277
278		if err := o.Recorder.Record(info.Object); err != nil {
279			klog.V(4).Infof("error recording current command: %v", err)
280		}
281
282		if o.DryRunStrategy == cmdutil.DryRunClient {
283			return o.PrintObj(info.Object)
284		}
285		if o.DryRunStrategy == cmdutil.DryRunServer {
286			if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
287				return err
288			}
289		}
290
291		// Serialize the object with the annotation applied.
292		obj, err := resource.
293			NewHelper(info.Client, info.Mapping).
294			DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
295			Replace(info.Namespace, info.Name, true, info.Object)
296		if err != nil {
297			return cmdutil.AddSourceToErr("replacing", info.Source, err)
298		}
299
300		info.Refresh(obj, true)
301		return o.PrintObj(info.Object)
302	})
303}
304
305func (o *ReplaceOptions) forceReplace() error {
306	for i, filename := range o.DeleteOptions.FilenameOptions.Filenames {
307		if filename == "-" {
308			tempDir, err := ioutil.TempDir("", "kubectl_replace_")
309			if err != nil {
310				return err
311			}
312			defer os.RemoveAll(tempDir)
313			tempFilename := filepath.Join(tempDir, "resource.stdin")
314			err = cmdutil.DumpReaderToFile(os.Stdin, tempFilename)
315			if err != nil {
316				return err
317			}
318			o.DeleteOptions.FilenameOptions.Filenames[i] = tempFilename
319		}
320	}
321
322	r := o.Builder().
323		Unstructured().
324		ContinueOnError().
325		NamespaceParam(o.Namespace).DefaultNamespace().
326		ResourceTypeOrNameArgs(false, o.BuilderArgs...).RequireObject(false).
327		FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
328		Flatten().
329		Do()
330	if err := r.Err(); err != nil {
331		return err
332	}
333
334	if err := o.DeleteOptions.DeleteResult(r); err != nil {
335		return err
336	}
337
338	timeout := o.DeleteOptions.Timeout
339	if timeout == 0 {
340		timeout = 5 * time.Minute
341	}
342	err := r.Visit(func(info *resource.Info, err error) error {
343		if err != nil {
344			return err
345		}
346
347		return wait.PollImmediate(1*time.Second, timeout, func() (bool, error) {
348			if err := info.Get(); !errors.IsNotFound(err) {
349				return false, err
350			}
351			return true, nil
352		})
353	})
354	if err != nil {
355		return err
356	}
357
358	r = o.Builder().
359		Unstructured().
360		Schema(o.Schema).
361		ContinueOnError().
362		NamespaceParam(o.Namespace).DefaultNamespace().
363		FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
364		Flatten().
365		Do()
366	err = r.Err()
367	if err != nil {
368		return err
369	}
370
371	count := 0
372	err = r.Visit(func(info *resource.Info, err error) error {
373		if err != nil {
374			return err
375		}
376
377		if err := util.CreateOrUpdateAnnotation(o.createAnnotation, info.Object, scheme.DefaultJSONEncoder()); err != nil {
378			return err
379		}
380
381		if err := o.Recorder.Record(info.Object); err != nil {
382			klog.V(4).Infof("error recording current command: %v", err)
383		}
384
385		obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object)
386		if err != nil {
387			return err
388		}
389
390		count++
391		info.Refresh(obj, true)
392		return o.PrintObj(info.Object)
393	})
394	if err != nil {
395		return err
396	}
397	if count == 0 {
398		return fmt.Errorf("no objects passed to replace")
399	}
400	return nil
401}
402