diff --git a/cmd/newrelic/command.go b/cmd/newrelic/command.go index b686002e0..4fc232b65 100644 --- a/cmd/newrelic/command.go +++ b/cmd/newrelic/command.go @@ -1,30 +1,23 @@ package main import ( - "context" - "errors" - "fmt" "os" - "strconv" - "github.com/jedib0t/go-pretty/v6/text" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" - "github.com/newrelic/newrelic-cli/internal/credentials" - "github.com/newrelic/newrelic-cli/internal/install/types" + "github.com/newrelic/newrelic-cli/internal/config" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "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 +) // Command represents the base command when called without any subcommands var Command = &cobra.Command{ @@ -37,202 +30,23 @@ var Command = &cobra.Command{ } func initializeCLI(cmd *cobra.Command, args []string) { - initializeProfile(utils.SignalCtx) -} - -func initializeProfile(ctx context.Context) { - var accountID int - var region string - var licenseKey string - var insightsInsertKey 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 - } - - 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") - insightsInsertKey = os.Getenv("NEW_RELIC_INSIGHTS_INSERT_KEY") - - // 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 - } - - // Default the region to US if it's not in the environment - if region == "" { - region = "US" - } - - // 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 - } - } - - // 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 - } - } - - if licenseKey == "" { - // We should have an account ID by now, so fetch the license key for it. - licenseKey, err = fetchLicenseKey(ctx, nrClient, accountID) - if err != nil { - log.Error(err) - return - } - } - - if insightsInsertKey == "" { - // We should have an API key by now, so fetch the insights insert key for it. - insightsInsertKey, err = fetchInsightsInsertKey(nrClient, accountID) - if err != nil { - log.Error(err) - } - } - - if !hasProfileWithDefaultName(c.Profiles) { - p := credentials.Profile{ - Region: region, - APIKey: apiKey, - AccountID: accountID, - LicenseKey: licenseKey, - InsightsInsertKey: insightsInsertKey, - } - - err = c.AddProfile(defaultProfileName, p) - if err != nil { - return - } - - log.Infof("profile %s added", text.FgCyan.Sprint(defaultProfileName)) - } - - 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 - } - - log.Infof("setting %s as default profile", text.FgCyan.Sprint(defaultProfileName)) - } - }) - }) - - if err != nil { - log.Debugf("couldn't initialize default profile: %s", err) - } -} - -func hasProfileWithDefaultName(profiles map[string]credentials.Profile) bool { - for profileName := range profiles { - if profileName == defaultProfileName { - return true - } - } - - return false -} - -func fetchLicenseKey(ctx context.Context, client *newrelic.NewRelic, accountID int) (string, error) { - var licenseKey string - retryFunc := func() error { - l, err := execLicenseKeyRequest(ctx, client, accountID) - if err != nil { - return err - } - - licenseKey = l - return nil - } - - r := utils.NewRetry(3, 1, retryFunc) - if err := r.ExecWithRetries(ctx); err != nil { - return "", err - } - - return licenseKey, nil -} - -func execLicenseKeyRequest(ctx context.Context, client *newrelic.NewRelic, accountID int) (string, error) { - query := `query($accountId: Int!) { actor { account(id: $accountId) { licenseKey } } }` - - variables := map[string]interface{}{ - "accountId": accountID, - } + logLevel := configAPI.GetLogLevel() + config.InitLogger(logLevel) - resp, err := client.NerdGraph.QueryWithContext(ctx, query, variables) - if err != nil { - return "", err + if client.NRClient == nil { + client.NRClient = createClient() } - - queryResp := resp.(nerdgraph.QueryResponse) - actor := queryResp.Actor.(map[string]interface{}) - account := actor["account"].(map[string]interface{}) - - if l, ok := account["licenseKey"]; ok { - if l != nil { - return l.(string), nil - } - } - - return "", types.ErrorFetchingLicenseKey } -func fetchInsightsInsertKey(client *newrelic.NewRelic, accountID int) (string, error) { - // Check for an existing key first - keys, err := client.APIAccess.ListInsightsInsertKeys(accountID) +func createClient() *newrelic.NewRelic { + c, err := client.NewClient(configAPI.GetActiveProfileName()) if err != nil { - return "", types.ErrorFetchingInsightsInsertKey - } - - // We already have a key, return it - if len(keys) > 0 { - return keys[0].Key, nil - } - - // Create a new key if one doesn't exist - key, err := client.APIAccess.CreateInsightsInsertKey(accountID) - if err != nil { - return "", types.ErrorFetchingInsightsInsertKey - } - - return key.Key, nil -} - -// 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) { - params := accounts.ListAccountsParams{ - Scope: &accounts.RegionScopeTypes.IN_REGION, - } - - accounts, err := client.Accounts.ListAccounts(params) - if err != nil { - return 0, err - } - - if len(accounts) == 1 { - return accounts[0].ID, 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 0, errors.New("multiple accounts found, please set NEW_RELIC_ACCOUNT_ID") + return c } // Execute adds all child commands to the root command and sets flags appropriately. @@ -253,7 +67,11 @@ func init() { cobra.OnInitialize(initConfig) Command.PersistentFlags().StringVar(&outputFormat, "format", output.DefaultFormat.String(), "output text format ["+output.FormatOptions()+"]") + Command.PersistentFlags().StringVar(&config.FlagProfileName, "profile", "", "the authentication profile to use") Command.PersistentFlags().BoolVar(&outputPlain, "plain", false, "output compact text") + Command.PersistentFlags().BoolVar(&config.FlagDebug, "debug", false, "debug level logging") + Command.PersistentFlags().BoolVar(&config.FlagTrace, "trace", false, "trace level logging") + Command.PersistentFlags().IntVarP(&config.FlagAccountID, "accountId", "a", 0, "trace level logging") } func initConfig() { diff --git a/cmd/newrelic/command_integration_test.go b/cmd/newrelic/command_integration_test.go deleted file mode 100644 index 7b5465a11..000000000 --- a/cmd/newrelic/command_integration_test.go +++ /dev/null @@ -1,166 +0,0 @@ -// +build integration - -package main - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "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" - "github.com/newrelic/newrelic-cli/internal/install/types" - - "github.com/newrelic/newrelic-client-go/newrelic" - "github.com/newrelic/newrelic-client-go/pkg/nerdgraph" - mock "github.com/newrelic/newrelic-client-go/pkg/testhelpers" -) - -func TestInitializeProfile(t *testing.T) { - - apiKey := os.Getenv("NEW_RELIC_API_KEY") - envAccountID := os.Getenv("NEW_RELIC_ACCOUNT_ID") - if apiKey == "" || 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 - - // Init without the necessary environment variables - os.Setenv("NEW_RELIC_API_KEY", "") - os.Setenv("NEW_RELIC_ACCOUNT_ID", "") - initializeProfile(context.Background()) - - // 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) - - // Init with environment - os.Setenv("NEW_RELIC_API_KEY", apiKey) - os.Setenv("NEW_RELIC_ACCOUNT_ID", envAccountID) - initializeProfile(context.Background()) - - // 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) - assert.NotEmpty(t, c.Profiles[defaultProfileName].LicenseKey) - assert.NotEmpty(t, c.Profiles[defaultProfileName].InsightsInsertKey) - - // 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) - } - - initializeProfile(context.Background()) -} - -func TestFetchLicenseKey_missingKey(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(missingLicenseKey)) - }) - - ts := httptest.NewServer(handler) - tc := mock.NewTestConfig(t, ts) - - nr := &newrelic.NewRelic{ - NerdGraph: nerdgraph.New(tc), - } - - response, err := fetchLicenseKey(context.Background(), nr, 0) - require.Error(t, err, types.ErrorFetchingLicenseKey) - require.Empty(t, response) -} - -func TestFetchLicenseKey_nilKey(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(withNilLicenseKey)) - }) - - ts := httptest.NewServer(handler) - tc := mock.NewTestConfig(t, ts) - - nr := &newrelic.NewRelic{ - NerdGraph: nerdgraph.New(tc), - } - - response, err := fetchLicenseKey(context.Background(), nr, 0) - require.Error(t, err, types.ErrorFetchingLicenseKey) - require.Empty(t, response) -} -func TestFetchLicenseKey_withKey(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(withLicenseKey)) - }) - - ts := httptest.NewServer(handler) - tc := mock.NewTestConfig(t, ts) - - nr := &newrelic.NewRelic{ - NerdGraph: nerdgraph.New(tc), - } - - response, err := fetchLicenseKey(context.Background(), nr, 0) - require.NoError(t, err) - require.Equal(t, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", response) -} - -var missingLicenseKey string = ` -{ - "data": { - "actor": { - "account": { - } - } - } -} -` - -var withLicenseKey string = ` -{ - "data": { - "actor": { - "account": { - "licenseKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - } - } - } -} -` - -var withNilLicenseKey string = ` -{ - "data": { - "actor": { - "account": { - "licenseKey": null - } - } - } -} -` diff --git a/cmd/newrelic/main.go b/cmd/newrelic/main.go index 8870781b1..051e1fd17 100644 --- a/cmd/newrelic/main.go +++ b/cmd/newrelic/main.go @@ -12,7 +12,8 @@ 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" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" + configCmd "github.com/newrelic/newrelic-cli/internal/config/command" "github.com/newrelic/newrelic-cli/internal/decode" diagnose "github.com/newrelic/newrelic-cli/internal/diagnose" "github.com/newrelic/newrelic-cli/internal/edge" @@ -22,6 +23,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/profile" "github.com/newrelic/newrelic-cli/internal/reporting" "github.com/newrelic/newrelic-cli/internal/utils" "github.com/newrelic/newrelic-cli/internal/workload" @@ -37,18 +39,17 @@ func init() { Command.AddCommand(agent.Command) Command.AddCommand(apiaccess.Command) Command.AddCommand(apm.Command) - Command.AddCommand(config.Command) - Command.AddCommand(credentials.Command) + Command.AddCommand(configCmd.Command) Command.AddCommand(decode.Command) Command.AddCommand(diagnose.Command) Command.AddCommand(edge.Command) Command.AddCommand(entities.Command) Command.AddCommand(events.Command) Command.AddCommand(install.Command) - Command.AddCommand(install.TestCommand) Command.AddCommand(nerdgraph.Command) Command.AddCommand(nerdstorage.Command) Command.AddCommand(nrql.Command) + Command.AddCommand(profile.Command) Command.AddCommand(reporting.Command) Command.AddCommand(utils.Command) Command.AddCommand(workload.Command) @@ -69,18 +70,16 @@ 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 - } + if configAPI.GetConfigTernary(config.PreReleaseFeatures).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 37c095ba1..59f11a5a2 100644 --- a/go.mod +++ b/go.mod @@ -9,18 +9,18 @@ require ( github.com/client9/misspell v0.3.4 github.com/fatih/color v1.12.0 github.com/git-chglog/git-chglog v0.14.2 + github.com/go-openapi/strfmt v0.20.1 // indirect github.com/go-task/task/v3 v3.4.3 github.com/golangci/golangci-lint v1.39.0 github.com/google/uuid v1.2.0 github.com/goreleaser/goreleaser v0.157.0 github.com/hokaccha/go-prettyjson v0.0.0-20210113012101-fb4e108d2519 - github.com/imdario/mergo v0.3.12 github.com/itchyny/gojq v0.12.4 + github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/jedib0t/go-pretty/v6 v6.2.2 github.com/joshdk/go-junit v0.0.0-20210226021600-6145f504ca0d github.com/llorllale/go-gitlint v0.0.0-20210608233938-d6303cc52cc5 github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/mapstructure v1.4.1 github.com/newrelic/newrelic-client-go v0.60.2 github.com/newrelic/tutone v0.6.1 github.com/pkg/errors v0.9.1 @@ -29,9 +29,9 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.7.0 github.com/tidwall/gjson v1.8.0 + github.com/tidwall/sjson v1.1.7 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 golang.org/x/term v0.0.0-20210503060354-a79de5458b56 golang.org/x/tools v0.1.0 diff --git a/go.sum b/go.sum index a9ddff181..262ebaf05 100644 --- a/go.sum +++ b/go.sum @@ -161,6 +161,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/ashanbrown/forbidigo v1.0.0/go.mod h1:PH+zMRWE15yW69fYfe7Kn8nYR6yYyafc3ntEGh2BBAg= github.com/ashanbrown/forbidigo v1.1.0 h1:SJOPJyqsrVL3CvR0veFZFmIM0fXS/Kvyikqvfphd0Z4= github.com/ashanbrown/forbidigo v1.1.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBFg8t0sG2FIxmI= @@ -171,6 +173,7 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.36.30 h1:hAwyfe7eZa7sM+S5mIJZFiNFwJMia9Whz6CYblioLoU= github.com/aws/aws-sdk-go v1.36.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= @@ -316,6 +319,10 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-openapi/errors v0.19.8 h1:doM+tQdZbUm9gydV9yR+iQNmztbjj7I3sW4sIcAwIzc= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/strfmt v0.20.1 h1:1VgxvehFne1mbChGeCmZ5pc0LxUf6yaACVSIYAR91Xc= +github.com/go-openapi/strfmt v0.20.1/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -323,6 +330,7 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -348,6 +356,30 @@ github.com/go-toolsmith/typep v1.0.2 h1:8xdsa1+FSIH/RhEkgnD1j2CJOy5mNllW1Q9tRiYw github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -390,6 +422,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= @@ -582,6 +615,8 @@ github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4 github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/jedib0t/go-pretty/v6 v6.2.2 h1:o3McN0rQ4X+IU+HduppSp9TwRdGLRW2rhJXy9CJaCRw= github.com/jedib0t/go-pretty/v6 v6.2.2/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -626,6 +661,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julz/importas v0.0.0-20210228071311-d0bf5cb4e1db h1:ZmwBthGFMVAieuVpLzuedUH9l4pY/0iFG16DN9dS38o= github.com/julz/importas v0.0.0-20210228071311-d0bf5cb4e1db/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= @@ -638,6 +675,7 @@ github.com/kisielk/errcheck v1.6.0 h1:YTDO4pNy7AUN/021p+JGHycQyYNIyMoenM1YDVK6Rl github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -683,6 +721,8 @@ github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQS github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/maratori/testpackage v1.0.1 h1:QtJ5ZjqapShm0w5DosRjg0PRlSdAdlx+W6cCKoALdbQ= github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/matoous/godox v0.0.0-20200801072554-4fb83dc2941e/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 h1:pWxk9e//NbPwfxat7RXkts09K+dEBJWakUWwICVqYbA= @@ -755,6 +795,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/moricho/tparallel v0.2.1 h1:95FytivzT6rYzdJLdtfn6m1bfFJylOJK41+lgv/EHf4= github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1/go.mod h1:FIczTrinKo8VaLxe6PWTPEXRXDIHz2QAwiaBaP5/4a8= @@ -782,6 +823,7 @@ github.com/nishanths/predeclared v0.2.1/go.mod h1:HvkGJcA3naj4lOwnFXFDkFxVtSqQMB github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -807,6 +849,7 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -869,6 +912,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -908,6 +953,7 @@ github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOms github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -981,8 +1027,11 @@ github.com/tidwall/gjson v1.8.0 h1:Qt+orfosKn0rbNTZqHYDqBrmm3UDA4KRkv70fDzG+PQ= github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8= github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/sjson v1.1.7 h1:sgVPwu/yygHJ2m1pJDLgGM/h+1F5odx5Q9ljG3imRm8= +github.com/tidwall/sjson v1.1.7/go.mod h1:w/yG+ezBeTdUxiKs5NcPicO9diP38nk96QBAbIIGeFs= github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94 h1:ig99OeTyDwQWhPe2iw9lwfQVF1KB3Q4fpP3X7/2VBG8= github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= @@ -1042,10 +1091,14 @@ github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnW github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= @@ -1057,6 +1110,8 @@ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6yfWfDmlWd35khcWpUa4L0xI/k= +go.mongodb.org/mongo-driver v1.5.1 h1:9nOVLGDfOaZ9R0tBumx/BcuqkbFpyTCU2r/Po7A2azI= +go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= go.mozilla.org/mozlog v0.0.0-20170222151521-4bb13139d403/go.mod h1:jHoPAGnDrCy6kaI2tAze5Prf0Nr0w/oNkROt2lw3n3o= go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -1084,6 +1139,7 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1220,11 +1276,14 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1310,10 +1369,14 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= diff --git a/internal/apiaccess/command.go b/internal/apiaccess/command.go index 17c9f94ea..604a135cc 100644 --- a/internal/apiaccess/command.go +++ b/internal/apiaccess/command.go @@ -8,7 +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/apiaccess" ) @@ -28,14 +27,12 @@ var cmdKey = &cobra.Command{ Short: "Fetch a single key by ID and type.\n\n---\n**NR Internal** | [#help-unified-api](https://newrelic.slack.com/archives/CBHJRSPSA) | visibility(customer)\n\n", Long: "", Example: "newrelic apiAccess apiAccessGetKey --id --keyType", + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { + resp, err := client.NRClient.APIAccess.GetAPIAccessKeyWithContext(utils.SignalCtx, apiAccessGetKeyid, apiaccess.APIAccessKeyType(apiAccessGetKeykeyType)) + utils.LogIfFatal(err) - resp, err := nrClient.APIAccess.GetAPIAccessKeyWithContext(utils.SignalCtx, apiAccessGetKeyid, apiaccess.APIAccessKeyType(apiAccessGetKeykeyType)) - utils.LogIfFatal(err) - - utils.LogIfFatal(output.Print(resp)) - }) + utils.LogIfFatal(output.Print(resp)) }, } var apiAccessCreateKeysInput string @@ -45,19 +42,17 @@ var cmdAPIAccessCreateKeys = &cobra.Command{ Short: "Create keys. You can create keys for multiple accounts at once. 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).", Long: "", Example: "newrelic apiAccess apiAccessCreateKeys --keys", + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - - var keys apiaccess.APIAccessCreateInput + var keys apiaccess.APIAccessCreateInput - err := json.Unmarshal([]byte(apiAccessCreateKeysInput), &keys) - utils.LogIfFatal(err) + err := json.Unmarshal([]byte(apiAccessCreateKeysInput), &keys) + utils.LogIfFatal(err) - resp, err := nrClient.APIAccess.CreateAPIAccessKeysWithContext(utils.SignalCtx, keys) - utils.LogIfFatal(err) + resp, err := client.NRClient.APIAccess.CreateAPIAccessKeysWithContext(utils.SignalCtx, keys) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(resp)) - }) + utils.LogIfFatal(output.Print(resp)) }, } var apiAccessUpdateKeysInput string @@ -67,19 +62,17 @@ var cmdAPIAccessUpdateKeys = &cobra.Command{ Short: "Update keys. You can update keys for multiple accounts at once. 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).", Long: "", Example: "newrelic apiAccess apiAccessUpdateKeys --keys", + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { + var keys apiaccess.APIAccessUpdateInput - var keys apiaccess.APIAccessUpdateInput + err := json.Unmarshal([]byte(apiAccessUpdateKeysInput), &keys) + utils.LogIfFatal(err) - err := json.Unmarshal([]byte(apiAccessUpdateKeysInput), &keys) - utils.LogIfFatal(err) + resp, err := client.NRClient.APIAccess.UpdateAPIAccessKeysWithContext(utils.SignalCtx, keys) + utils.LogIfFatal(err) - resp, err := nrClient.APIAccess.UpdateAPIAccessKeysWithContext(utils.SignalCtx, keys) - utils.LogIfFatal(err) - - utils.LogIfFatal(output.Print(resp)) - }) + utils.LogIfFatal(output.Print(resp)) }, } var apiAccessDeleteKeysInput string @@ -89,19 +82,17 @@ var cmdAPIAccessDeleteKeys = &cobra.Command{ Short: "A mutation to delete keys.", Long: "", Example: "newrelic apiAccess apiAccessDeleteKeys --keys", + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - - var keys apiaccess.APIAccessDeleteInput + var keys apiaccess.APIAccessDeleteInput - err := json.Unmarshal([]byte(apiAccessDeleteKeysInput), &keys) - utils.LogIfFatal(err) + err := json.Unmarshal([]byte(apiAccessDeleteKeysInput), &keys) + utils.LogIfFatal(err) - resp, err := nrClient.APIAccess.DeleteAPIAccessKeyWithContext(utils.SignalCtx, keys) - utils.LogIfFatal(err) + resp, err := client.NRClient.APIAccess.DeleteAPIAccessKeyWithContext(utils.SignalCtx, keys) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(resp)) - }) + utils.LogIfFatal(output.Print(resp)) }, } diff --git a/internal/apm/command.go b/internal/apm/command.go index 814ba9f44..e2b8f9b6e 100644 --- a/internal/apm/command.go +++ b/internal/apm/command.go @@ -5,8 +5,7 @@ import ( ) var ( - apmAccountID string - apmAppID int + apmAppID int ) // Command represents the apm command @@ -17,6 +16,5 @@ var Command = &cobra.Command{ func init() { // Flags for all things APM - Command.PersistentFlags().StringVarP(&apmAccountID, "accountId", "a", "", "A New Relic account ID") Command.PersistentFlags().IntVarP(&apmAppID, "applicationId", "", 0, "A New Relic APM application ID") } diff --git a/internal/apm/command_application.go b/internal/apm/command_application.go index c32873f61..03eb81e0b 100644 --- a/internal/apm/command_application.go +++ b/internal/apm/command_application.go @@ -1,13 +1,15 @@ package apm import ( + "strconv" + 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" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "github.com/newrelic/newrelic-cli/internal/output" "github.com/newrelic/newrelic-cli/internal/utils" ) @@ -33,54 +35,54 @@ 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: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { + accountID := configAPI.GetActiveProfileAccountID() - if appGUID == "" && appName == "" && apmAccountID == "" { + if appGUID == "" && appName == "" && accountID == 0 { utils.LogIfError(cmd.Help()) 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 + var err error + + // Look for just the GUID if passed in + if appGUID != "" { + if appName != "" || accountID != 0 { + log.Warnf("Searching for --guid only, ignoring --accountId and --name") + } + + var singleResult *entities.EntityInterface + singleResult, err = client.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 accountID != 0 { + params.Tags = []entities.EntitySearchQueryBuilderTag{{Key: "accountId", Value: strconv.Itoa(accountID)}} } - utils.LogIfFatal(output.Print(entityResults)) - }) + results, err := client.NRClient.Entities.GetEntitySearch( + entities.EntitySearchOptions{}, + "", + params, + []entities.EntitySearchSortCriteria{}, + ) + + entityResults = results.Results.Entities + utils.LogIfFatal(err) + } + + utils.LogIfFatal(output.Print(entityResults)) }, } @@ -93,21 +95,20 @@ var cmdAppGet = &cobra.Command{ The get command performs a query for an APM application by GUID. `, Example: "newrelic apm application get --guid ", + PreRun: client.RequireClient, 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 != "" { + results, err = client.NRClient.Entities.GetEntity(entities.EntityGUID(appGUID)) + utils.LogIfFatal(err) + } else { + utils.LogIfError(cmd.Help()) + log.Fatal(" --guid is required") + } - utils.LogIfFatal(output.Print(results)) - }) + utils.LogIfFatal(output.Print(results)) }, } diff --git a/internal/apm/command_deployment.go b/internal/apm/command_deployment.go index a08801ce9..5d7c7baf9 100644 --- a/internal/apm/command_deployment.go +++ b/internal/apm/command_deployment.go @@ -4,7 +4,6 @@ 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" @@ -37,18 +36,17 @@ var cmdDeploymentList = &cobra.Command{ The list command returns deployments for a New Relic APM application. `, Example: "newrelic apm deployment list --applicationId ", + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { if apmAppID == 0 { utils.LogIfError(cmd.Help()) log.Fatal("--applicationId is required") } - client.WithClient(func(nrClient *newrelic.NewRelic) { - deployments, err := nrClient.APM.ListDeploymentsWithContext(utils.SignalCtx, apmAppID) - utils.LogIfFatal(err) + deployments, err := client.NRClient.APM.ListDeploymentsWithContext(utils.SignalCtx, apmAppID) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(deployments)) - }) + utils.LogIfFatal(output.Print(deployments)) }, } @@ -61,18 +59,17 @@ The create command creates a new deployment marker for a New Relic APM application. `, Example: "newrelic apm deployment create --applicationId --revision ", + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { if apmAppID == 0 { utils.LogIfError(cmd.Help()) log.Fatal("--applicationId and --revision are required") } - client.WithClient(func(nrClient *newrelic.NewRelic) { - d, err := nrClient.APM.CreateDeploymentWithContext(utils.SignalCtx, apmAppID, deployment) - utils.LogIfFatal(err) + d, err := client.NRClient.APM.CreateDeploymentWithContext(utils.SignalCtx, apmAppID, deployment) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(d)) - }) + utils.LogIfFatal(output.Print(d)) }, } @@ -84,18 +81,17 @@ var cmdDeploymentDelete = &cobra.Command{ The delete command performs a delete operation for an APM deployment. `, Example: "newrelic apm deployment delete --applicationId --deploymentID ", + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { if apmAppID == 0 { utils.LogIfError(cmd.Help()) 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.NRClient.APM.DeleteDeployment(apmAppID, deployment.ID) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(d)) - }) + utils.LogIfFatal(output.Print(d)) }, } diff --git a/internal/client/client.go b/internal/client/client.go index a1ad88a1a..c2769a4d3 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1,64 +1,131 @@ package client import ( + "context" "errors" "fmt" - "os" - "github.com/newrelic/newrelic-client-go/newrelic" + "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/config" - "github.com/newrelic/newrelic-cli/internal/credentials" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" + "github.com/newrelic/newrelic-cli/internal/install/types" + "github.com/newrelic/newrelic-cli/internal/utils" + "github.com/newrelic/newrelic-client-go/newrelic" + "github.com/newrelic/newrelic-client-go/pkg/apiaccess" + + log "github.com/sirupsen/logrus" ) var ( + NRClient *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 - } +// NewClient initializes the New Relic client. +func NewClient(profileName string) (*newrelic.NewRelic, error) { + apiKey := configAPI.GetProfileString(profileName, config.APIKey) + insightsInsertKey := configAPI.GetProfileString(profileName, config.InsightsInsertKey) - 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") + if apiKey == "" && 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 := configAPI.GetProfileString(profileName, config.Region) + logLevel := configAPI.GetLogLevel() userAgent := fmt.Sprintf("newrelic-cli/%s (https://github.com/newrelic/newrelic-cli)", version) cfgOpts := []newrelic.ConfigOption{ newrelic.ConfigPersonalAPIKey(apiKey), newrelic.ConfigInsightsInsertKey(insightsInsertKey), - newrelic.ConfigLogLevel(cfg.LogLevel), - newrelic.ConfigRegion(regionValue), + newrelic.ConfigLogLevel(logLevel), + newrelic.ConfigRegion(region), newrelic.ConfigUserAgent(userAgent), newrelic.ConfigServiceName(serviceName), } - nerdGraphURLOverride := os.Getenv("NEW_RELIC_NERDGRAPH_URL") - if nerdGraphURLOverride != "" { - cfgOpts = append(cfgOpts, newrelic.ConfigNerdGraphBaseURL(nerdGraphURLOverride)) + nrClient, err := newrelic.New(cfgOpts...) + if err != nil { + return nil, fmt.Errorf("unable to create New Relic client with error: %s", err) } - nrClient, err := newrelic.New(cfgOpts...) + return nrClient, nil +} + +func RequireClient(cmd *cobra.Command, args []string) { + if NRClient == nil { + log.Fatalf("could not initialize New Relic client, make sure your profile is configured with `newrelic profile configure`") + } +} + +func FetchLicenseKey(accountID int, profileName string) (string, error) { + client, err := NewClient(profileName) + if err != nil { + return "", err + } + + var key string + retryFunc := func() error { + key, err = execLicenseKeyRequest(utils.SignalCtx, client, accountID) + if err != nil { + return err + } + + return nil + } + + r := utils.NewRetry(3, 1, retryFunc) + if err := r.ExecWithRetries(utils.SignalCtx); err != nil { + return "", err + } + + return key, nil +} + +func execLicenseKeyRequest(ctx context.Context, client *newrelic.NewRelic, accountID int) (string, error) { + params := apiaccess.APIAccessKeySearchQuery{ + Scope: apiaccess.APIAccessKeySearchScope{ + AccountIDs: []int{accountID}, + IngestTypes: []apiaccess.APIAccessIngestKeyType{apiaccess.APIAccessIngestKeyTypeTypes.LICENSE}, + }, + Types: []apiaccess.APIAccessKeyType{apiaccess.APIAccessKeyTypeTypes.INGEST}, + } + + keys, err := client.APIAccess.SearchAPIAccessKeysWithContext(ctx, params) + if err != nil { + return "", err + } + + if len(keys) > 0 { + return keys[0].Key, nil + } + + return "", types.ErrorFetchingLicenseKey +} + +func FetchInsightsInsertKey(accountID int, profileName string) (string, error) { + client, err := NewClient(profileName) + if err != nil { + return "", err + } + + // Check for an existing key first + keys, err := client.APIAccess.ListInsightsInsertKeys(accountID) + if err != nil { + return "", types.ErrorFetchingInsightsInsertKey + } + + // We already have a key, return it + if len(keys) > 0 { + return keys[0].Key, nil + } + // Create a new key if one doesn't exist + key, err := client.APIAccess.CreateInsightsInsertKey(accountID) if err != nil { - return nil, nil, fmt.Errorf("unable to create New Relic client with error: %s", err) + return "", types.ErrorFetchingInsightsInsertKey } - return nrClient, defProfile, nil + return key.Key, 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/api/api.go b/internal/config/api/api.go new file mode 100644 index 000000000..7b40974cc --- /dev/null +++ b/internal/config/api/api.go @@ -0,0 +1,273 @@ +package api + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + log "github.com/sirupsen/logrus" + + "github.com/newrelic/newrelic-cli/internal/config" + "github.com/newrelic/newrelic-cli/internal/utils" +) + +// GetActiveProfileName retrieves the profile in use for this command execution. +// To retrieve the active profile, the following criteria are evaluated in order, +// short circuiting and returning the described value if true: +// 1. a profile has been provided with the global `--profile` flag +// 2. a profile is set in the default profile config file +// 3. "default" is returned if none of the above are true +func GetActiveProfileName() string { + if config.FlagProfileName != "" { + return config.FlagProfileName + } + + profileName, err := GetDefaultProfileName() + if err != nil || profileName == "" { + return config.DefaultProfileName + } + + return profileName +} + +// GetDefaultProfileName retrieves the profile set in the default profile config +// file. If the file does not exist, an empty string will be returned. +func GetDefaultProfileName() (string, error) { + defaultProfileFilePath := filepath.Join(config.BasePath, config.DefaultProfileFileName) + data, err := ioutil.ReadFile(defaultProfileFilePath) + if err != nil { + if os.IsNotExist(err) { + return "", nil + } + + return "", err + } + + return strings.Trim(string(data), "\""), nil +} + +// GetProfileNames retrieves all profile names currently configured in the credentials file. +func GetProfileNames() []string { + return config.CredentialsProvider.GetScopes() +} + +// SetDefaultProfile sets the given profile as the new default in the default profile +// config file. If the given profile does not exist, the set operation will return +// an error. +func SetDefaultProfile(profileName string) error { + if ok := utils.StringInSlice(profileName, GetProfileNames()); !ok { + return fmt.Errorf("profile %s does not exist", profileName) + } + + defaultProfileFilePath := filepath.Join(config.BasePath, config.DefaultProfileFileName) + return ioutil.WriteFile(defaultProfileFilePath, []byte("\""+profileName+"\""), 0644) +} + +// RemoveProfile removes a profile from the credentials file. If the profile being +// removed is the default, it will attempt to find another profile to set as the +// new default. If another profile cannot be found, the default profile config file +// will be deleted. +func RemoveProfile(profileName string) error { + if err := config.CredentialsProvider.RemoveScope(profileName); err != nil { + return err + } + + d, err := GetDefaultProfileName() + if err != nil { + return err + } + + if d == profileName { + names := GetProfileNames() + if len(names) > 0 { + if err = SetDefaultProfile(names[0]); err != nil { + return fmt.Errorf("could not set new default profile") + } + + log.Infof("setting %s as the new default profile", names[0]) + } else { + if err := removeDefaultProfile(); err != nil { + return fmt.Errorf("could not delete default profile") + } + } + } + + return nil +} + +// RequireActiveProfileAccountID retrieves the currently configured account ID, +// returning an error if the value retrieved is the zero value. +// When returning an account ID, the following will be evaluated in order, short-circuiting +// and returning the described value if true: +// 1. An environment variable override has been set with NEW_RELIC_ACCOUNT_ID +// 2. An account ID has been provided with the `--accountId` global flag +// 3. An account ID has been set in the active profile +func RequireActiveProfileAccountID() int { + v := GetActiveProfileAccountID() + if v == 0 { + log.Fatalf("%s is required", config.AccountID) + } + + return v +} + +// GetActiveProfileAccountID retrieves the currently configured account ID. +// When returning an account ID, the following will be evaluated in order, short-circuiting +// and returning the described value if true: +// 1. An environment variable override has been set with NEW_RELIC_ACCOUNT_ID +// 2. An account ID has been provided with the `--accountId` global flag +// 3. An account ID has been set in the active profile +// 4. The zero value will be returned if none of the above are true +func GetActiveProfileAccountID() int { + return getActiveProfileIntWithOverride(config.AccountID, config.FlagAccountID) +} + +// GetActiveProfileString retrieves the value set for the given key in the active +// profile, if any. Environment variable overrides will be preferred over values +// set in the active profile, and a default value will be returned if it has been +// configured and no value has been set for the key in the active profile. +// An attempt will be made to convert the underlying value to a string if is not +// already stored that way. Failing the above, the zero value wil be returned. +func GetActiveProfileString(key config.FieldKey) string { + return GetProfileString(GetActiveProfileName(), key) +} + +// GetProfileString retrieves the value set for the given key and profile, if any. +// Environment variable overrides will be preferred over values set in the given +// profile, and a default value will be returned if it has been configured and no +// value has been set for the key in the given profile. +// An attempt will be made to convert the underlying value to a string if is not +// already stored that way. Failing the above, the zero value wil be returned. +func GetProfileString(profileName string, key config.FieldKey) string { + v, err := config.CredentialsProvider.GetStringWithScope(profileName, key) + if err != nil { + return "" + } + + return v +} + +// GetProfileInt retrieves the value set for the given key and profile, if any. +// Environment variable overrides will be preferred over values set in the given +// profile, and a default value will be returned if it has been configured and no +// value has been set for the key in the given profile. +// An attempt will be made to convert the underlying value to an int if is not +// already stored that way. Failing the above, the zero value wil be returned. +func GetProfileInt(profileName string, key config.FieldKey) int { + v, err := config.CredentialsProvider.GetIntWithScope(profileName, key) + if err != nil { + return 0 + } + + return int(v) +} + +// SetProfileValue sets a value for the given key and profile. +func SetProfileValue(profileName string, key config.FieldKey, value interface{}) error { + return config.CredentialsProvider.SetWithScope(profileName, key, value) +} + +// GetLogLevel retrieves the currently configured log level. +// When returning a log level, the following will be evaluated in order, short-circuiting +// and returning the described value if true: +// 1. An environment variable override has been set with NEW_RELIC_CLI_LOG_LEVEL +// 2. A log level has been provided with the `--trace` global flag +// 2. A log level has been provided with the `--debug` global flag +// 3. A log level has been set in the config file +// 4. If none of the above is true, the default log level will be returned. +func GetLogLevel() string { + if config.FlagDebug { + return "debug" + } + + if config.FlagTrace { + return "trace" + } + + return GetConfigString(config.LogLevel) +} + +// GetConfigString retrieves the config value set for the given key, if any. +// Environment variable overrides will be preferred over values set in the given +// profile, and a default value will be returned if it has been configured and no +// value has been set for the key in the config file. +// An attempt will be made to convert the underlying value to a string if is not +// already stored that way. Failing the above, the zero value wil be returned. +func GetConfigString(key config.FieldKey) string { + v, err := config.ConfigStore.GetString(key) + if err != nil { + return "" + } + + return v +} + +// GetConfigTernary retrieves the config value set for the given key, if any. +// Environment variable overrides will be preferred over values set in the given +// profile, and a default value will be returned if it has been configured and no +// value has been set for the key in the config file. +// An attempt will be made to convert the underlying value to a Ternary if is not +// already stored that way. Failing the above, the zero value wil be returned. +func GetConfigTernary(key config.FieldKey) config.Ternary { + v, err := config.ConfigStore.GetTernary(key) + if err != nil { + return config.Ternary("") + } + + return v +} + +// SetConfigValue sets a config value for the given key. +func SetConfigValue(key config.FieldKey, value interface{}) error { + return config.ConfigStore.Set(key, value) +} + +// DeleteConfigValue deletes a config value for the given key. +func DeleteConfigValue(key config.FieldKey) error { + return config.ConfigStore.DeleteKey(key) +} + +// GetConfigFieldDefinition retrieves the field definition for the given config key. +func GetConfigFieldDefinition(key config.FieldKey) *config.FieldDefinition { + return config.ConfigStore.GetFieldDefinition(key) +} + +// ForEachProfileFieldDefinition iterates the field definitions for the profile fields. +func ForEachProfileFieldDefinition(profileName string, fn func(d config.FieldDefinition)) { + config.CredentialsProvider.ForEachFieldDefinition(fn) +} + +// ForEachConfigFieldDefinition iterates the field definitions for the config fields. +func ForEachConfigFieldDefinition(fn func(d config.FieldDefinition)) { + config.ConfigStore.ForEachFieldDefinition(fn) +} + +// GetValidConfigFieldKeys returns all the config field keys that can be set. +func GetValidConfigFieldKeys() (fieldKeys []config.FieldKey) { + config.ConfigStore.ForEachFieldDefinition(func(fd config.FieldDefinition) { + fieldKeys = append(fieldKeys, fd.Key) + }) + + return fieldKeys +} + +func getActiveProfileIntWithOverride(key config.FieldKey, override int) int { + return getProfileIntWithOverride(GetActiveProfileName(), key, override) +} + +func getProfileIntWithOverride(profileName string, key config.FieldKey, override int) int { + o := int64(override) + v, err := config.CredentialsProvider.GetIntWithScopeAndOverride(profileName, key, &o) + if err != nil { + return 0 + } + + return int(v) +} + +func removeDefaultProfile() error { + defaultProfileFilePath := filepath.Join(config.BasePath, config.DefaultProfileFileName) + return os.Remove(defaultProfileFilePath) +} diff --git a/internal/config/api/api_integration_test.go b/internal/config/api/api_integration_test.go new file mode 100644 index 000000000..975734d44 --- /dev/null +++ b/internal/config/api/api_integration_test.go @@ -0,0 +1,95 @@ +// +build integration + +package api + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/newrelic/newrelic-cli/internal/config" +) + +func TestConfigSetLogLevel(t *testing.T) { + f, err := ioutil.TempDir("/tmp", "newrelic") + assert.NoError(t, err) + defer os.RemoveAll(f) + + config.Init(f) + + // Set the valid log levels + for _, l := range []string{ + "ERROR", + "WARN", + "INFO", + "DEBUG", + "TRACE", + } { + err = SetConfigValue("logLevel", l) + assert.NoError(t, err) + + actual := GetConfigString(config.LogLevel) + assert.Equal(t, l, actual) + } + + err = SetConfigValue("logLevel", "INVALID_VALUE") + assert.Error(t, err) +} + +func TestConfigSetSendUsageData(t *testing.T) { + f, err := ioutil.TempDir("/tmp", "newrelic") + assert.NoError(t, err) + defer os.RemoveAll(f) + + config.Init(f) + + // Set the valid sendUsageData values + for _, l := range []config.Ternary{ + config.TernaryValues.Allow, + config.TernaryValues.Disallow, + config.TernaryValues.Unknown, + } { + err = SetConfigValue("sendUsageData", l) + assert.NoError(t, err) + assert.Equal(t, GetConfigTernary(config.SendUsageData), l) + } + + err = SetConfigValue("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) + + config.Init(f) + + // Set the valid pre-release feature values + for _, l := range []config.Ternary{ + config.TernaryValues.Allow, + config.TernaryValues.Disallow, + config.TernaryValues.Unknown, + } { + err = SetConfigValue("preReleaseFeatures", l) + assert.NoError(t, err) + assert.Equal(t, GetConfigTernary(config.PreReleaseFeatures), l) + } + + err = SetConfigValue("preReleaseFeatures", "INVALID_VALUE") + assert.Error(t, err) +} + +func TestConfigSetPluginDir(t *testing.T) { + f, err := ioutil.TempDir("/tmp", "newrelic") + assert.NoError(t, err) + defer os.RemoveAll(f) + + config.Init(f) + + err = SetConfigValue(config.PluginDir, "test") + assert.NoError(t, err) + assert.Equal(t, "test", GetConfigString(config.PluginDir)) +} diff --git a/internal/config/api/api_test.go b/internal/config/api/api_test.go new file mode 100644 index 000000000..878ca75d5 --- /dev/null +++ b/internal/config/api/api_test.go @@ -0,0 +1,178 @@ +// +build integration + +package api + +import ( + "io/ioutil" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/newrelic/newrelic-cli/internal/config" +) + +var ( + testConfig = `{ + "*":{ + "loglevel":"debug", + "plugindir": ".newrelic/plugins", + "prereleasefeatures": "NOT_ASKED", + "sendusagedata": "NOT_ASKED", + } + }` + + testCredentials = `{ + "default": { + "apiKey": "testApiKey", + "insightsInsertKey": "testInsightsInsertKey", + "region": "testRegion", + "accountID": 12345, + "licenseKey": "testLicenseKey" + }, + }` +) + +func TestGetActiveProfileValues(t *testing.T) { + dir, err := ioutil.TempDir("", "newrelic-cli.config_test.*") + require.NoError(t, err) + defer os.RemoveAll(dir) + + config.Init(dir) + + err = ioutil.WriteFile(filepath.Join(dir, config.CredentialsFileName), []byte(testCredentials), 0644) + require.NoError(t, err) + + os.Unsetenv("NEW_RELIC_API_KEY") + os.Unsetenv("NEW_RELIC_LICENSE_KEY") + os.Unsetenv("NEW_RELIC_INSIGHTS_INSERT_KEY") + os.Unsetenv("NEW_RELIC_REGION") + os.Unsetenv("NEW_RELIC_ACCOUNT_ID") + + config.InitializeCredentialsStore() + + require.Equal(t, "testApiKey", GetActiveProfileString("apiKey")) + require.Equal(t, "testInsightsInsertKey", GetActiveProfileString("insightsInsertKey")) + require.Equal(t, "testRegion", GetActiveProfileString("region")) + require.Equal(t, "testLicenseKey", GetActiveProfileString("licenseKey")) + require.Equal(t, 12345, GetActiveProfileAccountID()) +} + +func TestGetActiveProfileValues_EnvVarOverride(t *testing.T) { + dir, err := ioutil.TempDir("", "newrelic-cli.config_test.*") + require.NoError(t, err) + defer os.RemoveAll(dir) + + config.Init(dir) + + err = ioutil.WriteFile(filepath.Join(dir, config.CredentialsFileName), []byte(testCredentials), 0644) + require.NoError(t, err) + + os.Setenv("NEW_RELIC_API_KEY", "apiKeyOverride") + os.Setenv("NEW_RELIC_LICENSE_KEY", "licenseKeyOverride") + os.Setenv("NEW_RELIC_INSIGHTS_INSERT_KEY", "insightsInsertKeyOverride") + os.Setenv("NEW_RELIC_REGION", "regionOverride") + os.Setenv("NEW_RELIC_ACCOUNT_ID", "67890") + + config.InitializeCredentialsStore() + + require.Equal(t, "apiKeyOverride", GetActiveProfileString("apiKey")) + require.Equal(t, "insightsInsertKeyOverride", GetActiveProfileString("insightsInsertKey")) + require.Equal(t, "regionOverride", GetActiveProfileString("region")) + require.Equal(t, "licenseKeyOverride", GetActiveProfileString("licenseKey")) + require.Equal(t, 67890, GetActiveProfileAccountID()) +} + +func TestGetConfigValues(t *testing.T) { + dir, err := ioutil.TempDir("", "newrelic-cli.config_test.*") + require.NoError(t, err) + defer os.RemoveAll(dir) + + config.Init(dir) + + err = ioutil.WriteFile(filepath.Join(dir, config.ConfigFileName), []byte(testConfig), 0644) + require.NoError(t, err) + + os.Unsetenv("NEW_RELIC_CLI_LOG_LEVEL") + os.Unsetenv("NEW_RELIC_CLI_PLUGIN_DIR") + os.Unsetenv("NEW_RELIC_CLI_PRERELEASEFEATURES") + os.Unsetenv("NEW_RELIC_CLI_SENDUSAGEDATA") + + config.InitializeConfigStore() + + require.Equal(t, "debug", GetConfigString("loglevel")) + require.Equal(t, ".newrelic/plugins", GetConfigString("plugindir")) + require.Equal(t, config.TernaryValues.Unknown, GetConfigTernary("prereleasefeatures")) + require.Equal(t, config.TernaryValues.Unknown, GetConfigTernary("sendusagedata")) +} + +func TestGetConfigValues_EnvVarOverride(t *testing.T) { + dir, err := ioutil.TempDir("", "newrelic-cli.config_test.*") + require.NoError(t, err) + defer os.RemoveAll(dir) + + config.Init(dir) + + err = ioutil.WriteFile(filepath.Join(dir, config.CredentialsFileName), []byte(testConfig), 0644) + require.NoError(t, err) + + os.Setenv("NEW_RELIC_CLI_LOG_LEVEL", "trace") + os.Setenv("NEW_RELIC_CLI_PLUGIN_DIR", "/tmp") + os.Setenv("NEW_RELIC_CLI_PRERELEASEFEATURES", "ALLOW") + os.Setenv("NEW_RELIC_CLI_SENDUSAGEDATA", "ALLOW") + + config.InitializeConfigStore() + + require.Equal(t, "trace", GetConfigString("loglevel")) + require.Equal(t, "/tmp", GetConfigString("plugindir")) + require.Equal(t, config.TernaryValues.Allow, GetConfigTernary("prereleasefeatures")) + require.Equal(t, config.TernaryValues.Allow, GetConfigTernary("sendusagedata")) +} + +func TestGetConfigValues_DefaultValues(t *testing.T) { + dir, err := ioutil.TempDir("", "newrelic-cli.config_test.*") + require.NoError(t, err) + defer os.RemoveAll(dir) + + config.Init(dir) + + os.Unsetenv("NEW_RELIC_CLI_LOG_LEVEL") + os.Unsetenv("NEW_RELIC_CLI_PLUGIN_DIR") + os.Unsetenv("NEW_RELIC_CLI_PRERELEASEFEATURES") + os.Unsetenv("NEW_RELIC_CLI_SENDUSAGEDATA") + + config.InitializeConfigStore() + + require.Equal(t, "info", GetConfigString("loglevel")) + require.Equal(t, filepath.Join(dir, config.DefaultPluginDir), GetConfigString("plugindir")) + require.Equal(t, config.TernaryValues.Unknown, GetConfigTernary("prereleasefeatures")) + require.Equal(t, config.TernaryValues.Unknown, GetConfigTernary("sendusagedata")) +} + +func TestRemoveProfile(t *testing.T) { + dir, err := ioutil.TempDir("", "newrelic-cli.config_test.*") + require.NoError(t, err) + defer os.RemoveAll(dir) + + config.Init(dir) + + filename := filepath.Join(dir, config.CredentialsFileName) + err = ioutil.WriteFile(filename, []byte(testCredentials), 0644) + require.NoError(t, err) + + os.Setenv("NEW_RELIC_API_KEY", "apiKeyOverride") + os.Setenv("NEW_RELIC_LICENSE_KEY", "licenseKeyOverride") + os.Setenv("NEW_RELIC_INSIGHTS_INSERT_KEY", "insightsInsertKeyOverride") + os.Setenv("NEW_RELIC_REGION", "regionOverride") + os.Setenv("NEW_RELIC_ACCOUNT_ID", "67890") + + config.InitializeCredentialsStore() + err = RemoveProfile("default") + require.NoError(t, err) + + data, err := ioutil.ReadFile(filename) + require.NoError(t, err) + require.Regexp(t, regexp.MustCompile(`{\s*}`), string(data)) +} diff --git a/internal/config/command.go b/internal/config/command.go deleted file mode 100644 index fe1204b97..000000000 --- a/internal/config/command.go +++ /dev/null @@ -1,104 +0,0 @@ -package config - -import ( - "github.com/spf13/cobra" - - "github.com/newrelic/newrelic-cli/internal/utils" -) - -var ( - // Display keys when printing output - key string - value string -) - -// Command is the base command for managing profiles -var Command = &cobra.Command{ - Use: "config", - Short: "Manage the configuration of the New Relic CLI", -} - -var cmdSet = &cobra.Command{ - Use: "set", - Short: "Set a configuration value", - Long: `Set a configuration value - -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)) - }) - }, -} - -var cmdGet = &cobra.Command{ - Use: "get", - Short: "Get a configuration value", - Long: `Get a configuration value - -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) - }) - }, -} - -var cmdList = &cobra.Command{ - Use: "list", - Short: "List the current configuration values", - Long: `List the current configuration values - -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() - }) - }, - Aliases: []string{ - "ls", - }, -} - -var cmdDelete = &cobra.Command{ - Use: "delete", - Short: "Delete a configuration value", - Long: `Delete a configuration value - -The delete command deletes a persistent configuration value for the New Relic CLI. -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)) - }) - }, - Aliases: []string{ - "rm", - }, -} - -func init() { - Command.AddCommand(cmdList) - - 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")) - - Command.AddCommand(cmdGet) - cmdGet.Flags().StringVarP(&key, "key", "k", "", "the key to get") - utils.LogIfError(cmdGet.MarkFlagRequired("key")) - - Command.AddCommand(cmdDelete) - cmdDelete.Flags().StringVarP(&key, "key", "k", "", "the key to delete") - utils.LogIfError(cmdDelete.MarkFlagRequired("key")) -} diff --git a/internal/config/command/command.go b/internal/config/command/command.go new file mode 100644 index 000000000..f3cd29edf --- /dev/null +++ b/internal/config/command/command.go @@ -0,0 +1,145 @@ +package command + +import ( + "strings" + + "github.com/spf13/cobra" + + log "github.com/sirupsen/logrus" + + "github.com/newrelic/newrelic-cli/internal/config" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" + "github.com/newrelic/newrelic-cli/internal/output" + "github.com/newrelic/newrelic-cli/internal/utils" +) + +var ( + // Display keys when printing output + key string + value string +) + +// Command is the base command for managing profiles +var Command = &cobra.Command{ + Use: "config", + Short: "Manage the configuration of the New Relic CLI", +} + +var cmdSet = &cobra.Command{ + Use: "set", + Short: "Set a configuration value", + Long: `Set a configuration value + +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) { + if !isValidFieldKey() { + log.Fatalf("%s is not a valid config field. valid values are %s", key, configAPI.GetValidConfigFieldKeys()) + } + + if err := configAPI.SetConfigValue(config.FieldKey(key), value); err != nil { + log.Fatal(err) + } + + log.Info("success") + }, +} + +func isValidFieldKey() (valid bool) { + configAPI.ForEachConfigFieldDefinition(func(fd config.FieldDefinition) { + if strings.EqualFold(string(fd.Key), key) { + valid = true + } + }) + + return valid +} + +var cmdGet = &cobra.Command{ + Use: "get", + Short: "Get a configuration value", + Long: `Get a configuration value + +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) { + if !isValidFieldKey() { + log.Fatalf("%s is not a valid config field. valid values are %s", key, configAPI.GetValidConfigFieldKeys()) + } + + output.Text(configAPI.GetConfigString(config.FieldKey(key))) + }, +} + +var cmdList = &cobra.Command{ + Use: "list", + Short: "List the current configuration values", + Long: `List the current configuration values + +The list command lists all persistent configuration values for the New Relic CLI. +`, + Example: "newrelic config list", + Run: func(cmd *cobra.Command, args []string) { + m := map[string]interface{}{} + + configAPI.ForEachConfigFieldDefinition(func(fd config.FieldDefinition) { + m[string(fd.Key)] = configAPI.GetConfigString(fd.Key) + }) + + output.Text(m) + }, + Aliases: []string{ + "ls", + }, +} + +var cmdReset = &cobra.Command{ + Use: "reset", + Short: "Reset a configuration value to its default", + Long: `Reset a configuration value + +The reset command resets a configuration value to its default. +`, + Example: "newrelic config reset --key ", + Run: func(cmd *cobra.Command, args []string) { + if !isValidFieldKey() { + log.Fatalf("%s is not a valid config field. valid values are %s", key, configAPI.GetValidConfigFieldKeys()) + } + + fd := configAPI.GetConfigFieldDefinition(config.FieldKey(key)) + + if fd.Default == nil { + log.Fatalf("key %s cannot be reset to a default value since no default exists", fd.Key) + } + + if err := configAPI.SetConfigValue(config.FieldKey(key), fd.Default); err != nil { + log.Fatal(err) + } + + log.Info("success") + }, + Aliases: []string{ + "rm", + "delete", + }, +} + +func init() { + Command.AddCommand(cmdList) + + 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")) + + Command.AddCommand(cmdGet) + cmdGet.Flags().StringVarP(&key, "key", "k", "", "the key to get") + utils.LogIfError(cmdGet.MarkFlagRequired("key")) + + Command.AddCommand(cmdReset) + cmdReset.Flags().StringVarP(&key, "key", "k", "", "the key to delete") + utils.LogIfError(cmdReset.MarkFlagRequired("key")) +} diff --git a/internal/config/command_test.go b/internal/config/command/command_test.go similarity index 77% rename from internal/config/command_test.go rename to internal/config/command/command_test.go index 8fe06290c..18b97f087 100644 --- a/internal/config/command_test.go +++ b/internal/config/command/command_test.go @@ -1,6 +1,6 @@ // +build unit -package config +package command import ( "testing" @@ -17,11 +17,11 @@ func TestConfigCommand(t *testing.T) { testcobra.CheckCobraRequiredFlags(t, Command, []string{}) } -func TestCmdDelete(t *testing.T) { - assert.Equal(t, "delete", cmdDelete.Name()) +func TestCmdReset(t *testing.T) { + assert.Equal(t, "reset", cmdReset.Name()) - testcobra.CheckCobraMetadata(t, cmdDelete) - testcobra.CheckCobraRequiredFlags(t, cmdDelete, []string{"key"}) + testcobra.CheckCobraMetadata(t, cmdReset) + testcobra.CheckCobraRequiredFlags(t, cmdReset, []string{"key"}) } func TestCmdGet(t *testing.T) { diff --git a/internal/config/config.go b/internal/config/config.go index 31c0969ae..95630b958 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,440 +2,144 @@ package config import ( "fmt" - "os" - "reflect" - "strings" + "path/filepath" - "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" + "github.com/newrelic/newrelic-client-go/pkg/region" ) 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" - - // DefaultEnvPrefix is used when reading environment variables - DefaultEnvPrefix = "NEW_RELIC_CLI" - - globalScopeIdentifier = "*" + APIKey FieldKey = "apiKey" + InsightsInsertKey FieldKey = "insightsInsertKey" + Region FieldKey = "region" + AccountID FieldKey = "accountID" + LicenseKey FieldKey = "licenseKey" + LogLevel FieldKey = "loglevel" + PluginDir FieldKey = "plugindir" + PreReleaseFeatures FieldKey = "prereleasefeatures" + SendUsageData FieldKey = "sendUsageData" + + DefaultProfileName = "default" + + DefaultProfileFileName = "default-profile.json" + ConfigFileName = "config.json" + CredentialsFileName = "credentials.json" + DefaultPluginDir = "plugins" ) var ( - // DefaultConfigDirectory is the default location for the CLI config files - DefaultConfigDirectory string - - defaultConfig *Config + ConfigStore *JSONStore + CredentialsProvider *JSONStore + BasePath string = configBasePath() + + FlagProfileName string + FlagDebug bool + FlagTrace bool + FlagAccountID int ) -// 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 - - configDir string -} - -// Value represents an instance of a configuration field. -type Value 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)) - } - - return c.Value == c.Default -} - func init() { - defaultConfig = &Config{ - LogLevel: DefaultLogLevel, - SendUsageData: TernaryValues.Unknown, - PreReleaseFeatures: TernaryValues.Unknown, - } - - cfgDir, err := utils.GetDefaultConfigDirectory() - if err != nil { - log.Fatalf("error building default config directory: %s", err) - } - - 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 configDir == "" { - configDir = DefaultConfigDirectory - } else { - configDir = os.ExpandEnv(configDir) - } - - cfg, err := load(configDir) - if err != nil { - return nil, err - } - - initLogger(cfg.LogLevel) - cfg.configDir = configDir - - return cfg, nil -} - -// List outputs a list of all the configuration values -func (c *Config) List() { - output.Text(c.getAll("")) -} - -// 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 - } - - err = c.set(key, defaultValue) - if err != nil { - return err - } - - output.Printf("%s %s removed successfully\n", text.FgGreen.Sprint("✔"), text.Bold.Sprint(key)) - - return nil -} - -// Get retrieves a config value. -func (c *Config) Get(key string) { - output.Text(c.getAll(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 - } - - output.Printf("%s set to %s\n", text.Bold.Sprint(key), text.FgCyan.Sprint(value)) - - return nil -} - -func load(configDir string) (*Config, error) { - cfgViper, err := readConfig(configDir) - if err != nil { - return nil, err - } - - allScopes, err := unmarshalAllScopes(cfgViper) - - if err != nil { - return nil, err - } - - config, ok := (*allScopes)[globalScopeIdentifier] - if !ok { - config = Config{} - } - - err = config.setDefaults() - if err != nil { - return nil, err - } - - err = config.applyOverrides() - if err != nil { - return nil, err - } - - return &config, nil -} - -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 - } - - err = os.MkdirAll(c.configDir, os.ModePerm) - if err != nil { - return err - } - - log.Debugf("creating config file at %s: %+v", path, cfgViper.AllSettings()) - - err = cfgViper.WriteConfigAs(path) - if err != nil { - return err - } - - return nil -} - -func (c *Config) getAll(key string) []Value { - values := []Value{} - - err := c.visitAllConfigFields(func(v *Value) error { - // Return early if name was supplied and doesn't match - if key != "" && key != v.Name { - return nil - } - - values = append(values, *v) - - return nil - }) - if err != nil { - log.Error(err) - } - - return values -} - -func (c *Config) set(key string, value interface{}) error { - cfgViper, err := readConfig(c.configDir) - if err != nil { - return err - } - - cfgViper.Set(globalScopeIdentifier+"."+key, value) - - allScopes, err := unmarshalAllScopes(cfgViper) - if err != nil { - return err - } - - config, ok := (*allScopes)[globalScopeIdentifier] - if !ok { - return fmt.Errorf("failed to locate global scope") - } - - err = config.setDefaults() - if err != nil { - return err - } - - err = config.validate() - 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 { - 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) - } - } - - return nil -} - -func (c *Config) getDefaultValue(key string) (interface{}, error) { - var dv interface{} - var found bool - - err := c.visitAllConfigFields(func(v *Value) error { - if key == v.Name { - dv = v.Default - found = true - } - - return nil - }) - - if err != nil { - return dv, err - } - - if found { - return dv, nil - } - - return nil, fmt.Errorf("failed to locate default value for %s", key) -} - -func (c *Config) applyOverrides() error { - log.Debug("setting config overrides") - - if v := os.Getenv("NEW_RELIC_CLI_PRERELEASEFEATURES"); v != "" { - c.PreReleaseFeatures = Ternary(v) - } - - return nil -} - -func (c *Config) setDefaults() error { - log.Debug("setting config default") - - if c == nil { - return nil - } - - if err := mergo.Merge(c, defaultConfig); 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) - } - } - - return nil - }) - - if err != nil { - return err - } - - return nil -} - -func (c *Config) visitAllConfigFields(f func(*Value) error) error { - cfgType := reflect.TypeOf(*c) - cfgValue := reflect.ValueOf(*c) - defaultCfgValue := reflect.ValueOf(*defaultConfig) - - // Iterate through the fields in the struct - for i := 0; i < cfgType.NumField(); i++ { - field := cfgType.Field(i) - - // Skip unexported fields - if field.PkgPath != "" { - continue - } - - name := field.Tag.Get("mapstructure") - value := cfgValue.Field(i).Interface() - defaultValue := defaultCfgValue.Field(i).Interface() - - err := f(&Value{ - Name: name, - Value: value, - Default: defaultValue, - }) - - if err != nil { - return err - } - } - - return 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) - } - - return &cfgMap, 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 - - 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) - } - } - - return cfgViper, nil -} - -func validConfigKeys() []string { - var keys []string - - cfgType := reflect.TypeOf(Config{}) - for i := 0; i < cfgType.NumField(); i++ { - field := cfgType.Field(i) - name := field.Tag.Get("mapstructure") - keys = append(keys, name) - } - - return keys -} - -func stringInStrings(s string, ss []string) bool { - for _, v := range ss { - if v == s { - return true - } - } - - return false -} - -// Function ignores the case -func stringInStringsIgnoreCase(s string, ss []string) bool { - for _, v := range ss { - if strings.EqualFold(v, s) { - return true - } - } - - return false + Init(configBasePath()) +} + +func Init(basePath string) { + BasePath = basePath + InitializeConfigStore() + InitializeCredentialsStore() +} + +func InitializeCredentialsStore() { + p, err := NewJSONStore( + PersistToFile(filepath.Join(BasePath, CredentialsFileName)), + EnforceStrictFields(), + ConfigureFields( + FieldDefinition{ + Key: APIKey, + EnvVar: "NEW_RELIC_API_KEY", + Sensitive: true, + }, + FieldDefinition{ + Key: InsightsInsertKey, + EnvVar: "NEW_RELIC_INSIGHTS_INSERT_KEY", + Sensitive: true, + }, + FieldDefinition{ + Key: Region, + EnvVar: "NEW_RELIC_REGION", + SetValidationFunc: StringInStrings(false, + region.Staging.String(), + region.US.String(), + region.EU.String(), + ), + Default: region.US.String(), + SetValueFunc: ToLower(), + }, + FieldDefinition{ + Key: AccountID, + EnvVar: "NEW_RELIC_ACCOUNT_ID", + SetValidationFunc: IntGreaterThan(0), + }, + FieldDefinition{ + Key: LicenseKey, + EnvVar: "NEW_RELIC_LICENSE_KEY", + Sensitive: true, + }, + ), + ) + + if err != nil { + log.Fatalf("could not create credentials provider: %s", err) + } + + CredentialsProvider = p +} + +func InitializeConfigStore() { + p, err := NewJSONStore( + PersistToFile(filepath.Join(BasePath, ConfigFileName)), + UseGlobalScope("*"), + EnforceStrictFields(), + ConfigureFields( + FieldDefinition{ + Key: LogLevel, + EnvVar: "NEW_RELIC_CLI_LOG_LEVEL", + Default: "info", + SetValidationFunc: StringInStrings(false, "Info", "Debug", "Trace", "Warn", "Error"), + }, + FieldDefinition{ + Key: PluginDir, + EnvVar: "NEW_RELIC_CLI_PLUGIN_DIR", + Default: filepath.Join(BasePath, DefaultPluginDir), + }, + FieldDefinition{ + Key: PreReleaseFeatures, + EnvVar: "NEW_RELIC_CLI_PRERELEASEFEATURES", + SetValidationFunc: IsTernary(), + Default: TernaryValues.Unknown, + }, + FieldDefinition{ + Key: SendUsageData, + EnvVar: "NEW_RELIC_CLI_SENDUSAGEDATA", + SetValidationFunc: IsTernary(), + Default: TernaryValues.Unknown, + }, + ), + ) + + if err != nil { + log.Fatalf("could not create configuration provider: %s", err) + } + + ConfigStore = p +} + +func configBasePath() string { + home, err := homedir.Dir() + if err != nil { + log.Fatalf("cannot locate user's home directory: %s", err) + } + + return fmt.Sprintf("%s/.newrelic", home) } diff --git a/internal/config/config_integration_test.go b/internal/config/config_integration_test.go deleted file mode 100644 index 070e78389..000000000 --- a/internal/config/config_integration_test.go +++ /dev/null @@ -1,116 +0,0 @@ -// +build integration - -package config - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -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) - } - - err = c.Set("logLevel", "INVALID_VALUE") - assert.Error(t, err) - - err = c.Set("loglevel", "Info") - assert.Error(t, err) - - err = c.Set("Loglevel", "Debug") - assert.Error(t, err) - -} - -func TestConfigSetSendUsageData(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 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) - } - - 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) - } - - err = c.Set("preReleaseFeatures", "INVALID_VALUE") - assert.Error(t, err) -} - -func TestConfigSetPluginDir(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) - - err = c.Set("pluginDir", "test") - assert.NoError(t, err) - assert.Equal(t, "test", c.PluginDir) -} 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/json_store.go b/internal/config/json_store.go new file mode 100644 index 000000000..f976ab9d3 --- /dev/null +++ b/internal/config/json_store.go @@ -0,0 +1,596 @@ +package config + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// JSONStore is a configurable json-backed configuration store. +type JSONStore struct { + cfg []byte + fields []FieldDefinition + fileName string + scope string + mu sync.Mutex + explicitValues bool +} + +// FieldKey is the key of a config field. +type FieldKey string + +// FieldDefinition contains the information required to describe a configuration field. +type FieldDefinition struct { + // EnvVar provides an environment variable override + EnvVar string + + // Key represents the key that will be used to store the underlying value. + Key FieldKey + + // EnvVar provides a default value to be returned during a get operation + // if no value can be found for this field. + Default interface{} + + // CaseSensitive determines whether this config key will be treated as case-sensitive + // or not. When false, keys passed to get and set operations will be performed + // with the canonical casing of the Key value provided in the field definition. + CaseSensitive bool + + // Sensitive marks the underlying value as sensitive. When true, the underlying + // field's value will be obfuscated when printed to the console during the + // execution of various commands. + Sensitive bool + + // SetValidationFunc is a validation func that is run when a set operation + // is performed for the underlying value. If the func returns an error, the + // set operation will not succeed. + SetValidationFunc FieldValueValidationFunc + + // SetValueFunc is a translation func that is run when a set operation + // is performed for the underlying value. The value provided will be run + // through the func provided and the resulting value will be set. + SetValueFunc FieldValueTranslationFunc +} + +// FieldValueValidationFunc is a configurable validation func that will ensure a field +// value conforms to some constraints before being set. +type FieldValueValidationFunc func(key FieldKey, value interface{}) error + +// FieldValueTranslationFunc is a configurable translation func that will modify +// a value before setting it in the underlying config instance. +type FieldValueTranslationFunc func(key FieldKey, value interface{}) (interface{}, error) + +// IntGreaterThan is a FieldValueValidationFunc ins a validation func that ensures +// the field value is an integer greater than 0. +func IntGreaterThan(greaterThan int) func(key FieldKey, value interface{}) error { + return func(key FieldKey, value interface{}) error { + var s int + var ok bool + if s, ok = value.(int); !ok { + return fmt.Errorf("%v is not an int", value) + } + + if s > greaterThan { + return nil + } + + return fmt.Errorf("value %d is not greater than %d", s, greaterThan) + } +} + +// IsTernary is a FieldValueValidationFunc ins a validation func that ensures +// the field value is a valid Ternary. +func IsTernary() func(key FieldKey, value interface{}) error { + return func(key FieldKey, value interface{}) error { + switch v := value.(type) { + case string: + return Ternary(v).Valid() + case Ternary: + return v.Valid() + default: + return fmt.Errorf("value %s for key %s is not valid", value, key) + } + + } +} + +// StringInStrings is a FieldValueValidationFunc ins a validation func that ensures +// the field value appears in the given collection of allowed values. +func StringInStrings(caseSensitive bool, allowedValues ...string) func(key FieldKey, value interface{}) error { + return func(key FieldKey, value interface{}) error { + var s string + var ok bool + if s, ok = value.(string); !ok { + return fmt.Errorf("%v is not a string", value) + } + + for _, v := range allowedValues { + if caseSensitive && s == v { + return nil + } + + if !caseSensitive && strings.EqualFold(s, v) { + return nil + } + } + + return fmt.Errorf("value %v not in allowed values: %s", s, allowedValues) + } +} + +// ToLower is a FieldValueTranslationFunc translation func that ensures the provided +// value is case-folded to lowercase before writing to the underlying config. +func ToLower() func(key FieldKey, value interface{}) (interface{}, error) { + return func(key FieldKey, value interface{}) (interface{}, error) { + if s, ok := value.(string); ok { + return strings.ToLower(s), nil + } + + return nil, fmt.Errorf("the value %s provided for %s is not a string", value, key) + } +} + +// JSONStoreOption is a func for supplying options when creating a new JSONStore. +type JSONStoreOption func(*JSONStore) error + +// NewJSONStore creates a new instance of JSONStore. +func NewJSONStore(opts ...JSONStoreOption) (*JSONStore, error) { + p := &JSONStore{} + + for _, fn := range opts { + if fn == nil { + continue + } + if err := fn(p); err != nil { + return nil, err + } + } + + return p, nil +} + +// EnforceStrictFields is a JSONStoreOption func that ensures that every field accessed +// is backed by a FieldDefinition. +func EnforceStrictFields() JSONStoreOption { + return func(p *JSONStore) error { + p.explicitValues = true + return nil + } +} + +// ConfigureFields is a JSONStoreOption func that allows the caller to describe the +// fields stored in this config instance with one or more field definitions. +func ConfigureFields(definitions ...FieldDefinition) JSONStoreOption { + return func(p *JSONStore) error { + for _, d := range definitions { + if !d.CaseSensitive { + for _, k := range p.getConfigValueKeys() { + if strings.EqualFold(string(k), string(d.Key)) { + return fmt.Errorf("unable to add case-insensitive field definition for %s, another field already defined with matching case-folded key", d.Key) + } + } + } + p.fields = append(p.fields, d) + } + return nil + } +} + +// PersistToFile is a JSONStoreOption func that ensures all writes to this config +// instance are persisted to disk. +func PersistToFile(fileName string) JSONStoreOption { + return func(p *JSONStore) error { + p.fileName = fileName + return nil + } +} + +// UseGlobalScope is a JSONStoreOption func that ensures all config fields are stored +// under a global object scope with the passed scope string as a key. +func UseGlobalScope(scope string) JSONStoreOption { + return func(p *JSONStore) error { + p.scope = scope + return nil + } +} + +// GetString retrieves a string from this config instance for the given key. An +// attempt will be made to convert the underlying type of this field's value to +// a string. If the value cannot be retrieved, a zero value will be returned. +func (p *JSONStore) GetString(key FieldKey) (string, error) { + return p.GetStringWithScopeAndOverride("", key, nil) +} + +// GetStringWithOverride retrieves a string from this config instance for the given +// key, overriding with the provided value if is not nil. An attempt will be made +// to convert the underlying type of this field's value to a string. If the value +// cannot be retrieved, a zero value will be returned. +func (p *JSONStore) GetStringWithOverride(key FieldKey, override *string) (string, error) { + return p.GetStringWithScopeAndOverride("", key, override) +} + +// GetString retrieves a string from this config instance for the given key, prefixing +// the key's path with the given scope. An attempt will be made to convert the underlying +// type of this field's value to a string. If the value cannot be retrieved, a zero +// value will be returned. +func (p *JSONStore) GetStringWithScope(scope string, key FieldKey) (string, error) { + return p.GetStringWithScopeAndOverride(scope, key, nil) +} + +// GetStringWithOverride retrieves a string from this config instance for the given +// key, prefixing the key's path with the given scope and overriding with the provided +// value if it is not nil. An attempt will be made to convert the underlying type +// of this field's value to a string. If the value cannot be retrieved, a zero value +// will be returned. +func (p *JSONStore) GetStringWithScopeAndOverride(scope string, key FieldKey, override *string) (string, error) { + var v interface{} + var err error + + if override == nil || *override == "" { + v, err = p.GetWithScope(scope, key) + } else { + v, err = p.GetWithScopeAndOverride(scope, key, *override) + } + + if err != nil { + return "", err + } + + switch v := v.(type) { + case int64: + return strconv.Itoa(int(v)), nil + case int32: + return strconv.Itoa(int(v)), nil + case float64: + return strconv.Itoa(int(v)), nil + case float32: + return strconv.Itoa(int(v)), nil + case int: + return strconv.Itoa(v), nil + case string: + return v, nil + } + + return "", fmt.Errorf("value %v for key %s is not a string", v, key) +} + +// GetInt retrieves an int64 from this config instance for the given key. An attempt +// will be made to convert the underlying type of this field's value to an int64. +// If the value cannot be retrieved, a zero value will be returned. +func (p *JSONStore) GetInt(key FieldKey) (int64, error) { + return p.GetIntWithScope("", key) +} + +// GetIntWithScope retrieves an int64 from this config instance for the given key, prefixing +// the key's path with the given scope. An attempt will be made to convert the underlying +// type of this field's value to an int64. If the value cannot be retrieved, a zero +// value will be returned. +func (p *JSONStore) GetIntWithScope(scope string, key FieldKey) (int64, error) { + return p.GetIntWithScopeAndOverride(scope, key, nil) +} + +// GetIntWithOverride retrieves an int64 from this config instance for the given +// key, prefixing the key's path with the given scope and overriding with the provided +// value if it is not nil. An attempt will be made to convert the underlying type +// of this field's value to an int64. If the value cannot be retrieved, a zero value +// will be returned. +func (p *JSONStore) GetIntWithScopeAndOverride(scope string, key FieldKey, override *int64) (int64, error) { + var v interface{} + var err error + + if override == nil || *override == 0 { + v, err = p.GetWithScopeAndOverride(scope, key, nil) + } else { + v, err = p.GetWithScopeAndOverride(scope, key, *override) + } + + if err != nil { + return 0, err + } + + switch v := v.(type) { + case int64: + return v, nil + case int32: + return int64(v), nil + case float64: + return int64(v), nil + case float32: + return int64(v), nil + case int: + return int64(v), nil + case string: + i, err := strconv.Atoi(v) + if err != nil { + return 0, fmt.Errorf("value %v for key %s is not an int", v, key) + } + return int64(i), nil + } + + return 0, fmt.Errorf("value %v for key %s is not an int", v, key) +} + +// GetInt retrieves a Ternary from this config instance for the given key. An attempt +// will be made to convert the underlying type of this field's value to a Ternary. +// If the value cannot be retrieved, a zero value will be returned. +func (p *JSONStore) GetTernary(key FieldKey) (Ternary, error) { + return p.GetTernaryWithScope("", key) +} + +// GetTernaryWithScope retrieves a Ternary from this config instance for the given +// key, prefixing the key's path with the given scope. An attempt will be made to +// convert the underlying type of this field's value to a Ternary. If the value cannot +// be retrieved, a zero value will be returned. +func (p *JSONStore) GetTernaryWithScope(scope string, key FieldKey) (Ternary, error) { + v, err := p.GetWithScope(scope, key) + if err != nil { + return Ternary(""), err + } + + switch v := v.(type) { + case string: + return Ternary(v), nil + case Ternary: + return v, nil + } + + return Ternary(""), fmt.Errorf("value %v for key %s is not a ternary", v, key) +} + +// Get retrieves a value from this config instance for the given key. +func (p *JSONStore) Get(key FieldKey) (interface{}, error) { + return p.GetWithScope("", key) +} + +// Get retrieves a value from this config instance for the given key, prefixing +// the key's path with the given scope. +func (p *JSONStore) GetWithScope(scope string, key FieldKey) (interface{}, error) { + return p.GetWithScopeAndOverride(scope, key, nil) +} + +// Get retrieves a value from this config instance for the given key, prefixing +// the key's path with the given scope and overriding with the provided value if +// it is not nil. +func (p *JSONStore) GetWithScopeAndOverride(scope string, key FieldKey, override interface{}) (interface{}, error) { + d := p.GetFieldDefinition(key) + + if d != nil { + if e, ok := os.LookupEnv(d.EnvVar); ok { + return e, nil + } + + if !d.CaseSensitive { + // use the case convention from the field definition + key = d.Key + } + } + + if override != nil { + return override, nil + } + + res, err := p.getFromConfig(p.getPath(scope, key)) + if err != nil { + if d != nil && d.Default != nil { + return d.Default, nil + } + + return nil, err + } + + return res.Value(), nil +} + +// Set sets a value within this config instance for the given key. The resulting +// config will be persisted to disk if PersistToDisk has been used. +func (p *JSONStore) Set(key FieldKey, value interface{}) error { + return p.SetWithScope("", key, value) +} + +// SetWithScope sets a value within this config instance for the given key, prefixing +// the key's path with the given scope. The resulting config will be persisted to +// disk if PersistToDisk has been used. +func (p *JSONStore) SetWithScope(scope string, key FieldKey, value interface{}) error { + v := p.GetFieldDefinition(key) + + if v != nil { + if v.SetValidationFunc != nil { + if err := v.SetValidationFunc(key, value); err != nil { + return err + } + } + + if !v.CaseSensitive { + // use the case convention from the field definition + key = v.Key + } + + if v.SetValueFunc != nil { + var err error + value, err = v.SetValueFunc(key, value) + if err != nil { + return err + } + } + + } else if p.explicitValues { + return fmt.Errorf("key '%s' is not valid, valid keys are: %v", key, p.getConfigValueKeys()) + } + + cfg := p.getConfig() + + p.mu.Lock() + defer p.mu.Unlock() + + cfg, err := sjson.Set(cfg, escapeWildcards(p.getPath(scope, key)), value) + if err != nil { + return err + } + + if err := p.writeConfig(cfg); err != nil { + return err + } + + return nil +} + +// Remove scope removes an entire scope from this config instance, including all +// the fields that appear underneath it. The resulting config will be persisted to +// disk if PersistToDisk has been used. +func (p *JSONStore) RemoveScope(scope string) error { + path := scope + if p.scope != "" { + path = fmt.Sprintf("%s.%s", p.scope, scope) + } + + return p.deletePath(path) +} + +// Remove scope removes the provided key from this config instance. The resulting +// config will be persisted to disk if PersistToDisk has been used. +func (p *JSONStore) DeleteKey(key FieldKey) error { + return p.DeleteKeyWithScope("", key) +} + +// Remove scope removes the provided key from this config instance, prefixing +// the key's path with the given scope. The resulting config will be persisted to disk if PersistToDisk has been used. +func (p *JSONStore) DeleteKeyWithScope(scope string, key FieldKey) error { + return p.deletePath(p.getPath(scope, key)) +} + +// ForEachFieldDefinition iterates through the defined fields for this config instance, +// yielding each to the func provided. +func (p *JSONStore) ForEachFieldDefinition(fn func(d FieldDefinition)) { + for _, f := range p.fields { + fn(f) + } +} + +// GetScopes returns a slice of all scopes defined within this config instance. +func (p *JSONStore) GetScopes() []string { + s := []string{} + result := gjson.Get(p.getConfig(), "@this") + result.ForEach(func(key, value gjson.Result) bool { + s = append(s, key.String()) + return true + }) + + return s +} + +// GetFieldDefinition returns a field definition for the given key if one exists. +func (p *JSONStore) GetFieldDefinition(key FieldKey) *FieldDefinition { + for _, v := range p.fields { + if !v.CaseSensitive && strings.EqualFold(string(key), string(v.Key)) { + return &v + } + + if v.CaseSensitive && key == v.Key { + return &v + } + } + + return nil +} + +func (p *JSONStore) getPath(scope string, key FieldKey) string { + path := string(key) + if scope != "" { + path = fmt.Sprintf("%s.%s", scope, key) + } + + if p.scope != "" { + path = fmt.Sprintf("%s.%s", p.scope, path) + } + + return path +} + +func (p *JSONStore) deletePath(path string) error { + p.mu.Lock() + defer p.mu.Unlock() + + cfg, err := sjson.Delete(p.getConfig(), escapeWildcards(path)) + if err != nil { + return err + } + + if err := p.writeConfig(cfg); err != nil { + return err + } + + return nil +} + +func (p *JSONStore) getFromConfig(path string) (*gjson.Result, error) { + res := gjson.Get(p.getConfig(), path) + + if !res.Exists() { + return nil, fmt.Errorf("no value found at path %s", path) + } + + return &res, nil +} + +func (p *JSONStore) writeConfig(json string) error { + p.cfg = []byte(json) + + if p.fileName != "" { + dir := filepath.Dir(p.fileName) + _, err := os.Stat(dir) + if err != nil { + err = os.Mkdir(dir, 0750) + if err != nil { + return err + } + } + + if err := ioutil.WriteFile(p.fileName, p.cfg, 0640); err != nil { + return err + } + } + + return nil +} + +func (p *JSONStore) getConfig() string { + if p.cfg == nil { + if p.fileName != "" { + p.setConfigFromFile() + } + } + + return string(p.cfg) +} + +func (p *JSONStore) setConfigFromFile() { + data, err := ioutil.ReadFile(p.fileName) + if err != nil { + return + } + + p.cfg = data +} + +// Escape wildcard characters, as required by sjson +func escapeWildcards(key string) string { + re := regexp.MustCompile(`([*?])`) + return re.ReplaceAllString(key, "\\$1") +} + +func (p *JSONStore) getConfigValueKeys() []FieldKey { + var keys []FieldKey + for _, v := range p.fields { + keys = append(keys, v.Key) + } + + return keys +} diff --git a/internal/config/json_store_test.go b/internal/config/json_store_test.go new file mode 100644 index 000000000..8f66213a5 --- /dev/null +++ b/internal/config/json_store_test.go @@ -0,0 +1,616 @@ +//+build integration + +package config + +import ( + "errors" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + testCfg = `{ + "*":{ + "loglevel":"debug", + "plugindir": "/Users/ctrombley/.newrelic/plugins", + "prereleasefeatures": "NOT_ASKED", + "sendusagedata": "NOT_ASKED", + "testInt": 42, + "testString": "value1", + "teststring": "value2" + "caseInsensitiveTest": "value" + } + }` +) + +func TestStore_Ctor_NilOption(t *testing.T) { + _, err := NewJSONStore(nil) + require.NoError(t, err) +} + +func TestStore_Ctor_OptionError(t *testing.T) { + _, err := NewJSONStore(func(*JSONStore) error { return errors.New("") }) + require.Error(t, err) +} + +func TestStore_Ctor_CaseInsensitiveKeyCollision(t *testing.T) { + _, err := NewJSONStore( + ConfigureFields( + FieldDefinition{Key: "asdf"}, + FieldDefinition{Key: "ASDF"}, + ), + ) + require.Error(t, err) +} + +func TestStore_Ctor_CaseSensitiveKeyOverlap(t *testing.T) { + _, err := NewJSONStore( + ConfigureFields( + FieldDefinition{ + Key: "asdf", + CaseSensitive: true, + }, + ), + ) + require.NoError(t, err) +} + +func TestStore_GetString(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(testCfg) + require.NoError(t, err) + + p, err := NewJSONStore( + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + actual, err := p.GetString("loglevel") + require.NoError(t, err) + require.Equal(t, "debug", actual) +} + +func TestStore_GetString_CaseSensitive(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(testCfg) + require.NoError(t, err) + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "testString", + CaseSensitive: true, + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + actual, err := p.GetString("testString") + require.NoError(t, err) + require.Equal(t, "value1", actual) + + actual, err = p.GetString("teststring") + require.NoError(t, err) + require.Equal(t, "value2", actual) +} + +func TestStore_GetString_CaseInsensitive(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(testCfg) + require.NoError(t, err) + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "caseInsensitiveTest", + CaseSensitive: false, + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + actual, err := p.GetString("caseinsensitivetest") + require.NoError(t, err) + require.Equal(t, "value", actual) +} + +func TestStore_GetString_NotDefined(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(testCfg) + require.NoError(t, err) + + p, err := NewJSONStore( + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + _, err = p.GetString("undefined") + require.Error(t, err) + require.Contains(t, err.Error(), "no value found") +} + +func TestStore_GetString_DefaultValue(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "prereleasefeatures", + Default: "NOT_ASKED", + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + actual, err := p.GetString("prereleasefeatures") + require.NoError(t, err) + require.Equal(t, "NOT_ASKED", actual) +} + +func TestStore_GetString_EnvVarOverride(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "prereleasefeatures", + Default: "NOT_ASKED", + EnvVar: "NEW_RELIC_CLI_PRERELEASE", + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = os.Setenv("NEW_RELIC_CLI_PRERELEASE", "testValue") + require.NoError(t, err) + + actual, err := p.GetString("prereleasefeatures") + require.NoError(t, err) + require.Equal(t, "testValue", actual) +} + +func TestStore_GetInt(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(testCfg) + require.NoError(t, err) + + p, err := NewJSONStore( + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + actual, err := p.GetInt("testInt") + require.NoError(t, err) + require.Equal(t, int64(42), actual) +} + +func TestStore_GetInt_NotDefined(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(testCfg) + require.NoError(t, err) + + p, err := NewJSONStore( + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + _, err = p.GetInt("undefined") + require.Error(t, err) + require.Contains(t, err.Error(), "no value found") +} + +func TestStore_GetInt_DefaultValue(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "testInt", + Default: 42, + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + actual, err := p.GetInt("testInt") + require.NoError(t, err) + require.Equal(t, int64(42), actual) +} + +func TestStore_GetInt_EnvVarOverride(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "testInt", + EnvVar: "TEST_INT", + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = os.Setenv("TEST_INT", "42") + require.NoError(t, err) + + actual, err := p.GetInt("testInt") + require.NoError(t, err) + require.Equal(t, int64(42), actual) +} + +func TestStore_GetInt_EnvVarOverride_WrongType(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "testInt", + EnvVar: "TEST_INT", + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = os.Setenv("TEST_INT", "TEST_VALUE") + require.NoError(t, err) + + _, err = p.GetInt("testInt") + require.Error(t, err) +} + +func TestStore_Set(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(testCfg) + require.NoError(t, err) + + p, err := NewJSONStore( + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("loglevel", "trace") + require.NoError(t, err) + + actual, err := p.GetString("loglevel") + require.NoError(t, err) + require.Equal(t, "trace", actual) +} + +func TestStore_SetTernary(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(testCfg) + require.NoError(t, err) + + p, err := NewJSONStore( + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("testTernary", TernaryValues.Allow) + require.NoError(t, err) + + actual, err := p.GetTernary("testTernary") + require.NoError(t, err) + require.Equal(t, TernaryValues.Allow, actual) +} + +func TestStore_SetTernary_Invalid(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(testCfg) + require.NoError(t, err) + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "testTernary", + SetValidationFunc: IsTernary(), + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("testTernary", Ternary("invalid")) + require.Error(t, err) + + err = p.Set("anotherTestTernary", "invalid") + require.NoError(t, err) + + actual, err := p.GetTernary("anotherTestTernary") + require.NoError(t, err) + require.False(t, actual.Bool()) +} + +func TestStore_Set_CaseSensitive(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(testCfg) + require.NoError(t, err) + + p, err := NewJSONStore( + EnforceStrictFields(), + ConfigureFields(FieldDefinition{ + Key: "loglevel", + CaseSensitive: true, + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("loglevel", "trace") + require.NoError(t, err) + + err = p.Set("logLevel", "info") + require.Error(t, err) +} + +func TestStore_Set_CaseInsensitive(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(testCfg) + require.NoError(t, err) + + p, err := NewJSONStore( + EnforceStrictFields(), + ConfigureFields(FieldDefinition{ + Key: "loglevel", + CaseSensitive: false, + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("loglevel", "trace") + require.NoError(t, err) + + err = p.Set("LOGLEVEL", "info") + require.NoError(t, err) + + actual, err := p.GetString("loglevel") + require.NoError(t, err) + require.Equal(t, "info", actual) +} + +func TestStore_Set_FileDoesNotExist(t *testing.T) { + p, err := NewJSONStore( + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("loglevel", "trace") + require.NoError(t, err) + + actual, err := p.GetString("loglevel") + require.NoError(t, err) + require.Equal(t, "trace", actual) +} + +func TestStore_Set_ExplicitValues_CaseInsensitive(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + EnforceStrictFields(), + ConfigureFields(FieldDefinition{ + Key: "allowed", + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("loglevel", "trace") + require.Error(t, err) + + err = p.Set("ALLOWED", "testValue") + require.NoError(t, err) +} + +func TestStore_Set_ExplicitValues_CaseSensitive(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + EnforceStrictFields(), + ConfigureFields(FieldDefinition{ + Key: "allowed", + CaseSensitive: true, + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("loglevel", "trace") + require.Error(t, err) + + err = p.Set("ALLOWED", "testValue") + require.Error(t, err) + + err = p.Set("allowed", "testValue") + require.NoError(t, err) +} + +func TestStore_Set_ValidationFunc_IntGreaterThan(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "loglevel", + SetValidationFunc: IntGreaterThan(0), + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("loglevel", 0) + require.Error(t, err) + + err = p.Set("loglevel", 1) + require.NoError(t, err) +} + +func TestStore_Set_ValidationFunc_IntGreaterThan_WrongType(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "loglevel", + SetValidationFunc: IntGreaterThan(0), + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("loglevel", "debug") + require.Error(t, err) + + err = p.Set("loglevel", 1) + require.NoError(t, err) +} + +func TestStore_Set_ValidationFunc_StringInStrings_CaseSensitive(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "loglevel", + SetValidationFunc: StringInStrings(true, "valid", "alsoValid"), + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("loglevel", "trace") + require.Error(t, err) + + err = p.Set("loglevel", "valid") + require.NoError(t, err) +} + +func TestStore_Set_ValidationFunc_StringInStrings_CaseInsensitive(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "loglevel", + SetValidationFunc: StringInStrings(false, "valid", "alsoValid"), + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("loglevel", "VALID") + require.NoError(t, err) + + err = p.Set("loglevel", "ALSOVALID") + require.NoError(t, err) +} + +func TestStore_Set_ValidationFunc_StringInStrings_WrongType(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "testInt", + SetValidationFunc: StringInStrings(false, "valid", "alsoValid"), + }), + PersistToFile(f.Name()), + UseGlobalScope("*"), + ) + require.NoError(t, err) + + err = p.Set("testInt", 42) + require.Error(t, err) + require.Contains(t, err.Error(), "is not a string") +} + +func TestStore_Set_ValueFunc_ToLower(t *testing.T) { + f, err := ioutil.TempFile("", "newrelic-cli.config_provider_test.*.json") + require.NoError(t, err) + defer f.Close() + + p, err := NewJSONStore( + ConfigureFields(FieldDefinition{ + Key: "testString", + SetValueFunc: ToLower(), + }), + PersistToFile(f.Name()), + ) + require.NoError(t, err) + + err = p.Set("testString", "TEST_VALUE") + require.NoError(t, err) + + v, err := p.GetString("testString") + require.NoError(t, err) + + require.Equal(t, "test_value", v) +} diff --git a/internal/config/logging.go b/internal/config/logging.go index 195d7f2ff..724e7dcff 100644 --- a/internal/config/logging.go +++ b/internal/config/logging.go @@ -20,7 +20,7 @@ var ( fileHookConfigured = false ) -func initLogger(logLevel string) { +func InitLogger(logLevel string) { l := log.StandardLogger() l.SetFormatter(&log.TextFormatter{ @@ -49,16 +49,16 @@ func InitFileLogger() { return } - _, err := os.Stat(DefaultConfigDirectory) + _, err := os.Stat(BasePath) if os.IsNotExist(err) { - errDir := os.MkdirAll(DefaultConfigDirectory, 0750) + errDir := os.MkdirAll(BasePath, 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(BasePath+"/"+DefaultLogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0640) if err == nil && !fileHookConfigured { l := log.StandardLogger() l.Hooks.Add(fileHook) 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 279cdf644..000000000 --- a/internal/credentials/profiles.go +++ /dev/null @@ -1,229 +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") - envLicenseKey := os.Getenv("NEW_RELIC_LICENSE_KEY") - envRegion := os.Getenv("NEW_RELIC_REGION") - envAccountID := os.Getenv("NEW_RELIC_ACCOUNT_ID") - - if envAPIKey == "" && envRegion == "" && envInsightsInsertKey == "" && envAccountID == "" && envLicenseKey == "" { - return p - } - - out := Profile{} - if p != nil { - out = *p - } - - if envAPIKey != "" { - out.APIKey = envAPIKey - } - - if envInsightsInsertKey != "" { - out.InsightsInsertKey = envInsightsInsertKey - } - - if envLicenseKey != "" { - out.LicenseKey = envLicenseKey - } - - 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/command_validate.go b/internal/diagnose/command_validate.go index 162d87a3e..9b838f0a0 100644 --- a/internal/diagnose/command_validate.go +++ b/internal/diagnose/command_validate.go @@ -6,7 +6,6 @@ import ( "github.com/newrelic/newrelic-cli/internal/client" "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" ) var cmdValidate = &cobra.Command{ @@ -17,16 +16,15 @@ var cmdValidate = &cobra.Command{ Checks the configuration in the default or specified configuation profile by sending data to the New Relic platform and verifying that it has been received.`, Example: "\tnewrelic diagnose validate", + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - v := NewConfigValidator(nrClient) + v := NewConfigValidator(client.NRClient) - log.Printf("Sending tracer event to New Relic.") - err := v.Validate(utils.SignalCtx) - if err != nil { - log.Fatal(err) - } - }) + log.Printf("Sending tracer event to New Relic.") + err := v.Validate(utils.SignalCtx) + if err != nil { + log.Fatal(err) + } }, } diff --git a/internal/diagnose/config_validator.go b/internal/diagnose/config_validator.go index 71d103f0a..6363c8fed 100644 --- a/internal/diagnose/config_validator.go +++ b/internal/diagnose/config_validator.go @@ -9,7 +9,8 @@ import ( "github.com/shirou/gopsutil/host" - "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/config" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "github.com/newrelic/newrelic-cli/internal/utils" "github.com/newrelic/newrelic-cli/internal/utils/validation" "github.com/newrelic/newrelic-client-go/newrelic" @@ -26,7 +27,6 @@ const ( type ConfigValidator struct { client *newrelic.NewRelic *validation.PollingNRQLValidator - profile *credentials.Profile PostRetryDelaySec int PostMaxRetries int } @@ -45,14 +45,15 @@ func NewConfigValidator(client *newrelic.NewRelic) *ConfigValidator { return &ConfigValidator{ client: client, PollingNRQLValidator: v, - profile: credentials.DefaultProfile(), PostRetryDelaySec: DefaultPostRetryDelaySec, PostMaxRetries: DefaultPostMaxRetries, } } func (c *ConfigValidator) Validate(ctx context.Context) error { - if err := c.validateKeys(ctx, c.profile); err != nil { + accountID := configAPI.GetActiveProfileAccountID() + + if err := c.validateKeys(ctx); err != nil { return err } @@ -70,7 +71,7 @@ func (c *ConfigValidator) Validate(ctx context.Context) error { } postEvent := func() error { - if err = c.client.Events.CreateEventWithContext(ctx, c.profile.AccountID, evt); err != nil { + if err = c.client.Events.CreateEventWithContext(ctx, accountID, evt); err != nil { log.Error(err) return ErrPostEvent } @@ -99,13 +100,13 @@ func (c *ConfigValidator) Validate(ctx context.Context) error { return err } -func (c *ConfigValidator) validateKeys(ctx context.Context, profile *credentials.Profile) error { +func (c *ConfigValidator) validateKeys(ctx context.Context) error { validateKeyFunc := func() error { - if err := c.validateLicenseKey(ctx, profile); err != nil { + if err := c.validateLicenseKey(ctx); err != nil { return err } - if err := c.validateInsightsInsertKey(ctx, profile); err != nil { + if err := c.validateInsightsInsertKey(ctx); err != nil { return err } return nil @@ -118,14 +119,16 @@ func (c *ConfigValidator) validateKeys(ctx context.Context, profile *credentials return nil } -func (c *ConfigValidator) validateInsightsInsertKey(ctx context.Context, profile *credentials.Profile) error { - insightsInsertKeys, err := c.client.APIAccess.ListInsightsInsertKeysWithContext(ctx, profile.AccountID) +func (c *ConfigValidator) validateInsightsInsertKey(ctx context.Context) error { + accountID := configAPI.GetActiveProfileAccountID() + insightsInsertKey := configAPI.GetActiveProfileString(config.InsightsInsertKey) + insightsInsertKeys, err := c.client.APIAccess.ListInsightsInsertKeysWithContext(ctx, accountID) if err != nil { return fmt.Errorf(ErrConnectionStringFormat, err) } for _, k := range insightsInsertKeys { - if k.Key == profile.InsightsInsertKey { + if k.Key == insightsInsertKey { return nil } } @@ -133,10 +136,12 @@ func (c *ConfigValidator) validateInsightsInsertKey(ctx context.Context, profile return ErrInsightsInsertKey } -func (c *ConfigValidator) validateLicenseKey(ctx context.Context, profile *credentials.Profile) error { +func (c *ConfigValidator) validateLicenseKey(ctx context.Context) error { + accountID := configAPI.GetActiveProfileAccountID() + licenseKey := configAPI.GetActiveProfileString(config.LicenseKey) params := apiaccess.APIAccessKeySearchQuery{ Scope: apiaccess.APIAccessKeySearchScope{ - AccountIDs: []int{profile.AccountID}, + AccountIDs: []int{accountID}, }, Types: []apiaccess.APIAccessKeyType{ apiaccess.APIAccessKeyTypeTypes.INGEST, @@ -149,7 +154,7 @@ func (c *ConfigValidator) validateLicenseKey(ctx context.Context, profile *crede } for _, k := range licenseKeys { - if k.Key == profile.LicenseKey { + if k.Key == licenseKey { return nil } } diff --git a/internal/edge/command_trace_observer.go b/internal/edge/command_trace_observer.go index d0d8a53e9..f59b516e3 100644 --- a/internal/edge/command_trace_observer.go +++ b/internal/edge/command_trace_observer.go @@ -5,14 +5,13 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "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" ) var ( - accountID int id int name string providerRegion string @@ -42,13 +41,13 @@ 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: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - traceObservers, err := nrClient.Edge.ListTraceObserversWithContext(utils.SignalCtx, accountID) - utils.LogIfFatal(err) + accountID := configAPI.RequireActiveProfileAccountID() + traceObservers, err := client.NRClient.Edge.ListTraceObserversWithContext(utils.SignalCtx, accountID) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(traceObservers)) - }) + utils.LogIfFatal(output.Print(traceObservers)) }, } @@ -71,18 +70,18 @@ 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: client.RequireClient, 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) - } + accountID := configAPI.RequireActiveProfileAccountID() + 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.NRClient.Edge.CreateTraceObserver(accountID, name, edge.EdgeProviderRegion(providerRegion)) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(traceObserver)) - log.Info("success") - }) + utils.LogIfFatal(output.Print(traceObserver)) + log.Info("success") }, } @@ -94,21 +93,19 @@ var cmdDelete = &cobra.Command{ The delete command accepts a trace observer's ID. `, Example: `newrelic edge trace-observer delete --accountId 12345678 --id 1234`, + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - _, err := nrClient.Edge.DeleteTraceObserver(accountID, id) - utils.LogIfFatal(err) + accountID := configAPI.RequireActiveProfileAccountID() + _, err := client.NRClient.Edge.DeleteTraceObserver(accountID, id) + utils.LogIfFatal(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) diff --git a/internal/entities/command_search.go b/internal/entities/command_search.go index c0f0b2935..bd03052ef 100644 --- a/internal/entities/command_search.go +++ b/internal/entities/command_search.go @@ -9,7 +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/entities" ) @@ -21,79 +20,78 @@ var cmdEntitySearch = &cobra.Command{ The search command performs a search for New Relic entities. `, Example: "newrelic entity search --name ", + PreRun: client.RequireClient, 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 != "" { - params.Name = entityName - } + if entityName == "" && entityType == "" && entityAlertSeverity == "" && entityDomain == "" { + utils.LogIfError(cmd.Help()) + 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}} - } + if entityTag != "" { + key, value, err := assembleTagValue(entityTag) + utils.LogIfFatal(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.") - } + 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.GetEntitySearchWithContext( - utils.SignalCtx, - entities.EntitySearchOptions{}, - "", - params, - []entities.EntitySearchSortCriteria{}, - ) - utils.LogIfFatal(err) + params.Reporting = reporting + } - entities := results.Results.Entities + results, err := client.NRClient.Entities.GetEntitySearchWithContext( + utils.SignalCtx, + entities.EntitySearchOptions{}, + "", + params, + []entities.EntitySearchSortCriteria{}, + ) + utils.LogIfFatal(err) - var result interface{} + entities := results.Results.Entities - if len(entityFields) > 0 { - mapped := mapEntities(entities, entityFields, utils.StructToMap) + var result interface{} - if len(mapped) == 1 { - result = mapped[0] - } else { - result = mapped - } + if len(entityFields) > 0 { + mapped := mapEntities(entities, entityFields, utils.StructToMap) + + 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)) - }) + utils.LogIfFatal(output.Print(result)) }, } diff --git a/internal/entities/command_tags.go b/internal/entities/command_tags.go index 80cb3e9b0..a30c1fa84 100644 --- a/internal/entities/command_tags.go +++ b/internal/entities/command_tags.go @@ -11,7 +11,6 @@ import ( "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,18 @@ var cmdTagsGet = &cobra.Command{ The get command returns JSON output of the tags for the requested entity. `, Example: "newrelic entity tags get --guid ", + PreRun: client.RequireClient, 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.GetTagsForEntityWithContext(utils.SignalCtx, entities.EntityGUID(value[0])) - utils.LogIfFatal(err) - utils.LogIfError(output.Print(tags)) - } else { - tags, err := nrClient.Entities.GetTagsForEntityWithContext(utils.SignalCtx, 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.NRClient.Entities.GetTagsForEntityWithContext(utils.SignalCtx, entities.EntityGUID(value[0])) + utils.LogIfFatal(err) + utils.LogIfError(output.Print(tags)) + } else { + tags, err := client.NRClient.Entities.GetTagsForEntityWithContext(utils.SignalCtx, entities.EntityGUID(entityGUID)) + utils.LogIfFatal(err) + utils.LogIfError(output.Print(tags)) + } }, } @@ -64,13 +62,12 @@ 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: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - _, err := nrClient.Entities.TaggingDeleteTagFromEntityWithContext(utils.SignalCtx, entities.EntityGUID(entityGUID), entityTags) - utils.LogIfFatal(err) + _, err := client.NRClient.Entities.TaggingDeleteTagFromEntityWithContext(utils.SignalCtx, entities.EntityGUID(entityGUID), entityTags) + utils.LogIfFatal(err) - log.Info("success") - }) + log.Info("success") }, } @@ -82,16 +79,15 @@ 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: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - tagValues, err := assembleTagValuesInput(entityValues) - utils.LogIfFatal(err) + tagValues, err := assembleTagValuesInput(entityValues) + utils.LogIfFatal(err) - _, err = nrClient.Entities.TaggingDeleteTagValuesFromEntityWithContext(utils.SignalCtx, entities.EntityGUID(entityGUID), tagValues) - utils.LogIfFatal(err) + _, err = client.NRClient.Entities.TaggingDeleteTagValuesFromEntityWithContext(utils.SignalCtx, entities.EntityGUID(entityGUID), tagValues) + utils.LogIfFatal(err) - log.Info("success") - }) + log.Info("success") }, } @@ -103,16 +99,15 @@ 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: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - tags, err := assembleTagsInput(entityTags) - utils.LogIfFatal(err) + tags, err := assembleTagsInput(entityTags) + utils.LogIfFatal(err) - _, err = nrClient.Entities.TaggingAddTagsToEntityWithContext(utils.SignalCtx, entities.EntityGUID(entityGUID), tags) - utils.LogIfFatal(err) + _, err = client.NRClient.Entities.TaggingAddTagsToEntityWithContext(utils.SignalCtx, entities.EntityGUID(entityGUID), tags) + utils.LogIfFatal(err) - log.Info("success") - }) + log.Info("success") }, } @@ -125,16 +120,15 @@ 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: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - tags, err := assembleTagsInput(entityTags) - utils.LogIfFatal(err) + tags, err := assembleTagsInput(entityTags) + utils.LogIfFatal(err) - _, err = nrClient.Entities.TaggingReplaceTagsOnEntityWithContext(utils.SignalCtx, entities.EntityGUID(entityGUID), tags) - utils.LogIfFatal(err) + _, err = client.NRClient.Entities.TaggingReplaceTagsOnEntityWithContext(utils.SignalCtx, entities.EntityGUID(entityGUID), tags) + utils.LogIfFatal(err) - log.Info("success") - }) + log.Info("success") }, } @@ -165,23 +159,6 @@ func assembleTagsInput(tags []string) ([]entities.TaggingTagInput, error) { return t, nil } -func assembleTagValues(values []string) ([]entities.EntitySearchQueryBuilderTag, error) { - var tagValues []entities.EntitySearchQueryBuilderTag - - for _, x := range values { - key, value, err := assembleTagValue(x) - - if err != nil { - return []entities.EntitySearchQueryBuilderTag{}, err - } - - tagValues = append(tagValues, entities.EntitySearchQueryBuilderTag{Key: key, Value: value}) - } - - return tagValues, nil -} - -// assembleTagValuesInput is the same as assembleTagValues func assembleTagValuesInput(values []string) ([]entities.TaggingTagValueInput, error) { var tagValues []entities.TaggingTagValueInput diff --git a/internal/entities/command_tags_test.go b/internal/entities/command_tags_test.go index bc4b78a4d..de73fdf40 100644 --- a/internal/entities/command_tags_test.go +++ b/internal/entities/command_tags_test.go @@ -146,6 +146,22 @@ func TestEntitiesAssembleTagValues(t *testing.T) { } } +func assembleTagValues(values []string) ([]entities.EntitySearchQueryBuilderTag, error) { + var tagValues []entities.EntitySearchQueryBuilderTag + + for _, x := range values { + key, value, err := assembleTagValue(x) + + if err != nil { + return []entities.EntitySearchQueryBuilderTag{}, err + } + + tagValues = append(tagValues, entities.EntitySearchQueryBuilderTag{Key: key, Value: value}) + } + + return tagValues, nil +} + func TestEntitiesAssembleTagValuesInput(t *testing.T) { var scenarios = []struct { tags []string diff --git a/internal/events/command_post.go b/internal/events/command_post.go index 2a3c66c38..545d9220a 100644 --- a/internal/events/command_post.go +++ b/internal/events/command_post.go @@ -7,14 +7,13 @@ 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" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" ) var ( - accountID int - event string + event string ) var cmdPost = &cobra.Command{ @@ -29,32 +28,30 @@ 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: client.RequireClient, 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") - } + accountID := configAPI.RequireActiveProfileAccountID() + if configAPI.GetActiveProfileString(config.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) - } + err := json.Unmarshal([]byte(event), &e) + if err != nil { + log.Fatal(err) + } - if err := nrClient.Events.CreateEventWithContext(utils.SignalCtx, accountID, event); err != nil { - log.Fatal(err) - } + if err := client.NRClient.Events.CreateEventWithContext(utils.SignalCtx, 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")) } diff --git a/internal/install/command.go b/internal/install/command.go index 65cf4d777..b59ce936b 100644 --- a/internal/install/command.go +++ b/internal/install/command.go @@ -1,16 +1,17 @@ package install import ( - "errors" + "fmt" + "os" 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" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "github.com/newrelic/newrelic-cli/internal/install/types" - "github.com/newrelic/newrelic-client-go/newrelic" + "github.com/newrelic/newrelic-cli/internal/utils" ) var ( @@ -23,14 +24,13 @@ var ( skipApm bool skipInfra bool testMode bool - debug bool - trace bool ) // Command represents the install command. var Command = &cobra.Command{ - Use: "install", - Short: "Install New Relic.", + Use: "install", + Short: "Install New Relic.", + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { ic := types.InstallerContext{ AssumeYes: assumeYes, @@ -43,40 +43,66 @@ var Command = &cobra.Command{ SkipInfraInstall: skipInfra, } - config.InitFileLogger() + err := assertProfileIsValid() + if err != nil { + log.Fatal(err) + } - 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") - } + i := NewRecipeInstaller(ic, client.NRClient) - err := assertProfileIsValid(profile) - if err != nil { - log.Fatal(err) + // Run the install. + if err := i.Install(); err != nil { + if err == types.ErrInterrupt { + return } - i := NewRecipeInstaller(ic, nrClient) - - // Run the install. - if err := i.Install(); err != nil { - if err == types.ErrInterrupt { - return - } - - log.Fatalf("We encountered an error during the installation: %s. If this problem persists please visit the documentation and support page for additional help here: https://one.newrelic.com/-/06vjAeZLKjP", err) - } - }) + log.Fatalf("We encountered an error during the installation: %s. If this problem persists please visit the documentation and support page for additional help here: https://one.newrelic.com/-/06vjAeZLKjP", err) + } }, } -func assertProfileIsValid(profile *credentials.Profile) error { - if profile == nil { - return errors.New("default profile has not been set") +func assertProfileIsValid() error { + accountID := configAPI.GetActiveProfileAccountID() + if accountID == 0 { + return fmt.Errorf("accountID is required") + } + + if configAPI.GetActiveProfileString(config.APIKey) == "" { + return fmt.Errorf("API key is required") + } + + if configAPI.GetActiveProfileString(config.Region) == "" { + return fmt.Errorf("region is required") } + + licenseKey, err := client.FetchLicenseKey(accountID, config.FlagProfileName) + if err != nil { + return fmt.Errorf("could not fetch license key for account %d: %s", accountID, err) + } + if licenseKey != configAPI.GetActiveProfileString(config.LicenseKey) { + os.Setenv("NEW_RELIC_LICENSE_KEY", licenseKey) + log.Debugf("using license key %s", utils.Obfuscate(licenseKey)) + } + + insightsInsertKey, err := client.FetchInsightsInsertKey(accountID, config.FlagProfileName) + if err != nil { + return fmt.Errorf("could not fetch Insights insert key key for account %d: %s", accountID, err) + } + if insightsInsertKey != configAPI.GetActiveProfileString(config.InsightsInsertKey) { + os.Setenv("NEW_RELIC_INSIGHTS_INSERT_KEY", insightsInsertKey) + log.Debugf("using Insights insert key %s", utils.Obfuscate(insightsInsertKey)) + } + + // Reinitialize client, overriding fetched values + c, err := client.NewClient(configAPI.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) + } + + client.NRClient = c + return nil } @@ -85,11 +111,9 @@ func init() { Command.Flags().StringSliceVarP(&recipeNames, "recipe", "n", []string{}, "the name of a recipe to install") 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(&skipApm, "skipApm", "a", false, "skips installation for APM") + Command.Flags().BoolVarP(&skipApm, "skipApm", "s", false, "skips installation for APM") Command.Flags().BoolVarP(&skipInfra, "skipInfra", "i", false, "skips installation for infrastructure agent (only for targeted install)") 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().StringVarP(&localRecipes, "localRecipes", "", "", "a path to local recipes to load instead of service other fetching") } diff --git a/internal/install/execution/install_status.go b/internal/install/execution/install_status.go index e37112ae7..a91a14d8e 100644 --- a/internal/install/execution/install_status.go +++ b/internal/install/execution/install_status.go @@ -116,7 +116,7 @@ func NewInstallStatus(reporters []StatusSubscriber, PlatformLinkGenerator LinkGe s := InstallStatus{ DocumentID: uuid.New().String(), Timestamp: utils.GetTimestamp(), - LogFilePath: config.DefaultConfigDirectory + "/" + config.DefaultLogFile, + LogFilePath: config.BasePath + "/" + config.DefaultLogFile, statusSubscriber: reporters, PlatformLinkGenerator: PlatformLinkGenerator, HTTPSProxy: httpproxy.FromEnvironment().HTTPSProxy, diff --git a/internal/install/execution/nerdstorage_status_reporter.go b/internal/install/execution/nerdstorage_status_reporter.go index 19ca4e6b7..68fc27e3a 100644 --- a/internal/install/execution/nerdstorage_status_reporter.go +++ b/internal/install/execution/nerdstorage_status_reporter.go @@ -3,7 +3,7 @@ package execution import ( log "github.com/sirupsen/logrus" - "github.com/newrelic/newrelic-cli/internal/credentials" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "github.com/newrelic/newrelic-cli/internal/install/types" "github.com/newrelic/newrelic-client-go/pkg/nerdstorage" ) @@ -120,8 +120,8 @@ func (r NerdstorageStatusReporter) writeStatus(status *InstallStatus) error { log.Debug("no entity GUIDs available, skipping entity-scoped status updates") } - defaultProfile := credentials.DefaultProfile() - _, err = r.client.WriteDocumentWithAccountScope(defaultProfile.AccountID, i) + accountID := configAPI.GetActiveProfileAccountID() + _, err = r.client.WriteDocumentWithAccountScope(accountID, i) if err != nil { log.Debug("failed to write to account scoped nerd storage") } diff --git a/internal/install/execution/nerdstorage_status_reporter_unit_test.go b/internal/install/execution/nerdstorage_status_reporter_unit_test.go index 0566536d5..fd64001c6 100644 --- a/internal/install/execution/nerdstorage_status_reporter_unit_test.go +++ b/internal/install/execution/nerdstorage_status_reporter_unit_test.go @@ -8,12 +8,10 @@ import ( "github.com/stretchr/testify/require" - "github.com/newrelic/newrelic-cli/internal/credentials" "github.com/newrelic/newrelic-cli/internal/install/types" ) func TestRecipeAvailable_Basic(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) c := NewMockNerdStorageClient() r := NewNerdStorageStatusReporter(c) slg := NewPlatformLinkGenerator() diff --git a/internal/install/execution/platform_link_generator.go b/internal/install/execution/platform_link_generator.go index 30881931e..1922cf9c4 100644 --- a/internal/install/execution/platform_link_generator.go +++ b/internal/install/execution/platform_link_generator.go @@ -4,7 +4,8 @@ import ( "fmt" "strings" - "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/config" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "github.com/newrelic/newrelic-cli/internal/utils" "github.com/newrelic/newrelic-client-go/pkg/region" ) @@ -54,7 +55,7 @@ func generateExplorerLink(filter string) string { return fmt.Sprintf("https://%s/launcher/nr1-core.explorer?platform[filters]=%s&platform[accountId]=%d", nrPlatformHostname(), utils.Base64Encode(filter), - credentials.DefaultProfile().AccountID, + configAPI.GetActiveProfileAccountID(), ) } @@ -64,16 +65,12 @@ func generateEntityLink(entityGUID string) string { // nrPlatformHostname returns the host for the platform based on the region set. func nrPlatformHostname() string { - defaultProfile := credentials.DefaultProfile() - if defaultProfile == nil { - return nrPlatformHostnames.US - } - - if strings.EqualFold(defaultProfile.Region, region.Staging.String()) { + r := configAPI.GetActiveProfileString(config.Region) + if strings.EqualFold(r, region.Staging.String()) { return nrPlatformHostnames.Staging } - if strings.EqualFold(defaultProfile.Region, region.EU.String()) { + if strings.EqualFold(r, region.EU.String()) { return nrPlatformHostnames.EU } diff --git a/internal/install/execution/recipe_var_provider.go b/internal/install/execution/recipe_var_provider.go index 7fc5367e1..75e8039c2 100644 --- a/internal/install/execution/recipe_var_provider.go +++ b/internal/install/execution/recipe_var_provider.go @@ -6,13 +6,13 @@ import ( "net/url" "os" "regexp" - "strconv" survey "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" log "github.com/sirupsen/logrus" - "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/config" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "github.com/newrelic/newrelic-cli/internal/install/types" ) @@ -68,7 +68,9 @@ func (re *RecipeVarProvider) Prepare(m types.DiscoveryManifest, r types.OpenInst } func varsFromProfile(licenseKey string) (types.RecipeVars, error) { - defaultProfile := credentials.DefaultProfile() + accountID := configAPI.GetActiveProfileString(config.AccountID) + apiKey := configAPI.GetActiveProfileString(config.APIKey) + region := configAPI.GetActiveProfileString(config.Region) if licenseKey == "" { return types.RecipeVars{}, errors.New("license key not found") @@ -77,9 +79,9 @@ func varsFromProfile(licenseKey string) (types.RecipeVars, error) { vars := make(types.RecipeVars) vars["NEW_RELIC_LICENSE_KEY"] = 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_ACCOUNT_ID"] = accountID + vars["NEW_RELIC_API_KEY"] = apiKey + vars["NEW_RELIC_REGION"] = region return vars, nil } diff --git a/internal/install/execution/recipe_var_provider_test.go b/internal/install/execution/recipe_var_provider_test.go index 6d83597ed..b78795ed8 100644 --- a/internal/install/execution/recipe_var_provider_test.go +++ b/internal/install/execution/recipe_var_provider_test.go @@ -14,19 +14,12 @@ import ( "github.com/go-task/task/v3/taskfile" "github.com/stretchr/testify/require" - "github.com/newrelic/newrelic-cli/internal/credentials" "github.com/newrelic/newrelic-cli/internal/install/types" ) func TestRecipeVarProvider_Basic(t *testing.T) { e := NewRecipeVarProvider() - licenseKey := "testLicenseKey" - p := credentials.Profile{ - LicenseKey: "", - } - credentials.SetDefaultProfile(p) - m := types.DiscoveryManifest{ Hostname: "testHostname", OS: "testOS", @@ -80,7 +73,7 @@ func TestRecipeVarProvider_Basic(t *testing.T) { Install: installYaml, } - v, err := e.Prepare(m, r, false, licenseKey) + v, err := e.Prepare(m, r, false, "testLicenseKey") require.NoError(t, err) require.Contains(t, m.OS, v["OS"]) require.Contains(t, m.Platform, v["Platform"]) @@ -94,12 +87,6 @@ func TestRecipeVarProvider_Basic(t *testing.T) { func TestRecipeVarProvider_OverrideDownloadURL(t *testing.T) { e := NewRecipeVarProvider() - licenseKey := "testLicenseKey" - p := credentials.Profile{ - LicenseKey: "", - } - credentials.SetDefaultProfile(p) - m := types.DiscoveryManifest{ Hostname: "testHostname", OS: "testOS", @@ -156,7 +143,7 @@ func TestRecipeVarProvider_OverrideDownloadURL(t *testing.T) { // Test for NEW_RELIC_DOWNLOAD_URL os.Setenv("NEW_RELIC_DOWNLOAD_URL", "https://another.download.newrelic.com/") - v, err := e.Prepare(m, r, false, licenseKey) + v, err := e.Prepare(m, r, false, "testLicenseKey") require.NoError(t, err) require.Contains(t, "https://another.download.newrelic.com/", v["NEW_RELIC_DOWNLOAD_URL"]) } @@ -164,12 +151,6 @@ func TestRecipeVarProvider_OverrideDownloadURL(t *testing.T) { func TestRecipeVarProvider_AllowInfraStaging(t *testing.T) { e := NewRecipeVarProvider() - licenseKey := "testLicenseKey" - p := credentials.Profile{ - LicenseKey: "", - } - credentials.SetDefaultProfile(p) - m := types.DiscoveryManifest{ Hostname: "testHostname", OS: "testOS", @@ -226,7 +207,7 @@ func TestRecipeVarProvider_AllowInfraStaging(t *testing.T) { // Test for NEW_RELIC_DOWNLOAD_URL os.Setenv("NEW_RELIC_DOWNLOAD_URL", "https://nr-downloads-ohai-staging.s3.amazonaws.com/") - v, err := e.Prepare(m, r, false, licenseKey) + v, err := e.Prepare(m, r, false, "testLicenseKey") require.NoError(t, err) require.Contains(t, "https://nr-downloads-ohai-staging.s3.amazonaws.com/", v["NEW_RELIC_DOWNLOAD_URL"]) } @@ -234,12 +215,6 @@ func TestRecipeVarProvider_AllowInfraStaging(t *testing.T) { func TestRecipeVarProvider_AllowInfraTesting(t *testing.T) { e := NewRecipeVarProvider() - licenseKey := "testLicenseKey" - p := credentials.Profile{ - LicenseKey: "", - } - credentials.SetDefaultProfile(p) - m := types.DiscoveryManifest{ Hostname: "testHostname", OS: "testOS", @@ -296,7 +271,7 @@ func TestRecipeVarProvider_AllowInfraTesting(t *testing.T) { // Test for NEW_RELIC_DOWNLOAD_URL os.Setenv("NEW_RELIC_DOWNLOAD_URL", "https://nr-downloads-ohai-testing.s3.amazonaws.com/") - v, err := e.Prepare(m, r, false, licenseKey) + v, err := e.Prepare(m, r, false, "testLicenseKey") require.NoError(t, err) require.Contains(t, "https://nr-downloads-ohai-testing.s3.amazonaws.com/", v["NEW_RELIC_DOWNLOAD_URL"]) } @@ -304,12 +279,6 @@ func TestRecipeVarProvider_AllowInfraTesting(t *testing.T) { func TestRecipeVarProvider_DisallowUnknownInfraTesting(t *testing.T) { e := NewRecipeVarProvider() - licenseKey := "testLicenseKey" - p := credentials.Profile{ - LicenseKey: "", - } - credentials.SetDefaultProfile(p) - m := types.DiscoveryManifest{ Hostname: "testHostname", OS: "testOS", @@ -366,7 +335,7 @@ func TestRecipeVarProvider_DisallowUnknownInfraTesting(t *testing.T) { // Test for NEW_RELIC_DOWNLOAD_URL os.Setenv("NEW_RELIC_DOWNLOAD_URL", "https://nr-downloads-ohai-unknown.s3.amazonaws.com/") - v, err := e.Prepare(m, r, false, licenseKey) + v, err := e.Prepare(m, r, false, "testLicenseKey") require.NoError(t, err) require.Contains(t, "https://download.newrelic.com/", v["NEW_RELIC_DOWNLOAD_URL"]) } @@ -374,12 +343,6 @@ func TestRecipeVarProvider_DisallowUnknownInfraTesting(t *testing.T) { func TestRecipeVarProvider_OverrideDownloadURL_RefusedNotHttps(t *testing.T) { e := NewRecipeVarProvider() - licenseKey := "testLicenseKey" - p := credentials.Profile{ - LicenseKey: "", - } - credentials.SetDefaultProfile(p) - m := types.DiscoveryManifest{ Hostname: "testHostname", OS: "testOS", @@ -436,7 +399,7 @@ func TestRecipeVarProvider_OverrideDownloadURL_RefusedNotHttps(t *testing.T) { // Test for NEW_RELIC_DOWNLOAD_URL os.Setenv("NEW_RELIC_DOWNLOAD_URL", "http://another.download.newrelic.com/") - v, err := e.Prepare(m, r, false, licenseKey) + v, err := e.Prepare(m, r, false, "testLicenseKey") require.NoError(t, err) require.Contains(t, "https://download.newrelic.com/", v["NEW_RELIC_DOWNLOAD_URL"]) } @@ -444,12 +407,6 @@ func TestRecipeVarProvider_OverrideDownloadURL_RefusedNotHttps(t *testing.T) { func TestRecipeVarProvider_OverrideDownloadURL_RefusedNotNewRelic(t *testing.T) { e := NewRecipeVarProvider() - licenseKey := "testLicenseKey" - p := credentials.Profile{ - LicenseKey: "", - } - credentials.SetDefaultProfile(p) - m := types.DiscoveryManifest{ Hostname: "testHostname", OS: "testOS", @@ -506,7 +463,7 @@ func TestRecipeVarProvider_OverrideDownloadURL_RefusedNotNewRelic(t *testing.T) // Test for NEW_RELIC_DOWNLOAD_URL os.Setenv("NEW_RELIC_DOWNLOAD_URL", "http://github.com/") - v, err := e.Prepare(m, r, false, licenseKey) + v, err := e.Prepare(m, r, false, "testLicenseKey") require.NoError(t, err) require.Contains(t, "https://download.newrelic.com/", v["NEW_RELIC_DOWNLOAD_URL"]) } diff --git a/internal/install/license_key_fetcher.go b/internal/install/license_key_fetcher.go index 0124e19c5..3a553ad8d 100644 --- a/internal/install/license_key_fetcher.go +++ b/internal/install/license_key_fetcher.go @@ -2,11 +2,11 @@ package install import ( "context" - "strconv" log "github.com/sirupsen/logrus" - "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/client" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "github.com/newrelic/newrelic-cli/internal/install/recipes" ) @@ -28,38 +28,12 @@ func NewServiceLicenseKeyFetcher(client recipes.NerdGraphClient) LicenseKeyFetch } func (f *ServiceLicenseKeyFetcher) FetchLicenseKey(ctx context.Context) (string, error) { - var resp licenseKeyDataQueryResult - - vars := map[string]interface{}{} - - defaultProfile := credentials.DefaultProfile() - - query := ` - query{ - actor { - account(id: ` + strconv.Itoa(defaultProfile.AccountID) + `) { - licenseKey - } - } - }` - - if err := f.client.QueryWithResponseAndContext(ctx, query, vars, &resp); err != nil { + accountID := configAPI.GetActiveProfileAccountID() + licenseKey, err := client.FetchLicenseKey(accountID, configAPI.GetActiveProfileName()) + if err != nil { return "", err } - licenseKey := resp.Actor.Account.LicenseKey log.Debugf("Found license key %s", licenseKey) return licenseKey, nil } - -type licenseKeyDataQueryResult struct { - Actor licenseKeyActorQueryResult `json:"actor"` -} - -type licenseKeyActorQueryResult struct { - Account licenseKeyAccountQueryResult `json:"account"` -} - -type licenseKeyAccountQueryResult struct { - LicenseKey string `json:"licenseKey"` -} diff --git a/internal/install/packs/service_packs_installer.go b/internal/install/packs/service_packs_installer.go index 3726540d2..15eca5a25 100644 --- a/internal/install/packs/service_packs_installer.go +++ b/internal/install/packs/service_packs_installer.go @@ -10,7 +10,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/newrelic/newrelic-cli/internal/credentials" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "github.com/newrelic/newrelic-cli/internal/install/execution" "github.com/newrelic/newrelic-cli/internal/install/types" "github.com/newrelic/newrelic-cli/internal/install/ux" @@ -69,15 +69,14 @@ func (p *ServicePacksInstaller) Install(ctx context.Context, packs []types.OpenI } func (p *ServicePacksInstaller) createObservabilityPackDashboard(ctx context.Context, d types.OpenInstallationObservabilityPackDashboard) (*dashboards.DashboardCreateResult, error) { - defaultProfile := credentials.DefaultProfile() - accountID := defaultProfile.AccountID + accountID := configAPI.GetActiveProfileAccountID() body, err := getJSONfromURL(d.URL) if err != nil { return nil, err } - dashboard, err := transformDashboardJSON(body, defaultProfile.AccountID) + dashboard, err := transformDashboardJSON(body, accountID) if err != nil { return nil, err } diff --git a/internal/install/recipe_installer.go b/internal/install/recipe_installer.go index ff0bec2db..c032cb32f 100644 --- a/internal/install/recipe_installer.go +++ b/internal/install/recipe_installer.go @@ -142,10 +142,10 @@ func (i *RecipeInstaller) Install() error { errChan := make(chan error) var err error - // log.Printf("Validating connectivity to the New Relic platform...") - // if err = i.configValidator.Validate(utils.SignalCtx); err != nil { - // return err - // } + log.Printf("Validating connectivity to the New Relic platform...") + if err = i.configValidator.Validate(utils.SignalCtx); err != nil { + return err + } go func(ctx context.Context) { errChan <- i.install(ctx) diff --git a/internal/install/recipe_installer_test.go b/internal/install/recipe_installer_test.go index 982c6e03f..b4dc722b6 100644 --- a/internal/install/recipe_installer_test.go +++ b/internal/install/recipe_installer_test.go @@ -13,7 +13,6 @@ import ( log "github.com/sirupsen/logrus" - "github.com/newrelic/newrelic-cli/internal/credentials" "github.com/newrelic/newrelic-cli/internal/diagnose" "github.com/newrelic/newrelic-cli/internal/install/discovery" "github.com/newrelic/newrelic-cli/internal/install/execution" @@ -91,7 +90,7 @@ func TestShouldGetRecipeFromFile(t *testing.T) { } func TestInstall_DiscoveryComplete(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") ic := types.InstallerContext{} statusReporter := execution.NewMockStatusReporter() statusReporters = []execution.StatusSubscriber{statusReporter} @@ -272,7 +271,7 @@ func TestInstall_RecipeInstalled(t *testing.T) { } func TestInstall_RecipeFailed(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") ic := types.InstallerContext{} statusReporters = []execution.StatusSubscriber{execution.NewMockStatusReporter()} @@ -305,7 +304,7 @@ func TestInstall_RecipeFailed(t *testing.T) { } func TestInstall_NonInfraRecipeFailed(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") ic := types.InstallerContext{} statusReporters = []execution.StatusSubscriber{execution.NewMockStatusReporter()} @@ -337,7 +336,7 @@ func TestInstall_NonInfraRecipeFailed(t *testing.T) { } func TestInstall_AllRecipesFailed(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") ic := types.InstallerContext{} statusReporters = []execution.StatusSubscriber{execution.NewMockStatusReporter()} @@ -568,7 +567,7 @@ func TestInstall_RecipeSkippedApmAnyKeyword(t *testing.T) { } func TestInstall_RecipeSkipped_SkipIntegrations(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") log.SetLevel(log.TraceLevel) ic := types.InstallerContext{ AssumeYes: true, @@ -636,7 +635,7 @@ func TestInstall_RecipeSkipped_MultiSelect(t *testing.T) { } func TestInstall_RecipeRecommended(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") ic := types.InstallerContext{ SkipLoggingInstall: true, } @@ -758,7 +757,7 @@ func TestInstall_TargetedInstall_InstallsInfraAgent(t *testing.T) { } func TestInstall_TargetedInstall_FilterAllButProvided(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") ic := types.InstallerContext{ RecipeNames: []string{testRecipeName}, } @@ -787,7 +786,7 @@ func TestInstall_TargetedInstall_FilterAllButProvided(t *testing.T) { } func TestInstall_TargetedInstall_InstallsInfraAgentDependency(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") ic := types.InstallerContext{ RecipeNames: []string{testRecipeName}, } @@ -815,7 +814,7 @@ func TestInstall_TargetedInstall_InstallsInfraAgentDependency(t *testing.T) { } func TestInstall_TargetedInstallInfraAgent_NoInfraAgentDuplicate(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") ic := types.InstallerContext{ RecipeNames: []string{types.InfraAgentRecipeName}, } @@ -838,7 +837,7 @@ func TestInstall_TargetedInstallInfraAgent_NoInfraAgentDuplicate(t *testing.T) { } func TestInstall_TargetedInstall_SkipInfra(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") ic := types.InstallerContext{ SkipInfraInstall: true, RecipeNames: []string{types.InfraAgentRecipeName, testRecipeName}, @@ -867,7 +866,7 @@ func TestInstall_TargetedInstall_SkipInfra(t *testing.T) { } func TestInstall_TargetedInstall_SkipInfraDependency(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") ic := types.InstallerContext{ SkipInfraInstall: true, } diff --git a/internal/install/recipes/local_recipe_fetcher_test.go b/internal/install/recipes/local_recipe_fetcher_test.go index 141036c88..39eb7c009 100644 --- a/internal/install/recipes/local_recipe_fetcher_test.go +++ b/internal/install/recipes/local_recipe_fetcher_test.go @@ -19,7 +19,7 @@ func TestLocalRecipeFetcher_FetchRecipes_EmptyManifest(t *testing.T) { defer os.RemoveAll(tmp) require.NoError(t, err) - config.DefaultConfigDirectory = tmp + config.Init(tmp) path := filepath.Join(tmp, "recipes") err = os.MkdirAll(path, 0750) @@ -40,7 +40,7 @@ func TestLocalRecipeFetcher_FetchRecipes(t *testing.T) { defer os.RemoveAll(tmp) require.NoError(t, err) - config.DefaultConfigDirectory = tmp + config.Init(tmp) path := filepath.Join(tmp, "recipes") diff --git a/internal/install/scenario_builder.go b/internal/install/scenario_builder.go deleted file mode 100644 index 78fb213c9..000000000 --- a/internal/install/scenario_builder.go +++ /dev/null @@ -1,279 +0,0 @@ -package install - -import ( - "github.com/newrelic/newrelic-cli/internal/diagnose" - "github.com/newrelic/newrelic-cli/internal/install/discovery" - "github.com/newrelic/newrelic-cli/internal/install/execution" - "github.com/newrelic/newrelic-cli/internal/install/recipes" - "github.com/newrelic/newrelic-cli/internal/install/types" - "github.com/newrelic/newrelic-cli/internal/install/ux" - "github.com/newrelic/newrelic-cli/internal/install/validation" - "github.com/newrelic/newrelic-client-go/pkg/nrdb" -) - -type TestScenario string - -const ( - Basic TestScenario = "BASIC" - Fail TestScenario = "FAIL" - ExecDiscovery TestScenario = "EXEC_DISCOVERY" -) - -var ( - TestScenarios = []TestScenario{ - Basic, - Fail, - } - emptyResults = []nrdb.NRDBResult{ - map[string]interface{}{ - "count": 0.0, - }, - } - nonEmptyResults = []nrdb.NRDBResult{ - map[string]interface{}{ - "count": 1.0, - }, - } -) - -func TestScenarioValues() []string { - v := make([]string, len(TestScenarios)) - for i, s := range TestScenarios { - v[i] = string(s) - } - - return v -} - -type ScenarioBuilder struct { - installerContext types.InstallerContext -} - -func NewScenarioBuilder(ic types.InstallerContext) *ScenarioBuilder { - b := ScenarioBuilder{ - installerContext: ic, - } - - return &b -} - -func (b *ScenarioBuilder) BuildScenario(s TestScenario) *RecipeInstaller { - switch s { - case Basic: - return b.Basic() - case Fail: - return b.Fail() - case ExecDiscovery: - return b.ExecDiscovery() - } - - return nil -} - -func (b *ScenarioBuilder) Basic() *RecipeInstaller { - - // mock implementations - rf := setupRecipeFetcherGuidedInstall() - ers := []execution.StatusSubscriber{ - execution.NewMockStatusReporter(), - execution.NewTerminalStatusReporter(), - } - slg := execution.NewPlatformLinkGenerator() - statusRollup := execution.NewInstallStatus(ers, slg) - c := validation.NewMockNRDBClient() - c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 2) - v := validation.NewPollingRecipeValidator(c) - cv := diagnose.NewMockConfigValidator() - mv := discovery.NewEmptyManifestValidator() - lkf := NewMockLicenseKeyFetcher() - ff := recipes.NewRecipeFileFetcher() - d := discovery.NewPSUtilDiscoverer() - gff := discovery.NewGlobFileFilterer() - re := execution.NewGoTaskRecipeExecutor() - p := ux.NewPromptUIPrompter() - s := ux.NewPlainProgress() - rfi := recipes.NewRecipeFilterRunner(b.installerContext, statusRollup) - rvp := execution.NewRecipeVarProvider() - - i := RecipeInstaller{ - discoverer: d, - fileFilterer: gff, - recipeFetcher: rf, - recipeExecutor: re, - recipeValidator: v, - recipeFileFetcher: ff, - status: statusRollup, - prompter: p, - progressIndicator: s, - configValidator: cv, - manifestValidator: mv, - licenseKeyFetcher: lkf, - recipeFilterer: rfi, - recipeVarPreparer: rvp, - } - - i.InstallerContext = b.installerContext - - return &i -} - -func (b *ScenarioBuilder) Fail() *RecipeInstaller { - - // mock implementations - rf := setupRecipeFetcherGuidedInstall() - ers := []execution.StatusSubscriber{ - execution.NewMockStatusReporter(), - execution.NewTerminalStatusReporter(), - } - slg := execution.NewPlatformLinkGenerator() - statusRollup := execution.NewInstallStatus(ers, slg) - c := validation.NewMockNRDBClient() - c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 2) - v := validation.NewPollingRecipeValidator(c) - cv := diagnose.NewMockConfigValidator() - mv := discovery.NewEmptyManifestValidator() - - lkf := NewMockLicenseKeyFetcher() - ff := recipes.NewRecipeFileFetcher() - d := discovery.NewPSUtilDiscoverer() - gff := discovery.NewGlobFileFilterer() - re := execution.NewMockFailingRecipeExecutor() - p := ux.NewPromptUIPrompter() - pi := ux.NewPlainProgress() - - i := RecipeInstaller{ - discoverer: d, - fileFilterer: gff, - recipeFetcher: rf, - recipeExecutor: re, - recipeValidator: v, - recipeFileFetcher: ff, - status: statusRollup, - prompter: p, - progressIndicator: pi, - configValidator: cv, - manifestValidator: mv, - licenseKeyFetcher: lkf, - } - - i.InstallerContext = b.installerContext - - return &i -} - -func (b *ScenarioBuilder) ExecDiscovery() *RecipeInstaller { - - // mock implementations - rf := setupRecipeFetcherExecDiscovery() - ers := []execution.StatusSubscriber{ - execution.NewMockStatusReporter(), - execution.NewTerminalStatusReporter(), - } - slg := execution.NewPlatformLinkGenerator() - statusRollup := execution.NewInstallStatus(ers, slg) - c := validation.NewMockNRDBClient() - c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 2) - v := validation.NewPollingRecipeValidator(c) - cv := diagnose.NewMockConfigValidator() - mv := discovery.NewEmptyManifestValidator() - - lkf := NewMockLicenseKeyFetcher() - ff := recipes.NewRecipeFileFetcher() - d := discovery.NewPSUtilDiscoverer() - gff := discovery.NewGlobFileFilterer() - re := execution.NewMockFailingRecipeExecutor() - p := ux.NewPromptUIPrompter() - pi := ux.NewPlainProgress() - rvp := execution.NewRecipeVarProvider() - - rr := recipes.NewRecipeFilterRunner(b.installerContext, statusRollup) - - i := RecipeInstaller{ - discoverer: d, - fileFilterer: gff, - recipeFetcher: rf, - recipeExecutor: re, - recipeValidator: v, - recipeFileFetcher: ff, - status: statusRollup, - prompter: p, - progressIndicator: pi, - configValidator: cv, - manifestValidator: mv, - licenseKeyFetcher: lkf, - recipeVarPreparer: rvp, - recipeFilterer: rr, - } - - i.InstallerContext = b.installerContext - - return &i -} - -func setupRecipeFetcherGuidedInstall() recipes.RecipeFetcher { - f := recipes.NewMockRecipeFetcher() - f.FetchRecipesVal = []types.OpenInstallationRecipe{ - { - Name: "infrastructure-agent-installer", - DisplayName: "Infrastructure Agent", - PreInstall: types.OpenInstallationPreInstallConfiguration{ - Info: ` -This is the Infrastructure Agent Installer preinstall message. -It is made up of a multi line string. - `, - }, - PostInstall: types.OpenInstallationPostInstallConfiguration{ - Info: ` -This is the Infrastructure Agent Installer postinstall message. -It is made up of a multi line string. - `, - }, - ValidationNRQL: "test NRQL", - Install: ` -version: '3' -tasks: - default: -`, - }, - { - Name: "logs-integration", - DisplayName: "Logs integration", - ValidationNRQL: "test NRQL", - LogMatch: []types.OpenInstallationLogMatch{ - { - Name: "docker log", - File: "/var/lib/docker/containers/*/*.log", - }, - }, - Install: ` -version: '3' -tasks: - default: -`, - }, - } - - return f -} - -func setupRecipeFetcherExecDiscovery() recipes.RecipeFetcher { - f := recipes.NewMockRecipeFetcher() - f.FetchRecipesVal = []types.OpenInstallationRecipe{ - { - Name: "matching-recipe", - DisplayName: "matching-recipe", - PreInstall: types.OpenInstallationPreInstallConfiguration{ - RequireAtDiscovery: "true", - }, - }, - { - Name: "non-matching-recipe", - DisplayName: "non-matching-recipe", - PreInstall: types.OpenInstallationPreInstallConfiguration{ - RequireAtDiscovery: "bogus command", - }, - }, - } - - return f -} diff --git a/internal/install/test_command.go b/internal/install/test_command.go deleted file mode 100644 index be6d61a00..000000000 --- a/internal/install/test_command.go +++ /dev/null @@ -1,65 +0,0 @@ -package install - -import ( - "fmt" - "strings" - - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - - "github.com/newrelic/newrelic-cli/internal/install/types" -) - -var ( - testScenario string -) - -// TestCommand represents the test command for the install command. -var TestCommand = &cobra.Command{ - Use: "installTest", - Short: "Run a UX test of the install command.", - Hidden: true, - Run: func(cmd *cobra.Command, args []string) { - ic := types.InstallerContext{ - RecipePaths: recipePaths, - RecipeNames: recipeNames, - SkipIntegrations: skipIntegrations, - SkipLoggingInstall: skipLoggingInstall, - SkipApm: skipApm, - AssumeYes: assumeYes, - } - - b := NewScenarioBuilder(ic) - i := b.BuildScenario(TestScenario(testScenario)) - - if i == nil { - log.Fatalf("Scenario %s is not valid. Valid values are %s", testScenario, strings.Join(TestScenarioValues(), ",")) - } - - if trace { - log.SetLevel(log.TraceLevel) - } else if debug { - log.SetLevel(log.DebugLevel) - } - - if err := i.Install(); err != nil { - if err == types.ErrInterrupt { - return - } - - log.Fatalf("test failed: %s", err) - } - }, -} - -func init() { - TestCommand.Flags().StringSliceVarP(&recipePaths, "recipePath", "c", []string{}, "the path to a recipe file to install") - TestCommand.Flags().StringSliceVarP(&recipeNames, "recipe", "n", []string{}, "the name of a recipe to install") - TestCommand.Flags().BoolVarP(&skipIntegrations, "skipIntegrations", "r", false, "skips installation of recommended New Relic integrations") - TestCommand.Flags().BoolVarP(&skipLoggingInstall, "skipLoggingInstall", "l", false, "skips installation of New Relic Logging") - TestCommand.Flags().BoolVarP(&skipApm, "skipApm", "a", false, "skips installation for APM") - TestCommand.Flags().StringVarP(&testScenario, "testScenario", "s", string(Basic), fmt.Sprintf("test scenario to run, defaults to BASIC. Valid values are %s", strings.Join(TestScenarioValues(), ","))) - TestCommand.Flags().BoolVar(&debug, "debug", false, "debug level logging") - TestCommand.Flags().BoolVar(&trace, "trace", false, "trace level logging") - TestCommand.Flags().BoolVarP(&assumeYes, "assumeYes", "y", false, "use \"yes\" for all questions during install") -} diff --git a/internal/install/validation/polling_recipe_validator_test.go b/internal/install/validation/polling_recipe_validator_test.go index 838afaaec..dc76d57fa 100644 --- a/internal/install/validation/polling_recipe_validator_test.go +++ b/internal/install/validation/polling_recipe_validator_test.go @@ -4,12 +4,12 @@ package validation import ( "context" + "os" "testing" "time" "github.com/stretchr/testify/require" - "github.com/newrelic/newrelic-cli/internal/credentials" "github.com/newrelic/newrelic-cli/internal/install/types" "github.com/newrelic/newrelic-cli/internal/install/ux" "github.com/newrelic/newrelic-client-go/pkg/nrdb" @@ -33,7 +33,7 @@ var ( ) func TestValidate(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") c := NewMockNRDBClient() c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 1) @@ -51,7 +51,7 @@ func TestValidate(t *testing.T) { } func TestValidate_PassAfterNAttempts(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") c := NewMockNRDBClient() pi := ux.NewMockProgressIndicator() v := NewPollingRecipeValidator(c) @@ -71,7 +71,7 @@ func TestValidate_PassAfterNAttempts(t *testing.T) { } func TestValidate_FailAfterNAttempts(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") c := NewMockNRDBClient() pi := ux.NewMockProgressIndicator() v := NewPollingRecipeValidator(c) @@ -89,7 +89,7 @@ func TestValidate_FailAfterNAttempts(t *testing.T) { } func TestValidate_FailAfterMaxAttempts(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") c := NewMockNRDBClient() c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 2) @@ -109,7 +109,7 @@ func TestValidate_FailAfterMaxAttempts(t *testing.T) { } func TestValidate_FailIfContextDone(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.Setenv("NEW_RELIC_ACCOUNT_ID", "12345") c := NewMockNRDBClient() c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 2) @@ -131,7 +131,7 @@ func TestValidate_FailIfContextDone(t *testing.T) { } func TestValidate_QueryError(t *testing.T) { - credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) + os.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 b5ead6a74..315fe5a0b 100644 --- a/internal/nerdgraph/command_query.go +++ b/internal/nerdgraph/command_query.go @@ -11,7 +11,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" ng "github.com/newrelic/newrelic-client-go/pkg/nerdgraph" ) @@ -42,32 +41,31 @@ keys are the variables to be referenced in the GraphQL query. return nil }, + PreRun: client.RequireClient, 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) - } - - query := args[0] + err := json.Unmarshal([]byte(variables), &variablesParsed) + if err != nil { + log.Fatal(err) + } - result, err := nrClient.NerdGraph.QueryWithContext(utils.SignalCtx, query, variablesParsed) - if err != nil { - log.Fatal(err) - } + query := args[0] - reqBodyBytes := new(bytes.Buffer) + result, err := client.NRClient.NerdGraph.QueryWithContext(utils.SignalCtx, query, variablesParsed) + if err != nil { + log.Fatal(err) + } - encoder := json.NewEncoder(reqBodyBytes) - err = encoder.Encode(ng.QueryResponse{ - Actor: result.(ng.QueryResponse).Actor, - }) - utils.LogIfFatal(err) + reqBodyBytes := new(bytes.Buffer) - utils.LogIfFatal(output.Print(reqBodyBytes)) + encoder := json.NewEncoder(reqBodyBytes) + err = encoder.Encode(ng.QueryResponse{ + Actor: result.(ng.QueryResponse).Actor, }) + utils.LogIfFatal(err) + + utils.LogIfFatal(output.Print(reqBodyBytes)) }, } diff --git a/internal/nerdstorage/command.go b/internal/nerdstorage/command.go index e38df8980..44aa5eb2c 100644 --- a/internal/nerdstorage/command.go +++ b/internal/nerdstorage/command.go @@ -5,7 +5,6 @@ import ( ) var ( - accountID int entityGUID string packageID string collection string diff --git a/internal/nerdstorage/command_collection.go b/internal/nerdstorage/command_collection.go index 9eb56b8a9..227a5a357 100644 --- a/internal/nerdstorage/command_collection.go +++ b/internal/nerdstorage/command_collection.go @@ -7,9 +7,9 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "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" ) @@ -39,33 +39,33 @@ GUID. A valid Nerdpack package ID is required. # User scope newrelic nerdstorage collection get --scope USER --packageId b0dee5a1-e809-4d6f-bd3c-0682cd079612 --collection myCol `, + PreRun: client.RequireClient, 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.GetCollectionWithAccountScopeWithContext(utils.SignalCtx, accountID, input) - case "entity": - resp, err = nrClient.NerdStorage.GetCollectionWithEntityScopeWithContext(utils.SignalCtx, entityGUID, input) - case "user": - resp, err = nrClient.NerdStorage.GetCollectionWithUserScopeWithContext(utils.SignalCtx, 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{} + var err error + + input := nerdstorage.GetCollectionInput{ + PackageID: packageID, + Collection: collection, + } + + switch strings.ToLower(scope) { + case "account": + accountID := configAPI.RequireActiveProfileAccountID() + resp, err = client.NRClient.NerdStorage.GetCollectionWithAccountScopeWithContext(utils.SignalCtx, accountID, input) + case "entity": + resp, err = client.NRClient.NerdStorage.GetCollectionWithEntityScopeWithContext(utils.SignalCtx, entityGUID, input) + case "user": + resp, err = client.NRClient.NerdStorage.GetCollectionWithUserScopeWithContext(utils.SignalCtx, 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") }, } @@ -88,31 +88,31 @@ GUID. A valid Nerdpack package ID is required. # User scope newrelic nerdstorage collection delete --scope USER --packageId b0dee5a1-e809-4d6f-bd3c-0682cd079612 --collection myCol `, + PreRun: client.RequireClient, 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.DeleteCollectionWithAccountScopeWithContext(utils.SignalCtx, accountID, input) - case "entity": - _, err = nrClient.NerdStorage.DeleteCollectionWithEntityScopeWithContext(utils.SignalCtx, entityGUID, input) - case "user": - _, err = nrClient.NerdStorage.DeleteCollectionWithUserScopeWithContext(utils.SignalCtx, input) - default: - log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") - } - if err != nil { - log.Fatal(err) - } - - log.Info("success") - }) + var err error + + input := nerdstorage.DeleteCollectionInput{ + PackageID: packageID, + Collection: collection, + } + + switch strings.ToLower(scope) { + case "account": + accountID := configAPI.RequireActiveProfileAccountID() + _, err = client.NRClient.NerdStorage.DeleteCollectionWithAccountScopeWithContext(utils.SignalCtx, accountID, input) + case "entity": + _, err = client.NRClient.NerdStorage.DeleteCollectionWithEntityScopeWithContext(utils.SignalCtx, entityGUID, input) + case "user": + _, err = client.NRClient.NerdStorage.DeleteCollectionWithUserScopeWithContext(utils.SignalCtx, input) + default: + log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") + } + if err != nil { + log.Fatal(err) + } + + log.Info("success") }, } @@ -120,7 +120,6 @@ 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") @@ -136,7 +135,6 @@ func init() { utils.LogIfError(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") diff --git a/internal/nerdstorage/command_document.go b/internal/nerdstorage/command_document.go index 2113a471b..5b1056ea0 100644 --- a/internal/nerdstorage/command_document.go +++ b/internal/nerdstorage/command_document.go @@ -8,9 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "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,34 +40,34 @@ GUID. A valid Nerdpack package ID is required. # User scope newrelic nerdstorage document get --scope USER --packageId b0dee5a1-e809-4d6f-bd3c-0682cd079612 --collection myCol --documentId myDoc `, + PreRun: client.RequireClient, 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.GetDocumentWithAccountScopeWithContext(utils.SignalCtx, accountID, input) - case "entity": - document, err = nrClient.NerdStorage.GetDocumentWithEntityScopeWithContext(utils.SignalCtx, entityGUID, input) - case "user": - document, err = nrClient.NerdStorage.GetDocumentWithUserScopeWithContext(utils.SignalCtx, 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{} + var err error + + input := nerdstorage.GetDocumentInput{ + PackageID: packageID, + Collection: collection, + DocumentID: documentID, + } + + switch strings.ToLower(scope) { + case "account": + accountID := configAPI.RequireActiveProfileAccountID() + document, err = client.NRClient.NerdStorage.GetDocumentWithAccountScopeWithContext(utils.SignalCtx, accountID, input) + case "entity": + document, err = client.NRClient.NerdStorage.GetDocumentWithEntityScopeWithContext(utils.SignalCtx, entityGUID, input) + case "user": + document, err = client.NRClient.NerdStorage.GetDocumentWithUserScopeWithContext(utils.SignalCtx, 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") }, } @@ -90,37 +90,37 @@ GUID. A valid Nerdpack package ID is required. # User scope newrelic nerdstorage document write --scope USER --packageId b0dee5a1-e809-4d6f-bd3c-0682cd079612 --collection myCol --documentId myDoc --document '{"field": "myValue"}' `, + PreRun: client.RequireClient, 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.WriteDocumentWithAccountScopeWithContext(utils.SignalCtx, accountID, input) - case "entity": - _, err = nrClient.NerdStorage.WriteDocumentWithEntityScopeWithContext(utils.SignalCtx, entityGUID, input) - case "user": - _, err = nrClient.NerdStorage.WriteDocumentWithUserScopeWithContext(utils.SignalCtx, 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{} + 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": + accountID := configAPI.RequireActiveProfileAccountID() + _, err = client.NRClient.NerdStorage.WriteDocumentWithAccountScopeWithContext(utils.SignalCtx, accountID, input) + case "entity": + _, err = client.NRClient.NerdStorage.WriteDocumentWithEntityScopeWithContext(utils.SignalCtx, entityGUID, input) + case "user": + _, err = client.NRClient.NerdStorage.WriteDocumentWithUserScopeWithContext(utils.SignalCtx, input) + default: + log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") + } + if err != nil { + log.Fatal(err) + } + + log.Info("success") }, } @@ -143,32 +143,32 @@ GUID. A valid Nerdpack package ID is required. # User scope newrelic nerdstorage document delete --scope USER --packageId b0dee5a1-e809-4d6f-bd3c-0682cd079612 --collection myCol --documentId myDoc `, + PreRun: client.RequireClient, 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.DeleteDocumentWithAccountScopeWithContext(utils.SignalCtx, accountID, input) - case "entity": - _, err = nrClient.NerdStorage.DeleteDocumentWithEntityScopeWithContext(utils.SignalCtx, entityGUID, input) - case "user": - _, err = nrClient.NerdStorage.DeleteDocumentWithUserScopeWithContext(utils.SignalCtx, input) - default: - log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") - } - if err != nil { - log.Fatal(err) - } - - log.Info("success") - }) + var err error + + input := nerdstorage.DeleteDocumentInput{ + PackageID: packageID, + Collection: collection, + DocumentID: documentID, + } + + switch strings.ToLower(scope) { + case "account": + accountID := configAPI.RequireActiveProfileAccountID() + _, err = client.NRClient.NerdStorage.DeleteDocumentWithAccountScopeWithContext(utils.SignalCtx, accountID, input) + case "entity": + _, err = client.NRClient.NerdStorage.DeleteDocumentWithEntityScopeWithContext(utils.SignalCtx, entityGUID, input) + case "user": + _, err = client.NRClient.NerdStorage.DeleteDocumentWithUserScopeWithContext(utils.SignalCtx, input) + default: + log.Fatal("scope must be one of ACCOUNT, ENTITY, or USER") + } + if err != nil { + log.Fatal(err) + } + + log.Info("success") }, } @@ -176,7 +176,6 @@ 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") @@ -196,7 +195,6 @@ func init() { utils.LogIfError(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") @@ -220,7 +218,6 @@ func init() { utils.LogIfError(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") diff --git a/internal/nrql/command_query.go b/internal/nrql/command_query.go index 85b18e5fb..5a106a561 100644 --- a/internal/nrql/command_query.go +++ b/internal/nrql/command_query.go @@ -5,14 +5,13 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "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" ) var ( - accountID int historyLimit int query string ) @@ -27,16 +26,15 @@ 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: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { + accountID := configAPI.RequireActiveProfileAccountID() + result, err := client.NRClient.Nrdb.QueryWithContext(utils.SignalCtx, accountID, nrdb.NRQL(query)) + if err != nil { + log.Fatal(err) + } - result, err := nrClient.Nrdb.QueryWithContext(utils.SignalCtx, accountID, nrdb.NRQL(query)) - if err != nil { - log.Fatal(err) - } - - utils.LogIfFatal(output.Print(result.Results)) - }) + utils.LogIfFatal(output.Print(result.Results)) }, } @@ -48,34 +46,30 @@ 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: client.RequireClient, 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.NRClient.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")) 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/text.go b/internal/output/text.go index 73ae44dfd..d3d8eeba4 100644 --- a/internal/output/text.go +++ b/internal/output/text.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "reflect" + "sort" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" @@ -24,7 +25,7 @@ func (o *Output) text(data interface{}) error { switch v := reflect.ValueOf(data); v.Kind() { case reflect.String: fmt.Println(data) - case reflect.Slice, reflect.Struct: + case reflect.Slice, reflect.Struct, reflect.Map: return o.renderAsTable(data) default: return fmt.Errorf("unable to format data type: %T", data) @@ -47,31 +48,18 @@ func (o *Output) renderAsTable(data interface{}) error { // Let's see what they sent us switch v := reflect.ValueOf(data); v.Kind() { + case reflect.Map: + o.createTableFromMap(tw, v) case reflect.Slice: + // Create the header from the field names typ := reflect.TypeOf(data).Elem() - cols := typ.NumField() - header := make([]interface{}, cols) - colConfig := make([]table.ColumnConfig, cols) - - for i := 0; i < cols; i++ { - header[i] = typ.Field(i).Name - colConfig[i].Name = typ.Field(i).Name - colConfig[i].WidthMin = len(typ.Field(i).Name) - colConfig[i].WidthMax = o.terminalWidth * 3 / 4 - colConfig[i].WidthMaxEnforcer = text.WrapSoft - } - tw.SetColumnConfigs(colConfig) - tw.AppendHeader(table.Row(header)) - - // Add all the rows - for i := 0; i < v.Len(); i++ { - row := make([]interface{}, v.Index(i).NumField()) - for f := 0; f < v.Index(i).NumField(); f++ { - row[f] = v.Index(i).Field(f).Interface() - } - tw.AppendRow(table.Row(row)) + switch typ.Kind() { + case reflect.Map: + o.createTableFromMapSlice(tw, v) + case reflect.Struct: + o.createTableFromStructSlice(tw, v, typ) } // Single Struct becomes table view of Field | Value @@ -97,6 +85,113 @@ func (o *Output) renderAsTable(data interface{}) error { return nil } +func (o *Output) createTableFromStructSlice(tw table.Writer, v reflect.Value, elem reflect.Type) { + cols := elem.NumField() + header := make([]interface{}, cols) + colConfig := make([]table.ColumnConfig, cols) + + for i := 0; i < cols; i++ { + header[i] = elem.Field(i).Name + colConfig[i].Name = elem.Field(i).Name + colConfig[i].WidthMin = len(elem.Field(i).Name) + colConfig[i].WidthMax = o.terminalWidth * 3 / 4 + colConfig[i].WidthMaxEnforcer = text.WrapSoft + } + tw.SetColumnConfigs(colConfig) + tw.AppendHeader(table.Row(header)) + + // Add all the rows + for i := 0; i < v.Len(); i++ { + row := make([]interface{}, v.Index(i).NumField()) + for f := 0; f < v.Index(i).NumField(); f++ { + row[f] = v.Index(i).Field(f).Interface() + } + tw.AppendRow(table.Row(row)) + } +} + +func (o *Output) createTableFromMap(tw table.Writer, v reflect.Value) { + keys := v.MapKeys() + sortedKeys := sortedValueStrings(keys) + + // Add the header + cols := len(keys) + header := make([]interface{}, cols) + colConfig := make([]table.ColumnConfig, cols) + + for j, k := range sortedKeys { + header[j] = k + colConfig[j].Name = k + colConfig[j].WidthMin = len(k) + colConfig[j].WidthMax = o.terminalWidth * 3 / 4 + colConfig[j].WidthMaxEnforcer = text.WrapSoft + } + tw.SetColumnConfigs(colConfig) + tw.AppendHeader(table.Row(header)) + + row := make([]interface{}, len(keys)) + for j, k := range sortedKeys { + if key := findStringValue(k, keys); key != nil { + val := v.MapIndex(*key) + row[j] = val.Interface() + } + } + tw.AppendRow(table.Row(row)) +} + +func (o *Output) createTableFromMapSlice(tw table.Writer, v reflect.Value) { + var keys []reflect.Value + var sortedKeys []string + for i := 0; i < v.Len(); i++ { + if i == 0 { + keys = v.Index(i).MapKeys() + sortedKeys = sortedValueStrings(keys) + + // Add the header + cols := len(keys) + header := make([]interface{}, cols) + colConfig := make([]table.ColumnConfig, cols) + + for j, k := range sortedKeys { + header[j] = k + colConfig[j].Name = k + colConfig[j].WidthMin = len(k) + colConfig[j].WidthMax = o.terminalWidth * 3 / 4 + colConfig[j].WidthMaxEnforcer = text.WrapSoft + } + tw.SetColumnConfigs(colConfig) + tw.AppendHeader(table.Row(header)) + } + + row := make([]interface{}, len(keys)) + for j, k := range sortedKeys { + if key := findStringValue(k, keys); key != nil { + val := v.Index(i).MapIndex(*key) + row[j] = val.Interface() + } + } + tw.AppendRow(table.Row(row)) + } +} + +func sortedValueStrings(values []reflect.Value) (s []string) { + for _, v := range values { + s = append(s, v.String()) + } + sort.Strings(s) + return +} + +func findStringValue(s string, v []reflect.Value) *reflect.Value { + for _, v := range v { + if v.String() == s { + return &v + } + } + + return nil +} + func (o *Output) newTableWriter() table.Writer { t := table.NewWriter() t.SetOutputMirror(os.Stdout) diff --git a/internal/profile/command.go b/internal/profile/command.go new file mode 100644 index 000000000..d2d91f1ce --- /dev/null +++ b/internal/profile/command.go @@ -0,0 +1,310 @@ +package profile + +import ( + "fmt" + "strconv" + + "github.com/AlecAivazis/survey/v2" + "github.com/jedib0t/go-pretty/text" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-cli/internal/config" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" + "github.com/newrelic/newrelic-cli/internal/output" + "github.com/newrelic/newrelic-cli/internal/utils" + "github.com/newrelic/newrelic-client-go/pkg/accounts" +) + +const ( + DefaultProfileName = "default" + defaultProfileString = " (default)" +) + +var ( + showKeys bool + flagRegion string + apiKey string + insightsInsertKey string + accountID int + licenseKey string + acceptDefaults bool +) + +// 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. +`, + Aliases: []string{ + "configure", + }, + Example: "newrelic profile add --profile --region --apiKey --insightsInsertKey --accountId --licenseKey ", + PreRun: requireProfileName, + Run: func(cmd *cobra.Command, args []string) { + addStringValueToProfile(config.FlagProfileName, apiKey, config.APIKey, "User API Key", nil, nil) + addStringValueToProfile(config.FlagProfileName, flagRegion, config.Region, "Region", nil, []string{"US", "EU"}) + addIntValueToProfile(config.FlagProfileName, accountID, config.AccountID, "Account ID", fetchAccountIDs) + addStringValueToProfile(config.FlagProfileName, insightsInsertKey, config.InsightsInsertKey, "Insights Insert Key", fetchInsightsInsertKey(), nil) + addStringValueToProfile(config.FlagProfileName, licenseKey, config.LicenseKey, "License Key", fetchLicenseKey(), nil) + + profile, err := configAPI.GetDefaultProfileName() + if err != nil { + log.Fatal(err) + } + + if profile == "" { + if err := configAPI.SetDefaultProfile(config.FlagProfileName); err != nil { + log.Fatal(err) + } + } + + log.Info("success") + }, +} + +func addStringValueToProfile(profileName string, val string, key config.FieldKey, label string, defaultFunc func() (string, error), selectValues []string) { + if val == "" { + defaultValue := configAPI.GetProfileString(profileName, key) + + if defaultValue == "" && defaultFunc != nil { + d, err := defaultFunc() + if err != nil { + log.Debug(err) + } else { + defaultValue = d + } + } + + prompt := &survey.Input{ + Message: fmt.Sprintf("%s:", label), + Default: defaultValue, + } + + if selectValues != nil { + prompt.Suggest = func(string) []string { return selectValues } + } + + var input string + if !acceptDefaults { + if err := survey.AskOne(prompt, &input); err != nil { + log.Fatal(err) + } + } + + if input != "" { + val = input + } else { + val = defaultValue + } + } + + if err := configAPI.SetProfileValue(profileName, key, val); err != nil { + log.Fatal(err) + } +} + +func addIntValueToProfile(profileName string, val int, key config.FieldKey, label string, defaultFunc func() ([]int, error)) { + if val == 0 { + prompt := &survey.Input{ + Message: fmt.Sprintf("%s:", label), + } + + defaultValue := configAPI.GetProfileInt(profileName, key) + + if defaultValue == 0 && defaultFunc != nil { + d, err := defaultFunc() + if err != nil { + log.Debug(err) + } else { + if len(d) == 1 { + defaultValue = d[0] + } else if len(d) > 0 { + prompt.Suggest = func(string) []string { return utils.IntSliceToStringSlice(d) } + } + } + } + + if defaultValue != 0 { + prompt.Default = strconv.Itoa(defaultValue) + } + + var input string + if !acceptDefaults { + if err := survey.AskOne(prompt, &input); err != nil { + log.Fatal(err) + } + } + + if input != "" { + i, err := strconv.Atoi(input) + if err != nil { + log.Fatal(err) + } + + val = i + } else { + val = defaultValue + } + } + + if err := configAPI.SetProfileValue(profileName, key, val); err != nil { + log.Fatal(err) + } +} + +// fetchAccountID will try and retrieve the available account IDs for the given user. +func fetchAccountIDs() (ids []int, err error) { + client, err := client.NewClient(config.FlagProfileName) + if err != nil { + return nil, err + } + + params := accounts.ListAccountsParams{ + Scope: &accounts.RegionScopeTypes.IN_REGION, + } + + accounts, err := client.Accounts.ListAccounts(params) + if err != nil { + return nil, err + } + + for _, a := range accounts { + ids = append(ids, a.ID) + } + + return ids, nil +} + +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 --profile ", + Run: func(cmd *cobra.Command, args []string) { + err := configAPI.SetDefaultProfile(config.FlagProfileName) + 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) { + list := []map[string]interface{}{} + for _, p := range configAPI.GetProfileNames() { + out := map[string]interface{}{} + + name := p + if p == configAPI.GetActiveProfileName() { + name += text.FgHiBlack.Sprint(defaultProfileString) + } + out["Name"] = name + + configAPI.ForEachProfileFieldDefinition(p, func(d config.FieldDefinition) { + v := configAPI.GetProfileString(p, d.Key) + if !showKeys && d.Sensitive { + v = text.FgHiBlack.Sprint(utils.Obfuscate(v)) + } + + out[string(d.Key)] = v + }) + + list = append(list, out) + } + + output.Text(list) + }, + 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 --profile ", + Run: func(cmd *cobra.Command, args []string) { + err := configAPI.RemoveProfile(config.FlagProfileName) + if err != nil { + log.Fatal(err) + } + + log.Info("success") + }, + Aliases: []string{ + "remove", + "rm", + }, +} + +func requireProfileName(cmd *cobra.Command, args []string) { + if config.FlagProfileName == "" { + log.Fatal("the --profile argument is required") + } +} + +func init() { + // Add + Command.AddCommand(cmdAdd) + 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") + cmdAdd.Flags().BoolVarP(&acceptDefaults, "acceptDefaults", "y", false, "suppress prompts and accept default values") + + // Default + Command.AddCommand(cmdDefault) + + // List + Command.AddCommand(cmdList) + cmdList.Flags().BoolVarP(&showKeys, "show-keys", "s", false, "list the profiles on your keychain") + + // Remove + Command.AddCommand(cmdDelete) +} + +func fetchLicenseKey() func() (string, error) { + accountID := configAPI.GetProfileInt(config.FlagProfileName, config.AccountID) + return func() (string, error) { + return client.FetchLicenseKey(accountID, config.FlagProfileName) + } +} + +func fetchInsightsInsertKey() func() (string, error) { + accountID := configAPI.GetProfileInt(config.FlagProfileName, config.AccountID) + return func() (string, error) { + return client.FetchInsightsInsertKey(accountID, config.FlagProfileName) + } +} diff --git a/internal/credentials/command_test.go b/internal/profile/command_test.go similarity index 70% rename from internal/credentials/command_test.go rename to internal/profile/command_test.go index 707814f46..9e1b6b27b 100644 --- a/internal/credentials/command_test.go +++ b/internal/profile/command_test.go @@ -1,6 +1,6 @@ // +build unit -package credentials +package profile 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) @@ -22,19 +22,17 @@ func TestCredentialsAdd(t *testing.T) { assert.Equal(t, "add", cmdAdd.Name()) testcobra.CheckCobraMetadata(t, cmdAdd) - testcobra.CheckCobraRequiredFlags(t, cmdAdd, []string{"name", "region"}) - testcobra.CheckCobraCommandAliases(t, cmdAdd, []string{}) + testcobra.CheckCobraCommandAliases(t, cmdAdd, []string{"configure"}) } -func TestCredentialsDefault(t *testing.T) { +func TestProfilesDefault(t *testing.T) { assert.Equal(t, "default", cmdDefault.Name()) testcobra.CheckCobraMetadata(t, cmdDefault) - testcobra.CheckCobraRequiredFlags(t, cmdDefault, []string{"name"}) 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,10 +40,9 @@ 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) - testcobra.CheckCobraRequiredFlags(t, cmdDelete, []string{"name"}) testcobra.CheckCobraCommandAliases(t, cmdDelete, []string{"remove", "rm"}) // DEPRECATED: from nr1 cli } diff --git a/internal/reporting/command_junit.go b/internal/reporting/command_junit.go index 8ca95b110..dd8bcf487 100644 --- a/internal/reporting/command_junit.go +++ b/internal/reporting/command_junit.go @@ -9,16 +9,15 @@ 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" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "github.com/newrelic/newrelic-cli/internal/output" "github.com/newrelic/newrelic-cli/internal/utils" - "github.com/newrelic/newrelic-client-go/newrelic" ) const junitEventType = "TestRun" var ( - accountID int path string dryRun bool outputEvents bool @@ -31,49 +30,50 @@ var cmdJUnit = &cobra.Command{ `, Example: `newrelic reporting junit --accountId 12345678 --path unit.xml`, + PreRun: client.RequireClient, 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") - } + accountID := configAPI.RequireActiveProfileAccountID() - id, err := uuid.NewRandom() - if err != nil { - log.Fatal(err) - } + if configAPI.GetActiveProfileString(config.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") + } - xml, err := ioutil.ReadFile(path) - if err != nil { - log.Fatal(err) - } + id, err := uuid.NewRandom() + if err != nil { + log.Fatal(err) + } - suites, err := junit.Ingest(xml) - if err != nil { - log.Fatalf("failed to ingest JUnit xml %v", err) - } + xml, err := ioutil.ReadFile(path) + if err != nil { + log.Fatal(err) + } - events := []map[string]interface{}{} + suites, err := junit.Ingest(xml) + if err != nil { + log.Fatalf("failed to ingest JUnit xml %v", err) + } - for _, suite := range suites { - for _, test := range suite.Tests { - events = append(events, createTestRunEvent(id, suite, test)) - } - } + events := []map[string]interface{}{} - if outputEvents { - utils.LogIfFatal(output.Print(events)) + for _, suite := range suites { + for _, test := range suite.Tests { + events = append(events, createTestRunEvent(id, suite, test)) } + } - if dryRun { - return - } + if outputEvents { + utils.LogIfFatal(output.Print(events)) + } - if err := nrClient.Events.CreateEventWithContext(utils.SignalCtx, accountID, events); err != nil { - log.Fatal(err) - } + if dryRun { + return + } + + if err := client.NRClient.Events.CreateEventWithContext(utils.SignalCtx, accountID, events); err != nil { + log.Fatal(err) + } - log.Info("success") - }) + log.Info("success") }, } @@ -105,10 +105,8 @@ 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")) } 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 4bcdf367c..cd025b4a0 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -7,6 +7,7 @@ import ( "os" "os/signal" "reflect" + "strconv" "strings" "syscall" "time" @@ -141,3 +142,28 @@ func StringInSlice(str string, slice []string) bool { return false } + +func IntSliceToStringSlice(in []int) (out []string) { + for _, i := range in { + out = append(out, strconv.Itoa(i)) + } + + return out +} + +// Obfuscate receives a string, and replaces everything after the first 8 +// characters with an asterisk before returning the result. +func Obfuscate(input string) string { + result := make([]string, len(input)) + parts := strings.Split(input, "") + + for i, x := range parts { + if i < 8 { + result[i] = x + } else { + result[i] = "*" + } + } + + return strings.Join(result, "") +} diff --git a/internal/utils/validation/polling_nrql_validator.go b/internal/utils/validation/polling_nrql_validator.go index edcbda9cf..9ced38f61 100644 --- a/internal/utils/validation/polling_nrql_validator.go +++ b/internal/utils/validation/polling_nrql_validator.go @@ -2,11 +2,10 @@ package validation import ( "context" - "errors" "fmt" "time" - "github.com/newrelic/newrelic-cli/internal/credentials" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "github.com/newrelic/newrelic-cli/internal/install/ux" "github.com/newrelic/newrelic-cli/internal/utils" "github.com/newrelic/newrelic-client-go/pkg/nrdb" @@ -116,14 +115,11 @@ func (m *PollingNRQLValidator) tryValidate(ctx context.Context, query string) (b } func (m *PollingNRQLValidator) executeQuery(ctx context.Context, query string) ([]nrdb.NRDBResult, error) { - profile := credentials.DefaultProfile() - if profile == nil || profile.AccountID == 0 { - return nil, errors.New("no account ID found in default profile") - } + accountID := configAPI.RequireActiveProfileAccountID() 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/workload/command_workload.go b/internal/workload/command_workload.go index 5e275db08..23c5c8971 100644 --- a/internal/workload/command_workload.go +++ b/internal/workload/command_workload.go @@ -7,15 +7,14 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" "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" "github.com/newrelic/newrelic-client-go/pkg/workloads" ) var ( - accountID int name string entityGUIDs []string entitySearchQueries []string @@ -31,13 +30,12 @@ var cmdGet = &cobra.Command{ The get command retrieves a specific workload by its workload GUID. `, Example: `newrelic workload get --accountId 12345678 --guid MjUyMDUyOHxOUjF8V09SS0xPQUR8MTI4Myt`, + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - workload, err := nrClient.Entities.GetEntitiesWithContext(utils.SignalCtx, []entities.EntityGUID{entities.EntityGUID(guid)}) - utils.LogIfFatal(err) + workload, err := client.NRClient.Entities.GetEntitiesWithContext(utils.SignalCtx, []entities.EntityGUID{entities.EntityGUID(guid)}) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(workload)) - }) + utils.LogIfFatal(output.Print(workload)) }, } @@ -49,22 +47,22 @@ var cmdList = &cobra.Command{ The list command retrieves the workloads for the given account ID. `, Example: `newrelic workload list --accountId 12345678`, + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - builder := entities.EntitySearchQueryBuilder{ - Type: entities.EntitySearchQueryBuilderTypeTypes.WORKLOAD, - Tags: []entities.EntitySearchQueryBuilderTag{ - { - Key: "accountId", - Value: strconv.Itoa(accountID), - }, + accountID := configAPI.RequireActiveProfileAccountID() + builder := entities.EntitySearchQueryBuilder{ + Type: entities.EntitySearchQueryBuilderTypeTypes.WORKLOAD, + Tags: []entities.EntitySearchQueryBuilderTag{ + { + Key: "accountId", + Value: strconv.Itoa(accountID), }, - } - workload, err := nrClient.Entities.GetEntitySearchWithContext(utils.SignalCtx, entities.EntitySearchOptions{}, "", builder, nil) - utils.LogIfFatal(err) + }, + } + workload, err := client.NRClient.Entities.GetEntitySearchWithContext(utils.SignalCtx, entities.EntitySearchOptions{}, "", builder, nil) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(workload)) - }) + utils.LogIfFatal(output.Print(workload)) }, } @@ -81,36 +79,37 @@ 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: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - createInput := workloads.WorkloadCreateInput{ - Name: name, - } + accountID := configAPI.RequireActiveProfileAccountID() - if len(entityGUIDs) > 0 { - for _, e := range entityGUIDs { - createInput.EntityGUIDs = append(createInput.EntityGUIDs, entities.EntityGUID(e)) - } - } + createInput := workloads.WorkloadCreateInput{ + Name: name, + } - if len(entitySearchQueries) > 0 { - var queryInputs []workloads.WorkloadEntitySearchQueryInput - for _, q := range entitySearchQueries { - queryInputs = append(queryInputs, workloads.WorkloadEntitySearchQueryInput{Query: q}) - } - createInput.EntitySearchQueries = queryInputs + if len(entityGUIDs) > 0 { + for _, e := range entityGUIDs { + createInput.EntityGUIDs = append(createInput.EntityGUIDs, entities.EntityGUID(e)) } + } - if len(scopeAccountIDs) > 0 { - createInput.ScopeAccounts = &workloads.WorkloadScopeAccountsInput{AccountIDs: scopeAccountIDs} + if len(entitySearchQueries) > 0 { + var queryInputs []workloads.WorkloadEntitySearchQueryInput + for _, q := range entitySearchQueries { + queryInputs = append(queryInputs, workloads.WorkloadEntitySearchQueryInput{Query: q}) } + createInput.EntitySearchQueries = queryInputs + } + + if len(scopeAccountIDs) > 0 { + createInput.ScopeAccounts = &workloads.WorkloadScopeAccountsInput{AccountIDs: scopeAccountIDs} + } - workload, err := nrClient.Workloads.WorkloadCreateWithContext(utils.SignalCtx, accountID, createInput) - utils.LogIfFatal(err) + workload, err := client.NRClient.Workloads.WorkloadCreateWithContext(utils.SignalCtx, accountID, createInput) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(workload)) - log.Info("success") - }) + utils.LogIfFatal(output.Print(workload)) + log.Info("success") }, } @@ -127,36 +126,35 @@ 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: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - updateInput := workloads.WorkloadUpdateInput{ - Name: name, - } + updateInput := workloads.WorkloadUpdateInput{ + Name: name, + } - if len(entityGUIDs) > 0 { - for _, e := range entityGUIDs { - updateInput.EntityGUIDs = append(updateInput.EntityGUIDs, entities.EntityGUID(e)) - } + if len(entityGUIDs) > 0 { + for _, e := range entityGUIDs { + updateInput.EntityGUIDs = append(updateInput.EntityGUIDs, entities.EntityGUID(e)) } + } - if len(entitySearchQueries) > 0 { - var queryInputs []workloads.WorkloadUpdateCollectionEntitySearchQueryInput - for _, q := range entitySearchQueries { - queryInputs = append(queryInputs, workloads.WorkloadUpdateCollectionEntitySearchQueryInput{Query: q}) - } - updateInput.EntitySearchQueries = queryInputs + if len(entitySearchQueries) > 0 { + var queryInputs []workloads.WorkloadUpdateCollectionEntitySearchQueryInput + for _, q := range entitySearchQueries { + queryInputs = append(queryInputs, workloads.WorkloadUpdateCollectionEntitySearchQueryInput{Query: q}) } + updateInput.EntitySearchQueries = queryInputs + } - if len(scopeAccountIDs) > 0 { - updateInput.ScopeAccounts = &workloads.WorkloadScopeAccountsInput{AccountIDs: scopeAccountIDs} - } + if len(scopeAccountIDs) > 0 { + updateInput.ScopeAccounts = &workloads.WorkloadScopeAccountsInput{AccountIDs: scopeAccountIDs} + } - workload, err := nrClient.Workloads.WorkloadUpdateWithContext(utils.SignalCtx, entities.EntityGUID(guid), updateInput) - utils.LogIfFatal(err) + workload, err := client.NRClient.Workloads.WorkloadUpdateWithContext(utils.SignalCtx, entities.EntityGUID(guid), updateInput) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(workload)) - log.Info("success") - }) + utils.LogIfFatal(output.Print(workload)) + log.Info("success") }, } @@ -171,18 +169,19 @@ 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: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - duplicateInput := workloads.WorkloadDuplicateInput{ - Name: name, - } + accountID := configAPI.RequireActiveProfileAccountID() + + duplicateInput := workloads.WorkloadDuplicateInput{ + Name: name, + } - workload, err := nrClient.Workloads.WorkloadDuplicateWithContext(utils.SignalCtx, accountID, entities.EntityGUID(guid), duplicateInput) - utils.LogIfFatal(err) + workload, err := client.NRClient.Workloads.WorkloadDuplicateWithContext(utils.SignalCtx, accountID, entities.EntityGUID(guid), duplicateInput) + utils.LogIfFatal(err) - utils.LogIfFatal(output.Print(workload)) - log.Info("success") - }) + utils.LogIfFatal(output.Print(workload)) + log.Info("success") }, } @@ -194,37 +193,30 @@ var cmdDelete = &cobra.Command{ The delete command accepts a workload's entity GUID. `, Example: `newrelic workload delete --guid 'MjUyMDUyOHxBOE28QVBQTElDQVRDT058MjE1MDM3Nzk1'`, + PreRun: client.RequireClient, Run: func(cmd *cobra.Command, args []string) { - client.WithClient(func(nrClient *newrelic.NewRelic) { - _, err := nrClient.Workloads.WorkloadDeleteWithContext(utils.SignalCtx, entities.EntityGUID(guid)) - utils.LogIfFatal(err) + _, err := client.NRClient.Workloads.WorkloadDeleteWithContext(utils.SignalCtx, entities.EntityGUID(guid)) + utils.LogIfFatal(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")) // 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")) // Update @@ -239,9 +231,7 @@ func init() { // 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")) // Delete diff --git a/test.json b/test.json new file mode 100644 index 000000000..fb822fd4f --- /dev/null +++ b/test.json @@ -0,0 +1,3133 @@ +[{ + "classname": "github.com/newrelic/newrelic-cli/cmd/newrelic", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestCompletion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/cmd/newrelic", + "test": "TestCompletion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/cmd/newrelic", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDocumentation", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/cmd/newrelic", + "test": "TestDocumentation", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/cmd/newrelic", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRootCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/cmd/newrelic", + "test": "TestRootCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/cmd/newrelic", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/cmd/newrelic", + "test": "TestVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/agent", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestAgentConfig", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/agent", + "test": "TestAgentConfig", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/agent", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestAgentConfigObfuscate", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/agent", + "test": "TestAgentConfigObfuscate", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/agent", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestAgentCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/agent", + "test": "TestAgentCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/agent", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestObfuscateStringWithKey", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/agent", + "test": "TestObfuscateStringWithKey", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/apm", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestApmApp", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/apm", + "test": "TestApmApp", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/apm", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestApmAppGet", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/apm", + "test": "TestApmAppGet", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/apm", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestApmAppSearch", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/apm", + "test": "TestApmAppSearch", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/apm", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestApmDeployment", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/apm", + "test": "TestApmDeployment", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/apm", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestApmDeploymentList", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/apm", + "test": "TestApmDeploymentList", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/apm", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestApmDeploymentCreate", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/apm", + "test": "TestApmDeploymentCreate", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/apm", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestApmDeleteDeployment", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/apm", + "test": "TestApmDeleteDeployment", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/apm", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestAPMCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/apm", + "test": "TestAPMCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestConfigCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestConfigCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestCmdReset", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestCmdReset", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestCmdGet", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestCmdGet", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestCmdSet", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestCmdSet", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 60, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestGetActiveProfileValues", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestGetActiveProfileValues", + "time": "0.060000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestGetActiveProfileValues_EnvVarOverride", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestGetActiveProfileValues_EnvVarOverride", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestGetConfigValues", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestGetConfigValues", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestGetConfigValues_EnvVarOverride", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestGetConfigValues_EnvVarOverride", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestGetConfigValues_DefaultValues", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestGetConfigValues_DefaultValues", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 10, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRemoveProfile", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestRemoveProfile", + "time": "0.010000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestTernaryString", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestTernaryString", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestTernaryBool", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestTernaryBool", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/configuration", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestTernaryValid", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/configuration", + "test": "TestTernaryValid", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/decode", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDecodeEntityCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/decode", + "test": "TestDecodeEntityCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/decode", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDecodeCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/decode", + "test": "TestDecodeCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/decode", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDecodeURLCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/decode", + "test": "TestDecodeURLCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/edge", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestNerdGraphCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/edge", + "test": "TestNerdGraphCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/edge", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestList", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/edge", + "test": "TestList", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/edge", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestCreate", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/edge", + "test": "TestCreate", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/edge", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDelete", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/edge", + "test": "TestDelete", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/entities", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestEntitiesSearch", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/entities", + "test": "TestEntitiesSearch", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/entities", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestEntitiesGetTags", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/entities", + "test": "TestEntitiesGetTags", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/entities", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestEntitiesDeleteTags", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/entities", + "test": "TestEntitiesDeleteTags", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/entities", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestEntitiesDeleteTagValues", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/entities", + "test": "TestEntitiesDeleteTagValues", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/entities", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestEntitiesCreateTags", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/entities", + "test": "TestEntitiesCreateTags", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/entities", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestEntitiesReplaceTags", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/entities", + "test": "TestEntitiesReplaceTags", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/entities", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestEntitiesAssembleTagsInput", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/entities", + "test": "TestEntitiesAssembleTagsInput", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/entities", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestEntitiesAssembleTagValues", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/entities", + "test": "TestEntitiesAssembleTagValues", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/entities", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestEntitiesAssembleTagValuesInput", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/entities", + "test": "TestEntitiesAssembleTagValuesInput", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/entities", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestEntitiesAssembleTagValue", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/entities", + "test": "TestEntitiesAssembleTagValue", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/entities", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestEntitiesCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/entities", + "test": "TestEntitiesCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/events", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestPost", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/events", + "test": "TestPost", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/events", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestNerdGraphCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/events", + "test": "TestNerdGraphCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstallCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstallCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestLoadRecipeFile", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestLoadRecipeFile", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFetchRecipeFile_FailedStatusCode", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestFetchRecipeFile_FailedStatusCode", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestNewRecipeFile", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestNewRecipeFile", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestNewRecipeInstaller_InstallerContextFields", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestNewRecipeInstaller_InstallerContextFields", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldGetRecipeFromURL", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestShouldGetRecipeFromURL", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldGetRecipeFromFile", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestShouldGetRecipeFromFile", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_DiscoveryComplete", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_DiscoveryComplete", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_FailsOnInvalidOs", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_FailsOnInvalidOs", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_UnsupportedKernelArch", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_UnsupportedKernelArch", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_RecipeAvailable", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_RecipeAvailable", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFetchAndInstallPacks", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestFetchAndInstallPacks", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_RecipeInstalled", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_RecipeInstalled", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_RecipeFailed", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_RecipeFailed", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_NonInfraRecipeFailed", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_NonInfraRecipeFailed", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_AllRecipesFailed", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_AllRecipesFailed", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_InstallComplete", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_InstallComplete", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_InstallCanceled", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_InstallCanceled", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_InstallCompleteError", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_InstallCompleteError", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_InstallCompleteError_NoFailureWhenAnyRecipeSucceeds", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_InstallCompleteError_NoFailureWhenAnyRecipeSucceeds", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_RecipeSkipped", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_RecipeSkipped", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_RecipeSkippedApm", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_RecipeSkippedApm", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_RecipeSkippedApmAnyKeyword", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_RecipeSkippedApmAnyKeyword", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_RecipeSkipped_SkipIntegrations", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_RecipeSkipped_SkipIntegrations", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_RecipeSkipped_MultiSelect", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_RecipeSkipped_MultiSelect", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_RecipeRecommended", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_RecipeRecommended", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_RecipeSkipped_AssumeYes", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_RecipeSkipped_AssumeYes", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_TargetedInstall_InstallsInfraAgent", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_TargetedInstall_InstallsInfraAgent", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_TargetedInstall_FilterAllButProvided", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_TargetedInstall_FilterAllButProvided", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_TargetedInstall_InstallsInfraAgentDependency", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_TargetedInstall_InstallsInfraAgentDependency", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_TargetedInstallInfraAgent_NoInfraAgentDuplicate", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_TargetedInstallInfraAgent_NoInfraAgentDuplicate", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_TargetedInstall_SkipInfra", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_TargetedInstall_SkipInfra", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_TargetedInstall_SkipInfraDependency", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_TargetedInstall_SkipInfraDependency", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstall_GuidReport", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install", + "test": "TestInstall_GuidReport", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailManifestOs", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailManifestOs", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailManifestWindowsVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailManifestWindowsVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailManifestUbuntuVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailManifestUbuntuVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailUbuntuWithoutVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailUbuntuWithoutVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailUbuntuVeryOldVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailUbuntuVeryOldVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailUbuntuMinOldVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailUbuntuMinOldVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailUbuntuMinVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailUbuntuMinVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailUbuntuMinUnspecifiedVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailUbuntuMinUnspecifiedVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldPassUbuntuMinVersionFull", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldPassUbuntuMinVersionFull", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldPassUbuntuMinVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldPassUbuntuMinVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldPassUbuntu", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldPassUbuntu", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_FailsOnInvalidOs", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_FailsOnInvalidOs", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_FailsOnNoOs", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_FailsOnNoOs", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_DoesntFailForWindowsOs", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_DoesntFailForWindowsOs", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_DoesntFailForLinuxOs", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_DoesntFailForLinuxOs", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailWindowsWithoutVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailWindowsWithoutVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailWindowsVeryOldVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailWindowsVeryOldVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailWindowsOldWithAnyPlatform", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailWindowsOldWithAnyPlatform", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailWindowsMinOldVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailWindowsMinOldVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailWindowsMinVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailWindowsMinVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFailWindowsMinUnspecifiedVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldFailWindowsMinUnspecifiedVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldPassWindowsMinVersionFull", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldPassWindowsMinVersionFull", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldPassWindowsMinVersion", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldPassWindowsMinVersion", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldPassWindows", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "Test_ShouldPassWindows", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestIsValidOpenInstallationPlatform", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "TestIsValidOpenInstallationPlatform", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestIsValidOpenInstallationPlatformFamily", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "TestIsValidOpenInstallationPlatformFamily", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFilterValues_ValidPlatform", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "TestFilterValues_ValidPlatform", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFilterValues_InvalidPlatform", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/discovery", + "test": "TestFilterValues_InvalidPlatform", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 40, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestExecute_SystemVariableInterpolation", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestExecute_SystemVariableInterpolation", + "time": "0.040000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestNewInstallStatus", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestNewInstallStatus", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestStatusWithAvailableRecipes_Basic", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestStatusWithAvailableRecipes_Basic", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestStatusWithRecipeEvent_Basic", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestStatusWithRecipeEvent_Basic", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestStatusWithRecipeEvent_ErrorMessages", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestStatusWithRecipeEvent_ErrorMessages", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestExecutionStatusWithRecipeEvent_RecipeExists", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestExecutionStatusWithRecipeEvent_RecipeExists", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestStatusWithRecipeEvent_EntityGUID", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestStatusWithRecipeEvent_EntityGUID", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestStatusWithRecipeEvent_EntityGUIDExists", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestStatusWithRecipeEvent_EntityGUIDExists", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstallStatus_statusUpdateMethods", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestInstallStatus_statusUpdateMethods", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstallStatus_observabilityPackStatusUpdateMethods", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestInstallStatus_observabilityPackStatusUpdateMethods", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstallStatus_shouldNotFailAvailableOnComplete", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestInstallStatus_shouldNotFailAvailableOnComplete", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstallStatus_shouldFailAvailableOnCancel", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestInstallStatus_shouldFailAvailableOnCancel", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstallStatus_multipleRecipeStatuses", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestInstallStatus_multipleRecipeStatuses", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestStatus_HTTPSProxy", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestStatus_HTTPSProxy", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestLineCaptureBuffer", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestLineCaptureBuffer", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestMockStatusReporter_interface", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestMockStatusReporter_interface", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeAvailable_Basic", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestRecipeAvailable_Basic", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeAvailable_UserScopeError", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestRecipeAvailable_UserScopeError", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeInstalled_Basic", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestRecipeInstalled_Basic", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeInstalled_UserScopeOnly", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestRecipeInstalled_UserScopeOnly", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeInstalled_MultipleEntityGUIDs", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestRecipeInstalled_MultipleEntityGUIDs", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeInstalled_UserScopeError", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestRecipeInstalled_UserScopeError", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeInstalled_EntityScopeError", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestRecipeInstalled_EntityScopeError", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeFailed_Basic", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestRecipeFailed_Basic", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeFailed_UserScopeOnly", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestRecipeFailed_UserScopeOnly", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeFailed_UserScopeError", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestRecipeFailed_UserScopeError", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeFailed_EntityScopeError", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestRecipeFailed_EntityScopeError", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstallComplete_Basic", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestInstallComplete_Basic", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstallComplete_UserScopeError", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestInstallComplete_UserScopeError", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstallCanceled_Basic", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestInstallCanceled_Basic", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestInstallCanceled_UserScopeError", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestInstallCanceled_UserScopeError", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDiscoveryComplete_Basic", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestDiscoveryComplete_Basic", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDiscoveryComplete_UserScopeError", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestDiscoveryComplete_UserScopeError", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 20, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestPosixShellExecution_Basic", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestPosixShellExecution_Basic", + "time": "0.020000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 10, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestPosixShellExecution_Error", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestPosixShellExecution_Error", + "time": "0.010000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 10, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestPosixShellExecution_AllVars", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestPosixShellExecution_AllVars", + "time": "0.010000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 10, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestPosixShellExecution_RecipeVars", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestPosixShellExecution_RecipeVars", + "time": "0.010000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 50, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestPosixShellExecution_EnvVars", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestPosixShellExecution_EnvVars", + "time": "0.050000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestExecutePreInstall_Basic", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestExecutePreInstall_Basic", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestExecutePreInstall_Error", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestExecutePreInstall_Error", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestExecutePreInstall_RecipeVars", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestExecutePreInstall_RecipeVars", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestExecutePreInstall_EnvVars", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestExecutePreInstall_EnvVars", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestExecutePreInstall_AllVars", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestExecutePreInstall_AllVars", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestTerminalStatusReporter_interface", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "TestTerminalStatusReporter_interface", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldGenerateEntityLink", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "Test_ShouldGenerateEntityLink", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldNotGenerateEntityLink", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "Test_ShouldNotGenerateEntityLink", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldNotGenerateEntityLinkWhenNoRecipes", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "Test_ShouldNotGenerateEntityLinkWhenNoRecipes", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldGenerateExplorerLink", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "Test_ShouldGenerateExplorerLink", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldNotGenerateExplorerLink", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "Test_ShouldNotGenerateExplorerLink", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/execution", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldNotGenerateExplorerLinkWhenNoRecipes", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/execution", + "test": "Test_ShouldNotGenerateExplorerLinkWhenNoRecipes", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/packs", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFetchPacks_NoResults", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/packs", + "test": "TestFetchPacks_NoResults", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/packs", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFetchPacks_OneResult", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/packs", + "test": "TestFetchPacks_OneResult", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/packs", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFetchPacks_SingleRecipeWithTwoPacks", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/packs", + "test": "TestFetchPacks_SingleRecipeWithTwoPacks", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/packs", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFetchPacks_MultipleRecipesWithSinglePacks", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/packs", + "test": "TestFetchPacks_MultipleRecipesWithSinglePacks", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/packs", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFetchPacks_MultipleRecipesWithTwoPacks", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/packs", + "test": "TestFetchPacks_MultipleRecipesWithTwoPacks", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/packs", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestTransformDashboardJson", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/packs", + "test": "TestTransformDashboardJson", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecommend_CustomScript_Success", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestRecommend_CustomScript_Success", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecommend_CustomScript_Failure", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestRecommend_CustomScript_Failure", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldGetRecipeFirstNameValid", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestShouldGetRecipeFirstNameValid", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldGetRecipeFirstNameValidWhole", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestShouldGetRecipeFirstNameValidWhole", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldGetRecipeFirstNameInvalid", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestShouldGetRecipeFirstNameInvalid", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFindAll_Empty", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_ShouldFindAll_Empty", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFindSingleRecipe", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_ShouldFindSingleRecipe", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFindSingleOsRecipe", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_ShouldFindSingleOsRecipe", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldNotFindSingleOsRecipe", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_ShouldNotFindSingleOsRecipe", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFindMostMatchingSingleRecipe", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_ShouldFindMostMatchingSingleRecipe", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFindMostMatchingSingleRecipeWithoutPlatform", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_ShouldFindMostMatchingSingleRecipeWithoutPlatform", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldDiscardMostMatchingWithoutAllFieldsMatching", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_ShouldDiscardMostMatchingWithoutAllFieldsMatching", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldFindMultipleNames", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_ShouldFindMultipleNames", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_ShouldOrderInfraLogFirst", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_ShouldOrderInfraLogFirst", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_matchRecipeCriteria_Basic", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_matchRecipeCriteria_Basic", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_matchRecipeCriteria_EmptyString", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_matchRecipeCriteria_EmptyString", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_matchRecipeCriteria_KeyMissing", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_matchRecipeCriteria_KeyMissing", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_shouldFindMaxMatch_First", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_shouldFindMaxMatch_First", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "Test_shouldFindMaxMatch_Last", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "Test_shouldFindMaxMatch_Last", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldFindMatchesMultipleWithCmdLineInsteadOfName", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestShouldFindMatchesMultipleWithCmdLineInsteadOfName", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFindMatchesMultiple_NoMatchingProcess", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestFindMatchesMultiple_NoMatchingProcess", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFindMatchesMultiple_SingleMatchingProcess_SingleOHIRecipe", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestFindMatchesMultiple_SingleMatchingProcess_SingleOHIRecipe", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFindMatchesMultiple_SingleMatchingProcess_SingleAPMRecipe", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestFindMatchesMultiple_SingleMatchingProcess_SingleAPMRecipe", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFindMatchesMultiple_SingleMatchingProcess_MultipleRecipes", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestFindMatchesMultiple_SingleMatchingProcess_MultipleRecipes", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFindMatchesMultiple_MultipleMatchingProcesses_SingleRecipe", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestFindMatchesMultiple_MultipleMatchingProcesses_SingleRecipe", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFindMatchesMultiple_MultipleMatchingProcesses_MultipleRecipes", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestFindMatchesMultiple_MultipleMatchingProcesses_MultipleRecipes", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldFilterOutNewRelicInstall", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestShouldFilterOutNewRelicInstall", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldFilterOutNewRelicInstallCaseInsensitive", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestShouldFilterOutNewRelicInstallCaseInsensitive", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldFilterOutNewRelicInstallWindows", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestShouldFilterOutNewRelicInstallWindows", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldFilterOutNewRelicInstallWindowsNoQuote", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestShouldFilterOutNewRelicInstallWindowsNoQuote", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestFetchFilters", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/recipes", + "test": "TestFetchFilters", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDiscoveryManifest_ConstrainRecipes", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestDiscoveryManifest_ConstrainRecipes", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestGoTaskGeneralError", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestGoTaskGeneralError", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldInstallInfraAgent_Default", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestShouldInstallInfraAgent_Default", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldInstallInfraAgent_RecipePathsProvided", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestShouldInstallInfraAgent_RecipePathsProvided", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldInstallLogging_Default", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestShouldInstallLogging_Default", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldInstallLogging_RecipesProvided", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestShouldInstallLogging_RecipesProvided", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldInstallIntegrations_Default", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestShouldInstallIntegrations_Default", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldInstallLogging_RecipePathsProvided", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestShouldInstallLogging_RecipePathsProvided", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldInstallApm_Default", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestShouldInstallApm_Default", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldInstallApm_RecipePathsProvided", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestShouldInstallApm_RecipePathsProvided", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeNamesProvided", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestRecipeNamesProvided", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipePathsProvided", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestRecipePathsProvided", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipesProvided", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestRecipesProvided", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestToStringByFieldName", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestToStringByFieldName", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/types", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestRecipeVars_ToSlice", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/types", + "test": "TestRecipeVars_ToSlice", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/ux", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestPlainProgressIndicator_interface", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/ux", + "test": "TestPlainProgressIndicator_interface", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/validation", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestValidate", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/validation", + "test": "TestValidate", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/validation", + "durationMs": 40, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestValidate_PassAfterNAttempts", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/validation", + "test": "TestValidate_PassAfterNAttempts", + "time": "0.040000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/validation", + "durationMs": 30, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestValidate_FailAfterNAttempts", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/validation", + "test": "TestValidate_FailAfterNAttempts", + "time": "0.030000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/validation", + "durationMs": 10, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestValidate_FailAfterMaxAttempts", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/validation", + "test": "TestValidate_FailAfterMaxAttempts", + "time": "0.010000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/validation", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestValidate_FailIfContextDone", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/validation", + "test": "TestValidate_FailIfContextDone", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/install/validation", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestValidate_QueryError", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/install/validation", + "test": "TestValidate_QueryError", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/nerdgraph", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestQuery", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/nerdgraph", + "test": "TestQuery", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/nerdgraph", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestNerdGraphCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/nerdgraph", + "test": "TestNerdGraphCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestCollectionGet", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "test": "TestCollectionGet", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestCollectionDelete", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "test": "TestCollectionDelete", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDocumentWrite", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "test": "TestDocumentWrite", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDocumentGet", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "test": "TestDocumentGet", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDocumentDelete", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "test": "TestDocumentDelete", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestNerdStorageCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/nerdstorage", + "test": "TestNerdStorageCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/nrql", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestQuery", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/nrql", + "test": "TestQuery", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/nrql", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestNerdGraphCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/nrql", + "test": "TestNerdGraphCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/pipe", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestStdinPipeReader", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/pipe", + "test": "TestStdinPipeReader", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/pipe", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestJsonToFilteredMap", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/pipe", + "test": "TestJsonToFilteredMap", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/pipe", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestReadStdin", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/pipe", + "test": "TestReadStdin", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/pipe", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestGetPipeInputInnerFunc", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/pipe", + "test": "TestGetPipeInputInnerFunc", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/pipe", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestGetPipeInput", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/pipe", + "test": "TestGetPipeInput", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/pipe", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestGet", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/pipe", + "test": "TestGet", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/profile", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestProfilesCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/profile", + "test": "TestProfilesCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/profile", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestCredentialsAdd", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/profile", + "test": "TestCredentialsAdd", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/profile", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestProfilesDefault", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/profile", + "test": "TestProfilesDefault", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/profile", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestProfilesList", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/profile", + "test": "TestProfilesList", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/profile", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestProfilesDelete", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/profile", + "test": "TestProfilesDelete", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/reporting", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestJUnit", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/reporting", + "test": "TestJUnit", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/reporting", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestNerdGraphCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/reporting", + "test": "TestNerdGraphCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/utils", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestJq", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/utils", + "test": "TestJq", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/utils", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestSemver", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/utils", + "test": "TestSemver", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/utils", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestSemverCheck", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/utils", + "test": "TestSemverCheck", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/utils", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestTerraform", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/utils", + "test": "TestTerraform", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/utils", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestTerraformDashboard", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/utils", + "test": "TestTerraformDashboard", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/utils", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestUtilsCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/utils", + "test": "TestUtilsCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/utils", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldRetryAndPass", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/utils", + "test": "TestShouldRetryAndPass", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/utils", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldRetryAndFail", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/utils", + "test": "TestShouldRetryAndFail", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/utils", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestShouldNotRetry", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/utils", + "test": "TestShouldNotRetry", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/utils", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestStructToMap", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/utils", + "test": "TestStructToMap", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/workload", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestNerdGraphCommand", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/workload", + "test": "TestNerdGraphCommand", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/workload", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestList", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/workload", + "test": "TestList", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/workload", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestGet", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/workload", + "test": "TestGet", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/workload", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestCreate", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/workload", + "test": "TestCreate", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/workload", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestUpdate", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/workload", + "test": "TestUpdate", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/workload", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDuplicate", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/workload", + "test": "TestDuplicate", + "time": "0.000000" +}, { + "classname": "github.com/newrelic/newrelic-cli/internal/workload", + "durationMs": 0, + "eventType": "TestRun", + "go.version": "go1.16.4 darwin/amd64", + "id": "125e067c-b501-42b1-b121-f05e9a423912", + "name": "TestDelete", + "package": "", + "status": "passed", + "suite": "github.com/newrelic/newrelic-cli/internal/workload", + "test": "TestDelete", + "time": "0.000000" +}] \ No newline at end of file