diff --git a/cmd/newrelic/command.go b/cmd/newrelic/command.go index b46e1255a..152956320 100644 --- a/cmd/newrelic/command.go +++ b/cmd/newrelic/command.go @@ -2,7 +2,6 @@ package main import ( "errors" - "fmt" "os" "strconv" @@ -11,18 +10,19 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" - "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" "github.com/newrelic/newrelic-client-go/newrelic" "github.com/newrelic/newrelic-client-go/pkg/accounts" "github.com/newrelic/newrelic-client-go/pkg/nerdgraph" ) -var outputFormat string -var outputPlain bool - -const defaultProfileName string = "default" +var ( + outputFormat string + outputPlain bool + debug bool + trace bool +) // Command represents the base command when called without any subcommands var Command = &cobra.Command{ @@ -35,115 +35,141 @@ var Command = &cobra.Command{ } func initializeCLI(cmd *cobra.Command, args []string) { - initializeProfile() + initializeLogger() + + // If default profile has not been set, atteempt to initialize it + if config.GetDefaultProfileName() == "" { + initializeDefaultProfile() + } + + // If profile has been overridden, verify it exists + if config.ProfileOverride != "" { + if !config.ProfileExists(config.ProfileOverride) { + log.Fatalf("profile not found: %s", config.ProfileOverride) + } + } + + if client.Client == nil { + client.Client = createClient() + } } -func initializeProfile() { +func initializeLogger() { + var logLevel string + if debug { + logLevel = "DEBUG" + config.LogLevelOverride = logLevel + } else if trace { + logLevel = "TRACE" + config.LogLevelOverride = logLevel + } else { + logLevel = config.GetLogLevel() + } + + config.InitLogger(logLevel) +} + +func createClient() *newrelic.NewRelic { + c, err := client.NewClient(config.GetActiveProfileName()) + if err != nil { + // An error was encountered initializing the client. This may not be a + // problem since many commands don't require the use of an initialized client + log.Debugf("error initializing client: %s", err) + } + + return c +} + +func initializeDefaultProfile() { var accountID int var region string var licenseKey string var err error - credentials.WithCredentials(func(c *credentials.Credentials) { - if c.DefaultProfile != "" { - err = errors.New("default profile already exists, not attempting to initialize") - return - } + userKey := os.Getenv("NEW_RELIC_API_KEY") + envAccountID := os.Getenv("NEW_RELIC_ACCOUNT_ID") + region = os.Getenv("NEW_RELIC_REGION") + licenseKey = os.Getenv("NEW_RELIC_LICENSE_KEY") - apiKey := os.Getenv("NEW_RELIC_API_KEY") - envAccountID := os.Getenv("NEW_RELIC_ACCOUNT_ID") - region = os.Getenv("NEW_RELIC_REGION") - licenseKey = os.Getenv("NEW_RELIC_LICENSE_KEY") + // If we don't have a personal API key we can't initialize a profile. + if userKey == "" { + log.Debugf("NEW_RELIC_API_KEY key not set, cannot initialize default profile") + return + } - // If we don't have a personal API key we can't initialize a profile. - if apiKey == "" { - err = errors.New("api key not provided, not attempting to initialize default profile") - return - } + log.Infof("default profile does not exist and API key detected. attempting to initialize") - // Default the region to US if it's not in the environment - if region == "" { - region = "US" - } + if config.ProfileExists(config.DefaultDefaultProfileName) { + log.Warnf("a profile named %s already exists, cannot initialize default profile", config.DefaultDefaultProfileName) + return + } - // Use the accountID from the environment if we have it. - if envAccountID != "" { - accountID, err = strconv.Atoi(envAccountID) - if err != nil { - err = fmt.Errorf("couldn't parse account ID: %s", err) - return - } - } + // Saving an initial value will also set the default profile. + if err = config.SaveValueToProfile(config.DefaultDefaultProfileName, config.UserKey, userKey); err != nil { + log.Warnf("error saving API key to profile, cannot initialize default profile: %s", err) + return + } - // We should have an API key by this point, initialize the client. - client.WithClient(func(nrClient *newrelic.NewRelic) { - // If we still don't have an account ID try to look one up from the API. - if accountID == 0 { - accountID, err = fetchAccountID(nrClient) - if err != nil { - return - } - } + // Default the region to US if it's not in the environment + if region == "" { + region = "US" + } - if licenseKey == "" { - // We should have an account ID by now, so fetch the license key for it. - licenseKey, err = fetchLicenseKey(nrClient, accountID) - if err != nil { - return - } - } + if err = config.SaveValueToActiveProfile(config.Region, region); err != nil { + log.Warnf("couldn't save region to default profile: %s", err) + } - if !hasProfileWithDefaultName(c.Profiles) { - p := credentials.Profile{ - Region: region, - APIKey: apiKey, - AccountID: accountID, - LicenseKey: licenseKey, - } + // Initialize a client. + client.Client = createClient() - err = c.AddProfile(defaultProfileName, p) - if err != nil { - return - } + // Use the accountID from the environment if we have it. + if envAccountID != "" { + accountID, err = strconv.Atoi(envAccountID) + if err != nil { + log.Warnf("NEW_RELIC_ACCOUNT_ID has invalid value %s, attempting to fetch account ID", envAccountID) + } + } - log.Infof("profile %s added", text.FgCyan.Sprint(defaultProfileName)) - } + // If we still don't have an account ID try to look one up from the API. + if accountID == 0 { + accountID, err = fetchAccountID() + if err != nil { + log.Warnf("couldn't fetch account ID: %s", err) + } + } - if len(c.Profiles) == 1 { - err = c.SetDefaultProfile(defaultProfileName) - if err != nil { - err = fmt.Errorf("error setting %s as the default profile: %s", text.FgCyan.Sprint(defaultProfileName), err) - return - } + if accountID != 0 { + if err = config.SaveValueToActiveProfile(config.AccountID, accountID); err != nil { + log.Warnf("couldn't save account ID to default profile: %s", err) + } - log.Infof("setting %s as default profile", text.FgCyan.Sprint(defaultProfileName)) - } - }) - }) + if licenseKey == "" { + log.Infof("attempting to resolve license key for account ID %d", accountID) - if err != nil { - log.Debugf("couldn't initialize default profile: %s", err) + licenseKey, err = fetchLicenseKey(accountID) + if err != nil { + log.Warnf("couldn't fetch license key for account ID %d: %s", accountID, err) + } + } } -} -func hasProfileWithDefaultName(profiles map[string]credentials.Profile) bool { - for profileName := range profiles { - if profileName == defaultProfileName { - return true + if licenseKey != "" { + if err = config.SaveValueToActiveProfile(config.LicenseKey, licenseKey); err != nil { + log.Warnf("couldn't save license key to default profile: %s", err) } } - return false + log.Infof("profile %s added", text.FgCyan.Sprint(config.DefaultDefaultProfileName)) } -func fetchLicenseKey(client *newrelic.NewRelic, accountID int) (string, error) { +func fetchLicenseKey(accountID int) (string, error) { query := ` query($accountId: Int!) { actor { account(id: $accountId) { licenseKey } } }` variables := map[string]interface{}{ "accountId": accountID, } - resp, err := client.NerdGraph.Query(query, variables) + resp, err := client.Client.NerdGraph.Query(query, variables) if err != nil { return "", err } @@ -158,12 +184,12 @@ func fetchLicenseKey(client *newrelic.NewRelic, accountID int) (string, error) { // fetchAccountID will try and retrieve an account ID for the given user. If it // finds more than one account it will returrn an error. -func fetchAccountID(client *newrelic.NewRelic) (int, error) { +func fetchAccountID() (int, error) { params := accounts.ListAccountsParams{ Scope: &accounts.RegionScopeTypes.IN_REGION, } - accounts, err := client.Accounts.ListAccounts(params) + accounts, err := client.Client.Accounts.ListAccounts(params) if err != nil { return 0, err } @@ -194,9 +220,18 @@ func init() { Command.PersistentFlags().StringVar(&outputFormat, "format", output.DefaultFormat.String(), "output text format ["+output.FormatOptions()+"]") Command.PersistentFlags().BoolVar(&outputPlain, "plain", false, "output compact text") + Command.PersistentFlags().BoolVar(&debug, "debug", false, "debug level logging") + Command.PersistentFlags().BoolVar(&trace, "trace", false, "trace level logging") + Command.PersistentFlags().StringVar(&config.ProfileOverride, "profile", "", "the authentication profile to use") + Command.PersistentFlags().IntVar(&config.AccountIDOverride, "accountId", 0, "the account ID to use for this command") } func initConfig() { - utils.LogIfError(output.SetFormat(output.ParseFormat(outputFormat))) - utils.LogIfError(output.SetPrettyPrint(!outputPlain)) + if err := output.SetFormat(output.ParseFormat(outputFormat)); err != nil { + log.Error(err) + } + + if err := output.SetPrettyPrint(!outputPlain); err != nil { + log.Error(err) + } } diff --git a/cmd/newrelic/command_completion.go b/cmd/newrelic/command_completion.go index 131993ebb..05fbd491d 100644 --- a/cmd/newrelic/command_completion.go +++ b/cmd/newrelic/command_completion.go @@ -5,8 +5,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - - "github.com/newrelic/newrelic-cli/internal/utils" ) var ( @@ -64,5 +62,7 @@ func init() { Command.AddCommand(cmdCompletion) cmdCompletion.Flags().StringVar(&completionShell, "shell", "", "Output completion for the specified shell. (bash, powershell, zsh)") - utils.LogIfError(cmdCompletion.MarkFlagRequired("shell")) + if err := cmdCompletion.MarkFlagRequired("shell"); err != nil { + log.Error(err) + } } diff --git a/cmd/newrelic/command_documentation.go b/cmd/newrelic/command_documentation.go index bdf1f5dd1..820174565 100644 --- a/cmd/newrelic/command_documentation.go +++ b/cmd/newrelic/command_documentation.go @@ -4,8 +4,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" - - "github.com/newrelic/newrelic-cli/internal/utils" ) var ( @@ -24,7 +22,9 @@ newrelic documentation --outputDir --type (markdown|manpage) Example: "newrelic documentation --outputDir /tmp", Run: func(cmd *cobra.Command, args []string) { if docOutputDir == "" { - utils.LogIfError(cmd.Help()) + if err := cmd.Help(); err != nil { + log.Warn(err) + } log.Fatal("--outputDir is required") } @@ -44,7 +44,9 @@ newrelic documentation --outputDir --type (markdown|manpage) log.Error(err) } default: - utils.LogIfError(cmd.Help()) + if err := cmd.Help(); err != nil { + log.Error(err) + } log.Error("--type must be one of [markdown, manpage]") } }, @@ -55,5 +57,7 @@ func init() { cmdDocumentation.Flags().StringVarP(&docOutputDir, "outputDir", "o", "", "Output directory for generated documentation") cmdDocumentation.Flags().StringVar(&docFormat, "format", "markdown", "Documentation format [markdown, manpage] default 'markdown'") - utils.LogIfError(cmdDocumentation.MarkFlagRequired("outputDir")) + if err := cmdDocumentation.MarkFlagRequired("outputDir"); err != nil { + log.Error(err) + } } diff --git a/cmd/newrelic/command_integration_test.go b/cmd/newrelic/command_integration_test.go index 488a1a31c..566e6ee5a 100644 --- a/cmd/newrelic/command_integration_test.go +++ b/cmd/newrelic/command_integration_test.go @@ -3,58 +3,63 @@ package main import ( - "fmt" "io/ioutil" "os" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/newrelic/newrelic-cli/internal/config" - "github.com/newrelic/newrelic-cli/internal/credentials" ) func TestInitializeProfile(t *testing.T) { + envUserKey := os.Getenv("NEW_RELIC_API_KEY") + envAccountID := os.Getenv("NEW_RELIC_ACCOUNT_ID") + if envUserKey == "" || envAccountID == "" { + t.Skipf("NEW_RELIC_API_KEY and NEW_RELIC_ACCOUNT_ID are required to run this test") + } + f, err := ioutil.TempDir("/tmp", "newrelic") defer os.RemoveAll(f) - assert.NoError(t, err) - config.DefaultConfigDirectory = f + require.NoError(t, err) - apiKey := os.Getenv("NEW_RELIC_API_KEY") - envAccountID := os.Getenv("NEW_RELIC_ACCOUNT_ID") + config.ConfigDir = f // Init without the necessary environment variables os.Setenv("NEW_RELIC_API_KEY", "") os.Setenv("NEW_RELIC_ACCOUNT_ID", "") - initializeProfile() + initializeDefaultProfile() - // Load credentials from disk - c, err := credentials.LoadCredentials(f) - assert.NoError(t, err) - assert.Equal(t, 0, len(c.Profiles)) - assert.Equal(t, f, c.ConfigDirectory) - assert.Equal(t, "", c.DefaultProfile) + require.NoError(t, err) + require.Equal(t, 0, len(config.GetProfileNames())) + require.Equal(t, "", config.GetDefaultProfileName()) // Init with environment - os.Setenv("NEW_RELIC_API_KEY", apiKey) + os.Setenv("NEW_RELIC_API_KEY", envUserKey) os.Setenv("NEW_RELIC_ACCOUNT_ID", envAccountID) - initializeProfile() - - // Initialize the new configuration directory - c, err = credentials.LoadCredentials(f) - assert.NoError(t, err) - assert.Equal(t, 1, len(c.Profiles)) - assert.Equal(t, f, c.ConfigDirectory) - assert.Equal(t, defaultProfileName, c.DefaultProfile) - assert.Equal(t, apiKey, c.Profiles[defaultProfileName].APIKey) - assert.NotEmpty(t, c.Profiles[defaultProfileName].Region) - assert.NotEmpty(t, c.Profiles[defaultProfileName].AccountID) - - // Ensure that we don't Fatal out if the default profile already exists, but - // was not specified in the default-profile.json. - if err = os.Remove(fmt.Sprintf("%s/%s.json", f, credentials.DefaultProfileFile)); err != nil { - t.Fatal(err) - } + initializeDefaultProfile() + + os.Setenv("NEW_RELIC_API_KEY", "") + os.Setenv("NEW_RELIC_ACCOUNT_ID", "") + + actualUserKey, err := config.GetActiveProfileValue(config.UserKey) + require.NoError(t, err) + + actualRegion, err := config.GetActiveProfileValue(config.Region) + require.NoError(t, err) + + actualAccountID, err := config.GetActiveProfileValue(config.AccountID) + require.NoError(t, err) + + actualLicenseKey, err := config.GetActiveProfileValue(config.LicenseKey) + require.NoError(t, err) + + require.Equal(t, 1, len(config.GetProfileNames())) + require.Equal(t, config.DefaultDefaultProfileName, config.GetDefaultProfileName()) + require.Equal(t, envUserKey, actualUserKey) + require.NotEmpty(t, actualRegion) + require.NotEmpty(t, actualLicenseKey) + require.NotEmpty(t, actualAccountID) - initializeProfile() + initializeDefaultProfile() } diff --git a/cmd/newrelic/main.go b/cmd/newrelic/main.go index a20da5041..94f5b2222 100644 --- a/cmd/newrelic/main.go +++ b/cmd/newrelic/main.go @@ -10,7 +10,6 @@ import ( "github.com/newrelic/newrelic-cli/internal/apiaccess" "github.com/newrelic/newrelic-cli/internal/apm" "github.com/newrelic/newrelic-cli/internal/config" - "github.com/newrelic/newrelic-cli/internal/credentials" diagnose "github.com/newrelic/newrelic-cli/internal/diagnose" "github.com/newrelic/newrelic-cli/internal/edge" "github.com/newrelic/newrelic-cli/internal/entities" @@ -19,6 +18,7 @@ import ( "github.com/newrelic/newrelic-cli/internal/nerdgraph" "github.com/newrelic/newrelic-cli/internal/nerdstorage" "github.com/newrelic/newrelic-cli/internal/nrql" + "github.com/newrelic/newrelic-cli/internal/profiles" "github.com/newrelic/newrelic-cli/internal/reporting" "github.com/newrelic/newrelic-cli/internal/workload" ) @@ -32,7 +32,6 @@ func init() { // Bind imported sub-commands Command.AddCommand(apm.Command) Command.AddCommand(config.Command) - Command.AddCommand(credentials.Command) Command.AddCommand(diagnose.Command) Command.AddCommand(edge.Command) Command.AddCommand(events.Command) @@ -40,6 +39,7 @@ func init() { Command.AddCommand(nerdgraph.Command) Command.AddCommand(nerdstorage.Command) Command.AddCommand(nrql.Command) + Command.AddCommand(profiles.Command) Command.AddCommand(reporting.Command) Command.AddCommand(workload.Command) Command.AddCommand(agent.Command) @@ -61,18 +61,18 @@ func main() { // CheckPrereleaseMode unhides subcommands marked as hidden when the pre-release // flag is active. func CheckPrereleaseMode(c *cobra.Command) { - config.WithConfig(func(cfg *config.Config) { - if !cfg.PreReleaseFeatures.Bool() { - return - } + v := config.GetConfigValueTernary(config.PrereleaseFeatures) + + if !v.Bool() { + return + } - log.Debug("Pre-release mode active") + log.Debug("Pre-release mode active") - for _, cmd := range c.Commands() { - if cmd.Hidden { - log.Debugf("Activating pre-release subcommand: %s", cmd.Name()) - cmd.Hidden = false - } + for _, cmd := range c.Commands() { + if cmd.Hidden { + log.Debugf("Activating pre-release subcommand: %s", cmd.Name()) + cmd.Hidden = false } - }) + } } diff --git a/go.mod b/go.mod index 8487f5edd..f9d86f4b1 100644 --- a/go.mod +++ b/go.mod @@ -12,13 +12,11 @@ require ( github.com/google/uuid v1.2.0 github.com/goreleaser/goreleaser v0.155.0 github.com/hokaccha/go-prettyjson v0.0.0-20210113012101-fb4e108d2519 - github.com/imdario/mergo v0.3.11 github.com/jedib0t/go-pretty/v6 v6.1.0 github.com/joshdk/go-junit v0.0.0-20201221202203-061ee62ada40 github.com/llorllale/go-gitlint v0.0.0-20200802191503-5984945d4b80 github.com/manifoldco/promptui v0.8.0 github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/mapstructure v1.4.1 github.com/newrelic/newrelic-client-go v0.57.1 github.com/newrelic/tutone v0.5.0 github.com/psampaz/go-mod-outdated v0.7.0 diff --git a/internal/agent/command_config.go b/internal/agent/command_config.go index faff90ca9..3d4c1de39 100644 --- a/internal/agent/command_config.go +++ b/internal/agent/command_config.go @@ -1,10 +1,10 @@ package agent import ( + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" ) var ( @@ -21,7 +21,7 @@ var cmdConfigObfuscate = &cobra.Command{ Use: "obfuscate", Short: "Obfuscate a configuration value using a key", Long: `Obfuscate a configuration value using a key. The obfuscated value -should be placed in the Agent configuration or in an environment variable." +should be placed in the Agent configuration or in an environment variable. `, Example: "newrelic agent config obfuscate --value --key ", Run: func(cmd *cobra.Command, args []string) { @@ -30,7 +30,9 @@ should be placed in the Agent configuration or in an environment variable." ObfuscatedValue: obfuscateStringWithKey(textToEncode, encodeKey), } - utils.LogIfFatal(output.Print(result)) + if err := output.Print(result); err != nil { + log.Fatal(err) + } }, } @@ -43,6 +45,10 @@ func init() { cmdConfigObfuscate.Flags().StringVarP(&encodeKey, "key", "k", "", "the key to use when obfuscating the clear-text value") cmdConfigObfuscate.Flags().StringVarP(&textToEncode, "value", "v", "", "the value, in clear text, to be obfuscated") - utils.LogIfError(cmdConfigObfuscate.MarkFlagRequired("key")) - utils.LogIfError(cmdConfigObfuscate.MarkFlagRequired("value")) + if err := cmdConfigObfuscate.MarkFlagRequired("key"); err != nil { + log.Error(err) + } + if err := cmdConfigObfuscate.MarkFlagRequired("value"); err != nil { + log.Error(err) + } } diff --git a/internal/apiaccess/command.go b/internal/apiaccess/command.go index 90523a3c3..7cae98a7c 100644 --- a/internal/apiaccess/command.go +++ b/internal/apiaccess/command.go @@ -3,12 +3,12 @@ package apiaccess import ( "encoding/json" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" "github.com/newrelic/newrelic-client-go/pkg/apiaccess" ) @@ -18,6 +18,11 @@ var Command = &cobra.Command{ Long: "", Example: "newrelic apiaccess apiAccess --help", Hidden: true, // Mark as pre-release + PersistentPreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, } var apiAccessGetKeyid string @@ -29,13 +34,14 @@ var cmdKey = &cobra.Command{ Long: "", Example: "newrelic apiAccess apiAccessGetKey --id --keyType", Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - - resp, err := nrClient.APIAccess.GetAPIAccessKey(apiAccessGetKeyid, apiaccess.APIAccessKeyType(apiAccessGetKeykeyType)) - utils.LogIfFatal(err) - - utils.LogIfFatal(output.Print(resp)) - }) + resp, err := client.Client.APIAccess.GetAPIAccessKey(apiAccessGetKeyid, apiaccess.APIAccessKeyType(apiAccessGetKeykeyType)) + if err != nil { + log.Fatal(err) + } + + if err := output.Print(resp); err != nil { + log.Fatal(err) + } }, } var apiAccessCreateKeysInput string @@ -46,18 +52,20 @@ var cmdAPIAccessCreateKeys = &cobra.Command{ Long: "", Example: "newrelic apiAccess apiAccessCreateKeys --keys", Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - - var keys apiaccess.APIAccessCreateInput - - err := json.Unmarshal([]byte(apiAccessCreateKeysInput), &keys) - utils.LogIfFatal(err) - - resp, err := nrClient.APIAccess.CreateAPIAccessKeys(keys) - utils.LogIfFatal(err) - - utils.LogIfFatal(output.Print(resp)) - }) + var keys apiaccess.APIAccessCreateInput + err := json.Unmarshal([]byte(apiAccessCreateKeysInput), &keys) + if err != nil { + log.Fatal(err) + } + + resp, err := client.Client.APIAccess.CreateAPIAccessKeys(keys) + if err != nil { + log.Fatal(err) + } + + if err := output.Print(resp); err != nil { + log.Fatal(err) + } }, } var apiAccessUpdateKeysInput string @@ -68,18 +76,19 @@ var cmdAPIAccessUpdateKeys = &cobra.Command{ Long: "", Example: "newrelic apiAccess apiAccessUpdateKeys --keys", Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - - var keys apiaccess.APIAccessUpdateInput - - err := json.Unmarshal([]byte(apiAccessUpdateKeysInput), &keys) - utils.LogIfFatal(err) - - resp, err := nrClient.APIAccess.UpdateAPIAccessKeys(keys) - utils.LogIfFatal(err) - - utils.LogIfFatal(output.Print(resp)) - }) + var keys apiaccess.APIAccessUpdateInput + if err := json.Unmarshal([]byte(apiAccessUpdateKeysInput), &keys); err != nil { + log.Fatal(err) + } + + resp, err := client.Client.APIAccess.UpdateAPIAccessKeys(keys) + if err != nil { + log.Fatal(err) + } + + if err := output.Print(resp); err != nil { + log.Fatal(err) + } }, } var apiAccessDeleteKeysInput string @@ -90,18 +99,19 @@ var cmdAPIAccessDeleteKeys = &cobra.Command{ Long: "", Example: "newrelic apiAccess apiAccessDeleteKeys --keys", Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - - var keys apiaccess.APIAccessDeleteInput - - err := json.Unmarshal([]byte(apiAccessDeleteKeysInput), &keys) - utils.LogIfFatal(err) - - resp, err := nrClient.APIAccess.DeleteAPIAccessKey(keys) - utils.LogIfFatal(err) - - utils.LogIfFatal(output.Print(resp)) - }) + var keys apiaccess.APIAccessDeleteInput + if err := json.Unmarshal([]byte(apiAccessDeleteKeysInput), &keys); err != nil { + log.Fatal(err) + } + + resp, err := client.Client.APIAccess.DeleteAPIAccessKey(keys) + if err != nil { + log.Fatal(err) + } + + if err = output.Print(resp); err != nil { + log.Fatal(err) + } }, } @@ -109,21 +119,21 @@ func init() { Command.AddCommand(cmdKey) cmdKey.Flags().StringVar(&apiAccessGetKeyid, "id", "", "The `id` of the key. This can be used to identify a key without revealing the key itself (used to update and delete).") - utils.LogIfError(cmdKey.MarkFlagRequired("id")) + if err := cmdKey.MarkFlagRequired("id"); err != nil { + log.Error(err) + } cmdKey.Flags().StringVar(&apiAccessGetKeykeyType, "keyType", "", "The type of key.") - utils.LogIfError(cmdKey.MarkFlagRequired("keyType")) + if err := cmdKey.MarkFlagRequired("keyType"); err != nil { + log.Error(err) + } Command.AddCommand(cmdAPIAccessCreateKeys) - cmdAPIAccessCreateKeys.Flags().StringVar(&apiAccessCreateKeysInput, "keys", "", "A list of the configurations for each key you want to create.") Command.AddCommand(cmdAPIAccessUpdateKeys) - cmdAPIAccessUpdateKeys.Flags().StringVar(&apiAccessUpdateKeysInput, "keys", "", "The configurations of each key you want to update.") Command.AddCommand(cmdAPIAccessDeleteKeys) - cmdAPIAccessDeleteKeys.Flags().StringVar(&apiAccessDeleteKeysInput, "keys", "", "A list of each key `id` that you want to delete. You can read more about managing keys on [this documentation page](https://docs.newrelic.com/docs/apis/nerdgraph/examples/use-nerdgraph-manage-license-keys-personal-api-keys).") - } diff --git a/internal/apm/command_application.go b/internal/apm/command_application.go index c32873f61..e3c1c4901 100644 --- a/internal/apm/command_application.go +++ b/internal/apm/command_application.go @@ -4,12 +4,11 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/newrelic/newrelic-client-go/newrelic" "github.com/newrelic/newrelic-client-go/pkg/entities" "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" ) var ( @@ -33,58 +32,71 @@ var cmdAppSearch = &cobra.Command{ The search command performs a query for an APM application name and/or account ID. `, Example: "newrelic apm application search --name ", + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { if appGUID == "" && appName == "" && apmAccountID == "" { - utils.LogIfError(cmd.Help()) + if err := cmd.Help(); err != nil { + log.Fatal(err) + } + log.Fatal("one of --accountId, --guid, --name are required") } - client.WithClient(func(nrClient *newrelic.NewRelic) { - var entityResults []entities.EntityOutlineInterface - var err error - - // Look for just the GUID if passed in - if appGUID != "" { - if appName != "" || apmAccountID != "" { - log.Warnf("Searching for --guid only, ignoring --accountId and --name") - } - - var singleResult *entities.EntityInterface - singleResult, err = nrClient.Entities.GetEntity(entities.EntityGUID(appGUID)) - utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(*singleResult)) - } else { - params := entities.EntitySearchQueryBuilder{ - Domain: entities.EntitySearchQueryBuilderDomain("APM"), - Type: entities.EntitySearchQueryBuilderType("APPLICATION"), - } - - if appName != "" { - params.Name = appName - } - - if apmAccountID != "" { - params.Tags = []entities.EntitySearchQueryBuilderTag{{Key: "accountId", Value: apmAccountID}} - } - - results, err := nrClient.Entities.GetEntitySearch( - entities.EntitySearchOptions{}, - "", - params, - []entities.EntitySearchSortCriteria{}, - ) - - entityResults = results.Results.Entities - utils.LogIfFatal(err) + var entityResults []entities.EntityOutlineInterface + + // Look for just the GUID if passed in + if appGUID != "" { + if appName != "" || apmAccountID != "" { + log.Warnf("Searching for --guid only, ignoring --accountId and --name") + } + + var singleResult *entities.EntityInterface + singleResult, err := client.Client.Entities.GetEntity(entities.EntityGUID(appGUID)) + if err != nil { + log.Fatal(err) + } + + if err := output.Print(*singleResult); err != nil { + log.Fatal(err) + } + } else { + params := entities.EntitySearchQueryBuilder{ + Domain: entities.EntitySearchQueryBuilderDomain("APM"), + Type: entities.EntitySearchQueryBuilderType("APPLICATION"), + } + + if appName != "" { + params.Name = appName + } + + if apmAccountID != "" { + params.Tags = []entities.EntitySearchQueryBuilderTag{{Key: "accountId", Value: apmAccountID}} } - utils.LogIfFatal(output.Print(entityResults)) - }) + results, err := client.Client.Entities.GetEntitySearch( + entities.EntitySearchOptions{}, + "", + params, + []entities.EntitySearchSortCriteria{}, + ) + + entityResults = results.Results.Entities + if err != nil { + log.Fatal(err) + } + } + + if err := output.Print(entityResults); err != nil { + log.Fatal(err) + } }, } -// var cmdAppGet = &cobra.Command{ Use: "get", Short: "Get a New Relic application", @@ -93,21 +105,30 @@ var cmdAppGet = &cobra.Command{ The get command performs a query for an APM application by GUID. `, Example: "newrelic apm application get --guid ", + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - var results *entities.EntityInterface - var err error - - if appGUID != "" { - results, err = nrClient.Entities.GetEntity(entities.EntityGUID(appGUID)) - utils.LogIfFatal(err) - } else { - utils.LogIfError(cmd.Help()) - log.Fatal(" --guid is required") + var results *entities.EntityInterface + var err error + + if appGUID == "" { + if err = cmd.Help(); err != nil { + log.Fatal(err) } + log.Fatal(" --guid is required") + } - utils.LogIfFatal(output.Print(results)) - }) + results, err = client.Client.Entities.GetEntity(entities.EntityGUID(appGUID)) + if err != nil { + log.Fatal(err) + } + + if err := output.Print(results); err != nil { + log.Fatal(err) + } }, } diff --git a/internal/apm/command_deployment.go b/internal/apm/command_deployment.go index 06748d4b3..5aae91280 100644 --- a/internal/apm/command_deployment.go +++ b/internal/apm/command_deployment.go @@ -4,12 +4,11 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/newrelic/newrelic-client-go/newrelic" "github.com/newrelic/newrelic-client-go/pkg/apm" "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" ) var ( @@ -37,18 +36,27 @@ var cmdDeploymentList = &cobra.Command{ The list command returns deployments for a New Relic APM application. `, Example: "newrelic apm deployment list --applicationId ", + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { if apmAppID == 0 { - utils.LogIfError(cmd.Help()) + if err := cmd.Help(); err != nil { + log.Error(err) + } log.Fatal("--applicationId is required") } - client.WithClient(func(nrClient *newrelic.NewRelic) { - deployments, err := nrClient.APM.ListDeployments(apmAppID) - utils.LogIfFatal(err) + deployments, err := client.Client.APM.ListDeployments(apmAppID) + if err != nil { + log.Fatal(err) + } - utils.LogIfFatal(output.Print(deployments)) - }) + if err = output.Print(deployments); err != nil { + log.Fatal(err) + } }, } @@ -61,18 +69,27 @@ The create command creates a new deployment marker for a New Relic APM application. `, Example: "newrelic apm deployment create --applicationId --revision ", + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { if apmAppID == 0 { - utils.LogIfError(cmd.Help()) + if err := cmd.Help(); err != nil { + log.Error(err) + } log.Fatal("--applicationId and --revision are required") } - client.WithClient(func(nrClient *newrelic.NewRelic) { - d, err := nrClient.APM.CreateDeployment(apmAppID, deployment) - utils.LogIfFatal(err) + d, err := client.Client.APM.CreateDeployment(apmAppID, deployment) + if err != nil { + log.Fatal(err) + } - utils.LogIfFatal(output.Print(d)) - }) + if err = output.Print(d); err != nil { + log.Fatal(err) + } }, } @@ -84,18 +101,27 @@ var cmdDeploymentDelete = &cobra.Command{ The delete command performs a delete operation for an APM deployment. `, Example: "newrelic apm deployment delete --applicationId --deploymentID ", + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { if apmAppID == 0 { - utils.LogIfError(cmd.Help()) + if err := cmd.Help(); err != nil { + log.Error(err) + } log.Fatal("--applicationId is required") } - client.WithClient(func(nrClient *newrelic.NewRelic) { - d, err := nrClient.APM.DeleteDeployment(apmAppID, deployment.ID) - utils.LogIfFatal(err) + d, err := client.Client.APM.DeleteDeployment(apmAppID, deployment.ID) + if err != nil { + log.Fatal(err) + } - utils.LogIfFatal(output.Print(d)) - }) + if err = output.Print(d); err != nil { + log.Fatal(err) + } }, } @@ -110,9 +136,13 @@ func init() { cmdDeploymentCreate.Flags().StringVarP(&deployment.Changelog, "change-log", "", "", "the change log stored with the deployment") cmdDeploymentCreate.Flags().StringVarP(&deployment.Revision, "revision", "r", "", "a freeform string representing the revision of the deployment") - utils.LogIfError(cmdDeploymentCreate.MarkFlagRequired("revision")) + if err := cmdDeploymentCreate.MarkFlagRequired("revision"); err != nil { + log.Error(err) + } cmdDeployment.AddCommand(cmdDeploymentDelete) cmdDeploymentDelete.Flags().IntVarP(&deployment.ID, "deploymentID", "d", 0, "the ID of the deployment to be deleted") - utils.LogIfError(cmdDeploymentDelete.MarkFlagRequired("deploymentID")) + if err := cmdDeploymentDelete.MarkFlagRequired("deploymentID"); err != nil { + log.Error(err) + } } diff --git a/internal/client/client.go b/internal/client/client.go index a1ad88a1a..4f382bec8 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -5,46 +5,32 @@ import ( "fmt" "os" - "github.com/newrelic/newrelic-client-go/newrelic" - "github.com/newrelic/newrelic-cli/internal/config" - "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-client-go/newrelic" ) var ( + Client *newrelic.NewRelic serviceName = "newrelic-cli" version = "dev" ) -// CreateNRClient initializes the New Relic client. -func CreateNRClient(cfg *config.Config, creds *credentials.Credentials) (*newrelic.NewRelic, *credentials.Profile, error) { - var ( - err error - apiKey string - insightsInsertKey string - regionValue string - ) - - // Create the New Relic Client - defProfile := creds.Default() - - if defProfile != nil { - apiKey = defProfile.APIKey - insightsInsertKey = defProfile.InsightsInsertKey - regionValue = defProfile.Region - } - - if apiKey == "" { - return nil, nil, errors.New("an API key is required, set a default profile or use the NEW_RELIC_API_KEY environment variable") +func NewClient(profileName string) (*newrelic.NewRelic, error) { + userKey := config.GetProfileValueString(profileName, config.UserKey) + insightsInsertKey := config.GetProfileValueString(profileName, config.InsightsInsertKey) + if userKey == "" && insightsInsertKey == "" { + return nil, errors.New("a User API key or Ingest API key is required, set a default profile or use the NEW_RELIC_API_KEY or NEW_RELIC_INSIGHTS_INSERT_KEY environment variables") } + region := config.GetProfileValueString(profileName, config.Region) + logLevel := config.GetLogLevel() userAgent := fmt.Sprintf("newrelic-cli/%s (https://github.com/newrelic/newrelic-cli)", version) cfgOpts := []newrelic.ConfigOption{ - newrelic.ConfigPersonalAPIKey(apiKey), + newrelic.ConfigPersonalAPIKey(userKey), newrelic.ConfigInsightsInsertKey(insightsInsertKey), - newrelic.ConfigLogLevel(cfg.LogLevel), - newrelic.ConfigRegion(regionValue), + newrelic.ConfigLogLevel(logLevel), + newrelic.ConfigRegion(region), newrelic.ConfigUserAgent(userAgent), newrelic.ConfigServiceName(serviceName), } @@ -55,10 +41,9 @@ func CreateNRClient(cfg *config.Config, creds *credentials.Credentials) (*newrel } nrClient, err := newrelic.New(cfgOpts...) - if err != nil { - return nil, nil, fmt.Errorf("unable to create New Relic client with error: %s", err) + return nil, fmt.Errorf("unable to create New Relic client with error: %s", err) } - return nrClient, defProfile, nil + return nrClient, nil } diff --git a/internal/client/helpers.go b/internal/client/helpers.go deleted file mode 100644 index 175a9ea81..000000000 --- a/internal/client/helpers.go +++ /dev/null @@ -1,49 +0,0 @@ -package client - -import ( - log "github.com/sirupsen/logrus" - - "github.com/newrelic/newrelic-cli/internal/config" - "github.com/newrelic/newrelic-cli/internal/credentials" - "github.com/newrelic/newrelic-client-go/newrelic" -) - -// WithClient returns a New Relic client. -func WithClient(f func(c *newrelic.NewRelic)) { - WithClientFrom(config.DefaultConfigDirectory, f) -} - -// WithClientFrom returns a New Relic client, initialized from configuration in the specified location. -func WithClientFrom(configDir string, f func(c *newrelic.NewRelic)) { - config.WithConfigFrom(configDir, func(cfg *config.Config) { - credentials.WithCredentialsFrom(configDir, func(creds *credentials.Credentials) { - nrClient, _, err := CreateNRClient(cfg, creds) - if err != nil { - log.Fatal(err) - } - - f(nrClient) - }) - }) -} - -// WithClientAndProfile returns a New Relic client and the profile used to initialize it, -// after environment oveerrides have been applied. -func WithClientAndProfile(f func(c *newrelic.NewRelic, p *credentials.Profile)) { - WithClientAndProfileFrom(config.DefaultConfigDirectory, f) -} - -// WithClientAndProfileFrom returns a New Relic client and default profile used to initialize it, -// after environment oveerrides have been applied. -func WithClientAndProfileFrom(configDir string, f func(c *newrelic.NewRelic, p *credentials.Profile)) { - config.WithConfigFrom(configDir, func(cfg *config.Config) { - credentials.WithCredentialsFrom(configDir, func(creds *credentials.Credentials) { - nrClient, defaultProfile, err := CreateNRClient(cfg, creds) - if err != nil { - log.Fatal(err) - } - - f(nrClient, defaultProfile) - }) - }) -} diff --git a/internal/config/command.go b/internal/config/command.go index fe1204b97..b795c6a0c 100644 --- a/internal/config/command.go +++ b/internal/config/command.go @@ -1,9 +1,10 @@ package config import ( + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/newrelic/newrelic-cli/internal/utils" + "github.com/newrelic/newrelic-cli/internal/output" ) var ( @@ -27,9 +28,10 @@ The set command sets a persistent configuration value for the New Relic CLI. `, Example: "newrelic config set --key --value ", Run: func(cmd *cobra.Command, args []string) { - WithConfig(func(cfg *Config) { - utils.LogIfError(cfg.Set(key, value)) - }) + err := SaveConfigValue(CfgFieldKey(key), value) + if err != nil { + log.Fatal(err) + } }, } @@ -42,9 +44,8 @@ The get command gets a persistent configuration value for the New Relic CLI. `, Example: "newrelic config get --key ", Run: func(cmd *cobra.Command, args []string) { - WithConfig(func(cfg *Config) { - cfg.Get(key) - }) + val := GetConfigValueString(CfgFieldKey(key)) + output.Text(val) }, } @@ -57,9 +58,17 @@ The list command lists all persistent configuration values for the New Relic CLI `, Example: "newrelic config list", Run: func(cmd *cobra.Command, args []string) { - WithConfig(func(cfg *Config) { - cfg.List() - }) + vals := []CfgValue{} + for _, v := range ConfigFields { + val := GetConfigValueString(v.Key) + vals = append(vals, CfgValue{ + Name: v.Name, + Value: val, + Default: v.Default, + }) + } + + output.Text(vals) }, Aliases: []string{ "ls", @@ -76,9 +85,15 @@ This will have the effect of resetting the value to its default. `, Example: "newrelic config delete --key ", Run: func(cmd *cobra.Command, args []string) { - WithConfig(func(cfg *Config) { - utils.LogIfError(cfg.Delete(key)) - }) + f := findConfigField(CfgFieldKey(key)) + if f == nil { + log.Fatalf("config value not found: %s, value must be one of %s", key, validConfigFieldKeys()) + } + + err := SaveConfigValue(CfgFieldKey(key), f.Default) + if err != nil { + log.Fatal(err) + } }, Aliases: []string{ "rm", @@ -91,14 +106,23 @@ func init() { Command.AddCommand(cmdSet) cmdSet.Flags().StringVarP(&key, "key", "k", "", "the key to set") cmdSet.Flags().StringVarP(&value, "value", "v", "", "the value to set") - utils.LogIfError(cmdSet.MarkFlagRequired("key")) - utils.LogIfError(cmdSet.MarkFlagRequired("value")) + if err := cmdSet.MarkFlagRequired("key"); err != nil { + log.Error(err) + } + + if err := cmdSet.MarkFlagRequired("value"); err != nil { + log.Error(err) + } Command.AddCommand(cmdGet) cmdGet.Flags().StringVarP(&key, "key", "k", "", "the key to get") - utils.LogIfError(cmdGet.MarkFlagRequired("key")) + if err := cmdGet.MarkFlagRequired("key"); err != nil { + log.Error(err) + } Command.AddCommand(cmdDelete) cmdDelete.Flags().StringVarP(&key, "key", "k", "", "the key to delete") - utils.LogIfError(cmdDelete.MarkFlagRequired("key")) + if err := cmdDelete.MarkFlagRequired("key"); err != nil { + log.Error(err) + } } diff --git a/internal/config/config.go b/internal/config/config.go index 31c0969ae..8d11cf04d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,441 +1,558 @@ package config import ( + "bytes" + "encoding/json" "fmt" + "io/ioutil" "os" - "reflect" + "path" + "path/filepath" + "strconv" "strings" - "github.com/newrelic/newrelic-cli/internal/utils" - - "github.com/imdario/mergo" - "github.com/jedib0t/go-pretty/v6/text" + "github.com/mitchellh/go-homedir" log "github.com/sirupsen/logrus" "github.com/spf13/viper" - - "github.com/newrelic/newrelic-cli/internal/output" ) const ( - // DefaultConfigName is the default name of the global configuration file - DefaultConfigName = "config" - - // DefaultConfigType to read, though any file type supported by viper is allowed - DefaultConfigType = "json" + configType = "json" + globalScopeIdentifier = "*" + DefaultDefaultProfileName = "default" +) - // DefaultEnvPrefix is used when reading environment variables - DefaultEnvPrefix = "NEW_RELIC_CLI" +type CfgFieldKey string +type ProfileFieldKey string - globalScopeIdentifier = "*" +const ( + LogLevel CfgFieldKey = "loglevel" + PluginDir CfgFieldKey = "plugindir" + PrereleaseFeatures CfgFieldKey = "prereleasefeatures" + SendUsageData CfgFieldKey = "sendusagedata" + + UserKey ProfileFieldKey = "apiKey" + Region ProfileFieldKey = "region" + AccountID ProfileFieldKey = "accountID" + InsightsInsertKey ProfileFieldKey = "insightsInsertKey" + LicenseKey ProfileFieldKey = "licenseKey" ) var ( - // DefaultConfigDirectory is the default location for the CLI config files - DefaultConfigDirectory string - - defaultConfig *Config + ConfigFields = []CfgField{ + { + Name: "LogLevel", + Key: LogLevel, + Default: "info", + ValidationFunc: stringInSlice(LogLevels(), false), + }, + { + Name: "SendUsageData", + Key: SendUsageData, + Default: TernaryValues.Unknown, + ValidationFunc: stringInSlice(ValidTernaryValues, false), + }, + { + Name: "PluginDir", + Key: PluginDir, + Default: "", + }, + { + Name: "PrereleaseFeatures", + Key: PrereleaseFeatures, + Default: TernaryValues.Unknown, + ValidationFunc: stringInSlice(ValidTernaryValues, false), + }, + } + ProfileFields = []ProfileField{ + { + Name: "APIKey", + Key: UserKey, + EnvOverride: "NEW_RELIC_API_KEY", + }, + { + Name: "Region", + Key: Region, + EnvOverride: "NEW_RELIC_REGION", + ValidationFunc: stringInSlice(validRegions(), false), + }, + { + Name: "AccountID", + Key: AccountID, + EnvOverride: "NEW_RELIC_ACCOUNT_ID", + ValidationFunc: isNumber(), + }, + { + Name: "LicenseKey", + Key: LicenseKey, + EnvOverride: "NEW_RELIC_LICENSE_KEY", + }, + { + Name: "InsightsInsertKey", + Key: InsightsInsertKey, + EnvOverride: "NEW_RELIC_INSIGHTS_INSERT_KEY", + }, + } + + EnvVarResolver envResolver = &OSEnvResolver{} + ConfigDir string + ProfileOverride string + AccountIDOverride int + LogLevelOverride string + configFilename = "config.json" + credsFilename = "credentials.json" + defaultProfileFilename = "default-profile.json" ) -// Config contains the main CLI configuration -type Config struct { - LogLevel string `mapstructure:"logLevel"` // LogLevel for verbose output - PluginDir string `mapstructure:"pluginDir"` // PluginDir is the directory where plugins will be installed - SendUsageData Ternary `mapstructure:"sendUsageData"` // SendUsageData enables sending usage statistics to New Relic - PreReleaseFeatures Ternary `mapstructure:"preReleaseFeatures"` // PreReleaseFeatures enables display on features within the CLI that are announced but not generally available to customers +type CfgField struct { + Name string + Key CfgFieldKey + Default interface{} + ValidationFunc func(interface{}) error +} - configDir string +type ProfileField struct { + Name string + Key ProfileFieldKey + EnvOverride string + ValidationFunc func(interface{}) error } -// Value represents an instance of a configuration field. -type Value struct { +type CfgValue struct { Name string Value interface{} Default interface{} } -// IsDefault returns true if the field's value is the default value. -func (c *Value) IsDefault() bool { - if v, ok := c.Value.(string); ok { - return strings.EqualFold(v, c.Default.(string)) +func init() { + var err error + ConfigDir, err = getDefaultConfigDirectory() + if err != nil { + log.Debug(err) } - - return c.Value == c.Default } -func init() { - defaultConfig = &Config{ - LogLevel: DefaultLogLevel, - SendUsageData: TernaryValues.Unknown, - PreReleaseFeatures: TernaryValues.Unknown, +func GetLogLevel() string { + if LogLevelOverride != "" { + return LogLevelOverride } - cfgDir, err := utils.GetDefaultConfigDirectory() + return GetConfigValueString(LogLevel) +} + +func GetConfigValueString(key CfgFieldKey) string { + f := findConfigField(key) + v, err := GetConfigValue(key) if err != nil { - log.Fatalf("error building default config directory: %s", err) + log.Debugf("could not get config value %s, using default value %s", key, f.Default) + return f.Default.(string) } - DefaultConfigDirectory = cfgDir - defaultConfig.PluginDir = DefaultConfigDirectory + "/plugins" -} - -// LoadConfig loads the configuration from disk, substituting defaults -// if the file does not exist. -func LoadConfig(configDir string) (*Config, error) { - log.Debugf("loading config file from %s", configDir) + if s, ok := v.(Ternary); ok { + return s.String() + } - if configDir == "" { - configDir = DefaultConfigDirectory - } else { - configDir = os.ExpandEnv(configDir) + if s, ok := v.(string); ok { + return s } - cfg, err := load(configDir) + log.Debugf("could not get config value %s, using default value %s", key, f.Default) + return f.Default.(string) +} + +func GetConfigValueTernary(key CfgFieldKey) Ternary { + f := findConfigField(key) + v, err := GetConfigValue(key) if err != nil { - return nil, err + log.Debugf("could not get config value %s, using default value %s", key, f.Default) + return f.Default.(Ternary) } - initLogger(cfg.LogLevel) - cfg.configDir = configDir + if s, ok := v.(string); ok { + return Ternary(s) + } - return cfg, nil + log.Debugf("could not get config value %s, using default value %s", key, f.Default) + return f.Default.(Ternary) } -// List outputs a list of all the configuration values -func (c *Config) List() { - output.Text(c.getAll("")) +func GetConfigValue(key CfgFieldKey) (interface{}, error) { + if ok := isValidConfigKey(key); !ok { + return nil, fmt.Errorf("config key %s is not valid. valid keys are %s", key, validConfigFieldKeys()) + } + + return config().Get(keyGlobalScope(string(key))), nil } -// Delete deletes a config value. -// This has the effect of reverting the value back to its default. -func (c *Config) Delete(key string) error { - defaultValue, err := c.getDefaultValue(key) - if err != nil { - return err +func GetProfileValue(profileName string, key ProfileFieldKey) (interface{}, error) { + if ok := isValidProfileKey(key); !ok { + return nil, fmt.Errorf("credential key %s is not valid. valid keys are %s", key, validProfileFieldKeys()) } - err = c.set(key, defaultValue) - if err != nil { - return err + if o := getProfileValueEnvOverride(key); o != "" { + log.Infof("using env var override for config field %s", key) + return o, nil } - output.Printf("%s %s removed successfully\n", text.FgGreen.Sprint("✔"), text.Bold.Sprint(key)) - - return nil + return profiles().Get(keyProfile(profileName, key)), nil } -// Get retrieves a config value. -func (c *Config) Get(key string) { - output.Text(c.getAll(key)) +func GetActiveProfileValue(key ProfileFieldKey) (interface{}, error) { + return GetProfileValue(GetActiveProfileName(), key) } -// Set is used to update a config value. -func (c *Config) Set(key string, value interface{}) error { - if !stringInStrings(key, validConfigKeys()) { - return fmt.Errorf("\"%s\" is not a valid key; Please use one of: %s", key, validConfigKeys()) - } - - err := c.set(key, value) - if err != nil { - return err +func GetActiveProfileAccountID() int { + if AccountIDOverride != 0 { + return AccountIDOverride } - output.Printf("%s set to %s\n", text.Bold.Sprint(key), text.FgCyan.Sprint(value)) + return GetProfileValueInt(GetActiveProfileName(), AccountID) +} - return nil +func GetActiveProfileValueInt(key ProfileFieldKey) int { + return GetProfileValueInt(GetActiveProfileName(), key) } -func load(configDir string) (*Config, error) { - cfgViper, err := readConfig(configDir) +func GetProfileValueInt(profileName string, key ProfileFieldKey) int { + v, err := GetProfileValue(profileName, key) if err != nil { - return nil, err + log.Debugf("could not get profile value %s, using default value", key) + return 0 } - allScopes, err := unmarshalAllScopes(cfgViper) + if i, ok := v.(int); ok { + return i + } - if err != nil { - return nil, err + if i, ok := v.(float64); ok { + return int(i) } - config, ok := (*allScopes)[globalScopeIdentifier] - if !ok { - config = Config{} + if s, ok := v.(string); ok { + i, err := strconv.Atoi(s) + if err != nil { + log.Debugf("could not get profile value %s, using default value", key) + return 0 + } + + return i } - err = config.setDefaults() + log.Debugf("could not get profile value %s, using default value", key) + return 0 +} + +func GetActiveProfileValueString(key ProfileFieldKey) string { + return GetProfileValueString(GetActiveProfileName(), key) +} + +func GetProfileValueString(profileName string, key ProfileFieldKey) string { + v, err := GetProfileValue(profileName, key) if err != nil { - return nil, err + log.Debugf("could not get profile value %s, using default value", key) + return "" } - err = config.applyOverrides() - if err != nil { - return nil, err + if s, ok := v.(string); ok { + return s } - return &config, nil + log.Debugf("could not get profile value %s, using default value", key) + return "" } -func (c *Config) createFile(path string, cfgViper *viper.Viper) error { - err := c.visitAllConfigFields(func(v *Value) error { - cfgViper.Set(globalScopeIdentifier+"."+v.Name, v.Value) - return nil - }) - if err != nil { - return err +func GetActiveProfileName() string { + defaultProfile := defaultProfileName() + if ProfileOverride == "" { + return defaultProfile } - err = os.MkdirAll(c.configDir, os.ModePerm) - if err != nil { - return err + if !ProfileExists(ProfileOverride) { + log.Warnf("profile %s requested but not found. using default profile: %s", ProfileOverride, defaultProfile) + return defaultProfile } - log.Debugf("creating config file at %s: %+v", path, cfgViper.AllSettings()) + log.Infof("using requested profile %s", ProfileOverride) + return ProfileOverride +} - err = cfgViper.WriteConfigAs(path) - if err != nil { - return err - } +func GetDefaultProfileName() string { + return defaultProfileName() +} - return nil +func SaveDefaultProfileName(profileName string) error { + return saveDefaultProfileName(profileName) } -func (c *Config) getAll(key string) []Value { - values := []Value{} +func SaveConfigValue(key CfgFieldKey, value interface{}) error { + field := findConfigField(key) - err := c.visitAllConfigFields(func(v *Value) error { - // Return early if name was supplied and doesn't match - if key != "" && key != v.Name { - return nil + if field == nil { + return fmt.Errorf("config key %s is not valid. valid keys are %s", key, validConfigFieldKeys()) + } + + if field.ValidationFunc != nil { + if err := field.ValidationFunc(value); err != nil { + return fmt.Errorf("config value %s is not valid for key %s: %s", value, key, err) } + } - values = append(values, *v) + c := config() + c.Set(keyGlobalScope(string(key)), value) - return nil - }) - if err != nil { - log.Error(err) + cfgFilePath := path.Join(ConfigDir, configFilename) + if err := c.WriteConfigAs(cfgFilePath); err != nil { + return err } - return values + return nil } -func (c *Config) set(key string, value interface{}) error { - cfgViper, err := readConfig(c.configDir) - if err != nil { - return err +func SaveValueToActiveProfile(key ProfileFieldKey, value interface{}) error { + return SaveValueToProfile(GetActiveProfileName(), key, value) +} + +func SaveValueToProfile(profileName string, key ProfileFieldKey, value interface{}) error { + field := findProfileField(key) + + if field.ValidationFunc != nil { + if err := field.ValidationFunc(value); err != nil { + return fmt.Errorf("config value %s is not valid for key %s: %s", value, key, err) + } } - cfgViper.Set(globalScopeIdentifier+"."+key, value) + p := profiles() + keyPath := fmt.Sprintf("%s.%s", profileName, key) + p.Set(keyPath, value) - allScopes, err := unmarshalAllScopes(cfgViper) - if err != nil { + credsFilePath := path.Join(ConfigDir, credsFilename) + if err := p.WriteConfigAs(credsFilePath); err != nil { return err } - config, ok := (*allScopes)[globalScopeIdentifier] - if !ok { - return fmt.Errorf("failed to locate global scope") + if defaultProfileName() == "" { + log.Infof("setting %s as default profile", profileName) + if err := SaveDefaultProfileName(profileName); err != nil { + return err + } } - err = config.setDefaults() - if err != nil { - return err + return nil +} + +func RemoveProfile(profileName string) error { + if !ProfileExists(profileName) { + log.Fatalf("profile not found: %s", profileName) } - err = config.validate() + p := profiles() + configMap := p.AllSettings() + delete(configMap, profileName) + + encodedConfig, _ := json.MarshalIndent(configMap, "", " ") + err := p.ReadConfig(bytes.NewReader(encodedConfig)) if err != nil { return err } - // Update our instance of the config with what was taken from cfgViper. This - // is required for the createFile below to function properly, as it relies on - // the instance values. - if err := mergo.Merge(c, config, mergo.WithOverride); err != nil { + credsFilePath := path.Join(ConfigDir, credsFilename) + if err := p.WriteConfigAs(credsFilePath); err != nil { return err } - path := fmt.Sprintf("%s/%s.%s", c.configDir, DefaultConfigName, DefaultConfigType) - if _, err := os.Stat(path); os.IsNotExist(err) { - createErr := c.createFile(path, cfgViper) - if createErr != nil { - return createErr - } - } else { - log.Debugf("writing config file at %s", path) - err = cfgViper.WriteConfigAs(path) - if err != nil { - log.Error(err) + if defaultProfileName() == profileName { + log.Infof("unsetting %s as default profile.", profileName) + + defaultProfileFilePath := filepath.Join(ConfigDir, defaultProfileFilename) + if err := os.Remove(defaultProfileFilePath); err != nil { + log.Warnf("could not unset default profile %s", profileName) } } return nil } -func (c *Config) getDefaultValue(key string) (interface{}, error) { - var dv interface{} - var found bool +func GetProfileNames() []string { + profileMap := map[string]interface{}{} + if err := profiles().Unmarshal(&profileMap); err != nil { + log.Debug(err) + return []string{} + } - err := c.visitAllConfigFields(func(v *Value) error { - if key == v.Name { - dv = v.Default - found = true - } + n := []string{} + for k := range profileMap { + n = append(n, k) + } - return nil - }) + return n +} - if err != nil { - return dv, err +func getProfileValueEnvOverride(key ProfileFieldKey) string { + field := findProfileField(key) + if e := EnvVarResolver.Getenv(field.EnvOverride); e != "" { + return e } - if found { - return dv, nil + return "" +} + +func config() *viper.Viper { + v, err := loadConfigFile() + if err != nil { + if err == os.ErrNotExist { + log.Debug("config file not found, writing defaults") + err = writeConfigDefaults(v) + if err != nil { + log.Fatal("could not write config defaults") + } + } + + log.Debug(err) } - return nil, fmt.Errorf("failed to locate default value for %s", key) + return v } -func (c *Config) applyOverrides() error { - log.Debug("setting config overrides") - - if v := os.Getenv("NEW_RELIC_CLI_PRERELEASEFEATURES"); v != "" { - c.PreReleaseFeatures = Ternary(v) +func profiles() *viper.Viper { + v, err := loadCredsFile() + if err != nil { + log.Debug(err) } - return nil + return v } -func (c *Config) setDefaults() error { - log.Debug("setting config default") +func ProfileExists(profile string) bool { + for _, p := range GetProfileNames() { + if strings.EqualFold(profile, p) { + return true + } + } + + return false +} - if c == nil { - return nil +func writeConfigDefaults(v *viper.Viper) error { + for _, c := range ConfigFields { + v.Set(keyGlobalScope(string(c.Key)), c.Default) } - if err := mergo.Merge(c, defaultConfig); err != nil { + if err := v.WriteConfigAs(path.Join(ConfigDir, configFilename)); err != nil { return err } return nil } -func (c *Config) validate() error { - err := c.visitAllConfigFields(func(v *Value) error { - switch k := strings.ToLower(v.Name); k { - case "loglevel": - validValues := []string{"Info", "Debug", "Trace", "Warn", "Error"} - if !stringInStringsIgnoreCase(v.Value.(string), validValues) { - return fmt.Errorf("\"%s\" is not a valid %s value; Please use one of: %s", v.Value, v.Name, validValues) - } - case "sendusagedata", "prereleasefeatures": - err := (v.Value.(Ternary)).Valid() - if err != nil { - return fmt.Errorf("invalid value for '%s': %s", v.Name, err) - } - } +func loadConfigFile() (*viper.Viper, error) { + v := viper.New() + v.SetConfigName(configFilename) + v.SetConfigType(configType) + v.AddConfigPath(ConfigDir) - return nil - }) + for _, c := range ConfigFields { + v.SetDefault(fmt.Sprintf("*.%s", c.Key), c.Default) + } - if err != nil { - return err + if err := loadFile(v); err != nil { + return nil, err } - return nil + return v, nil } -func (c *Config) visitAllConfigFields(f func(*Value) error) error { - cfgType := reflect.TypeOf(*c) - cfgValue := reflect.ValueOf(*c) - defaultCfgValue := reflect.ValueOf(*defaultConfig) +func loadCredsFile() (*viper.Viper, error) { + v := viper.New() + v.SetConfigName(credsFilename) + v.SetConfigType(configType) + v.AddConfigPath(ConfigDir) - // Iterate through the fields in the struct - for i := 0; i < cfgType.NumField(); i++ { - field := cfgType.Field(i) + if err := loadFile(v); err != nil { + return nil, fmt.Errorf("credentials file not found: %s", path.Join(ConfigDir, credsFilename)) + } - // Skip unexported fields - if field.PkgPath != "" { - continue - } + return v, nil +} - name := field.Tag.Get("mapstructure") - value := cfgValue.Field(i).Interface() - defaultValue := defaultCfgValue.Field(i).Interface() +func defaultProfileName() string { + p, err := loadDefaultProfileName() + if err != nil { + log.Debug("default profile not found") + } - err := f(&Value{ - Name: name, - Value: value, - Default: defaultValue, - }) + return p +} - if err != nil { - return err - } +func loadDefaultProfileName() (string, error) { + defaultProfileFilePath := filepath.Join(ConfigDir, defaultProfileFilename) + defaultProfileBytes, err := ioutil.ReadFile(defaultProfileFilePath) + if err != nil { + return "", err } - return nil + v := strings.Trim(string(defaultProfileBytes), "\"") + + return v, nil } -func unmarshalAllScopes(cfgViper *viper.Viper) (*map[string]Config, error) { - cfgMap := map[string]Config{} - err := cfgViper.Unmarshal(&cfgMap) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal config with error: %v", err) +func loadFile(v *viper.Viper) error { + err := v.ReadInConfig() + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + log.Debug("file not found, using defaults") + } else if e, ok := err.(viper.ConfigParseError); ok { + return e } - return &cfgMap, nil + return nil } -func readConfig(configDir string) (*viper.Viper, error) { - cfgViper := viper.New() - cfgViper.SetEnvPrefix(DefaultEnvPrefix) - cfgViper.SetConfigName(DefaultConfigName) - cfgViper.SetConfigType(DefaultConfigType) - cfgViper.AddConfigPath(configDir) // adding provided directory as search path - cfgViper.AutomaticEnv() // read in environment variables that match +func saveDefaultProfileName(profileName string) error { + defaultProfileFilePath := filepath.Join(ConfigDir, defaultProfileFilename) - err := cfgViper.ReadInConfig() - // nolint - if err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - log.Debug("no config file found, using defaults") - } else if e, ok := err.(viper.ConfigParseError); ok { - return nil, fmt.Errorf("error parsing config file: %v", e) - } + if err := ioutil.WriteFile(defaultProfileFilePath, []byte("\""+profileName+"\""), 0640); err != nil { + return err } - return cfgViper, nil + return nil } -func validConfigKeys() []string { - var keys []string +func keyGlobalScope(key string) string { + return fmt.Sprintf("%s.%s", globalScopeIdentifier, key) +} + +func keyProfile(profileName string, key ProfileFieldKey) string { + return fmt.Sprintf("%s.%s", profileName, key) +} - cfgType := reflect.TypeOf(Config{}) - for i := 0; i < cfgType.NumField(); i++ { - field := cfgType.Field(i) - name := field.Tag.Get("mapstructure") - keys = append(keys, name) +func getDefaultConfigDirectory() (string, error) { + home, err := homedir.Dir() + if err != nil { + return "", err } - return keys + return fmt.Sprintf("%s/.newrelic", home), nil } -func stringInStrings(s string, ss []string) bool { - for _, v := range ss { - if v == s { - return true +func findProfileField(key ProfileFieldKey) *ProfileField { + profileKey := string(key) + + for _, c := range ProfileFields { + if strings.EqualFold(profileKey, string(c.Key)) { + return &c } } - return false + return nil } -// Function ignores the case -func stringInStringsIgnoreCase(s string, ss []string) bool { - for _, v := range ss { - if strings.EqualFold(v, s) { - return true +func findConfigField(key CfgFieldKey) *CfgField { + configKey := string(key) + + for _, c := range ConfigFields { + if strings.EqualFold(configKey, string(c.Key)) { + return &c } } - return false + return nil } diff --git a/internal/config/config_integration_test.go b/internal/config/config_integration_test.go index 070e78389..30f335e0e 100644 --- a/internal/config/config_integration_test.go +++ b/internal/config/config_integration_test.go @@ -3,114 +3,323 @@ package config import ( + "fmt" "io/ioutil" + "math/rand" "os" + "path" + "path/filepath" "testing" + "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestConfigSetLogLevel(t *testing.T) { - f, err := ioutil.TempDir("/tmp", "newrelic") - assert.NoError(t, err) - defer os.RemoveAll(f) - - // Initialize the new configuration directory - c, err := LoadConfig(f) - assert.NoError(t, err) - assert.Equal(t, c.configDir, f) - - // Set the valid log levels - for _, l := range []string{ - "ERROR", - "WARN", - "INFO", - "DEBUG", - "TRACE", - } { - err = c.Set("logLevel", l) - assert.NoError(t, err) - assert.Equal(t, l, c.LogLevel) - - // Double check that the config is written to disk - var c2 *Config - c2, err = LoadConfig(f) - assert.NoError(t, err) - assert.Equal(t, l, c2.LogLevel) - } +func TestGetConfigValue_Basic(t *testing.T) { + mockConfigFiles := createMockConfigFiles(t) + defer mockConfigFiles.teardown() + + configValue, err := GetConfigValue(LogLevel) + require.NoError(t, err) + require.Equal(t, "info", configValue) + + configValue, err = GetConfigValue(PluginDir) + require.NoError(t, err) + require.Equal(t, "/tmp", configValue) + + configValue, err = GetConfigValue(PrereleaseFeatures) + require.NoError(t, err) + require.Equal(t, TernaryValues.Unknown.String(), configValue) + + configValue, err = GetConfigValue(SendUsageData) + require.NoError(t, err) + require.Equal(t, TernaryValues.Unknown.String(), configValue) +} + +func TestGetConfigValue_InvalidKey(t *testing.T) { + mockConfigFiles := createMockConfigFiles(t) + defer mockConfigFiles.teardown() + + _, err := GetConfigValue("LOGLEVEL") + require.NoError(t, err) + + _, err = GetConfigValue("logLevel") + require.NoError(t, err) + + _, err = GetConfigValue("logLevel") + require.NoError(t, err) + + _, err = GetConfigValue("invalidKey") + require.Error(t, err) +} + +func TestGetConfigValue_DefaultValues(t *testing.T) { + setupBlankSlateScenario(t) + + configValue, err := GetConfigValue(LogLevel) + require.NoError(t, err) + require.Equal(t, "info", configValue) + + configValue, err = GetConfigValue(PluginDir) + require.NoError(t, err) + require.Equal(t, "", configValue) + + configValue, err = GetConfigValue(PrereleaseFeatures) + require.NoError(t, err) + require.Equal(t, TernaryValues.Unknown, configValue) + + configValue, err = GetConfigValue(SendUsageData) + require.NoError(t, err) + require.Equal(t, TernaryValues.Unknown, configValue) +} + +func TestSetConfigValue_Basic(t *testing.T) { + mockConfigFiles := createMockConfigFiles(t) + defer mockConfigFiles.teardown() + + err := SaveConfigValue(LogLevel, "debug") + require.NoError(t, err) + + configValue, err := GetConfigValue(LogLevel) + require.NoError(t, err) + require.Equal(t, "debug", configValue) +} + +func TestSetConfigValue_FileNotExists(t *testing.T) { + setupBlankSlateScenario(t) + + configFilePath := path.Join(ConfigDir, configFilename) + + _, err := os.Stat(configFilePath) + require.True(t, os.IsNotExist(err)) + + err = SaveConfigValue(LogLevel, "debug") + require.NoError(t, err) + + configValue, err := GetConfigValue(LogLevel) + require.NoError(t, err) + require.Equal(t, "debug", configValue) + + _, err = os.Stat(configFilePath) + require.NoError(t, err) + + os.Remove(configFilePath) +} + +func TestSetConfigValue_InvalidValue(t *testing.T) { + mockConfigFiles := createMockConfigFiles(t) + defer mockConfigFiles.teardown() + + var err error + + err = SaveConfigValue(LogLevel, "invalid") + require.Error(t, err) + + err = SaveConfigValue(PrereleaseFeatures, "invalid") + require.Error(t, err) + + err = SaveConfigValue(SendUsageData, "invalid") + require.Error(t, err) + + err = SaveConfigValue(PluginDir, "/any/path/is/valid") + require.NoError(t, err) +} + +func TestGetActiveProfileValue_Basic(t *testing.T) { + EnvVarResolver = &MockEnvResolver{} + mockConfigFiles := createMockConfigFiles(t) + defer mockConfigFiles.teardown() + + credsValue, err := GetActiveProfileValue(UserKey) + require.NoError(t, err) + require.Equal(t, "testApiKey", credsValue) +} + +func TestGetActiveProfileValue_InvalidKey(t *testing.T) { + EnvVarResolver = &MockEnvResolver{} + mockConfigFiles := createMockConfigFiles(t) + defer mockConfigFiles.teardown() + + _, err := GetActiveProfileValue("APIKEY") + require.NoError(t, err) - err = c.Set("logLevel", "INVALID_VALUE") - assert.Error(t, err) + _, err = GetActiveProfileValue("apiKey") + require.NoError(t, err) - err = c.Set("loglevel", "Info") - assert.Error(t, err) + _, err = GetActiveProfileValue("apikey") + require.NoError(t, err) - err = c.Set("Loglevel", "Debug") - assert.Error(t, err) + _, err = GetActiveProfileValue("invalidKey") + require.Error(t, err) +} + +func TestGetActiveProfileValue_EnvVarOverride(t *testing.T) { + m := &MockEnvResolver{} + EnvVarResolver = m + mockConfigFiles := createMockConfigFiles(t) + defer mockConfigFiles.teardown() + + m.GetenvVal = "newAPIKey" + + credsValue, err := GetActiveProfileValue(UserKey) + require.NoError(t, err) + require.Equal(t, "newAPIKey", credsValue) +} + +func TestSetProfileValue_Basic(t *testing.T) { + EnvVarResolver = &MockEnvResolver{} + mockConfigFiles := createMockConfigFiles(t) + defer mockConfigFiles.teardown() + + err := SaveValueToProfile(DefaultDefaultProfileName, UserKey, "NRAK-abc123") + require.NoError(t, err) + + credsValue, err := GetProfileValue(DefaultDefaultProfileName, UserKey) + require.NoError(t, err) + require.Equal(t, "NRAK-abc123", credsValue) +} + +func TestSetProfileValue_FileNotExists(t *testing.T) { + EnvVarResolver = &MockEnvResolver{} + setupBlankSlateScenario(t) + + credsFilePath := path.Join(ConfigDir, credsFilename) + defaultProfileFilePath := path.Join(ConfigDir, defaultProfileFilename) + + _, err := os.Stat(credsFilePath) + require.True(t, os.IsNotExist(err)) + + credsValue, err := GetActiveProfileValue(UserKey) + require.NoError(t, err) + require.Nil(t, credsValue) + + err = SaveValueToProfile("default", UserKey, "NRAK-abc123") + require.NoError(t, err) + + credsValue, err = GetActiveProfileValue(UserKey) + require.NoError(t, err) + require.Equal(t, "NRAK-abc123", credsValue) + + _, err = os.Stat(credsFilePath) + require.NoError(t, err) + + err = os.Remove(credsFilePath) + require.NoError(t, err) + + err = os.Remove(defaultProfileFilePath) + require.NoError(t, err) +} + +func TestGetDefaultProfileName_Basic(t *testing.T) { + mockConfigFiles := createMockConfigFiles(t) + defer mockConfigFiles.teardown() + + require.Equal(t, "default", GetDefaultProfileName()) +} + +func TestSetDefaultProfileName_FileNotExists(t *testing.T) { + EnvVarResolver = &MockEnvResolver{} + setupBlankSlateScenario(t) + + defaultProfileFilePath := path.Join(ConfigDir, defaultProfileFilename) + + _, err := os.Stat(defaultProfileFilePath) + require.True(t, os.IsNotExist(err)) + + require.Empty(t, GetDefaultProfileName()) + + err = SaveDefaultProfileName("default") + require.NoError(t, err) + require.Equal(t, "default", GetDefaultProfileName()) + _, err = os.Stat(defaultProfileFilePath) + require.NoError(t, err) + + os.Remove(defaultProfileFilePath) } -func TestConfigSetSendUsageData(t *testing.T) { - f, err := ioutil.TempDir("/tmp", "newrelic") - assert.NoError(t, err) - defer os.RemoveAll(f) +type mockConfigFiles struct { + configFile *os.File + credsFile *os.File + defaultProfileFile *os.File +} - // Initialize the new configuration directory - c, err := LoadConfig(f) - assert.NoError(t, err) - assert.Equal(t, c.configDir, f) +func (s *mockConfigFiles) teardown() { + os.Remove(s.configFile.Name()) + os.Remove(s.credsFile.Name()) + os.Remove(s.defaultProfileFile.Name()) +} - // Set the valid sendUsageData values - for _, l := range []Ternary{ - TernaryValues.Allow, - TernaryValues.Disallow, - TernaryValues.Unknown, - } { - err = c.Set("sendUsageData", l) - assert.NoError(t, err) - assert.Equal(t, l, c.SendUsageData) +func createMockConfigFiles(t *testing.T) mockConfigFiles { + configFile, err := ioutil.TempFile("", "config*.json") + require.NoError(t, err) + + configJSON := ` +{ + "*": { + "loglevel": "info", + "plugindir": "/tmp", + "prereleasefeatures": "NOT_ASKED", + "sendusagedata": "NOT_ASKED" } +} +` + _, err = configFile.Write([]byte(configJSON)) + require.NoError(t, err) - err = c.Set("sendUsageData", "INVALID_VALUE") - assert.Error(t, err) -} - -func TestConfigSetPreReleaseFeatures(t *testing.T) { - f, err := ioutil.TempDir("/tmp", "newrelic") - assert.NoError(t, err) - defer os.RemoveAll(f) - - // Initialize the new configuration directory - c, err := LoadConfig(f) - assert.NoError(t, err) - assert.Equal(t, c.configDir, f) - - // Set the valid pre-release feature values - for _, l := range []Ternary{ - TernaryValues.Allow, - TernaryValues.Disallow, - TernaryValues.Unknown, - } { - err = c.Set("preReleaseFeatures", l) - assert.NoError(t, err) - assert.Equal(t, l, c.PreReleaseFeatures) + credsFile, err := ioutil.TempFile("", "credentials*.json") + require.NoError(t, err) + + credsJSON := ` +{ + "default": { + "apiKey": "testApiKey", + "region": "US", + "accountID": 12345, + "licenseKey": "testLicenseKey" + } +} +` + _, err = credsFile.Write(([]byte(credsJSON))) + require.NoError(t, err) + + defaultProfileFile, err := ioutil.TempFile("", "default-profile*.json") + require.NoError(t, err) + + defaultProfileJSON := `"default"` + + _, err = defaultProfileFile.Write(([]byte(defaultProfileJSON))) + require.NoError(t, err) + + // package-level vars + ConfigDir = filepath.Dir(configFile.Name()) + configFilename = filepath.Base(configFile.Name()) + credsFilename = filepath.Base(credsFile.Name()) + defaultProfileFilename = filepath.Base(defaultProfileFile.Name()) + + s := mockConfigFiles{ + configFile: configFile, + credsFile: credsFile, + defaultProfileFile: defaultProfileFile, } - err = c.Set("preReleaseFeatures", "INVALID_VALUE") - assert.Error(t, err) + return s } -func TestConfigSetPluginDir(t *testing.T) { - f, err := ioutil.TempDir("/tmp", "newrelic") - assert.NoError(t, err) - defer os.RemoveAll(f) +func setupBlankSlateScenario(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + configFilename = fmt.Sprintf("config%s.json", randNumBytes(8)) + credsFilename = fmt.Sprintf("creds%s.json", randNumBytes(8)) + defaultProfileFilename = fmt.Sprintf("default-profile%s.json", randNumBytes(8)) + ConfigDir = os.TempDir() +} - // Initialize the new configuration directory - c, err := LoadConfig(f) - assert.NoError(t, err) - assert.Equal(t, c.configDir, f) +const numBytes = "0123456789" - err = c.Set("pluginDir", "test") - assert.NoError(t, err) - assert.Equal(t, "test", c.PluginDir) +func randNumBytes(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = numBytes[rand.Intn(len(numBytes))] + } + return string(b) } diff --git a/internal/config/env_resolver.go b/internal/config/env_resolver.go new file mode 100644 index 000000000..98e28a2d6 --- /dev/null +++ b/internal/config/env_resolver.go @@ -0,0 +1,39 @@ +package config + +import "os" + +type envResolver interface { + Getenv(key string) string +} + +type OSEnvResolver struct{} + +func (r *OSEnvResolver) Getenv(key string) string { + return os.Getenv(key) +} + +func NewOSEnvResolver() *OSEnvResolver { + return &OSEnvResolver{} +} + +type MockEnvResolver struct { + GetenvVal string + GetenvVals map[string]string +} + +func NewMockEnvResolver() *MockEnvResolver { + return &MockEnvResolver{ + GetenvVals: map[string]string{}, + } +} + +func (r *MockEnvResolver) Setenv(key string, val string) { + r.GetenvVals[key] = val +} + +func (r *MockEnvResolver) Getenv(key string) string { + if r.GetenvVals != nil { + return r.GetenvVals[key] + } + return r.GetenvVal +} diff --git a/internal/config/helpers.go b/internal/config/helpers.go deleted file mode 100644 index a40ed8ca5..000000000 --- a/internal/config/helpers.go +++ /dev/null @@ -1,20 +0,0 @@ -package config - -import ( - log "github.com/sirupsen/logrus" -) - -// WithConfig loads and returns the CLI configuration. -func WithConfig(f func(c *Config)) { - WithConfigFrom(DefaultConfigDirectory, f) -} - -// WithConfigFrom loads and returns the CLI configuration from a specified location. -func WithConfigFrom(configDir string, f func(c *Config)) { - c, err := LoadConfig(configDir) - if err != nil { - log.Fatal(err) - } - - f(c) -} diff --git a/internal/config/logging.go b/internal/config/logging.go index de566e46e..2b97ea378 100644 --- a/internal/config/logging.go +++ b/internal/config/logging.go @@ -16,11 +16,50 @@ const ( DefaultLogFile = "newrelic-cli.log" ) +// LogLevelValue represents a single log level value +type LogLevelValue string + +func (l LogLevelValue) String() string { + return string(l) +} + +// LogLevelValues provides the set of log level values +var LogLevelValues = struct { + Panic LogLevelValue + Fatal LogLevelValue + Error LogLevelValue + Warn LogLevelValue + Info LogLevelValue + Debug LogLevelValue + Trace LogLevelValue +}{ + Panic: "panic", + Fatal: "fatal", + Error: "error", + Warn: "warn", + Info: "info", + Debug: "debug", + Trace: "trace", +} + +// LogLevels returns a slice of valid log values +func LogLevels() []string { + return []string{ + LogLevelValues.Panic.String(), + LogLevelValues.Fatal.String(), + LogLevelValues.Error.String(), + LogLevelValues.Warn.String(), + LogLevelValues.Info.String(), + LogLevelValues.Debug.String(), + LogLevelValues.Trace.String(), + } +} + var ( fileHookConfigured = false ) -func initLogger(logLevel string) { +func InitLogger(logLevel string) { l := log.StandardLogger() l.SetFormatter(&log.TextFormatter{ @@ -29,16 +68,16 @@ func initLogger(logLevel string) { EnvironmentOverrideColors: true, }) - _, err := os.Stat(DefaultConfigDirectory) + _, err := os.Stat(ConfigDir) if os.IsNotExist(err) { - errDir := os.MkdirAll(DefaultConfigDirectory, 0750) + errDir := os.MkdirAll(ConfigDir, 0750) if errDir != nil { log.Warnf("Could not create log file folder: %s", err) } } - fileHook, err := NewLogrusFileHook(DefaultConfigDirectory+"/"+DefaultLogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0640) + fileHook, err := NewLogrusFileHook(ConfigDir+"/"+DefaultLogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0640) if err == nil && !fileHookConfigured { l.Hooks.Add(fileHook) fileHookConfigured = true diff --git a/internal/config/ternary.go b/internal/config/ternary.go index f4bccffdb..09958d41f 100644 --- a/internal/config/ternary.go +++ b/internal/config/ternary.go @@ -24,6 +24,12 @@ var TernaryValues = struct { Unknown: "NOT_ASKED", } +var ValidTernaryValues = []string{ + TernaryValues.Allow.String(), + TernaryValues.Disallow.String(), + TernaryValues.Unknown.String(), +} + // Valid returns true for a valid value, false otherwise func (t Ternary) Valid() error { val := string(t) diff --git a/internal/config/validation.go b/internal/config/validation.go new file mode 100644 index 000000000..44dc00b43 --- /dev/null +++ b/internal/config/validation.go @@ -0,0 +1,104 @@ +package config + +import ( + "fmt" + "strings" + + "github.com/newrelic/newrelic-client-go/pkg/region" +) + +func RequireInsightsInsertKey() (string, error) { + return RequireActiveProfileFieldString(InsightsInsertKey) +} + +func RequireUserKey() (string, error) { + return RequireActiveProfileFieldString(UserKey) +} + +func RequireAccountID() (int, error) { + return RequireActiveProfileFieldInt(AccountID) +} + +func RequireActiveProfileFieldString(key ProfileFieldKey) (string, error) { + v := GetActiveProfileValueString(key) + if v == "" { + f := findProfileField(key) + return "", fmt.Errorf("%s is required, set it in your default profile or use the %s environment variable", key, f.EnvOverride) + } + + return v, nil +} + +func RequireActiveProfileFieldInt(key ProfileFieldKey) (int, error) { + v := GetActiveProfileValueInt(key) + if v == 0 { + f := findProfileField(key) + return 0, fmt.Errorf("%s is required, set it in your default profile or use the %s environment variable", key, f.EnvOverride) + } + + return v, nil +} + +func isValidConfigKey(key CfgFieldKey) bool { + return findConfigField(key) != nil +} + +func isValidProfileKey(key ProfileFieldKey) bool { + return findProfileField(key) != nil +} + +func validConfigFieldKeys() []string { + valid := make([]string, len(ConfigFields)) + + for i, v := range ConfigFields { + valid[i] = string(v.Key) + } + + return valid +} + +func validProfileFieldKeys() []string { + valid := make([]string, len(ProfileFields)) + + for i, v := range ProfileFields { + valid[i] = string(v.Key) + } + + return valid +} + +func stringInSlice(validVals []string, caseSensitive bool) func(interface{}) error { + return func(val interface{}) error { + for _, v := range validVals { + + if !caseSensitive && strings.EqualFold(v, val.(string)) { + return nil + } + + if v == val { + return nil + } + } + + return fmt.Errorf("valid values are %s", validVals) + } +} + +func validRegions() []string { + validRegions := []string{} + for k := range region.Regions { + validRegions = append(validRegions, string(k)) + } + + return validRegions +} + +func isNumber() func(interface{}) error { + return func(val interface{}) error { + if _, ok := val.(int); ok { + return nil + } + + return fmt.Errorf("value is required to be numeric") + } +} diff --git a/internal/credentials/command.go b/internal/credentials/command.go deleted file mode 100644 index 6d7811fe8..000000000 --- a/internal/credentials/command.go +++ /dev/null @@ -1,179 +0,0 @@ -package credentials - -import ( - "github.com/jedib0t/go-pretty/v6/text" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -var ( - // Display keys when printing output - showKeys bool - profileName string - flagRegion string - apiKey string - insightsInsertKey string - accountID int - licenseKey string -) - -// Command is the base command for managing profiles -var Command = &cobra.Command{ - Use: "profile", - Short: "Manage the authentication profiles for this tool", - Aliases: []string{ - "profiles", // DEPRECATED: accept but not consistent with the rest of the singular usage - }, -} - -var cmdAdd = &cobra.Command{ - Use: "add", - Short: "Add a new profile", - Long: `Add a new profile - -The add command creates a new profile for use with the New Relic CLI. -API key and region are required. An Insights insert key is optional, but required -for posting custom events with the ` + "`newrelic events`" + `command. -`, - Example: "newrelic profile add --name --region --apiKey --insightsInsertKey --accountId --licenseKey ", - Run: func(cmd *cobra.Command, args []string) { - WithCredentials(func(creds *Credentials) { - p := Profile{ - Region: flagRegion, - APIKey: apiKey, - InsightsInsertKey: insightsInsertKey, - AccountID: accountID, - LicenseKey: licenseKey, - } - - err := creds.AddProfile(profileName, p) - if err != nil { - log.Fatal(err) - } - - log.Infof("profile %s added", text.FgCyan.Sprint(profileName)) - - if len(creds.Profiles) == 1 { - err := creds.SetDefaultProfile(profileName) - if err != nil { - log.Fatal(err) - } - - log.Infof("setting %s as default profile", text.FgCyan.Sprint(profileName)) - } - }) - }, -} - -var cmdDefault = &cobra.Command{ - Use: "default", - Short: "Set the default profile name", - Long: `Set the default profile name - -The default command sets the profile to use by default using the specified name. -`, - Example: "newrelic profile default --name ", - Run: func(cmd *cobra.Command, args []string) { - WithCredentials(func(creds *Credentials) { - err := creds.SetDefaultProfile(profileName) - if err != nil { - log.Fatal(err) - } - - log.Info("success") - }) - }, -} - -var cmdList = &cobra.Command{ - Use: "list", - Short: "List the profiles available", - Long: `List the profiles available - -The list command prints out the available profiles' credentials. -`, - Example: "newrelic profile list", - Run: func(cmd *cobra.Command, args []string) { - WithCredentials(func(creds *Credentials) { - if creds != nil { - creds.List() - } else { - log.Info("no profiles found") - } - }) - }, - Aliases: []string{ - "ls", - }, -} - -var cmdDelete = &cobra.Command{ - Use: "delete", - Short: "Delete a profile", - Long: `Delete a profile - -The delete command removes the profile specified by name. -`, - Example: "newrelic profile delete --name ", - Run: func(cmd *cobra.Command, args []string) { - WithCredentials(func(creds *Credentials) { - err := creds.RemoveProfile(profileName) - if err != nil { - log.Fatal(err) - } - - log.Info("success") - }) - }, - Aliases: []string{ - "remove", - "rm", - }, -} - -func init() { - var err error - - // Add - Command.AddCommand(cmdAdd) - cmdAdd.Flags().StringVarP(&profileName, "name", "n", "", "unique profile name to add") - cmdAdd.Flags().StringVarP(&flagRegion, "region", "r", "", "the US or EU region") - cmdAdd.Flags().StringVarP(&apiKey, "apiKey", "", "", "your personal API key") - cmdAdd.Flags().StringVarP(&insightsInsertKey, "insightsInsertKey", "", "", "your Insights insert key") - cmdAdd.Flags().StringVarP(&licenseKey, "licenseKey", "", "", "your license key") - cmdAdd.Flags().IntVarP(&accountID, "accountId", "", 0, "your account ID") - err = cmdAdd.MarkFlagRequired("name") - if err != nil { - log.Error(err) - } - - err = cmdAdd.MarkFlagRequired("region") - if err != nil { - log.Error(err) - } - - err = cmdAdd.MarkFlagRequired("apiKey") - if err != nil { - log.Error(err) - } - - // Default - Command.AddCommand(cmdDefault) - cmdDefault.Flags().StringVarP(&profileName, "name", "n", "", "the profile name to set as default") - err = cmdDefault.MarkFlagRequired("name") - if err != nil { - log.Error(err) - } - - // List - Command.AddCommand(cmdList) - cmdList.Flags().BoolVarP(&showKeys, "show-keys", "s", false, "list the profiles on your keychain") - - // Remove - Command.AddCommand(cmdDelete) - cmdDelete.Flags().StringVarP(&profileName, "name", "n", "", "the profile name to delete") - err = cmdDelete.MarkFlagRequired("name") - if err != nil { - log.Error(err) - } -} diff --git a/internal/credentials/credentials.go b/internal/credentials/credentials.go deleted file mode 100644 index 56d30e4e2..000000000 --- a/internal/credentials/credentials.go +++ /dev/null @@ -1,217 +0,0 @@ -package credentials - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "strings" - - "github.com/jedib0t/go-pretty/v6/text" - log "github.com/sirupsen/logrus" - - "github.com/newrelic/newrelic-cli/internal/config" - "github.com/newrelic/newrelic-cli/internal/output" -) - -const ( - // DefaultCredentialsFile is the default place to load profiles from - DefaultCredentialsFile = "credentials" - - defaultConfigType = "json" - defaultProfileString = " (default)" - hiddenKeyString = "" -) - -// Credentials is the metadata around all configured profiles -type Credentials struct { - DefaultProfile string - Profiles map[string]Profile - ConfigDirectory string -} - -// LoadCredentials loads the current CLI credentials from disk. -func LoadCredentials(configDir string) (*Credentials, error) { - log.Debug("loading credentials file") - - if configDir == "" { - configDir = config.DefaultConfigDirectory - } else { - configDir = os.ExpandEnv(configDir) - } - - creds := &Credentials{ - ConfigDirectory: configDir, - } - - profiles, err := LoadProfiles(configDir) - if err != nil { - log.Debugf("no credential profiles: see newrelic profiles --help") - } - - defaultProfile, err := LoadDefaultProfile(configDir) - if err != nil { - log.Debugf("no default profile set: see newrelic profiles --help") - } - - creds.Profiles = *profiles - creds.DefaultProfile = defaultProfile - - return creds, nil -} - -func (c *Credentials) profileExists(profileName string) bool { - for k := range c.Profiles { - if k == profileName { - return true - } - } - - return false -} - -// AddProfile adds a new profile to the credentials file. -func (c *Credentials) AddProfile(profileName string, p Profile) error { - var err error - - if c.profileExists(profileName) { - return fmt.Errorf("profile with name %s already exists", profileName) - } - - // Case fold the region - p.Region = strings.ToUpper(p.Region) - - c.Profiles[profileName] = p - - file, _ := json.MarshalIndent(c.Profiles, "", " ") - defaultCredentialsFile := os.ExpandEnv(fmt.Sprintf("%s/%s.json", c.ConfigDirectory, DefaultCredentialsFile)) - - if _, err = os.Stat(c.ConfigDirectory); os.IsNotExist(err) { - err = os.MkdirAll(c.ConfigDirectory, os.ModePerm) - if err != nil { - return err - } - } - - err = ioutil.WriteFile(defaultCredentialsFile, file, 0600) - if err != nil { - return err - } - - return nil -} - -// RemoveProfile removes an existing profile from the credentials file. -func (c *Credentials) RemoveProfile(profileName string) error { - if !c.profileExists(profileName) { - return fmt.Errorf("profile with name %s not found", profileName) - } - - delete(c.Profiles, profileName) - - file, _ := json.MarshalIndent(c.Profiles, "", " ") - defaultCredentialsFile := os.ExpandEnv(fmt.Sprintf("%s/%s.json", c.ConfigDirectory, DefaultCredentialsFile)) - - err := ioutil.WriteFile(defaultCredentialsFile, file, 0600) - if err != nil { - return err - } - - if profileName == c.DefaultProfile { - c.DefaultProfile = "" - defaultProfileFileName := os.ExpandEnv(fmt.Sprintf("%s/%s.json", c.ConfigDirectory, DefaultProfileFile)) - - err := os.Remove(defaultProfileFileName) - if err != nil { - return err - } - } - - log.Infof("profile %s has been removed", profileName) - - return nil -} - -// SetDefaultProfile modifies the profile name to use by default. -func (c *Credentials) SetDefaultProfile(profileName string) error { - if !c.profileExists(profileName) { - return fmt.Errorf("profile with name %s not found", profileName) - } - - if c.ConfigDirectory == "" { - return fmt.Errorf("credential ConfigDirectory is empty: %s", c.ConfigDirectory) - } - - c.DefaultProfile = profileName - - defaultProfileFileName := os.ExpandEnv(fmt.Sprintf("%s/%s.json", c.ConfigDirectory, DefaultProfileFile)) - content := fmt.Sprintf("\"%s\"", profileName) - - err := ioutil.WriteFile(defaultProfileFileName, []byte(content), 0600) - if err != nil { - return fmt.Errorf("error writing to file %s: %s", defaultProfileFileName, err) - } - - return nil -} - -// List outputs a list of all the configured Credentials -func (c *Credentials) List() { - out := []profileList{} - - // Print them out - for k, v := range c.Profiles { - name := k - - if k == c.DefaultProfile { - name += text.FgHiBlack.Sprint(defaultProfileString) - } - - var accountID int - if v.AccountID != 0 { - accountID = v.AccountID - } - - var apiKey string - if v.APIKey != "" { - apiKey = text.FgHiBlack.Sprint(hiddenKeyString) - } - - var insightsInsertKey string - if v.InsightsInsertKey != "" { - insightsInsertKey = text.FgHiBlack.Sprint(hiddenKeyString) - } - - var licenseKey string - if v.LicenseKey != "" { - licenseKey = text.FgHiBlack.Sprint(hiddenKeyString) - } - - if showKeys { - apiKey = v.APIKey - insightsInsertKey = v.InsightsInsertKey - licenseKey = v.LicenseKey - } - - out = append(out, profileList{ - Name: name, - Region: v.Region, - APIKey: apiKey, - InsightsInsertKey: insightsInsertKey, - AccountID: accountID, - LicenseKey: licenseKey, - }) - } - - output.Text(out) -} - -// The order of fields in this struct dictates the ordering of the output table. -type profileList struct { - Name string - AccountID int - Region string - APIKey string - LicenseKey string - InsightsInsertKey string -} diff --git a/internal/credentials/credentials_integration_test.go b/internal/credentials/credentials_integration_test.go deleted file mode 100644 index 66f0978a3..000000000 --- a/internal/credentials/credentials_integration_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// +build integration - -package credentials - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/newrelic/newrelic-client-go/pkg/region" -) - -var overrideEnvVars = []string{ - "NEW_RELIC_API_KEY", - "NEW_RELIC_REGION", -} - -func TestApplyOverrides(t *testing.T) { - // Do not run this in parallel, we are messing with the environment - - f, err := ioutil.TempDir("/tmp", "newrelic") - assert.NoError(t, err) - defer os.RemoveAll(f) - - // Initialize the new configuration directory - c, err := LoadCredentials(f) - assert.NoError(t, err) - - // Create an initial profile to work with - testProfile := Profile{ - Region: "us", - APIKey: "apiKeyGoesHere", - InsightsInsertKey: "insightsInsertKeyGoesHere", - } - err = c.AddProfile("testCase1", testProfile) - assert.NoError(t, err) - p := c.Profiles["testCase1"] - assert.NotNil(t, p) - - // Clear env vars we are going to mess with, and reset on exit - for _, v := range overrideEnvVars { - if val, ok := os.LookupEnv(v); ok { - defer os.Setenv(v, val) - os.Unsetenv(v) - } - } - - // Default case (no overrides) - p2 := applyOverrides(&p) - assert.NotNil(t, p2) - assert.Equal(t, p.APIKey, p2.APIKey) - assert.Equal(t, p.Region, p2.Region) - - // Override just the API Key - os.Setenv("NEW_RELIC_API_KEY", "anotherAPIKey") - p2 = applyOverrides(&p) - assert.NotNil(t, p2) - assert.Equal(t, "anotherAPIKey", p2.APIKey) - assert.Equal(t, p.Region, p2.Region) - - // Both - os.Setenv("NEW_RELIC_REGION", "US") - p2 = applyOverrides(&p) - assert.NotNil(t, p2) - assert.Equal(t, "anotherAPIKey", p2.APIKey) - assert.Equal(t, region.US.String(), p2.Region) - - // Override just the REGION (valid) - os.Unsetenv("NEW_RELIC_API_KEY") - p2 = applyOverrides(&p) - assert.NotNil(t, p2) - assert.Equal(t, p.APIKey, p2.APIKey) - assert.Equal(t, region.US.String(), p2.Region) - - // Region lowercase - os.Setenv("NEW_RELIC_REGION", "eu") - p2 = applyOverrides(&p) - assert.NotNil(t, p2) - assert.Equal(t, p.APIKey, p2.APIKey) - assert.Equal(t, region.EU.String(), p2.Region) -} diff --git a/internal/credentials/helpers.go b/internal/credentials/helpers.go deleted file mode 100644 index e02c2feb9..000000000 --- a/internal/credentials/helpers.go +++ /dev/null @@ -1,40 +0,0 @@ -package credentials - -import ( - log "github.com/sirupsen/logrus" - - "github.com/newrelic/newrelic-cli/internal/config" -) - -var defaultProfile *Profile - -// WithCredentials loads and returns the CLI credentials. -func WithCredentials(f func(c *Credentials)) { - WithCredentialsFrom(config.DefaultConfigDirectory, f) -} - -// WithCredentialsFrom loads and returns the CLI credentials from a specified location. -func WithCredentialsFrom(configDir string, f func(c *Credentials)) { - c, err := LoadCredentials(configDir) - if err != nil { - log.Fatal(err) - } - - f(c) -} - -// DefaultProfile retrieves the current default profile. -func DefaultProfile() *Profile { - if defaultProfile == nil { - WithCredentials(func(c *Credentials) { - defaultProfile = c.Default() - }) - } - - return defaultProfile -} - -// SetDefaultProfile allows mocking of the default profile for testing purposes. -func SetDefaultProfile(p Profile) { - defaultProfile = &p -} diff --git a/internal/credentials/profiles.go b/internal/credentials/profiles.go deleted file mode 100644 index c6bd541c8..000000000 --- a/internal/credentials/profiles.go +++ /dev/null @@ -1,224 +0,0 @@ -package credentials - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "reflect" - "strconv" - "strings" - - "github.com/mitchellh/mapstructure" - log "github.com/sirupsen/logrus" - "github.com/spf13/viper" - - "github.com/newrelic/newrelic-client-go/pkg/region" - - "github.com/newrelic/newrelic-cli/internal/config" -) - -// DefaultProfileFile is the configuration file containing the default profile name -const DefaultProfileFile = "default-profile" - -// Profile contains data of a single profile -type Profile struct { - APIKey string `mapstructure:"apiKey" json:"apiKey,omitempty"` // For accessing New Relic GraphQL resources - InsightsInsertKey string `mapstructure:"insightsInsertKey" json:"insightsInsertKey,omitempty"` // For posting custom events - Region string `mapstructure:"region" json:"region,omitempty"` // Region to use for New Relic resources - AccountID int `mapstructure:"accountID" json:"accountID,omitempty"` // AccountID to use for New Relic resources - LicenseKey string `mapstructure:"licenseKey" json:"licenseKey,omitempty"` // License key to use for agent config and ingest -} - -// LoadProfiles reads the credential profiles from the default path. -func LoadProfiles(configDir string) (*map[string]Profile, error) { - cfgViper, err := readCredentials(configDir) - - if err != nil { - return &map[string]Profile{}, fmt.Errorf("error while reading credentials: %s", err) - } - - creds, err := unmarshalProfiles(cfgViper) - if err != nil { - return &map[string]Profile{}, fmt.Errorf("error unmarshaling profiles: %s", err) - } - - return creds, nil -} - -// LoadDefaultProfile reads the profile name from the default profile file. -func LoadDefaultProfile(configDir string) (string, error) { - defProfile, err := readDefaultProfile(configDir) - if err != nil { - return "", err - } - - return defProfile, nil -} - -// Default returns the default profile -func (c *Credentials) Default() *Profile { - var p *Profile - if c.DefaultProfile != "" { - if val, ok := c.Profiles[c.DefaultProfile]; ok { - p = &val - } - } - - p = applyOverrides(p) - return p -} - -// applyOverrides reads Profile info out of the Environment to override config -func applyOverrides(p *Profile) *Profile { - envAPIKey := os.Getenv("NEW_RELIC_API_KEY") - envInsightsInsertKey := os.Getenv("NEW_RELIC_INSIGHTS_INSERT_KEY") - envRegion := os.Getenv("NEW_RELIC_REGION") - envAccountID := os.Getenv("NEW_RELIC_ACCOUNT_ID") - - if envAPIKey == "" && envRegion == "" && envInsightsInsertKey == "" && envAccountID == "" { - return p - } - - out := Profile{} - if p != nil { - out = *p - } - - if envAPIKey != "" { - out.APIKey = envAPIKey - } - - if envInsightsInsertKey != "" { - out.InsightsInsertKey = envInsightsInsertKey - } - - if envRegion != "" { - out.Region = strings.ToUpper(envRegion) - } - - if envAccountID != "" { - accountID, err := strconv.Atoi(envAccountID) - if err != nil { - log.Warnf("Invalid account ID: %s", envAccountID) - return &out - } - - out.AccountID = accountID - } - - return &out -} - -func readDefaultProfile(configDir string) (string, error) { - var defaultProfile string - - _, err := os.Stat(configDir) - if err != nil { - return "", fmt.Errorf("unable to read default-profile from %s: %s", configDir, err) - } - - configPath := os.ExpandEnv(fmt.Sprintf("%s/%s.%s", configDir, DefaultProfileFile, defaultConfigType)) - - // The default-profile.json is a quoted string of the name for the default profile. - byteValue, err := ioutil.ReadFile(configPath) - if err != nil { - return "", fmt.Errorf("error while reading default profile file %s: %s", configPath, err) - } - err = json.Unmarshal(byteValue, &defaultProfile) - if err != nil { - return "", fmt.Errorf("error while unmarshaling default profile: %s", err) - } - - return defaultProfile, nil -} - -func readCredentials(configDir string) (*viper.Viper, error) { - credViper := viper.New() - credViper.SetConfigName(DefaultCredentialsFile) - credViper.SetConfigType(defaultConfigType) - credViper.SetEnvPrefix(config.DefaultEnvPrefix) - credViper.AddConfigPath(configDir) // adding home directory as first search path - credViper.AutomaticEnv() // read in environment variables that match - - // Read in config - err := credViper.ReadInConfig() - if err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - - filePath := os.ExpandEnv(fmt.Sprintf("%s/%s.json", configDir, DefaultCredentialsFile)) - - err = credViper.WriteConfigAs(filePath) - if err != nil { - return nil, fmt.Errorf("error initializing new configuration directory %s: %s", filePath, err) - } - } - - if e, ok := err.(viper.ConfigParseError); ok { - return nil, fmt.Errorf("error parsing profile config file: %v", e) - } - } - - return credViper, nil -} - -func unmarshalProfiles(cfgViper *viper.Viper) (*map[string]Profile, error) { - cfgMap := map[string]Profile{} - - // Have to pass in the default hooks to add one... - err := cfgViper.Unmarshal(&cfgMap, - viper.DecodeHook( - mapstructure.ComposeDecodeHookFunc( - mapstructure.StringToTimeDurationHookFunc(), - mapstructure.StringToSliceHookFunc(","), - StringToRegionHookFunc(), // Custom parsing of Region on unmarshal - ), - )) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal credentials with error: %v", err) - } - - log.Debugf("loaded credentials from: %v", cfgViper.ConfigFileUsed()) - - return &cfgMap, nil -} - -// MarshalJSON allows us to override the default behavior on marshal -// and lowercase the region string for backwards compatibility -func (p Profile) MarshalJSON() ([]byte, error) { - return json.Marshal(&struct { - APIKey string `json:"apiKey,omitempty"` - InsightsInsertKey string `json:"insightsInsertKey,omitempty"` - Region string `json:"region,omitempty"` - AccountID int `json:"accountID,omitempty"` - LicenseKey string `json:"licenseKey,omitempty"` - }{ - APIKey: p.APIKey, - InsightsInsertKey: p.InsightsInsertKey, - AccountID: p.AccountID, - LicenseKey: p.LicenseKey, - Region: strings.ToLower(p.Region), - }) -} - -// StringToRegionHookFunc takes a string and runs it through the region -// parser to create a valid region (or error) -func StringToRegionHookFunc() mapstructure.DecodeHookFunc { - return func( - f reflect.Type, - t reflect.Type, - data interface{}) (interface{}, error) { - var n region.Name - - if f.Kind() != reflect.String { - return data, nil - } - if t != reflect.TypeOf(n) { - return data, nil - } - - // Convert it by parsing - reg, err := region.Parse(data.(string)) - return reg, err - } -} diff --git a/internal/credentials/profiles_integration_test.go b/internal/credentials/profiles_integration_test.go deleted file mode 100644 index 22f091c65..000000000 --- a/internal/credentials/profiles_integration_test.go +++ /dev/null @@ -1,186 +0,0 @@ -// +build integration - -package credentials - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/newrelic/newrelic-client-go/pkg/region" -) - -func TestCredentialsLoadCredentialsNoDirectory(t *testing.T) { - c, err := LoadCredentials("/tmp/notexist") - assert.NoError(t, err) - assert.Equal(t, len(c.Profiles), 0) - assert.Equal(t, c.DefaultProfile, "") - assert.Equal(t, c.ConfigDirectory, "/tmp/notexist") -} - -func TestCredentialsLoadCredentialsHomeDirectory(t *testing.T) { - c, err := LoadCredentials("$HOME/.newrelictesting") - assert.NoError(t, err) - - home := os.Getenv("HOME") - filePath := fmt.Sprintf("%s/.newrelictesting", home) - - assert.Equal(t, len(c.Profiles), 0) - assert.Equal(t, c.DefaultProfile, "") - assert.Equal(t, c.ConfigDirectory, filePath) -} - -func TestCredentials(t *testing.T) { - f, err := ioutil.TempDir("/tmp", "newrelic") - assert.NoError(t, err) - defer os.RemoveAll(f) - - // Initialize the new configuration directory - c, err := LoadCredentials(f) - assert.NoError(t, err) - assert.Equal(t, len(c.Profiles), 0) - assert.Equal(t, c.DefaultProfile, "") - assert.Equal(t, c.ConfigDirectory, f) - - // Create an initial profile to work with - testProfile := Profile{ - Region: "us", - APIKey: "apiKeyGoesHere", - InsightsInsertKey: "insightsInsertKeyGoesHere", - } - err = c.AddProfile("testCase1", testProfile) - assert.NoError(t, err) - assert.Equal(t, len(c.Profiles), 1) - assert.Equal(t, region.US.String(), c.Profiles["testCase1"].Region) - assert.Equal(t, "apiKeyGoesHere", c.Profiles["testCase1"].APIKey) - assert.Equal(t, "insightsInsertKeyGoesHere", c.Profiles["testCase1"].InsightsInsertKey) - assert.Equal(t, "", c.DefaultProfile) - - // Set the default profile to the only one we've got - err = c.SetDefaultProfile("testCase1") - assert.NoError(t, err) - assert.Equal(t, c.DefaultProfile, "testCase1") - - // Adding a profile with the same name should result in an error - testProfile = Profile{ - Region: "us", - APIKey: "foot", - } - err = c.AddProfile("testCase1", testProfile) - assert.Error(t, err) - assert.Equal(t, len(c.Profiles), 1) - assert.True(t, c.profileExists("testCase1")) - - // Create a second profile to work with - testProfile = Profile{ - Region: "us", - APIKey: "apiKeyGoesHere", - } - err = c.AddProfile("testCase2", testProfile) - assert.NoError(t, err) - assert.Equal(t, len(c.Profiles), 2) - assert.Equal(t, c.Profiles["testCase2"].Region, region.US.String()) - assert.Equal(t, c.Profiles["testCase2"].APIKey, "apiKeyGoesHere") - - // Set the default profile to the new one - err = c.SetDefaultProfile("testCase2") - assert.NoError(t, err) - assert.Equal(t, c.DefaultProfile, "testCase2") - - // Delete the initial profile - err = c.RemoveProfile("testCase1") - assert.NoError(t, err) - assert.Equal(t, len(c.Profiles), 1) - - err = c.RemoveProfile("testCase1") - assert.Error(t, err) - assert.Equal(t, len(c.Profiles), 1) - - // Load the credentials again to verify json - c2, err := LoadCredentials(f) - assert.NoError(t, err) - assert.Equal(t, len(c2.Profiles), 1) - assert.Equal(t, c2.DefaultProfile, "testCase2") - assert.Equal(t, c2.ConfigDirectory, f) - assert.False(t, c.profileExists("testCase1")) - - // Remove the default profile and check the results - _, err = os.Stat(fmt.Sprintf("%s/%s.json", f, "default-profile")) - assert.NoError(t, err) - - err = c.RemoveProfile("testCase2") - assert.NoError(t, err) - assert.Equal(t, c.DefaultProfile, "") - _, err = os.Stat(fmt.Sprintf("%s/%s.json", f, "default-profile")) - assert.Error(t, err) - assert.True(t, os.IsNotExist(err)) -} - -func TestCredentialLowerCaseRegion(t *testing.T) { - f, err := ioutil.TempDir("/tmp", "newrelic") - assert.NoError(t, err) - defer os.RemoveAll(f) - - // Initialize the new configuration directory - c, err := LoadCredentials(f) - assert.NoError(t, err) - assert.Equal(t, len(c.Profiles), 0) - assert.Equal(t, c.DefaultProfile, "") - assert.Equal(t, c.ConfigDirectory, f) - - // Create an initial profile to work with - testProfile := Profile{ - Region: "US", - APIKey: "apiKeyGoesHere", - InsightsInsertKey: "insightsInsertKeyGoesHere", - } - err = c.AddProfile("testCase1", testProfile) - assert.NoError(t, err) - assert.Equal(t, len(c.Profiles), 1) - assert.Equal(t, region.US.String(), c.Profiles["testCase1"].Region) - assert.Equal(t, "apiKeyGoesHere", c.Profiles["testCase1"].APIKey) - assert.Equal(t, "insightsInsertKeyGoesHere", c.Profiles["testCase1"].InsightsInsertKey) -} - -// TestCredentialCompatibilityNR1 -func TestCredentialCompatibilityNR1(t *testing.T) { - t.Parallel() - - f, err := ioutil.TempDir("/tmp", "newrelic") - assert.NoError(t, err) - defer os.RemoveAll(f) - - // Custom struct to mirror the config of NR1, and bypass - // any custom marshal / unmarshal code we have - testCredentialData := map[string]struct { - APIKey string - Region string - }{ - "test": { - APIKey: "apiKeyGoesHere", - Region: "us", - }, - "testeu": { - APIKey: "apiKeyEU", - Region: "EU", - }, - } - file, jsonErr := json.MarshalIndent(testCredentialData, "", " ") - assert.NoError(t, jsonErr) - - err = ioutil.WriteFile(f+"/credentials.json", file, 0600) - assert.NoError(t, err) - - c, loadErr := LoadCredentials(f) - assert.NoError(t, loadErr) - assert.Equal(t, len(testCredentialData), len(c.Profiles)) - - for k := range c.Profiles { - assert.Equal(t, testCredentialData[k].APIKey, c.Profiles[k].APIKey) - assert.Equal(t, testCredentialData[k].Region, c.Profiles[k].Region) - } -} diff --git a/internal/credentials/profiles_test.go b/internal/credentials/profiles_test.go deleted file mode 100644 index 5196db71b..000000000 --- a/internal/credentials/profiles_test.go +++ /dev/null @@ -1,23 +0,0 @@ -// +build unit - -package credentials - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestProfileMarshal(t *testing.T) { - t.Parallel() - - p := Profile{ - APIKey: "testAPIKey", - Region: "TEST", - } - - // Ensure that the region name is Lowercase - m, err := p.MarshalJSON() - assert.NoError(t, err) - assert.Equal(t, `{"apiKey":"testAPIKey","region":"test"}`, string(m)) -} diff --git a/internal/diagnose/helpers.go b/internal/diagnose/helpers.go index 5a1c6fdee..03c5ac637 100644 --- a/internal/diagnose/helpers.go +++ b/internal/diagnose/helpers.go @@ -12,7 +12,7 @@ import ( "path" "runtime" - "github.com/newrelic/newrelic-cli/internal/utils" + "github.com/newrelic/newrelic-cli/internal/config" log "github.com/sirupsen/logrus" ) @@ -42,8 +42,7 @@ func downloadBinary() error { resp, err := http.Get(downloadURL) if err != nil { log.Warnf("failed to download the latest nrdiag: %s", err) - home, _ := utils.GetDefaultConfigDirectory() - log.Infof("If this problem persists, you can download the zip file from https://download.newrelic.com/nrdiag/nrdiag_latest.zip and place the appropriate binary for your system in %s/bin, then try again.", home) + log.Infof("If this problem persists, you can download the zip file from https://download.newrelic.com/nrdiag/nrdiag_latest.zip and place the appropriate binary for your system in %s/bin, then try again.", config.ConfigDir) return err } defer resp.Body.Close() @@ -125,11 +124,7 @@ func runDiagnostics(args ...string) error { } func getBinaryPath() string { - configDirectory, err := utils.GetDefaultConfigDirectory() - if err != nil { - log.Fatal(err) - } - return path.Join(configDirectory, "bin", "nrdiag") + return path.Join(config.ConfigDir, "bin", "nrdiag") } const downloadURL = "https://download.newrelic.com/nrdiag/nrdiag_latest.zip" diff --git a/internal/edge/command_trace_observer.go b/internal/edge/command_trace_observer.go index b8b880d6d..7341d3363 100644 --- a/internal/edge/command_trace_observer.go +++ b/internal/edge/command_trace_observer.go @@ -5,9 +5,8 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" "github.com/newrelic/newrelic-client-go/pkg/edge" ) @@ -19,6 +18,8 @@ var ( validProviderRegions = []string{ string(edge.EdgeProviderRegionTypes.AWS_US_EAST_1), string(edge.EdgeProviderRegionTypes.AWS_US_EAST_2), + string(edge.EdgeProviderRegionTypes.AWS_US_WEST_2), + string(edge.EdgeProviderRegionTypes.AWS_EU_WEST_1), } ) @@ -42,13 +43,25 @@ var cmdList = &cobra.Command{ The list command retrieves the trace observers for the given account ID. `, Example: `newrelic edge trace-observer list --accountId 12345678`, + PreRun: func(cmd *cobra.Command, args []string) { + var err error + if accountID, err = config.RequireAccountID(); err != nil { + log.Fatal(err) + } + + if _, err = config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - traceObservers, err := nrClient.Edge.ListTraceObservers(accountID) - utils.LogIfFatal(err) + traceObservers, err := client.Client.Edge.ListTraceObservers(accountID) + if err != nil { + log.Fatal(err) + } - utils.LogIfFatal(output.Print(traceObservers)) - }) + if err := output.Print(traceObservers); err != nil { + log.Fatal(err) + } }, } @@ -71,18 +84,31 @@ The create command requires an account ID, observer name, and provider region. Valid provider regions are AWS_US_EAST_1 and AWS_US_EAST_2. `, Example: `newrelic edge trace-observer create --name 'My Observer' --accountId 12345678 --providerRegion AWS_US_EAST_1`, + PreRun: func(cmd *cobra.Command, args []string) { + var err error + if accountID, err = config.RequireAccountID(); err != nil { + log.Fatal(err) + } + + if _, err = config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - if ok := isValidProviderRegion(providerRegion); !ok { - log.Fatalf("%s is not a valid provider region, valid values are %s", providerRegion, validProviderRegions) - } + if ok := isValidProviderRegion(providerRegion); !ok { + log.Fatalf("%s is not a valid provider region, valid values are %s", providerRegion, validProviderRegions) + } - traceObserver, err := nrClient.Edge.CreateTraceObserver(accountID, name, edge.EdgeProviderRegion(providerRegion)) - utils.LogIfFatal(err) + traceObserver, err := client.Client.Edge.CreateTraceObserver(accountID, name, edge.EdgeProviderRegion(providerRegion)) + if err != nil { + log.Fatal(err) + } - utils.LogIfFatal(output.Print(traceObserver)) - log.Info("success") - }) + if err = output.Print(traceObserver); err != nil { + log.Fatal(err) + } + + log.Info("success") }, } @@ -94,21 +120,29 @@ var cmdDelete = &cobra.Command{ The delete command accepts a trace observer's ID. `, Example: `newrelic edge trace-observer delete --accountId 12345678 --id 1234`, + PreRun: func(cmd *cobra.Command, args []string) { + var err error + if accountID, err = config.RequireAccountID(); err != nil { + log.Fatal(err) + } + + if _, err = config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - _, err := nrClient.Edge.DeleteTraceObserver(accountID, id) - utils.LogIfFatal(err) + _, err := client.Client.Edge.DeleteTraceObserver(accountID, id) + if err != nil { + log.Fatal(err) + } - log.Info("success") - }) + log.Info("success") }, } func init() { // Root sub-command Command.AddCommand(cmdTraceObserver) - cmdTraceObserver.PersistentFlags().IntVarP(&accountID, "accountId", "a", 0, "A New Relic account ID") - utils.LogIfError(cmdTraceObserver.MarkPersistentFlagRequired("accountId")) // List cmdTraceObserver.AddCommand(cmdList) @@ -117,11 +151,18 @@ func init() { cmdTraceObserver.AddCommand(cmdCreate) cmdCreate.Flags().StringVarP(&name, "name", "n", "", "the name of the trace observer") cmdCreate.Flags().StringVarP(&providerRegion, "providerRegion", "r", "", "the provider region in which to create the trace observer") - utils.LogIfError(cmdCreate.MarkFlagRequired("name")) - utils.LogIfError(cmdCreate.MarkFlagRequired("providerRegion")) + if err := cmdCreate.MarkFlagRequired("name"); err != nil { + log.Error(err) + } + + if err := cmdCreate.MarkFlagRequired("providerRegion"); err != nil { + log.Error(err) + } // Delete cmdTraceObserver.AddCommand(cmdDelete) cmdDelete.Flags().IntVarP(&id, "id", "i", 0, "the ID of the trace observer to delete") - utils.LogIfError(cmdDelete.MarkFlagRequired("id")) + if err := cmdDelete.MarkFlagRequired("id"); err != nil { + log.Error(err) + } } diff --git a/internal/entities/command_search.go b/internal/entities/command_search.go index 35d6a1476..18435c59d 100644 --- a/internal/entities/command_search.go +++ b/internal/entities/command_search.go @@ -7,9 +7,9 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/output" "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" "github.com/newrelic/newrelic-client-go/pkg/entities" ) @@ -21,78 +21,93 @@ var cmdEntitySearch = &cobra.Command{ The search command performs a search for New Relic entities. `, Example: "newrelic entity search --name ", + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - params := entities.EntitySearchQueryBuilder{} + params := entities.EntitySearchQueryBuilder{} - if entityName == "" && entityType == "" && entityAlertSeverity == "" && entityDomain == "" { - utils.LogIfError(cmd.Help()) - log.Fatal("one of --name, --type, --alert-severity, or --domain are required") + if entityName == "" && entityType == "" && entityAlertSeverity == "" && entityDomain == "" { + if err := cmd.Help(); err != nil { + log.Error(err) } - if entityName != "" { - params.Name = entityName - } + log.Fatal("one of --name, --type, --alert-severity, or --domain are required") + } - if entityType != "" { - params.Type = entities.EntitySearchQueryBuilderType(entityType) - } + if entityName != "" { + params.Name = entityName + } - if entityAlertSeverity != "" { - params.AlertSeverity = entities.EntityAlertSeverity(entityAlertSeverity) - } + if entityType != "" { + params.Type = entities.EntitySearchQueryBuilderType(entityType) + } - if entityDomain != "" { - params.Domain = entities.EntitySearchQueryBuilderDomain(entityDomain) - } + if entityAlertSeverity != "" { + params.AlertSeverity = entities.EntityAlertSeverity(entityAlertSeverity) + } - if entityTag != "" { - key, value, err := assembleTagValue(entityTag) - utils.LogIfFatal(err) + if entityDomain != "" { + params.Domain = entities.EntitySearchQueryBuilderDomain(entityDomain) + } - params.Tags = []entities.EntitySearchQueryBuilderTag{{Key: key, Value: value}} + var key, value string + var err error + if entityTag != "" { + key, value, err = assembleTagValue(entityTag) + if err != nil { + log.Fatal(err) } - if entityReporting != "" { - reporting, err := strconv.ParseBool(entityReporting) + params.Tags = []entities.EntitySearchQueryBuilderTag{{Key: key, Value: value}} + } - if err != nil { - log.Fatalf("invalid value provided for flag --reporting. Must be true or false.") - } + var reporting bool + if entityReporting != "" { + reporting, err = strconv.ParseBool(entityReporting) - params.Reporting = reporting + if err != nil { + log.Fatalf("invalid value provided for flag --reporting. Must be true or false.") } - results, err := nrClient.Entities.GetEntitySearch( - entities.EntitySearchOptions{}, - "", - params, - []entities.EntitySearchSortCriteria{}, - ) - utils.LogIfFatal(err) + params.Reporting = reporting + } + + results, err := client.Client.Entities.GetEntitySearch( + entities.EntitySearchOptions{}, + "", + params, + []entities.EntitySearchSortCriteria{}, + ) + if err != nil { + log.Fatal(err) + } - entities := results.Results.Entities + entities := results.Results.Entities - var result interface{} + var result interface{} - if len(entityFields) > 0 { - mapped := mapEntities(entities, entityFields, utils.StructToMap) + if len(entityFields) > 0 { + mapped := mapEntities(entities, entityFields, utils.StructToMap) - if len(mapped) == 1 { - result = mapped[0] - } else { - result = mapped - } + if len(mapped) == 1 { + result = mapped[0] + } else { + result = mapped + } + } else { + if len(entities) == 1 { + result = entities[0] } else { - if len(entities) == 1 { - result = entities[0] - } else { - result = entities - } + result = entities } + } - utils.LogIfFatal(output.Print(result)) - }) + if err := output.Print(result); err != nil { + log.Fatal(err) + } }, } diff --git a/internal/entities/command_tags.go b/internal/entities/command_tags.go index ef6956a86..df92b19d1 100644 --- a/internal/entities/command_tags.go +++ b/internal/entities/command_tags.go @@ -8,10 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/output" "github.com/newrelic/newrelic-cli/internal/pipe" - "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" "github.com/newrelic/newrelic-client-go/pkg/entities" ) @@ -39,19 +38,32 @@ var cmdTagsGet = &cobra.Command{ The get command returns JSON output of the tags for the requested entity. `, Example: "newrelic entity tags get --guid ", + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - // Temporary until bulk actions can be build into newrelic-client-go - if value, ok := pipe.Get("guid"); ok { - tags, err := nrClient.Entities.GetTagsForEntity(entities.EntityGUID(value[0])) - utils.LogIfFatal(err) - utils.LogIfError(output.Print(tags)) - } else { - tags, err := nrClient.Entities.GetTagsForEntity(entities.EntityGUID(entityGUID)) - utils.LogIfFatal(err) - utils.LogIfError(output.Print(tags)) + // Temporary until bulk actions can be build into newrelic-client-go + if value, ok := pipe.Get("guid"); ok { + tags, err := client.Client.Entities.GetTagsForEntity(entities.EntityGUID(value[0])) + if err != nil { + log.Fatal(err) + } + + if err := output.Print(tags); err != nil { + log.Fatal(err) + } + } else { + tags, err := client.Client.Entities.GetTagsForEntity(entities.EntityGUID(entityGUID)) + if err != nil { + log.Fatal(err) } - }) + + if err := output.Print(tags); err != nil { + log.Fatal(err) + } + } }, } @@ -64,13 +76,18 @@ The delete command deletes all tags on the given entity that match the specified keys. `, Example: "newrelic entity tags delete --guid --tag tag1 --tag tag2 --tag tag3,tag4", + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - _, err := nrClient.Entities.TaggingDeleteTagFromEntity(entities.EntityGUID(entityGUID), entityTags) - utils.LogIfFatal(err) + _, err := client.Client.Entities.TaggingDeleteTagFromEntity(entities.EntityGUID(entityGUID), entityTags) + if err != nil { + log.Fatal(err) + } - log.Info("success") - }) + log.Info("success") }, } @@ -82,16 +99,23 @@ var cmdTagsDeleteValues = &cobra.Command{ The delete-values command deletes the specified tag:value pairs on a given entity. `, Example: "newrelic entity tags delete-values --guid --tag tag1:value1", + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - tagValues, err := assembleTagValuesInput(entityValues) - utils.LogIfFatal(err) + tagValues, err := assembleTagValuesInput(entityValues) + if err != nil { + log.Fatal(err) + } - _, err = nrClient.Entities.TaggingDeleteTagValuesFromEntity(entities.EntityGUID(entityGUID), tagValues) - utils.LogIfFatal(err) + _, err = client.Client.Entities.TaggingDeleteTagValuesFromEntity(entities.EntityGUID(entityGUID), tagValues) + if err != nil { + log.Fatal(err) + } - log.Info("success") - }) + log.Info("success") }, } @@ -103,16 +127,23 @@ var cmdTagsCreate = &cobra.Command{ The create command adds tag:value pairs to the given entity. `, Example: "newrelic entity tags create --guid --tag tag1:value1", + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - tags, err := assembleTagsInput(entityTags) - utils.LogIfFatal(err) + tags, err := assembleTagsInput(entityTags) + if err != nil { + log.Fatal(err) + } - _, err = nrClient.Entities.TaggingAddTagsToEntity(entities.EntityGUID(entityGUID), tags) - utils.LogIfFatal(err) + _, err = client.Client.Entities.TaggingAddTagsToEntity(entities.EntityGUID(entityGUID), tags) + if err != nil { + log.Fatal(err) + } - log.Info("success") - }) + log.Info("success") }, } @@ -125,16 +156,23 @@ The replace command replaces any existing tag:value pairs with those provided for the given entity. `, Example: "newrelic entity tags replace --guid --tag tag1:value1", + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - tags, err := assembleTagsInput(entityTags) - utils.LogIfFatal(err) + tags, err := assembleTagsInput(entityTags) + if err != nil { + log.Fatal(err) + } - _, err = nrClient.Entities.TaggingReplaceTagsOnEntity(entities.EntityGUID(entityGUID), tags) - utils.LogIfFatal(err) + _, err = client.Client.Entities.TaggingReplaceTagsOnEntity(entities.EntityGUID(entityGUID), tags) + if err != nil { + log.Fatal(err) + } - log.Info("success") - }) + log.Info("success") }, } @@ -224,30 +262,52 @@ func init() { if !pipe.Exists("guid") { cmdTagsGet.Flags().StringVarP(&entityGUID, "guid", "g", "", "the entity GUID to retrieve tags for") - utils.LogIfError(cmdTagsGet.MarkFlagRequired("guid")) + if err := cmdTagsGet.MarkFlagRequired("guid"); err != nil { + log.Error(err) + } } cmdTags.AddCommand(cmdTagsDelete) cmdTagsDelete.Flags().StringVarP(&entityGUID, "guid", "g", "", "the entity GUID to delete tags on") cmdTagsDelete.Flags().StringSliceVarP(&entityTags, "tag", "t", []string{}, "the tag keys to delete from the entity") - utils.LogIfError(cmdTagsDelete.MarkFlagRequired("guid")) - utils.LogIfError(cmdTagsDelete.MarkFlagRequired("tag")) + if err := cmdTagsDelete.MarkFlagRequired("guid"); err != nil { + log.Error(err) + } + + if err := cmdTagsDelete.MarkFlagRequired("tag"); err != nil { + log.Error(err) + } cmdTags.AddCommand(cmdTagsDeleteValues) cmdTagsDeleteValues.Flags().StringVarP(&entityGUID, "guid", "g", "", "the entity GUID to delete tag values on") cmdTagsDeleteValues.Flags().StringSliceVarP(&entityValues, "value", "v", []string{}, "the tag key:value pairs to delete from the entity") - utils.LogIfError(cmdTagsDeleteValues.MarkFlagRequired("guid")) - utils.LogIfError(cmdTagsDeleteValues.MarkFlagRequired("value")) + if err := cmdTagsDeleteValues.MarkFlagRequired("guid"); err != nil { + log.Error(err) + } + + if err := cmdTagsDeleteValues.MarkFlagRequired("value"); err != nil { + log.Error(err) + } cmdTags.AddCommand(cmdTagsCreate) cmdTagsCreate.Flags().StringVarP(&entityGUID, "guid", "g", "", "the entity GUID to create tag values on") cmdTagsCreate.Flags().StringSliceVarP(&entityTags, "tag", "t", []string{}, "the tag names to add to the entity") - utils.LogIfError(cmdTagsCreate.MarkFlagRequired("guid")) - utils.LogIfError(cmdTagsCreate.MarkFlagRequired("tag")) + if err := cmdTagsCreate.MarkFlagRequired("guid"); err != nil { + log.Error(err) + } + + if err := cmdTagsCreate.MarkFlagRequired("tag"); err != nil { + log.Error(err) + } cmdTags.AddCommand(cmdTagsReplace) cmdTagsReplace.Flags().StringVarP(&entityGUID, "guid", "g", "", "the entity GUID to replace tag values on") cmdTagsReplace.Flags().StringSliceVarP(&entityTags, "tag", "t", []string{}, "the tag names to replace on the entity") - utils.LogIfError(cmdTagsReplace.MarkFlagRequired("guid")) - utils.LogIfError(cmdTagsReplace.MarkFlagRequired("tag")) + if err := cmdTagsReplace.MarkFlagRequired("guid"); err != nil { + log.Error(err) + } + + if err := cmdTagsReplace.MarkFlagRequired("tag"); err != nil { + log.Error(err) + } } diff --git a/internal/events/command_post.go b/internal/events/command_post.go index 942c5f4be..ccadb05cf 100644 --- a/internal/events/command_post.go +++ b/internal/events/command_post.go @@ -7,14 +7,12 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" - "github.com/newrelic/newrelic-cli/internal/credentials" - "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" + "github.com/newrelic/newrelic-cli/internal/config" ) var ( - accountID int event string + accountID int ) var cmdPost = &cobra.Command{ @@ -29,32 +27,35 @@ The accepted payload requires the use of an ` + "`eventType`" + `field that represents the custom event's type. `, Example: `newrelic events post --accountId 12345 --event '{ "eventType": "Payment", "amount": 123.45 }'`, + PreRun: func(cmd *cobra.Command, args []string) { + var err error + if accountID, err = config.RequireAccountID(); err != nil { + log.Fatal(err) + } + + if _, err = config.RequireInsightsInsertKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClientAndProfile(func(nrClient *newrelic.NewRelic, profile *credentials.Profile) { - if profile.InsightsInsertKey == "" { - log.Fatal("an Insights insert key is required, set one in your default profile or use the NEW_RELIC_INSIGHTS_INSERT_KEY environment variable") - } - - var e map[string]interface{} + var e map[string]interface{} - err := json.Unmarshal([]byte(event), &e) - if err != nil { - log.Fatal(err) - } + if err := json.Unmarshal([]byte(event), &e); err != nil { + log.Fatal(err) + } - if err := nrClient.Events.CreateEvent(accountID, event); err != nil { - log.Fatal(err) - } + if err := client.Client.Events.CreateEvent(accountID, event); err != nil { + log.Fatal(err) + } - log.Info("success") - }) + log.Info("success") }, } func init() { Command.AddCommand(cmdPost) - cmdPost.Flags().IntVarP(&accountID, "accountId", "a", 0, "the account ID to create the custom event in") cmdPost.Flags().StringVarP(&event, "event", "e", "{}", "a JSON-formatted event payload to post") - utils.LogIfError(cmdPost.MarkFlagRequired("accountId")) - utils.LogIfError(cmdPost.MarkFlagRequired("event")) + if err := cmdPost.MarkFlagRequired("event"); err != nil { + log.Error(err) + } } diff --git a/internal/install/command.go b/internal/install/command.go index bf6b2715d..a7ab1e2a7 100644 --- a/internal/install/command.go +++ b/internal/install/command.go @@ -1,15 +1,11 @@ package install import ( - "errors" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" "github.com/newrelic/newrelic-cli/internal/config" - "github.com/newrelic/newrelic-cli/internal/credentials" - "github.com/newrelic/newrelic-client-go/newrelic" ) var ( @@ -21,9 +17,6 @@ var ( skipInfraInstall bool skipIntegrations bool skipLoggingInstall bool - testMode bool - debug bool - trace bool ) // Command represents the install command. @@ -31,6 +24,15 @@ var Command = &cobra.Command{ Use: "install", Short: "Install New Relic.", Hidden: true, + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireAccountID(); err != nil { + log.Fatal(err) + } + + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { ic := InstallerContext{ AdvancedMode: advancedMode, @@ -43,37 +45,20 @@ var Command = &cobra.Command{ SkipLoggingInstall: skipLoggingInstall, } - client.WithClientAndProfile(func(nrClient *newrelic.NewRelic, profile *credentials.Profile) { - if trace { - log.SetLevel(log.TraceLevel) - nrClient.SetLogLevel("trace") - } else if debug { - log.SetLevel(log.DebugLevel) - nrClient.SetLogLevel("debug") - } - - err := assertProfileIsValid(profile) - if err != nil { - log.Fatal(err) - } + activeProfile := config.GetActiveProfileName() + if activeProfile == "" { + log.Fatal("no active profile has been set") + } - i := NewRecipeInstaller(ic, nrClient) + i := NewRecipeInstaller(ic, client.Client) - // Run the install. - if err := i.Install(); err != nil { - log.Fatalf("Could not install New Relic: %s, check the install log for details: %s", err, config.DefaultLogFile) - } - }) + // Run the install. + if err := i.Install(); err != nil { + log.Fatalf("Could not install New Relic: %s, check the install log for details: %s", err, config.DefaultLogFile) + } }, } -func assertProfileIsValid(profile *credentials.Profile) error { - if profile == nil { - return errors.New("default profile has not been set") - } - return nil -} - func init() { Command.Flags().StringSliceVarP(&recipePaths, "recipePath", "c", []string{}, "the path to a recipe file to install") Command.Flags().StringSliceVarP(&recipeNames, "recipe", "n", []string{}, "the name of a recipe to install") @@ -81,9 +66,6 @@ func init() { Command.Flags().BoolVarP(&skipInfraInstall, "skipInfraInstall", "i", false, "skips installation of New Relic Infrastructure Agent") Command.Flags().BoolVarP(&skipIntegrations, "skipIntegrations", "r", false, "skips installation of recommended New Relic integrations") Command.Flags().BoolVarP(&skipLoggingInstall, "skipLoggingInstall", "l", false, "skips installation of New Relic Logging") - Command.Flags().BoolVarP(&testMode, "testMode", "t", false, "fakes operations for UX testing") - Command.Flags().BoolVar(&debug, "debug", false, "debug level logging") - Command.Flags().BoolVar(&trace, "trace", false, "trace level logging") Command.Flags().BoolVarP(&assumeYes, "assumeYes", "y", false, "use \"yes\" for all questions during install") Command.Flags().BoolVarP(&advancedMode, "advanced", "", false, "use \"advanced\" mode") } diff --git a/internal/install/execution/go_task_recipe_executor.go b/internal/install/execution/go_task_recipe_executor.go index cae47c4a5..a9e48d859 100644 --- a/internal/install/execution/go_task_recipe_executor.go +++ b/internal/install/execution/go_task_recipe_executor.go @@ -16,7 +16,7 @@ import ( log "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" - "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/install/recipes" "github.com/newrelic/newrelic-cli/internal/install/types" ) @@ -146,17 +146,21 @@ func (re *GoTaskRecipeExecutor) Execute(ctx context.Context, m types.DiscoveryMa } func varsFromProfile() (types.RecipeVars, error) { - defaultProfile := credentials.DefaultProfile() - if defaultProfile.LicenseKey == "" { + licenseKey := config.GetActiveProfileValueString(config.LicenseKey) + if licenseKey == "" { return types.RecipeVars{}, errors.New("license key not found in default profile") } + accountID := config.GetActiveProfileAccountID() + userKey := config.GetActiveProfileValueString(config.UserKey) + region := config.GetActiveProfileValueString(config.Region) + vars := make(types.RecipeVars) - vars["NEW_RELIC_LICENSE_KEY"] = defaultProfile.LicenseKey - vars["NEW_RELIC_ACCOUNT_ID"] = strconv.Itoa(defaultProfile.AccountID) - vars["NEW_RELIC_API_KEY"] = defaultProfile.APIKey - vars["NEW_RELIC_REGION"] = defaultProfile.Region + vars["NEW_RELIC_LICENSE_KEY"] = licenseKey + vars["NEW_RELIC_ACCOUNT_ID"] = strconv.Itoa(accountID) + vars["NEW_RELIC_API_KEY"] = userKey + vars["NEW_RELIC_REGION"] = region return vars, nil } diff --git a/internal/install/execution/go_task_recipe_executor_test.go b/internal/install/execution/go_task_recipe_executor_test.go index b3e240da5..73beb80a5 100644 --- a/internal/install/execution/go_task_recipe_executor_test.go +++ b/internal/install/execution/go_task_recipe_executor_test.go @@ -15,16 +15,15 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" - "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/install/recipes" "github.com/newrelic/newrelic-cli/internal/install/types" ) func TestExecute_SystemVariableInterpolation(t *testing.T) { - p := credentials.Profile{ - LicenseKey: "testLicenseKey", - } - credentials.SetDefaultProfile(p) + mockEnvVarResolver := config.NewMockEnvResolver() + mockEnvVarResolver.Setenv("NEW_RELIC_LICENSE_KEY", "testLicenseKey") + config.EnvVarResolver = mockEnvVarResolver e := NewGoTaskRecipeExecutor() diff --git a/internal/install/execution/nerdstorage_status_reporter_integration_test.go b/internal/install/execution/nerdstorage_status_reporter_integration_test.go index 62e8b5149..2d1bc9e16 100644 --- a/internal/install/execution/nerdstorage_status_reporter_integration_test.go +++ b/internal/install/execution/nerdstorage_status_reporter_integration_test.go @@ -12,22 +12,18 @@ import ( "github.com/newrelic/newrelic-cli/internal/install/types" "github.com/newrelic/newrelic-client-go/newrelic" - "github.com/newrelic/newrelic-client-go/pkg/config" "github.com/newrelic/newrelic-client-go/pkg/nerdstorage" "github.com/newrelic/newrelic-client-go/pkg/workloads" ) func TestReportRecipeSucceeded_Basic(t *testing.T) { - apiKey := os.Getenv("NEW_RELIC_API_KEY") + userKey := os.Getenv("NEW_RELIC_API_KEY") accountID := os.Getenv("NEW_RELIC_ACCOUNT_ID") - if apiKey == "" || accountID == "" { + if userKey == "" || accountID == "" { t.Skipf("NEW_RELIC_API_KEY and NEW_RELIC_ACCOUNT_ID are required to run this test") } - cfg := config.Config{ - PersonalAPIKey: apiKey, - } - c, err := newrelic.New(newrelic.ConfigPersonalAPIKey(cfg.PersonalAPIKey)) + c, err := newrelic.New(newrelic.ConfigPersonalAPIKey(userKey)) if err != nil { t.Fatalf("error creating integration test client") } @@ -66,16 +62,13 @@ func TestReportRecipeSucceeded_Basic(t *testing.T) { require.NotEmpty(t, s) } func TestReportRecipeSucceeded_UserScopeOnly(t *testing.T) { - apiKey := os.Getenv("NEW_RELIC_API_KEY") + userKey := os.Getenv("NEW_RELIC_API_KEY") accountID := os.Getenv("NEW_RELIC_ACCOUNT_ID") - if apiKey == "" || accountID == "" { + if userKey == "" || accountID == "" { t.Skipf("NEW_RELIC_API_KEY and NEW_RELIC_ACCOUNT_ID are required to run this test") } - cfg := config.Config{ - PersonalAPIKey: apiKey, - } - c, err := newrelic.New(newrelic.ConfigPersonalAPIKey(cfg.PersonalAPIKey)) + c, err := newrelic.New(newrelic.ConfigPersonalAPIKey(userKey)) if err != nil { t.Fatalf("error creating integration test client") } diff --git a/internal/install/execution/status.go b/internal/install/execution/status.go index 41a4b1495..976be0d0c 100644 --- a/internal/install/execution/status.go +++ b/internal/install/execution/status.go @@ -52,7 +52,7 @@ func NewStatusRollup(reporters []StatusReporter) *StatusRollup { s := StatusRollup{ DocumentID: uuid.New().String(), Timestamp: utils.GetTimestamp(), - LogFilePath: config.DefaultConfigDirectory + "/" + config.DefaultLogFile, + LogFilePath: config.ConfigDir + "/" + config.DefaultLogFile, statusReporters: reporters, } diff --git a/internal/install/validation/polling_recipe_validator.go b/internal/install/validation/polling_recipe_validator.go index 143be4aea..721f60657 100644 --- a/internal/install/validation/polling_recipe_validator.go +++ b/internal/install/validation/polling_recipe_validator.go @@ -10,7 +10,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/install/types" "github.com/newrelic/newrelic-client-go/pkg/nrdb" ) @@ -139,14 +139,15 @@ func substituteHostname(dm types.DiscoveryManifest, r types.Recipe) (string, err } func (m *PollingRecipeValidator) executeQuery(ctx context.Context, query string) ([]nrdb.NRDBResult, error) { - profile := credentials.DefaultProfile() - if profile == nil || profile.AccountID == 0 { + accountID := config.GetActiveProfileAccountID() + + if accountID == 0 { return nil, errors.New("no account ID found in default profile") } nrql := nrdb.NRQL(query) - result, err := m.client.QueryWithContext(ctx, profile.AccountID, nrql) + result, err := m.client.QueryWithContext(ctx, accountID, nrql) if err != nil { return nil, err } diff --git a/internal/install/validation/polling_recipe_validator_test.go b/internal/install/validation/polling_recipe_validator_test.go index d88c73a86..0ffd2cf5e 100644 --- a/internal/install/validation/polling_recipe_validator_test.go +++ b/internal/install/validation/polling_recipe_validator_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/install/types" "github.com/newrelic/newrelic-client-go/pkg/nrdb" ) @@ -28,7 +28,10 @@ var ( ) func TestValidate(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + mockEnvVarResolver := config.NewMockEnvResolver() + config.EnvVarResolver = mockEnvVarResolver + mockEnvVarResolver.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") + c := NewMockNRDBClient() c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 1) @@ -44,7 +47,10 @@ func TestValidate(t *testing.T) { } func TestValidate_PassAfterNAttempts(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + mockEnvVarResolver := config.NewMockEnvResolver() + config.EnvVarResolver = mockEnvVarResolver + mockEnvVarResolver.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") + c := NewMockNRDBClient() v := NewPollingRecipeValidator(c) v.maxAttempts = 5 @@ -62,7 +68,10 @@ func TestValidate_PassAfterNAttempts(t *testing.T) { } func TestValidate_FailAfterNAttempts(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + mockEnvVarResolver := config.NewMockEnvResolver() + config.EnvVarResolver = mockEnvVarResolver + mockEnvVarResolver.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") + c := NewMockNRDBClient() v := NewPollingRecipeValidator(c) v.maxAttempts = 3 @@ -78,7 +87,10 @@ func TestValidate_FailAfterNAttempts(t *testing.T) { } func TestValidate_FailAfterMaxAttempts(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + mockEnvVarResolver := config.NewMockEnvResolver() + config.EnvVarResolver = mockEnvVarResolver + mockEnvVarResolver.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") + c := NewMockNRDBClient() c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 2) @@ -96,7 +108,10 @@ func TestValidate_FailAfterMaxAttempts(t *testing.T) { } func TestValidate_FailIfContextDone(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + mockEnvVarResolver := config.NewMockEnvResolver() + config.EnvVarResolver = mockEnvVarResolver + mockEnvVarResolver.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") + c := NewMockNRDBClient() c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 2) @@ -116,7 +131,10 @@ func TestValidate_FailIfContextDone(t *testing.T) { } func TestValidate_QueryError(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + mockEnvVarResolver := config.NewMockEnvResolver() + config.EnvVarResolver = mockEnvVarResolver + mockEnvVarResolver.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") + c := NewMockNRDBClient() c.ThrowError("test error") diff --git a/internal/nerdgraph/command_query.go b/internal/nerdgraph/command_query.go index f331c46d1..ecb525d47 100644 --- a/internal/nerdgraph/command_query.go +++ b/internal/nerdgraph/command_query.go @@ -9,9 +9,8 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" ng "github.com/newrelic/newrelic-client-go/pkg/nerdgraph" ) @@ -42,32 +41,37 @@ keys are the variables to be referenced in the GraphQL query. return nil }, + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - var variablesParsed map[string]interface{} + var variablesParsed map[string]interface{} - err := json.Unmarshal([]byte(variables), &variablesParsed) - if err != nil { - log.Fatal(err) - } + if err := json.Unmarshal([]byte(variables), &variablesParsed); err != nil { + log.Fatal(err) + } - query := args[0] + query := args[0] - result, err := nrClient.NerdGraph.Query(query, variablesParsed) - if err != nil { - log.Fatal(err) - } + result, err := client.Client.NerdGraph.Query(query, variablesParsed) + if err != nil { + log.Fatal(err) + } - reqBodyBytes := new(bytes.Buffer) + reqBodyBytes := new(bytes.Buffer) - encoder := json.NewEncoder(reqBodyBytes) - err = encoder.Encode(ng.QueryResponse{ - Actor: result.(ng.QueryResponse).Actor, - }) - utils.LogIfFatal(err) + encoder := json.NewEncoder(reqBodyBytes) + if err = encoder.Encode(ng.QueryResponse{ + Actor: result.(ng.QueryResponse).Actor, + }); err != nil { + log.Fatal(err) + } - utils.LogIfFatal(output.Print(reqBodyBytes)) - }) + if err := output.Print(reqBodyBytes); err != nil { + log.Fatal(err) + } }, } diff --git a/internal/nerdstorage/command.go b/internal/nerdstorage/command.go index e38df8980..f943a63be 100644 --- a/internal/nerdstorage/command.go +++ b/internal/nerdstorage/command.go @@ -1,7 +1,10 @@ package nerdstorage import ( + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + + "github.com/newrelic/newrelic-cli/internal/config" ) var ( @@ -18,4 +21,14 @@ var ( var Command = &cobra.Command{ Use: "nerdstorage", Short: "Read, write, and delete NerdStorage documents and collections.", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + var err error + if accountID, err = config.RequireAccountID(); err != nil { + log.Fatal(err) + } + + if _, err = config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, } diff --git a/internal/nerdstorage/command_collection.go b/internal/nerdstorage/command_collection.go index 1977edf95..1f42ac40f 100644 --- a/internal/nerdstorage/command_collection.go +++ b/internal/nerdstorage/command_collection.go @@ -8,8 +8,6 @@ import ( "github.com/newrelic/newrelic-cli/internal/client" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" "github.com/newrelic/newrelic-client-go/pkg/nerdstorage" ) @@ -40,32 +38,33 @@ GUID. A valid Nerdpack package ID is required. newrelic nerdstorage collection get --scope USER --packageId b0dee5a1-e809-4d6f-bd3c-0682cd079612 --collection myCol `, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - var resp []interface{} - var err error - - input := nerdstorage.GetCollectionInput{ - PackageID: packageID, - Collection: collection, - } - - switch strings.ToLower(scope) { - case "account": - resp, err = nrClient.NerdStorage.GetCollectionWithAccountScope(accountID, input) - case "entity": - resp, err = nrClient.NerdStorage.GetCollectionWithEntityScope(entityGUID, input) - case "user": - resp, err = nrClient.NerdStorage.GetCollectionWithUserScope(input) - default: - log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") - } - if err != nil { - log.Fatal(err) - } - - utils.LogIfFatal(output.Print(resp)) - log.Info("success") - }) + var resp []interface{} + + input := nerdstorage.GetCollectionInput{ + PackageID: packageID, + Collection: collection, + } + + var err error + switch strings.ToLower(scope) { + case "account": + resp, err = client.Client.NerdStorage.GetCollectionWithAccountScope(accountID, input) + case "entity": + resp, err = client.Client.NerdStorage.GetCollectionWithEntityScope(entityGUID, input) + case "user": + resp, err = client.Client.NerdStorage.GetCollectionWithUserScope(input) + default: + log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") + } + if err != nil { + log.Fatal(err) + } + + if err := output.Print(resp); err != nil { + log.Fatal(err) + } + + log.Info("success") }, } @@ -89,30 +88,27 @@ GUID. A valid Nerdpack package ID is required. newrelic nerdstorage collection delete --scope USER --packageId b0dee5a1-e809-4d6f-bd3c-0682cd079612 --collection myCol `, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - var err error - - input := nerdstorage.DeleteCollectionInput{ - PackageID: packageID, - Collection: collection, - } - - switch strings.ToLower(scope) { - case "account": - _, err = nrClient.NerdStorage.DeleteCollectionWithAccountScope(accountID, input) - case "entity": - _, err = nrClient.NerdStorage.DeleteCollectionWithEntityScope(entityGUID, input) - case "user": - _, err = nrClient.NerdStorage.DeleteCollectionWithUserScope(input) - default: - log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") - } - if err != nil { - log.Fatal(err) - } - - log.Info("success") - }) + input := nerdstorage.DeleteCollectionInput{ + PackageID: packageID, + Collection: collection, + } + + var err error + switch strings.ToLower(scope) { + case "account": + _, err = client.Client.NerdStorage.DeleteCollectionWithAccountScope(accountID, input) + case "entity": + _, err = client.Client.NerdStorage.DeleteCollectionWithEntityScope(entityGUID, input) + case "user": + _, err = client.Client.NerdStorage.DeleteCollectionWithUserScope(input) + default: + log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") + } + if err != nil { + log.Fatal(err) + } + + log.Info("success") }, } @@ -120,34 +116,38 @@ func init() { Command.AddCommand(cmdCollection) cmdCollection.AddCommand(cmdCollectionGet) - cmdCollectionGet.Flags().IntVarP(&accountID, "accountId", "a", 0, "the account ID") cmdCollectionGet.Flags().StringVarP(&entityGUID, "entityGuid", "e", "", "the entity GUID") cmdCollectionGet.Flags().StringVarP(&packageID, "packageId", "p", "", "the external package ID") cmdCollectionGet.Flags().StringVarP(&collection, "collection", "c", "", "the collection name to get the document from") cmdCollectionGet.Flags().StringVarP(&scope, "scope", "s", "USER", "the scope to get the document from") - err := cmdCollectionGet.MarkFlagRequired("packageId") - utils.LogIfError(err) + if err := cmdCollectionGet.MarkFlagRequired("packageId"); err != nil { + log.Error(err) + } - err = cmdCollectionGet.MarkFlagRequired("scope") - utils.LogIfError(err) + if err := cmdCollectionGet.MarkFlagRequired("scope"); err != nil { + log.Error(err) + } - err = cmdCollectionGet.MarkFlagRequired("collection") - utils.LogIfError(err) + if err := cmdCollectionGet.MarkFlagRequired("collection"); err != nil { + log.Error(err) + } cmdCollection.AddCommand(cmdCollectionDelete) - cmdCollectionDelete.Flags().IntVarP(&accountID, "accountId", "a", 0, "the account ID") cmdCollectionDelete.Flags().StringVarP(&entityGUID, "entityGuid", "e", "", "the entity GUID") cmdCollectionDelete.Flags().StringVarP(&packageID, "packageId", "", "p", "the external package ID") cmdCollectionDelete.Flags().StringVarP(&collection, "collection", "c", "", "the collection name to delete the document from") cmdCollectionDelete.Flags().StringVarP(&scope, "scope", "s", "USER", "the scope to delete the document from") - err = cmdCollectionDelete.MarkFlagRequired("packageId") - utils.LogIfError(err) + if err := cmdCollectionDelete.MarkFlagRequired("packageId"); err != nil { + log.Error(err) + } - err = cmdCollectionDelete.MarkFlagRequired("scope") - utils.LogIfError(err) + if err := cmdCollectionDelete.MarkFlagRequired("scope"); err != nil { + log.Error(err) + } - err = cmdCollectionDelete.MarkFlagRequired("collection") - utils.LogIfError(err) + if err := cmdCollectionDelete.MarkFlagRequired("collection"); err != nil { + log.Error(err) + } } diff --git a/internal/nerdstorage/command_document.go b/internal/nerdstorage/command_document.go index d01ab05ac..a71e03dc6 100644 --- a/internal/nerdstorage/command_document.go +++ b/internal/nerdstorage/command_document.go @@ -9,8 +9,6 @@ import ( "github.com/newrelic/newrelic-cli/internal/client" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" "github.com/newrelic/newrelic-client-go/pkg/nerdstorage" ) @@ -41,33 +39,34 @@ GUID. A valid Nerdpack package ID is required. newrelic nerdstorage document get --scope USER --packageId b0dee5a1-e809-4d6f-bd3c-0682cd079612 --collection myCol --documentId myDoc `, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - var document interface{} - var err error - - input := nerdstorage.GetDocumentInput{ - PackageID: packageID, - Collection: collection, - DocumentID: documentID, - } - - switch strings.ToLower(scope) { - case "account": - document, err = nrClient.NerdStorage.GetDocumentWithAccountScope(accountID, input) - case "entity": - document, err = nrClient.NerdStorage.GetDocumentWithEntityScope(entityGUID, input) - case "user": - document, err = nrClient.NerdStorage.GetDocumentWithUserScope(input) - default: - log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") - } - if err != nil { - log.Fatal(err) - } - - utils.LogIfFatal(output.Print(document)) - log.Info("success") - }) + var document interface{} + + input := nerdstorage.GetDocumentInput{ + PackageID: packageID, + Collection: collection, + DocumentID: documentID, + } + + var err error + switch strings.ToLower(scope) { + case "account": + document, err = client.Client.NerdStorage.GetDocumentWithAccountScope(accountID, input) + case "entity": + document, err = client.Client.NerdStorage.GetDocumentWithEntityScope(entityGUID, input) + case "user": + document, err = client.Client.NerdStorage.GetDocumentWithUserScope(input) + default: + log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") + } + if err != nil { + log.Fatal(err) + } + + if err = output.Print(document); err != nil { + log.Fatal(err) + } + + log.Info("success") }, } @@ -91,36 +90,34 @@ GUID. A valid Nerdpack package ID is required. newrelic nerdstorage document write --scope USER --packageId b0dee5a1-e809-4d6f-bd3c-0682cd079612 --collection myCol --documentId myDoc --document '{"field": "myValue"}' `, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - var unmarshaled map[string]interface{} - err := json.Unmarshal([]byte(document), &unmarshaled) - if err != nil { - log.Fatalf("error parsing provided document: %s", err) - } - - input := nerdstorage.WriteDocumentInput{ - PackageID: packageID, - Collection: collection, - DocumentID: documentID, - Document: unmarshaled, - } - - switch strings.ToLower(scope) { - case "account": - _, err = nrClient.NerdStorage.WriteDocumentWithAccountScope(accountID, input) - case "entity": - _, err = nrClient.NerdStorage.WriteDocumentWithEntityScope(entityGUID, input) - case "user": - _, err = nrClient.NerdStorage.WriteDocumentWithUserScope(input) - default: - log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") - } - if err != nil { - log.Fatal(err) - } - - log.Info("success") - }) + var unmarshaled map[string]interface{} + if err := json.Unmarshal([]byte(document), &unmarshaled); err != nil { + log.Fatalf("error parsing provided document: %s", err) + } + + input := nerdstorage.WriteDocumentInput{ + PackageID: packageID, + Collection: collection, + DocumentID: documentID, + Document: unmarshaled, + } + + var err error + switch strings.ToLower(scope) { + case "account": + _, err = client.Client.NerdStorage.WriteDocumentWithAccountScope(accountID, input) + case "entity": + _, err = client.Client.NerdStorage.WriteDocumentWithEntityScope(entityGUID, input) + case "user": + _, err = client.Client.NerdStorage.WriteDocumentWithUserScope(input) + default: + log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") + } + if err != nil { + log.Fatal(err) + } + + log.Info("success") }, } @@ -144,31 +141,29 @@ GUID. A valid Nerdpack package ID is required. newrelic nerdstorage document delete --scope USER --packageId b0dee5a1-e809-4d6f-bd3c-0682cd079612 --collection myCol --documentId myDoc `, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - var err error - - input := nerdstorage.DeleteDocumentInput{ - PackageID: packageID, - Collection: collection, - DocumentID: documentID, - } - - switch strings.ToLower(scope) { - case "account": - _, err = nrClient.NerdStorage.DeleteDocumentWithAccountScope(accountID, input) - case "entity": - _, err = nrClient.NerdStorage.DeleteDocumentWithEntityScope(entityGUID, input) - case "user": - _, err = nrClient.NerdStorage.DeleteDocumentWithUserScope(input) - default: - log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") - } - if err != nil { - log.Fatal(err) - } - - log.Info("success") - }) + input := nerdstorage.DeleteDocumentInput{ + PackageID: packageID, + Collection: collection, + DocumentID: documentID, + } + + var err error + switch strings.ToLower(scope) { + case "account": + _, err = client.Client.NerdStorage.DeleteDocumentWithAccountScope(accountID, input) + case "entity": + _, err = client.Client.NerdStorage.DeleteDocumentWithEntityScope(entityGUID, input) + case "user": + _, err = client.Client.NerdStorage.DeleteDocumentWithUserScope(input) + default: + log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") + } + + if err != nil { + log.Fatal(err) + } + + log.Info("success") }, } @@ -176,27 +171,29 @@ func init() { Command.AddCommand(cmdDocument) cmdDocument.AddCommand(cmdDocumentGet) - cmdDocumentGet.Flags().IntVarP(&accountID, "accountId", "a", 0, "the account ID") cmdDocumentGet.Flags().StringVarP(&entityGUID, "entityGuid", "e", "", "the entity GUID") cmdDocumentGet.Flags().StringVarP(&packageID, "packageId", "p", "", "the external package ID") cmdDocumentGet.Flags().StringVarP(&collection, "collection", "c", "", "the collection name to get the document from") cmdDocumentGet.Flags().StringVarP(&documentID, "documentId", "d", "", "the document ID") cmdDocumentGet.Flags().StringVarP(&scope, "scope", "s", "USER", "the scope to get the document from") - err := cmdDocumentGet.MarkFlagRequired("packageId") - utils.LogIfError(err) + if err := cmdDocumentGet.MarkFlagRequired("packageId"); err != nil { + log.Error(err) + } - err = cmdDocumentGet.MarkFlagRequired("scope") - utils.LogIfError(err) + if err := cmdDocumentGet.MarkFlagRequired("scope"); err != nil { + log.Error(err) + } - err = cmdDocumentGet.MarkFlagRequired("collection") - utils.LogIfError(err) + if err := cmdDocumentGet.MarkFlagRequired("collection"); err != nil { + log.Error(err) + } - err = cmdDocumentGet.MarkFlagRequired("documentId") - utils.LogIfError(err) + if err := cmdDocumentGet.MarkFlagRequired("documentId"); err != nil { + log.Error(err) + } cmdDocument.AddCommand(cmdDocumentWrite) - cmdDocumentWrite.Flags().IntVarP(&accountID, "accountId", "a", 0, "the account ID") cmdDocumentWrite.Flags().StringVarP(&entityGUID, "entityGuid", "e", "", "the entity GUID") cmdDocumentWrite.Flags().StringVarP(&packageID, "packageId", "p", "", "the external package ID") cmdDocumentWrite.Flags().StringVarP(&collection, "collection", "c", "", "the collection name to write the document to") @@ -204,38 +201,46 @@ func init() { cmdDocumentWrite.Flags().StringVarP(&document, "document", "o", "{}", "the document to be written, in JSON format") cmdDocumentWrite.Flags().StringVarP(&scope, "scope", "s", "USER", "the scope to write the document to") - err = cmdDocumentWrite.MarkFlagRequired("packageId") - utils.LogIfError(err) + if err := cmdDocumentWrite.MarkFlagRequired("packageId"); err != nil { + log.Error(err) + } - err = cmdDocumentWrite.MarkFlagRequired("scope") - utils.LogIfError(err) + if err := cmdDocumentWrite.MarkFlagRequired("scope"); err != nil { + log.Error(err) + } - err = cmdDocumentWrite.MarkFlagRequired("document") - utils.LogIfError(err) + if err := cmdDocumentWrite.MarkFlagRequired("document"); err != nil { + log.Error(err) + } - err = cmdDocumentWrite.MarkFlagRequired("collection") - utils.LogIfError(err) + if err := cmdDocumentWrite.MarkFlagRequired("collection"); err != nil { + log.Error(err) + } - err = cmdDocumentWrite.MarkFlagRequired("documentId") - utils.LogIfError(err) + if err := cmdDocumentWrite.MarkFlagRequired("documentId"); err != nil { + log.Error(err) + } cmdDocument.AddCommand(cmdDocumentDelete) - cmdDocumentDelete.Flags().IntVarP(&accountID, "accountId", "a", 0, "the account ID") cmdDocumentDelete.Flags().StringVarP(&entityGUID, "entityGuid", "e", "", "the entity GUID") cmdDocumentDelete.Flags().StringVarP(&packageID, "packageId", "p", "", "the external package ID") cmdDocumentDelete.Flags().StringVarP(&collection, "collection", "c", "", "the collection name to delete the document from") cmdDocumentDelete.Flags().StringVarP(&documentID, "documentId", "d", "", "the document ID") cmdDocumentDelete.Flags().StringVarP(&scope, "scope", "s", "USER", "the scope to delete the document from") - err = cmdDocumentDelete.MarkFlagRequired("packageId") - utils.LogIfError(err) + if err := cmdDocumentDelete.MarkFlagRequired("packageId"); err != nil { + log.Error(err) + } - err = cmdDocumentDelete.MarkFlagRequired("scope") - utils.LogIfError(err) + if err := cmdDocumentDelete.MarkFlagRequired("scope"); err != nil { + log.Error(err) + } - err = cmdDocumentDelete.MarkFlagRequired("collection") - utils.LogIfError(err) + if err := cmdDocumentDelete.MarkFlagRequired("collection"); err != nil { + log.Error(err) + } - err = cmdDocumentDelete.MarkFlagRequired("documentId") - utils.LogIfError(err) + if err := cmdDocumentDelete.MarkFlagRequired("documentId"); err != nil { + log.Error(err) + } } diff --git a/internal/nrql/command_query.go b/internal/nrql/command_query.go index 07aea22e2..2a644069b 100644 --- a/internal/nrql/command_query.go +++ b/internal/nrql/command_query.go @@ -5,9 +5,8 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" "github.com/newrelic/newrelic-client-go/pkg/nrdb" ) @@ -27,16 +26,24 @@ This command requires the --accountId flag, which specifies the account to issue the query against. `, Example: `newrelic nrql query --accountId 12345678 --query 'SELECT count(*) FROM Transaction TIMESERIES'`, + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + var err error + if accountID, err = config.RequireAccountID(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - - result, err := nrClient.Nrdb.Query(accountID, nrdb.NRQL(query)) - if err != nil { - log.Fatal(err) - } - - utils.LogIfFatal(output.Print(result.Results)) - }) + result, err := client.Client.Nrdb.Query(accountID, nrdb.NRQL(query)) + if err != nil { + log.Fatal(err) + } + + if err := output.Print(result.Results); err != nil { + log.Fatal(err) + } }, } @@ -48,37 +55,38 @@ var cmdHistory = &cobra.Command{ The history command will fetch a list of the most recent NRQL queries you executed. `, Example: `newrelic nrql query history`, + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - - result, err := nrClient.Nrdb.QueryHistory() - if err != nil { - log.Fatal(err) - } + result, err := client.Client.Nrdb.QueryHistory() + if err != nil { + log.Fatal(err) + } - if result == nil { - log.Info("no history found. Try using the 'newrelc nrql query' command") - return - } + if result == nil { + log.Info("no history found. Try using the 'newrelc nrql query' command") + return + } - count := len(*result) + count := len(*result) - if count < historyLimit { - historyLimit = count - } + if count < historyLimit { + historyLimit = count + } - output.Text((*result)[0:historyLimit]) - }) + output.Text((*result)[0:historyLimit]) }, } func init() { Command.AddCommand(cmdQuery) - cmdQuery.Flags().IntVarP(&accountID, "accountId", "a", 0, "the New Relic account ID where you want to query") - utils.LogIfError(cmdQuery.MarkFlagRequired("accountId")) - cmdQuery.Flags().StringVarP(&query, "query", "q", "", "the NRQL query you want to execute") - utils.LogIfError(cmdQuery.MarkFlagRequired("query")) + if err := cmdQuery.MarkFlagRequired("query"); err != nil { + log.Error(err) + } Command.AddCommand(cmdHistory) cmdHistory.Flags().IntVarP(&historyLimit, "limit", "l", 10, "history items to return (default: 10, max: 100)") diff --git a/internal/nrql/command_query_test.go b/internal/nrql/command_query_test.go index b5b822f21..2e18a29da 100644 --- a/internal/nrql/command_query_test.go +++ b/internal/nrql/command_query_test.go @@ -14,5 +14,4 @@ func TestQuery(t *testing.T) { assert.Equal(t, "query", cmdQuery.Name()) testcobra.CheckCobraMetadata(t, cmdQuery) - testcobra.CheckCobraRequiredFlags(t, cmdQuery, []string{"accountId", "query"}) } diff --git a/internal/output/output.go b/internal/output/output.go index 15780542a..5c6b889c3 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -1,11 +1,10 @@ package output import ( + "log" "strings" "github.com/hokaccha/go-prettyjson" - - "github.com/newrelic/newrelic-cli/internal/utils" ) // globalOutput is the package level config of Output @@ -102,5 +101,7 @@ func ensureGlobalOutput() (err error) { } func init() { - utils.LogIfFatal(ensureGlobalOutput()) + if err := ensureGlobalOutput(); err != nil { + log.Fatal(err) + } } diff --git a/internal/output/print.go b/internal/output/print.go index 8cbf9046a..f38fc776d 100644 --- a/internal/output/print.go +++ b/internal/output/print.go @@ -2,8 +2,7 @@ package output import ( "fmt" - - "github.com/newrelic/newrelic-cli/internal/utils" + "log" ) // Print outputs the data in the expected format @@ -28,30 +27,49 @@ func Print(data interface{}) (err error) { // Printf renders output based on the format and data provided func Printf(format string, a ...interface{}) { - utils.LogIfFatal(ensureGlobalOutput()) + if err := ensureGlobalOutput(); err != nil { + log.Fatal(err) + } data := fmt.Sprintf(format, a...) - utils.LogIfFatal(globalOutput.text(data)) + if err := globalOutput.text(data); err != nil { + log.Fatal(err) + } } // JSON allows you to override the default output method and // explicitly print JSON to the screen func JSON(data interface{}) { - utils.LogIfFatal(ensureGlobalOutput()) - utils.LogIfFatal(globalOutput.json(data)) + if err := ensureGlobalOutput(); err != nil { + log.Fatal(err) + } + + if err := globalOutput.json(data); err != nil { + log.Fatal(err) + } } // Text allows you to override the default output method and // explicitly print text to the screen func Text(data interface{}) { - utils.LogIfFatal(ensureGlobalOutput()) - utils.LogIfFatal(globalOutput.text(data)) + if err := ensureGlobalOutput(); err != nil { + log.Fatal(err) + } + + if err := globalOutput.text(data); err != nil { + log.Fatal(err) + } } // YAML allows you to override the default output method and // explicitly print YAML to the screen func YAML(data interface{}) { - utils.LogIfFatal(ensureGlobalOutput()) - utils.LogIfFatal(globalOutput.yaml(data)) + if err := ensureGlobalOutput(); err != nil { + log.Fatal(err) + } + + if err := globalOutput.yaml(data); err != nil { + log.Fatal(err) + } } diff --git a/internal/pipe/pipe.go b/internal/pipe/pipe.go index c4d24a582..b3f62ae99 100644 --- a/internal/pipe/pipe.go +++ b/internal/pipe/pipe.go @@ -12,9 +12,8 @@ import ( "os" "strings" + log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" - - "github.com/newrelic/newrelic-cli/internal/utils" ) // Created Interface and struct to surround io.Reader for easy mocking @@ -104,7 +103,7 @@ func getPipeInputInnerFunc(pipe pipeReader, pipeInputExists bool, acceptedPipeIn pipeInputMap := map[string][]string{} inputArray, err := readStdin(pipe, acceptedPipeInput) if err != nil { - utils.LogIfError(err) + log.Error(err) return map[string][]string{} } for _, key := range acceptedPipeInput { diff --git a/internal/profiles/command.go b/internal/profiles/command.go new file mode 100644 index 000000000..2358af6be --- /dev/null +++ b/internal/profiles/command.go @@ -0,0 +1,244 @@ +package profiles + +import ( + "strconv" + + "github.com/jedib0t/go-pretty/v6/text" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/newrelic/newrelic-cli/internal/config" + "github.com/newrelic/newrelic-cli/internal/output" +) + +var ( + // Display keys when printing output + showKeys bool + profileName string + flagRegion string + userKey string + insightsInsertKey string + accountID int + licenseKey string +) + +const ( + defaultProfileString = " (default)" + hiddenKeyString = "" +) + +// Command is the base command for managing profiles +var Command = &cobra.Command{ + Use: "profile", + Short: "Manage the authentication profiles for this tool", + Aliases: []string{ + "profiles", // DEPRECATED: accept but not consistent with the rest of the singular usage + }, +} + +var cmdAdd = &cobra.Command{ + Use: "add", + Short: "Add a new profile", + Long: `Add a new profile + +The add command creates a new profile for use with the New Relic CLI. +API key and region are required. An Insights insert key is optional, but required +for posting custom events with the ` + "`newrelic events`" + `command. +`, + Example: "newrelic profile add --name --region --apiKey --insightsInsertKey --accountId --licenseKey ", + Run: func(cmd *cobra.Command, args []string) { + if config.ProfileExists(profileName) { + log.Fatalf("profile already exists: %s", profileName) + } + + if err := config.SaveValueToProfile(profileName, config.UserKey, userKey); err != nil { + if e := config.RemoveProfile(profileName); e != nil { + log.Error(e) + } + log.Fatal(err) + } + + if err := config.SaveValueToProfile(profileName, config.Region, flagRegion); err != nil { + if e := config.RemoveProfile(profileName); e != nil { + log.Error(e) + } + log.Fatal(err) + } + + if err := config.SaveValueToProfile(profileName, config.InsightsInsertKey, insightsInsertKey); err != nil { + if e := config.RemoveProfile(profileName); e != nil { + log.Error(e) + } + log.Fatal(err) + } + + if err := config.SaveValueToProfile(profileName, config.AccountID, accountID); err != nil { + if e := config.RemoveProfile(profileName); e != nil { + log.Error(e) + } + log.Fatal(err) + } + + if err := config.SaveValueToProfile(profileName, config.LicenseKey, licenseKey); err != nil { + if e := config.RemoveProfile(profileName); e != nil { + log.Error(e) + } + log.Fatal(err) + } + + log.Infof("profile %s added", text.FgCyan.Sprint(profileName)) + }, +} + +var cmdDefault = &cobra.Command{ + Use: "default", + Short: "Set the default profile name", + Long: `Set the default profile name + +The default command sets the profile to use by default using the specified name. +`, + Example: "newrelic profile default --name ", + Run: func(cmd *cobra.Command, args []string) { + if err := config.SaveDefaultProfileName(profileName); err != nil { + log.Fatal(err) + } + + log.Info("success") + }, +} + +var cmdList = &cobra.Command{ + Use: "list", + Short: "List the profiles available", + Long: `List the profiles available + +The list command prints out the available profiles' credentials. +`, + Example: "newrelic profile list", + Run: func(cmd *cobra.Command, args []string) { + out := []profileList{} + profileNames := config.GetProfileNames() + + if len(profileNames) == 0 { + log.Info("no profiles found") + return + } + + // Print them out + for _, n := range profileNames { + var accountIDStr string + accountIDVal := config.GetProfileValueInt(n, config.AccountID) + if accountIDVal != 0 { + accountIDStr = strconv.Itoa(accountIDVal) + } + + userKeyStr := config.GetProfileValueString(n, config.UserKey) + insightsInsertKeyStr := config.GetProfileValueString(n, config.InsightsInsertKey) + licenseKeyStr := config.GetProfileValueString(n, config.LicenseKey) + regionStr := config.GetProfileValueString(n, config.Region) + + if !showKeys { + if userKeyStr != "" { + userKeyStr = text.FgHiBlack.Sprint(hiddenKeyString) + } + + if insightsInsertKeyStr != "" { + insightsInsertKeyStr = text.FgHiBlack.Sprint(hiddenKeyString) + } + + if licenseKeyStr != "" { + licenseKeyStr = text.FgHiBlack.Sprint(hiddenKeyString) + } + } + + if n == config.GetDefaultProfileName() { + n += text.FgHiBlack.Sprint(defaultProfileString) + } + + out = append(out, profileList{ + Name: n, + Region: regionStr, + UserKey: userKeyStr, + InsightsInsertKey: insightsInsertKeyStr, + AccountID: accountIDStr, + LicenseKey: licenseKeyStr, + }) + } + + output.Text(out) + }, + Aliases: []string{ + "ls", + }, +} + +var cmdDelete = &cobra.Command{ + Use: "delete", + Short: "Delete a profile", + Long: `Delete a profile + +The delete command removes the profile specified by name. +`, + Example: "newrelic profile delete --name ", + Run: func(cmd *cobra.Command, args []string) { + err := config.RemoveProfile(profileName) + if err != nil { + log.Fatal(err) + } + + log.Info("success") + }, + Aliases: []string{ + "remove", + "rm", + }, +} + +func init() { + // Add + Command.AddCommand(cmdAdd) + cmdAdd.Flags().StringVarP(&profileName, "name", "n", "", "unique profile name to add") + cmdAdd.Flags().StringVarP(&flagRegion, "region", "r", "", "the US or EU region") + cmdAdd.Flags().StringVarP(&userKey, "apiKey", "", "", "your User API key") + cmdAdd.Flags().StringVarP(&insightsInsertKey, "insightsInsertKey", "", "", "your Insights insert key") + cmdAdd.Flags().StringVarP(&licenseKey, "licenseKey", "", "", "your license key") + cmdAdd.Flags().IntVarP(&accountID, "accountId", "", 0, "your account ID") + if err := cmdAdd.MarkFlagRequired("name"); err != nil { + log.Error(err) + } + + if err := cmdAdd.MarkFlagRequired("region"); err != nil { + log.Error(err) + } + + if err := cmdAdd.MarkFlagRequired("apiKey"); err != nil { + log.Error(err) + } + + // Default + Command.AddCommand(cmdDefault) + cmdDefault.Flags().StringVarP(&profileName, "name", "n", "", "the profile name to set as default") + if err := cmdDefault.MarkFlagRequired("name"); err != nil { + log.Error(err) + } + + // List + Command.AddCommand(cmdList) + cmdList.Flags().BoolVarP(&showKeys, "show-keys", "s", false, "list the profiles on your keychain") + + // Remove + Command.AddCommand(cmdDelete) + cmdDelete.Flags().StringVarP(&profileName, "name", "n", "", "the profile name to delete") + if err := cmdDelete.MarkFlagRequired("name"); err != nil { + log.Error(err) + } +} + +type profileList struct { + Name string + AccountID string + Region string + UserKey string + LicenseKey string + InsightsInsertKey string +} diff --git a/internal/credentials/command_test.go b/internal/profiles/command_test.go similarity index 85% rename from internal/credentials/command_test.go rename to internal/profiles/command_test.go index 707814f46..97231f26d 100644 --- a/internal/credentials/command_test.go +++ b/internal/profiles/command_test.go @@ -1,6 +1,6 @@ // +build unit -package credentials +package profiles import ( "testing" @@ -10,7 +10,7 @@ import ( "github.com/newrelic/newrelic-cli/internal/testcobra" ) -func TestCredentialsCommand(t *testing.T) { +func TestProfilesCommand(t *testing.T) { assert.Equal(t, "profile", Command.Name()) testcobra.CheckCobraMetadata(t, Command) @@ -18,7 +18,7 @@ func TestCredentialsCommand(t *testing.T) { testcobra.CheckCobraCommandAliases(t, Command, []string{"profiles"}) // DEPRECATED: from nr1 cli } -func TestCredentialsAdd(t *testing.T) { +func TestProfilesAdd(t *testing.T) { assert.Equal(t, "add", cmdAdd.Name()) testcobra.CheckCobraMetadata(t, cmdAdd) @@ -26,7 +26,7 @@ func TestCredentialsAdd(t *testing.T) { testcobra.CheckCobraCommandAliases(t, cmdAdd, []string{}) } -func TestCredentialsDefault(t *testing.T) { +func TestProfilesDefault(t *testing.T) { assert.Equal(t, "default", cmdDefault.Name()) testcobra.CheckCobraMetadata(t, cmdDefault) @@ -34,7 +34,7 @@ func TestCredentialsDefault(t *testing.T) { testcobra.CheckCobraCommandAliases(t, cmdDefault, []string{}) } -func TestCredentialsList(t *testing.T) { +func TestProfilesList(t *testing.T) { assert.Equal(t, "list", cmdList.Name()) testcobra.CheckCobraMetadata(t, cmdList) @@ -42,7 +42,7 @@ func TestCredentialsList(t *testing.T) { testcobra.CheckCobraCommandAliases(t, cmdList, []string{"ls"}) // DEPRECATED: from nr1 cli } -func TestCredentialsDelete(t *testing.T) { +func TestProfilesDelete(t *testing.T) { assert.Equal(t, "delete", cmdDelete.Name()) testcobra.CheckCobraMetadata(t, cmdDelete) diff --git a/internal/reporting/command_junit.go b/internal/reporting/command_junit.go index 8ee1910b8..6150aab0e 100644 --- a/internal/reporting/command_junit.go +++ b/internal/reporting/command_junit.go @@ -9,10 +9,8 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" - "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" ) const junitEventType = "TestRun" @@ -31,49 +29,60 @@ var cmdJUnit = &cobra.Command{ `, Example: `newrelic reporting junit --accountId 12345678 --path unit.xml`, + PreRun: func(cmd *cobra.Command, args []string) { + var err error + if accountID, err = config.RequireAccountID(); err != nil { + log.Fatal(err) + } + + if _, err = config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClientAndProfile(func(nrClient *newrelic.NewRelic, profile *credentials.Profile) { - if profile.InsightsInsertKey == "" { - log.Fatal("an Insights insert key is required, set one in your default profile or use the NEW_RELIC_INSIGHTS_INSERT_KEY environment variable") - } - - id, err := uuid.NewRandom() - if err != nil { - log.Fatal(err) + insightsInsertKey := config.GetActiveProfileValueString(config.InsightsInsertKey) + if insightsInsertKey == "" { + log.Fatal("an Insights insert key is required, set one in your default profile or use the NEW_RELIC_INSIGHTS_INSERT_KEY environment variable") + } + + id, err := uuid.NewRandom() + if err != nil { + log.Fatal(err) + } + + xml, err := ioutil.ReadFile(path) + if err != nil { + log.Fatal(err) + } + + suites, err := junit.Ingest(xml) + if err != nil { + log.Fatalf("failed to ingest JUnit xml %v", err) + } + + events := []map[string]interface{}{} + + for _, suite := range suites { + for _, test := range suite.Tests { + events = append(events, createTestRunEvent(id, suite, test)) } + } - xml, err := ioutil.ReadFile(path) - if err != nil { + if outputEvents { + if err := output.Print(events); err != nil { log.Fatal(err) } + } - suites, err := junit.Ingest(xml) - if err != nil { - log.Fatalf("failed to ingest JUnit xml %v", err) - } - - events := []map[string]interface{}{} + if dryRun { + return + } - for _, suite := range suites { - for _, test := range suite.Tests { - events = append(events, createTestRunEvent(id, suite, test)) - } - } + if err := client.Client.Events.CreateEvent(accountID, events); err != nil { + log.Fatal(err) + } - if outputEvents { - utils.LogIfFatal(output.Print(events)) - } - - if dryRun { - return - } - - if err := nrClient.Events.CreateEvent(accountID, events); err != nil { - log.Fatal(err) - } - - log.Info("success") - }) + log.Info("success") }, } @@ -105,10 +114,10 @@ func createTestRunEvent(testRunID uuid.UUID, suite junit.Suite, test junit.Test) func init() { Command.AddCommand(cmdJUnit) - cmdJUnit.Flags().IntVarP(&accountID, "accountId", "a", 0, "the New Relic account ID to send test run results to") cmdJUnit.Flags().StringVarP(&path, "path", "p", "", "the path to a JUnit-formatted test results file") cmdJUnit.Flags().BoolVarP(&outputEvents, "output", "o", false, "output generated custom events to stdout") cmdJUnit.Flags().BoolVar(&dryRun, "dryRun", false, "suppress posting custom events to NRDB") - utils.LogIfError(cmdJUnit.MarkFlagRequired("accountId")) - utils.LogIfError(cmdJUnit.MarkFlagRequired("path")) + if err := cmdJUnit.MarkFlagRequired("path"); err != nil { + log.Error(err) + } } diff --git a/internal/reporting/command_junit_test.go b/internal/reporting/command_junit_test.go index 18dd815a6..182731847 100644 --- a/internal/reporting/command_junit_test.go +++ b/internal/reporting/command_junit_test.go @@ -14,5 +14,4 @@ func TestJUnit(t *testing.T) { assert.Equal(t, "junit", cmdJUnit.Name()) testcobra.CheckCobraMetadata(t, cmdJUnit) - testcobra.CheckCobraRequiredFlags(t, cmdJUnit, []string{"accountId", "path"}) } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 3bfe26af0..7ba2bbf4d 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -2,7 +2,6 @@ package utils import ( "context" - "fmt" "os" "os/signal" "reflect" @@ -10,16 +9,14 @@ import ( "syscall" "time" - "github.com/mitchellh/go-homedir" - log "github.com/sirupsen/logrus" ) var ( - SignalCtx context.Context = getSignalContext() + SignalCtx context.Context = createSignalContext() ) -func getSignalContext() context.Context { +func createSignalContext() context.Context { ch := make(chan os.Signal, 1) ctx, cancel := context.WithCancel(context.Background()) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) @@ -63,33 +60,6 @@ func StructToMap(item interface{}, fields []string) map[string]interface{} { return mapped } -// LogIfError wraps the err nil check to cleanup the code. -// Logs at Error level -func LogIfError(err error) { - if err != nil { - log.Error(err) - } -} - -// LogIfFatal wraps the err nil check to cleanup the code. -// Logs at Fatal level -func LogIfFatal(err error) { - if err != nil { - log.Fatal(err) - } -} - -// GetDefaultConfigDirectory returns the full path to the .newrelic -// directory within the user's home directory. -func GetDefaultConfigDirectory() (string, error) { - home, err := homedir.Dir() - if err != nil { - return "", err - } - - return fmt.Sprintf("%s/.newrelic", home), nil -} - // MinOf returns the minimum int value provided. func MinOf(vars ...int) int { min := vars[0] diff --git a/internal/workload/command_workload.go b/internal/workload/command_workload.go index 1bca4d897..7b16ebe19 100644 --- a/internal/workload/command_workload.go +++ b/internal/workload/command_workload.go @@ -5,9 +5,8 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/output" - "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" "github.com/newrelic/newrelic-client-go/pkg/workloads" ) @@ -28,13 +27,25 @@ var cmdGet = &cobra.Command{ The get command retrieves a specific workload by its account ID and workload GUID. `, Example: `newrelic workload create --accountId 12345678 --guid MjUyMDUyOHxOUjF8V09SS0xPQUR8MTI4Myt`, + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + + var err error + if accountID, err = config.RequireAccountID(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - workload, err := nrClient.Workloads.GetWorkload(accountID, guid) - utils.LogIfFatal(err) - - utils.LogIfFatal(output.Print(workload)) - }) + workload, err := client.Client.Workloads.GetWorkload(accountID, guid) + if err != nil { + log.Fatal(err) + } + + if err := output.Print(workload); err != nil { + log.Fatal(err) + } }, } @@ -46,13 +57,25 @@ var cmdList = &cobra.Command{ The list command retrieves the workloads for the given account ID. `, Example: `newrelic workload list --accountId 12345678`, + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + + var err error + if accountID, err = config.RequireAccountID(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - workload, err := nrClient.Workloads.ListWorkloads(accountID) - utils.LogIfFatal(err) - - utils.LogIfFatal(output.Print(workload)) - }) + workload, err := client.Client.Workloads.ListWorkloads(accountID) + if err != nil { + log.Fatal(err) + } + + if err := output.Print(workload); err != nil { + log.Fatal(err) + } }, } @@ -69,34 +92,47 @@ IDs can optionally be provided to include entities from different sub-accounts t you also have access to. `, Example: `newrelic workload create --name 'Example workload' --accountId 12345678 --entitySearchQuery "name like 'Example application'"`, + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + + var err error + if accountID, err = config.RequireAccountID(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - createInput := workloads.CreateInput{ - Name: name, + createInput := workloads.CreateInput{ + Name: name, + } + + if len(entityGUIDs) > 0 { + createInput.EntityGUIDs = entityGUIDs + } + + if len(entitySearchQueries) > 0 { + var queryInputs []workloads.EntitySearchQueryInput + for _, q := range entitySearchQueries { + queryInputs = append(queryInputs, workloads.EntitySearchQueryInput{Query: q}) } + createInput.EntitySearchQueries = queryInputs + } - if len(entityGUIDs) > 0 { - createInput.EntityGUIDs = entityGUIDs - } + if len(scopeAccountIDs) > 0 { + createInput.ScopeAccountsInput = &workloads.ScopeAccountsInput{AccountIDs: scopeAccountIDs} + } - if len(entitySearchQueries) > 0 { - var queryInputs []workloads.EntitySearchQueryInput - for _, q := range entitySearchQueries { - queryInputs = append(queryInputs, workloads.EntitySearchQueryInput{Query: q}) - } - createInput.EntitySearchQueries = queryInputs - } + workload, err := client.Client.Workloads.CreateWorkload(accountID, createInput) + if err != nil { + log.Fatal(err) + } - if len(scopeAccountIDs) > 0 { - createInput.ScopeAccountsInput = &workloads.ScopeAccountsInput{AccountIDs: scopeAccountIDs} - } + if err := output.Print(workload); err != nil { + log.Fatal(err) + } - workload, err := nrClient.Workloads.CreateWorkload(accountID, createInput) - utils.LogIfFatal(err) - - utils.LogIfFatal(output.Print(workload)) - log.Info("success") - }) + log.Info("success") }, } @@ -113,33 +149,38 @@ together with an OR. Multiple account scope IDs can optionally be provided to i entities from different sub-accounts that you also have access to. `, Example: `newrelic workload update --guid 'MjUyMDUyOHxBOE28QVBQTElDQVRDT058MjE1MDM3Nzk1' --name 'Updated workflow'`, + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - updateInput := workloads.UpdateInput{ - Name: name, + updateInput := workloads.UpdateInput{ + Name: name, + } + + if len(entityGUIDs) > 0 { + updateInput.EntityGUIDs = entityGUIDs + } + + if len(entitySearchQueries) > 0 { + var queryInputs []workloads.EntitySearchQueryInput + for _, q := range entitySearchQueries { + queryInputs = append(queryInputs, workloads.EntitySearchQueryInput{Query: q}) } + updateInput.EntitySearchQueries = queryInputs + } - if len(entityGUIDs) > 0 { - updateInput.EntityGUIDs = entityGUIDs - } - - if len(entitySearchQueries) > 0 { - var queryInputs []workloads.EntitySearchQueryInput - for _, q := range entitySearchQueries { - queryInputs = append(queryInputs, workloads.EntitySearchQueryInput{Query: q}) - } - updateInput.EntitySearchQueries = queryInputs - } - - if len(scopeAccountIDs) > 0 { - updateInput.ScopeAccountsInput = &workloads.ScopeAccountsInput{AccountIDs: scopeAccountIDs} - } + if len(scopeAccountIDs) > 0 { + updateInput.ScopeAccountsInput = &workloads.ScopeAccountsInput{AccountIDs: scopeAccountIDs} + } - _, err := nrClient.Workloads.UpdateWorkload(guid, updateInput) - utils.LogIfFatal(err) + _, err := client.Client.Workloads.UpdateWorkload(guid, updateInput) + if err != nil { + log.Fatal(err) + } - log.Info("success") - }) + log.Info("success") }, } @@ -154,18 +195,31 @@ If the name isn't specified, the name + ' copy' of the source workload is used t compose the new name. `, Example: `newrelic workload duplicate --guid 'MjUyMDUyOHxBOE28QVBQTElDQVRDT058MjE1MDM3Nzk1' --accountID 12345678 --name 'New Workload'`, + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + + var err error + if accountID, err = config.RequireAccountID(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - duplicateInput := &workloads.DuplicateInput{ - Name: name, - } + duplicateInput := &workloads.DuplicateInput{ + Name: name, + } - workload, err := nrClient.Workloads.DuplicateWorkload(accountID, guid, duplicateInput) - utils.LogIfFatal(err) + workload, err := client.Client.Workloads.DuplicateWorkload(accountID, guid, duplicateInput) + if err != nil { + log.Fatal(err) + } - utils.LogIfFatal(output.Print(workload)) - log.Info("success") - }) + if err := output.Print(workload); err != nil { + log.Fatal(err) + } + + log.Info("success") }, } @@ -177,38 +231,41 @@ var cmdDelete = &cobra.Command{ The delete command accepts a workload's entity GUID. `, Example: `newrelic workload delete --guid 'MjUyMDUyOHxBOE28QVBQTElDQVRDT058MjE1MDM3Nzk1'`, + PreRun: func(cmd *cobra.Command, args []string) { + if _, err := config.RequireUserKey(); err != nil { + log.Fatal(err) + } + }, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - _, err := nrClient.Workloads.DeleteWorkload(guid) - utils.LogIfFatal(err) + _, err := client.Client.Workloads.DeleteWorkload(guid) + if err != nil { + log.Fatal(err) + } - log.Info("success") - }) + log.Info("success") }, } func init() { // Get Command.AddCommand(cmdGet) - cmdGet.Flags().IntVarP(&accountID, "accountId", "a", 0, "the New Relic account ID where the workload is located") cmdGet.Flags().StringVarP(&guid, "guid", "g", "", "the GUID of the workload") - utils.LogIfError(cmdGet.MarkFlagRequired("accountId")) - utils.LogIfError(cmdGet.MarkFlagRequired("guid")) + if err := cmdGet.MarkFlagRequired("guid"); err != nil { + log.Error(err) + } // List Command.AddCommand(cmdList) - cmdList.Flags().IntVarP(&accountID, "accountId", "a", 0, "the New Relic account ID you want to list workloads for") - utils.LogIfError(cmdList.MarkFlagRequired("accountId")) // Create Command.AddCommand(cmdCreate) - cmdCreate.Flags().IntVarP(&accountID, "accountId", "a", 0, "the New Relic account ID where you want to create the workload") cmdCreate.Flags().StringVarP(&name, "name", "n", "", "the name of the workload") cmdCreate.Flags().StringSliceVarP(&entityGUIDs, "entityGuid", "e", []string{}, "the list of entity Guids composing the workload") cmdCreate.Flags().StringSliceVarP(&entitySearchQueries, "entitySearchQuery", "q", []string{}, "a list of search queries, combined using an OR operator") cmdCreate.Flags().IntSliceVarP(&scopeAccountIDs, "scopeAccountIds", "s", []int{}, "accounts that will be used to get entities from") - utils.LogIfError(cmdCreate.MarkFlagRequired("accountId")) - utils.LogIfError(cmdCreate.MarkFlagRequired("name")) + if err := cmdCreate.MarkFlagRequired("name"); err != nil { + log.Error(err) + } // Update Command.AddCommand(cmdUpdate) @@ -217,18 +274,22 @@ func init() { cmdUpdate.Flags().StringSliceVarP(&entityGUIDs, "entityGuid", "e", []string{}, "the list of entity Guids composing the workload") cmdUpdate.Flags().StringSliceVarP(&entitySearchQueries, "entitySearchQuery", "q", []string{}, "a list of search queries, combined using an OR operator") cmdUpdate.Flags().IntSliceVarP(&scopeAccountIDs, "scopeAccountIds", "s", []int{}, "accounts that will be used to get entities from") - utils.LogIfError(cmdUpdate.MarkFlagRequired("guid")) + if err := cmdUpdate.MarkFlagRequired("guid"); err != nil { + log.Error(err) + } // Duplicate Command.AddCommand(cmdDuplicate) cmdDuplicate.Flags().StringVarP(&guid, "guid", "g", "", "the GUID of the workload you want to duplicate") - cmdDuplicate.Flags().IntVarP(&accountID, "accountId", "a", 0, "the New Relic Account ID where you want to create the new workload") cmdDuplicate.Flags().StringVarP(&name, "name", "n", "", "the name of the workload to duplicate") - utils.LogIfError(cmdDuplicate.MarkFlagRequired("accountId")) - utils.LogIfError(cmdDuplicate.MarkFlagRequired("guid")) + if err := cmdDuplicate.MarkFlagRequired("guid"); err != nil { + log.Error(err) + } // Delete Command.AddCommand(cmdDelete) cmdDelete.Flags().StringVarP(&guid, "guid", "g", "", "the GUID of the workload to delete") - utils.LogIfError(cmdDelete.MarkFlagRequired("guid")) + if err := cmdDelete.MarkFlagRequired("guid"); err != nil { + log.Error(err) + } } diff --git a/templates/command.go.tmpl b/templates/command.go.tmpl index 4bdfae911..d55db57da 100644 --- a/templates/command.go.tmpl +++ b/templates/command.go.tmpl @@ -37,14 +37,20 @@ var {{ $cmdVarName }} = &cobra.Command{ var {{ .Name }} {{ .ClientType }} err := json.Unmarshal([]byte({{ .VariableName }}), &{{ .Name }}) - utils.LogIfFatal(err) + if err != nil { + log.Fatal(err) + } {{- end -}} {{ end }} resp, err := {{ .ClientMethod }}({{ .ClientMethodArgs | join ", " }}) - utils.LogIfFatal(err) + if err != nil { + log.Fatal(err) + } - utils.LogIfFatal(output.Print(resp)) + if err := output.Print(resp); err != nil { + log.Fatal(err) + } }) }, } @@ -68,7 +74,9 @@ func init() { {{ $cmdVarName }}.Flags().{{- .FlagMethodName -}}(&{{ .VariableName }}, {{ .Name | quote }}, {{ $defaultVal }}, {{ .Description | quote }}) {{- if .Required }} - utils.LogIfError({{- $cmdVarName -}}.MarkFlagRequired({{.Name | quote }})) + if err := {{- $cmdVarName -}}.MarkFlagRequired({{.Name | quote }}); err != nil { + log.Error(err) + } {{ end }} {{- end }} {{- end }}