Exemplo n.º 1
0
func RunEdit(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string, filenames []string) error {
	var printer kubectl.ResourcePrinter
	var ext string
	switch format := cmdutil.GetFlagString(cmd, "output"); format {
	case "json":
		printer = &kubectl.JSONPrinter{}
		ext = ".json"
	case "yaml":
		printer = &kubectl.YAMLPrinter{}
		ext = ".yaml"
	default:
		return cmdutil.UsageError(cmd, "The flag 'output' must be one of yaml|json")
	}

	cmdNamespace, enforceNamespace, err := f.DefaultNamespace()
	if err != nil {
		return err
	}

	mapper, typer := f.Object()
	resourceMapper := &resource.Mapper{
		ObjectTyper:  typer,
		RESTMapper:   mapper,
		ClientMapper: resource.ClientMapperFunc(f.ClientForMapping),
		Decoder:      f.Decoder(true),
	}

	r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)).
		NamespaceParam(cmdNamespace).DefaultNamespace().
		FilenameParam(enforceNamespace, filenames...).
		ResourceTypeOrNameArgs(true, args...).
		Latest().
		Flatten().
		Do()
	err = r.Err()
	if err != nil {
		return err
	}

	infos, err := r.Infos()
	if err != nil {
		return err
	}

	clientConfig, err := f.ClientConfig()
	if err != nil {
		return err
	}

	encoder := f.JSONEncoder()

	windowsLineEndings := cmdutil.GetFlagBool(cmd, "windows-line-endings")
	edit := editor.NewDefaultEditor(f.EditorEnvs())
	defaultVersion, err := cmdutil.OutputVersion(cmd, clientConfig.GroupVersion)
	if err != nil {
		return err
	}
	results := editResults{}
	for {
		objs, err := resource.AsVersionedObjects(infos, defaultVersion.String(), encoder)
		if err != nil {
			return preservedFile(err, results.file, out)
		}
		// if input object is a list, traverse and edit each item one at a time
		for _, obj := range objs {
			// TODO: add an annotating YAML printer that can print inline comments on each field,
			//   including descriptions or validation errors

			// generate the file to edit
			buf := &bytes.Buffer{}
			var w io.Writer = buf
			if windowsLineEndings {
				w = util.NewCRLFWriter(w)
			}
			if err := results.header.writeTo(w); err != nil {
				return preservedFile(err, results.file, out)
			}
			if err := printer.PrintObj(obj, w); err != nil {
				return preservedFile(err, results.file, out)
			}
			original := buf.Bytes()

			// launch the editor
			edited, file, err := edit.LaunchTempFile("kubectl-edit-", ext, buf)
			if err != nil {
				return preservedFile(err, results.file, out)
			}

			// cleanup any file from the previous pass
			if len(results.file) > 0 {
				os.Remove(results.file)
			}

			glog.V(4).Infof("User edited:\n%s", string(edited))
			lines, err := hasLines(bytes.NewBuffer(edited))
			if err != nil {
				return preservedFile(err, file, out)
			}
			// Compare content without comments
			if bytes.Equal(stripComments(original), stripComments(edited)) {
				if len(results.edit) > 0 {
					preservedFile(nil, file, out)
				} else {
					os.Remove(file)
				}
				fmt.Fprintln(out, "Edit cancelled, no changes made.")
				continue
			}
			if !lines {
				if len(results.edit) > 0 {
					preservedFile(nil, file, out)
				} else {
					os.Remove(file)
				}
				fmt.Fprintln(out, "Edit cancelled, saved file was empty.")
				continue
			}

			results = editResults{
				file: file,
			}

			// parse the edited file
			updates, err := resourceMapper.InfoForData(edited, "edited-file")
			if err != nil {
				return fmt.Errorf("The edited file had a syntax error: %v", err)
			}

			// put configuration annotation in "updates"
			if err := kubectl.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), updates, encoder); err != nil {
				return preservedFile(err, file, out)
			}
			if cmdutil.ShouldRecord(cmd, updates) {
				err = cmdutil.RecordChangeCause(updates.Object, f.Command())
				if err != nil {
					return err
				}
			}
			// encode updates back to "edited" since we'll only generate patch from "edited"
			if edited, err = runtime.Encode(encoder, updates.Object); err != nil {
				return preservedFile(err, file, out)
			}

			visitor := resource.NewFlattenListVisitor(updates, resourceMapper)

			// need to make sure the original namespace wasn't changed while editing
			if err = visitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil {
				return preservedFile(err, file, out)
			}

			// use strategic merge to create a patch
			originalJS, err := yaml.ToJSON(original)
			if err != nil {
				return preservedFile(err, file, out)
			}
			editedJS, err := yaml.ToJSON(edited)
			if err != nil {
				return preservedFile(err, file, out)
			}
			patch, err := strategicpatch.CreateStrategicMergePatch(originalJS, editedJS, obj)
			// TODO: change all jsonmerge to strategicpatch
			// for checking preconditions
			preconditions := []jsonmerge.PreconditionFunc{}
			if err != nil {
				glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
				return preservedFile(err, file, out)
			} else {
				preconditions = append(preconditions, jsonmerge.RequireKeyUnchanged("apiVersion"))
				preconditions = append(preconditions, jsonmerge.RequireKeyUnchanged("kind"))
				preconditions = append(preconditions, jsonmerge.RequireMetadataKeyUnchanged("name"))
				results.version = defaultVersion
			}

			if hold, msg := jsonmerge.TestPreconditionsHold(patch, preconditions); !hold {
				fmt.Fprintf(out, "error: %s", msg)
				return preservedFile(nil, file, out)
			}

			err = visitor.Visit(func(info *resource.Info, err error) error {
				patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, api.StrategicMergePatchType, patch)
				if err != nil {
					fmt.Fprintln(out, results.addError(err, info))
					return nil
				}
				info.Refresh(patched, true)
				cmdutil.PrintSuccess(mapper, false, out, info.Mapping.Resource, info.Name, "edited")
				return nil
			})
			if err != nil {
				return preservedFile(err, file, out)
			}

			if results.retryable > 0 {
				fmt.Fprintf(out, "You can run `kubectl replace -f %s` to try this update again.\n", file)
				return errExit
			}
			if results.conflict > 0 {
				fmt.Fprintf(out, "You must update your local resource version and run `kubectl replace -f %s` to overwrite the remote changes.\n", file)
				return errExit
			}
			if len(results.edit) == 0 {
				if results.notfound == 0 {
					os.Remove(file)
				} else {
					fmt.Fprintf(out, "The edits you made on deleted resources have been saved to %q\n", file)
				}
			}
		}
		if len(results.edit) == 0 {
			return nil
		}

		// loop again and edit the remaining items
		infos = results.edit
	}
	return nil
}
Exemplo n.º 2
0
// RunEdit contains all the necessary functionality for the OpenShift cli edit command
func RunEdit(fullName string, f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, args []string, filenames util.StringList) error {
	var printer kubectl.ResourcePrinter
	var ext string
	switch format := cmdutil.GetFlagString(cmd, "output"); format {
	case "json":
		printer = &kubectl.JSONPrinter{}
		ext = ".json"
	case "yaml":
		printer = &kubectl.YAMLPrinter{}
		ext = ".yaml"
	default:
		return cmdutil.UsageError(cmd, "The flag 'output' must be one of yaml|json")
	}

	cmdNamespace, explicit, err := f.DefaultNamespace()
	if err != nil {
		return err
	}

	mapper, typer := f.Object()
	rmap := &resource.Mapper{
		ObjectTyper:  typer,
		RESTMapper:   mapper,
		ClientMapper: f.ClientMapperForCommand(),
	}

	b := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()).
		NamespaceParam(cmdNamespace).DefaultNamespace().
		FilenameParam(explicit, filenames...).
		//SelectorParam(selector).
		ResourceTypeOrNameArgs(true, args...).
		Latest()
	if err != nil {
		return err
	}

	clientConfig, err := f.ClientConfig()
	if err != nil {
		return err
	}

	r := b.Flatten().Do()
	infos, err := r.Infos()
	if err != nil {
		return err
	}

	defaultVersion := cmdutil.OutputVersion(cmd, clientConfig.Version)
	results := editResults{}
	for {
		obj, err := resource.AsVersionedObject(infos, false, defaultVersion)
		if err != nil {
			return preservedFile(err, results.file, cmd.Out())
		}

		// TODO: add an annotating YAML printer that can print inline comments on each field,
		//   including descriptions or validation errors

		// generate the file to edit
		buf := &bytes.Buffer{}
		if err := results.header.WriteTo(buf); err != nil {
			return preservedFile(err, results.file, cmd.Out())
		}
		if err := printer.PrintObj(obj, buf); err != nil {
			return preservedFile(err, results.file, cmd.Out())
		}
		original := buf.Bytes()

		// launch the editor
		edit := editor.NewDefaultEditor()
		edited, file, err := edit.LaunchTempFile("oc-edit-", ext, buf)
		if err != nil {
			return preservedFile(err, results.file, cmd.Out())
		}

		// cleanup any file from the previous pass
		if len(results.file) > 0 {
			os.Remove(results.file)
		}

		glog.V(4).Infof("User edited:\n%s", string(edited))
		lines, err := hasLines(bytes.NewBuffer(edited))
		if err != nil {
			return preservedFile(err, file, cmd.Out())
		}
		if bytes.Equal(original, edited) {
			if len(results.edit) > 0 {
				preservedFile(nil, file, cmd.Out())
			} else {
				os.Remove(file)
			}
			fmt.Fprintln(cmd.Out(), "Edit cancelled, no changes made.")
			return nil
		}
		if !lines {
			if len(results.edit) > 0 {
				preservedFile(nil, file, cmd.Out())
			} else {
				os.Remove(file)
			}
			fmt.Fprintln(cmd.Out(), "Edit cancelled, saved file was empty.")
			return nil
		}

		results = editResults{
			file: file,
		}

		// parse the edited file
		updates, err := rmap.InfoForData(edited, "edited-file")
		if err != nil {
			results.header.reasons = append(results.header.reasons, editReason{
				head: fmt.Sprintf("The edited file had a syntax error: %v", err),
			})
			continue
		}

		visitor := resource.NewFlattenListVisitor(updates, rmap)

		// need to make sure the original namespace wasn't changed while editing
		if err = visitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil {
			return preservedFile(err, file, cmd.Out())
		}

		// attempt to calculate a delta for merging conflicts
		delta, err := jsonmerge.NewDelta(original, edited)
		if err != nil {
			glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
			delta = nil
		} else {
			delta.AddPreconditions(jsonmerge.RequireKeyUnchanged("apiVersion"))
			results.delta = delta
			results.version = defaultVersion
		}

		err = visitor.Visit(func(info *resource.Info) error {
			data, err := info.Mapping.Codec.Encode(info.Object)
			if err != nil {
				return err
			}
			updated, err := resource.NewHelper(info.Client, info.Mapping).Replace(info.Namespace, info.Name, false, data)
			if err != nil {
				fmt.Fprintln(cmd.Out(), results.AddError(err, info))
				return nil
			}
			info.Refresh(updated, true)
			fmt.Fprintf(out, "%s/%s\n", info.Mapping.Resource, info.Name)
			return nil
		})
		if err != nil {
			return preservedFile(err, file, cmd.Out())
		}

		if results.retryable > 0 {
			fmt.Fprintf(cmd.Out(), "You can run `%s update -f %s` to try this update again.\n", fullName, file)
			return errExit
		}
		if results.conflict > 0 {
			fmt.Fprintf(cmd.Out(), "You must update your local resource version and run `%s update -f %s` to overwrite the remote changes.\n", fullName, file)
			return errExit
		}
		if len(results.edit) == 0 {
			if results.notfound == 0 {
				os.Remove(file)
			} else {
				fmt.Fprintf(cmd.Out(), "The edits you made on deleted resources have been saved to %q\n", file)
			}
			return nil
		}

		// loop again and edit the remaining items
		infos = results.edit
	}
	return nil
}
Exemplo n.º 3
0
func RunEdit(f *cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args []string, options *EditOptions) error {
	var printer kubectl.ResourcePrinter
	var ext string
	switch format := cmdutil.GetFlagString(cmd, "output"); format {
	case "json":
		printer = &kubectl.JSONPrinter{}
		ext = ".json"
	case "yaml":
		printer = &kubectl.YAMLPrinter{}
		ext = ".yaml"
	default:
		return cmdutil.UsageError(cmd, "The flag 'output' must be one of yaml|json")
	}

	cmdNamespace, enforceNamespace, err := f.DefaultNamespace()
	if err != nil {
		return err
	}

	mapper, typer := f.Object(cmdutil.GetIncludeThirdPartyAPIs(cmd))
	resourceMapper := &resource.Mapper{
		ObjectTyper:  typer,
		RESTMapper:   mapper,
		ClientMapper: resource.ClientMapperFunc(f.ClientForMapping),

		// NB: we use `f.Decoder(false)` to get a plain deserializer for
		// the resourceMapper, since it's used to read in edits and
		// we don't want to convert into the internal version when
		// reading in edits (this would cause us to potentially try to
		// compare two different GroupVersions).
		Decoder: f.Decoder(false),
	}

	r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)).
		NamespaceParam(cmdNamespace).DefaultNamespace().
		FilenameParam(enforceNamespace, options.Recursive, options.Filenames...).
		ResourceTypeOrNameArgs(true, args...).
		Flatten().
		Latest().
		Do()
	err = r.Err()
	if err != nil {
		return err
	}

	infos, err := r.Infos()
	if err != nil {
		return err
	}

	clientConfig, err := f.ClientConfig()
	if err != nil {
		return err
	}

	encoder := f.JSONEncoder()
	defaultVersion, err := cmdutil.OutputVersion(cmd, clientConfig.GroupVersion)
	if err != nil {
		return err
	}
	originalObj, err := resource.AsVersionedObject(infos, false, defaultVersion, encoder)
	if err != nil {
		return err
	}

	var (
		windowsLineEndings = cmdutil.GetFlagBool(cmd, "windows-line-endings")
		edit               = editor.NewDefaultEditor(f.EditorEnvs())
		results            = editResults{}
		original           = []byte{}
		edited             = []byte{}
		file               string
	)

	containsError := false

	for {
		// infos mutates over time to be the list of things we've tried and failed to edit
		// this means that our overall list changes over time.
		objToEdit, err := resource.AsVersionedObject(infos, false, defaultVersion, encoder)
		if err != nil {
			return err
		}

		// generate the file to edit
		buf := &bytes.Buffer{}
		var w io.Writer = buf
		if windowsLineEndings {
			w = crlf.NewCRLFWriter(w)
		}
		if err := results.header.writeTo(w); err != nil {
			return preservedFile(err, results.file, errOut)
		}
		if !containsError {
			if err := printer.PrintObj(objToEdit, w); err != nil {
				return preservedFile(err, results.file, errOut)
			}
			original = buf.Bytes()
		} else {
			// In case of an error, preserve the edited file.
			// Remove the comments (header) from it since we already
			// have included the latest header in the buffer above.
			buf.Write(manualStrip(edited))
		}

		// launch the editor
		editedDiff := edited
		edited, file, err = edit.LaunchTempFile(fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])), ext, buf)
		if err != nil {
			return preservedFile(err, results.file, errOut)
		}
		if bytes.Equal(stripComments(editedDiff), stripComments(edited)) {
			// Ugly hack right here. We will hit this either (1) when we try to
			// save the same changes we tried to save in the previous iteration
			// which means our changes are invalid or (2) when we exit the second
			// time. The second case is more usual so we can probably live with it.
			// TODO: A less hacky fix would be welcome :)
			fmt.Fprintln(errOut, "Edit cancelled, no valid changes were saved.")
			return nil
		}

		// cleanup any file from the previous pass
		if len(results.file) > 0 {
			os.Remove(results.file)
		}
		glog.V(4).Infof("User edited:\n%s", string(edited))

		// Compare content without comments
		if bytes.Equal(stripComments(original), stripComments(edited)) {
			os.Remove(file)
			fmt.Fprintln(errOut, "Edit cancelled, no changes made.")
			return nil
		}
		lines, err := hasLines(bytes.NewBuffer(edited))
		if err != nil {
			return preservedFile(err, file, errOut)
		}
		if !lines {
			os.Remove(file)
			fmt.Fprintln(errOut, "Edit cancelled, saved file was empty.")
			return nil
		}

		results = editResults{
			file: file,
		}

		// parse the edited file
		updates, err := resourceMapper.InfoForData(edited, "edited-file")
		if err != nil {
			// syntax error
			containsError = true
			results.header.reasons = append(results.header.reasons, editReason{head: fmt.Sprintf("The edited file had a syntax error: %v", err)})
			continue
		}
		// not a syntax error as it turns out...
		containsError = false

		namespaceVisitor := resource.NewFlattenListVisitor(updates, resourceMapper)
		// need to make sure the original namespace wasn't changed while editing
		if err = namespaceVisitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil {
			return preservedFile(err, file, errOut)
		}

		mutatedObjects := []runtime.Object{}
		annotationVisitor := resource.NewFlattenListVisitor(updates, resourceMapper)
		// iterate through all items to apply annotations
		if err = annotationVisitor.Visit(func(info *resource.Info, incomingErr error) error {
			// put configuration annotation in "updates"
			if err := kubectl.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), info, encoder); err != nil {
				return err
			}
			if cmdutil.ShouldRecord(cmd, info) {
				if err := cmdutil.RecordChangeCause(info.Object, f.Command()); err != nil {
					return err
				}
			}
			mutatedObjects = append(mutatedObjects, info.Object)

			return nil

		}); err != nil {
			return preservedFile(err, file, errOut)
		}

		// if we mutated a list in the visitor, persist the changes on the overall object
		if meta.IsListType(updates.Object) {
			meta.SetList(updates.Object, mutatedObjects)
		}

		patchVisitor := resource.NewFlattenListVisitor(updates, resourceMapper)
		err = patchVisitor.Visit(func(info *resource.Info, incomingErr error) error {
			currOriginalObj := originalObj

			// if we're editing a list, then navigate the list to find the item that we're currently trying to edit
			if meta.IsListType(originalObj) {
				currOriginalObj = nil
				editObjUID, err := meta.NewAccessor().UID(info.Object)
				if err != nil {
					return err
				}

				listItems, err := meta.ExtractList(originalObj)
				if err != nil {
					return err
				}

				// iterate through the list to find the item with the matching UID
				for i := range listItems {
					originalObjUID, err := meta.NewAccessor().UID(listItems[i])
					if err != nil {
						return err
					}
					if editObjUID == originalObjUID {
						currOriginalObj = listItems[i]
						break
					}
				}
				if currOriginalObj == nil {
					return fmt.Errorf("no original object found for %#v", info.Object)
				}

			}

			originalSerialization, err := runtime.Encode(encoder, currOriginalObj)
			if err != nil {
				return err
			}
			editedSerialization, err := runtime.Encode(encoder, info.Object)
			if err != nil {
				return err
			}

			// compute the patch on a per-item basis
			// use strategic merge to create a patch
			originalJS, err := yaml.ToJSON(originalSerialization)
			if err != nil {
				return err
			}
			editedJS, err := yaml.ToJSON(editedSerialization)
			if err != nil {
				return err
			}

			if reflect.DeepEqual(originalJS, editedJS) {
				// no edit, so just skip it.
				cmdutil.PrintSuccess(mapper, false, out, info.Mapping.Resource, info.Name, "skipped")
				return nil
			}

			patch, err := strategicpatch.CreateStrategicMergePatch(originalJS, editedJS, currOriginalObj)
			// TODO: change all jsonmerge to strategicpatch
			// for checking preconditions
			preconditions := []jsonmerge.PreconditionFunc{}
			if err != nil {
				glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
				return err
			} else {
				preconditions = append(preconditions, jsonmerge.RequireKeyUnchanged("apiVersion"))
				preconditions = append(preconditions, jsonmerge.RequireKeyUnchanged("kind"))
				preconditions = append(preconditions, jsonmerge.RequireMetadataKeyUnchanged("name"))
				results.version = defaultVersion
			}

			if hold, msg := jsonmerge.TestPreconditionsHold(patch, preconditions); !hold {
				fmt.Fprintf(errOut, "error: %s", msg)
				return preservedFile(nil, file, errOut)
			}

			patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, api.StrategicMergePatchType, patch)
			if err != nil {
				fmt.Fprintln(out, results.addError(err, info))
				return nil
			}
			info.Refresh(patched, true)
			cmdutil.PrintSuccess(mapper, false, out, info.Mapping.Resource, info.Name, "edited")
			return nil
		})
		if err != nil {
			return preservedFile(err, results.file, errOut)
		}

		// Handle all possible errors
		//
		// 1. retryable: propose kubectl replace -f
		// 2. notfound: indicate the location of the saved configuration of the deleted resource
		// 3. invalid: retry those on the spot by looping ie. reloading the editor
		if results.retryable > 0 {
			fmt.Fprintf(errOut, "You can run `%s replace -f %s` to try this update again.\n", filepath.Base(os.Args[0]), file)
			return errExit
		}
		if results.notfound > 0 {
			fmt.Fprintf(errOut, "The edits you made on deleted resources have been saved to %q\n", file)
			return errExit
		}

		if len(results.edit) == 0 {
			if results.notfound == 0 {
				os.Remove(file)
			} else {
				fmt.Fprintf(out, "The edits you made on deleted resources have been saved to %q\n", file)
			}
			return nil
		}

		// loop again and edit the remaining items
		infos = results.edit
	}
}
Exemplo n.º 4
0
func RunEdit(f *cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args []string, filenames []string) error {
	var printer kubectl.ResourcePrinter
	var ext string
	switch format := cmdutil.GetFlagString(cmd, "output"); format {
	case "json":
		printer = &kubectl.JSONPrinter{}
		ext = ".json"
	case "yaml":
		printer = &kubectl.YAMLPrinter{}
		ext = ".yaml"
	default:
		return cmdutil.UsageError(cmd, "The flag 'output' must be one of yaml|json")
	}

	cmdNamespace, enforceNamespace, err := f.DefaultNamespace()
	if err != nil {
		return err
	}

	mapper, typer := f.Object()
	resourceMapper := &resource.Mapper{
		ObjectTyper:  typer,
		RESTMapper:   mapper,
		ClientMapper: resource.ClientMapperFunc(f.ClientForMapping),
		Decoder:      f.Decoder(true),
	}

	r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)).
		NamespaceParam(cmdNamespace).DefaultNamespace().
		FilenameParam(enforceNamespace, filenames...).
		ResourceTypeOrNameArgs(true, args...).
		Latest().
		Flatten().
		Do()
	err = r.Err()
	if err != nil {
		return err
	}

	infos, err := r.Infos()
	if err != nil {
		return err
	}

	clientConfig, err := f.ClientConfig()
	if err != nil {
		return err
	}

	encoder := f.JSONEncoder()
	defaultVersion, err := cmdutil.OutputVersion(cmd, clientConfig.GroupVersion)
	if err != nil {
		return err
	}
	objs, err := resource.AsVersionedObjects(infos, defaultVersion.String(), encoder)
	if err != nil {
		return err
	}

	var (
		windowsLineEndings = cmdutil.GetFlagBool(cmd, "windows-line-endings")
		edit               = editor.NewDefaultEditor(f.EditorEnvs())
		results            = editResults{}
		original           = []byte{}
		edited             = []byte{}
		file               string
	)

