From 7eb61548904dcca29dfb62f46128c67cea0cdb26 Mon Sep 17 00:00:00 2001 From: Chris Trombley Date: Tue, 11 May 2021 09:59:34 -0700 Subject: [PATCH 1/5] feat(diagnose): add validate subcommand --- internal/diagnose/command_validate.go | 32 +++++ internal/diagnose/config_validator.go | 69 +++++++++ .../discovery/noop_process_filterer.go | 17 +++ .../execution/go_task_recipe_executor_test.go | 3 +- internal/install/recipe_installer.go | 2 +- internal/install/recipes/recipe_file.go | 3 +- .../validation/mock_recipe_validator.go | 10 +- .../validation/polling_recipe_validator.go | 123 ++-------------- .../polling_recipe_validator_test.go | 42 +++--- .../install/validation/recipe_validator.go | 2 +- .../validation => utils}/nrdb_client.go | 4 +- .../validation/polling_nrql_validator.go | 132 ++++++++++++++++++ 12 files changed, 299 insertions(+), 140 deletions(-) create mode 100644 internal/diagnose/command_validate.go create mode 100644 internal/diagnose/config_validator.go create mode 100644 internal/install/discovery/noop_process_filterer.go rename internal/{install/validation => utils}/nrdb_client.go (77%) create mode 100644 internal/utils/validation/polling_nrql_validator.go diff --git a/internal/diagnose/command_validate.go b/internal/diagnose/command_validate.go new file mode 100644 index 000000000..a722a8efb --- /dev/null +++ b/internal/diagnose/command_validate.go @@ -0,0 +1,32 @@ +package diagnose + +import ( + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-client-go/newrelic" +) + +var cmdValidate = &cobra.Command{ + Use: "validate", + Short: "Validate your CLI configuration and connectivity", + Long: `Validate your CLI configuration and connectivity. + +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", + Run: func(cmd *cobra.Command, args []string) { + client.WithClient(func(nrClient *newrelic.NewRelic) { + v := NewConfigValidator(nrClient) + err := v.ValidateConfig(cmd.Context()) + if err != nil { + log.Fatal(err) + } + }) + }, +} + +func init() { + Command.AddCommand(cmdValidate) +} diff --git a/internal/diagnose/config_validator.go b/internal/diagnose/config_validator.go new file mode 100644 index 000000000..078a91cfb --- /dev/null +++ b/internal/diagnose/config_validator.go @@ -0,0 +1,69 @@ +package diagnose + +import ( + "context" + "fmt" + + log "github.com/sirupsen/logrus" + + "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/install/discovery" + "github.com/newrelic/newrelic-cli/internal/utils/validation" + "github.com/newrelic/newrelic-client-go/newrelic" +) + +const ( + validationEventType = "NrIntegrationError" +) + +type ConfigValidator struct { + client *newrelic.NewRelic + *validation.PollingNRQLValidator + discovery.Discoverer +} + +type ValidationTracerEvent struct { + EventType string `json:"eventType"` + Hostname string `json:"hostname"` +} + +func NewConfigValidator(client *newrelic.NewRelic) *ConfigValidator { + pf := discovery.NewNoOpProcessFilterer() + + return &ConfigValidator{ + client: client, + PollingNRQLValidator: validation.NewPollingNRQLValidator(&client.Nrdb), + Discoverer: discovery.NewPSUtilDiscoverer(pf), + } +} + +func (c *ConfigValidator) ValidateConfig(ctx context.Context) error { + defaultProfile := credentials.DefaultProfile() + manifest, err := c.Discover(ctx) + if err != nil { + return err + } + + evt := ValidationTracerEvent{ + EventType: validationEventType, + Hostname: manifest.Hostname, + } + + log.Printf("Sending tracer event to New Relic.") + + err = c.client.Events.CreateEvent(defaultProfile.AccountID, evt) + if err != nil { + return err + } + + query := fmt.Sprintf(` + FROM %s + SELECT count(*) + WHERE hostname LIKE '%s%%' + SINCE 10 MINUTES AGO + `, validationEventType, manifest.Hostname) + + _, err = c.Validate(ctx, query) + + return err +} diff --git a/internal/install/discovery/noop_process_filterer.go b/internal/install/discovery/noop_process_filterer.go new file mode 100644 index 000000000..792aa88a3 --- /dev/null +++ b/internal/install/discovery/noop_process_filterer.go @@ -0,0 +1,17 @@ +package discovery + +import ( + "context" + + "github.com/newrelic/newrelic-cli/internal/install/types" +) + +type NoOpProcessFilterer struct{} + +func NewNoOpProcessFilterer() *NoOpProcessFilterer { + return &NoOpProcessFilterer{} +} + +func (f *NoOpProcessFilterer) filter(ctx context.Context, processes []types.GenericProcess, manifest types.DiscoveryManifest) ([]types.MatchedProcess, error) { + return []types.MatchedProcess{}, nil +} diff --git a/internal/install/execution/go_task_recipe_executor_test.go b/internal/install/execution/go_task_recipe_executor_test.go index 4da4a059c..74f5270ce 100644 --- a/internal/install/execution/go_task_recipe_executor_test.go +++ b/internal/install/execution/go_task_recipe_executor_test.go @@ -14,9 +14,10 @@ import ( "gopkg.in/yaml.v2" "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" - "github.com/stretchr/testify/require" ) func TestExecute_SystemVariableInterpolation(t *testing.T) { diff --git a/internal/install/recipe_installer.go b/internal/install/recipe_installer.go index 11e04668f..137dfac0f 100644 --- a/internal/install/recipe_installer.go +++ b/internal/install/recipe_installer.go @@ -232,7 +232,7 @@ func (i *RecipeInstaller) executeAndValidate(ctx context.Context, m *types.Disco var validationDurationMilliseconds int64 start := time.Now() if r.ValidationNRQL != "" { - entityGUID, err = i.recipeValidator.Validate(ctx, *m, *r) + entityGUID, err = i.recipeValidator.ValidateRecipe(ctx, *m, *r) if err != nil { validationDurationMilliseconds = time.Since(start).Milliseconds() msg := fmt.Sprintf("encountered an error while validating receipt of data for %s: %s", r.Name, err) diff --git a/internal/install/recipes/recipe_file.go b/internal/install/recipes/recipe_file.go index 5b8101772..60d21b350 100644 --- a/internal/install/recipes/recipe_file.go +++ b/internal/install/recipes/recipe_file.go @@ -6,8 +6,9 @@ import ( "net/http" "net/url" - "github.com/newrelic/newrelic-cli/internal/install/types" "gopkg.in/yaml.v2" + + "github.com/newrelic/newrelic-cli/internal/install/types" ) type RecipeFileFetcherImpl struct { diff --git a/internal/install/validation/mock_recipe_validator.go b/internal/install/validation/mock_recipe_validator.go index 2afc12741..b44440886 100644 --- a/internal/install/validation/mock_recipe_validator.go +++ b/internal/install/validation/mock_recipe_validator.go @@ -20,7 +20,15 @@ func NewMockRecipeValidator() *MockRecipeValidator { return &MockRecipeValidator{} } -func (m *MockRecipeValidator) Validate(ctx context.Context, dm types.DiscoveryManifest, r types.OpenInstallationRecipe) (string, error) { +func (m *MockRecipeValidator) ValidateQuery(ctx context.Context, query string) (string, error) { + return m.validate(ctx) +} + +func (m *MockRecipeValidator) ValidateRecipe(ctx context.Context, dm types.DiscoveryManifest, r types.OpenInstallationRecipe) (string, error) { + return m.validate(ctx) +} + +func (m *MockRecipeValidator) validate(ctx context.Context) (string, error) { m.ValidateCallCount++ var err error diff --git a/internal/install/validation/polling_recipe_validator.go b/internal/install/validation/polling_recipe_validator.go index 5bd340851..9ab664ecc 100644 --- a/internal/install/validation/polling_recipe_validator.go +++ b/internal/install/validation/polling_recipe_validator.go @@ -3,127 +3,38 @@ package validation import ( "bytes" "context" - "errors" - "fmt" "html/template" - "time" - "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" + "github.com/newrelic/newrelic-cli/internal/utils" + utilsValidation "github.com/newrelic/newrelic-cli/internal/utils/validation" ) type contextKey int -const ( - defaultMaxAttempts = 60 - defaultInterval = 5 * time.Second - TestIdentifierKey contextKey = iota -) - // PollingRecipeValidator is an implementation of the RecipeValidator interface // that polls NRDB to assert data is being reported for the given recipe. type PollingRecipeValidator struct { - maxAttempts int - interval time.Duration - client nrdbClient - progressIndicator ux.ProgressIndicator + utilsValidation.PollingNRQLValidator } // NewPollingRecipeValidator returns a new instance of PollingRecipeValidator. -func NewPollingRecipeValidator(c nrdbClient) *PollingRecipeValidator { +func NewPollingRecipeValidator(c utils.NRDBClient) *PollingRecipeValidator { v := PollingRecipeValidator{ - maxAttempts: defaultMaxAttempts, - interval: defaultInterval, - client: c, - progressIndicator: ux.NewSpinner(), + PollingNRQLValidator: *utilsValidation.NewPollingNRQLValidator(c), } return &v } -// Validate polls NRDB to assert data is being reported for the given recipe. -func (m *PollingRecipeValidator) Validate(ctx context.Context, dm types.DiscoveryManifest, r types.OpenInstallationRecipe) (string, error) { - return m.waitForData(ctx, dm, r) -} - -func (m *PollingRecipeValidator) waitForData(ctx context.Context, dm types.DiscoveryManifest, r types.OpenInstallationRecipe) (string, error) { - count := 0 - ticker := time.NewTicker(m.interval) - defer ticker.Stop() - - progressMsg := "Checking for data in New Relic (this may take a few minutes)..." - m.progressIndicator.Start(progressMsg) - defer m.progressIndicator.Stop() - - for { - if count == m.maxAttempts { - m.progressIndicator.Fail("") - return "", fmt.Errorf("reached max validation attempts") - } - - ok, entityGUID, err := m.tryValidate(ctx, dm, r) - if err != nil { - m.progressIndicator.Fail("") - return "", err - } - - count++ - - if ok { - m.progressIndicator.Success("") - return entityGUID, nil - } - - select { - case <-ticker.C: - continue - - case <-ctx.Done(): - m.progressIndicator.Fail("") - return "", fmt.Errorf("validation cancelled") - } - } -} - -func (m *PollingRecipeValidator) tryValidate(ctx context.Context, dm types.DiscoveryManifest, r types.OpenInstallationRecipe) (bool, string, error) { +// ValidateRecipe polls NRDB to assert data is being reported for the given recipe. +func (m *PollingRecipeValidator) ValidateRecipe(ctx context.Context, dm types.DiscoveryManifest, r types.OpenInstallationRecipe) (string, error) { query, err := substituteHostname(dm, r) if err != nil { - return false, "", err - } - - results, err := m.executeQuery(ctx, query) - if err != nil { - return false, "", err - } - - if len(results) == 0 { - return false, "", nil - } - - // The query is assumed to use a count aggregate function - count := results[0]["count"].(float64) - - if count > 0 { - // Try and parse an entity GUID from the results. The query is assumed to - // optionally use a facet over entityGuid. The standard case seems to be - // that all entities contain a facet of "entityGuid", and so if we find it - // here, we return it. - if entityGUID, ok := results[0]["entityGuid"]; ok { - return true, entityGUID.(string), nil - } - - // In the logs integration, the facet doesn't contain "entityGuid", but - // does contain, "entity.guid", so here we check for that also. - if entityGUID, ok := results[0]["entity.guids"]; ok { - return true, entityGUID.(string), nil - } - - return true, "", nil + return "", err } - return false, "", nil + return m.Validate(ctx, query) } func substituteHostname(dm types.DiscoveryManifest, r types.OpenInstallationRecipe) (string, error) { @@ -145,19 +56,3 @@ func substituteHostname(dm types.DiscoveryManifest, r types.OpenInstallationReci return tpl.String(), nil } - -func (m *PollingRecipeValidator) 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") - } - - nrql := nrdb.NRQL(query) - - result, err := m.client.QueryWithContext(ctx, profile.AccountID, nrql) - if err != nil { - return nil, err - } - - return result.Results, nil -} diff --git a/internal/install/validation/polling_recipe_validator_test.go b/internal/install/validation/polling_recipe_validator_test.go index f7c5d2678..838afaaec 100644 --- a/internal/install/validation/polling_recipe_validator_test.go +++ b/internal/install/validation/polling_recipe_validator_test.go @@ -15,6 +15,10 @@ import ( "github.com/newrelic/newrelic-client-go/pkg/nrdb" ) +const ( + TestIdentifierKey contextKey = iota +) + var ( emptyResults = []nrdb.NRDBResult{ map[string]interface{}{ @@ -36,12 +40,12 @@ func TestValidate(t *testing.T) { pi := ux.NewMockProgressIndicator() v := NewPollingRecipeValidator(c) - v.progressIndicator = pi + v.ProgressIndicator = pi r := types.OpenInstallationRecipe{} m := types.DiscoveryManifest{} - _, err := v.Validate(getTestContext(), m, r) + _, err := v.ValidateRecipe(getTestContext(), m, r) require.NoError(t, err) } @@ -51,16 +55,16 @@ func TestValidate_PassAfterNAttempts(t *testing.T) { c := NewMockNRDBClient() pi := ux.NewMockProgressIndicator() v := NewPollingRecipeValidator(c) - v.progressIndicator = pi - v.maxAttempts = 5 - v.interval = 10 * time.Millisecond + v.ProgressIndicator = pi + v.MaxAttempts = 5 + v.Interval = 10 * time.Millisecond c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 5) r := types.OpenInstallationRecipe{} m := types.DiscoveryManifest{} - _, err := v.Validate(getTestContext(), m, r) + _, err := v.ValidateRecipe(getTestContext(), m, r) require.NoError(t, err) require.Equal(t, 5, c.Attempts()) @@ -71,14 +75,14 @@ func TestValidate_FailAfterNAttempts(t *testing.T) { c := NewMockNRDBClient() pi := ux.NewMockProgressIndicator() v := NewPollingRecipeValidator(c) - v.progressIndicator = pi - v.maxAttempts = 3 - v.interval = 10 * time.Millisecond + v.ProgressIndicator = pi + v.MaxAttempts = 3 + v.Interval = 10 * time.Millisecond r := types.OpenInstallationRecipe{} m := types.DiscoveryManifest{} - _, err := v.Validate(getTestContext(), m, r) + _, err := v.ValidateRecipe(getTestContext(), m, r) require.Error(t, err) require.Equal(t, 3, c.Attempts()) @@ -92,14 +96,14 @@ func TestValidate_FailAfterMaxAttempts(t *testing.T) { pi := ux.NewMockProgressIndicator() v := NewPollingRecipeValidator(c) - v.progressIndicator = pi - v.maxAttempts = 1 - v.interval = 10 * time.Millisecond + v.ProgressIndicator = pi + v.MaxAttempts = 1 + v.Interval = 10 * time.Millisecond r := types.OpenInstallationRecipe{} m := types.DiscoveryManifest{} - _, err := v.Validate(getTestContext(), m, r) + _, err := v.ValidateRecipe(getTestContext(), m, r) require.Error(t, err) } @@ -112,8 +116,8 @@ func TestValidate_FailIfContextDone(t *testing.T) { pi := ux.NewMockProgressIndicator() v := NewPollingRecipeValidator(c) - v.progressIndicator = pi - v.interval = 1 * time.Second + v.ProgressIndicator = pi + v.Interval = 1 * time.Second r := types.OpenInstallationRecipe{} m := types.DiscoveryManifest{} @@ -121,7 +125,7 @@ func TestValidate_FailIfContextDone(t *testing.T) { ctx, cancel := context.WithCancel(getTestContext()) cancel() - _, err := v.Validate(ctx, m, r) + _, err := v.ValidateRecipe(ctx, m, r) require.Error(t, err) } @@ -134,12 +138,12 @@ func TestValidate_QueryError(t *testing.T) { pi := ux.NewMockProgressIndicator() v := NewPollingRecipeValidator(c) - v.progressIndicator = pi + v.ProgressIndicator = pi r := types.OpenInstallationRecipe{} m := types.DiscoveryManifest{} - _, err := v.Validate(getTestContext(), m, r) + _, err := v.ValidateRecipe(getTestContext(), m, r) require.EqualError(t, err, "test error") } diff --git a/internal/install/validation/recipe_validator.go b/internal/install/validation/recipe_validator.go index 54ad54959..c9f2a2496 100644 --- a/internal/install/validation/recipe_validator.go +++ b/internal/install/validation/recipe_validator.go @@ -8,5 +8,5 @@ import ( // RecipeValidator validates installation of a recipe. type RecipeValidator interface { - Validate(context.Context, types.DiscoveryManifest, types.OpenInstallationRecipe) (entityGUID string, err error) + ValidateRecipe(context.Context, types.DiscoveryManifest, types.OpenInstallationRecipe) (entityGUID string, err error) } diff --git a/internal/install/validation/nrdb_client.go b/internal/utils/nrdb_client.go similarity index 77% rename from internal/install/validation/nrdb_client.go rename to internal/utils/nrdb_client.go index 328f39ca5..0061541b6 100644 --- a/internal/install/validation/nrdb_client.go +++ b/internal/utils/nrdb_client.go @@ -1,4 +1,4 @@ -package validation +package utils import ( "context" @@ -6,6 +6,6 @@ import ( "github.com/newrelic/newrelic-client-go/pkg/nrdb" ) -type nrdbClient interface { +type NRDBClient interface { QueryWithContext(context.Context, int, nrdb.NRQL) (*nrdb.NRDBResultContainer, error) } diff --git a/internal/utils/validation/polling_nrql_validator.go b/internal/utils/validation/polling_nrql_validator.go new file mode 100644 index 000000000..edcbda9cf --- /dev/null +++ b/internal/utils/validation/polling_nrql_validator.go @@ -0,0 +1,132 @@ +package validation + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/newrelic/newrelic-cli/internal/credentials" + "github.com/newrelic/newrelic-cli/internal/install/ux" + "github.com/newrelic/newrelic-cli/internal/utils" + "github.com/newrelic/newrelic-client-go/pkg/nrdb" +) + +const ( + defaultMaxAttempts = 60 + defaultInterval = 5 * time.Second +) + +// PollingNRQLValidator polls NRDB to assert data is being reported for the given query. +type PollingNRQLValidator struct { + MaxAttempts int + Interval time.Duration + ProgressIndicator ux.ProgressIndicator + client utils.NRDBClient +} + +// NewPollingNRQLValidator returns a new instance of PollingNRQLValidator. +func NewPollingNRQLValidator(c utils.NRDBClient) *PollingNRQLValidator { + v := PollingNRQLValidator{ + client: c, + MaxAttempts: defaultMaxAttempts, + Interval: defaultInterval, + ProgressIndicator: ux.NewSpinner(), + } + + return &v +} + +// Validate polls NRDB to assert data is being reported for the given query. +func (m *PollingNRQLValidator) Validate(ctx context.Context, query string) (string, error) { + return m.waitForData(ctx, query) +} + +func (m *PollingNRQLValidator) waitForData(ctx context.Context, query string) (string, error) { + count := 0 + ticker := time.NewTicker(m.Interval) + defer ticker.Stop() + + progressMsg := "Checking for data in New Relic (this may take a few minutes)..." + m.ProgressIndicator.Start(progressMsg) + defer m.ProgressIndicator.Stop() + + for { + if count == m.MaxAttempts { + m.ProgressIndicator.Fail("") + return "", fmt.Errorf("reached max validation attempts") + } + + ok, entityGUID, err := m.tryValidate(ctx, query) + if err != nil { + m.ProgressIndicator.Fail("") + return "", err + } + + count++ + + if ok { + m.ProgressIndicator.Success("") + return entityGUID, nil + } + + select { + case <-ticker.C: + continue + + case <-ctx.Done(): + m.ProgressIndicator.Fail("") + return "", fmt.Errorf("validation cancelled") + } + } +} + +func (m *PollingNRQLValidator) tryValidate(ctx context.Context, query string) (bool, string, error) { + results, err := m.executeQuery(ctx, query) + if err != nil { + return false, "", err + } + + if len(results) == 0 { + return false, "", nil + } + + // The query is assumed to use a count aggregate function + count := results[0]["count"].(float64) + + if count > 0 { + // Try and parse an entity GUID from the results. The query is assumed to + // optionally use a facet over entityGuid. The standard case seems to be + // that all entities contain a facet of "entityGuid", and so if we find it + // here, we return it. + if entityGUID, ok := results[0]["entityGuid"]; ok { + return true, entityGUID.(string), nil + } + + // In the logs integration, the facet doesn't contain "entityGuid", but + // does contain, "entity.guid", so here we check for that also. + if entityGUID, ok := results[0]["entity.guids"]; ok { + return true, entityGUID.(string), nil + } + + return true, "", nil + } + + return false, "", nil +} + +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") + } + + nrql := nrdb.NRQL(query) + + result, err := m.client.QueryWithContext(ctx, profile.AccountID, nrql) + if err != nil { + return nil, err + } + + return result.Results, nil +} From 0bea130153c7a641c474f5b8131454ef58876892 Mon Sep 17 00:00:00 2001 From: Chris Trombley Date: Wed, 12 May 2021 22:57:28 -0700 Subject: [PATCH 2/5] feat(newrelic): bootstrap an insights insert key on first use --- cmd/newrelic/command.go | 45 +++++++++++++++++++++--- cmd/newrelic/command_integration_test.go | 2 ++ internal/diagnose/command_validate.go | 13 ++++++- internal/diagnose/config_validator.go | 43 +++++++++++++++------- internal/install/types/errors.go | 1 + 5 files changed, 86 insertions(+), 18 deletions(-) diff --git a/cmd/newrelic/command.go b/cmd/newrelic/command.go index f247ff7d5..e5623f72e 100644 --- a/cmd/newrelic/command.go +++ b/cmd/newrelic/command.go @@ -44,6 +44,7 @@ func initializeProfile() { var accountID int var region string var licenseKey string + var insightsInsertKey string var err error credentials.WithCredentials(func(c *credentials.Credentials) { @@ -56,6 +57,7 @@ func initializeProfile() { 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 == "" { @@ -96,12 +98,21 @@ func initializeProfile() { } } + 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, + Region: region, + APIKey: apiKey, + AccountID: accountID, + LicenseKey: licenseKey, + InsightsInsertKey: insightsInsertKey, } err = c.AddProfile(defaultProfileName, p) @@ -166,6 +177,32 @@ func fetchLicenseKey(client *newrelic.NewRelic, accountID int) (string, error) { return "", types.ErrorFetchingLicenseKey } +type insightsKey struct { + ID int `json:"id"` + Key string `json:"key"` +} + +func fetchInsightsInsertKey(client *newrelic.NewRelic, accountID int) (string, error) { + // 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 "", 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) { diff --git a/cmd/newrelic/command_integration_test.go b/cmd/newrelic/command_integration_test.go index 6b7b7d54e..3e383f788 100644 --- a/cmd/newrelic/command_integration_test.go +++ b/cmd/newrelic/command_integration_test.go @@ -60,6 +60,8 @@ func TestInitializeProfile(t *testing.T) { 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. diff --git a/internal/diagnose/command_validate.go b/internal/diagnose/command_validate.go index a722a8efb..5372b5351 100644 --- a/internal/diagnose/command_validate.go +++ b/internal/diagnose/command_validate.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" "github.com/newrelic/newrelic-cli/internal/client" + "github.com/newrelic/newrelic-cli/internal/utils" "github.com/newrelic/newrelic-client-go/newrelic" ) @@ -19,8 +20,18 @@ data to the New Relic platform and verifying that it has been received.`, Run: func(cmd *cobra.Command, args []string) { client.WithClient(func(nrClient *newrelic.NewRelic) { v := NewConfigValidator(nrClient) - err := v.ValidateConfig(cmd.Context()) + err := v.ValidateConfig(utils.SignalCtx) if err != nil { + if err == ErrDiscovery { + log.Fatal("Failed to detect your system's hostname. Please contact New Relic support.") + } + if err == ErrPostEvent { + log.Fatal("There was a failure posting data to New Relic. This could be ") + } + if err == ErrValidation { + log.Fatal("validation failed!") + } + log.Fatal(err) } }) diff --git a/internal/diagnose/config_validator.go b/internal/diagnose/config_validator.go index 078a91cfb..477ea612f 100644 --- a/internal/diagnose/config_validator.go +++ b/internal/diagnose/config_validator.go @@ -2,14 +2,17 @@ package diagnose import ( "context" + "errors" "fmt" + "reflect" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/newrelic/newrelic-cli/internal/credentials" - "github.com/newrelic/newrelic-cli/internal/install/discovery" "github.com/newrelic/newrelic-cli/internal/utils/validation" "github.com/newrelic/newrelic-client-go/newrelic" + "github.com/shirou/gopsutil/host" ) const ( @@ -19,51 +22,65 @@ const ( type ConfigValidator struct { client *newrelic.NewRelic *validation.PollingNRQLValidator - discovery.Discoverer } type ValidationTracerEvent struct { EventType string `json:"eventType"` Hostname string `json:"hostname"` + Purpose string `json:"purpose"` + GUID string `json:"guid"` } func NewConfigValidator(client *newrelic.NewRelic) *ConfigValidator { - pf := discovery.NewNoOpProcessFilterer() + v := validation.NewPollingNRQLValidator(&client.Nrdb) + v.MaxAttempts = 20 return &ConfigValidator{ client: client, - PollingNRQLValidator: validation.NewPollingNRQLValidator(&client.Nrdb), - Discoverer: discovery.NewPSUtilDiscoverer(pf), + PollingNRQLValidator: v, } } func (c *ConfigValidator) ValidateConfig(ctx context.Context) error { defaultProfile := credentials.DefaultProfile() - manifest, err := c.Discover(ctx) + + i, err := host.InfoWithContext(ctx) if err != nil { - return err + log.Error(err) + return ErrDiscovery } evt := ValidationTracerEvent{ EventType: validationEventType, - Hostname: manifest.Hostname, + Hostname: i.Hostname, + Purpose: "New Relic CLI configuration validation", + GUID: uuid.NewString(), } log.Printf("Sending tracer event to New Relic.") - err = c.client.Events.CreateEvent(defaultProfile.AccountID, evt) - if err != nil { - return err + if err = c.client.Events.CreateEvent(defaultProfile.AccountID, evt); err != nil { + log.Error(reflect.TypeOf(err)) + log.Error(err) + return ErrPostEvent } query := fmt.Sprintf(` FROM %s SELECT count(*) WHERE hostname LIKE '%s%%' + AND guid = '%s' SINCE 10 MINUTES AGO - `, validationEventType, manifest.Hostname) + `, evt.EventType, evt.Hostname, evt.GUID) - _, err = c.Validate(ctx, query) + if _, err = c.Validate(ctx, query); err != nil { + log.Error(err) + err = ErrValidation + } return err } + +var ErrDiscovery = errors.New("discovery failed") +var ErrPostEvent = errors.New("posting an event failed") +var ErrValidation = errors.New("validation failed") diff --git a/internal/install/types/errors.go b/internal/install/types/errors.go index 80e6c2866..302ac3cf8 100644 --- a/internal/install/types/errors.go +++ b/internal/install/types/errors.go @@ -9,3 +9,4 @@ var ErrInterrupt = errors.New("operation canceled") // nolint: golint var ErrorFetchingLicenseKey = errors.New("Oops, we're having some difficulties fetching your license key. Please try again later, or see our documentation for installing manually https://docs.newrelic.com/docs/using-new-relic/cross-product-functions/install-configure/install-new-relic") +var ErrorFetchingInsightsInsertKey = errors.New("error retrieving Insights insert key") From da0135994d9a814d8e01907f9af62f7397c7ee03 Mon Sep 17 00:00:00 2001 From: Chris Trombley Date: Thu, 13 May 2021 12:51:36 -0700 Subject: [PATCH 3/5] chore(apiaccess): update newrelic-client-go --- cmd/newrelic/command.go | 5 ----- go.mod | 2 +- go.sum | 8 ++++---- internal/diagnose/config_validator.go | 3 ++- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/cmd/newrelic/command.go b/cmd/newrelic/command.go index e5623f72e..1cccaf5f7 100644 --- a/cmd/newrelic/command.go +++ b/cmd/newrelic/command.go @@ -177,11 +177,6 @@ func fetchLicenseKey(client *newrelic.NewRelic, accountID int) (string, error) { return "", types.ErrorFetchingLicenseKey } -type insightsKey struct { - ID int `json:"id"` - Key string `json:"key"` -} - func fetchInsightsInsertKey(client *newrelic.NewRelic, accountID int) (string, error) { // Check for an existing key first keys, err := client.APIAccess.ListInsightsInsertKeys(accountID) diff --git a/go.mod b/go.mod index b4bdb25b2..d6bdd8c75 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/llorllale/go-gitlint v0.0.0-20200802191503-5984945d4b80 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.4.1 - github.com/newrelic/newrelic-client-go v0.58.5 + github.com/newrelic/newrelic-client-go v0.59.0 github.com/newrelic/tutone v0.6.1 github.com/pkg/errors v0.9.1 github.com/psampaz/go-mod-outdated v0.8.0 diff --git a/go.sum b/go.sum index 1f8ca7a9a..b4b265600 100644 --- a/go.sum +++ b/go.sum @@ -527,8 +527,8 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= -github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4= +github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -760,8 +760,8 @@ github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5w github.com/nbutton23/zxcvbn-go v0.0.0-20201221231540-e56b841a3c88/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA= github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= -github.com/newrelic/newrelic-client-go v0.58.5 h1:Bp9vzKjc/9nQXvO1q/cdRW6YppGhXAHh8GZrlevIrVM= -github.com/newrelic/newrelic-client-go v0.58.5/go.mod h1:+wOK+2m1ClVuAqnpMAW7v924ryKW1eYylvkPJR0b3PA= +github.com/newrelic/newrelic-client-go v0.59.0 h1:SQsZmVdE0elfioi3EoMCv0sNyKsZ4QnYOeoUfVzzkzA= +github.com/newrelic/newrelic-client-go v0.59.0/go.mod h1:CIVFEfoZomM9/V1eowdwCv9TG6Eid5X587leQVIwCKo= github.com/newrelic/tutone v0.6.1 h1:hO3gumrOvTeIQjHHEB8J9oGloC0GHxbIAXU1FazSe6Q= github.com/newrelic/tutone v0.6.1/go.mod h1:dOSVZu/5kTucS3dxLIf6S5Ra3FPzBcT9NDBm4kfDGx0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= diff --git a/internal/diagnose/config_validator.go b/internal/diagnose/config_validator.go index 477ea612f..4217134f6 100644 --- a/internal/diagnose/config_validator.go +++ b/internal/diagnose/config_validator.go @@ -9,10 +9,11 @@ import ( "github.com/google/uuid" log "github.com/sirupsen/logrus" + "github.com/shirou/gopsutil/host" + "github.com/newrelic/newrelic-cli/internal/credentials" "github.com/newrelic/newrelic-cli/internal/utils/validation" "github.com/newrelic/newrelic-client-go/newrelic" - "github.com/shirou/gopsutil/host" ) const ( From f46290e9e094b5c53f43bc5b3c223bb87b283cf2 Mon Sep 17 00:00:00 2001 From: Chris Trombley Date: Thu, 13 May 2021 14:36:43 -0700 Subject: [PATCH 4/5] chore(diagnose): update error messages --- internal/diagnose/command_validate.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/diagnose/command_validate.go b/internal/diagnose/command_validate.go index 5372b5351..155e98727 100644 --- a/internal/diagnose/command_validate.go +++ b/internal/diagnose/command_validate.go @@ -26,10 +26,10 @@ data to the New Relic platform and verifying that it has been received.`, log.Fatal("Failed to detect your system's hostname. Please contact New Relic support.") } if err == ErrPostEvent { - log.Fatal("There was a failure posting data to New Relic. This could be ") + log.Fatal("There was a failure posting data to New Relic.") } if err == ErrValidation { - log.Fatal("validation failed!") + log.Fatal("There was a failure locating the data that was posted to New Relic.") } log.Fatal(err) From a4f727f1f434cd53b59ab042874868429c896c04 Mon Sep 17 00:00:00 2001 From: Chris Trombley Date: Thu, 13 May 2021 14:40:32 -0700 Subject: [PATCH 5/5] chore(diagnose): clean up mock recipe validator contract --- internal/install/validation/mock_recipe_validator.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internal/install/validation/mock_recipe_validator.go b/internal/install/validation/mock_recipe_validator.go index b44440886..528b93905 100644 --- a/internal/install/validation/mock_recipe_validator.go +++ b/internal/install/validation/mock_recipe_validator.go @@ -20,15 +20,7 @@ func NewMockRecipeValidator() *MockRecipeValidator { return &MockRecipeValidator{} } -func (m *MockRecipeValidator) ValidateQuery(ctx context.Context, query string) (string, error) { - return m.validate(ctx) -} - func (m *MockRecipeValidator) ValidateRecipe(ctx context.Context, dm types.DiscoveryManifest, r types.OpenInstallationRecipe) (string, error) { - return m.validate(ctx) -} - -func (m *MockRecipeValidator) validate(ctx context.Context) (string, error) { m.ValidateCallCount++ var err error