diff --git a/cmd/axiom/main.go b/cmd/axiom/main.go index e024af6..3179358 100644 --- a/cmd/axiom/main.go +++ b/cmd/axiom/main.go @@ -112,7 +112,10 @@ func printError(w io.Writer, err error, cmd *cobra.Command) { // Print some nicer output for Axiom API errors. if errors.Is(err, axiom.ErrNotFound) || errors.Is(err, axiom.ErrExists) || errors.Is(err, axiom.ErrUnauthorized) || errors.Is(err, axiom.ErrUnauthenticated) { - fmt.Fprintf(w, "Error: %s\n", errors.Unwrap(err)) + if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil { + err = unwrappedErr + } + fmt.Fprintf(w, "Error: %s\n", err) return } diff --git a/internal/cmd/auth/auth_login.go b/internal/cmd/auth/auth_login.go index 0dc99c7..8805349 100644 --- a/internal/cmd/auth/auth_login.go +++ b/internal/cmd/auth/auth_login.go @@ -20,14 +20,7 @@ import ( "github.com/axiomhq/cli/pkg/surveyext" ) -const ( - oAuth2ClientID = "13c885a8-f46a-4424-82d2-883cf7ccfe49" - - typeCloud = "Cloud" - typeSelfhost = "Selfhost" -) - -var validDeploymentTypes = []string{typeCloud, typeSelfhost} +const oAuth2ClientID = "13c885a8-f46a-4424-82d2-883cf7ccfe49" type loginOptions struct { *cmdutil.Factory @@ -35,12 +28,6 @@ type loginOptions struct { // AutoLogin specifies if the CLI redirects to the Axiom UI for // authentication. AutoLogin bool - // Type of the deployment to authenticate with. Default to "Cloud". Can be - // overwritten by flag. - Type string - // Url of the deployment to authenticate with. Default to the Axiom Cloud - // URL. Can be overwritten by flag. - URL string // Alias of the deployment for future reference. If not supplied as a flag, // which is optional, the user will be asked for it. Alias string @@ -50,10 +37,15 @@ type loginOptions struct { Token string // OrganizationID of the organization the supplied token is valid for. If // not supplied as a flag, which is optional, the user will be asked for it. - // Only valid for cloud deployments. OrganizationID string // Force the creation and skip the confirmation prompt. Force bool + + // Alternate deployment support: + + // URL of the deployment to authenticate with. Default to the Axiom Cloud + // URL. Can be overwritten by a hidden flag. + URL string } // NewLoginCmd creates ans returns the login command. @@ -63,7 +55,7 @@ func NewLoginCmd(f *cmdutil.Factory) *cobra.Command { } cmd := &cobra.Command{ - Use: "login [(-t|--type)=cloud|selfhost] [(-u|--url) ] [(-a|--alias) ] [(-o|--org-id) ] [-f|--force]", + Use: "login [(-a|--alias) ] [(-o|--org-id) ] [-f|--force]", Short: "Login to Axiom", DisableFlagsInUseLine: true, @@ -73,21 +65,13 @@ func NewLoginCmd(f *cmdutil.Factory) *cobra.Command { $ axiom auth login # Provide parameters on the command-line: - $ echo $AXIOM_ACCESS_TOKEN | axiom auth login --alias="axiom-eu-west-1" --url="https://axiom.eu-west-1.aws.com" -f + $ echo $AXIOM_TOKEN | axiom auth login --alias="axiom-cloud" --org-id="fancy-horse-1234" -f `), PreRunE: func(cmd *cobra.Command, _ []string) error { if !opts.IO.IsStdinTTY() || opts.AutoLogin { return nil } - - // If the user specifies the url, we assume he wants to authenticate - // against a selfhost deployment unless he explicitly specifies the - // hidden type flag that specifies the type of the deployment. - if cmd.Flag("url").Changed && !cmd.Flag("type").Changed { - opts.Type = typeSelfhost - } - return completeLogin(cmd.Context(), opts) }, @@ -100,58 +84,30 @@ func NewLoginCmd(f *cmdutil.Factory) *cobra.Command { } cmd.Flags().BoolVar(&opts.AutoLogin, "auto-login", true, "Login through the Axiom UI") - cmd.Flags().StringVarP(&opts.Type, "type", "t", strings.ToLower(typeCloud), "Type of the deployment") - cmd.Flags().StringVarP(&opts.URL, "url", "u", client.CloudURL, "Url of the deployment") cmd.Flags().StringVarP(&opts.Alias, "alias", "a", "", "Alias of the deployment") cmd.Flags().StringVarP(&opts.OrganizationID, "org-id", "o", "", "Organization ID") cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Skip the confirmation prompt") + cmd.Flags().StringVarP(&opts.URL, "url", "u", client.CloudURL, "Url of the deployment") _ = cmd.RegisterFlagCompletionFunc("auto-login", cmdutil.NoCompletion) - _ = cmd.RegisterFlagCompletionFunc("type", cmdutil.NoCompletion) - _ = cmd.RegisterFlagCompletionFunc("url", cmdutil.NoCompletion) _ = cmd.RegisterFlagCompletionFunc("alias", cmdutil.NoCompletion) _ = cmd.RegisterFlagCompletionFunc("org-id", cmdutil.NoCompletion) _ = cmd.RegisterFlagCompletionFunc("force", cmdutil.NoCompletion) + _ = cmd.RegisterFlagCompletionFunc("url", cmdutil.NoCompletion) if !opts.IO.IsStdinTTY() { _ = cmd.MarkFlagRequired("alias") + _ = cmd.MarkFlagRequired("org-id") } - _ = cmd.PersistentFlags().MarkHidden("type") + _ = cmd.Flags().MarkHidden("url") return cmd } func completeLogin(ctx context.Context, opts *loginOptions) error { - // 1. Cloud or Selfhost? - if opts.Type == "" { - if err := survey.AskOne(&survey.Select{ - Message: "Which kind of Axiom deployment are you using?", - Default: validDeploymentTypes[0], - Options: validDeploymentTypes, - }, &opts.Type, opts.IO.SurveyIO()); err != nil { - return err - } - } - - opts.Type = strings.ToLower(opts.Type) - - // 2. If Cloud mode but no URL, set the correct URL instead of asking the - // user for it. - if opts.Type == strings.ToLower(typeCloud) && opts.URL == "" { - opts.URL = client.CloudURL - } else if opts.URL == "" { - if err := survey.AskOne(&survey.Input{ - Message: "What is the url of the deployment?", - }, &opts.URL, survey.WithValidator(survey.ComposeValidators( - survey.Required, - surveyext.ValidateURL, - )), opts.IO.SurveyIO()); err != nil { - return err - } - } - - if opts.URL != "" && !strings.HasPrefix(opts.URL, "http://") && !strings.HasPrefix(opts.URL, "https://") { + // Make sure to accept urls without a scheme. + if !strings.HasPrefix(opts.URL, "http://") && !strings.HasPrefix(opts.URL, "https://") { opts.URL = "https://" + opts.URL } @@ -162,7 +118,7 @@ func completeLogin(ctx context.Context, opts *loginOptions) error { } u.Path = "/profile" - // 3. Wheather to open the browser or not. + // 1. Wheather to open the browser or not. askTokenMsg := "What is your personal access token?" if ok, err := surveyext.AskConfirm("You need to retrieve a personal access token from your profile page. Should I open that page in your default browser?", true, opts.IO.SurveyIO()); err != nil { @@ -173,7 +129,7 @@ func completeLogin(ctx context.Context, opts *loginOptions) error { return err } - // 3. The token to use. + // 2. The token to use. if err := survey.AskOne(&survey.Password{ Message: askTokenMsg, }, &opts.Token, survey.WithValidator(survey.ComposeValidators( @@ -183,11 +139,10 @@ func completeLogin(ctx context.Context, opts *loginOptions) error { return err } - // 4. Try to authenticate and fetch the organizations available to the user - // in case the deployment is a cloud deployment. If only one organization is - // available, that one is selected by default, without asking the user for - // it. - if opts.Type == strings.ToLower(typeCloud) && opts.OrganizationID == "" { + // 3. Try to authenticate and fetch the organizations available to the user. + // If only one organization is available, that one is selected by default, + // without asking the user for it. + if opts.OrganizationID == "" { axiomClient, err := client.New(ctx, opts.URL, opts.Token, "axiom", opts.Config.Insecure) if err != nil { return err @@ -238,11 +193,11 @@ func completeLogin(ctx context.Context, opts *loginOptions) error { // Just use "cloud" as the alias if this is their first deployment and they // are authenticating against Axiom Cloud. - if hostRef == strings.ToLower(typeCloud) { + if hostRef == "cloud" { opts.Alias = hostRef } - // 5. Ask for an alias to use. + // 4. Ask for an alias to use. if opts.Alias == "" { if err := survey.AskOne(&survey.Input{ Message: "Under which name should the deployment be referenced in the future?", @@ -260,28 +215,12 @@ func completeLogin(ctx context.Context, opts *loginOptions) error { } func autoLogin(ctx context.Context, opts *loginOptions) error { - opts.Type = strings.ToLower(opts.Type) - - // 1. If Cloud mode but no URL, set the correct URL instead of asking the - // user for it. - if opts.Type == strings.ToLower(typeCloud) && opts.URL == "" { - opts.URL = client.CloudURL - } else if opts.URL == "" { - if err := survey.AskOne(&survey.Input{ - Message: "What is the url of the deployment?", - }, &opts.URL, survey.WithValidator(survey.ComposeValidators( - survey.Required, - surveyext.ValidateURL, - )), opts.IO.SurveyIO()); err != nil { - return err - } - } - - if opts.URL != "" && !strings.HasPrefix(opts.URL, "http://") && !strings.HasPrefix(opts.URL, "https://") { + // Make sure to accept urls without a scheme. + if !strings.HasPrefix(opts.URL, "http://") && !strings.HasPrefix(opts.URL, "https://") { opts.URL = "https://" + opts.URL } - // 2. Wheather to open the browser or not. But the URL to open and have the + // 1. Wheather to open the browser or not. But the URL to open and have the // user login is presented nonetheless. stop := func() {} loginFunc := func(_ context.Context, loginURL string) error { @@ -311,22 +250,49 @@ func autoLogin(ctx context.Context, opts *loginOptions) error { return err } - // 3. Try to authenticate and fetch the organizations available to the user - // in case the deployment is a cloud deployment. If at least one - // organization is available, the first one is selected. - if opts.Type == strings.ToLower(typeCloud) && opts.OrganizationID == "" { + // 2. Try to authenticate and fetch the organizations available to the user. + // If only one organization is available, that one is selected by default, + // without asking the user for it. + if opts.OrganizationID == "" { axiomClient, err := client.New(ctx, opts.URL, opts.Token, "axiom", opts.Config.Insecure) if err != nil { return err } - organizations, err := axiomClient.Organizations.List(ctx) - if err != nil { + if organizations, err := axiomClient.Organizations.List(ctx); err != nil { return err - } - - if len(organizations) > 0 { + } else if len(organizations) == 1 { opts.OrganizationID = organizations[0].ID + } else { + sort.Slice(organizations, func(i, j int) bool { + return strings.ToLower(organizations[i].Name) < strings.ToLower(organizations[j].Name) + }) + + organizationNames := make([]string, len(organizations)) + for i, organization := range organizations { + organizationNames[i] = organization.Name + } + + stop() + + var organizationName string + if err := survey.AskOne(&survey.Select{ + Message: "Which organization to use?", + Options: organizationNames, + Default: organizationNames[0], + Description: func(_ string, idx int) string { + return organizations[idx].ID + }, + }, &organizationName, opts.IO.SurveyIO()); err != nil { + return err + } + + for i, organization := range organizations { + if organization.Name == organizationName { + opts.OrganizationID = organizations[i].ID + break + } + } } } @@ -342,11 +308,11 @@ func autoLogin(ctx context.Context, opts *loginOptions) error { // Just use "cloud" as the alias if this is their first deployment and they // are authenticating against Axiom Cloud. - if hostRef == strings.ToLower(typeCloud) { + if hostRef == "cloud" { opts.Alias = hostRef } - // 4. Ask for an alias to use. + // 3. Ask for an alias to use. if opts.Alias == "" { if err := survey.AskOne(&survey.Input{ Message: "Under which name should the deployment be referenced in the future?", @@ -407,27 +373,17 @@ func runLogin(ctx context.Context, opts *loginOptions) error { if opts.IO.IsStderrTTY() { cs := opts.IO.ColorScheme() - if user != nil { - if (client.IsCloudURL(opts.URL) || opts.Config.ForceCloud) && client.IsPersonalToken(opts.Token) { - organization, err := axiomClient.Organizations.Get(ctx, opts.OrganizationID) - if err != nil { - return err - } - - fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to organization %s as %s\n", - cs.SuccessIcon(), cs.Bold(organization.Name), cs.Bold(user.Name)) - } else { - fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to deployment %s as %s\n", - cs.SuccessIcon(), cs.Bold(opts.Alias), cs.Bold(user.Name)) + if client.IsPersonalToken(opts.Token) { + organization, err := axiomClient.Organizations.Get(ctx, opts.OrganizationID) + if err != nil { + return err } + + fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to organization %s as %s\n", + cs.SuccessIcon(), cs.Bold(organization.Name), cs.Bold(user.Name)) } else { - if client.IsCloudURL(opts.URL) || opts.Config.ForceCloud { - fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to organization %s %s\n", - cs.SuccessIcon(), cs.Bold(opts.OrganizationID), cs.Red(cs.Bold("(ingestion/query only!)"))) - } else { - fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to deployment %s %s\n", - cs.SuccessIcon(), cs.Bold(opts.Alias), cs.Red(cs.Bold("(ingestion/query only!)"))) - } + fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to organization %s %s\n", + cs.SuccessIcon(), cs.Bold(opts.OrganizationID), cs.Red(cs.Bold("(ingestion/query only!)"))) } } diff --git a/internal/cmd/auth/auth_switch_org.go b/internal/cmd/auth/auth_switch_org.go index 7883642..975c7a9 100644 --- a/internal/cmd/auth/auth_switch_org.go +++ b/internal/cmd/auth/auth_switch_org.go @@ -44,7 +44,6 @@ func newSwitchOrgCmd(f *cmdutil.Factory) *cobra.Command { cmdutil.AsksForSetup(f, NewLoginCmd(f)), cmdutil.NeedsDeployments(f), cmdutil.NeedsActiveDeployment(f), - cmdutil.NeedsCloudDeployment(f), cmdutil.NeedsPersonalAccessToken(f), ), diff --git a/internal/cmd/auth/auth_update_token.go b/internal/cmd/auth/auth_update_token.go index 61fa8b6..8a113ec 100644 --- a/internal/cmd/auth/auth_update_token.go +++ b/internal/cmd/auth/auth_update_token.go @@ -40,7 +40,7 @@ func newUpdateTokenCmd(f *cmdutil.Factory) *cobra.Command { $ axiom auth update-token # Provide parameters on the command-line: - $ echo $AXIOM_PERSONAL_ACCESS_TOKEN | axiom auth update-token + $ echo $AXIOM_TOKEN | axiom auth update-token `), PersistentPreRunE: cmdutil.ChainRunFuncs( @@ -122,27 +122,17 @@ func runUpdateToken(ctx context.Context, opts *updateTokenOptions) error { if opts.IO.IsStderrTTY() { cs := opts.IO.ColorScheme() - if user != nil { - if client.IsCloudURL(activeDeployment.URL) || opts.Config.ForceCloud { - organization, err := axiomClient.Organizations.Get(ctx, activeDeployment.OrganizationID) - if err != nil { - return err - } - - fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to organization %s as %s\n", - cs.SuccessIcon(), cs.Bold(organization.Name), cs.Bold(user.Name)) - } else { - fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to deployment %s as %s\n", - cs.SuccessIcon(), cs.Bold(opts.Config.ActiveDeployment), cs.Bold(user.Name)) + if client.IsPersonalToken(opts.Token) { + organization, err := axiomClient.Organizations.Get(ctx, activeDeployment.OrganizationID) + if err != nil { + return err } + + fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to organization %s as %s\n", + cs.SuccessIcon(), cs.Bold(organization.Name), cs.Bold(user.Name)) } else { - if client.IsCloudURL(activeDeployment.URL) || opts.Config.ForceCloud { - fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to organization %s %s\n", - cs.SuccessIcon(), cs.Bold(activeDeployment.OrganizationID), cs.Red(cs.Bold("(ingestion only!)"))) - } else { - fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to deployment %s %s\n", - cs.SuccessIcon(), cs.Bold(opts.Config.ActiveDeployment), cs.Red(cs.Bold("(ingestion only!)"))) - } + fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to organization %s %s\n", + cs.SuccessIcon(), cs.Bold(activeDeployment.OrganizationID), cs.Red(cs.Bold("(ingestion/query only!)"))) } } diff --git a/internal/cmd/root/help_topic.go b/internal/cmd/root/help_topic.go index b486898..c60056e 100644 --- a/internal/cmd/root/help_topic.go +++ b/internal/cmd/root/help_topic.go @@ -25,7 +25,7 @@ var topics = map[string]string{ from the configuration file. AXIOM_ORG_ID: The organization id of the organization the access token - is valid for. Only valid for Axiom Cloud. + is valid for. AXIOM_PAGER, PAGER (in order of precedence): A terminal paging program to send standard output to, e.g. "less". diff --git a/internal/cmd/root/root.go b/internal/cmd/root/root.go index 6877e77..d800371 100644 --- a/internal/cmd/root/root.go +++ b/internal/cmd/root/root.go @@ -1,6 +1,7 @@ package root import ( + "fmt" "os" "github.com/MakeNowJust/heredoc" @@ -72,9 +73,23 @@ func NewCmd(f *cmdutil.Factory) *cobra.Command { } f.Config.Insecure = cmd.Flag("insecure").Changed - f.Config.ForceCloud = cmd.Flag("force-cloud").Changed f.IO.EnableActivityIndicator(!cmd.Flag("no-spinner").Changed) + // Warn users about auto-picked up environment variables. + if f.IO.IsStderrTTY() { + cs := f.IO.ColorScheme() + + if os.Getenv("AXIOM_ORG_ID") != "" { + fmt.Fprintf(f.IO.ErrOut(), "%s Organization ID is set using %q!\n", cs.WarningIcon(), "AXIOM_ORG_ID") + } + if os.Getenv("AXIOM_TOKEN") != "" { + fmt.Fprintf(f.IO.ErrOut(), "%s Token is set using %q!\n", cs.WarningIcon(), "AXIOM_TOKEN") + } + if os.Getenv("AXIOM_URL") != "" { + fmt.Fprintf(f.IO.ErrOut(), "%s URL is set using %q!\n", cs.WarningIcon(), "AXIOM_URL") + } + } + return nil }, @@ -108,7 +123,6 @@ func NewCmd(f *cmdutil.Factory) *cobra.Command { cmd.PersistentFlags().StringP("auth-token", "T", os.Getenv("AXIOM_TOKEN"), "Token to use") cmd.PersistentFlags().StringP("auth-url", "U", os.Getenv("AXIOM_URL"), "Url to use") cmd.PersistentFlags().BoolP("insecure", "I", false, "Bypass certificate validation") - cmd.PersistentFlags().BoolP("force-cloud", "F", false, "Treat deployment as Axiom Cloud") cmd.PersistentFlags().Bool("no-spinner", false, "Disable the activity indicator") // Core commands @@ -131,7 +145,8 @@ func NewCmd(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(newHelpTopic(f.IO, "environment")) // Hidden flags - _ = cmd.PersistentFlags().MarkHidden("force-cloud") + _ = cmd.PersistentFlags().MarkHidden("auth-url") + _ = cmd.PersistentFlags().MarkHidden("insecure") return cmd } diff --git a/internal/cmdutil/chain.go b/internal/cmdutil/chain.go index aaf40c3..0ff4e0f 100644 --- a/internal/cmdutil/chain.go +++ b/internal/cmdutil/chain.go @@ -69,10 +69,6 @@ var ( To update the token for the deployment, run: $ {{ bold "axiom auth update-token" }} `) - - notCloudDeploymentMsgTmpl = heredoc.Doc(` - {{ errorIcon }} Chosen deployment {{ bold .Deployment }} is not an Axiom Cloud deployment! - `) ) // RunFunc is a cobra run function which is compatible with PersistentPreRunE, @@ -235,28 +231,6 @@ func NeedsPersonalAccessToken(f *Factory) RunFunc { } } -// NeedsCloudDeployment prints an error message and errors silently if the -// active deployment is not an Axiom Cloud deployment. -func NeedsCloudDeployment(f *Factory) RunFunc { - return func(cmd *cobra.Command, _ []string) error { - // We need an active deployment. - dep, ok := f.Config.GetActiveDeployment() - if !ok { - return nil - } - - if client.IsCloudURL(dep.URL) || f.Config.ForceCloud { - return nil - } - - err := execTemplateSilent(f.IO, notCloudDeploymentMsgTmpl, map[string]string{ - "Deployment": f.Config.ActiveDeployment, - }) - - return err - } -} - // execTemplateSilent parses and executes a template, but still returns // ErrSilent on success. func execTemplateSilent(io *terminal.IO, tmplStr string, data map[string]string) (err error) { diff --git a/internal/config/config.go b/internal/config/config.go index ace6044..45bc969 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -34,7 +34,6 @@ type Config struct { URLOverride string `toml:"-" envconfig:"url"` TokenOverride string `toml:"-" envconfig:"token"` OrganizationIDOverride string `toml:"-" envconfig:"org_id"` - ForceCloud bool `toml:"-" envconfig:"force_cloud"` ConfigFilePath string `toml:"-"` @@ -210,5 +209,5 @@ func (c *Config) Write() error { func (c *Config) IsEmpty() bool { return c.ActiveDeployment == "" && len(c.Deployments) == 0 && !c.Insecure && c.URLOverride == "" && c.TokenOverride == "" && - c.OrganizationIDOverride == "" && !c.ForceCloud + c.OrganizationIDOverride == "" }