diff --git a/changes/31202-allow-special-chars-in-generated-gitops-files b/changes/31202-allow-special-chars-in-generated-gitops-files new file mode 100644 index 0000000000..f863f5f036 --- /dev/null +++ b/changes/31202-allow-special-chars-in-generated-gitops-files @@ -0,0 +1 @@ +- Updated `generate-gitops` command to output filenames with emojis and other special characters where applicable diff --git a/cmd/fleetctl/fleetctl/generate_gitops.go b/cmd/fleetctl/fleetctl/generate_gitops.go index f221d17b5e..dbda70d187 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops.go +++ b/cmd/fleetctl/fleetctl/generate_gitops.go @@ -4,10 +4,13 @@ import ( "bytes" "errors" "fmt" + "math" "os" pathUtils "path" "reflect" "regexp" + "slices" + "strconv" "strings" "unicode" @@ -284,8 +287,7 @@ func (cmd *GenerateGitopsCommand) Run() error { if cmd.CLI.String("team") != "" { transformedSelectedName := generateFilename(cmd.CLI.String("team")) for _, team := range teams { - transformedTeamName := generateFilename(team.Name) - if transformedSelectedName == transformedTeamName { + if transformedSelectedName == generateFilename(team.Name) { teamsToProcess = []teamToProcess{{ ID: &team.ID, Team: &team, @@ -488,6 +490,8 @@ func (cmd *GenerateGitopsCommand) Run() error { } // Replace any empty values with a blank. b = emptyVal.ReplaceAll(b, []byte(":")) + // Unescape any unicode chars added by the YAML marshaler. + b = unescapeUnicodeU8(b) } else { b = []byte(fileToWrite.(string)) } @@ -559,6 +563,8 @@ func (cmd *GenerateGitopsCommand) AddComment(filename, comment string) string { return token } +var footguns = []rune{'/', '\\', ':', '*', '?', '"', '<', '>', '|'} + // Given a name, generate a filename by replacing spaces with dashes and // removing any non-alphanumeric characters. func generateFilename(name string) string { @@ -568,8 +574,14 @@ func generateFilename(name string) string { return unicode.ToLower(r) case unicode.IsSpace(r): return '-' + // replace common footguns with unique letters. + case slices.Contains(footguns, r): + return rune('a' + slices.Index(footguns, r)) + // bail on control characters + case r < 0x20: + panic("Cannot process filename " + name + " because it has control characters in it.") default: - return -1 + return r } }, name) // Strip any leading/trailing dashes using regex. @@ -1502,4 +1514,17 @@ func (cmd *GenerateGitopsCommand) generateLabels() ([]map[string]interface{}, er return result, nil } +var uniEscape = regexp.MustCompile(`\\U([0-9A-Fa-f]{8})`) + +// Utility function to unescape Unicode U+XXXX sequences added by the YAML marshaler. +func unescapeUnicodeU8(b []byte) []byte { + return uniEscape.ReplaceAllFunc(b, func(m []byte) []byte { + v, err := strconv.ParseUint(string(m[2:]), 16, 32) + if err != nil || v > math.MaxInt32 { + return m + } + return []byte(string(rune(v))) + }) +} + var _ generateGitopsClient = (*service.Client)(nil) diff --git a/cmd/fleetctl/fleetctl/generate_gitops_test.go b/cmd/fleetctl/fleetctl/generate_gitops_test.go index 20c1d81491..eaf66baff8 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops_test.go +++ b/cmd/fleetctl/fleetctl/generate_gitops_test.go @@ -21,7 +21,8 @@ import ( ) type MockClient struct { - IsFree bool + IsFree bool + TeamNameOverride string } func (c *MockClient) GetAppConfig() (*fleet.EnrichedAppConfig, error) { @@ -53,19 +54,19 @@ func (MockClient) GetEnrollSecretSpec() (*fleet.EnrollSecretSpec, error) { return spec, nil } -func (MockClient) ListTeams(query string) ([]fleet.Team, error) { +func (c *MockClient) ListTeams(query string) ([]fleet.Team, error) { + var config fleet.TeamConfig b, err := os.ReadFile("./testdata/generateGitops/teamConfig.json") if err != nil { return nil, err } - var config fleet.TeamConfig if err := json.Unmarshal(b, &config); err != nil { return nil, err } teams := []fleet.Team{ { ID: 1, - Name: "Team A", + Name: "Team A 👍", Config: config, Secrets: []*fleet.EnrollSecret{ { @@ -74,6 +75,9 @@ func (MockClient) ListTeams(query string) ([]fleet.Team, error) { }, }, } + if c.TeamNameOverride != "" { + teams[0].Name = c.TeamNameOverride + } return teams, nil } @@ -1223,3 +1227,50 @@ func TestGenerateControlsAndMDMWithoutMDMEnabledAndConfigured(t *testing.T) { require.Empty(t, mdmRaw[key]) } } + +func TestSillyTeamNames(t *testing.T) { + sillyTeamNames := map[string]string{ + "🫆": "🫆.yml", + "🪾": "🪾.yml", + "🫜": "🫜.yml", + "🪉": "🪉.yml", + "🪏": "🪏.yml", + "🫟": "🫟.yml", + "👍": "👍.yml", + "a/team\\with:all*the?footguns\"inomg|": "aateambwithcalldtheefootgunsfingithomgi.yml", + } + + fleetClient := &MockClient{} + tempDir := os.TempDir() + "/" + uuid.New().String() + + t.Cleanup(func() { + if err := os.RemoveAll(tempDir); err != nil { + t.Fatalf("failed to remove temp dir: %v", err) + } + }) + + for name, expectedFilename := range sillyTeamNames { + t.Run(name, func(t *testing.T) { + flagSet := flag.NewFlagSet("test", flag.ContinueOnError) + flagSet.String("dir", tempDir, "") + flagSet.Bool("force", true, "") + fleetClient.TeamNameOverride = name + action := createGenerateGitopsAction(fleetClient) + buf := new(bytes.Buffer) + cliContext := cli.NewContext(&cli.App{ + Name: "test", + Usage: "test", + Writer: buf, + ErrWriter: buf, + }, flagSet, nil) + // Get the test app config. + err := action(cliContext) + require.NoError(t, err, buf.String()) + + // Expect a correctly-named .yaml + tgtPath := filepath.Join(tempDir, "teams", expectedFilename) + _, err = os.Stat(tgtPath) + require.NoError(t, err) + }) + } +} diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml index 3a4a2de14f..9e346f4547 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml @@ -90,9 +90,9 @@ org_settings: url: https://some-zendesk-url.com mdm: apple_business_manager: - - ios_team: "\U0001F4F1\U0001F3E2 Company-owned mobile devices" - ipados_team: "\U0001F4F1\U0001F3E2 Company-owned mobile devices" - macos_team: "\U0001F4BB Workstations" + - ios_team: "📱🏢 Company-owned mobile devices" + ipados_team: "📱🏢 Company-owned mobile devices" + macos_team: "💻 Workstations" organization_name: Fleet Device Management Inc. apple_server_url: http://some-apple-server-url.com end_user_authentication: @@ -105,10 +105,10 @@ org_settings: volume_purchasing_program: - location: Fleet Device Management Inc. teams: - - "\U0001F4BB Workstations" - - "\U0001F4BB\U0001F423 Workstations (canary)" - - "\U0001F4F1\U0001F3E2 Company-owned mobile devices" - - "\U0001F4F1\U0001F510 Personal mobile devices" + - "💻 Workstations" + - "💻🐣 Workstations (canary)" + - "📱🏢 Company-owned mobile devices" + - "📱🔐 Personal mobile devices" org_info: contact_url: https://fleetdm.com/company/contact org_logo_url: http://some-org-logo-url.com diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/profiles/team-macos-mobileconfig-profile.mobileconfig b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/profiles/team-macos-mobileconfig-profile.mobileconfig similarity index 100% rename from cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/profiles/team-macos-mobileconfig-profile.mobileconfig rename to cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/profiles/team-macos-mobileconfig-profile.mobileconfig diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/queries/my-software-package-darwin-preinstallquery.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/queries/my-software-package-darwin-preinstallquery.yml similarity index 100% rename from cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/queries/my-software-package-darwin-preinstallquery.yml rename to cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/queries/my-software-package-darwin-preinstallquery.yml diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/scripts/Script B.ps1 b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/scripts/Script B.ps1 similarity index 100% rename from cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/scripts/Script B.ps1 rename to cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/scripts/Script B.ps1 diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/scripts/my-software-package-darwin-install b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/scripts/my-software-package-darwin-install similarity index 100% rename from cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/scripts/my-software-package-darwin-install rename to cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/scripts/my-software-package-darwin-install diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/scripts/my-software-package-darwin-postinstall b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/scripts/my-software-package-darwin-postinstall similarity index 100% rename from cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/scripts/my-software-package-darwin-postinstall rename to cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/scripts/my-software-package-darwin-postinstall diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/scripts/my-software-package-darwin-uninstall b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/scripts/my-software-package-darwin-uninstall similarity index 100% rename from cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a/scripts/my-software-package-darwin-uninstall rename to cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/lib/team-a-👍/scripts/my-software-package-darwin-uninstall diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a-👍.yml similarity index 85% rename from cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a.yml rename to cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a-👍.yml index 7caa9b5aa2..c48e3bf08b 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a.yml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a-👍.yml @@ -26,18 +26,18 @@ controls: minimum_version: "98.2" macos_settings: custom_settings: - - path: ../lib/team-a/profiles/team-macos-mobileconfig-profile.mobileconfig + - path: "../lib/team-a-👍/profiles/team-macos-mobileconfig-profile.mobileconfig" macos_updates: deadline: "2020-12-31" minimum_version: "95.1" scripts: - - path: ../lib/team-a/scripts/Script B.ps1 + - path: "../lib/team-a-👍/scripts/Script B.ps1" windows_enabled_and_configured: true windows_require_bitlocker_pin: false windows_updates: deadline_days: 95 grace_period_days: 92 -name: Team A +name: "Team A 👍" policies: - calendar_events_enabled: false conditional_access_enabled: true @@ -73,18 +73,18 @@ software: - Browsers hash_sha256: software-package-hash # My Software Package (my-software.pkg) version 13.37 install_script: - path: ../lib/team-a/scripts/my-software-package-darwin-install + path: "../lib/team-a-👍/scripts/my-software-package-darwin-install" labels_include_any: - Label A - Label B post_install_script: - path: ../lib/team-a/scripts/my-software-package-darwin-postinstall + path: "../lib/team-a-👍/scripts/my-software-package-darwin-postinstall" pre_install_query: - path: ../lib/team-a/queries/my-software-package-darwin-preinstallquery.yml + path: "../lib/team-a-👍/queries/my-software-package-darwin-preinstallquery.yml" self_service: true setup_experience: true uninstall_script: - path: ../lib/team-a/scripts/my-software-package-darwin-uninstall + path: "../lib/team-a-👍/scripts/my-software-package-darwin-uninstall" url: https://example.com/download/my-software.pkg team_settings: features: