diff --git a/cmd/fleetctl/fleetctl/gitops.go b/cmd/fleetctl/fleetctl/gitops.go index 17d60242b8..50fa496880 100644 --- a/cmd/fleetctl/fleetctl/gitops.go +++ b/cmd/fleetctl/fleetctl/gitops.go @@ -303,7 +303,11 @@ func gitopsCommand() *cli.Command { } if !noTeamControls.Defined && !config.Controls.Defined { if appConfig.License.IsPremium() { - return fmt.Errorf("'controls' must be set on global config or %s", noTeamFilename) + suggestion := ", no-team.yml or unassigned.yml" + if noTeamFilename != "" { + suggestion = fmt.Sprintf(" or %s", noTeamFilename) + } + return fmt.Errorf("'controls' must be set on global config%s", suggestion) } return errors.New("'controls' must be set on global config") } diff --git a/cmd/fleetctl/fleetctl/gitops_test.go b/cmd/fleetctl/fleetctl/gitops_test.go index d9e144752c..f9de344f8c 100644 --- a/cmd/fleetctl/fleetctl/gitops_test.go +++ b/cmd/fleetctl/fleetctl/gitops_test.go @@ -218,6 +218,7 @@ queries: policies: agent_options: labels: +controls: org_settings: server_settings: server_url: https://example.com @@ -2396,7 +2397,7 @@ software: assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on policies included in `no-team.yml`: \"Foobar\""), err.Error()) }) - t.Run("global and no-team.yml DO NOT define controls -- controls is now optional", func(t *testing.T) { + t.Run("global and no-team.yml DO NOT define controls -- should fail", func(t *testing.T) { globalFileWithoutControlsAndSoftwareKeys := createGlobalFileWithoutControlsAndSoftwareKeys(t, fleetServerURL, orgName) noTeamFilePathWithoutControls := filepath.Join(t.TempDir(), "no-team.yml") @@ -2409,16 +2410,94 @@ software: `) require.NoError(t, err) - // Dry run, controls is now optional so this should succeed. - _ = RunAppForTest(t, []string{ + // Dry run. + _, err = RunAppNoChecks([]string{ "gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFileBasic.Name(), "-f", noTeamFileWithoutControls.Name(), "--dry-run", }) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "must be set on global config or no-team.yml"), err.Error()) + // Real run - _ = RunAppForTest(t, []string{ + _, err = RunAppNoChecks([]string{ "gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFileBasic.Name(), "-f", noTeamFileWithoutControls.Name(), }) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "must be set on global config or no-team.yml"), err.Error()) + }) + + t.Run("global DOES NOT define controls -- should fail", func(t *testing.T) { + globalFileWithoutControlsAndSoftwareKeys := createGlobalFileWithoutControlsAndSoftwareKeys(t, fleetServerURL, orgName) + + // Dry run. + _, err = RunAppNoChecks([]string{ + "gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "--dry-run", + }) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "must be set on global config, no-team.yml or unassigned.yml"), err.Error()) + + // Real run + _, err = RunAppNoChecks([]string{ + "gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), + }) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "must be set on global config, no-team.yml or unassigned.yml"), err.Error()) + }) + + t.Run("no-team provided without global -- should fail", func(t *testing.T) { + noTeamFilePathWithoutControls := filepath.Join(t.TempDir(), "no-team.yml") + noTeamFileWithoutControls, err := os.Create(noTeamFilePathWithoutControls) + require.NoError(t, err) + _, err = noTeamFileWithoutControls.WriteString(` +policies: +name: No team +software: +`) + require.NoError(t, err) + + // Dry run. + _, err = RunAppNoChecks([]string{ + "gitops", "-f", teamFileBasic.Name(), "-f", + noTeamFileWithoutControls.Name(), "--dry-run", + }) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "global config must be provided alongside no-team.yml"), err.Error()) + + _, err = RunAppNoChecks([]string{ + "gitops", "-f", teamFileBasic.Name(), "-f", + noTeamFileWithoutControls.Name(), + }) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "global config must be provided alongside no-team.yml"), err.Error()) + }) + + t.Run("unassigned provided without global -- should fail", func(t *testing.T) { + noTeamFilePathWithoutControls := filepath.Join(t.TempDir(), "unassigned.yml") + noTeamFileWithoutControls, err := os.Create(noTeamFilePathWithoutControls) + require.NoError(t, err) + _, err = noTeamFileWithoutControls.WriteString(` +policies: +name: Unassigned +software: +`) + require.NoError(t, err) + + // Dry run + _, err = RunAppNoChecks([]string{ + "gitops", "-f", teamFileBasic.Name(), "-f", + noTeamFileWithoutControls.Name(), "--dry-run", + }) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "global config must be provided alongside unassigned.yml"), err.Error()) + + // Real run + _, err = RunAppNoChecks([]string{ + "gitops", "-f", teamFileBasic.Name(), "-f", + noTeamFileWithoutControls.Name(), + }) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "global config must be provided alongside unassigned.yml"), err.Error()) }) t.Run("controls only defined in no-team.yml", func(t *testing.T) { diff --git a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go index 49b9803667..9895eb1e50 100644 --- a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go +++ b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go @@ -3825,8 +3825,9 @@ labels: require.NoError(t, err) require.Len(t, labels, 1) - // Step 2: Apply a minimal global config that omits policies, agent_options, controls, reports, labels. + // Step 2: Apply a minimal global config that omits policies, agent_options, reports, labels. const minimalGlobalConfig = ` +controls: org_settings: server_settings: server_url: $FLEET_URL diff --git a/pkg/spec/gitops.go b/pkg/spec/gitops.go index e01790b7b5..1b20e279a8 100644 --- a/pkg/spec/gitops.go +++ b/pkg/spec/gitops.go @@ -432,6 +432,11 @@ func GitOpsFromFile(filePath, baseDir string, appConfig *fleet.EnrichedAppConfig if topKey == "name" || topKey == "labels" || topKey == "settings" || topKey == "org_settings" { continue } + // "controls" can be set on _either_ global or "no team" file, and we can't say which it is if both + // files aren't supplied, so play it safe and require it to be set on one or the other. + if (result.IsNoTeam() || result.IsGlobal()) && topKey == "controls" { + continue + } // "agent_options" and "reports" are not supported in no-team/unassigned files. if result.IsNoTeam() && (topKey == "agent_options" || topKey == "reports") { continue