From ca8a5c5364265dcf81909fa7787c29a9fb96eb63 Mon Sep 17 00:00:00 2001 From: Daniel Moran Date: Wed, 14 Apr 2021 09:31:21 -0400 Subject: [PATCH] feat: implement ping command (#31) * build: use deepmap/oapi-codegen to generate an HTTP client * feat: add global CLI options * feat: load local config to find host and token * feat: implement ping command * test: add unit tests for ping command --- .gitattributes | 2 + Makefile | 13 +- cmd/influx/main.go | 136 +++++++++++++++++++ etc/checkfmt.sh | 6 + go.mod | 3 + go.sum | 84 +++++++++++- internal/api/api.yml | 113 ++++++++++++++++ internal/api/client.gen.go | 263 +++++++++++++++++++++++++++++++++++++ internal/api/gen.go | 4 + internal/api/types.gen.go | 68 ++++++++++ internal/api/types.go | 21 +++ internal/cli.go | 15 +++ internal/config/config.go | 132 +++++++++++++++++++ internal/config/local.go | 262 ++++++++++++++++++++++++++++++++++++ internal/ping.go | 43 ++++++ internal/ping_test.go | 144 ++++++++++++++++++++ tools.go | 1 + 17 files changed, 1307 insertions(+), 3 deletions(-) create mode 100644 .gitattributes create mode 100644 internal/api/api.yml create mode 100644 internal/api/client.gen.go create mode 100644 internal/api/gen.go create mode 100644 internal/api/types.gen.go create mode 100644 internal/api/types.go create mode 100644 internal/cli.go create mode 100644 internal/config/config.go create mode 100644 internal/config/local.go create mode 100644 internal/ping.go create mode 100644 internal/ping_test.go diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0cc03bc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +api/types.gen.go linguist-generated=true +api/client.gen.go linguist-generated=true diff --git a/Makefile b/Makefile index a3eed03..88f2e1f 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,14 @@ export GO_TEST=go test GO_TEST_PATHS=./... ### Build / dependency management +internal/api/types.gen.go: internal/api/api.yml internal/api/gen.go + go generate ./api + +internal/api/client.gen.go: internal/api/api.yml internal/api/gen.go + go generate ./api + +openapi: internal/api/types.gen.go internal/api/client.gen.go + fmt: $(SOURCES_NO_VENDOR) gofmt -w -s $^ go run github.com/daixiang0/gci -w $^ @@ -38,11 +46,12 @@ influx: bin/$(GOOS)/influx vendor: go.mod go.sum go mod vendor -build: fmt influx +build: openapi fmt influx clean: $(RM) -r bin $(RM) -r vendor + $(RM) internal/api/types.gen.go internal/api/client.gen.go ### Linters checkfmt: @@ -65,4 +74,4 @@ test-race: $(GO_TEST) -v -race -count=1 $(GO_TEST_PATHS) ### List of all targets that don't produce a file -.PHONY: influx fmt build checkfmt checktidy staticcheck vet test test-race +.PHONY: influx openapi fmt build checkfmt checktidy staticcheck vet test test-race diff --git a/cmd/influx/main.go b/cmd/influx/main.go index 9d5049b..8ead68e 100644 --- a/cmd/influx/main.go +++ b/cmd/influx/main.go @@ -1,19 +1,103 @@ package main import ( + "context" + "crypto/tls" "fmt" + "net/http" "os" + "runtime" "time" + "github.com/influxdata/influx-cli/v2/internal" + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/config" "github.com/urfave/cli/v2" ) +// Fields set via ldflags at build time. var ( version = "dev" commit = "none" date = "" ) +var ( + tokenFlag = "token" + hostFlag = "host" + skipVerifyFlag = "skip-verify" + traceIdFlag = "trace-debug-id" + configPathFlag = "config-path" + configNameFlag = "active-config" +) + +// loadConfig reads CLI configs from disk, returning the config with the +// name specified over the CLI (or default if no name was given). +func loadConfig(ctx *cli.Context) (config.Config, error) { + configs := config.GetConfigsOrDefault(ctx.String(configPathFlag)) + configName := ctx.String(configNameFlag) + if configName != "" { + if err := configs.Switch(configName); err != nil { + return config.Config{}, err + } + } + return configs.Active(), nil +} + +// newApiClient returns an API client configured to communicate with a remote InfluxDB instance over HTTP. +// Client parameters are pulled from the CLI context. +func newApiClient(ctx *cli.Context, injectToken bool) (api.ClientWithResponsesInterface, error) { + cfg, err := loadConfig(ctx) + if err != nil { + return nil, err + } + if ctx.IsSet(tokenFlag) { + cfg.Token = ctx.String(tokenFlag) + } + if ctx.IsSet(hostFlag) { + cfg.Host = ctx.String(hostFlag) + } + + clientTransport := http.DefaultTransport.(*http.Transport) + clientTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: ctx.Bool(skipVerifyFlag)} + + client := &http.Client{Transport: clientTransport} + userAgent := fmt.Sprintf("influx/%s (%s) Sha/%s Date/%s", version, runtime.GOOS, commit, date) + + opts := []api.ClientOption{ + api.WithHTTPClient(client), + api.WithRequestEditorFn(func(_ context.Context, req *http.Request) error { + req.Header.Set("User-Agent", userAgent) + return nil + }), + } + if injectToken { + authHeader := fmt.Sprintf("Token %s", cfg.Token) + opts = append(opts, api.WithRequestEditorFn(func(_ context.Context, req *http.Request) error { + req.Header.Set("Authorization", authHeader) + return nil + })) + } + + return api.NewClientWithResponses(cfg.Host, opts...) +} + +// newCli builds a CLI core that reads from stdin, writes to stdout/stderr, and +// optionally tracks a trace ID specified over the CLI. +func newCli(ctx *cli.Context) *internal.CLI { + var traceId *api.TraceSpan + if ctx.IsSet(traceIdFlag) { + tid := api.TraceSpan(ctx.String(traceIdFlag)) + traceId = &tid + } + return &internal.CLI{ + Stdin: ctx.App.Reader, + Stdout: ctx.App.Writer, + Stderr: ctx.App.ErrWriter, + TraceId: traceId, + } +} + func main() { if len(date) == 0 { date = time.Now().UTC().Format(time.RFC3339) @@ -21,6 +105,46 @@ func main() { cli.VersionFlag = nil + // NOTE: urfave/cli has dedicated support for global flags, but it only parses those flags + // if they're specified before any command names. This is incompatible with the old influx + // CLI, which repeatedly registered common flags on every "leaf" command, forcing the flags + // to be specified after _all_ command names were given. + // + // We replicate the pattern from the old CLI so existing scripts and docs stay valid. + commonFlags := []cli.Flag{ + &cli.StringFlag{ + Name: tokenFlag, + Usage: "Authentication token", + Aliases: []string{"t"}, + EnvVars: []string{"INFLUX_TOKEN"}, + }, + &cli.StringFlag{ + Name: hostFlag, + Usage: "HTTP address of InfluxDB", + EnvVars: []string{"INFLUX_HOST"}, + }, + &cli.BoolFlag{ + Name: skipVerifyFlag, + Usage: "Skip TLS certificate chain and host name verification", + }, + &cli.StringFlag{ + Name: configPathFlag, + Usage: "Path to the influx CLI configurations", + EnvVars: []string{"INFLUX_CLI_CONFIGS_PATH"}, + }, + &cli.StringFlag{ + Name: configNameFlag, + Usage: "Config name to use for command", + Aliases: []string{"c"}, + EnvVars: []string{"INFLUX_ACTIVE_CONFIG"}, + }, + &cli.StringFlag{ + Name: traceIdFlag, + Hidden: true, + EnvVars: []string{"INFLUX_TRACE_DEBUG_ID"}, + }, + } + app := cli.App{ Name: "influx", Usage: "Influx Client", @@ -34,6 +158,18 @@ func main() { return nil }, }, + { + Name: "ping", + Usage: "Check the InfluxDB /health endpoint", + Flags: commonFlags, + Action: func(ctx *cli.Context) error { + client, err := newApiClient(ctx, false) + if err != nil { + return err + } + return newCli(ctx).Ping(ctx.Context, client) + }, + }, }, } diff --git a/etc/checkfmt.sh b/etc/checkfmt.sh index 81bd10f..e42ac71 100755 --- a/etc/checkfmt.sh +++ b/etc/checkfmt.sh @@ -7,6 +7,12 @@ for file in $(go list -f '{{$dir := .Dir}}{{range .GoFiles}}{{printf "%s/%s\n" $ if ! grep -Exq '^// Code generated .* DO NOT EDIT\.$' $file; then FMT_OUT="$(gofmt -l -d -e $file)" # gofmt exits 0 regardless of whether it's formatted. GCI_OUT="$(go run github.com/daixiang0/gci -d $file)" + + # Work around annoying output of gci + if [[ "$GCI_OUT" = "skip file $file since no import" ]]; then + GCI_OUT="" + fi + if [[ -n "$FMT_OUT" || -n "$GCI_OUT" ]]; then HAS_FMT_ERR=1 echo "Not formatted: $file" diff --git a/go.mod b/go.mod index f65db2a..7f5b5f1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,10 @@ module github.com/influxdata/influx-cli/v2 go 1.16 require ( + github.com/BurntSushi/toml v0.3.1 github.com/daixiang0/gci v0.2.8 + github.com/deepmap/oapi-codegen v1.6.0 + github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 honnef.co/go/tools v0.1.3 ) diff --git a/go.sum b/go.sum index 49ffde4..b8eed4b 100644 --- a/go.sum +++ b/go.sum @@ -2,37 +2,112 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/daixiang0/gci v0.2.8 h1:1mrIGMBQsBu0P7j7m1M8Lb+ZeZxsZL+jyGX4YoMJJpg= github.com/daixiang0/gci v0.2.8/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/getkin/kin-openapi v0.53.0 h1:7WzP+MZRRe7YQz2Kc74Ley3dukJmXDvifVbElGmQfoA= +github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/labstack/echo/v4 v4.2.1 h1:LF5Iq7t/jrtUuSutNuiEWtB5eiHfZ5gSe2pcu5exjQw= +github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= @@ -41,6 +116,13 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= diff --git a/internal/api/api.yml b/internal/api/api.yml new file mode 100644 index 0000000..01da477 --- /dev/null +++ b/internal/api/api.yml @@ -0,0 +1,113 @@ +openapi: 3.0.0 +info: + title: Influx API Service + version: 2.0.0 +servers: + - url: /api/v2 +paths: + /health: + servers: + - url: / + get: + operationId: GetHealth + tags: + - Health + summary: Get the health of an instance + parameters: + - $ref: '#/components/parameters/TraceSpan' + responses: + '200': + description: The instance is healthy + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheck' + '503': + description: The instance is unhealthy + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheck' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + parameters: + TraceSpan: + in: header + name: Zap-Trace-Span + description: OpenTracing span context + example: + trace_id: '1' + span_id: '1' + baggage: + key: value + required: false + schema: + type: string + schemas: + HealthCheck: + type: object + required: + - name + - status + properties: + name: + type: string + message: + type: string + checks: + type: array + items: + $ref: '#/components/schemas/HealthCheck' + status: + $ref: '#/components/schemas/HealthCheckStatus' + version: + type: string + commit: + type: string + HealthCheckStatus: + type: string + readOnly: true + enum: + - pass + - fail + Error: + properties: + code: + $ref: '#/components/schemas/ErrorCode' + message: + readOnly: true + description: message is a human-readable message. + type: string + op: + readOnly: true + description: op describes the logical code operation during error. Useful for debugging. + type: string + err: + readOnly: true + description: err is a stack of errors that occurred during processing of the request. Useful for debugging. + type: string + required: + - code + - message + ErrorCode: + description: machine-readable error code. + readOnly: true + type: string + enum: + - internal error + - not found + - conflict + - invalid + - unprocessable entity + - empty value + - unavailable + - forbidden + - too many requests + - unauthorized + - method not allowed + - request too large diff --git a/internal/api/client.gen.go b/internal/api/client.gen.go new file mode 100644 index 0000000..5824927 --- /dev/null +++ b/internal/api/client.gen.go @@ -0,0 +1,263 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT. +package api + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/deepmap/oapi-codegen/pkg/runtime" +) + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetHealth request + GetHealth(ctx context.Context, params *GetHealthParams, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetHealth(ctx context.Context, params *GetHealthParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetHealthRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewGetHealthRequest generates requests for GetHealth +func NewGetHealthRequest(server string, params *GetHealthParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/health") + if operationPath[0] == '/' { + operationPath = operationPath[1:] + } + operationURL := url.URL{ + Path: operationPath, + } + + queryURL := serverURL.ResolveReference(&operationURL) + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + if params.ZapTraceSpan != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithLocation("simple", false, "Zap-Trace-Span", runtime.ParamLocationHeader, *params.ZapTraceSpan) + if err != nil { + return nil, err + } + + req.Header.Set("Zap-Trace-Span", headerParam0) + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetHealth request + GetHealthWithResponse(ctx context.Context, params *GetHealthParams, reqEditors ...RequestEditorFn) (*GetHealthResponse, error) +} + +type GetHealthResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *HealthCheck + JSON503 *HealthCheck + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r GetHealthResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetHealthResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetHealthWithResponse request returning *GetHealthResponse +func (c *ClientWithResponses) GetHealthWithResponse(ctx context.Context, params *GetHealthParams, reqEditors ...RequestEditorFn) (*GetHealthResponse, error) { + rsp, err := c.GetHealth(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetHealthResponse(rsp) +} + +// ParseGetHealthResponse parses an HTTP response from a GetHealthWithResponse call +func ParseGetHealthResponse(rsp *http.Response) (*GetHealthResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &GetHealthResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest HealthCheck + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest HealthCheck + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} diff --git a/internal/api/gen.go b/internal/api/gen.go new file mode 100644 index 0000000..2781bff --- /dev/null +++ b/internal/api/gen.go @@ -0,0 +1,4 @@ +package api + +//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -package api -generate types -o types.gen.go api.yml +//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -package api -generate client -o client.gen.go api.yml diff --git a/internal/api/types.gen.go b/internal/api/types.gen.go new file mode 100644 index 0000000..402714f --- /dev/null +++ b/internal/api/types.gen.go @@ -0,0 +1,68 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT. +package api + +// Error defines model for Error. +type Error struct { + + // machine-readable error code. + Code ErrorCode `json:"code"` + + // err is a stack of errors that occurred during processing of the request. Useful for debugging. + Err *string `json:"err,omitempty"` + + // message is a human-readable message. + Message string `json:"message"` + + // op describes the logical code operation during error. Useful for debugging. + Op *string `json:"op,omitempty"` +} + +// machine-readable error code. +type ErrorCode string + +// List of ErrorCode +const ( + ErrorCode_conflict ErrorCode = "conflict" + ErrorCode_empty_value ErrorCode = "empty value" + ErrorCode_forbidden ErrorCode = "forbidden" + ErrorCode_internal_error ErrorCode = "internal error" + ErrorCode_invalid ErrorCode = "invalid" + ErrorCode_method_not_allowed ErrorCode = "method not allowed" + ErrorCode_not_found ErrorCode = "not found" + ErrorCode_request_too_large ErrorCode = "request too large" + ErrorCode_too_many_requests ErrorCode = "too many requests" + ErrorCode_unauthorized ErrorCode = "unauthorized" + ErrorCode_unavailable ErrorCode = "unavailable" + ErrorCode_unprocessable_entity ErrorCode = "unprocessable entity" +) + +// HealthCheck defines model for HealthCheck. +type HealthCheck struct { + Checks *[]HealthCheck `json:"checks,omitempty"` + Commit *string `json:"commit,omitempty"` + Message *string `json:"message,omitempty"` + Name string `json:"name"` + Status HealthCheckStatus `json:"status"` + Version *string `json:"version,omitempty"` +} + +// HealthCheckStatus defines model for HealthCheckStatus. +type HealthCheckStatus string + +// List of HealthCheckStatus +const ( + HealthCheckStatus_fail HealthCheckStatus = "fail" + HealthCheckStatus_pass HealthCheckStatus = "pass" +) + +// TraceSpan defines model for TraceSpan. +type TraceSpan string + +// GetHealthParams defines parameters for GetHealth. +type GetHealthParams struct { + + // OpenTracing span context + ZapTraceSpan *TraceSpan `json:"Zap-Trace-Span,omitempty"` +} diff --git a/internal/api/types.go b/internal/api/types.go new file mode 100644 index 0000000..794ca30 --- /dev/null +++ b/internal/api/types.go @@ -0,0 +1,21 @@ +package api + +import ( + "fmt" + "strings" +) + +func (e *Error) Error() string { + if e.Message != "" && e.Err != nil { + var b strings.Builder + b.WriteString(e.Message) + b.WriteString(": ") + b.WriteString(*e.Err) + return b.String() + } else if e.Message != "" { + return e.Message + } else if e.Err != nil { + return *e.Err + } + return fmt.Sprintf("<%s>", e.Code) +} diff --git a/internal/cli.go b/internal/cli.go new file mode 100644 index 0000000..27d95c0 --- /dev/null +++ b/internal/cli.go @@ -0,0 +1,15 @@ +package internal + +import ( + "io" + + "github.com/influxdata/influx-cli/v2/internal/api" +) + +type CLI struct { + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + + TraceId *api.TraceSpan +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..46aceac --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,132 @@ +package config + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + + "github.com/influxdata/influx-cli/v2/internal/api" +) + +// Config store the crendentials of influxdb host and token. +type Config struct { + Name string `toml:"-" json:"-"` + Host string `toml:"url" json:"url"` + // Token is base64 encoded sequence. + Token string `toml:"token" json:"token"` + Org string `toml:"org" json:"org"` + Active bool `toml:"active,omitempty" json:"active,omitempty"` + PreviousActive bool `toml:"previous,omitempty" json:"previous,omitempty"` +} + +// DefaultConfig is default config without token +var DefaultConfig = Config{ + Name: "default", + Host: "http://localhost:8086", + Active: true, +} + +// DefaultPath computes the path where CLI configs will be stored if not overridden. +func DefaultPath() (string, error) { + var dir string + // By default, store meta and data files in current users home directory + u, err := user.Current() + if err == nil { + dir = u.HomeDir + } else if home := os.Getenv("HOME"); home != "" { + dir = home + } else { + wd, err := os.Getwd() + if err != nil { + return "", err + } + dir = wd + } + dir = filepath.Join(dir, ".influxdbv2", "config") + + return dir, nil +} + +// Service is the service to list and write configs. +type Service interface { + CreateConfig(Config) (Config, error) + DeleteConfig(name string) (Config, error) + UpdateConfig(Config) (Config, error) + SwitchActive(name string) (Config, error) + ListConfigs() (Configs, error) +} + +// Configs is map of configs indexed by name. +type Configs map[string]Config + +func GetConfigsOrDefault(path string) Configs { + r, err := os.Open(path) + if err != nil { + return Configs{ + DefaultConfig.Name: DefaultConfig, + } + } + defer r.Close() + + cfgs, err := NewLocalConfigService(path).ListConfigs() + if err != nil { + return Configs{ + DefaultConfig.Name: DefaultConfig, + } + } + + return cfgs +} + +var badNames = map[string]bool{ + "-": false, + "list": false, + "update": false, + "set": false, + "delete": false, + "switch": false, + "create": false, +} + +func blockBadName(cfgs Configs) error { + for n := range cfgs { + if _, ok := badNames[n]; ok { + return &api.Error{ + Code: api.ErrorCode_invalid, + Message: fmt.Sprintf("%q is not a valid config name", n), + } + } + } + return nil +} + +// Switch to another config. +func (cfgs Configs) Switch(name string) error { + if _, ok := cfgs[name]; !ok { + return &api.Error{ + Code: api.ErrorCode_not_found, + Message: fmt.Sprintf("config %q is not found", name), + } + } + for k, v := range cfgs { + v.PreviousActive = v.Active && (k != name) + v.Active = k == name + cfgs[k] = v + } + return nil +} + +func (cfgs Configs) Active() Config { + for _, cfg := range cfgs { + if cfg.Active { + return cfg + } + } + if len(cfgs) > 0 { + for _, cfg := range cfgs { + return cfg + } + } + return DefaultConfig +} diff --git a/internal/config/local.go b/internal/config/local.go new file mode 100644 index 0000000..ccb27ec --- /dev/null +++ b/internal/config/local.go @@ -0,0 +1,262 @@ +package config + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/BurntSushi/toml" + "github.com/influxdata/influx-cli/v2/internal/api" +) + +// store is the embedded store of the Config service. +type store interface { + parsePreviousActive() (Config, error) + ListConfigs() (Configs, error) + writeConfigs(cfgs Configs) error +} + +// localConfigsSVC can write and parse configs from a local path. +type localConfigsSVC struct { + store +} + +// NewLocalConfigService creates a new service that can write and parse configs +// to/from a path on local disk. +func NewLocalConfigService(path string) Service { + return &localConfigsSVC{ioStore{Path: path}} +} + +// CreateConfig create new config. +func (svc localConfigsSVC) CreateConfig(cfg Config) (Config, error) { + if cfg.Name == "" { + return Config{}, &api.Error{ + Code: api.ErrorCode_invalid, + Message: "config name is empty", + } + } + cfgs, err := svc.ListConfigs() + if err != nil { + return Config{}, err + } + if _, ok := cfgs[cfg.Name]; ok { + return Config{}, &api.Error{ + Code: api.ErrorCode_conflict, + Message: fmt.Sprintf("config %q already exists", cfg.Name), + } + } + cfgs[cfg.Name] = cfg + if cfg.Active { + if err := cfgs.Switch(cfg.Name); err != nil { + return Config{}, err + } + } + + return cfgs[cfg.Name], svc.writeConfigs(cfgs) +} + +// DeleteConfig will delete a config. +func (svc localConfigsSVC) DeleteConfig(name string) (Config, error) { + cfgs, err := svc.ListConfigs() + if err != nil { + return Config{}, err + } + + p, ok := cfgs[name] + if !ok { + return Config{}, &api.Error{ + Code: api.ErrorCode_not_found, + Message: fmt.Sprintf("config %q is not found", name), + } + } + delete(cfgs, name) + + if p.Active && len(cfgs) > 0 { + for name, cfg := range cfgs { + cfg.Active = true + cfgs[name] = cfg + break + } + } + + return p, svc.writeConfigs(cfgs) +} + +// SwitchActive will active the config by name, if name is "-", active the previous one. +func (svc localConfigsSVC) SwitchActive(name string) (Config, error) { + var up Config + if name == "-" { + p0, err := svc.parsePreviousActive() + if err != nil { + return Config{}, err + } + up.Name = p0.Name + } else { + up.Name = name + } + up.Active = true + return svc.UpdateConfig(up) +} + +// UpdateConfig will update the config. +func (svc localConfigsSVC) UpdateConfig(up Config) (Config, error) { + cfgs, err := svc.ListConfigs() + if err != nil { + return Config{}, err + } + p0, ok := cfgs[up.Name] + if !ok { + return Config{}, &api.Error{ + Code: api.ErrorCode_not_found, + Message: fmt.Sprintf("config %q is not found", up.Name), + } + } + if up.Token != "" { + p0.Token = up.Token + } + if up.Host != "" { + p0.Host = up.Host + } + if up.Org != "" { + p0.Org = up.Org + } + + cfgs[up.Name] = p0 + if up.Active { + if err := cfgs.Switch(up.Name); err != nil { + return Config{}, err + } + } + + return cfgs[up.Name], svc.writeConfigs(cfgs) +} + +type baseRW struct { + r io.Reader + w io.Writer +} + +// ParseActiveConfig returns the active config from the reader. +func ParseActiveConfig(r io.Reader) (Config, error) { + return (baseRW{r: r}).parseActiveConfig(true) +} + +// parsePreviousActive return the previous active config from the reader +func (s baseRW) parsePreviousActive() (Config, error) { + return s.parseActiveConfig(false) +} + +// ListConfigs decodes configs from io readers +func (s baseRW) ListConfigs() (Configs, error) { + cfgs := make(Configs) + _, err := toml.DecodeReader(s.r, &cfgs) + for n, cfg := range cfgs { + cfg.Name = n + cfgs[n] = cfg + } + return cfgs, err +} + +func (s baseRW) writeConfigs(cfgs Configs) error { + if err := blockBadName(cfgs); err != nil { + return err + } + var b2 bytes.Buffer + if err := toml.NewEncoder(s.w).Encode(cfgs); err != nil { + return err + } + // a list cloud 2 clusters, commented out + s.w.Write([]byte("# \n")) + cfgs = map[string]Config{ + "us-central": {Host: "https://us-central1-1.gcp.cloud2.influxdata.com", Token: "XXX"}, + "us-west": {Host: "https://us-west-2-1.aws.cloud2.influxdata.com", Token: "XXX"}, + "eu-central": {Host: "https://eu-central-1-1.aws.cloud2.influxdata.com", Token: "XXX"}, + } + + if err := toml.NewEncoder(&b2).Encode(cfgs); err != nil { + return err + } + reader := bufio.NewReader(&b2) + for { + line, _, err := reader.ReadLine() + + if err == io.EOF { + break + } + s.w.Write([]byte("# " + string(line) + "\n")) + } + return nil +} + +func (s baseRW) parseActiveConfig(currentOrPrevious bool) (Config, error) { + previousText := "" + if !currentOrPrevious { + previousText = "previous " + } + cfgs, err := s.ListConfigs() + if err != nil { + return DefaultConfig, err + } + var activated Config + var hasActive bool + for _, cfg := range cfgs { + check := cfg.Active + if !currentOrPrevious { + check = cfg.PreviousActive + } + if check && !hasActive { + activated = cfg + hasActive = true + } else if check { + return DefaultConfig, &api.Error{ + Code: api.ErrorCode_conflict, + Message: fmt.Sprintf("more than one %s activated configs found", previousText), + } + } + } + if hasActive { + return activated, nil + } + return DefaultConfig, &api.Error{ + Code: api.ErrorCode_not_found, + Message: fmt.Sprintf("%s activated config is not found", previousText), + } +} + +type ioStore struct { + Path string +} + +// ListConfigs from the local path. +func (s ioStore) ListConfigs() (Configs, error) { + r, err := os.Open(s.Path) + if err != nil { + return make(Configs), nil + } + return (baseRW{r: r}).ListConfigs() +} + +// parsePreviousActive from the local path. +func (s ioStore) parsePreviousActive() (Config, error) { + r, err := os.Open(s.Path) + if err != nil { + return Config{}, nil + } + return (baseRW{r: r}).parsePreviousActive() +} + +// writeConfigs to the path. +func (s ioStore) writeConfigs(cfgs Configs) error { + if err := os.MkdirAll(filepath.Dir(s.Path), os.ModePerm); err != nil { + return err + } + var b1 bytes.Buffer + if err := (baseRW{w: &b1}).writeConfigs(cfgs); err != nil { + return err + } + return ioutil.WriteFile(s.Path, b1.Bytes(), 0600) +} diff --git a/internal/ping.go b/internal/ping.go new file mode 100644 index 0000000..8723c99 --- /dev/null +++ b/internal/ping.go @@ -0,0 +1,43 @@ +package internal + +import ( + "context" + "fmt" + + "github.com/influxdata/influx-cli/v2/internal/api" +) + +type Client interface { + GetHealthWithResponse(context.Context, *api.GetHealthParams, ...api.RequestEditorFn) (*api.GetHealthResponse, error) +} + +// Ping checks the health of a remote InfluxDB instance. +func (c *CLI) Ping(ctx context.Context, client Client) error { + resp, err := client.GetHealthWithResponse(ctx, &api.GetHealthParams{ZapTraceSpan: c.TraceId}) + if err != nil { + return fmt.Errorf("failed to make health check request: %w", err) + } + + var failureMessage string + if resp.JSON503 != nil { + if resp.JSON503.Message != nil { + failureMessage = *resp.JSON503.Message + } else { + failureMessage = "status 503" + } + } else if resp.JSONDefault != nil { + failureMessage = resp.JSONDefault.Error() + } else if resp.JSON200.Status != api.HealthCheckStatus_pass { + if resp.JSON200.Message != nil { + failureMessage = *resp.JSON200.Message + } else { + failureMessage = fmt.Sprintf("check %s failed", resp.JSON200.Name) + } + } + + if failureMessage != "" { + return fmt.Errorf("health check failed: %s", failureMessage) + } + c.Stdout.Write([]byte("OK\n")) + return nil +} diff --git a/internal/ping_test.go b/internal/ping_test.go new file mode 100644 index 0000000..8d66700 --- /dev/null +++ b/internal/ping_test.go @@ -0,0 +1,144 @@ +package internal_test + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/influxdata/influx-cli/v2/internal" + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/stretchr/testify/require" +) + +type testClient struct { + GetHealthFn func(context.Context, *api.GetHealthParams, ...api.RequestEditorFn) (*api.GetHealthResponse, error) +} + +func (tc *testClient) GetHealthWithResponse(ctx context.Context, p *api.GetHealthParams, fns ...api.RequestEditorFn) (*api.GetHealthResponse, error) { + return tc.GetHealthFn(ctx, p, fns...) +} + +func Test_PingSuccessNoTracing(t *testing.T) { + t.Parallel() + + client := &testClient{ + GetHealthFn: func(_ context.Context, p *api.GetHealthParams, _ ...api.RequestEditorFn) (*api.GetHealthResponse, error) { + require.Nil(t, p.ZapTraceSpan) + return &api.GetHealthResponse{ + JSON200: &api.HealthCheck{Name: "test", Status: api.HealthCheckStatus_pass}, + }, nil + }, + } + + out := &bytes.Buffer{} + cli := &internal.CLI{Stdout: out} + + require.NoError(t, cli.Ping(context.Background(), client)) + require.Equal(t, "OK\n", out.String()) +} + +func Test_PingSuccessWithTracing(t *testing.T) { + t.Parallel() + + traceId := api.TraceSpan("trace-id") + client := &testClient{ + GetHealthFn: func(_ context.Context, p *api.GetHealthParams, _ ...api.RequestEditorFn) (*api.GetHealthResponse, error) { + require.Equal(t, traceId, *p.ZapTraceSpan) + return &api.GetHealthResponse{ + JSON200: &api.HealthCheck{Name: "test", Status: api.HealthCheckStatus_pass}, + }, nil + }, + } + + out := &bytes.Buffer{} + cli := &internal.CLI{Stdout: out, TraceId: &traceId} + + require.NoError(t, cli.Ping(context.Background(), client)) + require.Equal(t, "OK\n", out.String()) +} + +func Test_PingFailedRequest(t *testing.T) { + t.Parallel() + + e := "the internet is down" + client := &testClient{ + GetHealthFn: func(context.Context, *api.GetHealthParams, ...api.RequestEditorFn) (*api.GetHealthResponse, error) { + return nil, errors.New(e) + }, + } + + out := &bytes.Buffer{} + cli := &internal.CLI{Stdout: out} + + err := cli.Ping(context.Background(), client) + require.Error(t, err) + require.Contains(t, err.Error(), e) + require.Empty(t, out.String()) +} + +func Test_PingFailedStatus(t *testing.T) { + t.Parallel() + + e := "I broke" + client := &testClient{ + GetHealthFn: func(context.Context, *api.GetHealthParams, ...api.RequestEditorFn) (*api.GetHealthResponse, error) { + return &api.GetHealthResponse{ + JSON503: &api.HealthCheck{Name: "test", Status: api.HealthCheckStatus_fail, Message: &e}, + }, nil + }, + } + + out := &bytes.Buffer{} + cli := &internal.CLI{Stdout: out} + + err := cli.Ping(context.Background(), client) + require.Error(t, err) + require.Contains(t, err.Error(), e) + require.Empty(t, out.String()) +} + +func Test_PingFailedUnhandledError(t *testing.T) { + t.Parallel() + + e := "something went boom" + client := &testClient{ + GetHealthFn: func(context.Context, *api.GetHealthParams, ...api.RequestEditorFn) (*api.GetHealthResponse, error) { + return &api.GetHealthResponse{ + JSONDefault: &api.Error{ + Code: api.ErrorCode_internal_error, + Message: e, + }, + }, nil + }, + } + + out := &bytes.Buffer{} + cli := &internal.CLI{Stdout: out} + + err := cli.Ping(context.Background(), client) + require.Error(t, err) + require.Contains(t, err.Error(), e) + require.Empty(t, out.String()) +} + +func Test_PingFailedCheck(t *testing.T) { + t.Parallel() + + e := "oops, forgot to set the status code" + client := &testClient{ + GetHealthFn: func(_ context.Context, p *api.GetHealthParams, _ ...api.RequestEditorFn) (*api.GetHealthResponse, error) { + return &api.GetHealthResponse{ + JSON200: &api.HealthCheck{Name: "test", Status: api.HealthCheckStatus_fail, Message: &e}, + }, nil + }, + } + + out := &bytes.Buffer{} + cli := &internal.CLI{Stdout: out} + + err := cli.Ping(context.Background(), client) + require.Error(t, err) + require.Contains(t, err.Error(), e) + require.Empty(t, out.String()) +} diff --git a/tools.go b/tools.go index ac3cdab..feda55a 100644 --- a/tools.go +++ b/tools.go @@ -11,5 +11,6 @@ package influxcli import ( _ "github.com/daixiang0/gci" + _ "github.com/deepmap/oapi-codegen/cmd/oapi-codegen" _ "honnef.co/go/tools/cmd/staticcheck" )