From 1d41f9b78228595a96b560a48781624e53d58b2f Mon Sep 17 00:00:00 2001 From: Dom Delnano Date: Tue, 16 Jul 2024 14:41:35 -0700 Subject: [PATCH] Allow interactive `px` cli usage to prompt and save preferred cloud in pixie config file (#1964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Allow interactive px cli usage to prompt and save preferred cloud in pixie config file This is a continuation of the plan I outlined in #1960 now that cloud addr is required for the `px` cli. Relevant Issues: N/A Type of change: /kind feature Test Plan: Verified the following scenarios. I will make sure to update the PX cli release checklist accordingly if/when approved
Default cloud selection testing - [x] Running `px` command that requires cloud prompts for selection and whether to store it ``` $ ./px auth login Pixie CLI ✔ withpixie.ai:443 ✔ No <------ Cloud selection was opted out of Starting browser... (if browser-based login fails, try running `px auth login --manual` for headless login) Fetching refresh token ... Failed to perform browser based auth. Will try manual auth error=browser failed to open Please Visit: https://work.withpixie.ai:443/login?local_mode=true Copy and paste token here: ^C ``` - [x] Cli commands following storing preferred cloud use the preferred value ``` $ ./px auth login Pixie CLI ✔ withpixie.ai:443 ✔ Yes <------ Cloud selection was opted into saving Starting browser... (if browser-based login fails, try running `px auth login --manual` for headless login) Fetching refresh token ... Failed to perform browser based auth. Will try manual auth error=browser failed to open Please Visit: https://work.withpixie.ai:443/login?local_mode=true Copy and paste token here: ^C $ ./px auth login Pixie CLI Starting browser... (if browser-based login fails, try running `px auth login --manual` for headless login) Fetching refresh token ... Failed to perform browser based auth. Will try manual auth error=browser failed to open Please Visit: https://work.withpixie.ai:443/login?local_mode=true Copy and paste token here: ^C $ cat ~/.pixie/config.json {"uniqueClientID":"XXX","cloudAddr":"withpixie.ai:443"} ``` - [x] Using `--cloud_addr` overrides the value set in config file ``` $ cat ~/.pixie/config.json {"uniqueClientID":"XXX","cloudAddr":"boguscloud.com"} $ ./px --cloud_addr=withpixie.ai auth login Pixie CLI Starting browser... (if browser-based login fails, try running `px auth login --manual` for headless login) Fetching refresh token ... Failed to perform browser based auth. Will try manual auth error=browser failed to open Please Visit: https://work.withpixie.ai:443/login?local_mode=true Copy and paste token here: ``` - [x] Running non-interactively uses cloud stored in config file - [x] Running non-interactively without preferred cloud or `--cloud_addr` results in error
`px config` command testing - [x] `px config list` prints out cloud addr ``` $ ./px config list Pixie CLI CloudAddr: boguscloud.com ``` - [x] `px config set` validates arguments ``` $ ./px config set --key NonExistant --value tesitng Pixie CLI FATA[0000]src/pixie_cli/pkg/cmd/config.go:80 px.dev/pixie/src/pixie_cli/pkg/cmd.glob..func16() Key 'NonExistant' is not settable. Must be one of [CloudAddr] $ ./px config set --key CloudAddr --value withpixie.ai:443 --value testing Pixie CLI FATA[0000]src/pixie_cli/pkg/cmd/config.go:74 px.dev/pixie/src/pixie_cli/pkg/cmd.glob..func16() the number of --key and --value flags must match ``` - [x] `px config set` updates config file ``` $ ./px config set --key CloudAddr --value withpixie.ai:443 Pixie CLI $ ./px config list Pixie CLI CloudAddr: withpixie.ai:443 ```
Changelog Message: Update `px` cli to store preferred cloud in pixie config file Signed-off-by: Dom Del Nano --- go.mod | 2 +- src/pixie_cli/pkg/cmd/BUILD.bazel | 2 + src/pixie_cli/pkg/cmd/config.go | 108 +++++++++++++++++++++++++++ src/pixie_cli/pkg/cmd/root.go | 29 +++++++ src/pixie_cli/pkg/pxconfig/config.go | 30 +++++++- 5 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 src/pixie_cli/pkg/cmd/config.go diff --git a/go.mod b/go.mod index e3f43225073..daf58e7f9b3 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/mattn/go-isatty v0.0.17 github.com/mattn/go-runewidth v0.0.9 github.com/mikefarah/yq/v4 v4.30.8 + github.com/mitchellh/mapstructure v1.5.0 github.com/nats-io/nats-server/v2 v2.10.4 github.com/nats-io/nats.go v1.31.0 github.com/olekukonko/tablewriter v0.0.5 @@ -214,7 +215,6 @@ require ( github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect diff --git a/src/pixie_cli/pkg/cmd/BUILD.bazel b/src/pixie_cli/pkg/cmd/BUILD.bazel index c97640a4968..d0e79d5f461 100644 --- a/src/pixie_cli/pkg/cmd/BUILD.bazel +++ b/src/pixie_cli/pkg/cmd/BUILD.bazel @@ -23,6 +23,7 @@ go_library( "auth.go", "bindata.gen.go", "collect_logs.go", + "config.go", "create_bundle.go", "create_cloud_certs.go", "debug.go", @@ -73,6 +74,7 @@ go_library( "@com_github_lestrrat_go_jwx//jwt", "@com_github_manifoldco_promptui//:promptui", "@com_github_mattn_go_isatty//:go-isatty", + "@com_github_mitchellh_mapstructure//:mapstructure", "@com_github_segmentio_analytics_go_v3//:analytics-go", "@com_github_sirupsen_logrus//:logrus", "@com_github_spf13_cobra//:cobra", diff --git a/src/pixie_cli/pkg/cmd/config.go b/src/pixie_cli/pkg/cmd/config.go new file mode 100644 index 00000000000..5c11d94e865 --- /dev/null +++ b/src/pixie_cli/pkg/cmd/config.go @@ -0,0 +1,108 @@ +/* + * Copyright 2018- The Pixie Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package cmd + +import ( + "fmt" + "os" + + "github.com/mitchellh/mapstructure" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "golang.org/x/exp/slices" + + "px.dev/pixie/src/pixie_cli/pkg/pxconfig" +) + +func init() { + ConfigCmd.AddCommand(ConfigListCmd) + ConfigCmd.AddCommand(ConfigSetCmd) + + ConfigSetCmd.Flags().StringArray("key", []string{}, "Key to set") + ConfigSetCmd.Flags().StringArray("value", []string{}, "Value to set") +} + +var settableConfigKeys = pxconfig.GetSettableConfigKeys() + +var ConfigSetCmd = &cobra.Command{ + Use: "set", + Short: "Set configuration options", + Run: func(cmd *cobra.Command, args []string) { + keys, _ := cmd.Flags().GetStringArray("key") + values, _ := cmd.Flags().GetStringArray("value") + + input := map[string]interface{}{} + for i := 0; i < len(keys); i++ { + key := keys[i] + value := values[i] + input[key] = value + } + + cfg := pxconfig.Cfg() + err := mapstructure.Decode(input, cfg) + + if err != nil { + log.Fatalf("Failed to set config: %v", err) + } + + err = pxconfig.UpdateConfig(cfg) + if err != nil { + log.Fatalf("Failed to update config: %v", err) + } + }, + PreRun: func(cmd *cobra.Command, args []string) { + keys, _ := cmd.Flags().GetStringArray("key") + values, _ := cmd.Flags().GetStringArray("value") + + if len(keys) != len(values) { + log.Fatal("the number of --key and --value flags must match") + os.Exit(1) + } + + for _, key := range keys { + if !slices.Contains(settableConfigKeys, key) { + log.Fatalf("Key '%s' is not settable. Must be one of %v", key, settableConfigKeys) + } + } + }, +} + +// ConfigListCmd is the "config list" command. +var ConfigListCmd = &cobra.Command{ + Use: "list", + Short: "List configuration options", + Run: func(cmd *cobra.Command, args []string) { + cfg := pxconfig.Cfg().ConfigInfoSettable + + var configuration map[string]interface{} + err := mapstructure.Decode(cfg, &configuration) + if err != nil { + log.Fatalf("Failed to decode config: %v", err) + } + for key, value := range configuration { + fmt.Printf("%s: %v\n", key, value) + } + }, +} + +// ConfigCmd is the "config" command. +var ConfigCmd = &cobra.Command{ + Use: "config", + Short: "Get information about the pixie config file", +} diff --git a/src/pixie_cli/pkg/cmd/root.go b/src/pixie_cli/pkg/cmd/root.go index 1dde5bafa53..5199ba35b12 100644 --- a/src/pixie_cli/pkg/cmd/root.go +++ b/src/pixie_cli/pkg/cmd/root.go @@ -72,6 +72,7 @@ func init() { viper.BindPFlag("direct_vizier_key", RootCmd.PersistentFlags().Lookup("direct_vizier_key")) RootCmd.AddCommand(VersionCmd) + RootCmd.AddCommand(ConfigCmd) RootCmd.AddCommand(AuthCmd) RootCmd.AddCommand(CollectLogsCmd) RootCmd.AddCommand(CreateCloudCertsCmd) @@ -201,6 +202,8 @@ var RootCmd = &cobra.Command{ // Name a variable to store a slice of commands that don't require cloudAddr var cmdsCloudAddrNotReqd = []*cobra.Command{ CollectLogsCmd, + ConfigListCmd, + ConfigSetCmd, VersionCmd, } @@ -212,6 +215,12 @@ func getCloudAddrIfRequired(cmd *cobra.Command) string { } cloudAddr := viper.GetString("cloud_addr") + cfg := pxconfig.Cfg() + defaultCloudAddr = cfg.CloudAddr + if cloudAddr == "" && defaultCloudAddr != "" { + cloudAddr = defaultCloudAddr + viper.Set("cloud_addr", cloudAddr) + } if cloudAddr == "" { if !isatty.IsTerminal(os.Stdin.Fd()) { utils.Errorf("No cloud address provided during run within non-interactive shell. Please set the cloud address using the `--cloud_addr` flag or `PX_CLOUD_ADDR` environment variable.") @@ -229,6 +238,26 @@ func getCloudAddrIfRequired(cmd *cobra.Command) string { cloudAddr = selectedCloud viper.Set("cloud_addr", cloudAddr) + + defaultCloudPrompt := promptui.Select{ + Label: "Set as default cloud address?", + Items: []string{"Yes", "No"}, + } + _, storeDefault, err := defaultCloudPrompt.Run() + if err != nil { + utils.WithError(err).Fatal("Failed to select default cloud address") + os.Exit(1) + } + + if storeDefault == "Yes" { + cfg.CloudAddr = cloudAddr + err := pxconfig.UpdateConfig(cfg) + + if err != nil { + utils.WithError(err).Fatal("Failed to update config file with default cloud address") + os.Exit(1) + } + } } } return cloudAddr diff --git a/src/pixie_cli/pkg/pxconfig/config.go b/src/pixie_cli/pkg/pxconfig/config.go index 39e3a5e90a3..663d8c5ef62 100644 --- a/src/pixie_cli/pkg/pxconfig/config.go +++ b/src/pixie_cli/pkg/pxconfig/config.go @@ -21,6 +21,7 @@ package pxconfig import ( "encoding/json" "os" + "reflect" "sync" "github.com/gofrs/uuid" @@ -31,7 +32,12 @@ import ( // ConfigInfo store the config about the CLI. type ConfigInfo struct { // UniqueClientID is the ID assigned to this user on first startup when auth information is not know. This can be later associated with the UserID. - UniqueClientID string `json:"uniqueClientID"` + UniqueClientID string `json:"uniqueClientID"` + ConfigInfoSettable `mapstructure:",squash"` +} + +type ConfigInfoSettable struct { + CloudAddr string `json:"cloudAddr,omitempty"` } var ( @@ -39,6 +45,28 @@ var ( once sync.Once ) +func GetSettableConfigKeys() []string { + val := reflect.ValueOf(ConfigInfoSettable{}) + keys := []string{} + for i := 0; i < val.NumField(); i++ { + keys = append(keys, val.Type().Field(i).Name) + } + return keys +} + +func UpdateConfig(cfg *ConfigInfo) error { + configPath, err := utils.EnsureDefaultConfigFilePath() + if err != nil { + utils.WithError(err).Fatal("Failed to load/create config file path") + } + f, err := os.OpenFile(configPath, os.O_RDWR|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer f.Close() + return json.NewEncoder(f).Encode(cfg) +} + func writeDefaultConfig(path string) (*ConfigInfo, error) { f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0600) if err != nil {