Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(config): config/profiles refactor for simplicity #605

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
81ca149
chore(config): starting configuration refactor
ctrombley Jan 6, 2021
4aa6d17
chore(config): continue configuration refactor, add loadDefaultProfil…
sanderblue Jan 6, 2021
8098dd2
chore(config): set credential values
sanderblue Jan 8, 2021
5a73553
chore(config): add set functions for config.json values
sanderblue Jan 8, 2021
3a1f767
chore(config): linting fixes and refactoring
ctrombley Jan 12, 2021
4f6d8b5
chore(config): add default profile name getter and setter
ctrombley Jan 12, 2021
5c609db
chore(config): implement config commands and validation
ctrombley Jan 13, 2021
8ff5b1f
chore(config): add tests
ctrombley Jan 13, 2021
533409b
chore(config): add env var overrides
ctrombley Jan 13, 2021
aefc803
chore(config): integrate new config mechanism throught codebase
ctrombley Jan 14, 2021
0c70f9d
chore(config): add validation for ConfigField values
sanderblue Jan 14, 2021
3bd656a
chore: fix compilation errors
sanderblue Jan 14, 2021
f2c8f2d
chore(config): add findConfigField fn
sanderblue Jan 15, 2021
250dd28
chore(config): add validationfunc and profile overrides
ctrombley Jan 15, 2021
18e8cb7
chore(config): simplify client creation
ctrombley Jan 15, 2021
b12626b
chore(config): implement profile list command
ctrombley Jan 15, 2021
7687915
chore(config): shorten new package name
ctrombley Jan 15, 2021
de24b47
chore(config): add profile value validation
ctrombley Jan 16, 2021
2468949
chore(config): improve logging during default profile initialization
ctrombley Jan 16, 2021
a804d67
chore(config): remove typo
ctrombley Jan 19, 2021
8efd44c
chore(config): add persistent accountID field
ctrombley Jan 19, 2021
7060ad3
chore(config): incorporate code review feedback
ctrombley Jan 21, 2021
3071d45
chore(config): fix rebase errors
ctrombley Jan 21, 2021
e60f6e5
chore: incorporate review feedback
ctrombley Jan 21, 2021
dfdc409
chore: move profile checks to prerun hooks
ctrombley Feb 2, 2021
32c8edb
Merge branch 'master' into chore/config-refactor
ctrombley Feb 2, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 123 additions & 88 deletions cmd/newrelic/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"errors"
"fmt"
"os"
"strconv"

Expand All @@ -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{
Expand All @@ -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"
ctrombley marked this conversation as resolved.
Show resolved Hide resolved
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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
}
6 changes: 3 additions & 3 deletions cmd/newrelic/command_completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/newrelic/newrelic-cli/internal/utils"
)

var (
Expand Down Expand Up @@ -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)
}
}
14 changes: 9 additions & 5 deletions cmd/newrelic/command_documentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -24,7 +22,9 @@ newrelic documentation --outputDir <my directory> --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 <my directory> is required")
}

Expand All @@ -44,7 +44,9 @@ newrelic documentation --outputDir <my directory> --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]")
}
},
Expand All @@ -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)
}
}
Loading