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