Allow emoji in team names (#32491)

for #31202 

# Details

This PR updates the filename generation code in `generate-gitops` to be
more permissive. It will still replace spaces with dashes, but otherwise
all characters (including emojis).

# 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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
- generate-gitops now preserves emojis and other special characters in
generated filenames and outputs.
- Team names with emojis are fully supported, including corresponding
resource paths.
- Bug Fixes
- Replaced escaped Unicode sequences in YAML output with actual
characters, improving readability and parity with source data.
- Tests
- Updated test data and scenarios to include emoji-containing team names
and paths to ensure coverage for special character handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Scott Gress
2025-09-04 16:12:09 -05:00
committed by GitHub
parent 73b629ee1b
commit 06a43cbcfb
11 changed files with 98 additions and 21 deletions
+28 -3
View File
@@ -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)
+55 -4
View File
@@ -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\"in<it>omg|": "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)
})
}
}
@@ -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
@@ -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: