diff --git a/changes/44082-add-script-output-to-gitops b/changes/44082-add-script-output-to-gitops new file mode 100644 index 00000000000..b4c5020def7 --- /dev/null +++ b/changes/44082-add-script-output-to-gitops @@ -0,0 +1 @@ +- Added output to GitOps for scripts, indicating how many scripts would be applied (dry run) or were applied. \ No newline at end of file diff --git a/cmd/fleetctl/fleetctl/gitops_test.go b/cmd/fleetctl/fleetctl/gitops_test.go index 2192eb7052b..c3ae8e7c109 100644 --- a/cmd/fleetctl/fleetctl/gitops_test.go +++ b/cmd/fleetctl/fleetctl/gitops_test.go @@ -6943,3 +6943,72 @@ policies: require.ErrorContains(t, err, "labels_include_all") require.ErrorContains(t, err, "labels_exclude_any") } + +func TestGitOpsScriptsLogging(t *testing.T) { + // Cannot run t.Parallel() because SetupFullGitOpsPremiumServer sets env vars. + ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t) + + // Echo back a ScriptResponse for each input script so the real-run no-team + // log line reflects the count of scripts sent to the server. (In dry-run, + // the service short-circuits before reaching this mock.) + ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) { + responses := make([]fleet.ScriptResponse, len(scripts)) + for i, s := range scripts { + responses[i] = fleet.ScriptResponse{ID: uint(i + 1), Name: s.Name, TeamID: s.TeamID} //nolint:gosec // dismiss G115 + } + return responses, nil + } + + // No labels — keep the test focused on scripts. + ds.GetLabelSpecsFunc = func(ctx context.Context, filter fleet.TeamFilter) ([]*fleet.LabelSpec, error) { + return nil, nil + } + + tmpDir := t.TempDir() + scriptPath := filepath.Join(tmpDir, "script.sh") + require.NoError(t, os.WriteFile(scriptPath, []byte(`echo "hello"`), 0o644)) + + globalPath := filepath.Join(tmpDir, "global.yml") + require.NoError(t, os.WriteFile(globalPath, []byte(` +controls: + scripts: + - path: ./script.sh +queries: +policies: +agent_options: +org_settings: + server_settings: + server_url: $FLEET_SERVER_URL + org_info: + contact_url: https://example.com/contact + org_name: $ORG_NAME + secrets: + - secret: globalSecret +software: +`), 0o644)) + + teamPath := filepath.Join(tmpDir, "team.yml") + require.NoError(t, os.WriteFile(teamPath, []byte(` +name: $TEST_TEAM_NAME +team_settings: + secrets: + - secret: team-secret +agent_options: +controls: + scripts: + - path: ./script.sh +policies: +queries: +software: +`), 0o644)) + + // Dry run. + logs := RunAppForTest(t, []string{"gitops", "-f", globalPath, "-f", teamPath, "--dry-run"}) + assert.Contains(t, logs, "[+] would've applied 1 script\n") + assert.Contains(t, logs, fmt.Sprintf("[+] would've applied 1 script for fleet %s\n", teamName)) + + // Real run. + logs = RunAppForTest(t, []string{"gitops", "-f", globalPath, "-f", teamPath}) + assert.Contains(t, logs, "[+] applied 1 script\n") + assert.Contains(t, logs, fmt.Sprintf("[+] applied 1 script for fleet %s\n", teamName)) +} diff --git a/server/service/client.go b/server/service/client.go index 497d163060e..f0e8d92221b 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -527,6 +527,7 @@ const ( dryRunAppliedFormat = "[+] would've applied %s\n" appliedFormat = "[+] applied %s\n" applyingTeamFormat = "[+] applying %s for fleet %s\n" + appliedTeamFormat = "[+] applied %s for fleet %s\n" dryRunAppliedTeamFormat = "[+] would've applied %s for fleet %s\n" ) @@ -664,6 +665,12 @@ func (c *Client) ApplyGroup( return nil, nil, nil, nil, fmt.Errorf("applying scripts for unassigned hosts: %w", err) } teamsScripts["No team"] = noTeamScripts + + if opts.DryRun { + logfn(dryRunAppliedFormat, numberWithPluralization(len(scriptPayloads), "script", "scripts")) + } else { + logfn(appliedFormat, numberWithPluralization(len(noTeamScripts), "script", "scripts")) + } } rules, err := extractAppCfgYaraRules(specs.AppConfig) @@ -1066,6 +1073,13 @@ func (c *Client) ApplyGroup( return nil, nil, nil, nil, fmt.Errorf("applying scripts for fleet %q: %w", tmName, err) } teamsScripts[tmName] = scriptResponses + if opts.DryRun { + // We split here on dry-run to capture the number we want to apply + logfn(dryRunAppliedTeamFormat, numberWithPluralization(len(scripts), "script", "scripts"), tmName) + } else { + // vs. the number that actually got applied and returned by the server + logfn(appliedTeamFormat, numberWithPluralization(len(scriptResponses), "script", "scripts"), tmName) + } } } if len(tmSoftwarePackagesPayloads) > 0 {