diff --git a/changelog/fragments/1733248787-flag-to-skip-fleet-audit.yaml b/changelog/fragments/1733248787-flag-to-skip-fleet-audit.yaml new file mode 100644 index 00000000000..023cb7e589f --- /dev/null +++ b/changelog/fragments/1733248787-flag-to-skip-fleet-audit.yaml @@ -0,0 +1,32 @@ +# Kind can be one of: +# - breaking-change: a change to previously-documented behavior +# - deprecation: functionality that is being removed in a later release +# - bug-fix: fixes a problem in a previous version +# - enhancement: extends functionality but does not break or fix existing behavior +# - feature: new functionality +# - known-issue: problems that we are aware of in a given version +# - security: impacts on the security of a product or a user’s deployment. +# - upgrade: important information for someone upgrading from a prior version +# - other: does not fit into any of the other categories +kind: enhancement + +# Change summary; a 80ish characters long description of the change. +summary: Add a flag to skip audit/unenroll call to fleet server during uninstall + +# Long description; in case the summary is not enough to describe the change +# this field accommodate a description without length limits. +# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment. +description: This change adds a flag to skip audit/unenroll call to fleet server. While uninstalling elastic-agent it tries to notify fleet server about the uninstallation. But in somecases users might know that the fleet server is unreachable and this notification logs multiple failures continuously. Adding this flag skips this call. + +# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc. +component: "elastic-agent" + +# PR URL; optional; the PR number that added the changeset. +# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added. +# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number. +# Please provide it if you are adding a fragment for a different PR. +#pr: https://github.com/owner/repo/1234 + +# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of). +# If not present is automatically filled by the tooling with the issue linked to the PR number. +#issue: https://github.com/owner/repo/1234 diff --git a/internal/pkg/agent/cmd/install.go b/internal/pkg/agent/cmd/install.go index b6f8447b779..f189b71e029 100644 --- a/internal/pkg/agent/cmd/install.go +++ b/internal/pkg/agent/cmd/install.go @@ -252,7 +252,7 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error { return err } } else { - err := install.Uninstall(cmd.Context(), cfgFile, topPath, "", log, progBar) + err := install.Uninstall(cmd.Context(), cfgFile, topPath, "", log, progBar, false) if err != nil { progBar.Describe("Uninstall from binary failed") return err @@ -276,7 +276,7 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error { defer func() { if err != nil { progBar.Describe("Uninstalling") - innerErr := install.Uninstall(cmd.Context(), cfgFile, topPath, "", log, progBar) + innerErr := install.Uninstall(cmd.Context(), cfgFile, topPath, "", log, progBar, false) if innerErr != nil { progBar.Describe("Failed to Uninstall") } else { diff --git a/internal/pkg/agent/cmd/uninstall.go b/internal/pkg/agent/cmd/uninstall.go index 0fb69157082..2c7964ed3f6 100644 --- a/internal/pkg/agent/cmd/uninstall.go +++ b/internal/pkg/agent/cmd/uninstall.go @@ -36,6 +36,7 @@ Unless -f is used this command will ask confirmation before performing removal. cmd.Flags().BoolP("force", "f", false, "Force overwrite the current and do not prompt for confirmation") cmd.Flags().String("uninstall-token", "", "Uninstall token required for protected agent uninstall") + cmd.Flags().Bool("skip-fleet-audit", false, "Skip fleet audit/unenroll") return cmd } @@ -60,6 +61,7 @@ func uninstallCmd(streams *cli.IOStreams, cmd *cobra.Command) error { force, _ := cmd.Flags().GetBool("force") uninstallToken, _ := cmd.Flags().GetString("uninstall-token") + skipFleetAudit, _ := cmd.Flags().GetBool("skip-fleet-audit") if status == install.Broken { if !force { fmt.Fprintf(streams.Out, "Elastic Agent is installed but currently broken: %s\n", reason) @@ -94,7 +96,7 @@ func uninstallCmd(streams *cli.IOStreams, cmd *cobra.Command) error { fmt.Fprint(os.Stderr, logBuff.String()) }() - err = install.Uninstall(cmd.Context(), paths.ConfigFile(), paths.Top(), uninstallToken, log, progBar) + err = install.Uninstall(cmd.Context(), paths.ConfigFile(), paths.Top(), uninstallToken, log, progBar, skipFleetAudit) if err != nil { progBar.Describe("Failed to uninstall agent") return fmt.Errorf("error uninstalling agent: %w", err) diff --git a/internal/pkg/agent/install/uninstall.go b/internal/pkg/agent/install/uninstall.go index f08e753964d..4f2be70ff12 100644 --- a/internal/pkg/agent/install/uninstall.go +++ b/internal/pkg/agent/install/uninstall.go @@ -55,7 +55,7 @@ func (a *agentInfo) AgentID() string { } // Uninstall uninstalls persistently Elastic Agent on the system. -func Uninstall(ctx context.Context, cfgFile, topPath, uninstallToken string, log *logp.Logger, pt *progressbar.ProgressBar) error { +func Uninstall(ctx context.Context, cfgFile, topPath, uninstallToken string, log *logp.Logger, pt *progressbar.ProgressBar, skipFleetAudit bool) error { cwd, err := os.Getwd() if err != nil { return fmt.Errorf("unable to get current working directory") @@ -166,13 +166,19 @@ func Uninstall(ctx context.Context, cfgFile, topPath, uninstallToken string, log } pt.Describe("Removed install directory") - if notifyFleet && !localFleet { - notifyFleetAuditUninstall(ctx, log, pt, cfg, &agentID) //nolint:errcheck // ignore the error as we can't act on it - } - + notifyFleetIfNeeded(ctx, log, pt, cfg, agentID, notifyFleet, localFleet, skipFleetAudit, notifyFleetAuditUninstall) return nil } +// Injecting notifyFleetAuditUninstall for easier unit testing +func notifyFleetIfNeeded(ctx context.Context, log *logp.Logger, pt *progressbar.ProgressBar, cfg *configuration.Configuration, agentID agentInfo, notifyFleet, localFleet, skipFleetAudit bool, notifyFleetAuditUninstall NotifyFleetAuditUninstall) { + if notifyFleet && !localFleet && !skipFleetAudit { + notifyFleetAuditUninstall(ctx, log, pt, cfg, &agentID) //nolint:errcheck // ignore the error as we can't act on it) + } +} + +type NotifyFleetAuditUninstall func(ctx context.Context, log *logp.Logger, pt *progressbar.ProgressBar, cfg *configuration.Configuration, ai fleetapi.AgentInfo) error + // notifyFleetAuditUninstall will attempt to notify fleet-server of the agent's uninstall. // // There are retries for the attempt after a 10s wait, but it is a best-effort approach. diff --git a/internal/pkg/agent/install/uninstall_test.go b/internal/pkg/agent/install/uninstall_test.go index 19b5afdee95..d2465fa1d87 100644 --- a/internal/pkg/agent/install/uninstall_test.go +++ b/internal/pkg/agent/install/uninstall_test.go @@ -26,6 +26,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/secret" "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" "github.com/elastic/elastic-agent/internal/pkg/agent/vault" + "github.com/elastic/elastic-agent/internal/pkg/fleetapi" "github.com/elastic/elastic-agent/internal/pkg/fleetapi/client" "github.com/elastic/elastic-agent/internal/pkg/remote" ) @@ -226,3 +227,44 @@ func TestNotifyFleetAuditUnenroll(t *testing.T) { }) } + +type MockNotifyFleetAuditUninstall struct { + Called bool +} + +func (m *MockNotifyFleetAuditUninstall) Call(ctx context.Context, log *logp.Logger, pt *progressbar.ProgressBar, cfg *configuration.Configuration, ai fleetapi.AgentInfo) { + m.Called = true +} + +func TestSkipFleetAuditUnenroll(t *testing.T) { + log := &logp.Logger{} + pt := &progressbar.ProgressBar{} + cfg := &configuration.Configuration{} + var agentID agentInfo = "testID" + + testCases := []struct { + notifyFleet bool + localFleet bool + skipFleetAudit bool + }{ + {true, true, true}, + {true, true, false}, + {true, false, true}, + {false, true, true}, + {false, true, false}, + {false, false, true}, + {false, false, false}, + } + + for i, tc := range testCases { + t.Run( + fmt.Sprintf("test case #%d: %t:%t:%t", i, tc.notifyFleet, tc.localFleet, tc.skipFleetAudit), + func(t *testing.T) { + mockNotify := &MockNotifyFleetAuditUninstall{} + notifyFleetIfNeeded(context.Background(), log, pt, cfg, agentID, tc.notifyFleet, tc.localFleet, tc.skipFleetAudit, notifyFleetAuditUninstall) + assert.False(t, mockNotify.Called, "NotifyFleetAuditUninstall should not be invoked when notifyFleet: %t - localFleet: %t - skipFleetAudit: %t", tc.notifyFleet, tc.localFleet, tc.skipFleetAudit) + }, + ) + } + +}