diff --git a/internal/install/bundle_installer.go b/internal/install/bundle_installer.go index cc9298cc6..4c57624b8 100644 --- a/internal/install/bundle_installer.go +++ b/internal/install/bundle_installer.go @@ -3,6 +3,7 @@ package install import ( "context" "fmt" + "strings" log "github.com/sirupsen/logrus" @@ -26,7 +27,6 @@ type BundleInstaller struct { } func NewBundleInstaller(ctx context.Context, manifest *types.DiscoveryManifest, recipeInstallerInterface RecipeInstaller, statusReporter StatusReporter) *BundleInstaller { - return &BundleInstaller{ ctx: ctx, manifest: manifest, @@ -42,7 +42,6 @@ func NewPrompter() *ux.PromptUIPrompter { } func (bi *BundleInstaller) InstallStopOnError(bundle *recipes.Bundle, assumeYes bool) error { - bi.reportBundleStatus(bundle) installableBundleRecipes := bi.getInstallableBundleRecipes(bundle) @@ -52,7 +51,6 @@ func (bi *BundleInstaller) InstallStopOnError(bundle *recipes.Bundle, assumeYes for _, br := range installableBundleRecipes { err := bi.InstallBundleRecipe(br, assumeYes) - if err != nil { return err } @@ -80,7 +78,6 @@ func (bi *BundleInstaller) InstallContinueOnError(bundle *recipes.Bundle, assume prompter := ux.NewPromptUIPrompter() msg := "Continue installing? " isConfirmed, err := prompter.PromptYesNo(msg) - if err != nil { log.Debug(err) isConfirmed = false @@ -130,6 +127,9 @@ func (bi *BundleInstaller) InstallBundleRecipe(bundleRecipe *recipes.BundleRecip } recipeName := bundleRecipe.Recipe.Name + if strings.EqualFold(recipeName, types.SuperAgentRecipeName) && bundleRecipe.HasStatus(execution.RecipeStatusTypes.INSTALLED) { + return nil + } if bi.installedRecipes[bundleRecipe.Recipe.Name] { return nil } @@ -154,7 +154,7 @@ func (bi *BundleInstaller) getInstallableBundleRecipes(bundle *recipes.Bundle) [ var bundleRecipes []*recipes.BundleRecipe for _, bundleRecipe := range bundle.BundleRecipes { - if !bundleRecipe.HasStatus(execution.RecipeStatusTypes.AVAILABLE) { + if !bundleRecipe.LastStatus(execution.RecipeStatusTypes.AVAILABLE) { //Skip if not available continue } diff --git a/internal/install/bundle_installer_test.go b/internal/install/bundle_installer_test.go index 7240f18b1..c28b179c3 100644 --- a/internal/install/bundle_installer_test.go +++ b/internal/install/bundle_installer_test.go @@ -51,6 +51,16 @@ func TestInstallContinueOnErrorReturnsImmediatelyWhenNoIsEntered(t *testing.T) { test.mockStatusReporter.AssertNumberOfCalls(t, "ReportStatus", 1) } +func TestInstallContinueOnErrorReturnsImmediatelyWhenSuperAgentIsInstalled(t *testing.T) { + test := createBundleInstallerTest().withPrompterYesNoVal(false).withRecipeInstallerError() + test.addRecipeToBundle("super-agent", execution.RecipeStatusTypes.INSTALLED) + + test.BundleInstaller.InstallContinueOnError(test.bundle, false) + + test.mockRecipeInstaller.AssertNumberOfCalls(t, "executeAndValidateWithProgress", 0) + test.mockStatusReporter.AssertNumberOfCalls(t, "ReportStatus", 1) +} + func TestInstallContinueOnErrorIgnoresUxPromptIfBundleIsAdditionalTargeted(t *testing.T) { test := createBundleInstallerTest().withRecipeInstallerError() test.addRecipeToBundle("recipe1", execution.RecipeStatusTypes.UNSUPPORTED) diff --git a/internal/install/command.go b/internal/install/command.go index 392dd42d6..b5d15bdfe 100644 --- a/internal/install/command.go +++ b/internal/install/command.go @@ -170,7 +170,6 @@ func validateProfile(maxTimeoutSeconds int, sg *segment.Segment) *types.DetailEr } func checkNetwork() error { - if client.NRClient == nil { return nil } diff --git a/internal/install/recipe_installer.go b/internal/install/recipe_installer.go index a963f7292..597c003ae 100644 --- a/internal/install/recipe_installer.go +++ b/internal/install/recipe_installer.go @@ -24,10 +24,7 @@ import ( "github.com/newrelic/newrelic-client-go/v2/newrelic" ) -const ( - validationTimeout = 5 * time.Minute - superAgentProcessName = "newrelic-super-agent" -) +const validationTimeout = 5 * time.Minute var infraAgentEntityKey string @@ -210,10 +207,6 @@ Our Data Privacy Notice: https://newrelic.com/termsandconditions/services-notice return err } - // Check if super-agent process is already running on host - superAgentProcessOnHost := i.processEvaluator.FindProcess(superAgentProcessName) - log.Debugf("super agent running: %t\n", superAgentProcessOnHost) - hostname, _ := os.Hostname() if hostname == "" { message := "This system is not supported for automatic installation, no host info. Please see our documentation for requirements." @@ -413,10 +406,35 @@ func (i *RecipeInstall) isTargetInstallRecipe(recipeName string) bool { return false } +// installAdditionalBundle installs additional bundles for the given recipes. +// It checks if the host has a super agent process running, and proceeds with the additional bundle. +// If the list of recipes is provided, it creates a targeted bundle; otherwise, it creates a guided bundle. +// If the host has super agent installed infra agent and logs agent would be NULL +// It then installs the additional bundle and reports any unsupported recipes. func (i *RecipeInstall) installAdditionalBundle(bundler RecipeBundler, bundleInstaller RecipeBundleInstaller, repo *recipes.RecipeRepository) error { var additionalBundle *recipes.Bundle + bun, ok := bundler.(*recipes.Bundler) + + if i.hostHasSuperAgentProcess() && ok { + bun.HasSuperInstalled = true + log.Debugf("Super agent process found. Proceeding with additional bundle.") + } else { + log.Debugf("Super agent process not found. Proceeding with additional bundle.") + } + if i.RecipeNamesProvided() { + log.Debugf("bundling additional bundle") + log.Debugf("recipes in list %d", len(i.RecipeNames)) additionalBundle = bundler.CreateAdditionalTargetedBundle(i.RecipeNames) + if bun.HasSuperInstalled { + for _, coreRecipe := range bundler.(*recipes.Bundler).GetCoreRecipeNames() { + if i, ok := additionalBundle.ContainsName(coreRecipe); ok { + additionalBundle.BundleRecipes[i].AddDetectionStatus(execution.RecipeStatusTypes.NULL, 0) + } + } + log.Debugf("Additional Targeted bundle recipes in queue:%s", additionalBundle) + } + i.reportUnsupportedTargetedRecipes(additionalBundle, repo) log.Debugf("Additional Targeted bundle recipes:%s", additionalBundle) } else { @@ -427,6 +445,11 @@ func (i *RecipeInstall) installAdditionalBundle(bundler RecipeBundler, bundleIns bundleInstaller.InstallContinueOnError(additionalBundle, i.AssumeYes) if bundleInstaller.InstalledRecipesCount() == 0 { + if bun.HasSuperInstalled { + return &types.UncaughtError{ + Err: fmt.Errorf("super Agent is installed, preventing the installation of this recipe"), + } + } return &types.UncaughtError{ Err: fmt.Errorf("no recipes were installed"), } @@ -440,7 +463,15 @@ func (i *RecipeInstall) installAdditionalBundle(bundler RecipeBundler, bundleIns } func (i *RecipeInstall) installCoreBundle(bundler RecipeBundler, bundleInstaller RecipeBundleInstaller) error { - if i.shouldInstallCore() { + if i.hostHasSuperAgentProcess() { + if bun, ok := bundler.(*recipes.Bundler); ok { + bun.HasSuperInstalled = true + log.Debugf("Super agent process found") + } + } else { + log.Debugf("Super agent process not found. Proceeding with installation.") + } + if i.shouldInstallCore() && !bundler.(*recipes.Bundler).HasSuperInstalled { coreBundle := bundler.CreateCoreBundle() log.Debugf("Core bundle recipes:%s", coreBundle) err := bundleInstaller.InstallStopOnError(coreBundle, i.AssumeYes) @@ -749,3 +780,7 @@ func (i *RecipeInstall) finishHandlingFailure(recipeName string) { i.progressIndicator.Success("Complete!") } } + +func (i *RecipeInstall) hostHasSuperAgentProcess() bool { + return i.processEvaluator.FindProcess(types.SuperAgentProcessName) +} diff --git a/internal/install/recipe_installer_test.go b/internal/install/recipe_installer_test.go index 52d625dc9..f62d9c75d 100644 --- a/internal/install/recipe_installer_test.go +++ b/internal/install/recipe_installer_test.go @@ -8,11 +8,10 @@ import ( "strings" "testing" - "github.com/newrelic/newrelic-cli/internal/config" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/newrelic/newrelic-cli/internal/config" "github.com/newrelic/newrelic-cli/internal/install/execution" "github.com/newrelic/newrelic-cli/internal/install/recipes" "github.com/newrelic/newrelic-cli/internal/install/types" @@ -90,6 +89,29 @@ func TestInstallWithInvalidDiscoveryResultReturnsError(t *testing.T) { assert.True(t, strings.Contains(actual.Error(), expected.Error())) } +func TestInstallGuidedShouldSkipCoreInstallWhileSuperAgentIsInstalled(t *testing.T) { + r := &recipes.RecipeDetectionResult{ + Recipe: recipes.NewRecipeBuilder().Name(types.InfraAgentRecipeName).Build(), + Status: execution.RecipeStatusTypes.AVAILABLE, + } + statusReporter := execution.NewMockStatusReporter() + recipeInstall := NewRecipeInstallBuilder().WithRecipeDetectionResult(r).withShouldInstallCore(func() bool { return true }).WithStatusReporter(statusReporter).WithRunningProcess("super-agent-process", types.SuperAgentProcessName).Build() + + err := recipeInstall.Install() + + assert.Equal(t, "super Agent is installed, preventing the installation of this recipe", err.Error()) + assert.Equal(t, 1, statusReporter.RecipeDetectedCallCount, "Detection Count") + assert.Equal(t, 0, statusReporter.RecipeAvailableCallCount, "Available Count") + assert.Equal(t, 0, statusReporter.RecipeInstallingCallCount, "Installing Count") + assert.Equal(t, 0, statusReporter.RecipeFailedCallCount, "Failed Count") + assert.Equal(t, 0, statusReporter.RecipeUnsupportedCallCount, "Unsupported Count") + assert.Equal(t, 0, statusReporter.RecipeInstalledCallCount, "InstalledCount") + assert.Equal(t, 0, statusReporter.RecipeRecommendedCallCount, "Recommendation Count") + assert.Equal(t, 0, statusReporter.RecipeSkippedCallCount, "Skipped Count") + assert.Equal(t, 0, statusReporter.RecipeCanceledCallCount, "Cancelled Count") + assert.Equal(t, 0, statusReporter.ReportInstalled[r.Recipe.Name], "Recipe Installed") +} + func TestInstallGuidedShouldSkipCoreInstall(t *testing.T) { r := &recipes.RecipeDetectionResult{ Recipe: recipes.NewRecipeBuilder().Name(types.InfraAgentRecipeName).Build(), @@ -313,6 +335,32 @@ func TestInstallTargetedInstallShouldInstallCoreIfCoreWasSkipped(t *testing.T) { assert.Equal(t, 1, statusReporter.ReportInstalled[r.Recipe.Name], "Recipe1 Installed") } +func TestInstallTargetedInstallShouldInstallCoreIfCoreWasSkippedWhileSuperAgentIsInstalled(t *testing.T) { + r := &recipes.RecipeDetectionResult{ + Recipe: recipes.NewRecipeBuilder().Name(types.InfraAgentRecipeName).Build(), + Status: execution.RecipeStatusTypes.AVAILABLE, + } + statusReporter := execution.NewMockStatusReporter() + recipeInstall := NewRecipeInstallBuilder().WithRecipeDetectionResult(r).withShouldInstallCore(func() bool { return false }). + WithTargetRecipeName(types.InfraAgentRecipeName).WithStatusReporter(statusReporter). + WithRunningProcess("super-agent-process", types.SuperAgentProcessName).Build() + recipeInstall.AssumeYes = true + + err := recipeInstall.Install() + + assert.Equal(t, "super Agent is installed, preventing the installation of this recipe", err.Error()) + assert.Equal(t, 1, statusReporter.RecipeDetectedCallCount, "Detection Count") + assert.Equal(t, 1, statusReporter.RecipeAvailableCallCount, "Available Count") + assert.Equal(t, 0, statusReporter.RecipeInstallingCallCount, "Installing Count") + assert.Equal(t, 0, statusReporter.RecipeFailedCallCount, "Failed Count") + assert.Equal(t, 0, statusReporter.RecipeUnsupportedCallCount, "Unsupported Count") + assert.Equal(t, 0, statusReporter.RecipeInstalledCallCount, "InstalledCount") + assert.Equal(t, 0, statusReporter.RecipeRecommendedCallCount, "Recommendation Count") + assert.Equal(t, 0, statusReporter.RecipeSkippedCallCount, "Skipped Count") + assert.Equal(t, 0, statusReporter.RecipeCanceledCallCount, "Cancelled Count") + assert.Equal(t, 0, statusReporter.ReportInstalled[r.Recipe.Name], "Recipe1 Installed") +} + func TestInstallTargetedInstallWithoutRecipeShouldNotInstall(t *testing.T) { statusReporter := execution.NewMockStatusReporter() recipeInstall := NewRecipeInstallBuilder().WithTargetRecipeName("Other").WithStatusReporter(statusReporter).Build() @@ -751,7 +799,6 @@ func TestWhenSingleInstallRunningNoError(t *testing.T) { recipeInstall := NewRecipeInstallBuilder().WithRunningProcess("env=123 newrelic install", "newrelic").Build() err := recipeInstall.Install() - if err != nil { assert.False(t, strings.Contains(err.Error(), "only 1 newrelic install command can run at one time")) } @@ -770,7 +817,6 @@ func TestWhenSingleInstallRunningNoErrorWindows(t *testing.T) { recipeInstall := NewRecipeInstallBuilder().WithRunningProcess("env=123 C:\\path\\newrelic.exe install", "C:\\path\\newrelic.exe").Build() err := recipeInstall.Install() - if err != nil { assert.False(t, strings.Contains(err.Error(), "only 1 newrelic install command can run at one time")) } diff --git a/internal/install/recipes/bundle.go b/internal/install/recipes/bundle.go index 3442e1fbc..93bd5c271 100644 --- a/internal/install/recipes/bundle.go +++ b/internal/install/recipes/bundle.go @@ -31,7 +31,7 @@ var BundleTypes = struct { } func (b *Bundle) AddRecipe(bundleRecipe *BundleRecipe) { - if b.ContainsName(bundleRecipe.Recipe.Name) { + if _, ok := b.ContainsName(bundleRecipe.Recipe.Name); ok { return } b.BundleRecipes = append(b.BundleRecipes, bundleRecipe) @@ -45,15 +45,15 @@ func (b *Bundle) IsAdditionalTargeted() bool { return b.Type == BundleTypes.ADDITIONALTARGETED } -func (b *Bundle) ContainsName(name string) bool { +func (b *Bundle) ContainsName(name string) (int, bool) { for i := range b.BundleRecipes { if b.BundleRecipes[i].Recipe.Name == name { - return true + return i, true } } - return false + return -1, false } func (b *Bundle) GetBundleRecipe(name string) *BundleRecipe { diff --git a/internal/install/recipes/bundle_recipe.go b/internal/install/recipes/bundle_recipe.go index 0bd4769da..bc0d63261 100644 --- a/internal/install/recipes/bundle_recipe.go +++ b/internal/install/recipes/bundle_recipe.go @@ -36,6 +36,13 @@ func (br *BundleRecipe) HasStatus(status execution.RecipeStatusType) bool { return false } +func (br *BundleRecipe) LastStatus(status execution.RecipeStatusType) bool { + if len(br.DetectedStatuses) > 0 { + return br.DetectedStatuses[len(br.DetectedStatuses)-1].Status == status + } + return false +} + func (br *BundleRecipe) AreAllDependenciesAvailable() bool { for _, depName := range br.Recipe.Dependencies { if br.IsNameInDependencies(depName) { @@ -46,9 +53,11 @@ func (br *BundleRecipe) AreAllDependenciesAvailable() bool { } for _, ds := range br.Dependencies { if !ds.HasStatus(execution.RecipeStatusTypes.AVAILABLE) { + log.Debugf("recipe %s is not available dependency for %s", br.Recipe.Name, ds.Recipe.Name) return false } } + log.Debugf("all dependencies are available") return true } diff --git a/internal/install/recipes/bundle_test.go b/internal/install/recipes/bundle_test.go index 31bd70427..c7638a642 100644 --- a/internal/install/recipes/bundle_test.go +++ b/internal/install/recipes/bundle_test.go @@ -20,8 +20,10 @@ func TestBundle_ShouldAddRecipes(t *testing.T) { } bundle.AddRecipe(newRecipe) + index, found := bundle.ContainsName(newRecipe.Recipe.Name) require.Equal(t, len(bundle.BundleRecipes), 2) - require.True(t, true, bundle.ContainsName(newRecipe.Recipe.Name)) + require.Equal(t, true, found) + require.Equal(t, 1, index) } func TestBundle_ShouldNotUpdateRecipe(t *testing.T) { @@ -37,7 +39,9 @@ func TestBundle_ShouldNotUpdateRecipe(t *testing.T) { bundle.AddRecipe(bundleRecipe) require.Equal(t, len(bundle.BundleRecipes), 1) - require.Equal(t, true, bundle.ContainsName(bundleRecipe.Recipe.Name)) + _, found := bundle.ContainsName(bundleRecipe.Recipe.Name) + + require.Equal(t, true, found) } func TestBundle_ShouldContainRecipeName(t *testing.T) { @@ -48,8 +52,9 @@ func TestBundle_ShouldContainRecipeName(t *testing.T) { }, }, } + _, found := bundle.ContainsName("recipe1") - require.Equal(t, true, bundle.ContainsName("recipe1")) + require.Equal(t, true, found) } func TestBundle_ShouldNotContainRecipeName(t *testing.T) { @@ -60,8 +65,9 @@ func TestBundle_ShouldNotContainRecipeName(t *testing.T) { }, }, } + _, found := bundle.ContainsName("some other name") - require.Equal(t, false, bundle.ContainsName("some other name")) + require.Equal(t, false, found) } func TestBundle_ShouldBeGuided(t *testing.T) { diff --git a/internal/install/recipes/bundler.go b/internal/install/recipes/bundler.go index e7360641e..417c6e136 100644 --- a/internal/install/recipes/bundler.go +++ b/internal/install/recipes/bundler.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" + "github.com/newrelic/newrelic-cli/internal/install/execution" "github.com/newrelic/newrelic-cli/internal/install/types" "github.com/newrelic/newrelic-cli/internal/utils" ) @@ -20,6 +21,7 @@ type Bundler struct { AvailableRecipes RecipeDetectionResults Context context.Context cachedBundleRecipes map[string]*BundleRecipe + HasSuperInstalled bool } func NewBundler(context context.Context, availableRecipes RecipeDetectionResults) *Bundler { @@ -27,11 +29,12 @@ func NewBundler(context context.Context, availableRecipes RecipeDetectionResults Context: context, AvailableRecipes: availableRecipes, cachedBundleRecipes: make(map[string]*BundleRecipe), + HasSuperInstalled: false, } } func (b *Bundler) CreateCoreBundle() *Bundle { - return b.createBundle(b.getCoreRecipeNames(), BundleTypes.CORE) + return b.createBundle(b.GetCoreRecipeNames(), BundleTypes.CORE) } func (b *Bundler) CreateAdditionalGuidedBundle() *Bundle { @@ -50,7 +53,7 @@ func (b *Bundler) CreateAdditionalTargetedBundle(recipes []string) *Bundle { return b.createBundle(recipes, BundleTypes.ADDITIONALTARGETED) } -func (b *Bundler) getCoreRecipeNames() []string { +func (b *Bundler) GetCoreRecipeNames() []string { coreRecipeNames := make([]string, 0, len(coreRecipeMap)) for k := range coreRecipeMap { coreRecipeNames = append(coreRecipeNames, k) @@ -58,6 +61,8 @@ func (b *Bundler) getCoreRecipeNames() []string { return coreRecipeNames } +// createBundle creates a new bundle based on the given recipes and bundle type +// It iterates over the recipes, detects dependencies, and adds recipes to the bundle func (b *Bundler) createBundle(recipes []string, bType BundleType) *Bundle { bundle := &Bundle{Type: bType} @@ -65,15 +70,30 @@ func (b *Bundler) createBundle(recipes []string, bType BundleType) *Bundle { if d, ok := b.AvailableRecipes.GetRecipeDetection(r); ok { var bundleRecipe *BundleRecipe if dualDep, ok := detectDependencies(d.Recipe.Dependencies); ok { - dep := updateDependency(dualDep, recipes) + dep := b.updateDependency(dualDep, recipes) if dep != nil { + log.Debugf("Found dual dependency and selected : %s", dep) d.Recipe.Dependencies = dep } else { log.Debugf("could not process update for dual dependency: %s", dualDep) } } + bundleRecipe = b.getBundleRecipeWithDependencies(d.Recipe) + if bundleRecipe != nil { + for _, recipe := range recipes { + for _, dependency := range bundleRecipe.Dependencies { + if dependency.Recipe.Name != recipe { + log.Debugf("Found dependency %s", dependency.Recipe.Name) + if dep, ok := findRecipeDependency(dependency, types.SuperAgentRecipeName); ok { + log.Debugf("updating the dependency status for %s", dep) + dep.AddDetectionStatus(execution.RecipeStatusTypes.INSTALLED, 0) + } + } + } + } + log.Debugf("Adding bundle recipe:%s status:%+v dependencies:%+v", bundleRecipe.Recipe.Name, bundleRecipe.DetectedStatuses, bundleRecipe.Recipe.Dependencies) bundle.AddRecipe(bundleRecipe) } @@ -83,6 +103,24 @@ func (b *Bundler) createBundle(recipes []string, bType BundleType) *Bundle { return bundle } +// findRecipeDependency recursively searches for a recipe dependency +func findRecipeDependency(recipe *BundleRecipe, name string) (*BundleRecipe, bool) { + for _, dep := range recipe.Dependencies { + if strings.EqualFold(dep.Recipe.Name, name) { + return dep, true + } + found, _ := findRecipeDependency(dep, name) + if found != nil { + return found, true + } + } + return nil, false +} + +// getBundleRecipeWithDependencies returns a BundleRecipe for the given recipe, including its dependencies. +// If the recipe is already cached, it is returned immediately. Otherwise, the function recursively +// fetches the dependencies and builds the BundleRecipe. If any dependencies are missing, the +// bundle recipe is invalidated and nil is returned. func (b *Bundler) getBundleRecipeWithDependencies(recipe *types.OpenInstallationRecipe) *BundleRecipe { if br, ok := b.cachedBundleRecipes[recipe.Name]; ok { return br @@ -105,6 +143,7 @@ func (b *Bundler) getBundleRecipeWithDependencies(recipe *types.OpenInstallation log.Debugf("dependent recipe %s not found, skipping recipe %s", d, recipe.Name) } // A dependency is missing, invalidating the bundle recipe + log.Debugf("A dependency is missing, invalidating the bundle recipe %s", recipe.Name) b.cachedBundleRecipes[recipe.Name] = nil return nil } @@ -113,11 +152,13 @@ func (b *Bundler) getBundleRecipeWithDependencies(recipe *types.OpenInstallation if dt, ok := b.AvailableRecipes.GetRecipeDetection(recipe.Name); ok { bundleRecipe.AddDetectionStatus(dt.Status, dt.DurationMs) b.cachedBundleRecipes[recipe.Name] = bundleRecipe + log.Debugf("bundle recipe with dependency %v", bundleRecipe) return bundleRecipe } } b.cachedBundleRecipes[recipe.Name] = nil + log.Debugf("Returning nil for the recipe: %s", recipe.Name) return nil } @@ -144,26 +185,36 @@ func detectDependencies(deps []string) (string, bool) { // updateDependency updates a recipe's dependency with the first one of the form 'recipe-a || recipe-b' that is found in the targeted // recipes (e.g.: newrelic install -n recipe-a,recipe-c). If none of the recipe's dependency in the form 'recipe-a || recipe-b' are found // in the targeted recipes, the first one of those in that same form 'recipe-a || recipe-b' is used. The final result is that recipe's -// dependency will change from the form 'recipe-a || recipe-b' to, for example, 'recipe-a' only. -func updateDependency(dualDep string, recipes []string) []string { +// dependency will change from the form 'recipe-a || recipe-b' to the first recipe in that list, for example, 'recipe-a' only. +// If the dependency is a super dependency (i.e., '||' is present), and the super dependency is installed, the function will return the super dependency. +// If the dependency is not a super dependency and is found in the targeted recipes, the function will return that dependency. +// If the dependency is not a super dependency and is not found in the targeted recipes, the function will return the first part of the dependency. +func (b *Bundler) updateDependency(dualDep string, recipes []string) []string { var ( - deps []string - splitDeps = strings.Split(dualDep, `||`) + splitDeps = strings.Split(dualDep, `||`) + hasSuperDep bool ) if len(splitDeps) <= 1 { return nil } + for _, dep := range splitDeps { + dep = strings.TrimSpace(dep) + if dep == types.SuperAgentRecipeName { + hasSuperDep = true + break + } + } + if hasSuperDep && b.HasSuperInstalled { + return []string{types.SuperAgentRecipeName} + } for _, dep := range splitDeps { dep = strings.TrimSpace(dep) if utils.StringInSlice(dep, recipes) { - deps = []string{dep} - break - } else { - deps = []string{strings.TrimSpace(splitDeps[0])} // Defaults to first one of 'recipe-a || recipe-b' + return []string{dep} } } - return deps + return []string{strings.TrimSpace(splitDeps[0])} } diff --git a/internal/install/recipes/bundler_test.go b/internal/install/recipes/bundler_test.go index f1ca195d3..5ce6c9fa1 100644 --- a/internal/install/recipes/bundler_test.go +++ b/internal/install/recipes/bundler_test.go @@ -29,6 +29,7 @@ func TestCreateAdditionalTargetedBundleShouldNotSkipCoreRecipes(t *testing.T) { goldenRecipe := NewRecipeBuilder().Name(types.GoldenRecipeName).Build() mysqlRecipe := NewRecipeBuilder().Name("mysql").Build() bundler := createTestBundler() + bundler.HasSuperInstalled = true withAvailableRecipe(bundler, types.InfraAgentRecipeName, execution.RecipeStatusTypes.AVAILABLE, infraRecipe) withAvailableRecipe(bundler, types.LoggingRecipeName, execution.RecipeStatusTypes.AVAILABLE, loggingRecipe) withAvailableRecipe(bundler, types.GoldenRecipeName, execution.RecipeStatusTypes.AVAILABLE, goldenRecipe) @@ -260,6 +261,7 @@ func newBundler(context context.Context, availableRecipes RecipeDetectionResults Context: context, AvailableRecipes: availableRecipes, cachedBundleRecipes: make(map[string]*BundleRecipe), + HasSuperInstalled: false, } } @@ -303,6 +305,7 @@ func TestIncorrectDualDependencyReturnsFalsy(t *testing.T) { } func TestUpdateDependencyReturnsFirstDualDependencyThatIsInTargetedRecipes(t *testing.T) { + b := Bundler{} type test struct { dualDep string recipes []string @@ -318,7 +321,33 @@ func TestUpdateDependencyReturnsFirstDualDependencyThatIsInTargetedRecipes(t *te } for _, tc := range tests { - got := updateDependency(tc.dualDep, tc.recipes) + got := b.updateDependency(tc.dualDep, tc.recipes) + if !reflect.DeepEqual(tc.want, got) { + t.Fatalf("expected: %v, got: %v", tc.want, got) + } + } +} + +func TestUpdateDependencyReturnsSuperAgentDualDependencyThatIsInTargetedRecipes(t *testing.T) { + type test struct { + dualDep string + recipes []string + want []string + b Bundler + } + + tests := []test{ + {dualDep: "infra || super-agent", recipes: []string{"super-agent", "mysql", "logging"}, want: []string{"super-agent"}, b: Bundler{HasSuperInstalled: false}}, + {dualDep: "super-agent || infra", recipes: []string{"super-agent", "mysql", "logging"}, want: []string{"super-agent"}, b: Bundler{HasSuperInstalled: false}}, + {dualDep: "super-agent || infra", recipes: []string{"mysql", "infra", "logging"}, want: []string{"super-agent"}, b: Bundler{HasSuperInstalled: true}}, + {dualDep: "super-agent || infra", recipes: []string{"mysql", "infra", "super-agent"}, want: []string{"super-agent"}, b: Bundler{HasSuperInstalled: true}}, + {dualDep: "infra || super-agent", recipes: []string{"mysql", "infra", "super-agent"}, want: []string{"super-agent"}, b: Bundler{HasSuperInstalled: true}}, + {dualDep: "infra || super-agent", recipes: []string{"infra", "mysql", "logging"}, want: []string{"super-agent"}, b: Bundler{HasSuperInstalled: true}}, + {dualDep: "infra || super-agent", recipes: []string{"infra", "mysql", "logging"}, want: []string{"infra"}, b: Bundler{HasSuperInstalled: false}}, + } + + for _, tc := range tests { + got := tc.b.updateDependency(tc.dualDep, tc.recipes) if !reflect.DeepEqual(tc.want, got) { t.Fatalf("expected: %v, got: %v", tc.want, got) } diff --git a/internal/install/recipes/mock_process_evaluator.go b/internal/install/recipes/mock_process_evaluator.go index 4176d13b6..9856a0e3d 100644 --- a/internal/install/recipes/mock_process_evaluator.go +++ b/internal/install/recipes/mock_process_evaluator.go @@ -30,5 +30,11 @@ func (pe *MockProcessEvaluator) DetectionStatus(ctx context.Context, r *types.Op } func (pe *MockProcessEvaluator) FindProcess(process string) bool { + for _, p := range pe.processes { + name, _ := p.Name() + if name == process { + return true + } + } return false } diff --git a/internal/install/recipes/process_evaluator.go b/internal/install/recipes/process_evaluator.go index 1126534d6..c5e168bcb 100644 --- a/internal/install/recipes/process_evaluator.go +++ b/internal/install/recipes/process_evaluator.go @@ -2,7 +2,6 @@ package recipes import ( "context" - "github.com/newrelic/newrelic-cli/internal/install/execution" "github.com/shirou/gopsutil/v3/process" diff --git a/internal/install/types/recipe.go b/internal/install/types/recipe.go index 0f26c527a..c6f52153d 100644 --- a/internal/install/types/recipe.go +++ b/internal/install/types/recipe.go @@ -14,6 +14,7 @@ const ( LoggingSuperAgentRecipeName = "logs-integration-super-agent" GoldenRecipeName = "alerts-golden-signal" SuperAgentRecipeName = "super-agent" + SuperAgentProcessName = "newrelic-super-agent" ) var RecipeVariables = map[string]string{}