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 #41091 # Details This PR finishes the work of aliasing multi-platform keys by: * Added the renames to the list maintained by generate-gitops so that `fleetctl get` can use the new names * Updated the code that adds the new names to API and `fleetctl get` output to only add new nested keys under new parents, e.g. add `apple_settings.configuration_profiles`, but not `macos_settings.configuration_profiles`. The API key duplicator now runs through `RewriteDeprecatedKeys` which is a little heavier per-token, but for old keys we're doing less work so I think this ends up being slightly more performant than before, at least for large payloads. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. n/a, changelog for new keys added in previous PR ## Testing - [X] Added/updated automated tests updated tests for the duplicators - [X] QA'd all new/changed functionality manually - [X] `/config` and `/fleets` APIs now only return new keys under new parents - [X] `fleetctl get fleets` now returns new multiplatform keys
181 lines
5.2 KiB
Go
181 lines
5.2 KiB
Go
package endpointer
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
|
|
"github.com/go-json-experiment/json/jsontext"
|
|
)
|
|
|
|
// DuplicateJSONKeysOpts controls optional behavior of DuplicateJSONKeys.
|
|
type DuplicateJSONKeysOpts struct {
|
|
// Compact disables pretty-printing. By default, output is indented with
|
|
// two spaces to match the standard API response format.
|
|
Compact bool
|
|
}
|
|
|
|
// DuplicateJSONKeys takes marshaled JSON and, for each AliasRule, duplicates
|
|
// keys so that both the old (native) and new names appear in the output.
|
|
// For example, if a rule maps OldKey:"team_id" → NewKey:"fleet_id", and the
|
|
// JSON contains "team_id": 42, the output will contain both "team_id": 42
|
|
// and "fleet_id": 42.
|
|
//
|
|
// If the new key already exists in the same object scope, the duplication is
|
|
// skipped for that key (to avoid producing duplicate keys when the source
|
|
// struct already has both, or when the function is called more than once).
|
|
//
|
|
// The function uses jsontext.Decoder/Encoder for token-level processing,
|
|
// delegating all JSON lexing (string escaping, unicode, nesting) to the
|
|
// library. Duplicates are deferred until the closing '}' of each object so
|
|
// that naturally-occurring new keys can be detected and skipped.
|
|
func DuplicateJSONKeys(data []byte, rules []AliasRule, opts ...DuplicateJSONKeysOpts) []byte {
|
|
if len(rules) == 0 || len(data) == 0 {
|
|
return data
|
|
}
|
|
|
|
oldToNew := make(map[string]string, len(rules))
|
|
newToOld := make(map[string]string, len(rules))
|
|
for _, r := range rules {
|
|
oldToNew[r.OldKey] = r.NewKey
|
|
newToOld[r.NewKey] = r.OldKey
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
dec := jsontext.NewDecoder(bytes.NewReader(data), jsontext.AllowDuplicateNames(true))
|
|
encOpts := []jsontext.Options{jsontext.AllowDuplicateNames(true)}
|
|
if len(opts) == 0 || !opts[0].Compact {
|
|
encOpts = append(encOpts, jsontext.WithIndent(" "))
|
|
}
|
|
enc := jsontext.NewEncoder(&buf, encOpts...)
|
|
|
|
// pendingDup holds a key-value pair that should be inserted as a
|
|
// duplicate at the end of the current object scope (before '}'),
|
|
// unless the new key was found naturally in the same scope.
|
|
type pendingDup struct {
|
|
newKey string
|
|
value jsontext.Value
|
|
}
|
|
|
|
// Per-object-scope state: pending duplicates and naturally-seen new keys.
|
|
type scopeState struct {
|
|
pending []pendingDup
|
|
naturalNew map[string]bool
|
|
}
|
|
var scopes []scopeState
|
|
|
|
for {
|
|
tok, err := dec.ReadToken()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
// On any error, return the original data unchanged.
|
|
return data
|
|
}
|
|
|
|
kind := tok.Kind()
|
|
|
|
switch kind {
|
|
case '{':
|
|
scopes = append(scopes, scopeState{naturalNew: make(map[string]bool)})
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return data
|
|
}
|
|
|
|
case '}':
|
|
// Before closing the object, emit any pending duplicates whose
|
|
// new key was not seen naturally in this scope.
|
|
if len(scopes) > 0 {
|
|
scope := scopes[len(scopes)-1]
|
|
for _, dup := range scope.pending {
|
|
if scope.naturalNew[dup.newKey] {
|
|
continue // new key exists naturally; skip duplicate
|
|
}
|
|
if err := enc.WriteToken(jsontext.String(dup.newKey)); err != nil {
|
|
return data
|
|
}
|
|
if err := enc.WriteValue(dup.value); err != nil {
|
|
return data
|
|
}
|
|
}
|
|
scopes = scopes[:len(scopes)-1]
|
|
}
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return data
|
|
}
|
|
|
|
case '"':
|
|
// Determine if this string is an object key.
|
|
isKey := false
|
|
depth := dec.StackDepth()
|
|
if depth > 0 {
|
|
parentKind, length := dec.StackIndex(depth)
|
|
if parentKind == '{' && length%2 == 1 {
|
|
isKey = true
|
|
}
|
|
}
|
|
|
|
if isKey {
|
|
keyName := tok.String()
|
|
|
|
// Track new keys that appear naturally.
|
|
if len(scopes) > 0 && newToOld[keyName] != "" {
|
|
scopes[len(scopes)-1].naturalNew[keyName] = true
|
|
}
|
|
|
|
// Check if this key is deprecated and should generate a duplicate.
|
|
newKey, shouldDuplicate := oldToNew[keyName]
|
|
if shouldDuplicate {
|
|
// Write the old key.
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return data
|
|
}
|
|
|
|
// Read the raw value.
|
|
val, err := dec.ReadValue()
|
|
if err != nil {
|
|
return data
|
|
}
|
|
|
|
// Write the original value as-is for the old key — it
|
|
// already uses old names from json.Marshal, so no
|
|
// transformation is needed.
|
|
if err := enc.WriteValue(val); err != nil {
|
|
return data
|
|
}
|
|
|
|
// For the new key, rename nested keys to new names only
|
|
// (removing old names) so the new-name subtree is clean.
|
|
newVal, renameErr := RewriteOldToNewKeys([]byte(val), rules)
|
|
if renameErr != nil {
|
|
newVal = []byte(val) // fall back to original value on error
|
|
}
|
|
|
|
// Defer the duplicate for emission at '}'.
|
|
if len(scopes) > 0 {
|
|
scopes[len(scopes)-1].pending = append(
|
|
scopes[len(scopes)-1].pending,
|
|
pendingDup{newKey: newKey, value: jsontext.Value(newVal)},
|
|
)
|
|
}
|
|
} else { // !shouldDuplicate (no old key match) — just write the key as-is
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return data
|
|
}
|
|
}
|
|
} else { // !isKey — string value, not a key — just write as-is
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return data
|
|
}
|
|
}
|
|
|
|
default:
|
|
// All other tokens: [, ], numbers, bools, null — pass through.
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return data
|
|
}
|
|
}
|
|
}
|
|
|
|
return buf.Bytes()
|
|
}
|