
* feat: add username and password login * fix: make sure cookie is not empty * chore: go mod tidy * fix: prevent local config from influencing tests * fix: small cleanup on error handling * fix: remove unnecessary trim
381 lines
11 KiB
Go
381 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/influxdata/influx-cli/v2/api"
|
|
"github.com/influxdata/influx-cli/v2/clients"
|
|
"github.com/influxdata/influx-cli/v2/clients/signin"
|
|
"github.com/influxdata/influx-cli/v2/config"
|
|
"github.com/influxdata/influx-cli/v2/pkg/cli/middleware"
|
|
"github.com/influxdata/influx-cli/v2/pkg/signals"
|
|
"github.com/influxdata/influx-cli/v2/pkg/stdio"
|
|
"github.com/urfave/cli"
|
|
)
|
|
|
|
const (
|
|
tokenFlagName = "token"
|
|
hostFlagName = "host"
|
|
skipVerifyFlagName = "skip-verify"
|
|
traceIdFlagName = "trace-debug-id"
|
|
extraHttpHeaderFlagName = "extra-http-header"
|
|
configPathFlagName = "configs-path"
|
|
configNameFlagName = "active-config"
|
|
httpDebugFlagName = "http-debug"
|
|
printJsonFlagName = "json"
|
|
hideHeadersFlagName = "hide-headers"
|
|
)
|
|
|
|
// newCli builds a CLI core that reads from stdin, writes to stdout/stderr, manages a local config store,
|
|
// and optionally tracks a trace ID specified over the CLI.
|
|
func newCli(ctx *cli.Context) (clients.CLI, error) {
|
|
configPath := ctx.String(configPathFlagName)
|
|
var err error
|
|
if configPath == "" {
|
|
configPath, err = config.DefaultPath()
|
|
if err != nil {
|
|
return clients.CLI{}, err
|
|
}
|
|
}
|
|
configSvc := config.NewLocalConfigService(configPath)
|
|
var activeConfig config.Config
|
|
if ctx.IsSet(configNameFlagName) {
|
|
if activeConfig, err = configSvc.SwitchActive(ctx.String(configNameFlagName)); err != nil {
|
|
return clients.CLI{}, err
|
|
}
|
|
} else if activeConfig, err = configSvc.Active(); err != nil {
|
|
return clients.CLI{}, err
|
|
}
|
|
|
|
return clients.CLI{
|
|
StdIO: stdio.TerminalStdio,
|
|
PrintAsJSON: ctx.Bool(printJsonFlagName),
|
|
HideTableHeaders: ctx.Bool(hideHeadersFlagName),
|
|
ActiveConfig: activeConfig,
|
|
ConfigService: configSvc,
|
|
}, nil
|
|
}
|
|
|
|
// newApiClient returns an API clients configured to communicate with a remote InfluxDB instance over HTTP.
|
|
// Client parameters are pulled from the CLI context.
|
|
func newApiClient(ctx *cli.Context, configSvc config.Service, injectToken bool) (*api.APIClient, error) {
|
|
cfg, err := configSvc.Active()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ctx.IsSet(tokenFlagName) {
|
|
cfg.Token = ctx.String(tokenFlagName)
|
|
}
|
|
if ctx.IsSet(hostFlagName) {
|
|
cfg.Host = ctx.String(hostFlagName)
|
|
}
|
|
|
|
configParams := api.ConfigParams{
|
|
UserAgent: fmt.Sprintf("influx/%s (%s) Sha/%s Date/%s", version, runtime.GOOS, commit, date),
|
|
AllowInsecureTLS: ctx.Bool(skipVerifyFlagName),
|
|
Debug: ctx.Bool(httpDebugFlagName),
|
|
}
|
|
|
|
parsedHost, err := url.Parse(cfg.Host)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("host URL %q is invalid: %w", cfg.Host, err)
|
|
}
|
|
configParams.Host = parsedHost
|
|
|
|
if injectToken && cfg.Token != "" {
|
|
configParams.Token = &cfg.Token
|
|
} else if cfg.Cookie != "" {
|
|
cookie, err := signin.GetCookie(getContext(ctx), configParams, cfg.Cookie)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating session: %w", err)
|
|
}
|
|
configParams.Cookie = &cookie
|
|
}
|
|
if ctx.IsSet(traceIdFlagName) {
|
|
configParams.TraceId = api.PtrString(ctx.String(traceIdFlagName))
|
|
}
|
|
|
|
apiConfig := api.NewAPIConfig(configParams)
|
|
|
|
if ctx.IsSet(extraHttpHeaderFlagName) {
|
|
for _, h := range ctx.StringSlice(extraHttpHeaderFlagName) {
|
|
k, v, ok := strings.Cut(h, ":")
|
|
if !ok {
|
|
return nil, fmt.Errorf(`header flag syntax "key:value", missing value in %q`, h)
|
|
}
|
|
apiConfig.AddDefaultHeader(k, v)
|
|
}
|
|
}
|
|
|
|
return api.NewAPIClient(apiConfig), nil
|
|
}
|
|
|
|
func withContext() cli.BeforeFunc {
|
|
return func(ctx *cli.Context) error {
|
|
ctx.App.Metadata["context"] = signals.WithStandardSignals(context.Background())
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func getContext(ctx *cli.Context) context.Context {
|
|
c, ok := ctx.App.Metadata["context"].(context.Context)
|
|
if !ok {
|
|
panic("missing context")
|
|
}
|
|
return c
|
|
}
|
|
|
|
func withCli() cli.BeforeFunc {
|
|
return func(ctx *cli.Context) error {
|
|
c, err := newCli(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx.App.Metadata["cli"] = c
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func getCLI(ctx *cli.Context) clients.CLI {
|
|
i, ok := ctx.App.Metadata["cli"].(clients.CLI)
|
|
if !ok {
|
|
panic("missing CLI")
|
|
}
|
|
return i
|
|
}
|
|
|
|
func withApi(injectToken bool) cli.BeforeFunc {
|
|
key := "api-no-token"
|
|
if injectToken {
|
|
key = "api"
|
|
}
|
|
|
|
makeFn := func(ctx *cli.Context) error {
|
|
c := getCLI(ctx)
|
|
apiClient, err := newApiClient(ctx, c.ConfigService, injectToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx.App.Metadata[key] = apiClient
|
|
return nil
|
|
}
|
|
return middleware.WithBeforeFns(makeFn)
|
|
}
|
|
|
|
func getAPI(ctx *cli.Context) *api.APIClient {
|
|
i, ok := ctx.App.Metadata["api"].(*api.APIClient)
|
|
if !ok {
|
|
panic("missing APIClient with token")
|
|
}
|
|
return i
|
|
}
|
|
|
|
func getAPINoToken(ctx *cli.Context) *api.APIClient {
|
|
i, ok := ctx.App.Metadata["api-no-token"].(*api.APIClient)
|
|
if !ok {
|
|
panic("missing APIClient without token")
|
|
}
|
|
return i
|
|
}
|
|
|
|
type CommonBoolFlag struct {
|
|
cli.BoolFlag
|
|
}
|
|
|
|
type CommonStringFlag struct {
|
|
cli.StringFlag
|
|
}
|
|
|
|
type CommonStringSliceFlag struct {
|
|
cli.StringSliceFlag
|
|
}
|
|
|
|
// 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.
|
|
|
|
// configPathFlag returns the flag used by commands that access the CLI's local config store.
|
|
func configPathFlag() cli.Flag {
|
|
return &CommonStringFlag{
|
|
cli.StringFlag{
|
|
Name: configPathFlagName,
|
|
Usage: "Path to the influx CLI configurations",
|
|
EnvVar: "INFLUX_CONFIGS_PATH",
|
|
},
|
|
}
|
|
}
|
|
|
|
// coreFlags returns flags used by all CLI commands that make HTTP requests.
|
|
func coreFlags() []cli.Flag {
|
|
return []cli.Flag{
|
|
&CommonStringFlag{cli.StringFlag{
|
|
Name: hostFlagName,
|
|
Usage: "HTTP address of InfluxDB",
|
|
EnvVar: "INFLUX_HOST",
|
|
}},
|
|
&CommonBoolFlag{cli.BoolFlag{
|
|
Name: skipVerifyFlagName,
|
|
Usage: "Skip TLS certificate chain and host name verification",
|
|
EnvVar: "INFLUX_SKIP_VERIFY",
|
|
}},
|
|
configPathFlag(),
|
|
&CommonStringFlag{cli.StringFlag{
|
|
Name: configNameFlagName + ", c",
|
|
Usage: "Config name to use for command",
|
|
EnvVar: "INFLUX_ACTIVE_CONFIG",
|
|
}},
|
|
&CommonStringFlag{cli.StringFlag{
|
|
Name: traceIdFlagName,
|
|
Hidden: true,
|
|
EnvVar: "INFLUX_TRACE_DEBUG_ID",
|
|
}},
|
|
&CommonStringSliceFlag{cli.StringSliceFlag{
|
|
Name: extraHttpHeaderFlagName,
|
|
Hidden: true,
|
|
EnvVar: "INFLUX_EXTRA_HTTP_HEADER",
|
|
}},
|
|
&CommonBoolFlag{cli.BoolFlag{
|
|
Name: httpDebugFlagName,
|
|
}},
|
|
}
|
|
}
|
|
|
|
// printFlags returns flags used by commands that display API resources to the user.
|
|
func printFlags() []cli.Flag {
|
|
return []cli.Flag{
|
|
&CommonBoolFlag{cli.BoolFlag{
|
|
Name: printJsonFlagName,
|
|
Usage: "Output data as JSON",
|
|
EnvVar: "INFLUX_OUTPUT_JSON",
|
|
}},
|
|
&CommonBoolFlag{cli.BoolFlag{
|
|
Name: hideHeadersFlagName,
|
|
Usage: "Hide the table headers in output data",
|
|
EnvVar: "INFLUX_HIDE_HEADERS",
|
|
}},
|
|
}
|
|
}
|
|
|
|
// commonTokenFlag returns the flag used by commands that hit an authenticated API.
|
|
func commonTokenFlag() cli.Flag {
|
|
return &CommonStringFlag{cli.StringFlag{
|
|
Name: tokenFlagName + ", t",
|
|
Usage: "Token to authenticate request",
|
|
EnvVar: "INFLUX_TOKEN",
|
|
}}
|
|
}
|
|
|
|
func init() {
|
|
|
|
// SubcommandHelpTemplate is the text template for the subcommand help topic.
|
|
// cli.go uses text/template to render templates. You can
|
|
// render custom help text by setting this variable.
|
|
cli.CommandHelpTemplate = `NAME:
|
|
{{.HelpName}} - {{.Usage}}
|
|
|
|
USAGE:
|
|
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
|
|
|
|
CATEGORY:
|
|
{{.Category}}{{end}}{{if .Description}}
|
|
|
|
DESCRIPTION:
|
|
{{.Description}}{{end}}{{if .VisibleFlags}}
|
|
|
|
COMMON OPTIONS:
|
|
{{range .VisibleFlags}}{{if iscommon .}}{{.}}
|
|
{{end}}{{end}}
|
|
OPTIONS:
|
|
{{range .VisibleFlags}}{{if not (iscommon .)}}{{.}}
|
|
{{end}}{{end}}{{end}}
|
|
`
|
|
cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) {
|
|
customFunc := make(map[string]interface{})
|
|
customFunc["iscommon"] = func(flag cli.Flag) bool {
|
|
switch flag.(type) {
|
|
case *CommonBoolFlag:
|
|
return true
|
|
case *CommonStringFlag:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
cli.HelpPrinterCustom(w, templ, data, customFunc)
|
|
}
|
|
}
|
|
|
|
// commonFlagsNoToken returns flags used by commands that display API resources to the user, but don't need auth.
|
|
func commonFlagsNoToken() []cli.Flag {
|
|
return append(coreFlags(), printFlags()...)
|
|
}
|
|
|
|
// commonFlagsNoPrint returns flags used by commands that need auth, but don't display API resources to the user.
|
|
func commonFlagsNoPrint() []cli.Flag {
|
|
return append(coreFlags(), commonTokenFlag())
|
|
}
|
|
|
|
// commonFlags returns flags used by commands that need auth and display API resources to the user.
|
|
func commonFlags() []cli.Flag {
|
|
return append(commonFlagsNoToken(), commonTokenFlag())
|
|
}
|
|
|
|
// getOrgFlags returns flags used by commands that are scoped to a single org, binding
|
|
// the flags to the given params container.
|
|
func getOrgFlags(params *clients.OrgParams) []cli.Flag {
|
|
return []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "org-id",
|
|
Usage: "The ID of the organization",
|
|
EnvVar: "INFLUX_ORG_ID",
|
|
Destination: ¶ms.OrgID,
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "org, o",
|
|
Usage: "The name of the organization",
|
|
EnvVar: "INFLUX_ORG",
|
|
Destination: ¶ms.OrgName,
|
|
},
|
|
}
|
|
}
|
|
|
|
// checkOrgFlags returns an error if OrgId and OrgName are both set.
|
|
func checkOrgFlags(params *clients.OrgParams) error {
|
|
if params.OrgID != "" && params.OrgName != "" {
|
|
return fmt.Errorf("ambiguous org: use OrgId or OrgName, but not both. OrgID: %s, OrgName: %s",
|
|
params.OrgID, params.OrgName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getBucketFlags returns flags used by commands that are scoped to a single bucket, binding
|
|
// the flags to the given params container.
|
|
func getBucketFlags(params *clients.BucketParams) []cli.Flag {
|
|
return []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "bucket-id, i",
|
|
Usage: "The bucket ID, required if name isn't provided",
|
|
Destination: ¶ms.BucketID,
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "bucket, n",
|
|
Usage: "The bucket name, org or org-id will be required by choosing this",
|
|
Destination: ¶ms.BucketName,
|
|
},
|
|
}
|
|
}
|
|
|
|
// getOrgBucketFlags returns flags used by commands that are scoped to a single org/bucket, binding
|
|
// the flags to the given params container.
|
|
func getOrgBucketFlags(c *clients.OrgBucketParams) []cli.Flag {
|
|
return append(getBucketFlags(&c.BucketParams), getOrgFlags(&c.OrgParams)...)
|
|
}
|