diff --git a/docs/kubedb/README.md b/docs/kubedb/README.md index d9a1c5468..667240454 100644 --- a/docs/kubedb/README.md +++ b/docs/kubedb/README.md @@ -12,7 +12,8 @@ Basic Commands (Beginner): Basic Commands (Intermediate): get Display one or many resources - delete Delete resources by filenames, stdin, resources and names, or by resources and label selector + edit Edit a resource on the server + delete Delete resources by file names, stdin, resources and names, or by resources and label selector Troubleshooting and Debugging Commands: describe Show details of a specific resource or group of resources diff --git a/docs/kubedb/edit.md b/docs/kubedb/edit.md new file mode 100644 index 000000000..6cb5d8aec --- /dev/null +++ b/docs/kubedb/edit.md @@ -0,0 +1,31 @@ +# kubedb edit + +## Example + +##### Help for edit command + +```bash +$ kubedb edit --help + +Edit a resource from the default editor. + +The edit command allows you to directly edit any API resource you can retrieve via the command line tools. It will open +the editor defined by your KUBEDB _EDITOR, or EDITOR environment variables, or fall back to 'nano' + +Examples: + # Edit the elastic named 'elasticsearch-demo': + kubedb edit es/elasticsearch-demo + + # Use an alternative editor + KUBEDB_EDITOR="nano" kubedb edit es/elasticsearch-demo + +Options: + --all=false: [-all] to select all the specified resources. + -o, --output='yaml': Output format. One of: yaml|json. + -l, --selector='': Selector (label query) to filter on. + +Usage: + kubedb edit (RESOURCE/NAME) [flags] [options] + +Use "kubedb edit options" for a list of global command-line options (applies to all commands). +``` diff --git a/glide.lock b/glide.lock index 13ae74118..44648eed5 100644 --- a/glide.lock +++ b/glide.lock @@ -1,8 +1,8 @@ hash: 3bdc093bab543738c2fe4fba8cbc716c75d52206086eca445e718503a0d5a96e -updated: 2017-05-22T10:36:11.662187096+06:00 +updated: 2017-05-22T17:19:59.222862382+06:00 imports: - name: cloud.google.com/go - version: 3b1ae45394a234c385be014e9a488f2bb6eef821 + version: fe3d41e1ecb2ce36ad3a979037c9b9a2b726226f subpackages: - compute/metadata - internal @@ -176,7 +176,7 @@ imports: - name: github.com/shurcooL/sanitized_anchor_name version: 10ef21a441db47d8b13ebcc5fd2310f636973c77 - name: github.com/Sirupsen/logrus - version: 51fe59aca108dc5680109e7b2051cbdcfa5a253c + version: 4b6ea7319e214d98c938f12692336f7ca9348d6b - name: github.com/spf13/cobra version: ca57f0f5dba473a8a58765d16d7e811fb8027add - name: github.com/spf13/pflag @@ -358,6 +358,7 @@ imports: - pkg/kubectl - pkg/kubectl/cmd/templates - pkg/kubectl/cmd/util + - pkg/kubectl/cmd/util/editor - pkg/kubectl/resource - pkg/kubelet/qos - pkg/kubelet/types diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 48d008f11..fc11fea32 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -31,6 +31,7 @@ func NewKubedbCommand(in io.Reader, out, err io.Writer) *cobra.Command { Message: "Basic Commands (Intermediate):", Commands: []*cobra.Command{ NewCmdGet(out, err), + NewCmdEdit(out, err), NewCmdDelete(out, err), }, }, diff --git a/pkg/cmd/edit.go b/pkg/cmd/edit.go new file mode 100644 index 000000000..4f6b749ac --- /dev/null +++ b/pkg/cmd/edit.go @@ -0,0 +1,435 @@ +package cmd + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/golang/glog" + tapi "github.com/k8sdb/apimachinery/api" + "github.com/k8sdb/apimachinery/client/clientset" + "github.com/k8sdb/kubedb/pkg/cmd/editor" + "github.com/k8sdb/kubedb/pkg/cmd/encoder" + "github.com/k8sdb/kubedb/pkg/cmd/printer" + "github.com/k8sdb/kubedb/pkg/cmd/util" + "github.com/k8sdb/kubedb/pkg/kube" + "github.com/spf13/cobra" + kapi "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/meta" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/kubectl" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/strategicpatch" + "k8s.io/kubernetes/pkg/util/yaml" +) + +var ( + editLong = templates.LongDesc(` + Edit a resource from the default editor. + + The edit command allows you to directly edit any API resource you can retrieve via the + command line tools. It will open the editor defined by your KUBEDB_EDITOR, or EDITOR + environment variables, or fall back to 'nano'`) + + editExample = templates.Examples(` + # Edit the elastic named 'elasticsearch-demo': + kubedb edit es/elasticsearch-demo + + # Use an alternative editor + KUBEDB_EDITOR="nano" kubedb edit es/elasticsearch-demo`) +) + +func NewCmdEdit(out, errOut io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "edit (RESOURCE/NAME)", + Short: "Edit a resource on the server", + Long: editLong, + Example: fmt.Sprintf(editExample), + Run: func(cmd *cobra.Command, args []string) { + f := kube.NewKubeFactory(cmd) + cmdutil.CheckErr(RunEdit(f, out, errOut, cmd, args)) + }, + } + + util.AddEditFlags(cmd) + return cmd +} + +func RunEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args []string) error { + return runEdit(f, out, errOut, cmd, args) +} + +func runEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args []string) error { + o, err := printer.NewEditPrinter(cmd) + if err != nil { + return err + } + + if len(args) == 0 { + usageString := "Required resource not specified." + return cmdutil.UsageError(cmd, usageString) + } + + resources := strings.Split(args[0], ",") + for i, r := range resources { + items := strings.Split(r, "/") + kind, err := util.GetSupportedResourceKind(items[0]) + if err != nil { + return err + } + + if kind == tapi.ResourceKindSnapshot { + return fmt.Errorf(`resource type "%v" doesn't support edit operation`, items[0]) + } + + items[0] = kind + resources[i] = strings.Join(items, "/") + } + args[0] = strings.Join(resources, ",") + + mapper, resourceMapper, r, _, err := getMapperAndResult(f, cmd, args) + if err != nil { + return err + } + + normalEditInfos, err := r.Infos() + if err != nil { + return err + } + + var ( + edit = editor.NewDefaultEditor() + ) + + restClonfig, _ := f.ClientConfig() + extClient := clientset.NewExtensionsForConfigOrDie(restClonfig) + + editFn := func(info *resource.Info, err error) error { + var ( + results = editResults{} + original = []byte{} + edited = []byte{} + file string + ) + + containsError := false + infos := normalEditInfos + for { + originalObj := infos[0].Object + objToEdit := originalObj + + buf := &bytes.Buffer{} + var w io.Writer = buf + + 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 { + 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 containsError { + if bytes.Equal(stripComments(editedDiff), stripComments(edited)) { + 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)) + + // Compare content without comments + if bytes.Equal(stripComments(original), stripComments(edited)) { + os.Remove(file) + fmt.Fprintln(errOut, "Edit cancelled, no changes made.") + return nil + } + + results = editResults{ + file: file, + } + + // parse the edited file + updates, err := resourceMapper.InfoForData(stripComments(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 + } + + containsError = false + + err = visitToPatch(extClient, originalObj, updates, mapper, resourceMapper, out, errOut, unversioned.GroupVersion{}, &results, file) + if err != nil { + return preservedFile(err, results.file, errOut) + } + + 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 + } + } + } + + return editFn(nil, nil) +} + +func visitToPatch(extClient clientset.ExtensionInterface, originalObj runtime.Object, updates *resource.Info, mapper meta.RESTMapper, resourceMapper *resource.Mapper, out, errOut io.Writer, defaultVersion unversioned.GroupVersion, results *editResults, file string) error { + patchVisitor := resource.NewFlattenListVisitor(updates, resourceMapper) + err := patchVisitor.Visit(func(info *resource.Info, incomingErr error) error { + + currOriginalObj, err := util.GetStructuredObject(originalObj) + if err != nil { + return err + } + + originalSerialization, err := runtime.Encode(clientset.ExtendedCodec, currOriginalObj) + if err != nil { + return err + } + + editedSerialization, err := encoder.Encode(info.Object) + if err != nil { + return err + } + + editedSerialization = stripComments(editedSerialization) + + 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, false, "skipped") + return nil + } + + preconditions := util.GetPreconditionFunc(currOriginalObj.GetObjectKind().GroupVersionKind().Kind) + + fmt.Println() + patch, err := strategicpatch.CreateTwoWayMergePatch(originalJS, editedJS, currOriginalObj, preconditions...) + if err != nil { + if strategicpatch.IsPreconditionFailed(err) { + return preconditionFailedError() + } + return err + } + + results.version = defaultVersion + h := resource.NewHelper(extClient.RESTClient(), info.Mapping) + patched, err := extClient.RESTClient().Patch(kapi.MergePatchType). + NamespaceIfScoped(info.Namespace, h.NamespaceScoped). + Resource(h.Resource). + Name(info.Name). + Body(patch). + Do(). + Get() + + 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, false, "edited") + return nil + }) + return err +} + +func getMapperAndResult(f cmdutil.Factory, cmd *cobra.Command, args []string) (meta.RESTMapper, *resource.Mapper, *resource.Result, string, error) { + cmdNamespace, _, err := f.DefaultNamespace() + if err != nil { + return nil, nil, nil, "", err + } + var mapper meta.RESTMapper + var typer runtime.ObjectTyper + mapper, typer, err = f.UnstructuredObject() + if err != nil { + return nil, nil, nil, "", err + } + + resourceMapper := &resource.Mapper{ + ObjectTyper: typer, + RESTMapper: mapper, + ClientMapper: resource.ClientMapperFunc(f.UnstructuredClientForMapping), + Decoder: runtime.UnstructuredJSONScheme, + } + + b := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), runtime.UnstructuredJSONScheme). + SelectorParam(cmdutil.GetFlagString(cmd, "selector")). + SelectAllParam(cmdutil.GetFlagBool(cmd, "all")). + ResourceTypeOrNameArgs(false, args...). + RequireObject(true). + Latest() + + r := b.NamespaceParam(cmdNamespace).DefaultNamespace(). + ContinueOnError(). + Flatten(). + Do() + + err = r.Err() + if err != nil { + return nil, nil, nil, "", err + } + return mapper, resourceMapper, r, cmdNamespace, err +} + +type editReason struct { + head string + other []string +} + +type editHeader struct { + reasons []editReason +} + +// writeTo outputs the current header information into a stream +func (h *editHeader) writeTo(w io.Writer) error { + fmt.Fprint(w, `# Please edit the object below. Lines beginning with a '#' will be ignored, +# and an empty file will abort the edit. If an error occurs while saving this file will be +# reopened with the relevant failures. +# +`) + for _, r := range h.reasons { + if len(r.other) > 0 { + fmt.Fprintf(w, "# %s:\n", r.head) + } else { + fmt.Fprintf(w, "# %s\n", r.head) + } + for _, o := range r.other { + fmt.Fprintf(w, "# * %s\n", o) + } + fmt.Fprintln(w, "#") + } + return nil +} + +func (h *editHeader) flush() { + h.reasons = []editReason{} +} + +type editPrinterOptions struct { + printer kubectl.ResourcePrinter + ext string + addHeader bool +} + +// editResults capture the result of an update +type editResults struct { + header editHeader + notfound int + edit []*resource.Info + file string + + version unversioned.GroupVersion +} + +func (r *editResults) addError(err error, info *resource.Info) string { + switch { + case errors.IsInvalid(err): + r.edit = append(r.edit, info) + reason := editReason{ + head: fmt.Sprintf("%s %q was not valid", info.Mapping.Resource, info.Name), + } + if err, ok := err.(errors.APIStatus); ok { + if details := err.Status().Details; details != nil { + for _, cause := range details.Causes { + reason.other = append(reason.other, fmt.Sprintf("%s: %s", cause.Field, cause.Message)) + } + } + } + r.header.reasons = append(r.header.reasons, reason) + return fmt.Sprintf("error: %s %q is invalid", info.Mapping.Resource, info.Name) + case errors.IsNotFound(err): + r.notfound++ + return fmt.Sprintf("error: %s %q could not be found on the server", info.Mapping.Resource, info.Name) + default: + return fmt.Sprintf("error: %s %q could not be patched: %v", info.Mapping.Resource, info.Name, err) + } +} + +func preservedFile(err error, path string, out io.Writer) error { + if len(path) > 0 { + if _, err := os.Stat(path); !os.IsNotExist(err) { + fmt.Fprintf(out, "A copy of your changes has been stored to %q\n", path) + } + } + return err +} + +func stripComments(file []byte) []byte { + stripped := file + stripped, err := yaml.ToJSON(stripped) + if err != nil { + stripped = manualStrip(file) + } + return stripped +} + +func manualStrip(file []byte) []byte { + stripped := []byte{} + lines := bytes.Split(file, []byte("\n")) + for i, line := range lines { + if bytes.HasPrefix(bytes.TrimSpace(line), []byte("#")) { + continue + } + stripped = append(stripped, line...) + if i < len(lines)-1 { + stripped = append(stripped, '\n') + } + } + return stripped +} + +func preconditionFailedError() error { + return fmt.Errorf("%s", `At least one of the following was changed: + apiVersion + kind + name + namespace + status +Or any unchangeable data was modified`) +} diff --git a/pkg/cmd/editor/editor.go b/pkg/cmd/editor/editor.go new file mode 100644 index 000000000..b133d6b28 --- /dev/null +++ b/pkg/cmd/editor/editor.go @@ -0,0 +1,35 @@ +package editor + +import ( + "os" + + "k8s.io/kubernetes/pkg/kubectl/cmd/util/editor" +) + +const defaultEditor = "nano" + +func NewDefaultEditor() editor.Editor { + var editorName string + editorName = os.Getenv("EDITOR") + if len(editorName) != 0 { + editorName = os.ExpandEnv(editorName) + return editor.Editor{ + Args: []string{editorName}, + Shell: false, + } + } + + editorName = os.Getenv("KUBEDB_EDITOR") + if len(editorName) != 0 { + editorName = os.ExpandEnv(editorName) + return editor.Editor{ + Args: []string{editorName}, + Shell: false, + } + } + + return editor.Editor{ + Args: []string{defaultEditor}, + Shell: false, + } +} diff --git a/pkg/cmd/encoder/encode.go b/pkg/cmd/encoder/encode.go new file mode 100644 index 000000000..1795377cc --- /dev/null +++ b/pkg/cmd/encoder/encode.go @@ -0,0 +1,21 @@ +package encoder + +import ( + "bytes" + "encoding/json" + "io" + + "k8s.io/kubernetes/pkg/runtime" +) + +func Encode(obj runtime.Object) ([]byte, error) { + buf := &bytes.Buffer{} + if err := encode(obj, buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func encode(obj runtime.Object, w io.Writer) error { + return json.NewEncoder(w).Encode(obj) +} diff --git a/pkg/cmd/printer/printer.go b/pkg/cmd/printer/printer.go index 26838242b..05a6d6b6e 100644 --- a/pkg/cmd/printer/printer.go +++ b/pkg/cmd/printer/printer.go @@ -39,3 +39,29 @@ func NewPrinter(cmd *cobra.Command) (kubectl.ResourcePrinter, error) { return nil, fmt.Errorf("output format %q not recognized", format) } } + +type editPrinterOptions struct { + Printer kubectl.ResourcePrinter + Ext string + AddHeader bool +} + +func NewEditPrinter(cmd *cobra.Command) (*editPrinterOptions, error) { + switch format := cmdutil.GetFlagString(cmd, "output"); format { + case "json": + return &editPrinterOptions{ + Printer: &kubectl.JSONPrinter{}, + Ext: ".json", + AddHeader: true, + }, nil + // If flag -o is not specified, use yaml as default + case "yaml", "": + return &editPrinterOptions{ + Printer: &kubectl.YAMLPrinter{}, + Ext: ".yaml", + AddHeader: true, + }, nil + default: + return nil, cmdutil.UsageError(cmd, "The flag 'output' must be one of yaml|json") + } +} diff --git a/pkg/cmd/util/flags.go b/pkg/cmd/util/flags.go index 693e9e931..bbadc3efd 100644 --- a/pkg/cmd/util/flags.go +++ b/pkg/cmd/util/flags.go @@ -28,6 +28,12 @@ func AddDescribeFlags(cmd *cobra.Command) { cmd.Flags().Bool("all-namespaces", false, "If present, list the requested object(s) across all namespaces.") } +func AddEditFlags(cmd *cobra.Command) { + cmd.Flags().Bool("all", false, "[-all] to select all the specified resources.") + cmd.Flags().StringP("selector", "l", "", "Selector (label query) to filter on.") + cmd.Flags().StringP("output", "o", "yaml", "Output format. One of: yaml|json.") +} + func AddFilenameOptionFlags(cmd *cobra.Command, options *resource.FilenameOptions) { cmd.Flags().StringSliceVarP(&options.Filenames, "filename", "f", options.Filenames, "Filename to use to create the resource") cmd.Flags().BoolVarP(&options.Recursive, "recursive", "R", options.Recursive, "Process the directory used in -f, --filename recursively.") diff --git a/pkg/cmd/util/resource.go b/pkg/cmd/util/resource.go index 82819662a..76380ba19 100644 --- a/pkg/cmd/util/resource.go +++ b/pkg/cmd/util/resource.go @@ -4,9 +4,13 @@ import ( "fmt" "strings" + "github.com/ghodss/yaml" tapi "github.com/k8sdb/apimachinery/api" + "github.com/k8sdb/kubedb/pkg/cmd/decoder" k8serr "k8s.io/kubernetes/pkg/api/errors" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/strategicpatch" ) func GetSupportedResourceKind(resource string) (string, error) { @@ -94,3 +98,78 @@ func ResourceShortFormFor(resource string) (string, bool) { } return alias, exists } + +func GetObjectData(obj runtime.Object) ([]byte, error) { + return yaml.Marshal(obj) +} + +func GetStructuredObject(obj runtime.Object) (runtime.Object, error) { + kind := obj.GetObjectKind().GroupVersionKind().Kind + data, err := GetObjectData(obj) + if err != nil { + return obj, err + } + return decoder.Decode(kind, data) +} + +func checkChainKeyUnchanged(key string, mapData map[string]interface{}) bool { + keys := strings.Split(key, ".") + val, ok := mapData[keys[0]] + if !ok || len(keys) == 1 { + return !ok + } + + newKey := strings.Join(keys[1:], ".") + return checkChainKeyUnchanged(newKey, val.(map[string]interface{})) +} + +func RequireChainKeyUnchanged(key string) strategicpatch.PreconditionFunc { + return func(patch interface{}) bool { + patchMap, ok := patch.(map[string]interface{}) + if !ok { + fmt.Println("Invalid data") + return true + } + check := checkChainKeyUnchanged(key, patchMap) + if !check { + fmt.Println(key, "was changed") + } + return check + } +} + +func GetPreconditionFunc(kind string) []strategicpatch.PreconditionFunc { + preconditions := []strategicpatch.PreconditionFunc{ + strategicpatch.RequireKeyUnchanged("apiVersion"), + strategicpatch.RequireKeyUnchanged("kind"), + strategicpatch.RequireMetadataKeyUnchanged("name"), + strategicpatch.RequireMetadataKeyUnchanged("namespace"), + strategicpatch.RequireKeyUnchanged("status"), + } + + switch kind { + case tapi.ResourceKindElastic: + preconditions = append( + preconditions, + RequireChainKeyUnchanged("spec.version"), + RequireChainKeyUnchanged("spec.storage"), + RequireChainKeyUnchanged("spec.nodeSelector"), + RequireChainKeyUnchanged("spec.init"), + ) + case tapi.ResourceKindPostgres: + preconditions = append( + preconditions, + RequireChainKeyUnchanged("spec.version"), + RequireChainKeyUnchanged("spec.storage"), + RequireChainKeyUnchanged("spec.databaseSecret"), + RequireChainKeyUnchanged("spec.nodeSelector"), + RequireChainKeyUnchanged("spec.init"), + ) + case tapi.ResourceKindDormantDatabase: + preconditions = append( + preconditions, + RequireChainKeyUnchanged("spec.origin"), + ) + } + return preconditions +} diff --git a/vendor/github.com/Sirupsen/logrus/doc.go b/vendor/github.com/Sirupsen/logrus/doc.go new file mode 100644 index 000000000..dddd5f877 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/doc.go @@ -0,0 +1,26 @@ +/* +Package logrus is a structured logger for Go, completely API compatible with the standard library logger. + + +The simplest way to use Logrus is simply the package-level exported logger: + + package main + + import ( + log "github.com/Sirupsen/logrus" + ) + + func main() { + log.WithFields(log.Fields{ + "animal": "walrus", + "number": 1, + "size": 10, + }).Info("A walrus appears") + } + +Output: + time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10 + +For a full guide visit https://github.com/Sirupsen/logrus +*/ +package logrus diff --git a/vendor/github.com/Sirupsen/logrus/entry.go b/vendor/github.com/Sirupsen/logrus/entry.go index e164eecb5..89e966e7b 100644 --- a/vendor/github.com/Sirupsen/logrus/entry.go +++ b/vendor/github.com/Sirupsen/logrus/entry.go @@ -8,6 +8,9 @@ import ( "time" ) +// Defines the key when adding errors using WithError. +var ErrorKey = "error" + // An entry is the final or intermediate Logrus logging entry. It contains all // the fields passed with WithField{,s}. It's finally logged when Debug, Info, // Warn, Error, Fatal or Panic is called on it. These objects can be reused and @@ -53,6 +56,11 @@ func (entry *Entry) String() (string, error) { return reader.String(), err } +// Add an error as single field (using the key defined in ErrorKey) to the Entry. +func (entry *Entry) WithError(err error) *Entry { + return entry.WithField(ErrorKey, err) +} + // Add a single field to the Entry. func (entry *Entry) WithField(key string, value interface{}) *Entry { return entry.WithFields(Fields{key: value}) @@ -60,7 +68,7 @@ func (entry *Entry) WithField(key string, value interface{}) *Entry { // Add a map of fields to the Entry. func (entry *Entry) WithFields(fields Fields) *Entry { - data := Fields{} + data := make(Fields, len(entry.Data)+len(fields)) for k, v := range entry.Data { data[k] = v } @@ -70,12 +78,14 @@ func (entry *Entry) WithFields(fields Fields) *Entry { return &Entry{Logger: entry.Logger, Data: data} } -func (entry *Entry) log(level Level, msg string) { +// This function is not declared with a pointer value because otherwise +// race conditions will occur when using multiple goroutines +func (entry Entry) log(level Level, msg string) { entry.Time = time.Now() entry.Level = level entry.Message = msg - if err := entry.Logger.Hooks.Fire(level, entry); err != nil { + if err := entry.Logger.Hooks.Fire(level, &entry); err != nil { entry.Logger.mu.Lock() fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err) entry.Logger.mu.Unlock() @@ -100,7 +110,7 @@ func (entry *Entry) log(level Level, msg string) { // panic() to use in Entry#Panic(), we avoid the allocation by checking // directly here. if level <= PanicLevel { - panic(entry) + panic(&entry) } } @@ -126,6 +136,10 @@ func (entry *Entry) Warn(args ...interface{}) { } } +func (entry *Entry) Warning(args ...interface{}) { + entry.Warn(args...) +} + func (entry *Entry) Error(args ...interface{}) { if entry.Logger.Level >= ErrorLevel { entry.log(ErrorLevel, fmt.Sprint(args...)) @@ -184,6 +198,7 @@ func (entry *Entry) Fatalf(format string, args ...interface{}) { if entry.Logger.Level >= FatalLevel { entry.Fatal(fmt.Sprintf(format, args...)) } + os.Exit(1) } func (entry *Entry) Panicf(format string, args ...interface{}) { @@ -230,6 +245,7 @@ func (entry *Entry) Fatalln(args ...interface{}) { if entry.Logger.Level >= FatalLevel { entry.Fatal(entry.sprintlnn(args...)) } + os.Exit(1) } func (entry *Entry) Panicln(args ...interface{}) { diff --git a/vendor/github.com/Sirupsen/logrus/exported.go b/vendor/github.com/Sirupsen/logrus/exported.go index d08712448..9a0120ac1 100644 --- a/vendor/github.com/Sirupsen/logrus/exported.go +++ b/vendor/github.com/Sirupsen/logrus/exported.go @@ -9,6 +9,10 @@ var ( std = New() ) +func StandardLogger() *Logger { + return std +} + // SetOutput sets the standard logger output. func SetOutput(out io.Writer) { std.mu.Lock() @@ -32,6 +36,8 @@ func SetLevel(level Level) { // GetLevel returns the standard logger level. func GetLevel() Level { + std.mu.Lock() + defer std.mu.Unlock() return std.Level } @@ -42,6 +48,11 @@ func AddHook(hook Hook) { std.Hooks.Add(hook) } +// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key. +func WithError(err error) *Entry { + return std.WithField(ErrorKey, err) +} + // WithField creates an entry from the standard logger and adds a field to // it. If you want multiple fields, use `WithFields`. // diff --git a/vendor/github.com/Sirupsen/logrus/formatter.go b/vendor/github.com/Sirupsen/logrus/formatter.go index 038ce9fd2..104d689f1 100644 --- a/vendor/github.com/Sirupsen/logrus/formatter.go +++ b/vendor/github.com/Sirupsen/logrus/formatter.go @@ -1,5 +1,9 @@ package logrus +import "time" + +const DefaultTimestampFormat = time.RFC3339 + // The Formatter interface is used to implement a custom Formatter. It takes an // `Entry`. It exposes all the fields, including the default ones: // diff --git a/vendor/github.com/Sirupsen/logrus/hooks.go b/vendor/github.com/Sirupsen/logrus/hooks.go index 0da2b3653..3f151cdc3 100644 --- a/vendor/github.com/Sirupsen/logrus/hooks.go +++ b/vendor/github.com/Sirupsen/logrus/hooks.go @@ -11,11 +11,11 @@ type Hook interface { } // Internal type for storing the hooks on a logger instance. -type levelHooks map[Level][]Hook +type LevelHooks map[Level][]Hook // Add a hook to an instance of logger. This is called with // `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface. -func (hooks levelHooks) Add(hook Hook) { +func (hooks LevelHooks) Add(hook Hook) { for _, level := range hook.Levels() { hooks[level] = append(hooks[level], hook) } @@ -23,7 +23,7 @@ func (hooks levelHooks) Add(hook Hook) { // Fire all the hooks for the passed level. Used by `entry.log` to fire // appropriate hooks for a log entry. -func (hooks levelHooks) Fire(level Level, entry *Entry) error { +func (hooks LevelHooks) Fire(level Level, entry *Entry) error { for _, hook := range hooks[level] { if err := hook.Fire(entry); err != nil { return err diff --git a/vendor/github.com/Sirupsen/logrus/json_formatter.go b/vendor/github.com/Sirupsen/logrus/json_formatter.go index b09227c2b..2ad6dc5cf 100644 --- a/vendor/github.com/Sirupsen/logrus/json_formatter.go +++ b/vendor/github.com/Sirupsen/logrus/json_formatter.go @@ -3,18 +3,33 @@ package logrus import ( "encoding/json" "fmt" - "time" ) -type JSONFormatter struct{} +type JSONFormatter struct { + // TimestampFormat sets the format used for marshaling timestamps. + TimestampFormat string +} func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { data := make(Fields, len(entry.Data)+3) for k, v := range entry.Data { - data[k] = v + switch v := v.(type) { + case error: + // Otherwise errors are ignored by `encoding/json` + // https://github.com/Sirupsen/logrus/issues/137 + data[k] = v.Error() + default: + data[k] = v + } } prefixFieldClashes(data) - data["time"] = entry.Time.Format(time.RFC3339) + + timestampFormat := f.TimestampFormat + if timestampFormat == "" { + timestampFormat = DefaultTimestampFormat + } + + data["time"] = entry.Time.Format(timestampFormat) data["msg"] = entry.Message data["level"] = entry.Level.String() diff --git a/vendor/github.com/Sirupsen/logrus/logger.go b/vendor/github.com/Sirupsen/logrus/logger.go index b392e547a..2fdb23176 100644 --- a/vendor/github.com/Sirupsen/logrus/logger.go +++ b/vendor/github.com/Sirupsen/logrus/logger.go @@ -8,13 +8,13 @@ import ( type Logger struct { // The logs are `io.Copy`'d to this in a mutex. It's common to set this to a - // file, or leave it default which is `os.Stdout`. You can also set this to + // file, or leave it default which is `os.Stderr`. You can also set this to // something more adventorous, such as logging to Kafka. Out io.Writer // Hooks for the logger instance. These allow firing events based on logging // levels and log entries. For example, to send errors to an error tracking // service, log to StatsD or dump the core on fatal errors. - Hooks levelHooks + Hooks LevelHooks // All log entries pass through the formatter before logged to Out. The // included formatters are `TextFormatter` and `JSONFormatter` for which // TextFormatter is the default. In development (when a TTY is attached) it @@ -37,23 +37,23 @@ type Logger struct { // var log = &Logger{ // Out: os.Stderr, // Formatter: new(JSONFormatter), -// Hooks: make(levelHooks), +// Hooks: make(LevelHooks), // Level: logrus.DebugLevel, // } // // It's recommended to make this a global instance called `log`. func New() *Logger { return &Logger{ - Out: os.Stdout, + Out: os.Stderr, Formatter: new(TextFormatter), - Hooks: make(levelHooks), + Hooks: make(LevelHooks), Level: InfoLevel, } } // Adds a field to the log entry, note that you it doesn't log until you call // Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry. -// Ff you want multiple fields, use `WithFields`. +// If you want multiple fields, use `WithFields`. func (logger *Logger) WithField(key string, value interface{}) *Entry { return NewEntry(logger).WithField(key, value) } @@ -64,12 +64,22 @@ func (logger *Logger) WithFields(fields Fields) *Entry { return NewEntry(logger).WithFields(fields) } +// Add an error as single field to the log entry. All it does is call +// `WithError` for the given `error`. +func (logger *Logger) WithError(err error) *Entry { + return NewEntry(logger).WithError(err) +} + func (logger *Logger) Debugf(format string, args ...interface{}) { - NewEntry(logger).Debugf(format, args...) + if logger.Level >= DebugLevel { + NewEntry(logger).Debugf(format, args...) + } } func (logger *Logger) Infof(format string, args ...interface{}) { - NewEntry(logger).Infof(format, args...) + if logger.Level >= InfoLevel { + NewEntry(logger).Infof(format, args...) + } } func (logger *Logger) Printf(format string, args ...interface{}) { @@ -77,31 +87,46 @@ func (logger *Logger) Printf(format string, args ...interface{}) { } func (logger *Logger) Warnf(format string, args ...interface{}) { - NewEntry(logger).Warnf(format, args...) + if logger.Level >= WarnLevel { + NewEntry(logger).Warnf(format, args...) + } } func (logger *Logger) Warningf(format string, args ...interface{}) { - NewEntry(logger).Warnf(format, args...) + if logger.Level >= WarnLevel { + NewEntry(logger).Warnf(format, args...) + } } func (logger *Logger) Errorf(format string, args ...interface{}) { - NewEntry(logger).Errorf(format, args...) + if logger.Level >= ErrorLevel { + NewEntry(logger).Errorf(format, args...) + } } func (logger *Logger) Fatalf(format string, args ...interface{}) { - NewEntry(logger).Fatalf(format, args...) + if logger.Level >= FatalLevel { + NewEntry(logger).Fatalf(format, args...) + } + os.Exit(1) } func (logger *Logger) Panicf(format string, args ...interface{}) { - NewEntry(logger).Panicf(format, args...) + if logger.Level >= PanicLevel { + NewEntry(logger).Panicf(format, args...) + } } func (logger *Logger) Debug(args ...interface{}) { - NewEntry(logger).Debug(args...) + if logger.Level >= DebugLevel { + NewEntry(logger).Debug(args...) + } } func (logger *Logger) Info(args ...interface{}) { - NewEntry(logger).Info(args...) + if logger.Level >= InfoLevel { + NewEntry(logger).Info(args...) + } } func (logger *Logger) Print(args ...interface{}) { @@ -109,31 +134,46 @@ func (logger *Logger) Print(args ...interface{}) { } func (logger *Logger) Warn(args ...interface{}) { - NewEntry(logger).Warn(args...) + if logger.Level >= WarnLevel { + NewEntry(logger).Warn(args...) + } } func (logger *Logger) Warning(args ...interface{}) { - NewEntry(logger).Warn(args...) + if logger.Level >= WarnLevel { + NewEntry(logger).Warn(args...) + } } func (logger *Logger) Error(args ...interface{}) { - NewEntry(logger).Error(args...) + if logger.Level >= ErrorLevel { + NewEntry(logger).Error(args...) + } } func (logger *Logger) Fatal(args ...interface{}) { - NewEntry(logger).Fatal(args...) + if logger.Level >= FatalLevel { + NewEntry(logger).Fatal(args...) + } + os.Exit(1) } func (logger *Logger) Panic(args ...interface{}) { - NewEntry(logger).Panic(args...) + if logger.Level >= PanicLevel { + NewEntry(logger).Panic(args...) + } } func (logger *Logger) Debugln(args ...interface{}) { - NewEntry(logger).Debugln(args...) + if logger.Level >= DebugLevel { + NewEntry(logger).Debugln(args...) + } } func (logger *Logger) Infoln(args ...interface{}) { - NewEntry(logger).Infoln(args...) + if logger.Level >= InfoLevel { + NewEntry(logger).Infoln(args...) + } } func (logger *Logger) Println(args ...interface{}) { @@ -141,21 +181,32 @@ func (logger *Logger) Println(args ...interface{}) { } func (logger *Logger) Warnln(args ...interface{}) { - NewEntry(logger).Warnln(args...) + if logger.Level >= WarnLevel { + NewEntry(logger).Warnln(args...) + } } func (logger *Logger) Warningln(args ...interface{}) { - NewEntry(logger).Warnln(args...) + if logger.Level >= WarnLevel { + NewEntry(logger).Warnln(args...) + } } func (logger *Logger) Errorln(args ...interface{}) { - NewEntry(logger).Errorln(args...) + if logger.Level >= ErrorLevel { + NewEntry(logger).Errorln(args...) + } } func (logger *Logger) Fatalln(args ...interface{}) { - NewEntry(logger).Fatalln(args...) + if logger.Level >= FatalLevel { + NewEntry(logger).Fatalln(args...) + } + os.Exit(1) } func (logger *Logger) Panicln(args ...interface{}) { - NewEntry(logger).Panicln(args...) + if logger.Level >= PanicLevel { + NewEntry(logger).Panicln(args...) + } } diff --git a/vendor/github.com/Sirupsen/logrus/logrus.go b/vendor/github.com/Sirupsen/logrus/logrus.go index 43ee12e90..e59669111 100644 --- a/vendor/github.com/Sirupsen/logrus/logrus.go +++ b/vendor/github.com/Sirupsen/logrus/logrus.go @@ -3,6 +3,7 @@ package logrus import ( "fmt" "log" + "strings" ) // Fields type, used to pass to `WithFields`. @@ -33,7 +34,7 @@ func (level Level) String() string { // ParseLevel takes a string level and returns the Logrus log level constant. func ParseLevel(lvl string) (Level, error) { - switch lvl { + switch strings.ToLower(lvl) { case "panic": return PanicLevel, nil case "fatal": @@ -52,6 +53,16 @@ func ParseLevel(lvl string) (Level, error) { return l, fmt.Errorf("not a valid logrus Level: %q", lvl) } +// A constant exposing all logging levels +var AllLevels = []Level{ + PanicLevel, + FatalLevel, + ErrorLevel, + WarnLevel, + InfoLevel, + DebugLevel, +} + // These are the different logging levels. You can set the logging level to log // on your instance of logger, obtained with `logrus.New()`. const ( @@ -74,7 +85,11 @@ const ( ) // Won't compile if StdLogger can't be realized by a log.Logger -var _ StdLogger = &log.Logger{} +var ( + _ StdLogger = &log.Logger{} + _ StdLogger = &Entry{} + _ StdLogger = &Logger{} +) // StdLogger is what your logrus-enabled library should take, that way // it'll accept a stdlib logger and a logrus logger. There's no standard @@ -92,3 +107,37 @@ type StdLogger interface { Panicf(string, ...interface{}) Panicln(...interface{}) } + +// The FieldLogger interface generalizes the Entry and Logger types +type FieldLogger interface { + WithField(key string, value interface{}) *Entry + WithFields(fields Fields) *Entry + WithError(err error) *Entry + + Debugf(format string, args ...interface{}) + Infof(format string, args ...interface{}) + Printf(format string, args ...interface{}) + Warnf(format string, args ...interface{}) + Warningf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) + Panicf(format string, args ...interface{}) + + Debug(args ...interface{}) + Info(args ...interface{}) + Print(args ...interface{}) + Warn(args ...interface{}) + Warning(args ...interface{}) + Error(args ...interface{}) + Fatal(args ...interface{}) + Panic(args ...interface{}) + + Debugln(args ...interface{}) + Infoln(args ...interface{}) + Println(args ...interface{}) + Warnln(args ...interface{}) + Warningln(args ...interface{}) + Errorln(args ...interface{}) + Fatalln(args ...interface{}) + Panicln(args ...interface{}) +} diff --git a/vendor/github.com/Sirupsen/logrus/terminal_bsd.go b/vendor/github.com/Sirupsen/logrus/terminal_bsd.go new file mode 100644 index 000000000..71f8d67a5 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/terminal_bsd.go @@ -0,0 +1,9 @@ +// +build darwin freebsd openbsd netbsd dragonfly + +package logrus + +import "syscall" + +const ioctlReadTermios = syscall.TIOCGETA + +type Termios syscall.Termios diff --git a/vendor/github.com/Sirupsen/logrus/terminal_darwin.go b/vendor/github.com/Sirupsen/logrus/terminal_darwin.go deleted file mode 100644 index 8fe02a4ae..000000000 --- a/vendor/github.com/Sirupsen/logrus/terminal_darwin.go +++ /dev/null @@ -1,12 +0,0 @@ -// Based on ssh/terminal: -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package logrus - -import "syscall" - -const ioctlReadTermios = syscall.TIOCGETA - -type Termios syscall.Termios diff --git a/vendor/github.com/Sirupsen/logrus/terminal_freebsd.go b/vendor/github.com/Sirupsen/logrus/terminal_freebsd.go deleted file mode 100644 index 0428ee5d5..000000000 --- a/vendor/github.com/Sirupsen/logrus/terminal_freebsd.go +++ /dev/null @@ -1,20 +0,0 @@ -/* - Go 1.2 doesn't include Termios for FreeBSD. This should be added in 1.3 and this could be merged with terminal_darwin. -*/ -package logrus - -import ( - "syscall" -) - -const ioctlReadTermios = syscall.TIOCGETA - -type Termios struct { - Iflag uint32 - Oflag uint32 - Cflag uint32 - Lflag uint32 - Cc [20]uint8 - Ispeed uint32 - Ospeed uint32 -} diff --git a/vendor/github.com/Sirupsen/logrus/terminal_notwindows.go b/vendor/github.com/Sirupsen/logrus/terminal_notwindows.go index 276447bd5..b343b3a37 100644 --- a/vendor/github.com/Sirupsen/logrus/terminal_notwindows.go +++ b/vendor/github.com/Sirupsen/logrus/terminal_notwindows.go @@ -3,7 +3,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build linux,!appengine darwin freebsd +// +build linux darwin freebsd openbsd netbsd dragonfly package logrus @@ -12,9 +12,9 @@ import ( "unsafe" ) -// IsTerminal returns true if the given file descriptor is a terminal. +// IsTerminal returns true if stderr's file descriptor is a terminal. func IsTerminal() bool { - fd := syscall.Stdout + fd := syscall.Stderr var termios Termios _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) return err == 0 diff --git a/vendor/github.com/Sirupsen/logrus/terminal_solaris.go b/vendor/github.com/Sirupsen/logrus/terminal_solaris.go new file mode 100644 index 000000000..3e70bf7bf --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/terminal_solaris.go @@ -0,0 +1,15 @@ +// +build solaris + +package logrus + +import ( + "os" + + "golang.org/x/sys/unix" +) + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal() bool { + _, err := unix.IoctlGetTermios(int(os.Stdout.Fd()), unix.TCGETA) + return err == nil +} diff --git a/vendor/github.com/Sirupsen/logrus/terminal_windows.go b/vendor/github.com/Sirupsen/logrus/terminal_windows.go index 2e09f6f7e..0146845d1 100644 --- a/vendor/github.com/Sirupsen/logrus/terminal_windows.go +++ b/vendor/github.com/Sirupsen/logrus/terminal_windows.go @@ -18,9 +18,9 @@ var ( procGetConsoleMode = kernel32.NewProc("GetConsoleMode") ) -// IsTerminal returns true if the given file descriptor is a terminal. +// IsTerminal returns true if stderr's file descriptor is a terminal. func IsTerminal() bool { - fd := syscall.Stdout + fd := syscall.Stderr var st uint32 r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) return r != 0 && e == 0 diff --git a/vendor/github.com/Sirupsen/logrus/text_formatter.go b/vendor/github.com/Sirupsen/logrus/text_formatter.go index 78e788935..06ef20233 100644 --- a/vendor/github.com/Sirupsen/logrus/text_formatter.go +++ b/vendor/github.com/Sirupsen/logrus/text_formatter.go @@ -3,7 +3,7 @@ package logrus import ( "bytes" "fmt" - "regexp" + "runtime" "sort" "strings" "time" @@ -15,12 +15,12 @@ const ( green = 32 yellow = 33 blue = 34 + gray = 37 ) var ( baseTimestamp time.Time isTerminal bool - noQuoteNeeded *regexp.Regexp ) func init() { @@ -34,35 +34,59 @@ func miniTS() int { type TextFormatter struct { // Set to true to bypass checking for a TTY before outputting colors. - ForceColors bool + ForceColors bool + + // Force disabling colors. DisableColors bool - // Set to true to disable timestamp logging (useful when the output - // is redirected to a logging system already adding a timestamp) + + // Disable timestamp logging. useful when output is redirected to logging + // system that already adds timestamps. DisableTimestamp bool + + // Enable logging the full timestamp when a TTY is attached instead of just + // the time passed since beginning of execution. + FullTimestamp bool + + // TimestampFormat to use for display when a full timestamp is printed + TimestampFormat string + + // The fields are sorted by default for a consistent output. For applications + // that log extremely frequently and don't use the JSON formatter this may not + // be desired. + DisableSorting bool } func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { - - var keys []string + var keys []string = make([]string, 0, len(entry.Data)) for k := range entry.Data { keys = append(keys, k) } - sort.Strings(keys) + + if !f.DisableSorting { + sort.Strings(keys) + } b := &bytes.Buffer{} prefixFieldClashes(entry.Data) - isColored := (f.ForceColors || isTerminal) && !f.DisableColors + isColorTerminal := isTerminal && (runtime.GOOS != "windows") + isColored := (f.ForceColors || isColorTerminal) && !f.DisableColors + timestampFormat := f.TimestampFormat + if timestampFormat == "" { + timestampFormat = DefaultTimestampFormat + } if isColored { - printColored(b, entry, keys) + f.printColored(b, entry, keys, timestampFormat) } else { if !f.DisableTimestamp { - f.appendKeyValue(b, "time", entry.Time.Format(time.RFC3339)) + f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat)) } f.appendKeyValue(b, "level", entry.Level.String()) - f.appendKeyValue(b, "msg", entry.Message) + if entry.Message != "" { + f.appendKeyValue(b, "msg", entry.Message) + } for _, key := range keys { f.appendKeyValue(b, key, entry.Data[key]) } @@ -72,9 +96,11 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { return b.Bytes(), nil } -func printColored(b *bytes.Buffer, entry *Entry, keys []string) { +func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) { var levelColor int switch entry.Level { + case DebugLevel: + levelColor = gray case WarnLevel: levelColor = yellow case ErrorLevel, FatalLevel, PanicLevel: @@ -85,10 +111,14 @@ func printColored(b *bytes.Buffer, entry *Entry, keys []string) { levelText := strings.ToUpper(entry.Level.String())[0:4] - fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message) + if !f.FullTimestamp { + fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message) + } else { + fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message) + } for _, k := range keys { v := entry.Data[k] - fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%v", levelColor, k, v) + fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%+v", levelColor, k, v) } } @@ -96,7 +126,7 @@ func needsQuoting(text string) bool { for _, ch := range text { if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch < '9') || + (ch >= '0' && ch <= '9') || ch == '-' || ch == '.') { return false } @@ -104,21 +134,28 @@ func needsQuoting(text string) bool { return true } -func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key, value interface{}) { - switch value.(type) { +func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) { + + b.WriteString(key) + b.WriteByte('=') + + switch value := value.(type) { case string: - if needsQuoting(value.(string)) { - fmt.Fprintf(b, "%v=%s ", key, value) + if needsQuoting(value) { + b.WriteString(value) } else { - fmt.Fprintf(b, "%v=%q ", key, value) + fmt.Fprintf(b, "%q", value) } case error: - if needsQuoting(value.(error).Error()) { - fmt.Fprintf(b, "%v=%s ", key, value) + errmsg := value.Error() + if needsQuoting(errmsg) { + b.WriteString(errmsg) } else { - fmt.Fprintf(b, "%v=%q ", key, value) + fmt.Fprintf(b, "%q", value) } default: - fmt.Fprintf(b, "%v=%v ", key, value) + fmt.Fprint(b, value) } + + b.WriteByte(' ') } diff --git a/vendor/github.com/Sirupsen/logrus/writer.go b/vendor/github.com/Sirupsen/logrus/writer.go new file mode 100644 index 000000000..1e30b1c75 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/writer.go @@ -0,0 +1,31 @@ +package logrus + +import ( + "bufio" + "io" + "runtime" +) + +func (logger *Logger) Writer() *io.PipeWriter { + reader, writer := io.Pipe() + + go logger.writerScanner(reader) + runtime.SetFinalizer(writer, writerFinalizer) + + return writer +} + +func (logger *Logger) writerScanner(reader *io.PipeReader) { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + logger.Print(scanner.Text()) + } + if err := scanner.Err(); err != nil { + logger.Errorf("Error while reading from Writer: %s", err) + } + reader.Close() +} + +func writerFinalizer(writer *io.PipeWriter) { + writer.Close() +} diff --git a/vendor/k8s.io/kubernetes/pkg/kubectl/cmd/util/editor/editor.go b/vendor/k8s.io/kubernetes/pkg/kubectl/cmd/util/editor/editor.go new file mode 100644 index 000000000..9f53ee7c7 --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/kubectl/cmd/util/editor/editor.go @@ -0,0 +1,192 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package editor + +import ( + "fmt" + "io" + "io/ioutil" + "math/rand" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/golang/glog" + + "k8s.io/kubernetes/pkg/util/term" +) + +const ( + // sorry, blame Git + // TODO: on Windows rely on 'start' to launch the editor associated + // with the given file type. If we can't because of the need of + // blocking, use a script with 'ftype' and 'assoc' to detect it. + defaultEditor = "vi" + defaultShell = "/bin/bash" + windowsEditor = "notepad" + windowsShell = "cmd" +) + +type Editor struct { + Args []string + Shell bool +} + +// NewDefaultEditor creates a struct Editor that uses the OS environment to +// locate the editor program, looking at EDITOR environment variable to find +// the proper command line. If the provided editor has no spaces, or no quotes, +// it is treated as a bare command to be loaded. Otherwise, the string will +// be passed to the user's shell for execution. +func NewDefaultEditor(envs []string) Editor { + args, shell := defaultEnvEditor(envs) + return Editor{ + Args: args, + Shell: shell, + } +} + +func defaultEnvShell() []string { + shell := os.Getenv("SHELL") + if len(shell) == 0 { + shell = platformize(defaultShell, windowsShell) + } + flag := "-c" + if shell == windowsShell { + flag = "/C" + } + return []string{shell, flag} +} + +func defaultEnvEditor(envs []string) ([]string, bool) { + var editor string + for _, env := range envs { + if len(env) > 0 { + editor = os.Getenv(env) + } + if len(editor) > 0 { + break + } + } + if len(editor) == 0 { + editor = platformize(defaultEditor, windowsEditor) + } + if !strings.Contains(editor, " ") { + return []string{editor}, false + } + if !strings.ContainsAny(editor, "\"'\\") { + return strings.Split(editor, " "), false + } + // rather than parse the shell arguments ourselves, punt to the shell + shell := defaultEnvShell() + return append(shell, editor), true +} + +func (e Editor) args(path string) []string { + args := make([]string, len(e.Args)) + copy(args, e.Args) + if e.Shell { + last := args[len(args)-1] + args[len(args)-1] = fmt.Sprintf("%s %q", last, path) + } else { + args = append(args, path) + } + return args +} + +// Launch opens the described or returns an error. The TTY will be protected, and +// SIGQUIT, SIGTERM, and SIGINT will all be trapped. +func (e Editor) Launch(path string) error { + if len(e.Args) == 0 { + return fmt.Errorf("no editor defined, can't open %s", path) + } + abs, err := filepath.Abs(path) + if err != nil { + return err + } + args := e.args(abs) + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + glog.V(5).Infof("Opening file with editor %v", args) + if err := (term.TTY{In: os.Stdin, TryDev: true}).Safe(cmd.Run); err != nil { + if err, ok := err.(*exec.Error); ok { + if err.Err == exec.ErrNotFound { + return fmt.Errorf("unable to launch the editor %q", strings.Join(e.Args, " ")) + } + } + return fmt.Errorf("there was a problem with the editor %q", strings.Join(e.Args, " ")) + } + return nil +} + +// LaunchTempFile reads the provided stream into a temporary file in the given directory +// and file prefix, and then invokes Launch with the path of that file. It will return +// the contents of the file after launch, any errors that occur, and the path of the +// temporary file so the caller can clean it up as needed. +func (e Editor) LaunchTempFile(prefix, suffix string, r io.Reader) ([]byte, string, error) { + f, err := tempFile(prefix, suffix) + if err != nil { + return nil, "", err + } + defer f.Close() + path := f.Name() + if _, err := io.Copy(f, r); err != nil { + os.Remove(path) + return nil, path, err + } + // This file descriptor needs to close so the next process (Launch) can claim it. + f.Close() + if err := e.Launch(path); err != nil { + return nil, path, err + } + bytes, err := ioutil.ReadFile(path) + return bytes, path, err +} + +func tempFile(prefix, suffix string) (f *os.File, err error) { + dir := os.TempDir() + + for i := 0; i < 10000; i++ { + name := filepath.Join(dir, prefix+randSeq(5)+suffix) + f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) + if os.IsExist(err) { + continue + } + break + } + return +} + +var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789") + +func randSeq(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + +func platformize(linux, windows string) string { + if runtime.GOOS == "windows" { + return windows + } + return linux +}