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
This commit is contained in:
Daniel Moran 2021-04-14 09:31:21 -04:00 committed by GitHub
parent 8c062cacf0
commit ca8a5c5364
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1307 additions and 3 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
api/types.gen.go linguist-generated=true
api/client.gen.go linguist-generated=true

View File

@ -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

View File

@ -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)
},
},
},
}

View File

@ -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"

3
go.mod
View File

@ -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
)

84
go.sum
View File

@ -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=

113
internal/api/api.yml Normal file
View File

@ -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

263
internal/api/client.gen.go Normal file
View File

@ -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
}

4
internal/api/gen.go Normal file
View File

@ -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

68
internal/api/types.gen.go Normal file
View File

@ -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"`
}

21
internal/api/types.go Normal file
View File

@ -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)
}

15
internal/cli.go Normal file
View File

@ -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
}

132
internal/config/config.go Normal file
View File

@ -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
}

262
internal/config/local.go Normal file
View File

@ -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)
}

43
internal/ping.go Normal file
View File

@ -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
}

144
internal/ping_test.go Normal file
View File

@ -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())
}

View File

@ -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"
)