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:
@@ -0,0 +1 @@
|
||||
- Updated `generate-gitops` command to output filenames with emojis and other special characters where applicable
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+7
-7
@@ -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:
|
||||
Reference in New Issue
Block a user