From 69065e38f8278cfc3d761af6e974ddbb5d395114 Mon Sep 17 00:00:00 2001 From: Michael Persson Date: Tue, 16 Feb 2016 14:51:24 +0100 Subject: [PATCH] Added file suffix for tmp file, to support syntax highlighting. Updated vendoring --- .../mickep76/etcdtool/command/edit_command.go | 17 +- src/github.com/mickep76/etcdtool/version.go | 2 +- vendor/manifest | 22 +-- vendor/src/github.com/BurntSushi/toml/lex.go | 2 - .../src/github.com/codegangsta/cli/README.md | 17 +- vendor/src/github.com/codegangsta/cli/app.go | 50 +++-- .../github.com/codegangsta/cli/app_test.go | 89 ++++++++- .../github.com/codegangsta/cli/appveyor.yml | 16 ++ .../src/github.com/codegangsta/cli/command.go | 60 ++++-- .../codegangsta/cli/command_test.go | 52 ++++++ .../src/github.com/codegangsta/cli/context.go | 6 +- vendor/src/github.com/codegangsta/cli/flag.go | 61 +++--- .../github.com/codegangsta/cli/flag_test.go | 57 ++++-- vendor/src/github.com/codegangsta/cli/help.go | 6 +- .../src/golang.org/x/net/context/context.go | 2 +- .../golang.org/x/net/context/context_test.go | 4 +- .../x/net/context/ctxhttp/cancelreq.go | 19 ++ .../x/net/context/ctxhttp/cancelreq_go14.go | 23 +++ .../x/net/context/ctxhttp/ctxhttp.go | 140 ++++++++++++++ .../x/net/context/ctxhttp/ctxhttp_test.go | 176 ++++++++++++++++++ .../github.com/coreos/etcd/client/client.go | 97 +++++++++- .../coreos/etcd/client/client_test.go | 16 +- .../src/github.com/coreos/etcd/client/keys.go | 15 +- .../coreos/etcd/client/keys_test.go | 12 ++ .../github.com/coreos/etcd/client/members.go | 32 ++++ .../coreos/etcd/client/members_test.go | 77 ++++++++ .../src/github.com/coreos/etcd/client/srv.go | 6 +- .../github.com/coreos/etcd/client/srv_test.go | 4 +- .../src/github.com/coreos/etcd/client/util.go | 23 +++ .../etcd/pkg/transport/keepalive_listener.go | 30 +-- .../pkg/transport/keepalive_listener_test.go | 18 +- .../coreos/etcd/pkg/transport/listener.go | 8 +- .../etcd/pkg/transport/listener_test.go | 10 +- .../etcd/pkg/transport/timeout_transport.go | 11 +- .../coreos/etcd/pkg/types/urlsmap.go | 8 +- .../github.com/mickep76/etcdmap/etcdmap.go | 17 +- .../mickep76/iodatafmt/iodatafmt.go | 2 +- .../x/net/context/ctxhttp/cancelreq.go | 1 + .../x/net/context/ctxhttp/ctxhttp.go | 63 ++++++- .../x/net/context/ctxhttp/ctxhttp_test.go | 104 +++++++++++ 40 files changed, 1219 insertions(+), 156 deletions(-) create mode 100644 vendor/src/github.com/codegangsta/cli/appveyor.yml create mode 100644 vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/cancelreq.go create mode 100644 vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/cancelreq_go14.go create mode 100644 vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/ctxhttp.go create mode 100644 vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/ctxhttp_test.go create mode 100644 vendor/src/github.com/coreos/etcd/client/util.go diff --git a/src/github.com/mickep76/etcdtool/command/edit_command.go b/src/github.com/mickep76/etcdtool/command/edit_command.go index 12c575a..ea5d200 100644 --- a/src/github.com/mickep76/etcdtool/command/edit_command.go +++ b/src/github.com/mickep76/etcdtool/command/edit_command.go @@ -22,7 +22,7 @@ func NewEditCommand() cli.Command { cli.BoolFlag{Name: "validate, v", EnvVar: "ETCDTOOL_VALIDATE", Usage: "Validate data before import"}, cli.StringFlag{Name: "format, f", Value: "JSON", EnvVar: "ETCDTOOL_FORMAT", Usage: "Data serialization format YAML, TOML or JSON"}, cli.StringFlag{Name: "editor, e", Value: "vim", Usage: "Editor", EnvVar: "EDITOR"}, - cli.StringFlag{Name: "tmp-file, t", Value: ".etcdtool.swp", Usage: "Temporary file"}, + cli.StringFlag{Name: "tmp-file, t", Value: ".etcdtool", Usage: "Temporary file"}, }, Action: func(c *cli.Context) { editCommandFunc(c) @@ -72,33 +72,36 @@ func editCommandFunc(c *cli.Context) { fatal(err.Error()) } + // Temporary file append file type to support syntax highlighting + tmpfile := c.String("tmp-file") + "." + strings.ToLower(c.String("format")) + // Export to file. - exportFunc(dir, sort, c.String("tmp-file"), f, c, ki) + exportFunc(dir, sort, tmpfile, f, c, ki) // Get modified time stamp. - before, err := os.Stat(c.String("tmp-file")) + before, err := os.Stat(tmpfile) if err != nil { fatal(err.Error()) } // Edit file. - editFile(c.String("editor"), c.String("tmp-file")) + editFile(c.String("editor"), tmpfile) // Check modified time stamp. - after, err := os.Stat(c.String("tmp-file")) + after, err := os.Stat(tmpfile) if err != nil { fatal(err.Error()) } // Import from file if it has changed. if before.ModTime() != after.ModTime() { - importFunc(dir, c.String("tmp-file"), f, c.Bool("replace"), c.Bool("yes"), e, c, ki) + importFunc(dir, tmpfile, f, c.Bool("replace"), c.Bool("yes"), e, c, ki) } else { fmt.Printf("File wasn't modified, skipping import\n") } // Unlink file. - if err := os.Remove(c.String("tmp-file")); err != nil { + if err := os.Remove(tmpfile); err != nil { fatal(err.Error()) } } diff --git a/src/github.com/mickep76/etcdtool/version.go b/src/github.com/mickep76/etcdtool/version.go index 1fb8620..0195553 100644 --- a/src/github.com/mickep76/etcdtool/version.go +++ b/src/github.com/mickep76/etcdtool/version.go @@ -1,4 +1,4 @@ package main // Version for app. -const Version = "2.9" +const Version = "3.0" diff --git a/vendor/manifest b/vendor/manifest index cfd06dd..5140772 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -4,7 +4,7 @@ { "importpath": "github.com/BurntSushi/toml", "repository": "https://github.com/BurntSushi/toml", - "revision": "056c9bc7be7190eaa7715723883caffa5f8fa3e4", + "revision": "5c4df71dfe9ac89ef6287afc05e4c1b16ae65a1e", "branch": "master" }, { @@ -16,61 +16,61 @@ { "importpath": "github.com/codegangsta/cli", "repository": "https://github.com/codegangsta/cli", - "revision": "b5232bb2934f606f9f27a1305f1eea224e8e8b88", + "revision": "0ab42fd482c27cf2c95e7794ad3bb2082c2ab2d7", "branch": "master" }, { "importpath": "github.com/coreos/etcd/Godeps/_workspace/src/github.com/ugorji/go/codec", "repository": "https://github.com/coreos/etcd", - "revision": "9b0b15c9be660695082d683bee6d0b5ea59904c4", + "revision": "30e4d7d6aa97040c5040ffd2344ac0d6effe3d23", "branch": "master", "path": "/Godeps/_workspace/src/github.com/ugorji/go/codec" }, { "importpath": "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context", "repository": "https://github.com/coreos/etcd", - "revision": "9b0b15c9be660695082d683bee6d0b5ea59904c4", + "revision": "30e4d7d6aa97040c5040ffd2344ac0d6effe3d23", "branch": "master", "path": "/Godeps/_workspace/src/golang.org/x/net/context" }, { "importpath": "github.com/coreos/etcd/client", "repository": "https://github.com/coreos/etcd", - "revision": "9b0b15c9be660695082d683bee6d0b5ea59904c4", + "revision": "30e4d7d6aa97040c5040ffd2344ac0d6effe3d23", "branch": "master", "path": "/client" }, { "importpath": "github.com/coreos/etcd/pkg/pathutil", "repository": "https://github.com/coreos/etcd", - "revision": "9b0b15c9be660695082d683bee6d0b5ea59904c4", + "revision": "30e4d7d6aa97040c5040ffd2344ac0d6effe3d23", "branch": "master", "path": "/pkg/pathutil" }, { "importpath": "github.com/coreos/etcd/pkg/transport", "repository": "https://github.com/coreos/etcd", - "revision": "9b0b15c9be660695082d683bee6d0b5ea59904c4", + "revision": "30e4d7d6aa97040c5040ffd2344ac0d6effe3d23", "branch": "master", "path": "/pkg/transport" }, { "importpath": "github.com/coreos/etcd/pkg/types", "repository": "https://github.com/coreos/etcd", - "revision": "9b0b15c9be660695082d683bee6d0b5ea59904c4", + "revision": "30e4d7d6aa97040c5040ffd2344ac0d6effe3d23", "branch": "master", "path": "/pkg/types" }, { "importpath": "github.com/mickep76/etcdmap", "repository": "https://github.com/mickep76/etcdmap", - "revision": "7128b59ea24bc4ad44b1eb0e1c9998459c01fc80", + "revision": "7251bb03b7f4fe0199e096014bb5b61db0348e91", "branch": "master" }, { "importpath": "github.com/mickep76/iodatafmt", "repository": "https://github.com/mickep76/iodatafmt", - "revision": "b597e16ba86d5b61a9fc26e0d7cfa92682009e1d", + "revision": "b8a6f28eabf7e2ece95de10eadbbe88c2e065081", "branch": "master" }, { @@ -94,7 +94,7 @@ { "importpath": "golang.org/x/net/context", "repository": "https://go.googlesource.com/net", - "revision": "d28a91ad269180318493156412990f060d721258", + "revision": "cbbbe2bc0f2efdd2afb318d93f1eadb19350e4a3", "branch": "master", "path": "/context" }, diff --git a/vendor/src/github.com/BurntSushi/toml/lex.go b/vendor/src/github.com/BurntSushi/toml/lex.go index 2191228..08dce77 100644 --- a/vendor/src/github.com/BurntSushi/toml/lex.go +++ b/vendor/src/github.com/BurntSushi/toml/lex.go @@ -272,8 +272,6 @@ func lexTableNameStart(lx *lexer) stateFn { lx.ignore() lx.push(lexTableNameEnd) return lexValue // reuse string lexing - case isWhitespace(r): - return lexTableNameStart default: return lexBareTableName } diff --git a/vendor/src/github.com/codegangsta/cli/README.md b/vendor/src/github.com/codegangsta/cli/README.md index 26a1838..ae0a4ca 100644 --- a/vendor/src/github.com/codegangsta/cli/README.md +++ b/vendor/src/github.com/codegangsta/cli/README.md @@ -1,16 +1,19 @@ [![Coverage](http://gocover.io/_badge/github.com/codegangsta/cli?0)](http://gocover.io/github.com/codegangsta/cli) -[![Build Status](https://travis-ci.org/codegangsta/cli.png?branch=master)](https://travis-ci.org/codegangsta/cli) +[![Build Status](https://travis-ci.org/codegangsta/cli.svg?branch=master)](https://travis-ci.org/codegangsta/cli) [![GoDoc](https://godoc.org/github.com/codegangsta/cli?status.svg)](https://godoc.org/github.com/codegangsta/cli) # cli.go + `cli.go` is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. ## Overview + Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app. **This is where `cli.go` comes into play.** `cli.go` makes command line programming fun, organized, and expressive! ## Installation + Make sure you have a working Go environment (go 1.1+ is *required*). [See the install instructions](http://golang.org/doc/install.html). To install `cli.go`, simply run: @@ -24,6 +27,7 @@ export PATH=$PATH:$GOPATH/bin ``` ## Getting Started + One of the philosophies behind `cli.go` is that an API should be playful and full of discovery. So a `cli.go` app can be as little as one line of code in `main()`. ``` go @@ -123,6 +127,7 @@ GLOBAL OPTIONS ``` ### Arguments + You can lookup arguments by calling the `Args` function on `cli.Context`. ``` go @@ -134,7 +139,9 @@ app.Action = func(c *cli.Context) { ``` ### Flags + Setting and querying flags is simple. + ``` go ... app.Flags = []cli.Flag { @@ -159,6 +166,7 @@ app.Action = func(c *cli.Context) { ``` You can also set a destination variable for a flag, to which the content will be scanned. + ``` go ... var language string @@ -233,6 +241,7 @@ app.Flags = []cli.Flag { ### Subcommands Subcommands can be defined for a more git-like command line app. + ```go ... app.Commands = []cli.Command{ @@ -283,6 +292,7 @@ You can enable completion commands by setting the `EnableBashCompletion` flag on the `App` object. By default, this setting will only auto-complete to show an app's subcommands, but you can write your own completion methods for the App or its subcommands. + ```go ... var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"} @@ -325,8 +335,8 @@ automatically install it there if you are distributing a package). Don't forget to source the file to make it active in the current shell. ``` - sudo cp src/bash_autocomplete /etc/bash_completion.d/ - source /etc/bash_completion.d/ +sudo cp src/bash_autocomplete /etc/bash_completion.d/ +source /etc/bash_completion.d/ ``` Alternatively, you can just document that users should source the generic @@ -334,6 +344,7 @@ Alternatively, you can just document that users should source the generic to the name of their program (as above). ## Contribution Guidelines + Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch. If you have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together. diff --git a/vendor/src/github.com/codegangsta/cli/app.go b/vendor/src/github.com/codegangsta/cli/app.go index 0805fd6..1ea3fd0 100644 --- a/vendor/src/github.com/codegangsta/cli/app.go +++ b/vendor/src/github.com/codegangsta/cli/app.go @@ -5,18 +5,21 @@ import ( "io" "io/ioutil" "os" + "path" "time" ) -// App is the main structure of a cli application. It is recomended that +// App is the main structure of a cli application. It is recommended that // an app be created with the cli.NewApp() function type App struct { - // The name of the program. Defaults to os.Args[0] + // The name of the program. Defaults to path.Base(os.Args[0]) Name string // Full name of command for help, defaults to Name HelpName string // Description of the program. Usage string + // Text to override the USAGE section of help + UsageText string // Description of the program argument format. ArgsUsage string // Version of the program @@ -43,6 +46,10 @@ type App struct { Action func(context *Context) // Execute this function if the proper command cannot be found CommandNotFound func(context *Context, command string) + // Execute this function, if an usage error occurs. This is useful for displaying customized usage error messages. + // This function is able to replace the original error messages. + // If this function is not set, the "Incorrect usage" is displayed and the execution is interrupted. + OnUsageError func(context *Context, err error, isSubcommand bool) error // Compilation date Compiled time.Time // List of all authors who contributed @@ -70,9 +77,10 @@ func compileTime() time.Time { // Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action. func NewApp() *App { return &App{ - Name: os.Args[0], - HelpName: os.Args[0], + Name: path.Base(os.Args[0]), + HelpName: path.Base(os.Args[0]), Usage: "A new cli application", + UsageText: "", Version: "0.0.0", BashComplete: DefaultAppComplete, Action: helpCommand.Action, @@ -118,23 +126,26 @@ func (a *App) Run(arguments []string) (err error) { set.SetOutput(ioutil.Discard) err = set.Parse(arguments[1:]) nerr := normalizeFlags(a.Flags, set) + context := NewContext(a, set, nil) if nerr != nil { fmt.Fprintln(a.Writer, nerr) - context := NewContext(a, set, nil) ShowAppHelp(context) return nerr } - context := NewContext(a, set, nil) if checkCompletions(context) { return nil } if err != nil { - fmt.Fprintln(a.Writer, "Incorrect Usage.") - fmt.Fprintln(a.Writer) - ShowAppHelp(context) - return err + if a.OnUsageError != nil { + err := a.OnUsageError(context, err, false) + return err + } else { + fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") + ShowAppHelp(context) + return err + } } if !a.HideHelp && checkHelp(context) { @@ -149,8 +160,7 @@ func (a *App) Run(arguments []string) (err error) { if a.After != nil { defer func() { - afterErr := a.After(context) - if afterErr != nil { + if afterErr := a.After(context); afterErr != nil { if err != nil { err = NewMultiError(err, afterErr) } else { @@ -161,8 +171,10 @@ func (a *App) Run(arguments []string) (err error) { } if a.Before != nil { - err := a.Before(context) + err = a.Before(context) if err != nil { + fmt.Fprintf(a.Writer, "%v\n\n", err) + ShowAppHelp(context) return err } } @@ -238,10 +250,14 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } if err != nil { - fmt.Fprintln(a.Writer, "Incorrect Usage.") - fmt.Fprintln(a.Writer) - ShowSubcommandHelp(context) - return err + if a.OnUsageError != nil { + err = a.OnUsageError(context, err, true) + return err + } else { + fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") + ShowSubcommandHelp(context) + return err + } } if len(a.Commands) > 0 { diff --git a/vendor/src/github.com/codegangsta/cli/app_test.go b/vendor/src/github.com/codegangsta/cli/app_test.go index 59fa75a..7feaf1f 100644 --- a/vendor/src/github.com/codegangsta/cli/app_test.go +++ b/vendor/src/github.com/codegangsta/cli/app_test.go @@ -2,6 +2,7 @@ package cli import ( "bytes" + "errors" "flag" "fmt" "io" @@ -23,6 +24,7 @@ func ExampleApp_Run() { app.Action = func(c *Context) { fmt.Printf("Hello %v\n", c.String("name")) } + app.UsageText = "app [first_arg] [second_arg]" app.Author = "Harrison" app.Email = "harrison@lolwut.com" app.Authors = []Author{Author{Name: "Oliver Allen", Email: "oliver@toyshop.com"}} @@ -249,6 +251,24 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { expect(t, args[2], "--notARealFlag") } +func TestApp_CommandWithDash(t *testing.T) { + var args []string + + app := NewApp() + command := Command{ + Name: "cmd", + Action: func(c *Context) { + args = c.Args() + }, + } + app.Commands = []Command{command} + + app.Run([]string{"", "cmd", "my-arg", "-"}) + + expect(t, args[0], "my-arg") + expect(t, args[1], "-") +} + func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) { var args []string @@ -927,7 +947,7 @@ func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { err := app.Run([]string{"foo"}) if err == nil { - t.Fatalf("expected to recieve error from Run, got none") + t.Fatalf("expected to receive error from Run, got none") } if !strings.Contains(err.Error(), "before error") { @@ -942,6 +962,11 @@ func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { app := NewApp() app.Commands = []Command{ Command{ + Subcommands: []Command{ + Command{ + Name: "sub", + }, + }, Name: "bar", Before: func(c *Context) error { return fmt.Errorf("before error") }, After: func(c *Context) error { return fmt.Errorf("after error") }, @@ -950,7 +975,7 @@ func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { err := app.Run([]string{"foo", "bar"}) if err == nil { - t.Fatalf("expected to recieve error from Run, got none") + t.Fatalf("expected to receive error from Run, got none") } if !strings.Contains(err.Error(), "before error") { @@ -960,3 +985,63 @@ func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { t.Errorf("expected text of error from After method, but got none in \"%v\"", err) } } + +func TestApp_OnUsageError_WithWrongFlagValue(t *testing.T) { + app := NewApp() + app.Flags = []Flag{ + IntFlag{Name: "flag"}, + } + app.OnUsageError = func(c *Context, err error, isSubcommand bool) error { + if isSubcommand { + t.Errorf("Expect no subcommand") + } + if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") { + t.Errorf("Expect an invalid value error, but got \"%v\"", err) + } + return errors.New("intercepted: " + err.Error()) + } + app.Commands = []Command{ + Command{ + Name: "bar", + }, + } + + err := app.Run([]string{"foo", "--flag=wrong"}) + if err == nil { + t.Fatalf("expected to receive error from Run, got none") + } + + if !strings.HasPrefix(err.Error(), "intercepted: invalid value") { + t.Errorf("Expect an intercepted error, but got \"%v\"", err) + } +} + +func TestApp_OnUsageError_WithWrongFlagValue_ForSubcommand(t *testing.T) { + app := NewApp() + app.Flags = []Flag{ + IntFlag{Name: "flag"}, + } + app.OnUsageError = func(c *Context, err error, isSubcommand bool) error { + if isSubcommand { + t.Errorf("Expect subcommand") + } + if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") { + t.Errorf("Expect an invalid value error, but got \"%v\"", err) + } + return errors.New("intercepted: " + err.Error()) + } + app.Commands = []Command{ + Command{ + Name: "bar", + }, + } + + err := app.Run([]string{"foo", "--flag=wrong", "bar"}) + if err == nil { + t.Fatalf("expected to receive error from Run, got none") + } + + if !strings.HasPrefix(err.Error(), "intercepted: invalid value") { + t.Errorf("Expect an intercepted error, but got \"%v\"", err) + } +} diff --git a/vendor/src/github.com/codegangsta/cli/appveyor.yml b/vendor/src/github.com/codegangsta/cli/appveyor.yml new file mode 100644 index 0000000..3ca7afa --- /dev/null +++ b/vendor/src/github.com/codegangsta/cli/appveyor.yml @@ -0,0 +1,16 @@ +version: "{build}" + +os: Windows Server 2012 R2 + +install: + - go version + - go env + +build_script: + - cd %APPVEYOR_BUILD_FOLDER% + - go vet ./... + - go test -v ./... + +test: off + +deploy: off diff --git a/vendor/src/github.com/codegangsta/cli/command.go b/vendor/src/github.com/codegangsta/cli/command.go index 824e77b..bbf42ae 100644 --- a/vendor/src/github.com/codegangsta/cli/command.go +++ b/vendor/src/github.com/codegangsta/cli/command.go @@ -16,6 +16,8 @@ type Command struct { Aliases []string // A short description of the usage of this command Usage string + // Custom text to show on USAGE section of help + UsageText string // A longer explanation of how the command works Description string // A short description of the arguments of this command @@ -30,6 +32,10 @@ type Command struct { After func(context *Context) error // The function to call when this command is invoked Action func(context *Context) + // Execute this function, if an usage error occurs. This is useful for displaying customized usage error messages. + // This function is able to replace the original error messages. + // If this function is not set, the "Incorrect usage" is displayed and the execution is interrupted. + OnUsageError func(context *Context, err error) error // List of child commands Subcommands []Command // List of flags to parse @@ -54,8 +60,8 @@ func (c Command) FullName() string { } // Invokes the command given the context, parses ctx.Args() to generate command-specific flags -func (c Command) Run(ctx *Context) error { - if len(c.Subcommands) > 0 || c.Before != nil || c.After != nil { +func (c Command) Run(ctx *Context) (err error) { + if len(c.Subcommands) > 0 { return c.startApp(ctx) } @@ -74,7 +80,6 @@ func (c Command) Run(ctx *Context) error { set := flagSet(c.Name, c.Flags) set.SetOutput(ioutil.Discard) - var err error if !c.SkipFlagParsing { firstFlagIndex := -1 terminatorIndex := -1 @@ -82,6 +87,9 @@ func (c Command) Run(ctx *Context) error { if arg == "--" { terminatorIndex = index break + } else if arg == "-" { + // Do nothing. A dash alone is not really a flag. + continue } else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 { firstFlagIndex = index } @@ -111,10 +119,15 @@ func (c Command) Run(ctx *Context) error { } if err != nil { - fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) - return err + if c.OnUsageError != nil { + err := c.OnUsageError(ctx, err) + return err + } else { + fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") + fmt.Fprintln(ctx.App.Writer) + ShowCommandHelp(ctx, c.Name) + return err + } } nerr := normalizeFlags(c.Flags, set) @@ -133,6 +146,30 @@ func (c Command) Run(ctx *Context) error { if checkCommandHelp(context, c.Name) { return nil } + + if c.After != nil { + defer func() { + afterErr := c.After(context) + if afterErr != nil { + if err != nil { + err = NewMultiError(err, afterErr) + } else { + err = afterErr + } + } + }() + } + + if c.Before != nil { + err := c.Before(context) + if err != nil { + fmt.Fprintln(ctx.App.Writer, err) + fmt.Fprintln(ctx.App.Writer) + ShowCommandHelp(ctx, c.Name) + return err + } + } + context.Command = c c.Action(context) return nil @@ -166,7 +203,7 @@ func (c Command) startApp(ctx *Context) error { if c.HelpName == "" { app.HelpName = c.HelpName } else { - app.HelpName = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) + app.HelpName = app.Name } if c.Description != "" { @@ -205,12 +242,9 @@ func (c Command) startApp(ctx *Context) error { app.Action = helpSubcommand.Action } - var newCmds []Command - for _, cc := range app.Commands { - cc.commandNamePath = []string{c.Name, cc.Name} - newCmds = append(newCmds, cc) + for index, cc := range app.Commands { + app.Commands[index].commandNamePath = []string{c.Name, cc.Name} } - app.Commands = newCmds return app.RunAsSubcommand(ctx) } diff --git a/vendor/src/github.com/codegangsta/cli/command_test.go b/vendor/src/github.com/codegangsta/cli/command_test.go index ac10652..827da1d 100644 --- a/vendor/src/github.com/codegangsta/cli/command_test.go +++ b/vendor/src/github.com/codegangsta/cli/command_test.go @@ -3,7 +3,9 @@ package cli import ( "errors" "flag" + "fmt" "io/ioutil" + "strings" "testing" ) @@ -43,3 +45,53 @@ func TestCommandFlagParsing(t *testing.T) { expect(t, []string(context.Args()), c.testArgs) } } + +func TestCommand_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { + app := NewApp() + app.Commands = []Command{ + Command{ + Name: "bar", + Before: func(c *Context) error { return fmt.Errorf("before error") }, + After: func(c *Context) error { return fmt.Errorf("after error") }, + }, + } + + err := app.Run([]string{"foo", "bar"}) + if err == nil { + t.Fatalf("expected to receive error from Run, got none") + } + + if !strings.Contains(err.Error(), "before error") { + t.Errorf("expected text of error from Before method, but got none in \"%v\"", err) + } + if !strings.Contains(err.Error(), "after error") { + t.Errorf("expected text of error from After method, but got none in \"%v\"", err) + } +} + +func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) { + app := NewApp() + app.Commands = []Command{ + Command{ + Name: "bar", + Flags: []Flag{ + IntFlag{Name: "flag"}, + }, + OnUsageError: func(c *Context, err error) error { + if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") { + t.Errorf("Expect an invalid value error, but got \"%v\"", err) + } + return errors.New("intercepted: " + err.Error()) + }, + }, + } + + err := app.Run([]string{"foo", "bar", "--flag=wrong"}) + if err == nil { + t.Fatalf("expected to receive error from Run, got none") + } + + if !strings.HasPrefix(err.Error(), "intercepted: invalid value") { + t.Errorf("Expect an intercepted error, but got \"%v\"", err) + } +} diff --git a/vendor/src/github.com/codegangsta/cli/context.go b/vendor/src/github.com/codegangsta/cli/context.go index f541f41..0513d34 100644 --- a/vendor/src/github.com/codegangsta/cli/context.go +++ b/vendor/src/github.com/codegangsta/cli/context.go @@ -163,7 +163,7 @@ func (c *Context) GlobalIsSet(name string) bool { // Returns a slice of flag names used in this context. func (c *Context) FlagNames() (names []string) { for _, flag := range c.Command.Flags { - name := strings.Split(flag.getName(), ",")[0] + name := strings.Split(flag.GetName(), ",")[0] if name == "help" { continue } @@ -175,7 +175,7 @@ func (c *Context) FlagNames() (names []string) { // Returns a slice of global flag names used by the app. func (c *Context) GlobalFlagNames() (names []string) { for _, flag := range c.App.Flags { - name := strings.Split(flag.getName(), ",")[0] + name := strings.Split(flag.GetName(), ",")[0] if name == "help" || name == "version" { continue } @@ -360,7 +360,7 @@ func normalizeFlags(flags []Flag, set *flag.FlagSet) error { visited[f.Name] = true }) for _, f := range flags { - parts := strings.Split(f.getName(), ",") + parts := strings.Split(f.GetName(), ",") if len(parts) == 1 { continue } diff --git a/vendor/src/github.com/codegangsta/cli/flag.go b/vendor/src/github.com/codegangsta/cli/flag.go index 9b22d7f..e951c2d 100644 --- a/vendor/src/github.com/codegangsta/cli/flag.go +++ b/vendor/src/github.com/codegangsta/cli/flag.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "os" + "runtime" "strconv" "strings" "time" @@ -29,13 +30,13 @@ var HelpFlag = BoolFlag{ } // Flag is a common interface related to parsing flags in cli. -// For more advanced flag parsing techniques, it is recomended that +// For more advanced flag parsing techniques, it is recommended that // this interface be implemented. type Flag interface { fmt.Stringer // Apply Flag settings to the given flag set Apply(*flag.FlagSet) - getName() string + GetName() string } func flagSet(name string, flags []Flag) *flag.FlagSet { @@ -73,7 +74,18 @@ type GenericFlag struct { // help text to the user (uses the String() method of the generic flag to show // the value) func (f GenericFlag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s%s \"%v\"\t%v", prefixFor(f.Name), f.Name, f.Value, f.Usage)) + return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name), f.FormatValueHelp(), f.Usage)) +} + +func (f GenericFlag) FormatValueHelp() string { + if f.Value == nil { + return "" + } + s := f.Value.String() + if len(s) == 0 { + return "" + } + return fmt.Sprintf("\"%s\"", s) } // Apply takes the flagset and calls Set on the generic flag with the value @@ -95,7 +107,7 @@ func (f GenericFlag) Apply(set *flag.FlagSet) { }) } -func (f GenericFlag) getName() string { +func (f GenericFlag) GetName() string { return f.Name } @@ -159,7 +171,7 @@ func (f StringSliceFlag) Apply(set *flag.FlagSet) { }) } -func (f StringSliceFlag) getName() string { +func (f StringSliceFlag) GetName() string { return f.Name } @@ -231,7 +243,7 @@ func (f IntSliceFlag) Apply(set *flag.FlagSet) { }) } -func (f IntSliceFlag) getName() string { +func (f IntSliceFlag) GetName() string { return f.Name } @@ -273,7 +285,7 @@ func (f BoolFlag) Apply(set *flag.FlagSet) { }) } -func (f BoolFlag) getName() string { +func (f BoolFlag) GetName() string { return f.Name } @@ -316,7 +328,7 @@ func (f BoolTFlag) Apply(set *flag.FlagSet) { }) } -func (f BoolTFlag) getName() string { +func (f BoolTFlag) GetName() string { return f.Name } @@ -331,16 +343,15 @@ type StringFlag struct { // String returns the usage func (f StringFlag) String() string { - var fmtString string - fmtString = "%s %v\t%v" + return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name), f.FormatValueHelp(), f.Usage)) +} - if len(f.Value) > 0 { - fmtString = "%s \"%v\"\t%v" - } else { - fmtString = "%s %v\t%v" +func (f StringFlag) FormatValueHelp() string { + s := f.Value + if len(s) == 0 { + return "" } - - return withEnvHint(f.EnvVar, fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage)) + return fmt.Sprintf("\"%s\"", s) } // Apply populates the flag given the flag set and environment @@ -364,7 +375,7 @@ func (f StringFlag) Apply(set *flag.FlagSet) { }) } -func (f StringFlag) getName() string { +func (f StringFlag) GetName() string { return f.Name } @@ -407,7 +418,7 @@ func (f IntFlag) Apply(set *flag.FlagSet) { }) } -func (f IntFlag) getName() string { +func (f IntFlag) GetName() string { return f.Name } @@ -450,7 +461,7 @@ func (f DurationFlag) Apply(set *flag.FlagSet) { }) } -func (f DurationFlag) getName() string { +func (f DurationFlag) GetName() string { return f.Name } @@ -492,7 +503,7 @@ func (f Float64Flag) Apply(set *flag.FlagSet) { }) } -func (f Float64Flag) getName() string { +func (f Float64Flag) GetName() string { return f.Name } @@ -521,7 +532,15 @@ func prefixedNames(fullName string) (prefixed string) { func withEnvHint(envVar, str string) string { envText := "" if envVar != "" { - envText = fmt.Sprintf(" [$%s]", strings.Join(strings.Split(envVar, ","), ", $")) + prefix := "$" + suffix := "" + sep := ", $" + if runtime.GOOS == "windows" { + prefix = "%" + suffix = "%" + sep = "%, %" + } + envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(strings.Split(envVar, ","), sep), suffix) } return str + envText } diff --git a/vendor/src/github.com/codegangsta/cli/flag_test.go b/vendor/src/github.com/codegangsta/cli/flag_test.go index 4462d3f..3caa70a 100644 --- a/vendor/src/github.com/codegangsta/cli/flag_test.go +++ b/vendor/src/github.com/codegangsta/cli/flag_test.go @@ -6,6 +6,7 @@ import ( "reflect" "strings" "testing" + "runtime" ) var boolFlagTests = []struct { @@ -58,8 +59,12 @@ func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { flag := StringFlag{Name: test.name, Value: test.value, EnvVar: "APP_FOO"} output := flag.String() - if !strings.HasSuffix(output, " [$APP_FOO]") { - t.Errorf("%s does not end with [$APP_FOO]", output) + expectedSuffix := " [$APP_FOO]" + if runtime.GOOS == "windows" { + expectedSuffix = " [%APP_FOO%]" + } + if !strings.HasSuffix(output, expectedSuffix) { + t.Errorf("%s does not end with" + expectedSuffix, output) } } } @@ -110,8 +115,12 @@ func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) { flag := StringSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_QWWX"} output := flag.String() - if !strings.HasSuffix(output, " [$APP_QWWX]") { - t.Errorf("%q does not end with [$APP_QWWX]", output) + expectedSuffix := " [$APP_QWWX]" + if runtime.GOOS == "windows" { + expectedSuffix = " [%APP_QWWX%]" + } + if !strings.HasSuffix(output, expectedSuffix) { + t.Errorf("%q does not end with" + expectedSuffix, output) } } } @@ -143,8 +152,12 @@ func TestIntFlagWithEnvVarHelpOutput(t *testing.T) { flag := IntFlag{Name: test.name, EnvVar: "APP_BAR"} output := flag.String() - if !strings.HasSuffix(output, " [$APP_BAR]") { - t.Errorf("%s does not end with [$APP_BAR]", output) + expectedSuffix := " [$APP_BAR]" + if runtime.GOOS == "windows" { + expectedSuffix = " [%APP_BAR%]" + } + if !strings.HasSuffix(output, expectedSuffix) { + t.Errorf("%s does not end with" + expectedSuffix, output) } } } @@ -176,8 +189,12 @@ func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) { flag := DurationFlag{Name: test.name, EnvVar: "APP_BAR"} output := flag.String() - if !strings.HasSuffix(output, " [$APP_BAR]") { - t.Errorf("%s does not end with [$APP_BAR]", output) + expectedSuffix := " [$APP_BAR]" + if runtime.GOOS == "windows" { + expectedSuffix = " [%APP_BAR%]" + } + if !strings.HasSuffix(output, expectedSuffix) { + t.Errorf("%s does not end with" + expectedSuffix, output) } } } @@ -216,8 +233,12 @@ func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) { flag := IntSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_SMURF"} output := flag.String() - if !strings.HasSuffix(output, " [$APP_SMURF]") { - t.Errorf("%q does not end with [$APP_SMURF]", output) + expectedSuffix := " [$APP_SMURF]" + if runtime.GOOS == "windows" { + expectedSuffix = " [%APP_SMURF%]" + } + if !strings.HasSuffix(output, expectedSuffix) { + t.Errorf("%q does not end with" + expectedSuffix, output) } } } @@ -249,8 +270,12 @@ func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) { flag := Float64Flag{Name: test.name, EnvVar: "APP_BAZ"} output := flag.String() - if !strings.HasSuffix(output, " [$APP_BAZ]") { - t.Errorf("%s does not end with [$APP_BAZ]", output) + expectedSuffix := " [$APP_BAZ]" + if runtime.GOOS == "windows" { + expectedSuffix = " [%APP_BAZ%]" + } + if !strings.HasSuffix(output, expectedSuffix) { + t.Errorf("%s does not end with" + expectedSuffix, output) } } } @@ -283,8 +308,12 @@ func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) { flag := GenericFlag{Name: test.name, EnvVar: "APP_ZAP"} output := flag.String() - if !strings.HasSuffix(output, " [$APP_ZAP]") { - t.Errorf("%s does not end with [$APP_ZAP]", output) + expectedSuffix := " [$APP_ZAP]" + if runtime.GOOS == "windows" { + expectedSuffix = " [%APP_ZAP%]" + } + if !strings.HasSuffix(output, expectedSuffix) { + t.Errorf("%s does not end with" + expectedSuffix, output) } } } diff --git a/vendor/src/github.com/codegangsta/cli/help.go b/vendor/src/github.com/codegangsta/cli/help.go index a246f63..15916f8 100644 --- a/vendor/src/github.com/codegangsta/cli/help.go +++ b/vendor/src/github.com/codegangsta/cli/help.go @@ -15,7 +15,7 @@ var AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: - {{.HelpName}} {{if .Flags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .Flags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} {{if .Version}} VERSION: {{.Version}} @@ -180,7 +180,9 @@ func printHelp(out io.Writer, templ string, data interface{}) { t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) err := t.Execute(w, data) if err != nil { - panic(err) + // If the writer is closed, t.Execute will fail, and there's nothing + // we can do to recover. We could send this to os.Stderr if we need. + return } w.Flush() } diff --git a/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/context.go b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/context.go index ef2f3e8..11bd8d3 100644 --- a/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/context.go +++ b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/context.go @@ -189,7 +189,7 @@ func Background() Context { } // TODO returns a non-nil, empty Context. Code should use context.TODO when -// it's unclear which Context to use or it's is not yet available (because the +// it's unclear which Context to use or it is not yet available (because the // surrounding function has not yet been extended to accept a Context // parameter). TODO is recognized by static analysis tools that determine // whether Contexts are propagated correctly in a program. diff --git a/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/context_test.go b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/context_test.go index faf6772..05345fc 100644 --- a/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/context_test.go +++ b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/context_test.go @@ -375,7 +375,7 @@ func TestAllocs(t *testing.T) { <-c.Done() }, limit: 8, - gccgoLimit: 13, + gccgoLimit: 15, }, { desc: "WithCancel(bg)", @@ -536,7 +536,7 @@ func testLayers(t *testing.T, seed int64, testTimeout bool) { if testTimeout { select { case <-ctx.Done(): - case <-time.After(timeout + timeout/10): + case <-time.After(timeout + 100*time.Millisecond): errorf("ctx should have timed out") } checkValues("after timeout") diff --git a/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/cancelreq.go b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/cancelreq.go new file mode 100644 index 0000000..e3170e3 --- /dev/null +++ b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/cancelreq.go @@ -0,0 +1,19 @@ +// Copyright 2015 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. + +// +build go1.5 + +package ctxhttp + +import "net/http" + +func canceler(client *http.Client, req *http.Request) func() { + // TODO(djd): Respect any existing value of req.Cancel. + ch := make(chan struct{}) + req.Cancel = ch + + return func() { + close(ch) + } +} diff --git a/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/cancelreq_go14.go b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/cancelreq_go14.go new file mode 100644 index 0000000..56bcbad --- /dev/null +++ b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/cancelreq_go14.go @@ -0,0 +1,23 @@ +// Copyright 2015 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. + +// +build !go1.5 + +package ctxhttp + +import "net/http" + +type requestCanceler interface { + CancelRequest(*http.Request) +} + +func canceler(client *http.Client, req *http.Request) func() { + rc, ok := client.Transport.(requestCanceler) + if !ok { + return func() {} + } + return func() { + rc.CancelRequest(req) + } +} diff --git a/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/ctxhttp.go b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/ctxhttp.go new file mode 100644 index 0000000..8089ef0 --- /dev/null +++ b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/ctxhttp.go @@ -0,0 +1,140 @@ +// Copyright 2015 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 ctxhttp provides helper functions for performing context-aware HTTP requests. +package ctxhttp + +import ( + "io" + "net/http" + "net/url" + "strings" + + "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context" +) + +func nop() {} + +var ( + testHookContextDoneBeforeHeaders = nop + testHookDoReturned = nop + testHookDidBodyClose = nop +) + +// Do sends an HTTP request with the provided http.Client and returns an HTTP response. +// If the client is nil, http.DefaultClient is used. +// If the context is canceled or times out, ctx.Err() will be returned. +func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) { + if client == nil { + client = http.DefaultClient + } + + // Request cancelation changed in Go 1.5, see cancelreq.go and cancelreq_go14.go. + cancel := canceler(client, req) + + type responseAndError struct { + resp *http.Response + err error + } + result := make(chan responseAndError, 1) + + go func() { + resp, err := client.Do(req) + testHookDoReturned() + result <- responseAndError{resp, err} + }() + + var resp *http.Response + + select { + case <-ctx.Done(): + testHookContextDoneBeforeHeaders() + cancel() + // Clean up after the goroutine calling client.Do: + go func() { + if r := <-result; r.resp != nil { + testHookDidBodyClose() + r.resp.Body.Close() + } + }() + return nil, ctx.Err() + case r := <-result: + var err error + resp, err = r.resp, r.err + if err != nil { + return resp, err + } + } + + c := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + cancel() + case <-c: + // The response's Body is closed. + } + }() + resp.Body = ¬ifyingReader{resp.Body, c} + + return resp, nil +} + +// Get issues a GET request via the Do function. +func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + return Do(ctx, client, req) +} + +// Head issues a HEAD request via the Do function. +func Head(ctx context.Context, client *http.Client, url string) (*http.Response, error) { + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return nil, err + } + return Do(ctx, client, req) +} + +// Post issues a POST request via the Do function. +func Post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", bodyType) + return Do(ctx, client, req) +} + +// PostForm issues a POST request via the Do function. +func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) { + return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) +} + +// notifyingReader is an io.ReadCloser that closes the notify channel after +// Close is called or a Read fails on the underlying ReadCloser. +type notifyingReader struct { + io.ReadCloser + notify chan<- struct{} +} + +func (r *notifyingReader) Read(p []byte) (int, error) { + n, err := r.ReadCloser.Read(p) + if err != nil && r.notify != nil { + close(r.notify) + r.notify = nil + } + return n, err +} + +func (r *notifyingReader) Close() error { + err := r.ReadCloser.Close() + if r.notify != nil { + close(r.notify) + r.notify = nil + } + return err +} diff --git a/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/ctxhttp_test.go b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/ctxhttp_test.go new file mode 100644 index 0000000..8846f6b --- /dev/null +++ b/vendor/src/github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context/ctxhttp/ctxhttp_test.go @@ -0,0 +1,176 @@ +// Copyright 2015 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. + +// +build !plan9 + +package ctxhttp + +import ( + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context" +) + +const ( + requestDuration = 100 * time.Millisecond + requestBody = "ok" +) + +func TestNoTimeout(t *testing.T) { + ctx := context.Background() + resp, err := doRequest(ctx) + + if resp == nil || err != nil { + t.Fatalf("error received from client: %v %v", err, resp) + } +} + +func TestCancel(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + go func() { + time.Sleep(requestDuration / 2) + cancel() + }() + + resp, err := doRequest(ctx) + + if resp != nil || err == nil { + t.Fatalf("expected error, didn't get one. resp: %v", resp) + } + if err != ctx.Err() { + t.Fatalf("expected error from context but got: %v", err) + } +} + +func TestCancelAfterRequest(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + resp, err := doRequest(ctx) + + // Cancel before reading the body. + // Request.Body should still be readable after the context is canceled. + cancel() + + b, err := ioutil.ReadAll(resp.Body) + if err != nil || string(b) != requestBody { + t.Fatalf("could not read body: %q %v", b, err) + } +} + +func TestCancelAfterHangingRequest(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.(http.Flusher).Flush() + <-w.(http.CloseNotifier).CloseNotify() + }) + + serv := httptest.NewServer(handler) + defer serv.Close() + + ctx, cancel := context.WithCancel(context.Background()) + resp, err := Get(ctx, nil, serv.URL) + if err != nil { + t.Fatalf("unexpected error in Get: %v", err) + } + + // Cancel befer reading the body. + // Reading Request.Body should fail, since the request was + // canceled before anything was written. + cancel() + + done := make(chan struct{}) + + go func() { + b, err := ioutil.ReadAll(resp.Body) + if len(b) != 0 || err == nil { + t.Errorf(`Read got (%q, %v); want ("", error)`, b, err) + } + close(done) + }() + + select { + case <-time.After(1 * time.Second): + t.Errorf("Test timed out") + case <-done: + } +} + +func doRequest(ctx context.Context) (*http.Response, error) { + var okHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(requestDuration) + w.Write([]byte(requestBody)) + }) + + serv := httptest.NewServer(okHandler) + defer serv.Close() + + return Get(ctx, nil, serv.URL) +} + +// golang.org/issue/14065 +func TestClosesResponseBodyOnCancel(t *testing.T) { + defer func() { testHookContextDoneBeforeHeaders = nop }() + defer func() { testHookDoReturned = nop }() + defer func() { testHookDidBodyClose = nop }() + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + + // closed when Do enters select case <-ctx.Done() + enteredDonePath := make(chan struct{}) + + testHookContextDoneBeforeHeaders = func() { + close(enteredDonePath) + } + + testHookDoReturned = func() { + // We now have the result (the Flush'd headers) at least, + // so we can cancel the request. + cancel() + + // But block the client.Do goroutine from sending + // until Do enters into the <-ctx.Done() path, since + // otherwise if both channels are readable, select + // picks a random one. + <-enteredDonePath + } + + sawBodyClose := make(chan struct{}) + testHookDidBodyClose = func() { close(sawBodyClose) } + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + c := &http.Client{Transport: tr} + req, _ := http.NewRequest("GET", ts.URL, nil) + _, doErr := Do(ctx, c, req) + + select { + case <-sawBodyClose: + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for body to close") + } + + if doErr != ctx.Err() { + t.Errorf("Do error = %v; want %v", doErr, ctx.Err()) + } +} + +type noteCloseConn struct { + net.Conn + onceClose sync.Once + closefn func() +} + +func (c *noteCloseConn) Close() error { + c.onceClose.Do(c.closefn) + return c.Conn.Close() +} diff --git a/vendor/src/github.com/coreos/etcd/client/client.go b/vendor/src/github.com/coreos/etcd/client/client.go index ece4cc0..5071ca4 100644 --- a/vendor/src/github.com/coreos/etcd/client/client.go +++ b/vendor/src/github.com/coreos/etcd/client/client.go @@ -24,6 +24,7 @@ import ( "net/url" "reflect" "sort" + "strconv" "sync" "time" @@ -34,6 +35,7 @@ var ( ErrNoEndpoints = errors.New("client: no endpoints available") ErrTooManyRedirects = errors.New("client: too many redirects") ErrClusterUnavailable = errors.New("client: etcd cluster is unavailable or misconfigured") + ErrNoLeaderEndpoint = errors.New("client: no leader endpoint available") errTooManyRedirectChecks = errors.New("client: too many redirect checks") ) @@ -48,6 +50,29 @@ var DefaultTransport CancelableTransport = &http.Transport{ TLSHandshakeTimeout: 10 * time.Second, } +type EndpointSelectionMode int + +const ( + // EndpointSelectionRandom is the default value of the 'SelectionMode'. + // As the name implies, the client object will pick a node from the members + // of the cluster in a random fashion. If the cluster has three members, A, B, + // and C, the client picks any node from its three members as its request + // destination. + EndpointSelectionRandom EndpointSelectionMode = iota + + // If 'SelectionMode' is set to 'EndpointSelectionPrioritizeLeader', + // requests are sent directly to the cluster leader. This reduces + // forwarding roundtrips compared to making requests to etcd followers + // who then forward them to the cluster leader. In the event of a leader + // failure, however, clients configured this way cannot prioritize among + // the remaining etcd followers. Therefore, when a client sets 'SelectionMode' + // to 'EndpointSelectionPrioritizeLeader', it must use 'client.AutoSync()' to + // maintain its knowledge of current cluster state. + // + // This mode should be used with Client.AutoSync(). + EndpointSelectionPrioritizeLeader +) + type Config struct { // Endpoints defines a set of URLs (schemes, hosts and ports only) // that can be used to communicate with a logical etcd cluster. For @@ -73,7 +98,7 @@ type Config struct { // CheckRedirect specifies the policy for handling HTTP redirects. // If CheckRedirect is not nil, the Client calls it before // following an HTTP redirect. The sole argument is the number of - // requests that have alrady been made. If CheckRedirect returns + // requests that have already been made. If CheckRedirect returns // an error, Client.Do will not make any further requests and return // the error back it to the caller. // @@ -99,11 +124,17 @@ type Config struct { // watch start. But if server is behind some kind of proxy, the response // header may be cached at proxy, and Client cannot rely on this behavior. // + // Especially, wait request will ignore this timeout. + // // One API call may send multiple requests to different etcd servers until it // succeeds. Use context of the API to specify the overall timeout. // // A HeaderTimeoutPerRequest of zero means no timeout. HeaderTimeoutPerRequest time.Duration + + // SelectionMode is an EndpointSelectionMode enum that specifies the + // policy for choosing the etcd cluster node to which requests are sent. + SelectionMode EndpointSelectionMode } func (cfg *Config) transport() CancelableTransport { @@ -162,6 +193,11 @@ type Client interface { // this may differ from the initial Endpoints provided in the Config. Endpoints() []string + // SetEndpoints sets the set of API endpoints used by Client to resolve + // HTTP requests. If the given endpoints are not valid, an error will be + // returned + SetEndpoints(eps []string) error + httpClient } @@ -169,6 +205,7 @@ func New(cfg Config) (Client, error) { c := &httpClusterClient{ clientFactory: newHTTPClientFactory(cfg.transport(), cfg.checkRedirect(), cfg.HeaderTimeoutPerRequest), rand: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))), + selectionMode: cfg.SelectionMode, } if cfg.Username != "" { c.credentials = &credentials{ @@ -176,7 +213,7 @@ func New(cfg Config) (Client, error) { password: cfg.Password, } } - if err := c.reset(cfg.Endpoints); err != nil { + if err := c.SetEndpoints(cfg.Endpoints); err != nil { return nil, err } return c, nil @@ -216,10 +253,21 @@ type httpClusterClient struct { pinned int credentials *credentials sync.RWMutex - rand *rand.Rand + rand *rand.Rand + selectionMode EndpointSelectionMode +} + +func (c *httpClusterClient) getLeaderEndpoint() (string, error) { + mAPI := NewMembersAPI(c) + leader, err := mAPI.Leader(context.Background()) + if err != nil { + return "", err + } + + return leader.ClientURLs[0], nil // TODO: how to handle multiple client URLs? } -func (c *httpClusterClient) reset(eps []string) error { +func (c *httpClusterClient) SetEndpoints(eps []string) error { if len(eps) == 0 { return ErrNoEndpoints } @@ -233,9 +281,28 @@ func (c *httpClusterClient) reset(eps []string) error { neps[i] = *u } - c.endpoints = shuffleEndpoints(c.rand, neps) - // TODO: pin old endpoint if possible, and rebalance when new endpoint appears - c.pinned = 0 + switch c.selectionMode { + case EndpointSelectionRandom: + c.endpoints = shuffleEndpoints(c.rand, neps) + c.pinned = 0 + case EndpointSelectionPrioritizeLeader: + c.endpoints = neps + lep, err := c.getLeaderEndpoint() + if err != nil { + return ErrNoLeaderEndpoint + } + + for i := range c.endpoints { + if c.endpoints[i].String() == lep { + c.pinned = i + break + } + } + // If endpoints doesn't have the lu, just keep c.pinned = 0. + // Forwarding between follower and leader would be required but it works. + default: + return errors.New(fmt.Sprintf("invalid endpoint selection mode: %d", c.selectionMode)) + } return nil } @@ -341,7 +408,7 @@ func (c *httpClusterClient) Sync(ctx context.Context) error { return nil } - return c.reset(eps) + return c.SetEndpoints(eps) } func (c *httpClusterClient) AutoSync(ctx context.Context, interval time.Duration) error { @@ -378,9 +445,21 @@ func (c *simpleHTTPClient) Do(ctx context.Context, act httpAction) (*http.Respon return nil, nil, err } + isWait := false + if req != nil && req.URL != nil { + ws := req.URL.Query().Get("wait") + if len(ws) != 0 { + var err error + isWait, err = strconv.ParseBool(ws) + if err != nil { + return nil, nil, fmt.Errorf("wrong wait value %s (%v for %+v)", ws, err, req) + } + } + } + var hctx context.Context var hcancel context.CancelFunc - if c.headerTimeout > 0 { + if !isWait && c.headerTimeout > 0 { hctx, hcancel = context.WithTimeout(ctx, c.headerTimeout) } else { hctx, hcancel = context.WithCancel(ctx) diff --git a/vendor/src/github.com/coreos/etcd/client/client_test.go b/vendor/src/github.com/coreos/etcd/client/client_test.go index be526b1..74437d6 100644 --- a/vendor/src/github.com/coreos/etcd/client/client_test.go +++ b/vendor/src/github.com/coreos/etcd/client/client_test.go @@ -705,7 +705,7 @@ func TestHTTPClusterClientSync(t *testing.T) { clientFactory: cf, rand: rand.New(rand.NewSource(0)), } - err := hc.reset([]string{"http://127.0.0.1:2379"}) + err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"}) if err != nil { t.Fatalf("unexpected error during setup: %#v", err) } @@ -728,7 +728,7 @@ func TestHTTPClusterClientSync(t *testing.T) { t.Fatalf("incorrect endpoints post-Sync: want=%#v got=%#v", want, got) } - err = hc.reset([]string{"http://127.0.0.1:4009"}) + err = hc.SetEndpoints([]string{"http://127.0.0.1:4009"}) if err != nil { t.Fatalf("unexpected error during reset: %#v", err) } @@ -749,7 +749,7 @@ func TestHTTPClusterClientSyncFail(t *testing.T) { clientFactory: cf, rand: rand.New(rand.NewSource(0)), } - err := hc.reset([]string{"http://127.0.0.1:2379"}) + err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"}) if err != nil { t.Fatalf("unexpected error during setup: %#v", err) } @@ -783,7 +783,7 @@ func TestHTTPClusterClientAutoSyncCancelContext(t *testing.T) { clientFactory: cf, rand: rand.New(rand.NewSource(0)), } - err := hc.reset([]string{"http://127.0.0.1:2379"}) + err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"}) if err != nil { t.Fatalf("unexpected error during setup: %#v", err) } @@ -805,7 +805,7 @@ func TestHTTPClusterClientAutoSyncFail(t *testing.T) { clientFactory: cf, rand: rand.New(rand.NewSource(0)), } - err := hc.reset([]string{"http://127.0.0.1:2379"}) + err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"}) if err != nil { t.Fatalf("unexpected error during setup: %#v", err) } @@ -838,7 +838,7 @@ func TestHTTPClusterClientSyncPinEndpoint(t *testing.T) { clientFactory: cf, rand: rand.New(rand.NewSource(0)), } - err := hc.reset([]string{"http://127.0.0.1:4003", "http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"}) + err := hc.SetEndpoints([]string{"http://127.0.0.1:4003", "http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"}) if err != nil { t.Fatalf("unexpected error during setup: %#v", err) } @@ -867,7 +867,7 @@ func TestHTTPClusterClientResetFail(t *testing.T) { for i, tt := range tests { hc := &httpClusterClient{rand: rand.New(rand.NewSource(0))} - err := hc.reset(tt) + err := hc.SetEndpoints(tt) if err == nil { t.Errorf("#%d: expected non-nil error", i) } @@ -879,7 +879,7 @@ func TestHTTPClusterClientResetPinRandom(t *testing.T) { pinNum := 0 for i := 0; i < round; i++ { hc := &httpClusterClient{rand: rand.New(rand.NewSource(int64(i)))} - err := hc.reset([]string{"http://127.0.0.1:4001", "http://127.0.0.1:4002", "http://127.0.0.1:4003"}) + err := hc.SetEndpoints([]string{"http://127.0.0.1:4001", "http://127.0.0.1:4002", "http://127.0.0.1:4003"}) if err != nil { t.Fatalf("#%d: reset error (%v)", i, err) } diff --git a/vendor/src/github.com/coreos/etcd/client/keys.go b/vendor/src/github.com/coreos/etcd/client/keys.go index 67fa02d..c209cab 100644 --- a/vendor/src/github.com/coreos/etcd/client/keys.go +++ b/vendor/src/github.com/coreos/etcd/client/keys.go @@ -106,7 +106,7 @@ type KeysAPI interface { // Set assigns a new value to a Node identified by a given key. The caller // may define a set of conditions in the SetOptions. If SetOptions.Dir=true - // than value is ignored. + // then value is ignored. Set(ctx context.Context, key, value string, opts *SetOptions) (*Response, error) // Delete removes a Node identified by the given key, optionally destroying @@ -184,6 +184,11 @@ type SetOptions struct { // a TTL of 0. TTL time.Duration + // When refresh is set to true a TTL value can be updated + // without firing a watch or changing the node value. A + // value must not provided when refreshing a key. + Refresh bool + // Dir specifies whether or not this Node should be created as a directory. Dir bool } @@ -234,7 +239,7 @@ type DeleteOptions struct { type Watcher interface { // Next blocks until an etcd event occurs, then returns a Response - // represeting that event. The behavior of Next depends on the + // representing that event. The behavior of Next depends on the // WatcherOptions used to construct the Watcher. Next is designed to // be called repeatedly, each time blocking until a subsequent event // is available. @@ -327,6 +332,7 @@ func (k *httpKeysAPI) Set(ctx context.Context, key, val string, opts *SetOptions act.PrevIndex = opts.PrevIndex act.PrevExist = opts.PrevExist act.TTL = opts.TTL + act.Refresh = opts.Refresh act.Dir = opts.Dir } @@ -518,6 +524,7 @@ type setAction struct { PrevIndex uint64 PrevExist PrevExistType TTL time.Duration + Refresh bool Dir bool } @@ -549,6 +556,10 @@ func (a *setAction) HTTPRequest(ep url.URL) *http.Request { form.Add("ttl", strconv.FormatUint(uint64(a.TTL.Seconds()), 10)) } + if a.Refresh { + form.Add("refresh", "true") + } + u.RawQuery = params.Encode() body := strings.NewReader(form.Encode()) diff --git a/vendor/src/github.com/coreos/etcd/client/keys_test.go b/vendor/src/github.com/coreos/etcd/client/keys_test.go index cafe6be..14bc45b 100644 --- a/vendor/src/github.com/coreos/etcd/client/keys_test.go +++ b/vendor/src/github.com/coreos/etcd/client/keys_test.go @@ -356,6 +356,18 @@ func TestSetAction(t *testing.T) { wantURL: "http://example.com/foo", wantBody: "ttl=180&value=", }, + + // Refresh is set + { + act: setAction{ + Key: "foo", + TTL: 3 * time.Minute, + Refresh: true, + }, + wantURL: "http://example.com/foo", + wantBody: "refresh=true&ttl=180&value=", + }, + // Dir is set { act: setAction{ diff --git a/vendor/src/github.com/coreos/etcd/client/members.go b/vendor/src/github.com/coreos/etcd/client/members.go index b6f33ed..71b01b2 100644 --- a/vendor/src/github.com/coreos/etcd/client/members.go +++ b/vendor/src/github.com/coreos/etcd/client/members.go @@ -29,6 +29,7 @@ import ( var ( defaultV2MembersPrefix = "/v2/members" + defaultLeaderSuffix = "/leader" ) type Member struct { @@ -105,6 +106,9 @@ type MembersAPI interface { // Update instructs etcd to update an existing Member in the cluster. Update(ctx context.Context, mID string, peerURLs []string) error + + // Leader gets current leader of the cluster + Leader(ctx context.Context) (*Member, error) } type httpMembersAPI struct { @@ -199,6 +203,25 @@ func (m *httpMembersAPI) Remove(ctx context.Context, memberID string) error { return assertStatusCode(resp.StatusCode, http.StatusNoContent, http.StatusGone) } +func (m *httpMembersAPI) Leader(ctx context.Context) (*Member, error) { + req := &membersAPIActionLeader{} + resp, body, err := m.client.Do(ctx, req) + if err != nil { + return nil, err + } + + if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil { + return nil, err + } + + var leader Member + if err := json.Unmarshal(body, &leader); err != nil { + return nil, err + } + + return &leader, nil +} + type membersAPIActionList struct{} func (l *membersAPIActionList) HTTPRequest(ep url.URL) *http.Request { @@ -255,6 +278,15 @@ func assertStatusCode(got int, want ...int) (err error) { return fmt.Errorf("unexpected status code %d", got) } +type membersAPIActionLeader struct{} + +func (l *membersAPIActionLeader) HTTPRequest(ep url.URL) *http.Request { + u := v2MembersURL(ep) + u.Path = path.Join(u.Path, defaultLeaderSuffix) + req, _ := http.NewRequest("GET", u.String(), nil) + return req +} + // v2MembersURL add the necessary path to the provided endpoint // to route requests to the default v2 members API. func v2MembersURL(ep url.URL) *url.URL { diff --git a/vendor/src/github.com/coreos/etcd/client/members_test.go b/vendor/src/github.com/coreos/etcd/client/members_test.go index df7be1c..494e2dc 100644 --- a/vendor/src/github.com/coreos/etcd/client/members_test.go +++ b/vendor/src/github.com/coreos/etcd/client/members_test.go @@ -114,6 +114,23 @@ func TestMembersAPIActionRemove(t *testing.T) { } } +func TestMembersAPIActionLeader(t *testing.T) { + ep := url.URL{Scheme: "http", Host: "example.com"} + act := &membersAPIActionLeader{} + + wantURL := &url.URL{ + Scheme: "http", + Host: "example.com", + Path: "/v2/members/leader", + } + + got := *act.HTTPRequest(ep) + err := assertRequest(got, "GET", wantURL, http.Header{}, nil) + if err != nil { + t.Error(err.Error()) + } +} + func TestAssertStatusCode(t *testing.T) { if err := assertStatusCode(404, 400); err == nil { t.Errorf("assertStatusCode failed to detect conflict in 400 vs 404") @@ -520,3 +537,63 @@ func TestHTTPMembersAPIListError(t *testing.T) { } } } + +func TestHTTPMembersAPILeaderSuccess(t *testing.T) { + wantAction := &membersAPIActionLeader{} + mAPI := &httpMembersAPI{ + client: &actionAssertingHTTPClient{ + t: t, + act: wantAction, + resp: http.Response{ + StatusCode: http.StatusOK, + }, + body: []byte(`{"id":"94088180e21eb87b","name":"node2","peerURLs":["http://127.0.0.1:7002"],"clientURLs":["http://127.0.0.1:4002"]}`), + }, + } + + wantResponseMember := &Member{ + ID: "94088180e21eb87b", + Name: "node2", + PeerURLs: []string{"http://127.0.0.1:7002"}, + ClientURLs: []string{"http://127.0.0.1:4002"}, + } + + m, err := mAPI.Leader(context.Background()) + if err != nil { + t.Errorf("err = %v, want %v", err, nil) + } + if !reflect.DeepEqual(wantResponseMember, m) { + t.Errorf("incorrect member: member = %v, want %v", wantResponseMember, m) + } +} + +func TestHTTPMembersAPILeaderError(t *testing.T) { + tests := []httpClient{ + // generic httpClient failure + &staticHTTPClient{err: errors.New("fail!")}, + + // unrecognized HTTP status code + &staticHTTPClient{ + resp: http.Response{StatusCode: http.StatusTeapot}, + }, + + // fail to unmarshal body on StatusOK + &staticHTTPClient{ + resp: http.Response{ + StatusCode: http.StatusOK, + }, + body: []byte(`[{"id":"XX`), + }, + } + + for i, tt := range tests { + mAPI := &httpMembersAPI{client: tt} + m, err := mAPI.Leader(context.Background()) + if err == nil { + t.Errorf("#%d: err = nil, want not nil", i) + } + if m != nil { + t.Errorf("member slice = %v, want nil", m) + } + } +} diff --git a/vendor/src/github.com/coreos/etcd/client/srv.go b/vendor/src/github.com/coreos/etcd/client/srv.go index f74c122..0619796 100644 --- a/vendor/src/github.com/coreos/etcd/client/srv.go +++ b/vendor/src/github.com/coreos/etcd/client/srv.go @@ -27,7 +27,7 @@ var ( type srvDiscover struct{} -// NewSRVDiscover constructs a new Dicoverer that uses the stdlib to lookup SRV records. +// NewSRVDiscover constructs a new Discoverer that uses the stdlib to lookup SRV records. func NewSRVDiscover() Discoverer { return &srvDiscover{} } @@ -50,8 +50,8 @@ func (d *srvDiscover) Discover(domain string) ([]string, error) { return nil } - errHTTPS := updateURLs("etcd-server-ssl", "https") - errHTTP := updateURLs("etcd-server", "http") + errHTTPS := updateURLs("etcd-client-ssl", "https") + errHTTP := updateURLs("etcd-client", "http") if errHTTPS != nil && errHTTP != nil { return nil, fmt.Errorf("dns lookup errors: %s and %s", errHTTPS, errHTTP) diff --git a/vendor/src/github.com/coreos/etcd/client/srv_test.go b/vendor/src/github.com/coreos/etcd/client/srv_test.go index 7121f45..52d4a08 100644 --- a/vendor/src/github.com/coreos/etcd/client/srv_test.go +++ b/vendor/src/github.com/coreos/etcd/client/srv_test.go @@ -78,10 +78,10 @@ func TestSRVDiscover(t *testing.T) { for i, tt := range tests { lookupSRV = func(service string, proto string, domain string) (string, []*net.SRV, error) { - if service == "etcd-server-ssl" { + if service == "etcd-client-ssl" { return "", tt.withSSL, nil } - if service == "etcd-server" { + if service == "etcd-client" { return "", tt.withoutSSL, nil } return "", nil, errors.New("Unknown service in mock") diff --git a/vendor/src/github.com/coreos/etcd/client/util.go b/vendor/src/github.com/coreos/etcd/client/util.go new file mode 100644 index 0000000..fc0800b --- /dev/null +++ b/vendor/src/github.com/coreos/etcd/client/util.go @@ -0,0 +1,23 @@ +// Copyright 2016 CoreOS, Inc. +// +// 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 client + +// IsKeyNotFound returns true if the error code is ErrorCodeKeyNotFound. +func IsKeyNotFound(err error) bool { + if cErr, ok := err.(Error); ok { + return cErr.Code == ErrorCodeKeyNotFound + } + return false +} diff --git a/vendor/src/github.com/coreos/etcd/pkg/transport/keepalive_listener.go b/vendor/src/github.com/coreos/etcd/pkg/transport/keepalive_listener.go index 6f58061..1fe1ba8 100644 --- a/vendor/src/github.com/coreos/etcd/pkg/transport/keepalive_listener.go +++ b/vendor/src/github.com/coreos/etcd/pkg/transport/keepalive_listener.go @@ -21,17 +21,19 @@ import ( "time" ) +type keepAliveConn interface { + SetKeepAlive(bool) error + SetKeepAlivePeriod(d time.Duration) error +} + // NewKeepAliveListener returns a listener that listens on the given address. +// Be careful when wrap around KeepAliveListener with another Listener if TLSInfo is not nil. +// Some pkgs (like go/http) might expect Listener to return TLSConn type to start TLS handshake. // http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html -func NewKeepAliveListener(addr string, scheme string, info TLSInfo) (net.Listener, error) { - l, err := net.Listen("tcp", addr) - if err != nil { - return nil, err - } - +func NewKeepAliveListener(l net.Listener, scheme string, info TLSInfo) (net.Listener, error) { if scheme == "https" { if info.Empty() { - return nil, fmt.Errorf("cannot listen on TLS for %s: KeyFile and CertFile are not presented", scheme+"://"+addr) + return nil, fmt.Errorf("cannot listen on TLS for given listener: KeyFile and CertFile are not presented") } cfg, err := info.ServerConfig() if err != nil { @@ -53,13 +55,13 @@ func (kln *keepaliveListener) Accept() (net.Conn, error) { if err != nil { return nil, err } - tcpc := c.(*net.TCPConn) + kac := c.(keepAliveConn) // detection time: tcp_keepalive_time + tcp_keepalive_probes + tcp_keepalive_intvl // default on linux: 30 + 8 * 30 // default on osx: 30 + 8 * 75 - tcpc.SetKeepAlive(true) - tcpc.SetKeepAlivePeriod(30 * time.Second) - return tcpc, nil + kac.SetKeepAlive(true) + kac.SetKeepAlivePeriod(30 * time.Second) + return c, nil } // A tlsKeepaliveListener implements a network listener (net.Listener) for TLS connections. @@ -75,12 +77,12 @@ func (l *tlsKeepaliveListener) Accept() (c net.Conn, err error) { if err != nil { return } - tcpc := c.(*net.TCPConn) + kac := c.(keepAliveConn) // detection time: tcp_keepalive_time + tcp_keepalive_probes + tcp_keepalive_intvl // default on linux: 30 + 8 * 30 // default on osx: 30 + 8 * 75 - tcpc.SetKeepAlive(true) - tcpc.SetKeepAlivePeriod(30 * time.Second) + kac.SetKeepAlive(true) + kac.SetKeepAlivePeriod(30 * time.Second) c = tls.Server(c, l.config) return } diff --git a/vendor/src/github.com/coreos/etcd/pkg/transport/keepalive_listener_test.go b/vendor/src/github.com/coreos/etcd/pkg/transport/keepalive_listener_test.go index b8317dc..093ff57 100644 --- a/vendor/src/github.com/coreos/etcd/pkg/transport/keepalive_listener_test.go +++ b/vendor/src/github.com/coreos/etcd/pkg/transport/keepalive_listener_test.go @@ -16,6 +16,7 @@ package transport import ( "crypto/tls" + "net" "net/http" "os" "testing" @@ -25,7 +26,12 @@ import ( // that accepts connections. // TODO: verify the keepalive option is set correctly func TestNewKeepAliveListener(t *testing.T) { - ln, err := NewKeepAliveListener("127.0.0.1:0", "http", TLSInfo{}) + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("unexpected listen error: %v", err) + } + + ln, err = NewKeepAliveListener(ln, "http", TLSInfo{}) if err != nil { t.Fatalf("unexpected NewKeepAliveListener error: %v", err) } @@ -38,6 +44,7 @@ func TestNewKeepAliveListener(t *testing.T) { conn.Close() ln.Close() + ln, err = net.Listen("tcp", "127.0.0.1:0") // tls tmp, err := createTempFile([]byte("XXX")) if err != nil { @@ -46,7 +53,7 @@ func TestNewKeepAliveListener(t *testing.T) { defer os.Remove(tmp) tlsInfo := TLSInfo{CertFile: tmp, KeyFile: tmp} tlsInfo.parseFunc = fakeCertificateParserFunc(tls.Certificate{}, nil) - tlsln, err := NewKeepAliveListener("127.0.0.1:0", "https", tlsInfo) + tlsln, err := NewKeepAliveListener(ln, "https", tlsInfo) if err != nil { t.Fatalf("unexpected NewKeepAliveListener error: %v", err) } @@ -64,7 +71,12 @@ func TestNewKeepAliveListener(t *testing.T) { } func TestNewKeepAliveListenerTLSEmptyInfo(t *testing.T) { - _, err := NewListener("127.0.0.1:0", "https", TLSInfo{}) + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("unexpected listen error: %v", err) + } + + _, err = NewKeepAliveListener(ln, "https", TLSInfo{}) if err == nil { t.Errorf("err = nil, want not presented error") } diff --git a/vendor/src/github.com/coreos/etcd/pkg/transport/listener.go b/vendor/src/github.com/coreos/etcd/pkg/transport/listener.go index de7b9f0..56c77b4 100644 --- a/vendor/src/github.com/coreos/etcd/pkg/transport/listener.go +++ b/vendor/src/github.com/coreos/etcd/pkg/transport/listener.go @@ -26,7 +26,13 @@ import ( ) func NewListener(addr string, scheme string, info TLSInfo) (net.Listener, error) { - l, err := net.Listen("tcp", addr) + nettype := "tcp" + if scheme == "unix" { + // unix sockets via unix://laddr + nettype = scheme + } + + l, err := net.Listen(nettype, addr) if err != nil { return nil, err } diff --git a/vendor/src/github.com/coreos/etcd/pkg/transport/listener_test.go b/vendor/src/github.com/coreos/etcd/pkg/transport/listener_test.go index e2acbdd..48618b6 100644 --- a/vendor/src/github.com/coreos/etcd/pkg/transport/listener_test.go +++ b/vendor/src/github.com/coreos/etcd/pkg/transport/listener_test.go @@ -45,7 +45,7 @@ func fakeCertificateParserFunc(cert tls.Certificate, err error) func(certPEMBloc } // TestNewListenerTLSInfo tests that NewListener with valid TLSInfo returns -// a TLS listerner that accepts TLS connections. +// a TLS listener that accepts TLS connections. func TestNewListenerTLSInfo(t *testing.T) { tmp, err := createTempFile([]byte("XXX")) if err != nil { @@ -241,3 +241,11 @@ func TestTLSInfoConfigFuncs(t *testing.T) { } } } + +func TestNewListenerUnixSocket(t *testing.T) { + l, err := NewListener("testsocket", "unix", TLSInfo{}) + if err != nil { + t.Errorf("error listening on unix socket (%v)", err) + } + l.Close() +} diff --git a/vendor/src/github.com/coreos/etcd/pkg/transport/timeout_transport.go b/vendor/src/github.com/coreos/etcd/pkg/transport/timeout_transport.go index 365a083..e894c6e 100644 --- a/vendor/src/github.com/coreos/etcd/pkg/transport/timeout_transport.go +++ b/vendor/src/github.com/coreos/etcd/pkg/transport/timeout_transport.go @@ -23,14 +23,19 @@ import ( // NewTimeoutTransport returns a transport created using the given TLS info. // If read/write on the created connection blocks longer than its time limit, // it will return timeout error. +// If read/write timeout is set, transport will not be able to reuse connection. func NewTimeoutTransport(info TLSInfo, dialtimeoutd, rdtimeoutd, wtimeoutd time.Duration) (*http.Transport, error) { tr, err := NewTransport(info, dialtimeoutd) if err != nil { return nil, err } - // the timeouted connection will tiemout soon after it is idle. - // it should not be put back to http transport as an idle connection for future usage. - tr.MaxIdleConnsPerHost = -1 + + if rdtimeoutd != 0 || wtimeoutd != 0 { + // the timed out connection will timeout soon after it is idle. + // it should not be put back to http transport as an idle connection for future usage. + tr.MaxIdleConnsPerHost = -1 + } + tr.Dial = (&rwTimeoutDialer{ Dialer: net.Dialer{ Timeout: dialtimeoutd, diff --git a/vendor/src/github.com/coreos/etcd/pkg/types/urlsmap.go b/vendor/src/github.com/coreos/etcd/pkg/types/urlsmap.go index f7d5c1c..4fe9218 100644 --- a/vendor/src/github.com/coreos/etcd/pkg/types/urlsmap.go +++ b/vendor/src/github.com/coreos/etcd/pkg/types/urlsmap.go @@ -20,6 +20,7 @@ import ( "strings" ) +// URLsMap is a map from a name to its URLs. type URLsMap map[string]URLs // NewURLsMap returns a URLsMap instantiated from the given string, @@ -39,9 +40,9 @@ func NewURLsMap(s string) (URLsMap, error) { return cl, nil } -// String returns NameURLPairs into discovery-formatted name-to-URLs sorted by name. +// String turns URLsMap into discovery-formatted name-to-URLs sorted by name. func (c URLsMap) String() string { - pairs := make([]string, 0) + var pairs []string for name, urls := range c { for _, url := range urls { pairs = append(pairs, fmt.Sprintf("%s=%s", name, url.String())) @@ -54,7 +55,7 @@ func (c URLsMap) String() string { // URLs returns a list of all URLs. // The returned list is sorted in ascending lexicographical order. func (c URLsMap) URLs() []string { - urls := make([]string, 0) + var urls []string for _, us := range c { for _, u := range us { urls = append(urls, u.String()) @@ -64,6 +65,7 @@ func (c URLsMap) URLs() []string { return urls } +// Len returns the size of URLsMap. func (c URLsMap) Len() int { return len(c) } diff --git a/vendor/src/github.com/mickep76/etcdmap/etcdmap.go b/vendor/src/github.com/mickep76/etcdmap/etcdmap.go index a3eaa4d..0c8c290 100644 --- a/vendor/src/github.com/mickep76/etcdmap/etcdmap.go +++ b/vendor/src/github.com/mickep76/etcdmap/etcdmap.go @@ -162,8 +162,8 @@ func JSON(root *client.Node) ([]byte, error) { } // JSON returns an etcd directory as JSON []byte. -func ArrayJSON(root *client.Node) ([]byte, error) { - j, err := json.Marshal(Array(root)) +func ArrayJSON(root *client.Node, dirName string) ([]byte, error) { + j, err := json.Marshal(Array(root, dirName)) if err != nil { return []byte{}, err } @@ -182,8 +182,8 @@ func JSONIndent(root *client.Node, indent string) ([]byte, error) { } // JSONIndent returns an etcd directory as indented JSON []byte. -func ArrayJSONIndent(root *client.Node, indent string) ([]byte, error) { - j, err := json.MarshalIndent(Array(root), "", indent) +func ArrayJSONIndent(root *client.Node, dirName string, indent string) ([]byte, error) { + j, err := json.MarshalIndent(Array(root, dirName), "", indent) if err != nil { return []byte{}, err } @@ -209,16 +209,21 @@ func Map(root *client.Node) map[string]interface{} { } // Array returns a []interface{} including the directory name inside each entry from a etcd directory. -func Array(root *client.Node) []interface{} { +func Array(root *client.Node, dirName string) []interface{} { v := []interface{}{} + if dirName == "" { + dirName = "dir" + } + for _, n := range root.Nodes { keys := strings.Split(n.Key, "/") k := keys[len(keys)-1] if n.Dir { m := make(map[string]interface{}) m = Map(n) - m["dir_name"] = k + m[dirName] = k + v = append(v, m) } } return v diff --git a/vendor/src/github.com/mickep76/iodatafmt/iodatafmt.go b/vendor/src/github.com/mickep76/iodatafmt/iodatafmt.go index ca7d74b..a56dc7d 100644 --- a/vendor/src/github.com/mickep76/iodatafmt/iodatafmt.go +++ b/vendor/src/github.com/mickep76/iodatafmt/iodatafmt.go @@ -89,7 +89,7 @@ func Marshal(d interface{}, f DataFmt) ([]byte, error) { } return b.Bytes(), nil case JSON: - b, err := json.MarshalIndent(&d, "", " ") + b, err := json.MarshalIndent(&d, "", " ") if err != nil { return nil, err } diff --git a/vendor/src/golang.org/x/net/context/ctxhttp/cancelreq.go b/vendor/src/golang.org/x/net/context/ctxhttp/cancelreq.go index 48610e3..e3170e3 100644 --- a/vendor/src/golang.org/x/net/context/ctxhttp/cancelreq.go +++ b/vendor/src/golang.org/x/net/context/ctxhttp/cancelreq.go @@ -9,6 +9,7 @@ package ctxhttp import "net/http" func canceler(client *http.Client, req *http.Request) func() { + // TODO(djd): Respect any existing value of req.Cancel. ch := make(chan struct{}) req.Cancel = ch diff --git a/vendor/src/golang.org/x/net/context/ctxhttp/ctxhttp.go b/vendor/src/golang.org/x/net/context/ctxhttp/ctxhttp.go index 504dd63..62620d4 100644 --- a/vendor/src/golang.org/x/net/context/ctxhttp/ctxhttp.go +++ b/vendor/src/golang.org/x/net/context/ctxhttp/ctxhttp.go @@ -14,6 +14,14 @@ import ( "golang.org/x/net/context" ) +func nop() {} + +var ( + testHookContextDoneBeforeHeaders = nop + testHookDoReturned = nop + testHookDidBodyClose = nop +) + // Do sends an HTTP request with the provided http.Client and returns an HTTP response. // If the client is nil, http.DefaultClient is used. // If the context is canceled or times out, ctx.Err() will be returned. @@ -33,16 +41,44 @@ func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Resp go func() { resp, err := client.Do(req) + testHookDoReturned() result <- responseAndError{resp, err} }() + var resp *http.Response + select { case <-ctx.Done(): + testHookContextDoneBeforeHeaders() cancel() + // Clean up after the goroutine calling client.Do: + go func() { + if r := <-result; r.resp != nil { + testHookDidBodyClose() + r.resp.Body.Close() + } + }() return nil, ctx.Err() case r := <-result: - return r.resp, r.err + var err error + resp, err = r.resp, r.err + if err != nil { + return resp, err + } } + + c := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + cancel() + case <-c: + // The response's Body is closed. + } + }() + resp.Body = ¬ifyingReader{resp.Body, c} + + return resp, nil } // Get issues a GET request via the Do function. @@ -77,3 +113,28 @@ func Post(ctx context.Context, client *http.Client, url string, bodyType string, func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) { return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) } + +// notifyingReader is an io.ReadCloser that closes the notify channel after +// Close is called or a Read fails on the underlying ReadCloser. +type notifyingReader struct { + io.ReadCloser + notify chan<- struct{} +} + +func (r *notifyingReader) Read(p []byte) (int, error) { + n, err := r.ReadCloser.Read(p) + if err != nil && r.notify != nil { + close(r.notify) + r.notify = nil + } + return n, err +} + +func (r *notifyingReader) Close() error { + err := r.ReadCloser.Close() + if r.notify != nil { + close(r.notify) + r.notify = nil + } + return err +} diff --git a/vendor/src/golang.org/x/net/context/ctxhttp/ctxhttp_test.go b/vendor/src/golang.org/x/net/context/ctxhttp/ctxhttp_test.go index 47b53d7..77c25ba 100644 --- a/vendor/src/golang.org/x/net/context/ctxhttp/ctxhttp_test.go +++ b/vendor/src/golang.org/x/net/context/ctxhttp/ctxhttp_test.go @@ -2,12 +2,16 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// +build !plan9 + package ctxhttp import ( "io/ioutil" + "net" "net/http" "net/http/httptest" + "sync" "testing" "time" @@ -27,6 +31,7 @@ func TestNoTimeout(t *testing.T) { t.Fatalf("error received from client: %v %v", err, resp) } } + func TestCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) go func() { @@ -59,6 +64,44 @@ func TestCancelAfterRequest(t *testing.T) { } } +func TestCancelAfterHangingRequest(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.(http.Flusher).Flush() + <-w.(http.CloseNotifier).CloseNotify() + }) + + serv := httptest.NewServer(handler) + defer serv.Close() + + ctx, cancel := context.WithCancel(context.Background()) + resp, err := Get(ctx, nil, serv.URL) + if err != nil { + t.Fatalf("unexpected error in Get: %v", err) + } + + // Cancel befer reading the body. + // Reading Request.Body should fail, since the request was + // canceled before anything was written. + cancel() + + done := make(chan struct{}) + + go func() { + b, err := ioutil.ReadAll(resp.Body) + if len(b) != 0 || err == nil { + t.Errorf(`Read got (%q, %v); want ("", error)`, b, err) + } + close(done) + }() + + select { + case <-time.After(1 * time.Second): + t.Errorf("Test timed out") + case <-done: + } +} + func doRequest(ctx context.Context) (*http.Response, error) { var okHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(requestDuration) @@ -70,3 +113,64 @@ func doRequest(ctx context.Context) (*http.Response, error) { return Get(ctx, nil, serv.URL) } + +// golang.org/issue/14065 +func TestClosesResponseBodyOnCancel(t *testing.T) { + defer func() { testHookContextDoneBeforeHeaders = nop }() + defer func() { testHookDoReturned = nop }() + defer func() { testHookDidBodyClose = nop }() + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + + // closed when Do enters select case <-ctx.Done() + enteredDonePath := make(chan struct{}) + + testHookContextDoneBeforeHeaders = func() { + close(enteredDonePath) + } + + testHookDoReturned = func() { + // We now have the result (the Flush'd headers) at least, + // so we can cancel the request. + cancel() + + // But block the client.Do goroutine from sending + // until Do enters into the <-ctx.Done() path, since + // otherwise if both channels are readable, select + // picks a random one. + <-enteredDonePath + } + + sawBodyClose := make(chan struct{}) + testHookDidBodyClose = func() { close(sawBodyClose) } + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + c := &http.Client{Transport: tr} + req, _ := http.NewRequest("GET", ts.URL, nil) + _, doErr := Do(ctx, c, req) + + select { + case <-sawBodyClose: + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for body to close") + } + + if doErr != ctx.Err() { + t.Errorf("Do error = %v; want %v", doErr, ctx.Err()) + } +} + +type noteCloseConn struct { + net.Conn + onceClose sync.Once + closefn func() +} + +func (c *noteCloseConn) Close() error { + c.onceClose.Do(c.closefn) + return c.Conn.Close() +}