Files
fleet/pkg/spec/gitops_deprecations.go
T
Nico f2b2e23b0a GitOps changes for custom org's logo uploads (#44550)
<!-- 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 -->
2026-05-05 18:18:08 +02:00

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
}