...

Source file src/k8s.io/kubectl/pkg/cmd/replace/replace.go

Documentation: k8s.io/kubectl/pkg/cmd/replace

     1  /*
     2  Copyright 2014 The Kubernetes 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 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  	//Replace will create a resource if it doesn't exist already, so ignore not found error
   183  	deleteOpts.IgnoreNotFound = true
   184  	if o.PrintFlags.OutputFormat != nil {
   185  		deleteOpts.Output = *o.PrintFlags.OutputFormat
   186  	}
   187  	if deleteOpts.GracePeriod == 0 {
   188  		// To preserve backwards compatibility, but prevent accidental data loss, we convert --grace-period=0
   189  		// into --grace-period=1 and wait until the object is successfully deleted.
   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  	// raw only makes sense for a single file resource multiple objects aren't likely to do what you want.
   261  	// the validator enforces this, so
   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  		// Serialize the object with the annotation applied.
   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