<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #44333 # Checklist for submitter - [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. Also added some integration tests as a follow-up of the first PR (https://github.com/fleetdm/fleet/pull/44390). - [x] QA'd all new/changed functionality manually #### generate-gitops - Branched off to main, no URLs set, then ran generate-gitops on this branch. Deprecated keys gone, new keys present. <img width="447" height="170" alt="nourls_new" src="https://github.com/user-attachments/assets/61931615-d61b-44d3-8095-f7a2b9bd8871" /> - Branched off to main, set external URLs for both light and dark modes, then ran generate-gitops on this branch. Deprecated keys gone, new keys set with the external URLs. <img width="637" height="471" alt="externalurl_main" src="https://github.com/user-attachments/assets/c3782756-acc2-4b99-812d-86e145f11ad5" /> <img width="459" height="168" alt="externalurl_new" src="https://github.com/user-attachments/assets/aa2d8825-3c47-40ba-ab91-bb8202afe81a" /> - Within this branch, after uploading a custom logo for light mode, ran generate-gitops. The logo was saved in lib/org_logo/light.webp <img width="1510" height="639" alt="Screenshot 2026-05-04 at 4 06 59 PM" src="https://github.com/user-attachments/assets/13318c24-8fa4-4e29-b629-ff723d4afe5a" /> <img width="786" height="172" alt="Screenshot 2026-05-04 at 4 07 30 PM" src="https://github.com/user-attachments/assets/b46bd1df-7dcd-4489-b7da-4cbad77b25b8" /> #### gitops - Applied gitops with two external URLs. Verified in the UI that those are still present <img width="944" height="189" alt="Screenshot 2026-05-04 at 7 54 53 AM" src="https://github.com/user-attachments/assets/a34813ca-beb1-403e-9793-d42cc9c72f8b" /> <img width="637" height="259" alt="Screenshot 2026-05-04 at 8 01 04 AM" src="https://github.com/user-attachments/assets/74c2cd56-ab1d-4ddd-9b8e-22c49e9ae9d5" /> - Applied gitops with "" as the URLs to clear them. Verified the default fleet logo is shown. <img width="460" height="201" alt="Screenshot 2026-05-04 at 8 15 11 AM" src="https://github.com/user-attachments/assets/dcbafea3-b4ea-44aa-9045-08c4f5a64e98" /> <img width="648" height="269" alt="Screenshot 2026-05-04 at 8 15 50 AM" src="https://github.com/user-attachments/assets/451a28f9-e929-4b84-93d3-a7dd9afd5eca" /> - Applied gitops with a custom logo for light theme, using **org_logo_path_light_mode**: <img width="948" height="207" alt="Screenshot 2026-05-04 at 4 10 05 PM" src="https://github.com/user-attachments/assets/b1418cd4-31cc-4e53-b566-9af11ec21970" /> <img width="774" height="168" alt="Screenshot 2026-05-04 at 4 10 35 PM" src="https://github.com/user-attachments/assets/63f596eb-308f-4122-ad86-e1d718e9b525" /> ## New Fleet configuration settings - [x] Verified that the setting is exported via `fleetctl generate-gitops` - [x] 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) - See https://github.com/fleetdm/fleet/pull/43808. - [x] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [x] Verified that any relevant UI is disabled when GitOps mode is enabled <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * GitOps support for uploading custom org logos (dark/light) via local files. * `fleetctl generate-gitops` exports Fleet-hosted logos as local files and inserts path references. * New API endpoints to upload, delete, and fetch org logos. * **Deprecated** * Legacy logo keys consolidated into mode-specific URL keys (`org_logo_url_dark_mode`, `org_logo_url_light_mode`). * **Bug Fixes / Validation** * Validation/error when both a path and URL are provided for the same mode; file size and image-format checks enforced. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
196 lines
7.7 KiB
Go
196 lines
7.7 KiB
Go
package spec
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/platform/logging"
|
|
)
|
|
|
|
// DeprecatedKeyMapping defines a mapping from an old YAML key path to a new one.
|
|
// Paths use dot notation for nested keys, with [] to indicate "all array elements".
|
|
// Examples:
|
|
// - "team_settings" -> "settings"
|
|
// - "queries" -> "reports"
|
|
// - "org_settings.mdm.apple_business_manager[].macos_team" -> "org_settings.mdm.apple_business_manager[].macos_fleet"
|
|
type DeprecatedKeyMapping struct {
|
|
OldPath string
|
|
NewPath string
|
|
}
|
|
|
|
// DeprecatedGitOpsKeyMappings is the single source of truth for all deprecated key renames.
|
|
// It serves two purposes:
|
|
// 1. ApplyDeprecatedKeyMappings uses the full paths to migrate deprecated keys in gitops YAML input.
|
|
// 2. buildAliasRules (in generate_gitops.go) extracts leaf key names to rename keys in
|
|
// serialized output for generate_gitops, fleetctl get, and fleetctl apply.
|
|
//
|
|
// When adding new deprecations, add them here.
|
|
var DeprecatedGitOpsKeyMappings = []DeprecatedKeyMapping{
|
|
// Top-level gitops keys
|
|
{"team_settings", "settings"},
|
|
{"queries", "reports"},
|
|
|
|
// Controls: macos_settings -> apple_settings (parent first, then children)
|
|
{"controls.macos_settings", "controls.apple_settings"},
|
|
{"controls.apple_settings.custom_settings", "controls.apple_settings.configuration_profiles"},
|
|
|
|
// Controls: windows_settings children
|
|
{"controls.windows_settings.custom_settings", "controls.windows_settings.configuration_profiles"},
|
|
|
|
// Controls: android_settings children
|
|
{"controls.android_settings.custom_settings", "controls.android_settings.configuration_profiles"},
|
|
|
|
// Controls: macos_setup -> setup_experience (parent first, then children)
|
|
{"controls.macos_setup", "controls.setup_experience"},
|
|
{"controls.setup_experience.bootstrap_package", "controls.setup_experience.macos_bootstrap_package"},
|
|
{"controls.setup_experience.macos_setup_assistant", "controls.setup_experience.apple_setup_assistant"},
|
|
{"controls.setup_experience.enable_release_device_manually", "controls.setup_experience.apple_enable_release_device_manually"},
|
|
{"controls.setup_experience.script", "controls.setup_experience.macos_script"},
|
|
{"controls.setup_experience.manual_agent_install", "controls.setup_experience.macos_manual_agent_install"},
|
|
{"controls.setup_experience.enable_managed_local_account", "controls.setup_experience.enable_create_local_admin_account"},
|
|
|
|
// Org settings: server_settings
|
|
{"org_settings.server_settings.live_query_disabled", "org_settings.server_settings.live_reporting_disabled"},
|
|
{"org_settings.server_settings.query_reports_disabled", "org_settings.server_settings.discard_reports_data"},
|
|
{"org_settings.server_settings.query_report_cap", "org_settings.server_settings.report_cap"},
|
|
|
|
// Org settings: org_info logo URL fields renamed to mode-aware variants.
|
|
{"org_settings.org_info.org_logo_url", "org_settings.org_info.org_logo_url_dark_mode"},
|
|
{"org_settings.org_info.org_logo_url_light_background", "org_settings.org_info.org_logo_url_light_mode"},
|
|
|
|
// Nested keys in org_settings.mdm.apple_business_manager[]
|
|
{"org_settings.mdm.apple_business_manager[].macos_team", "org_settings.mdm.apple_business_manager[].macos_fleet"},
|
|
{"org_settings.mdm.apple_business_manager[].ios_team", "org_settings.mdm.apple_business_manager[].ios_fleet"},
|
|
{"org_settings.mdm.apple_business_manager[].ipados_team", "org_settings.mdm.apple_business_manager[].ipados_fleet"},
|
|
|
|
// Nested keys in org_settings.mdm.volume_purchasing_program[]
|
|
{"org_settings.mdm.volume_purchasing_program[].teams", "org_settings.mdm.volume_purchasing_program[].fleets"},
|
|
|
|
// The following entries are renameto tags on struct fields that appear in serialized
|
|
// API output (fleetctl get and fleetctl apply) but are not gitops input keys. They are
|
|
// included here so that buildAliasRules can derive them. The paths are leaf-only (no dots)
|
|
// since they don't participate in ApplyDeprecatedKeyMappings traversal.
|
|
{"available_teams", "available_fleets"},
|
|
{"default_team", "default_fleet"},
|
|
{"host_team_id", "host_fleet_id"},
|
|
{"inherited_query_count", "inherited_report_count"},
|
|
{"ios_team_id", "ios_fleet_id"},
|
|
{"ipados_team_id", "ipados_fleet_id"},
|
|
{"live_query_results", "live_report_results"},
|
|
{"macos_team_id", "macos_fleet_id"},
|
|
{"query_count", "report_count"},
|
|
{"query_id", "report_id"},
|
|
{"query_ids", "report_ids"},
|
|
{"query_name", "report_name"},
|
|
{"query_stats", "report_stats"},
|
|
{"scheduled_query_id", "scheduled_report_id"},
|
|
{"scheduled_query_name", "scheduled_report_name"},
|
|
{"team", "fleet"},
|
|
{"team_id", "fleet_id"},
|
|
{"team_ids_by_name", "fleet_ids_by_name"},
|
|
{"team_ids", "fleet_ids"},
|
|
{"team_name", "fleet_name"},
|
|
}
|
|
|
|
// ApplyDeprecatedKeyMappings walks the YAML data map and migrates deprecated keys to their new names.
|
|
// It logs warnings for each deprecated key found and returns an error if both old and new keys are specified.
|
|
// After this function returns successfully, only the new key names will be present in the data.
|
|
func ApplyDeprecatedKeyMappings(data map[string]any, logFn Logf) error {
|
|
for _, mapping := range DeprecatedGitOpsKeyMappings {
|
|
if err := migrateKeyPath(data, mapping.OldPath, mapping.NewPath, logFn); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrateKeyPath migrates a single deprecated key path to its new path.
|
|
// Paths are dot-separated, with [] indicating iteration over array elements.
|
|
func migrateKeyPath(data map[string]any, oldPath, newPath string, logFn Logf) error {
|
|
oldParts := strings.Split(oldPath, ".")
|
|
newParts := strings.Split(newPath, ".")
|
|
|
|
return migrateKeyPathRecursive(data, oldParts, newParts, oldPath, newPath, logFn)
|
|
}
|
|
|
|
func migrateKeyPathRecursive(data map[string]any, oldParts, newParts []string, fullOldPath, fullNewPath string, logFn Logf) error {
|
|
if len(oldParts) == 0 || len(newParts) == 0 {
|
|
return nil
|
|
}
|
|
|
|
oldKey := oldParts[0]
|
|
newKey := newParts[0]
|
|
|
|
// Check if this key references an array (e.g. "apple_business_manager[]").
|
|
if trimmed, ok := strings.CutSuffix(oldKey, "[]"); ok {
|
|
oldKey = trimmed
|
|
arr, ok := data[oldKey]
|
|
if !ok {
|
|
return nil // Key doesn't exist, nothing to migrate
|
|
}
|
|
|
|
arrSlice, ok := arr.([]any)
|
|
if !ok {
|
|
return nil // Not an array, skip
|
|
}
|
|
|
|
// Recurse into each array element with the remaining path parts.
|
|
for i, elem := range arrSlice {
|
|
elemMap, ok := elem.(map[string]any)
|
|
if !ok {
|
|
continue // Not a map, skip
|
|
}
|
|
|
|
if err := migrateKeyPathRecursive(elemMap, oldParts[1:], newParts[1:], fullOldPath, fullNewPath, logFn); err != nil {
|
|
return fmt.Errorf("in array element %d: %w", i, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Check if we're at the final key (leaf)
|
|
if len(oldParts) == 1 {
|
|
return migrateLeafKey(data, oldKey, newKey, fullOldPath, fullNewPath, logFn)
|
|
}
|
|
|
|
// Recurse into nested map
|
|
nested, ok := data[oldKey]
|
|
if !ok {
|
|
return nil // Key doesn't exist, nothing to migrate
|
|
}
|
|
|
|
nestedMap, ok := nested.(map[string]any)
|
|
if !ok {
|
|
return nil // Not a map, skip
|
|
}
|
|
|
|
return migrateKeyPathRecursive(nestedMap, oldParts[1:], newParts[1:], fullOldPath, fullNewPath, logFn)
|
|
}
|
|
|
|
// migrateLeafKey handles the actual key migration at the leaf level.
|
|
func migrateLeafKey(data map[string]any, oldKey, newKey, fullOldPath, fullNewPath string, logFn Logf) error {
|
|
oldValue, oldExists := data[oldKey]
|
|
_, newExists := data[newKey]
|
|
|
|
if !oldExists {
|
|
return nil // Old key doesn't exist, nothing to migrate
|
|
}
|
|
|
|
if newExists {
|
|
return fmt.Errorf("cannot specify both '%s' (deprecated) and '%s'; use only '%s'", fullOldPath, fullNewPath, fullNewPath)
|
|
}
|
|
|
|
// Log deprecation warning
|
|
if logFn != nil {
|
|
if logging.TopicEnabled(logging.DeprecatedFieldTopic) {
|
|
logFn("[!] '%s' is deprecated; use '%s' instead\n", fullOldPath, fullNewPath)
|
|
}
|
|
}
|
|
|
|
// Copy value to new key and remove old key
|
|
data[newKey] = oldValue
|
|
delete(data, oldKey)
|
|
|
|
return nil
|
|
}
|