diff --git a/cmd/newrelic/command.go b/cmd/newrelic/command.go index 58bb24c9b..b46e1255a 100644 --- a/cmd/newrelic/command.go +++ b/cmd/newrelic/command.go @@ -52,7 +52,6 @@ func initializeProfile() { 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") diff --git a/internal/client/client.go b/internal/client/client.go index 895dc76d4..e46daee8c 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -3,8 +3,6 @@ package client import ( "errors" "fmt" - "os" - "strings" "github.com/newrelic/newrelic-client-go/newrelic" @@ -29,8 +27,6 @@ func CreateNRClient(cfg *config.Config, creds *credentials.Credentials) (*newrel // Create the New Relic Client defProfile := creds.Default() - defProfile = applyOverrides(defProfile) - if defProfile != nil { apiKey = defProfile.APIKey insightsInsertKey = defProfile.InsightsInsertKey @@ -58,33 +54,3 @@ func CreateNRClient(cfg *config.Config, creds *credentials.Credentials) (*newrel return nrClient, defProfile, nil } - -// applyOverrides reads Profile info out of the Environment to override config -func applyOverrides(p *credentials.Profile) *credentials.Profile { - envAPIKey := os.Getenv("NEW_RELIC_API_KEY") - envInsightsInsertKey := os.Getenv("NEW_RELIC_INSIGHTS_INSERT_KEY") - envRegion := os.Getenv("NEW_RELIC_REGION") - - if envAPIKey == "" && envRegion == "" && envInsightsInsertKey == "" { - return p - } - - out := credentials.Profile{} - if p != nil { - out = *p - } - - if envAPIKey != "" { - out.APIKey = envAPIKey - } - - if envInsightsInsertKey != "" { - out.InsightsInsertKey = envInsightsInsertKey - } - - if envRegion != "" { - out.Region = strings.ToUpper(envRegion) - } - - return &out -} diff --git a/internal/config/config.go b/internal/config/config.go index f23e2f412..41319165c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,7 +5,6 @@ import ( "os" "reflect" "strings" - "time" "github.com/newrelic/newrelic-cli/internal/utils" @@ -106,9 +105,9 @@ func LoadConfig(configDir string) (*Config, error) { func (c *Config) setLogger() { log.SetFormatter(&log.TextFormatter{ - FullTimestamp: true, - TimestampFormat: time.RFC3339, - DisableLevelTruncation: true, + DisableLevelTruncation: true, + DisableTimestamp: true, + EnvironmentOverrideColors: true, }) switch level := strings.ToUpper(c.LogLevel); level { diff --git a/internal/client/client_integration_test.go b/internal/credentials/credentials_integration_test.go similarity index 92% rename from internal/client/client_integration_test.go rename to internal/credentials/credentials_integration_test.go index f2063b92c..66f0978a3 100644 --- a/internal/client/client_integration_test.go +++ b/internal/credentials/credentials_integration_test.go @@ -1,6 +1,6 @@ // +build integration -package client +package credentials import ( "io/ioutil" @@ -10,8 +10,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/newrelic/newrelic-client-go/pkg/region" - - "github.com/newrelic/newrelic-cli/internal/credentials" ) var overrideEnvVars = []string{ @@ -27,11 +25,11 @@ func TestApplyOverrides(t *testing.T) { defer os.RemoveAll(f) // Initialize the new configuration directory - c, err := credentials.LoadCredentials(f) + c, err := LoadCredentials(f) assert.NoError(t, err) // Create an initial profile to work with - testProfile := credentials.Profile{ + testProfile := Profile{ Region: "us", APIKey: "apiKeyGoesHere", InsightsInsertKey: "insightsInsertKeyGoesHere", diff --git a/internal/credentials/helpers.go b/internal/credentials/helpers.go index dba4a3bbe..e02c2feb9 100644 --- a/internal/credentials/helpers.go +++ b/internal/credentials/helpers.go @@ -27,8 +27,7 @@ func WithCredentialsFrom(configDir string, f func(c *Credentials)) { func DefaultProfile() *Profile { if defaultProfile == nil { WithCredentials(func(c *Credentials) { - p := c.Profiles[c.DefaultProfile] - defaultProfile = &p + defaultProfile = c.Default() }) } diff --git a/internal/credentials/profiles.go b/internal/credentials/profiles.go index 671ca7a95..c6bd541c8 100644 --- a/internal/credentials/profiles.go +++ b/internal/credentials/profiles.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "reflect" + "strconv" "strings" "github.com/mitchellh/mapstructure" @@ -57,13 +58,56 @@ func LoadDefaultProfile(configDir string) (string, error) { // Default returns the default profile func (c *Credentials) Default() *Profile { + var p *Profile if c.DefaultProfile != "" { if val, ok := c.Profiles[c.DefaultProfile]; ok { - return &val + p = &val } } - return nil + p = applyOverrides(p) + return p +} + +// applyOverrides reads Profile info out of the Environment to override config +func applyOverrides(p *Profile) *Profile { + envAPIKey := os.Getenv("NEW_RELIC_API_KEY") + envInsightsInsertKey := os.Getenv("NEW_RELIC_INSIGHTS_INSERT_KEY") + envRegion := os.Getenv("NEW_RELIC_REGION") + envAccountID := os.Getenv("NEW_RELIC_ACCOUNT_ID") + + if envAPIKey == "" && envRegion == "" && envInsightsInsertKey == "" && envAccountID == "" { + return p + } + + out := Profile{} + if p != nil { + out = *p + } + + if envAPIKey != "" { + out.APIKey = envAPIKey + } + + if envInsightsInsertKey != "" { + out.InsightsInsertKey = envInsightsInsertKey + } + + if envRegion != "" { + out.Region = strings.ToUpper(envRegion) + } + + if envAccountID != "" { + accountID, err := strconv.Atoi(envAccountID) + if err != nil { + log.Warnf("Invalid account ID: %s", envAccountID) + return &out + } + + out.AccountID = accountID + } + + return &out } func readDefaultProfile(configDir string) (string, error) { diff --git a/internal/install/command.go b/internal/install/command.go index 5f9c7a30b..0fd33365d 100644 --- a/internal/install/command.go +++ b/internal/install/command.go @@ -12,10 +12,11 @@ import ( ) var ( - interactiveMode bool - installLogging bool - autoDiscoveryMode bool - recipeFriendlyNames []string + specifyActions bool + interactiveMode bool + installLogging bool + recipeNames []string + recipeFilenames []string ) // Command represents the install command. @@ -25,10 +26,11 @@ var Command = &cobra.Command{ Hidden: true, Run: func(cmd *cobra.Command, args []string) { ic := installContext{ - interactiveMode: interactiveMode, - installLogging: installLogging, - autoDiscoveryMode: autoDiscoveryMode, - recipeFriendlyNames: recipeFriendlyNames, + interactiveMode: interactiveMode, + installLogging: installLogging, + recipeNames: recipeNames, + recipeFilenames: recipeFilenames, + specifyActions: specifyActions, } client.WithClientAndProfile(func(nrClient *newrelic.NewRelic, profile *credentials.Profile) { @@ -54,8 +56,9 @@ var Command = &cobra.Command{ } func init() { - Command.Flags().BoolVarP(&interactiveMode, "interactive", "i", true, "enables interactive mode") - Command.Flags().BoolVarP(&installLogging, "installLogging", "l", true, "installs New Relic logging") - Command.Flags().BoolVarP(&autoDiscoveryMode, "autoDiscovery", "d", true, "enables auto-discovery mode") - Command.Flags().StringSliceVarP(&recipeFriendlyNames, "recipe", "r", []string{}, "the name of a recipe to install") + Command.Flags().BoolVarP(&interactiveMode, "interactive", "i", false, "enables interactive mode if specifyActions has been used") + Command.Flags().BoolVarP(&installLogging, "installLogging", "l", false, "installs New Relic logging if specifyActions has been used") + Command.Flags().BoolVarP(&specifyActions, "specifyActions", "s", false, "specify the actions to be run during install") + Command.Flags().StringSliceVarP(&recipeNames, "recipe", "r", []string{}, "the name of a recipe to install") + Command.Flags().StringSliceVarP(&recipeFilenames, "recipeFile", "c", []string{}, "a recipe file to install") } diff --git a/internal/install/go_task_recipe_executor.go b/internal/install/go_task_recipe_executor.go index 05a994214..48a859ab5 100644 --- a/internal/install/go_task_recipe_executor.go +++ b/internal/install/go_task_recipe_executor.go @@ -25,7 +25,7 @@ func newGoTaskRecipeExecutor() *goTaskRecipeExecutor { } func (re *goTaskRecipeExecutor) execute(ctx context.Context, m discoveryManifest, r recipe) error { - log.Debugf("Executing recipe %s", r.Metadata.Name) + log.Debugf("Executing recipe %s", r.Name) f, err := r.ToRecipeFile() if err != nil { @@ -38,7 +38,7 @@ func (re *goTaskRecipeExecutor) execute(ctx context.Context, m discoveryManifest } // Create a temporary task file. - file, err := ioutil.TempFile("", r.Metadata.Name) + file, err := ioutil.TempFile("", r.Name) defer os.Remove(file.Name()) if err != nil { return err diff --git a/internal/install/install_context.go b/internal/install/install_context.go new file mode 100644 index 000000000..8b9eb8007 --- /dev/null +++ b/internal/install/install_context.go @@ -0,0 +1,26 @@ +package install + +type installContext struct { + specifyActions bool + interactiveMode bool + installLogging bool + installInfraAgent bool + recipeNames []string + recipeFilenames []string +} + +func (i *installContext) ShouldInstallInfraAgent() bool { + return !i.RecipeFilenamesProvided() && (!i.specifyActions || i.installInfraAgent) +} + +func (i *installContext) ShouldInstallLogging() bool { + return !i.RecipeFilenamesProvided() && (!i.specifyActions || i.installLogging) +} + +func (i *installContext) RecipeFilenamesProvided() bool { + return len(i.recipeFilenames) > 0 +} + +func (i *installContext) RecipeNamesProvided() bool { + return len(i.recipeNames) > 0 +} diff --git a/internal/install/install_context_test.go b/internal/install/install_context_test.go new file mode 100644 index 000000000..480cda0f2 --- /dev/null +++ b/internal/install/install_context_test.go @@ -0,0 +1,72 @@ +package install + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestShouldInstallInfraAgent_Default(t *testing.T) { + ic := installContext{} + require.True(t, ic.ShouldInstallInfraAgent()) +} + +func TestShouldInstallInfraAgent_SpecifyActions(t *testing.T) { + ic := installContext{ + specifyActions: true, + } + require.False(t, ic.ShouldInstallInfraAgent()) + + ic.installInfraAgent = true + require.True(t, ic.ShouldInstallInfraAgent()) +} + +func TestShouldInstallInfraAgent_RecipeFilenamesProvided(t *testing.T) { + ic := installContext{ + recipeFilenames: []string{"testFilename"}, + } + require.False(t, ic.ShouldInstallInfraAgent()) + + ic.installInfraAgent = true + require.False(t, ic.ShouldInstallInfraAgent()) +} +func TestShouldInstallLogging_Default(t *testing.T) { + ic := installContext{} + require.True(t, ic.ShouldInstallLogging()) +} + +func TestShouldInstallLogging_SpecifyActions(t *testing.T) { + ic := installContext{ + specifyActions: true, + } + require.False(t, ic.ShouldInstallLogging()) + + ic.installLogging = true + require.True(t, ic.ShouldInstallLogging()) +} + +func TestShouldInstallLogging_RecipeFilenamesProvided(t *testing.T) { + ic := installContext{ + recipeFilenames: []string{"testFilename"}, + } + require.False(t, ic.ShouldInstallLogging()) + + ic.installInfraAgent = true + require.False(t, ic.ShouldInstallLogging()) +} + +func TestRecipeNamesProvided(t *testing.T) { + ic := installContext{} + require.False(t, ic.RecipeNamesProvided()) + + ic.recipeNames = []string{"testName"} + require.True(t, ic.RecipeNamesProvided()) +} + +func TestRecipeFilenamesProvided(t *testing.T) { + ic := installContext{} + require.False(t, ic.RecipeFilenamesProvided()) + + ic.recipeFilenames = []string{"testFilename"} + require.True(t, ic.RecipeFilenamesProvided()) +} diff --git a/internal/install/mock_nrdb_client.go b/internal/install/mock_nrdb_client.go index 281a1c419..a52853266 100644 --- a/internal/install/mock_nrdb_client.go +++ b/internal/install/mock_nrdb_client.go @@ -37,13 +37,13 @@ func (c *mockNrdbClient) ThrowError(message string) { c.error = message } -func (c *mockNrdbClient) ReturnResultsAfterNAttempts(results []nrdb.NrdbResult, attempts int) { +func (c *mockNrdbClient) ReturnResultsAfterNAttempts(before []nrdb.NrdbResult, after []nrdb.NrdbResult, attempts int) { c.results = func() []nrdb.NrdbResult { if c.attempts < attempts { - return []nrdb.NrdbResult{} + return before } - return results + return after } } diff --git a/internal/install/polling_recipe_validator.go b/internal/install/polling_recipe_validator.go index 235ec1e4f..8e6dd07d1 100644 --- a/internal/install/polling_recipe_validator.go +++ b/internal/install/polling_recipe_validator.go @@ -1,7 +1,10 @@ package install import ( + "bytes" "context" + "errors" + "html/template" "time" log "github.com/sirupsen/logrus" @@ -31,7 +34,7 @@ func newPollingRecipeValidator(c nrdbClient) *pollingRecipeValidator { return &v } -func (m *pollingRecipeValidator) validate(ctx context.Context, r recipe) (bool, error) { +func (m *pollingRecipeValidator) validate(ctx context.Context, dm discoveryManifest, r recipe) (bool, error) { count := 0 ticker := time.NewTicker(m.interval) defer ticker.Stop() @@ -42,7 +45,7 @@ func (m *pollingRecipeValidator) validate(ctx context.Context, r recipe) (bool, } log.Debugf("Validation attempt #%d...", count+1) - ok, err := m.tryValidate(ctx, r) + ok, err := m.tryValidate(ctx, dm, r) if err != nil { return false, err } @@ -63,24 +66,60 @@ func (m *pollingRecipeValidator) validate(ctx context.Context, r recipe) (bool, } } -func (m *pollingRecipeValidator) tryValidate(ctx context.Context, r recipe) (bool, error) { - results, err := m.executeQuery(ctx, r.Metadata.ValidationNRQL) +func (m *pollingRecipeValidator) tryValidate(ctx context.Context, dm discoveryManifest, r recipe) (bool, error) { + query, err := substituteHostname(dm, r) if err != nil { return false, err } - if len(results) > 0 { + 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 { return true, nil } return false, nil } +func substituteHostname(dm discoveryManifest, r recipe) (string, error) { + tmpl, err := template.New("validationNRQL").Parse(r.ValidationNRQL) + if err != nil { + panic(err) + } + + v := struct { + HOSTNAME string + }{ + HOSTNAME: dm.Hostname, + } + + var tpl bytes.Buffer + if err = tmpl.Execute(&tpl, v); err != nil { + return "", err + } + + return tpl.String(), nil +} + func (m *pollingRecipeValidator) executeQuery(ctx context.Context, query string) ([]nrdb.NrdbResult, error) { - accountID := credentials.DefaultProfile().AccountID + 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, accountID, nrql) + result, err := m.client.QueryWithContext(ctx, profile.AccountID, nrql) if err != nil { return nil, err } diff --git a/internal/install/polling_recipe_validator_test.go b/internal/install/polling_recipe_validator_test.go index 0213da8df..b6ce1ae33 100644 --- a/internal/install/polling_recipe_validator_test.go +++ b/internal/install/polling_recipe_validator_test.go @@ -9,43 +9,53 @@ import ( "github.com/stretchr/testify/require" + "github.com/newrelic/newrelic-cli/internal/credentials" "github.com/newrelic/newrelic-client-go/pkg/nrdb" ) +var ( + emptyResults = []nrdb.NrdbResult{ + map[string]interface{}{ + "count": 0.0, + }, + } + nonEmptyResults = []nrdb.NrdbResult{ + map[string]interface{}{ + "count": 1.0, + }, + } +) + func TestValidate(t *testing.T) { + credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) c := newMockNrdbClient() - results := []nrdb.NrdbResult{ - map[string]interface{}{}, - } - - c.ReturnResultsAfterNAttempts(results, 1) + c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 1) v := newPollingRecipeValidator(c) r := recipe{} + m := discoveryManifest{} - ok, err := v.validate(context.Background(), r) + ok, err := v.validate(context.Background(), m, r) require.NoError(t, err) require.True(t, ok) } func TestValidate_PassAfterNAttempts(t *testing.T) { + credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) c := newMockNrdbClient() v := newPollingRecipeValidator(c) v.maxAttempts = 5 v.interval = 10 * time.Millisecond - results := []nrdb.NrdbResult{ - map[string]interface{}{}, - } - - c.ReturnResultsAfterNAttempts(results, 5) + c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 5) r := recipe{} + m := discoveryManifest{} - ok, err := v.validate(context.Background(), r) + ok, err := v.validate(context.Background(), m, r) require.NoError(t, err) require.True(t, ok) @@ -53,14 +63,16 @@ func TestValidate_PassAfterNAttempts(t *testing.T) { } func TestValidate_FailAfterNAttempts(t *testing.T) { + credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) c := newMockNrdbClient() v := newPollingRecipeValidator(c) v.maxAttempts = 3 v.interval = 10 * time.Millisecond r := recipe{} + m := discoveryManifest{} - ok, err := v.validate(context.Background(), r) + ok, err := v.validate(context.Background(), m, r) require.NoError(t, err) require.False(t, ok) @@ -68,50 +80,47 @@ func TestValidate_FailAfterNAttempts(t *testing.T) { } func TestValidate_FailAfterMaxAttempts(t *testing.T) { + credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) c := newMockNrdbClient() - results := []nrdb.NrdbResult{ - map[string]interface{}{}, - } - - c.ReturnResultsAfterNAttempts(results, 2) + c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 2) v := newPollingRecipeValidator(c) v.maxAttempts = 1 v.interval = 10 * time.Millisecond r := recipe{} + m := discoveryManifest{} - ok, err := v.validate(context.Background(), r) + ok, err := v.validate(context.Background(), m, r) require.NoError(t, err) require.False(t, ok) } func TestValidate_FailIfContextDone(t *testing.T) { + credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) c := newMockNrdbClient() - results := []nrdb.NrdbResult{ - map[string]interface{}{}, - } - - c.ReturnResultsAfterNAttempts(results, 2) + c.ReturnResultsAfterNAttempts(emptyResults, nonEmptyResults, 2) v := newPollingRecipeValidator(c) v.interval = 1 * time.Second r := recipe{} + m := discoveryManifest{} ctx, cancel := context.WithCancel(context.Background()) cancel() - ok, err := v.validate(ctx, r) + ok, err := v.validate(ctx, m, r) require.NoError(t, err) require.False(t, ok) } func TestValidate_QueryError(t *testing.T) { + credentials.SetDefaultProfile(credentials.Profile{AccountID: 12345}) c := newMockNrdbClient() c.ThrowError("test error") @@ -119,8 +128,9 @@ func TestValidate_QueryError(t *testing.T) { v := newPollingRecipeValidator(c) r := recipe{} + m := discoveryManifest{} - ok, err := v.validate(context.Background(), r) + ok, err := v.validate(context.Background(), m, r) require.False(t, ok) require.EqualError(t, err, "test error") diff --git a/internal/install/psutil_discoverer_integration_test.go b/internal/install/psutil_discoverer_integration_test.go index 2d15a4b89..194701abb 100644 --- a/internal/install/psutil_discoverer_integration_test.go +++ b/internal/install/psutil_discoverer_integration_test.go @@ -21,11 +21,9 @@ func TestDiscovery(t *testing.T) { mockRecipeFetcher.fetchRecipesFunc = func() ([]recipe, error) { return []recipe{ { - ID: "test", - Metadata: recipeMetadata{ - Name: "java", - ProcessMatch: []string{"java"}, - }, + ID: "test", + Name: "java", + ProcessMatch: []string{"java"}, }, }, nil } diff --git a/internal/install/recipe_file.go b/internal/install/recipe_file.go index a51e880fc..c628c4ef5 100644 --- a/internal/install/recipe_file.go +++ b/internal/install/recipe_file.go @@ -1,22 +1,28 @@ package install +import ( + "io/ioutil" + + "gopkg.in/yaml.v2" +) + type recipeFile struct { Description string `yaml:"description"` InputVars []variableConfig `yaml:"inputVars"` Install map[string]interface{} `yaml:"install"` - InstallTargets recipeInstallTarget `yaml:"installTargets"` + InstallTargets []recipeInstallTarget `yaml:"installTargets"` Keywords []string `yaml:"keywords"` - MELTMatch meltMatch `yaml:"meltMatch"` + LogMatch logMatch `yaml:"logMatch"` Name string `yaml:"name"` ProcessMatch []string `yaml:"processMatch"` Repository string `yaml:"repository"` - Variant variant `yaml:"variant"` ValidationNRQL string `yaml:"validationNrql"` } type variableConfig struct { Name string `yaml:"name"` Prompt string `yaml:"prompt"` + Secret bool `secret:"prompt"` Default string `yaml:"default"` } @@ -30,29 +36,67 @@ type recipeInstallTarget struct { KernelArch string `yaml:"kernelArch"` } -type meltMatch struct { - Events patternMatcher `yaml:"events"` - Metrics patternMatcher `yaml:"metrics"` - Logs loggingMatcher `yaml:"logs"` +type logMatch struct { + Name string `yaml:"name"` + File string `yaml:"file"` + Attributes logMatchAttributes `yaml:"attributes"` + Pattern string `yaml:"pattern"` + Systemd string `yaml:"systemd"` } -type patternMatcher struct { - Pattern []string `yaml:"pattern"` +type logMatchAttributes struct { + LogType string `yaml:"logtype"` +} + +func loadRecipeFile(filename string) (*recipeFile, error) { + out, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + f, err := newRecipeFile(string(out)) + if err != nil { + return nil, err + } + + return f, nil } -type loggingMatcher struct { - Pattern []string `yaml:"pattern"` - Files []string `yaml:"files"` +func newRecipeFile(recipeFileString string) (*recipeFile, error) { + var f recipeFile + err := yaml.Unmarshal([]byte(recipeFileString), &f) + if err != nil { + return nil, err + } + + return &f, nil } -type variant struct { - Arch []string `yaml:"arch"` - OS []string `yaml:"os"` - TargetEnvironment []string `yaml:"targetEnvironment"` +func (f *recipeFile) String() (string, error) { + out, err := yaml.Marshal(f) + if err != nil { + return "", err + } + + return string(out), nil } -type recipeVariant struct { - OS []string `json:"os"` - Arch []string `json:"arch"` - TargetEnvironment []string `json:"targetEnvironment"` +func (f *recipeFile) ToRecipe() (*recipe, error) { + fileStr, err := f.String() + if err != nil { + return nil, err + } + + r := recipe{ + File: fileStr, + Name: f.Name, + Description: f.Description, + Repository: f.Repository, + Keywords: f.Keywords, + ProcessMatch: f.ProcessMatch, + LogMatch: f.LogMatch, + ValidationNRQL: f.ValidationNRQL, + } + + return &r, nil } diff --git a/internal/install/recipe_file_test.go b/internal/install/recipe_file_test.go new file mode 100644 index 000000000..75f30aec9 --- /dev/null +++ b/internal/install/recipe_file_test.go @@ -0,0 +1,96 @@ +package install + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +var ( + testRecipeFileString = ` +--- +description: testDescription +keywords: + - testKeyword +name: testName +processMatch: + - testProcessMatch +repository: testRepository +validationNrql: testValidationNrql +inputVars: + - name: testName + prompt: testPrompt + secret: true + default: testDefault +installTargets: + - type: testType + os: testOS + platform: testPlatform + platformFamily: testPlatformFamily + platformVersion: testPlatformVersion + kernelVersion: testKerrnelVersion + kernelArch: testKernelArch +logMatch: + name: testName + file: testFile + attributes: + logtype: testlogtype + pattern: testPattern + systemd: testSystemd +` +) + +func TestLoadRecipeFile(t *testing.T) { + tmpFile, err := ioutil.TempFile(os.TempDir(), t.Name()) + if err != nil { + t.Fatal("error creating temp file") + } + + defer os.Remove(tmpFile.Name()) + + f, err := loadRecipeFile(tmpFile.Name()) + require.NoError(t, err) + require.NotNil(t, f) +} + +func TestNewRecipeFile(t *testing.T) { + var expected recipeFile + err := yaml.Unmarshal([]byte(testRecipeFileString), &expected) + require.NoError(t, err) + + actual, err := newRecipeFile(testRecipeFileString) + require.NoError(t, err) + require.True(t, reflect.DeepEqual(&expected, actual)) +} + +func TestString(t *testing.T) { + var f recipeFile + err := yaml.Unmarshal([]byte(testRecipeFileString), &f) + require.NoError(t, err) + + s, err := f.String() + require.NoError(t, err) + require.NotEmpty(t, s) +} + +func TestToRecipe(t *testing.T) { + var f recipeFile + err := yaml.Unmarshal([]byte(testRecipeFileString), &f) + require.NoError(t, err) + + r, err := f.ToRecipe() + require.NoError(t, err) + require.NotEmpty(t, r) + require.NotEmpty(t, r.File) + require.Equal(t, f.Name, r.Name) + require.Equal(t, f.Description, r.Description) + require.Equal(t, f.Repository, r.Repository) + require.Equal(t, f.ValidationNRQL, r.ValidationNRQL) + + require.NotEmpty(t, f.Keywords, r.Keywords) + require.NotEmpty(t, f.ProcessMatch, r.ProcessMatch) +} diff --git a/internal/install/recipe_installer.go b/internal/install/recipe_installer.go index 40b04ab47..7786c403b 100644 --- a/internal/install/recipe_installer.go +++ b/internal/install/recipe_installer.go @@ -30,21 +30,13 @@ func newRecipeInstaller( recipeValidator: v, } - i.autoDiscoveryMode = ic.autoDiscoveryMode i.interactiveMode = ic.interactiveMode i.installLogging = ic.installLogging - i.recipeFriendlyNames = ic.recipeFriendlyNames + i.recipeNames = ic.recipeNames return &i } -type installContext struct { - interactiveMode bool - autoDiscoveryMode bool - installLogging bool - recipeFriendlyNames []string -} - const ( infraAgentRecipeName = "Infrastructure Agent Installer" loggingRecipeName = "Logs integration" @@ -58,24 +50,30 @@ func (i *recipeInstaller) install() { m := i.discoverFatal() // Run the infra agent recipe, exiting on failure. - i.installInfraAgentFatal(m) + if i.ShouldInstallInfraAgent() { + i.installInfraAgentFatal(m) + } // Run the logging recipe if requested, exiting on failure. - if i.installLogging { + if i.ShouldInstallLogging() { i.installLoggingFatal(m) } // Retrieve a list of recipes to execute. var recipes []recipe - if i.autoDiscoveryMode { - // Ask the recipe service for recommendations. - recipes = i.fetchRecommendationsFatal(m) - } else { + if i.RecipeFilenamesProvided() { + for _, n := range i.recipeFilenames { + recipes = append(recipes, *i.recipeFromFilenameFatal(n)) + } + } else if i.RecipeNamesProvided() { // Execute the requested recipes. - for _, n := range i.recipeFriendlyNames { + for _, n := range i.recipeNames { r := i.fetchWarn(m, n) recipes = append(recipes, *r) } + } else { + // Ask the recipe service for recommendations. + recipes = i.fetchRecommendationsFatal(m) } // Execute and validate each of the recipes in the collection. @@ -96,6 +94,20 @@ func (i *recipeInstaller) discoverFatal() *discoveryManifest { return m } +func (i *recipeInstaller) recipeFromFilenameFatal(recipeFilename string) *recipe { + f, err := loadRecipeFile(recipeFilename) + if err != nil { + log.Fatalf("Could not load file %s: %s", recipeFilename, err) + } + + r, err := f.ToRecipe() + if err != nil { + log.Fatalf("Could not load file %s: %s", recipeFilename, err) + } + + return r +} + func (i *recipeInstaller) installInfraAgentFatal(m *discoveryManifest) { i.fetchExecuteAndValidateFatal(m, infraAgentRecipeName) } @@ -119,7 +131,7 @@ func (i *recipeInstaller) fetchExecuteAndValidateFatal(m *discoveryManifest, rec } func (i *recipeInstaller) fetchWarn(m *discoveryManifest, recipeName string) *recipe { - r, err := i.recipeFetcher.fetchRecipe(utils.SignalCtx, m, infraAgentRecipeName) + r, err := i.recipeFetcher.fetchRecipe(utils.SignalCtx, m, recipeName) if err != nil { log.Warnf("Could not install %s. Error retrieving recipe: %s", recipeName, err) return nil @@ -133,7 +145,7 @@ func (i *recipeInstaller) fetchWarn(m *discoveryManifest, recipeName string) *re } func (i *recipeInstaller) fetchFatal(m *discoveryManifest, recipeName string) *recipe { - r, err := i.recipeFetcher.fetchRecipe(utils.SignalCtx, m, infraAgentRecipeName) + r, err := i.recipeFetcher.fetchRecipe(utils.SignalCtx, m, recipeName) if err != nil { log.Fatalf("Could not install %s. Error retrieving recipe: %s", recipeName, err) } @@ -147,16 +159,16 @@ func (i *recipeInstaller) fetchFatal(m *discoveryManifest, recipeName string) *r func (i *recipeInstaller) executeAndValidate(m *discoveryManifest, r *recipe) (bool, error) { // Execute the recipe steps. - log.Infof("Installing %s...\n", r.Metadata.Name) + log.Infof("Installing %s...\n", r.Name) if err := i.recipeExecutor.execute(utils.SignalCtx, *m, *r); err != nil { - return false, fmt.Errorf("encountered an error while executing %s: %s", r.Metadata.Name, err) + return false, fmt.Errorf("encountered an error while executing %s: %s", r.Name, err) } - log.Infof("Installing %s...success\n", r.Metadata.Name) + log.Infof("Installing %s...success\n", r.Name) log.Info("Listening for data...") - ok, err := i.recipeValidator.validate(utils.SignalCtx, *r) + ok, err := i.recipeValidator.validate(utils.SignalCtx, *m, *r) if err != nil { - return false, fmt.Errorf("encountered an error while validating receipt of data for %s: %s", r.Metadata.Name, err) + return false, fmt.Errorf("encountered an error while validating receipt of data for %s: %s", r.Name, err) } if !ok { @@ -171,21 +183,21 @@ func (i *recipeInstaller) executeAndValidate(m *discoveryManifest, r *recipe) (b func (i *recipeInstaller) executeAndValidateFatal(m *discoveryManifest, r *recipe) { ok, err := i.executeAndValidate(m, r) if err != nil { - log.Fatalf("Could not install %s: %s", r.Metadata.Name, err) + log.Fatalf("Could not install %s: %s", r.Name, err) } if !ok { - log.Fatalf("Could not detect data from %s.", r.Metadata.Name) + log.Fatalf("Could not detect data from %s.", r.Name) } } func (i *recipeInstaller) executeAndValidateWarn(m *discoveryManifest, r *recipe) { ok, err := i.executeAndValidate(m, r) if err != nil { - log.Warnf("Could not install %s: %s", r.Metadata.Name, err) + log.Warnf("Could not install %s: %s", r.Name, err) } if !ok { - log.Warnf("Could not detect data from %s.", r.Metadata.Name) + log.Warnf("Could not detect data from %s.", r.Name) } } diff --git a/internal/install/recipe_validator.go b/internal/install/recipe_validator.go index 4ab6cffdc..3365fa5b7 100644 --- a/internal/install/recipe_validator.go +++ b/internal/install/recipe_validator.go @@ -3,5 +3,5 @@ package install import "context" type recipeValidator interface { - validate(context.Context, recipe) (bool, error) + validate(context.Context, discoveryManifest, recipe) (bool, error) } diff --git a/internal/install/regex_process_filterer.go b/internal/install/regex_process_filterer.go index a15b64771..cc4ed4260 100644 --- a/internal/install/regex_process_filterer.go +++ b/internal/install/regex_process_filterer.go @@ -42,7 +42,7 @@ func (f *regexProcessFilterer) filter(ctx context.Context, processes []genericPr } func match(r recipe, process genericProcess) bool { - for _, pattern := range r.Metadata.ProcessMatch { + for _, pattern := range r.ProcessMatch { name, err := process.Name() if err != nil { log.Debugf("could not retrieve process name for PID %d", process.PID()) diff --git a/internal/install/regex_process_filterer_test.go b/internal/install/regex_process_filterer_test.go index bf2fb2d96..7d5a0e881 100644 --- a/internal/install/regex_process_filterer_test.go +++ b/internal/install/regex_process_filterer_test.go @@ -12,11 +12,9 @@ import ( func TestFilter(t *testing.T) { recipes := []recipe{ { - ID: "test", - Metadata: recipeMetadata{ - Name: "java", - ProcessMatch: []string{"java"}, - }, + ID: "test", + Name: "java", + ProcessMatch: []string{"java"}, }, } diff --git a/internal/install/service_recipe_fetcher.go b/internal/install/service_recipe_fetcher.go index 3693fab62..db0941461 100644 --- a/internal/install/service_recipe_fetcher.go +++ b/internal/install/service_recipe_fetcher.go @@ -3,6 +3,7 @@ package install import ( "context" "fmt" + "strings" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" @@ -31,11 +32,11 @@ func (f *serviceRecipeFetcher) fetchRecipe(ctx context.Context, manifest *discov } var resp recipeSearchQueryResult - if err := f.client.QueryWithResponseAndContext(ctx, recommendationsQuery, vars, &resp); err != nil { + if err := f.client.QueryWithResponseAndContext(ctx, recipeSearchQuery, vars, &resp); err != nil { return nil, err } - results := resp.Account.OpenInstallation.RecipeSearch.Results + results := resp.Docs.OpenInstallation.RecipeSearch.Results if len(results) == 0 { return nil, fmt.Errorf("no results found for friendly name %s", friendlyName) @@ -63,7 +64,7 @@ func (f *serviceRecipeFetcher) fetchRecommendations(ctx context.Context, manifes return nil, err } - return resp.Account.OpenInstallation.Recommendations.Results, nil + return resp.Docs.OpenInstallation.Recommendations.Results, nil } func (f *serviceRecipeFetcher) fetchRecipes(ctx context.Context) ([]recipe, error) { @@ -72,39 +73,35 @@ func (f *serviceRecipeFetcher) fetchRecipes(ctx context.Context) ([]recipe, erro return nil, err } - return resp.Account.OpenInstallation.RecipeSearch.Results, nil + return resp.Docs.OpenInstallation.RecipeSearch.Results, nil } type recommendationsQueryResult struct { - Account recommendationsQueryAccount + Docs recommendationsQueryDocs `json:"docs"` } -type recommendationsQueryAccount struct { - OpenInstallation recommendationsQueryOpenInstallation +type recommendationsQueryDocs struct { + OpenInstallation recommendationsQueryOpenInstallation `json:"openInstallation"` } type recommendationsQueryOpenInstallation struct { - Recommendations recommendationsResult + Recommendations recommendationsResult `json:"recommendations"` } type recommendationsResult struct { - Results []recipe + Results []recipe `json:"recipe"` } type recipe struct { - ID string - Metadata recipeMetadata - File string -} - -type recipeMetadata struct { - Name string - Description string - Repository string - Variant recipeVariant - Keywords []string - ProcessMatch []string - ValidationNRQL string + ID string `json:"id"` + File string `json:"file"` + Name string `json:"name"` + Description string `json:"description"` + Repository string `json:"repository"` + Keywords []string `json:"keywords"` + ProcessMatch []string `json:"processMatch"` + LogMatch logMatch `json:"logMatch"` + ValidationNRQL string `json:"validationNrql"` } func (s *recipe) ToRecipeFile() (*recipeFile, error) { @@ -122,7 +119,7 @@ func (recommendations *recommendationsResult) ToRecipeFiles() []recipeFile { for i, s := range recommendations.Results { recipe, err := s.ToRecipeFile() if err != nil { - log.Warnf("could not parse recipe %s", s.Metadata.Name) + log.Warnf("could not parse recipe %s", s.Name) continue } r[i] = *recipe @@ -132,19 +129,23 @@ func (recommendations *recommendationsResult) ToRecipeFiles() []recipeFile { } type recommendationsInput struct { - Variant variantInput `json:"variant"` + InstallTarget installTarget `json:"installTarget"` ProcessDetails []processDetailInput `json:"processDetails"` } type recipeSearchInput struct { - Name string `json:"name"` - Variant variantInput `json:"variant"` + Name string `json:"name"` + InstallTarget installTarget `json:"installTarget"` } -type variantInput struct { - OS string `json:"os"` - Arch string `json:"arch"` - TargetEnvironment string `json:"targetEnvironment"` +type installTarget struct { + Type string `json:"type"` + OS string `json:"os"` + Platform string `json:"platform"` + PlatformFamily string `json:"platformFamily,omitempty"` + PlatformVersion string `json:"platformVersion"` + KernelArch string `json:"kernelArch,omitempty"` + KernelVersion string `json:"kernelVersion,omitempty"` } type processDetailInput struct { @@ -152,28 +153,25 @@ type processDetailInput struct { } type recipeSearchQueryResult struct { - Account recipeSearchQueryAccount + Docs recipeSearchQueryDocs `json:"docs"` } -type recipeSearchQueryAccount struct { - OpenInstallation recipeSearchQueryOpenInstallation +type recipeSearchQueryDocs struct { + OpenInstallation recipeSearchQueryOpenInstallation `json:"openInstallation"` } type recipeSearchQueryOpenInstallation struct { - RecipeSearch recipeSearchResult + RecipeSearch recipeSearchResult `json:"recipeSearch"` } type recipeSearchResult struct { - Results []recipe + Results []recipe `json:"results"` } func createRecipeSearchInput(d *discoveryManifest, friendlyName string) (*recipeSearchInput, error) { c := recipeSearchInput{ - Name: friendlyName, - Variant: variantInput{ - OS: d.PlatformFamily, - Arch: d.KernelArch, - }, + Name: friendlyName, + InstallTarget: createInstallTarget(d), } return &c, nil @@ -181,10 +179,7 @@ func createRecipeSearchInput(d *discoveryManifest, friendlyName string) (*recipe func createRecommendationsInput(d *discoveryManifest) (*recommendationsInput, error) { c := recommendationsInput{ - Variant: variantInput{ - OS: d.PlatformFamily, - Arch: d.KernelArch, - }, + InstallTarget: createInstallTarget(d), } for _, process := range d.Processes { @@ -202,21 +197,57 @@ func createRecommendationsInput(d *discoveryManifest) (*recommendationsInput, er return &c, nil } +func createInstallTarget(d *discoveryManifest) installTarget { + i := installTarget{ + PlatformVersion: strings.ToUpper(d.PlatformVersion), + //KernelArch: strings.ToUpper(d.KernelArch), + //KernelVersion: strings.ToUpper(d.KernelVersion), + } + + i.Type = "HOST" + i.OS = strings.ToUpper(d.OS) + i.Platform = strings.ToUpper(d.Platform) + //i.PlatformFamily = strings.ToUpper(d.PlatformFamily) + + return i +} + const ( recipeResultFragment = ` id - metadata { + name + description + repository + installTargets { + type + os + platform + platformFamily + platformVersion + kernelVersion + kernelArch + } + keywords + processMatch + logMatch { name - description - repository - processMatch - validationNrql - variant { - os - arch - targetEnvironment + file + pattern + systemd + attributes { + logtype } } + inputVars { + name + prompt + secret + default + } + validationNrql + preInstall { + prompt + } file ` recipeSearchQuery = ` diff --git a/internal/install/service_recipe_fetcher_test.go b/internal/install/service_recipe_fetcher_test.go index 0308d408d..a40f045e5 100644 --- a/internal/install/service_recipe_fetcher_test.go +++ b/internal/install/service_recipe_fetcher_test.go @@ -13,12 +13,10 @@ import ( func TestFetchFilters(t *testing.T) { r := []recipe{ { - ID: "test", - Metadata: recipeMetadata{ - Name: "test", - ProcessMatch: []string{ - "test", - }, + ID: "test", + Name: "test", + ProcessMatch: []string{ + "test", }, }, } @@ -39,8 +37,7 @@ func TestFetchFilters(t *testing.T) { func TestFetchRecommendations(t *testing.T) { r := []recipe{ { - ID: "test", - Metadata: recipeMetadata{}, + ID: "test", File: ` --- name: Test recipe file @@ -65,7 +62,7 @@ description: test description func wrapRecipes(r []recipe) recipeSearchQueryResult { return recipeSearchQueryResult{ - Account: recipeSearchQueryAccount{ + Docs: recipeSearchQueryDocs{ OpenInstallation: recipeSearchQueryOpenInstallation{ RecipeSearch: recipeSearchResult{ Results: r, @@ -77,7 +74,7 @@ func wrapRecipes(r []recipe) recipeSearchQueryResult { func wrapRecommendations(r []recipe) recommendationsQueryResult { return recommendationsQueryResult{ - Account: recommendationsQueryAccount{ + Docs: recommendationsQueryDocs{ OpenInstallation: recommendationsQueryOpenInstallation{ Recommendations: recommendationsResult{ Results: r,