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:** Resolves #41385 # Details This PR updates `fleetctl` to use the new API urls and params when communicating with Fleet server. This avoids deprecation warnings showing up on the server that users won't be able to fix. Most of the changes are straightforward `team_id` -> `fleet_id`. A couple of code changes have been pointed out. The most interesting is in icon URLs, which can be persisted in the database (so we'll need to do a migration in Fleet 5 if we want to drop support for `team_id`. Similarly the FMA download urls are briefly persisted in the db for the purpose of sending MDM commands. If we drop team_id support in Fleet 5 there could be a brief window where there are unprocessed commands in the db still with `team_id` in them, so we'll probably want to migrate those as well. # 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 - all internal ## Testing - [X] Added/updated automated tests - [X] QA'd all new/changed functionality manually - [X] ran `fleetctl gitops` on main and saw a bunch of deprecation warnings, ran it on this branch and the warnings were gone 💨 - [X] same with `fleetctl generate-gitops` - [X] ran `fleetctl get` commands and verified that the new URLs and params were used - [X] ran `fleetctl apply` commands and verified that the new URLs and params were used
278 lines
8.7 KiB
Go
278 lines
8.7 KiB
Go
package endpointer
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/go-json-experiment/json/jsontext"
|
|
)
|
|
|
|
// AliasConflictError is returned when both the deprecated and new field names
|
|
// are specified in the same JSON object scope. For example, if "team_id" is
|
|
// renamed to "fleet_id", and a request contains both, this error is returned.
|
|
type AliasConflictError struct {
|
|
Old string
|
|
New string
|
|
}
|
|
|
|
func (e *AliasConflictError) Error() string {
|
|
return fmt.Sprintf("Conflicting field names: cannot specify both `%s` (deprecated) and `%s` in the same request", e.Old, e.New)
|
|
}
|
|
|
|
// AliasRule defines a key-rename rule: the deprecated (old) key name and its
|
|
// replacement (new) key name. The struct's json tag uses OldKey (the current
|
|
// name), and renameto specifies NewKey (the target name). The rewriter
|
|
// accepts both names in requests: OldKey passes through as-is (with
|
|
// deprecation tracking) and NewKey is rewritten to OldKey for deserialization.
|
|
type AliasRule struct {
|
|
OldKey string
|
|
NewKey string
|
|
}
|
|
|
|
// JSONKeyRewriteReader is a streaming io.Reader that handles
|
|
// JSON key aliasing while reading. It:
|
|
//
|
|
// - Passes through OldKey (deprecated) names as-is (the struct expects them)
|
|
// and tracks them in usedDeprecated for deprecation logging.
|
|
// - Rewrites NewKey names to OldKey so the struct can deserialize them.
|
|
// - Detects alias conflicts: if both OldKey and NewKey appear in the same
|
|
// JSON object scope, it returns an *AliasConflictError.
|
|
//
|
|
// It uses jsontext.Decoder/Encoder for token-level processing, delegating all
|
|
// JSON lexing (string escaping, unicode, whitespace) to the library.
|
|
type JSONKeyRewriteReader struct {
|
|
reader *bytes.Reader
|
|
initErr error
|
|
|
|
// Map from old (deprecated) key to its AliasRule for fast lookup.
|
|
oldKeyIndex map[string]AliasRule
|
|
// Map from new key to its AliasRule for fast lookup.
|
|
newKeyIndex map[string]AliasRule
|
|
|
|
// Tracks which deprecated keys have been used (old key -> true).
|
|
usedDeprecated map[string]bool
|
|
}
|
|
|
|
// NewJSONKeyRewriteReader creates a new JSONKeyRewriteReader that wraps the
|
|
// given reader and applies the provided alias rules. It reads JSON tokens
|
|
// from src, handles bidirectional key aliasing, detects conflicts, and
|
|
// writes the result to an internal buffer.
|
|
func NewJSONKeyRewriteReader(src io.Reader, rules []AliasRule) *JSONKeyRewriteReader {
|
|
oldIdx := make(map[string]AliasRule, len(rules))
|
|
newIdx := make(map[string]AliasRule, len(rules))
|
|
for _, r := range rules {
|
|
oldIdx[r.OldKey] = r
|
|
newIdx[r.NewKey] = r
|
|
}
|
|
|
|
rw := &JSONKeyRewriteReader{
|
|
oldKeyIndex: oldIdx,
|
|
newKeyIndex: newIdx,
|
|
usedDeprecated: make(map[string]bool),
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := rw.rewrite(src, &buf); err != nil {
|
|
rw.initErr = err
|
|
return rw
|
|
}
|
|
rw.reader = bytes.NewReader(buf.Bytes())
|
|
return rw
|
|
}
|
|
|
|
// UsedDeprecatedKeys returns the list of deprecated key names that were
|
|
// encountered during reading. This should be called after the reader has been
|
|
// fully consumed (i.e., after json.Decoder.Decode or similar has returned),
|
|
// which guarantees the background goroutine has finished.
|
|
func (r *JSONKeyRewriteReader) UsedDeprecatedKeys() []string {
|
|
keys := make([]string, 0, len(r.usedDeprecated))
|
|
for k := range r.usedDeprecated {
|
|
keys = append(keys, k)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// Close closes the reader end of the pipe to unblock the transform goroutine
|
|
// if the consumer stops reading early.
|
|
func (r *JSONKeyRewriteReader) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// Read implements io.Reader by reading from the pipe.
|
|
func (r *JSONKeyRewriteReader) Read(p []byte) (int, error) {
|
|
if r.initErr != nil {
|
|
return 0, r.initErr
|
|
}
|
|
if r.reader == nil {
|
|
return 0, io.EOF
|
|
}
|
|
return r.reader.Read(p)
|
|
}
|
|
|
|
// RewriteDeprecatedKeys handles JSON key aliasing in data using
|
|
// the provided alias rules. It rewrites NewKey→OldKey (so the struct can
|
|
// deserialize), passes through OldKey as-is, and returns an error if both
|
|
// appear in the same scope (alias conflict) or the JSON is malformed.
|
|
//
|
|
// This is useful when a request body is captured as json.RawMessage and later
|
|
// decoded into a struct with `renameto` tags — the rewriter in MakeDecoder
|
|
// won't have seen the inner fields, so this function can be called before the
|
|
// deferred unmarshal.
|
|
func RewriteDeprecatedKeys(data []byte, rules []AliasRule) ([]byte, map[string]string, error) {
|
|
if len(rules) == 0 || len(data) == 0 {
|
|
return data, nil, nil
|
|
}
|
|
oldIdx := make(map[string]AliasRule, len(rules))
|
|
newIdx := make(map[string]AliasRule, len(rules))
|
|
for _, r := range rules {
|
|
oldIdx[r.OldKey] = r
|
|
newIdx[r.NewKey] = r
|
|
}
|
|
rw := &JSONKeyRewriteReader{
|
|
oldKeyIndex: oldIdx,
|
|
newKeyIndex: newIdx,
|
|
usedDeprecated: make(map[string]bool),
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := rw.rewrite(bytes.NewReader(data), &buf); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
deprecatedKeysMap := make(map[string]string, len(rw.usedDeprecated))
|
|
for k := range rw.usedDeprecated {
|
|
deprecatedKeysMap[k] = rw.oldKeyIndex[k].NewKey
|
|
}
|
|
return buf.Bytes(), deprecatedKeysMap, nil
|
|
}
|
|
|
|
// RewriteOldToNewKeys is the reverse of RewriteDeprecatedKey; it takes
|
|
// the rules and reverses them before translating keys.
|
|
// Use this in situations where a payload was rewritten from new to old keys
|
|
// for deserialization, but you want to return a response with the new keys
|
|
// for forward compatibility.
|
|
func RewriteOldToNewKeys(data []byte, rules []AliasRule) ([]byte, error) {
|
|
reversed := make([]AliasRule, len(rules))
|
|
for i, r := range rules {
|
|
reversed[i] = AliasRule{OldKey: r.NewKey, NewKey: r.OldKey}
|
|
}
|
|
result, _, err := RewriteDeprecatedKeys(data, reversed)
|
|
return result, err
|
|
}
|
|
|
|
// rewrite reads tokens from src, rewrites deprecated keys, checks for alias
|
|
// conflicts, and writes the transformed JSON to w.
|
|
func (r *JSONKeyRewriteReader) rewrite(src io.Reader, w io.Writer) error {
|
|
dec := jsontext.NewDecoder(src, jsontext.AllowDuplicateNames(true))
|
|
enc := jsontext.NewEncoder(w, jsontext.AllowDuplicateNames(true))
|
|
|
|
// Stack of per-object-scope key sets for conflict detection.
|
|
// Pushed on '{', popped on '}'.
|
|
var keyScopes []map[string]bool
|
|
|
|
for {
|
|
tok, err := dec.ReadToken()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
kind := tok.Kind()
|
|
|
|
switch kind {
|
|
case '{':
|
|
keyScopes = append(keyScopes, make(map[string]bool))
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return err
|
|
}
|
|
|
|
case '}':
|
|
if len(keyScopes) > 0 {
|
|
keyScopes = keyScopes[:len(keyScopes)-1]
|
|
}
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return err
|
|
}
|
|
|
|
case '"':
|
|
// Determine if this string is an object key by checking the
|
|
// decoder's stack: inside an object ('{') at an odd length
|
|
// means we just read a key (name).
|
|
isKey := false
|
|
depth := dec.StackDepth()
|
|
if depth > 0 {
|
|
parentKind, length := dec.StackIndex(depth)
|
|
// length is odd after reading a name (names and values
|
|
// are counted separately).
|
|
if parentKind == '{' && length%2 == 1 {
|
|
isKey = true
|
|
}
|
|
}
|
|
|
|
if isKey {
|
|
keyName := tok.String()
|
|
|
|
// Use OldKey as the canonical key for scope tracking.
|
|
// Both OldKey (pass-through) and NewKey (rewrite) resolve
|
|
// to the same canonical key for conflict detection.
|
|
|
|
if rule, ok := r.oldKeyIndex[keyName]; ok {
|
|
// This is an OldKey (deprecated name). Pass through
|
|
// as-is — the struct expects this name. Track it for
|
|
// deprecation logging.
|
|
canonicalKey := rule.OldKey
|
|
r.usedDeprecated[keyName] = true
|
|
|
|
// Conflict detection.
|
|
if len(keyScopes) > 0 {
|
|
scope := keyScopes[len(keyScopes)-1]
|
|
if scope[canonicalKey] {
|
|
return &AliasConflictError{Old: rule.OldKey, New: rule.NewKey}
|
|
}
|
|
scope[canonicalKey] = true
|
|
}
|
|
|
|
// Write the key as-is (old name, which the struct expects).
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return err
|
|
}
|
|
} else if rule, ok := r.newKeyIndex[keyName]; ok {
|
|
// This is a NewKey. Rewrite it to OldKey so the
|
|
// struct can deserialize it.
|
|
canonicalKey := rule.OldKey
|
|
|
|
// Conflict detection.
|
|
if len(keyScopes) > 0 {
|
|
scope := keyScopes[len(keyScopes)-1]
|
|
if scope[canonicalKey] {
|
|
return &AliasConflictError{Old: rule.OldKey, New: rule.NewKey}
|
|
}
|
|
scope[canonicalKey] = true
|
|
}
|
|
|
|
// Write the rewritten (old) key.
|
|
if err := enc.WriteToken(jsontext.String(canonicalKey)); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Not an aliased key — pass through unchanged.
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
// String value — pass through unchanged.
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
default:
|
|
// All other tokens: [, ], numbers, bools, null — pass through.
|
|
if err := enc.WriteToken(tok); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|