mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** For #40015 * Moves repeated empty mocks into a new `setupEmptyGitOpsMocks` method * Adds new "deprecation" tests: * In TestGitOpsFullGlobal, TestGitOpsFullTeam and TestGitOpsFullGlobalAndTeam tests "kitchen sink" with both new and deprecated keys * Added keys and checks to verify `setup_experience`, `apple_business_manager` and `volume_purchasing_program` configs * Consolidated map of deprecated -> new GitOps keys in one place
190 lines
7.3 KiB
Go
190 lines
7.3 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"},
|
|
|
|
// 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"},
|
|
|
|
// 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
|
|
}
|