// StartRecordingToSink starts sending events received from the specified eventBroadcaster to the given sink. // The return value can be ignored or used to stop recording, if desired. // TODO: make me an object with parameterizable queue length and retry interval func (eventBroadcaster *eventBroadcasterImpl) StartRecordingToSink(sink EventSink) watch.Interface { // The default math/rand package functions aren't thread safe, so create a // new Rand object for each StartRecording call. randGen := rand.New(rand.NewSource(time.Now().UnixNano())) var eventCache *historyCache = NewEventCache() return eventBroadcaster.StartEventWatcher( func(event *api.Event) { // Make a copy before modification, because there could be multiple listeners. // Events are safe to copy like this. eventCopy := *event event = &eventCopy var patch []byte previousEvent := eventCache.getEvent(event) updateExistingEvent := previousEvent.Count > 0 if updateExistingEvent { // we still need to copy Name because the Patch relies on the Name to find the target event event.Name = previousEvent.Name event.Count = previousEvent.Count + 1 // we need to make sure the Count and LastTimestamp are the only differences between event and the eventCopy2 eventCopy2 := *event eventCopy2.Count = 0 eventCopy2.LastTimestamp = unversioned.NewTime(time.Unix(0, 0)) newData, _ := json.Marshal(event) oldData, _ := json.Marshal(eventCopy2) patch, _ = strategicpatch.CreateStrategicMergePatch(oldData, newData, event) } tries := 0 for { if recordEvent(sink, event, patch, updateExistingEvent, eventCache) { break } tries++ if tries >= maxTriesPerEvent { glog.Errorf("Unable to write event '%#v' (retry limit exceeded!)", event) break } // Randomize the first sleep so that various clients won't all be // synced up if the master goes down. if tries == 1 { time.Sleep(time.Duration(float64(sleepDuration) * randGen.Float64())) } else { time.Sleep(sleepDuration) } } }) }
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() rmap := &resource.Mapper{ ObjectTyper: typer, RESTMapper: mapper, ClientMapper: f.ClientMapperForCommand(), } r := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()). 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 } defaultVersion := cmdutil.OutputVersion(cmd, clientConfig.Version) results := editResults{} for { objs, err := resource.AsVersionedObjects(infos, defaultVersion) 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{} 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)) 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 := rmap.InfoForData(edited, "edited-file") if err != nil { return fmt.Errorf("The edited file had a syntax error: %v", err) } // annotate the edited object for kubectl apply if err := kubectl.UpdateApplyAnnotation(updates); 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) } // 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 }