Allow "unassigned.yml" in GitOps (#40414)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #40433

# Details

This PR updates `fleetctl gitops` and `fleetctl generate_gitops` to use
`unassigned.yml` in place of `no-team.yml`. The two files are utilized
identically, except that `unassigned.yml` expects the `name:` to be
`Unassigned` rather than `No team`.

Internally, we still map some things to the string "no team" before
sending to the back end so that we don't have to update back-end code
and make more spaghetti to clean up when we 🔪 No Team as a concept in
Fleet 5.

We do pass the filename into the main `DoGitOps` method, but both I and
Claude did our best to determine that it's not used in any way that
would break with this change.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually
From test plan:
- [X] With a pre-existing GitOps folder w/ `no-team.yml`, run `fleetctl
gitops -f /path/to/no-team.yml --dry-run
--enable-log-topics=deprecated-field-names` and verify that everything
works as expected and you get the deprecation warning.
- [X] Do the above without `--dry-run` and verify via the UI and/or
`fleetctl generate-gitops` that the Fleet config is as expected.
- [X] Change `no-team.yml` to `unassigned.yml`, try a gitops run with
`fleetctl gitops -f /path/to/unassigned.yml --dry-run
--enable-log-topics=deprecated-field-names` and verify that you get an
error because the `name:` is still `No team`
- [X] Change the `name:` to `Unassigned`, repeat the run above and
verify that the output is the same as with `no-team.yml`, and that no
deprecation warning is listed.
- [X] Do the same as the above without `--dry-run` and verify that the
Fleet config is as expected.
- [X] Run `fleetctl generate-gitops` and verify that `unassigned.yml` is
output rather than `no-team.yml`, and any related files are under the
`lib/unassigned` folder rather than `lib/no-team`, and any paths inside
`unassigned.yml` (e.g. for scripts) are pointed at `lib/unassigned`.

## New Fleet configuration settings

- [X] Verified that the setting is exported via `fleetctl
generate-gitops`
- [ ] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
  @noahtalerman will work on this

---------

Co-authored-by: Ian Littman <iansltx@gmail.com>
This commit is contained in:
Scott Gress 2026-02-26 07:47:12 -06:00 committed by GitHub
parent ae0ea39b7e
commit 10c997b350
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 167 additions and 45 deletions

View file

@ -0,0 +1 @@
- Deprecated no-team.yml in GitOps in favor of unassigned.yml.

View file

@ -247,7 +247,7 @@ func generateGitopsCommand() *cli.Command {
},
&cli.StringFlag{
Name: "team",
Usage: "(Premium only) The team to output configuration for. Omit to export all configuration. Use 'global' to export global settings, or 'no-team' to export settings for No Team.",
Usage: "(Premium only) The team to output configuration for. Omit to export all configuration. Use 'global' to export global settings, or 'unassigned' to export settings for unassigned hosts.",
},
&cli.StringFlag{
Name: "dir",
@ -371,6 +371,9 @@ func (cmd *GenerateGitopsCommand) Run() error {
case cmd.CLI.String("team") == "global" || !cmd.AppConfig.License.IsPremium():
teamsToProcess = []teamToProcess{globalTeam}
case cmd.CLI.String("team") == "no-team":
fmt.Fprintf(cmd.CLI.App.ErrWriter, "[!] '--team no-team' is deprecated. Use '--team unassigned' instead.\n")
teamsToProcess = []teamToProcess{noTeam}
case cmd.CLI.String("team") == "unassigned":
teamsToProcess = []teamToProcess{noTeam}
default:
// Get the list of teams.
@ -418,10 +421,15 @@ func (cmd *GenerateGitopsCommand) Run() error {
}
// If it's a real team, start the filename with the team name.
if team != nil {
teamFileName = generateFilename(team.Name)
displayName := team.Name
// For the no-team virtual team (ID 0), always output as "Unassigned".
if team.ID == 0 {
displayName = "Unassigned"
}
teamFileName = generateFilename(displayName)
fileName = "fleets/" + teamFileName + ".yml"
cmd.FilesToWrite[fileName] = map[string]interface{}{
"name": team.Name,
"name": displayName,
}
} else {
fileName = "default.yml"
@ -546,8 +554,8 @@ func (cmd *GenerateGitopsCommand) Run() error {
fileName = "default.yml"
case "":
fileName = "default.yml"
case "no-team":
fileName = "fleets/no-team.yml"
case "no-team", "unassigned":
fileName = "fleets/unassigned.yml"
default:
teamFileName := generateFilename(cmd.CLI.String("team"))
fileName = "fleets/" + teamFileName + ".yml"

View file

@ -100,6 +100,8 @@ func gitopsCommand() *cli.Command {
if totalFilenames == 0 {
return errors.New("-f must be specified")
}
// TODO - remove No Team in Fleet 5
noTeamFilesEncountered := 0
for _, flFilename := range flFilenames.Value() {
if strings.TrimSpace(flFilename) == "" {
return errors.New("file name cannot be empty")
@ -107,6 +109,12 @@ func gitopsCommand() *cli.Command {
if len(filepath.Base(flFilename)) > filenameMaxLength {
return fmt.Errorf("file name must be less than %d characters: %s", filenameMaxLength, filepath.Base(flFilename))
}
if filepath.Base(flFilename) == "no-team.yml" || filepath.Base(flFilename) == "unassigned.yml" {
noTeamFilesEncountered++
if noTeamFilesEncountered > 1 {
return errors.New("Only one of `no-team.yml` or `unassigned.yml` can be provided. Use `unassigned.yml`; `no-team.yml` is deprecated.")
}
}
}
// Check license
@ -123,9 +131,15 @@ func gitopsCommand() *cli.Command {
}
// We need the controls from no-team.yml to apply them when applying the global app config.
noTeamControls, noTeamPresent, err := extractControlsForNoTeam(flFilenames, appConfig)
noTeamControls, noTeamPresent, noTeamFilename, err := extractControlsForNoTeam(flFilenames, appConfig)
if err != nil {
return fmt.Errorf("extracting controls from no-team.yml: %w", err)
return fmt.Errorf("extracting controls from %s: %w", noTeamFilename, err)
}
// Log a deprecation warning if the user is still using no-team.yml
if noTeamPresent && noTeamFilename == "no-team.yml" {
if logging.TopicEnabled(logging.DeprecatedFieldTopic) {
logf("[!] no-team.yml is deprecated; please rename the file to 'unassigned.yml' and update the team name to 'Unassigned'.\n")
}
}
var originalABMConfig []any
@ -257,7 +271,7 @@ func gitopsCommand() *cli.Command {
// fail if scripts are supplied on no-team and global config is missing
if noTeamPresent && !globalConfigLoaded {
return errors.New("global config must be provided alongside no-team.yml")
return fmt.Errorf("global config must be provided alongside %s", noTeamFilename)
}
labelMoves, err := computeLabelMoves(labelChanges)
@ -272,11 +286,11 @@ func gitopsCommand() *cli.Command {
if isGlobalConfig {
if noTeamControls.Set() && config.Controls.Set() {
return errors.New("'controls' cannot be set on both global config and on no-team.yml")
return fmt.Errorf("'controls' cannot be set on both global config and on %s", noTeamFilename)
}
if !noTeamControls.Defined && !config.Controls.Defined {
if appConfig.License.IsPremium() {
return errors.New("'controls' must be set on global config or no-team.yml")
return fmt.Errorf("'controls' must be set on global config or %s", noTeamFilename)
}
return errors.New("'controls' must be set on global config")
}
@ -543,7 +557,7 @@ func gitopsCommand() *cli.Command {
_, err := fleetClient.DoGitOps(
c.Context,
defaultNoTeamConfig,
"no-team.yml",
noTeamFilename,
logf,
flDryRun,
nil,
@ -802,9 +816,10 @@ func getCustomSettings(osSettings interface{}) ([]fleet.MDMProfileSpec, bool) {
return nil, false
}
func extractControlsForNoTeam(flFilenames cli.StringSlice, appConfig *fleet.EnrichedAppConfig) (spec.GitOpsControls, bool, error) {
func extractControlsForNoTeam(flFilenames cli.StringSlice, appConfig *fleet.EnrichedAppConfig) (spec.GitOpsControls, bool, string, error) {
for _, flFilename := range flFilenames.Value() {
if filepath.Base(flFilename) == "no-team.yml" {
fileName := filepath.Base(flFilename)
if fileName == "no-team.yml" || fileName == "unassigned.yml" {
if !appConfig.License.IsPremium() {
// Message is printed in the next flFilenames loop to avoid printing it multiple times
break
@ -812,12 +827,12 @@ func extractControlsForNoTeam(flFilenames cli.StringSlice, appConfig *fleet.Enri
baseDir := filepath.Dir(flFilename)
config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig, func(format string, a ...interface{}) {})
if err != nil {
return spec.GitOpsControls{}, false, err
return spec.GitOpsControls{}, false, fileName, err
}
return config.Controls, true, nil
return config.Controls, true, fileName, nil
}
}
return spec.GitOpsControls{}, false, nil
return spec.GitOpsControls{}, false, "", nil
}
// checkABMTeamAssignments validates the spec, and finds if:

View file

@ -800,11 +800,21 @@ software:
t.Setenv("TEST_TEAM_NAME", "no TEam")
_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
require.Error(t, err)
assert.Contains(t, err.Error(), fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", tmpFile.Name()))
assert.Contains(t, err.Error(), fmt.Sprintf("file `%s` for No Team must be named `no-team.yml`", tmpFile.Name()))
assert.Contains(t, err.Error(), "rename the file")
t.Setenv("TEST_TEAM_NAME", "no TEam")
_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
require.Error(t, err)
assert.Contains(t, err.Error(), fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", tmpFile.Name()))
assert.Contains(t, err.Error(), fmt.Sprintf("file `%s` for No Team must be named `no-team.yml`", tmpFile.Name()))
assert.Contains(t, err.Error(), "rename the file")
t.Setenv("TEST_TEAM_NAME", "unassigned")
_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
require.Error(t, err)
assert.Contains(t, err.Error(), fmt.Sprintf("file `%s` for unassigned hosts must be named `unassigned.yml`", tmpFile.Name()))
t.Setenv("TEST_TEAM_NAME", "unAsSIgNeD")
_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
require.Error(t, err)
assert.Contains(t, err.Error(), fmt.Sprintf("file `%s` for unassigned hosts must be named `unassigned.yml`", tmpFile.Name()))
t.Setenv("TEST_TEAM_NAME", "All teams")
_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
@ -2242,14 +2252,14 @@ software:
noTeamFilePathPoliciesCalendar.Name(), "--dry-run",
})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on \"No team\" policies: \"Foobar\""), err.Error())
assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on policies included in `no-team.yml`: \"Foobar\""), err.Error())
// Real run, both global and no-team.yml define controls.
_, err = RunAppNoChecks([]string{
"gitops", "-f", globalFileWithControls.Name(), "-f", teamFileBasic.Name(), "-f",
noTeamFilePathPoliciesCalendar.Name(),
})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on \"No team\" policies: \"Foobar\""), err.Error())
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 -- should fail", func(t *testing.T) {

View file

@ -20,7 +20,7 @@ controls:
minimum_version: "15.1"
update_new_hosts: true
scripts:
- path: ../lib/no-team/scripts/Script Z.ps1
- path: ../lib/unassigned/scripts/Script Z.ps1
windows_enabled_and_configured: true
windows_entra_tenant_ids:
- 5b84b6dd-d257-415e-b8b4-0240666ba4d4
@ -30,7 +30,7 @@ controls:
windows_updates:
deadline_days: 5
grace_period_days: 2
name: No team
name: Unassigned
policies:
- calendar_events_enabled: false
conditional_access_bypass_enabled: true

View file

@ -330,15 +330,30 @@ func GitOpsFromFile(filePath, baseDir string, appConfig *fleet.EnrichedAppConfig
}
case teamOk:
multiError = parseName(teamRaw, result, filePath, multiError)
if result.IsNoTeam() {
if filepath.Base(filePath) != "no-team.yml" {
multiError = multierror.Append(multiError, fmt.Errorf("file %q for 'No team' must be named 'no-team.yml'", filePath))
}
// If the file is no-team.yml, the name must be "No team".
switch {
case filepath.Base(filePath) == "no-team.yml" && !result.IsNoTeam():
multiError = multierror.Append(multiError, fmt.Errorf("file %q must have team name 'No Team'", filePath))
return result, multiError.ErrorOrNil()
case filepath.Base(filePath) == "unassigned.yml" && !result.IsUnassignedTeam():
multiError = multierror.Append(multiError, fmt.Errorf("file %q must have team name 'Unassigned'", filePath))
return result, multiError.ErrorOrNil()
case result.IsNoTeam() && filepath.Base(filePath) != "no-team.yml":
multiError = multierror.Append(multiError, fmt.Errorf("file `%s` for No Team must be named `no-team.yml`", filePath))
multiError = multierror.Append(multiError, errors.New("no-team.yml is deprecated; please rename the file to 'unassigned.yml' and update the team name to 'Unassigned'."))
return result, multiError.ErrorOrNil()
case result.IsUnassignedTeam() && filepath.Base(filePath) != "unassigned.yml":
multiError = multierror.Append(multiError, fmt.Errorf("file `%s` for unassigned hosts must be named `unassigned.yml`", filePath))
return result, multiError.ErrorOrNil()
case result.IsNoTeam() || result.IsUnassignedTeam():
// Coerce to "No Team" for easier processing.
// TODO - Remove No Team in Fleet 5
result.TeamName = ptr.String(noTeam)
// For No Team, we allow settings but only process webhook_settings from it
if settingsOk {
multiError = parseNoTeamSettings(settingsRaw, result, filePath, multiError)
}
} else {
default:
if !settingsOk {
multiError = multierror.Append(multiError, errors.New("'settings' is required when 'name' is provided"))
} else {
@ -394,11 +409,11 @@ func (g *GitOps) IsGlobal() bool {
}
func (g *GitOps) IsNoTeam() bool {
return g.TeamName != nil && isNoTeam(*g.TeamName)
return g.TeamName != nil && strings.EqualFold(*g.TeamName, noTeam)
}
func isNoTeam(teamName string) bool {
return strings.EqualFold(teamName, noTeam)
func (g *GitOps) IsUnassignedTeam() bool {
return g.TeamName != nil && strings.EqualFold(*g.TeamName, unassignedTeamName)
}
func (g *GitOps) CoercedTeamName() string {
@ -408,7 +423,10 @@ func (g *GitOps) CoercedTeamName() string {
return *g.TeamName
}
const noTeam = "No team"
const (
noTeam = "No team"
unassignedTeamName = "Unassigned"
)
func parseOrgSettings(raw json.RawMessage, result *GitOps, baseDir string, filePath string, multiError *multierror.Error) *multierror.Error {
var orgSettingsTop BaseItem
@ -570,7 +588,7 @@ func parseNoTeamSettings(raw json.RawMessage, result *GitOps, filePath string, m
for key := range teamSettingsMap {
if key != "webhook_settings" {
multiError = multierror.Append(multiError,
fmt.Errorf("unsupported settings option '%s' for 'No team' - only 'webhook_settings' is allowed", key))
fmt.Errorf("unsupported settings option '%s' in %s - only 'webhook_settings' is allowed", key, filepath.Base(filePath)))
}
}
@ -593,7 +611,7 @@ func parseNoTeamSettings(raw json.RawMessage, result *GitOps, filePath string, m
for key := range webhookMap {
if key != "failing_policies_webhook" {
multiError = multierror.Append(multiError,
fmt.Errorf("unsupported webhook_settings option '%s' for 'No team'; only 'failing_policies_webhook' is allowed", key))
fmt.Errorf("unsupported webhook_settings option '%s' in %s - only 'failing_policies_webhook' is allowed", key, filepath.Base(filePath)))
}
}
// If present, ensure failing_policies_webhook is an object or null
@ -670,7 +688,7 @@ func parseAgentOptions(top map[string]json.RawMessage, result *GitOps, baseDir s
agentOptionsRaw, ok := top["agent_options"]
if result.IsNoTeam() {
if ok {
logFn("[!] 'agent_options' is not supported for \"No team\". This key will be ignored.\n")
logFn("[!] 'agent_options' is not supported in %s. This key will be ignored.\n", filepath.Base(filePath))
}
return multiError
} else if !ok {
@ -1026,6 +1044,7 @@ func parseLabels(top map[string]json.RawMessage, result *GitOps, baseDir string,
}
func parsePolicies(top map[string]json.RawMessage, result *GitOps, baseDir string, filePath string, multiError *multierror.Error) *multierror.Error {
parentFilePath := filePath
policiesRaw, ok := top["policies"]
if !ok {
return multierror.Append(multiError, errors.New("'policies' key is required"))
@ -1040,7 +1059,7 @@ func parsePolicies(top map[string]json.RawMessage, result *GitOps, baseDir strin
multiError = multierror.Append(multiError, fmt.Errorf("failed to parse policy install_software %q: %v", item.Name, err))
continue
}
if err := parsePolicyRunScript(baseDir, result.TeamName, &item, result.Controls.Scripts); err != nil {
if err := parsePolicyRunScript(baseDir, parentFilePath, result.TeamName, &item, result.Controls.Scripts); err != nil {
multiError = multierror.Append(multiError, fmt.Errorf("failed to parse policy run_script %q: %v", item.Name, err))
continue
}
@ -1076,7 +1095,7 @@ func parsePolicies(top map[string]json.RawMessage, result *GitOps, baseDir strin
multiError = multierror.Append(multiError, fmt.Errorf("failed to parse policy install_software %q: %v", pp.Name, err))
continue
}
if err := parsePolicyRunScript(filepath.Dir(filePath), result.TeamName, pp, result.Controls.Scripts); err != nil {
if err := parsePolicyRunScript(filepath.Dir(filePath), parentFilePath, result.TeamName, pp, result.Controls.Scripts); err != nil {
multiError = multierror.Append(multiError, fmt.Errorf("failed to parse policy run_script %q: %v", pp.Name, err))
continue
}
@ -1103,7 +1122,7 @@ func parsePolicies(top map[string]json.RawMessage, result *GitOps, baseDir strin
item.Team = ""
}
if item.CalendarEventsEnabled && result.IsNoTeam() {
multiError = multierror.Append(multiError, fmt.Errorf("calendar events are not supported on \"No team\" policies: %q", item.Name))
multiError = multierror.Append(multiError, fmt.Errorf("calendar events are not supported on policies included in `%s`: %q", filepath.Base(parentFilePath), item.Name))
}
}
duplicates := getDuplicateNames(
@ -1117,7 +1136,7 @@ func parsePolicies(top map[string]json.RawMessage, result *GitOps, baseDir strin
return multiError
}
func parsePolicyRunScript(baseDir string, teamName *string, policy *Policy, scripts []BaseItem) error {
func parsePolicyRunScript(baseDir string, parentFilePath string, teamName *string, policy *Policy, scripts []BaseItem) error {
if policy.RunScript == nil {
policy.ScriptID = ptr.Uint(0) // unset the script
return nil
@ -1145,7 +1164,7 @@ func parsePolicyRunScript(baseDir string, teamName *string, policy *Policy, scri
}
if !scriptOnTeamFound {
if *teamName == noTeam {
return fmt.Errorf("policy script %s was not defined in controls in no-team.yml", scriptPath)
return fmt.Errorf("policy script %s was not defined in controls in %s", scriptPath, filepath.Base(parentFilePath))
}
return fmt.Errorf("policy script %s was not defined in controls for %s", scriptPath, *teamName)
}
@ -1228,7 +1247,7 @@ func parseQueries(top map[string]json.RawMessage, result *GitOps, baseDir string
reportsRaw, ok := top["reports"]
if result.IsNoTeam() {
if ok {
logFn("[!] 'reports' is not supported for \"No team\". This key will be ignored.\n")
logFn("[!] 'reports' is not supported in %s. This key will be ignored.\n", filepath.Base(filePath))
}
return multiError
} else if !ok {

View file

@ -623,35 +623,104 @@ func TestInvalidGitOpsYaml(t *testing.T) {
config += "name: No team\nsettings:\n features:\n enable_host_users: false\n"
noTeamPath3, noTeamBasePath3 := createNamedFileOnTempDir(t, "no-team.yml", config)
_, err = GitOpsFromFile(noTeamPath3, noTeamBasePath3, nil, nopLogf)
assert.ErrorContains(t, err, "unsupported settings option 'features' for 'No team' - only 'webhook_settings' is allowed")
assert.ErrorContains(t, err, "unsupported settings option 'features' in no-team.yml - only 'webhook_settings' is allowed")
// No team with multiple settings options (one valid, one invalid) should fail
config = getConfig([]string{"name", "settings"})
config += "name: No team\nsettings:\n webhook_settings:\n failing_policies_webhook:\n enable_failing_policies_webhook: true\n secrets:\n - secret: test\n"
noTeamPath4, noTeamBasePath4 := createNamedFileOnTempDir(t, "no-team.yml", config)
_, err = GitOpsFromFile(noTeamPath4, noTeamBasePath4, nil, nopLogf)
assert.ErrorContains(t, err, "unsupported settings option 'secrets' for 'No team' - only 'webhook_settings' is allowed")
assert.ErrorContains(t, err, "unsupported settings option 'secrets' in no-team.yml - only 'webhook_settings' is allowed")
// No team with host_status_webhook in webhook_settings should fail
config = getConfig([]string{"name", "settings"})
config += "name: No team\nsettings:\n webhook_settings:\n host_status_webhook:\n enable_host_status_webhook: true\n failing_policies_webhook:\n enable_failing_policies_webhook: true\n"
noTeamPath5a, noTeamBasePath5a := createNamedFileOnTempDir(t, "no-team.yml", config)
_, err = GitOpsFromFile(noTeamPath5a, noTeamBasePath5a, nil, nopLogf)
assert.ErrorContains(t, err, "unsupported webhook_settings option 'host_status_webhook' for 'No team'; only 'failing_policies_webhook' is allowed")
assert.ErrorContains(t, err, "unsupported webhook_settings option 'host_status_webhook' in no-team.yml - only 'failing_policies_webhook' is allowed")
// No team with vulnerabilities_webhook in webhook_settings should fail
config = getConfig([]string{"name", "settings"})
config += "name: No team\nsettings:\n webhook_settings:\n vulnerabilities_webhook:\n enable_vulnerabilities_webhook: true\n"
noTeamPath5b, noTeamBasePath5b := createNamedFileOnTempDir(t, "no-team.yml", config)
_, err = GitOpsFromFile(noTeamPath5b, noTeamBasePath5b, nil, nopLogf)
assert.ErrorContains(t, err, "unsupported webhook_settings option 'vulnerabilities_webhook' for 'No team'; only 'failing_policies_webhook' is allowed")
assert.ErrorContains(t, err, "unsupported webhook_settings option 'vulnerabilities_webhook' in no-team.yml - only 'failing_policies_webhook' is allowed")
// 'No team' file with invalid name.
config = getConfig([]string{"name", "settings"})
config += "name: No team\n"
noTeamPath6, noTeamBasePath6 := createNamedFileOnTempDir(t, "foobar.yml", config)
_, err = GitOpsFromFile(noTeamPath6, noTeamBasePath6, nil, nopLogf)
assert.ErrorContains(t, err, fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", noTeamPath6))
assert.ErrorContains(t, err, fmt.Sprintf("file `%s` for No Team must be named `no-team.yml`", noTeamPath6))
// no-team.yml with a non-"No Team" name should fail.
config = getConfig([]string{"name", "settings"})
config += "name: SomeOtherTeam\nsettings:\n secrets:\n"
noTeamPath7, noTeamBasePath7 := createNamedFileOnTempDir(t, "no-team.yml", config)
_, err = GitOpsFromFile(noTeamPath7, noTeamBasePath7, nil, nopLogf)
assert.ErrorContains(t, err, fmt.Sprintf("file %q must have team name 'No Team'", noTeamPath7))
// unassigned.yml with a non-"Unassigned" name should fail.
config = getConfig([]string{"name", "settings"})
config += "name: SomeOtherTeam\nsettings:\n secrets:\n"
unassignedPathBadName, unassignedBasePathBadName := createNamedFileOnTempDir(t, "unassigned.yml", config)
_, err = GitOpsFromFile(unassignedPathBadName, unassignedBasePathBadName, nil, nopLogf)
assert.ErrorContains(t, err, fmt.Sprintf("file %q must have team name 'Unassigned'", unassignedPathBadName))
// no-team.yml with "Unassigned" name should fail (wrong name for this file).
config = getConfig([]string{"name", "settings"})
config += "name: Unassigned\n"
noTeamPath8, noTeamBasePath8 := createNamedFileOnTempDir(t, "no-team.yml", config)
_, err = GitOpsFromFile(noTeamPath8, noTeamBasePath8, nil, nopLogf)
assert.ErrorContains(t, err, fmt.Sprintf("file %q must have team name 'No Team'", noTeamPath8))
// unassigned.yml with "No team" name should fail (wrong name for this file).
config = getConfig([]string{"name", "settings"})
config += "name: No team\n"
unassignedPathNoTeam, unassignedBasePathNoTeam := createNamedFileOnTempDir(t, "unassigned.yml", config)
_, err = GitOpsFromFile(unassignedPathNoTeam, unassignedBasePathNoTeam, nil, nopLogf)
assert.ErrorContains(t, err, fmt.Sprintf("file %q must have team name 'Unassigned'", unassignedPathNoTeam))
// 'Unassigned' team in unassigned.yml should work and coerce to "No team" internally.
config = getConfig([]string{"name", "settings"})
config += "name: Unassigned\n"
unassignedPath1, unassignedBasePath1 := createNamedFileOnTempDir(t, "unassigned.yml", config)
gitops, err = GitOpsFromFile(unassignedPath1, unassignedBasePath1, nil, nopLogf)
assert.NoError(t, err)
assert.NotNil(t, gitops)
assert.True(t, gitops.IsNoTeam(), "unassigned.yml should be treated as no-team after coercion")
assert.Equal(t, "No team", *gitops.TeamName)
// 'Unassigned' team with wrong filename should fail.
config = getConfig([]string{"name", "settings"})
config += "name: Unassigned\n"
unassignedPath2, unassignedBasePath2 := createNamedFileOnTempDir(t, "foobar.yml", config)
_, err = GitOpsFromFile(unassignedPath2, unassignedBasePath2, nil, nopLogf)
assert.ErrorContains(t, err, fmt.Sprintf("file `%s` for unassigned hosts must be named `unassigned.yml`", unassignedPath2))
// 'Unassigned' (case-insensitive) in unassigned.yml should work.
config = getConfig([]string{"name", "settings"})
config += "name: unassigned\n"
unassignedPath3, unassignedBasePath3 := createNamedFileOnTempDir(t, "unassigned.yml", config)
gitops, err = GitOpsFromFile(unassignedPath3, unassignedBasePath3, nil, nopLogf)
assert.NoError(t, err)
assert.NotNil(t, gitops)
assert.True(t, gitops.IsNoTeam())
// 'Unassigned' with webhook settings in unassigned.yml should work.
config = getConfig([]string{"name", "settings"})
config += "name: Unassigned\nsettings:\n webhook_settings:\n failing_policies_webhook:\n enable_failing_policies_webhook: true\n"
unassignedPath4, unassignedBasePath4 := createNamedFileOnTempDir(t, "unassigned.yml", config)
gitops, err = GitOpsFromFile(unassignedPath4, unassignedBasePath4, nil, nopLogf)
assert.NoError(t, err)
assert.NotNil(t, gitops)
// 'Unassigned' with invalid settings option should fail with unassigned.yml in message.
config = getConfig([]string{"name", "settings"})
config += "name: Unassigned\nsettings:\n features:\n enable_host_users: false\n"
unassignedPath5, unassignedBasePath5 := createNamedFileOnTempDir(t, "unassigned.yml", config)
_, err = GitOpsFromFile(unassignedPath5, unassignedBasePath5, nil, nopLogf)
assert.ErrorContains(t, err, "unsupported settings option 'features' in unassigned.yml")
// Missing secrets
config = getConfig([]string{"settings"})