outter:
	for i := range objs {
		obj := objs[i]
		// some bookkeeping
		results.header.flush()
		containsError := false

		for {
			// generate the file to edit
			buf := &bytes.Buffer{}
			var w io.Writer = buf
			if windowsLineEndings {
				w = util.NewCRLFWriter(w)
			}
			if err := results.header.writeTo(w); err != nil {
				return preservedFile(err, results.file, errOut)
			}
			if !containsError {
				if err := printer.PrintObj(obj, w); err != nil {
					return preservedFile(err, results.file, errOut)
				}
				original = buf.Bytes()
			} else {
				// In case of an error, preserve the edited file.
				// Remove the comments (header) from it since we already
				// have included the latest header in the buffer above.
				buf.Write(manualStrip(edited))
			}

			// launch the editor
			editedDiff := edited
			edited, file, err = edit.LaunchTempFile(fmt.Sprintf("%s-edit-", path.Base(os.Args[0])), ext, buf)
			if err != nil {
				return preservedFile(err, results.file, errOut)
			}
			if bytes.Equal(stripComments(editedDiff), stripComments(edited)) {
				// Ugly hack right here. We will hit this either (1) when we try to
				// save the same changes we tried to save in the previous iteration
				// which means our changes are invalid or (2) when we exit the second
				// time. The second case is more usual so we can probably live with it.
				// TODO: A less hacky fix would be welcome :)
				fmt.Fprintln(errOut, "Edit cancelled, no valid changes were saved.")
				continue outter
			}

			// cleanup any file from the previous pass
			if len(results.file) > 0 {
				os.Remove(results.file)
			}
			glog.V(4).Infof("User edited:\n%s", string(edited))

			// Compare content without comments
			if bytes.Equal(stripComments(original), stripComments(edited)) {
				os.Remove(file)
				fmt.Fprintln(errOut, "Edit cancelled, no changes made.")
				continue outter
			}
			lines, err := hasLines(bytes.NewBuffer(edited))
			if err != nil {
				return preservedFile(err, file, errOut)
			}
			if !lines {
				os.Remove(file)
				fmt.Fprintln(errOut, "Edit cancelled, saved file was empty.")
				continue outter
			}

			results = editResults{
				file: file,
			}

			// parse the edited file
			updates, err := resourceMapper.InfoForData(edited, "edited-file")
			if err != nil {
				// syntax error
				containsError = true
				results.header.reasons = append(results.header.reasons, editReason{head: fmt.Sprintf("The edited file had a syntax error: %v", err)})
				continue
			}
			// not a syntax error as it turns out...
			containsError = false

			// put configuration annotation in "updates"
			if err := kubectl.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), updates, encoder); err != nil {
				return preservedFile(err, file, errOut)
			}
			if cmdutil.ShouldRecord(cmd, updates) {
				err = cmdutil.RecordChangeCause(updates.Object, f.Command())
				if err != nil {
					return err
				}
			}
			editedCopy := edited
			if editedCopy, err = runtime.Encode(encoder, updates.Object); err != nil {
				return preservedFile(err, file, errOut)
			}

			visitor := resource.NewFlattenListVisitor(updates, resourceMapper)

			// need to make sure the original namespace wasn't changed while editing
			if err = visitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil {
				return preservedFile(err, file, errOut)
			}

			// use strategic merge to create a patch
			originalJS, err := yaml.ToJSON(original)
			if err != nil {
				return preservedFile(err, file, errOut)
			}
			editedJS, err := yaml.ToJSON(editedCopy)
			if err != nil {
				return preservedFile(err, file, errOut)
			}
			patch, err := strategicpatch.CreateStrategicMergePatch(originalJS, editedJS, obj)
			// TODO: change all jsonmerge to strategicpatch
			// for checking preconditions
			preconditions := []jsonmerge.PreconditionFunc{}
			if err != nil {
				glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
				return preservedFile(err, file, errOut)
			} else {
				preconditions = append(preconditions, jsonmerge.RequireKeyUnchanged("apiVersion"))
				preconditions = append(preconditions, jsonmerge.RequireKeyUnchanged("kind"))
				preconditions = append(preconditions, jsonmerge.RequireMetadataKeyUnchanged("name"))
				results.version = defaultVersion
			}

			if hold, msg := jsonmerge.TestPreconditionsHold(patch, preconditions); !hold {
				fmt.Fprintf(errOut, "error: %s\n", msg)
				return preservedFile(nil, file, errOut)
			}

			errorMsg := ""
			err = visitor.Visit(func(info *resource.Info, err error) error {
				patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, api.StrategicMergePatchType, patch)
				if err != nil {
					errorMsg = results.addError(err, info)
					return err
				}
				info.Refresh(patched, true)
				cmdutil.PrintSuccess(mapper, false, out, info.Mapping.Resource, info.Name, "edited")
				return nil
			})
			if err == nil {
				os.Remove(file)
				continue outter
			}
			// Handle all possible errors
			//
			// 1. retryable: propose kubectl replace -f
			// 2. notfound: indicate the location of the saved configuration of the deleted resource
			// 3. invalid: retry those on the spot by looping ie. reloading the editor
			if results.retryable > 0 {
				fmt.Fprintln(errOut, errorMsg)
				fmt.Fprintf(errOut, "You can run `%s replace -f %s` to try this update again.\n", path.Base(os.Args[0]), file)
				continue outter
			}
			if results.notfound > 0 {
				fmt.Fprintln(errOut, errorMsg)
				fmt.Fprintf(errOut, "The edits you made on deleted resources have been saved to %q\n", file)
				continue outter
			}
			// validation error
			containsError = true
		}
	}
	return nil
}
Exemplo n.º 5
0
// RunEdit contains all the necessary functionality for the OpenShift cli edit command.
func (o *EditOptions) RunEdit() error {
	r := o.builder.Flatten().Do()
	results := editResults{}
	infos, err := r.Infos()
	if err != nil {
		return err
	}
	for {
		obj, err := resource.AsVersionedObject(infos, false, o.version)
		if err != nil {
			return preservedFile(err, results.file, o.out)
		}

		// TODO: add an annotating YAML printer that can print inline comments on each field,
		//   including descriptions or validation errors

		// generate the file to edit
		buf := &bytes.Buffer{}
		if _, err := results.header.WriteTo(buf); err != nil {
			return preservedFile(err, results.file, o.out)
		}
		if err := o.printer.PrintObj(obj, buf); err != nil {
			return preservedFile(err, results.file, o.out)
		}
		original := buf.Bytes()

		// launch the editor
		edit := editor.NewDefaultEditor()
		edited, file, err := edit.LaunchTempFile("oc-edit-", o.ext, buf)
		if err != nil {
			return preservedFile(err, results.file, o.out)
		}

		// cleanup any file from the previous pass
		if len(results.file) > 0 {
			os.Remove(results.file)
		}

		glog.V(4).Infof("User edited:\n%s", string(edited))
		lines, err := hasLines(bytes.NewBuffer(edited))
		if err != nil {
			return preservedFile(err, file, o.out)
		}
		if bytes.Equal(original, edited) {
			if len(results.edit) > 0 {
				preservedFile(nil, file, o.out)
			} else {
				os.Remove(file)
			}
			fmt.Fprintln(o.out, "Edit cancelled, no changes made.")
			return nil
		}
		if !lines {
			if len(results.edit) > 0 {
				preservedFile(nil, file, o.out)
			} else {
				os.Remove(file)
			}
			fmt.Fprintln(o.out, "Edit cancelled, saved file was empty.")
			return nil
		}

		results = editResults{
			file: file,
		}

		// parse the edited file
		updates, err := o.rmap.InfoForData(edited, "edited-file")
		if err != nil {
			results.header.reasons = append(results.header.reasons, editReason{
				head: fmt.Sprintf("The edited file had a syntax error: %v", err),
			})
			continue
		}

		visitor := resource.NewFlattenListVisitor(updates, o.rmap)

		// need to make sure the original namespace wasn't changed while editing
		if err = visitor.Visit(resource.RequireNamespace(o.namespace)); err != nil {
			return preservedFile(err, file, o.out)
		}

		// attempt to calculate a delta for merging conflicts
		delta, err := jsonmerge.NewDelta(original, edited)
		if err != nil {
			glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
			delta = nil
		} else {
			delta.AddPreconditions(jsonmerge.RequireKeyUnchanged("apiVersion"))
			results.delta = delta
			results.version = o.version
		}

		err = visitor.Visit(func(info *resource.Info, err error) error {
			if err != nil {
				return err
			}
			data, err := info.Mapping.Codec.Encode(info.Object)
			if err != nil {
				return err
			}
			updated, err := resource.NewHelper(info.Client, info.Mapping).Replace(info.Namespace, info.Name, false, data)
			if err != nil {
				fmt.Fprintln(o.out, results.AddError(err, info))
				return nil
			}
			info.Refresh(updated, true)
			fmt.Fprintf(o.out, "%s/%s\n", info.Mapping.Resource, info.Name)
			return nil
		})
		if err != nil {
			return preservedFile(err, file, o.out)
		}

		if results.retryable > 0 {
			fmt.Fprintf(o.out, "You can run `%s update -f %s` to try this update again.\n", o.fullName, file)
			return errExit
		}
		if results.conflict > 0 {
			fmt.Fprintf(o.out, "You must update your local resource version and run `%s update -f %s` to overwrite the remote changes.\n", o.fullName, file)
			return errExit
		}
		if len(results.edit) == 0 {
			if results.notfound == 0 {
				os.Remove(file)
			} else {
				fmt.Fprintf(o.out, "The edits you made on deleted resources have been saved to %q\n", file)
			}
			return nil
		}

		// loop again and edit the remaining items
		infos = results.edit
	}
}
Exemplo n.º 6
0
func runEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args []string, options *resource.FilenameOptions, editMode EditMode) error {
	o, err := getPrinter(cmd)
	if err != nil {
		return err
	}

	mapper, resourceMapper, r, cmdNamespace, err := getMapperAndResult(f, args, options, editMode)
	if err != nil {
		return err
	}

	clientConfig, err := f.ClientConfig()
	if err != nil {
		return err
	}

	encoder := f.JSONEncoder()
	defaultVersion, err := cmdutil.OutputVersion(cmd, clientConfig.GroupVersion)
	if err != nil {
		return err
	}

	infos, err := r.Infos()
	if err != nil {
		return err
	}

	var (
		windowsLineEndings = cmdutil.GetFlagBool(cmd, "windows-line-endings")
		edit               = editor.NewDefaultEditor(f.EditorEnvs())
	)

	var (
		results  = editResults{}
		original = []byte{}
		edited   = []byte{}
		file     string
	)

	containsError := false
	for {
		originalObj, err := resource.AsVersionedObject(infos, false, defaultVersion, encoder)
		if err != nil {
			return err
		}

		objToEdit := originalObj

		// generate the file to edit
		buf := &bytes.Buffer{}
		var w io.Writer = buf
		if windowsLineEndings {
			w = crlf.NewCRLFWriter(w)
		}

		if o.addHeader {
			results.header.writeTo(w)
		}

		if !containsError {
			if err := o.printer.PrintObj(objToEdit, w); err != nil {
				return preservedFile(err, results.file, errOut)
			}
			original = buf.Bytes()
		} else {
			// In case of an error, preserve the edited file.
			// Remove the comments (header) from it since we already
			// have included the latest header in the buffer above.
			buf.Write(manualStrip(edited))
		}

		// launch the editor
		editedDiff := edited
		edited, file, err = edit.LaunchTempFile(fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])), o.ext, buf)
		if err != nil {
			return preservedFile(err, results.file, errOut)
		}
		if editMode == NormalEditMode || containsError {
			if bytes.Equal(stripComments(editedDiff), stripComments(edited)) {
				// Ugly hack right here. We will hit this either (1) when we try to
				// save the same changes we tried to save in the previous iteration
				// which means our changes are invalid or (2) when we exit the second
				// time. The second case is more usual so we can probably live with it.
				// TODO: A less hacky fix would be welcome :)
				return preservedFile(fmt.Errorf("%s", "Edit cancelled, no valid changes were saved."), file, errOut)
			}
		}

		// cleanup any file from the previous pass
		if len(results.file) > 0 {
			os.Remove(results.file)
		}
		glog.V(4).Infof("User edited:\n%s", string(edited))

		// Apply validation
		schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"), cmdutil.GetFlagString(cmd, "schema-cache-dir"))
		if err != nil {
			return preservedFile(err, file, errOut)
		}
		err = schema.ValidateBytes(stripComments(edited))
		if err != nil {
			results = editResults{
				file: file,
			}
			containsError = true
			fmt.Fprintln(out, results.addError(errors.NewInvalid(api.Kind(""), "", field.ErrorList{field.Invalid(nil, "The edited file failed validation", fmt.Sprintf("%v", err))}), infos[0]))
			continue
		}

		// Compare content without comments
		if bytes.Equal(stripComments(original), stripComments(edited)) {
			os.Remove(file)
			fmt.Fprintln(errOut, "Edit cancelled, no changes made.")
			return nil
		}

		lines, err := hasLines(bytes.NewBuffer(edited))
		if err != nil {
			return preservedFile(err, file, errOut)
		}
		if !lines {
			os.Remove(file)
			fmt.Fprintln(errOut, "Edit cancelled, saved file was empty.")
			return nil
		}

		results = editResults{
			file: file,
		}

		// parse the edited file
		updates, err := resourceMapper.InfoForData(edited, "edited-file")
		if err != nil {
			// syntax error
			containsError = true
			results.header.reasons = append(results.header.reasons, editReason{head: fmt.Sprintf("The edited file had a syntax error: %v", err)})
			continue
		}
		// not a syntax error as it turns out...
		containsError = false

		namespaceVisitor := resource.NewFlattenListVisitor(updates, resourceMapper)
		// need to make sure the original namespace wasn't changed while editing
		if err = namespaceVisitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil {
			return preservedFile(err, file, errOut)
		}

		// iterate through all items to apply annotations
		mutatedObjects, err := visitAnnotation(cmd, f, updates, resourceMapper, encoder)
		if err != nil {
			return preservedFile(err, file, errOut)
		}

		// if we mutated a list in the visitor, persist the changes on the overall object
		if meta.IsListType(updates.Object) {
			meta.SetList(updates.Object, mutatedObjects)
		}

		switch editMode {
		case NormalEditMode:
			err = visitToPatch(originalObj, updates, mapper, resourceMapper, encoder, out, errOut, defaultVersion, &results, file)
		case EditBeforeCreateMode:
			err = visitToCreate(updates, mapper, resourceMapper, out, errOut, defaultVersion, &results, file)
		default:
			err = fmt.Errorf("Not supported edit mode %q", editMode)
		}
		if err != nil {
			return preservedFile(err, results.file, errOut)
		}

		// Handle all possible errors
		//
		// 1. retryable: propose kubectl replace -f
		// 2. notfound: indicate the location of the saved configuration of the deleted resource
		// 3. invalid: retry those on the spot by looping ie. reloading the editor
		if results.retryable > 0 {
			fmt.Fprintf(errOut, "You can run `%s replace -f %s` to try this update again.\n", filepath.Base(os.Args[0]), file)
			return cmdutil.ErrExit
		}
		if results.notfound > 0 {
			fmt.Fprintf(errOut, "The edits you made on deleted resources have been saved to %q\n", file)
			return cmdutil.ErrExit
		}

		if len(results.edit) == 0 {
			if results.notfound == 0 {
				os.Remove(file)
			} else {
				fmt.Fprintf(out, "The edits you made on deleted resources have been saved to %q\n", file)
			}
			return nil
		}

		if len(results.header.reasons) > 0 {
			containsError = true
		}
	}
}
Exemplo n.º 7
0
func doEdit(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, prefix string, rejectSame bool, addSource func(*resource.Builder, bool, kubectl.ResourcePrinter) *resource.Builder, process editProcessingFunc) error {
	printer, ext, err := getEditPrinter(cmd)
	if err != nil {
		return err
	}

	cmdNamespace, enforceNamespace, err := f.DefaultNamespace()
	if err != nil {
		return err
	}

	mapper, typer := f.Object()
	rmap := &resource.Mapper{
		ObjectTyper:  typer,
		RESTMapper:   mapper,
		ClientMapper: f.ClientMapperForCommand(),
	}

	b := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()).
		NamespaceParam(cmdNamespace).DefaultNamespace().
		Flatten()

	b = addSource(b, enforceNamespace, printer)

	r := b.Do()
	err = r.Err()
	if err != nil {
		return err
	}

	infos, err := r.Infos()
	if err != nil {
		return err
	}

	clientConfig, err := f.ClientConfig()
	if err != nil {
		return err
	}

	defaultVersion := cmdutil.OutputVersion(cmd, clientConfig.Version)
	results := editResults{}
	for {
		obj, err := resource.AsVersionedObject(infos, false, defaultVersion)
		if err != nil {
			return preservedFile(err, results.file, out)
		}

		// TODO: add an annotating YAML printer that can print inline comments on each field,
		//   including descriptions or validation errors

		// generate the file to edit
		buf := &bytes.Buffer{}
		if err := results.header.writeTo(buf); err != nil {
			return preservedFile(err, results.file, out)
		}
		if err := printer.PrintObj(obj, buf); err != nil {
			return preservedFile(err, results.file, out)
		}
		original := buf.Bytes()

		// launch the editor
		edit := editor.NewDefaultEditor()
		edited, file, err := edit.LaunchTempFile("kubectl-edit-", ext, buf)
		if err != nil {
			return preservedFile(err, results.file, out)
		}

		// cleanup any file from the previous pass
		if len(results.file) > 0 {
			os.Remove(results.file)
		}

		glog.V(4).Infof("User edited:\n%s", string(edited))
		fmt.Printf("User edited:\n%s", string(edited))
		lines, err := hasLines(bytes.NewBuffer(edited))
		if err != nil {
			return preservedFile(err, file, out)
		}
		if rejectSame && bytes.Equal(original, edited) {
			if len(results.edit) > 0 {
				preservedFile(nil, file, out)
			} else {
				os.Remove(file)
			}
			fmt.Fprintln(out, "Edit cancelled, no changes made.")
			return nil
		}
		if !lines {
			if len(results.edit) > 0 {
				preservedFile(nil, file, out)
			} else {
				os.Remove(file)
			}
			fmt.Fprintln(out, "Edit cancelled, saved file was empty.")
			return nil
		}

		results = editResults{
			file: file,
		}

		// parse the edited file
		updates, err := rmap.InfoForData(edited, "edited-file")
		if err != nil {
			return preservedFile(err, file, out)
		}

		visitor := resource.NewFlattenListVisitor(updates, rmap)

		// need to make sure the original namespace wasn't changed while editing
		if err := visitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil {
			return preservedFile(err, file, out)
		}

		err = process(visitor, original, edited, obj, updates, file, &results, mapper, defaultVersion)

		if err != nil {
			return preservedFile(err, file, out)
		}

		if results.retryable > 0 {
			fmt.Fprintf(out, "You can run `kubectl replace -f %s` to try this update again.\n", file)
			return errExit
		}
		if results.conflict > 0 {
			fmt.Fprintf(out, "You must update your local resource version and run `kubectl replace -f %s` to overwrite the remote changes.\n", file)
			return errExit
		}
		if len(results.edit) == 0 {
			if results.notfound == 0 {
				os.Remove(file)
			} else {
				fmt.Fprintf(out, "The edits you made on deleted resources have been saved to %q\n", file)
			}
			return nil
		}

		// loop again and edit the remaining items
		infos = results.edit
	}
	return nil
}