fleet/pkg/spec/gitops_deprecations.go
Scott Gress 3ae98ee01d
Clean up Gitops tests and add deprecation tests (#43039)
<!-- 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
2026-04-08 11:57:03 -05:00

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
}