Skip to content

Detect unknown keys in GitOps (phase 1) - #40963

Merged
sgress454 merged 23 commits into
mainfrom
worktree-sgress454/unknown-keys-gitops
Mar 6, 2026
Merged

Detect unknown keys in GitOps (phase 1)#40963
sgress454 merged 23 commits into
mainfrom
worktree-sgress454/unknown-keys-gitops

Conversation

@sgress454

@sgress454 sgress454 commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Related issue: Resolves #40496

Details

This is the first phase of an effort to detect unknown keys in GitOps .yml files. In the regular fleetctl gitops case, it will fail when unknown keys are detected. This behavior can be changed with a new --allow-unknown-keys flag which will log the issues and continue.

In this first phase we are detecting unknown keys in most GitOps sections, other than the top-level org_settings: and settings: sections which have more complicated typing. I will tackle those separately as they require a bit more thought. Also ultimately I'd like us to be doing this validation in a more top-down fashion in one place, rather than spreading it across the code by doing it in each individual section, but this is a good first step.

As a bonus, I invited my pal Mr. Levenshtein to the party so that we can make suggestions when unknown keys are detected, like:

 * unknown key "queyr" in "./lib/some-report.yml"; did you mean "query"?

Note: the goal is to return as many validation errors as possible to the user, so they don't have to keep running fleetctl gitops to get the next error. I did not update any other errors to stop returning early, in an effort to keep this as low-touch as possible.

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 for more information.

Testing

  • Added/updated automated tests
  • QA'd all new/changed functionality manually
    • Tested this against existing it-and-security folder and one with updated keys from Add aliases for macos fields #40959; no unknown keys detected
    • Added unknown keys at various levels, GitOps errored with helpful messages
    • Same as above but with --allow-unknown-keys; GitOps outputted helpful messages but continued.

Summary by CodeRabbit

  • New Features

    • GitOps runs now fail when unknown or misspelled keys are present in configuration files.
    • New CLI flag --allow-unknown-keys lets unknown keys be treated as warnings instead of errors.
    • Unknown-key messages include suggested valid key names to help correct mistakes.
  • Tests

    • Expanded test coverage to validate unknown-key detection and the allow-as-warning option.

Comment thread pkg/spec/gitops.go
@sgress454 sgress454 changed the title Worktree sgress454/unknown keys gitops Detect unknown keys in GitOps (phase 1) Mar 4, 2026
@codecov

codecov Bot commented Mar 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.55224% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.35%. Comparing base (cbc6908) to head (a8d035d).
⚠️ Report is 39 commits behind head on main.

Files with missing lines Patch % Lines
pkg/spec/gitops_validate.go 87.20% 8 Missing and 8 partials ⚠️
pkg/spec/gitops.go 92.18% 3 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #40963      +/-   ##
==========================================
+ Coverage   66.31%   66.35%   +0.04%     
==========================================
  Files        2473     2475       +2     
  Lines      198069   198359     +290     
  Branches     8738     8888     +150     
==========================================
+ Hits       131347   131627     +280     
- Misses      54843    54844       +1     
- Partials    11879    11888       +9     
Flag Coverage Δ
backend 68.14% <89.55%> (+0.04%) ⬆️
backend-activity 87.37% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sgress454
sgress454 marked this pull request as ready for review March 4, 2026 19:58
@sgress454
sgress454 requested a review from a team as a code owner March 4, 2026 19:58

// collectFields recursively extracts JSON field names from a struct type,
// handling embedded structs by inlining their fields.
func collectFields(t reflect.Type, keys map[string]fieldInfo) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this handles a single type (doesn't recurse into non-inline struct fields). The way nested structs are validated is:

  1. validateUnkownKeys is called on the top-level value
  2. That calls validateMapKeys which calls knownJSONKeys on the top-level type, which calls collectFields (if not cached)
  3. validateMapKeys recursively calls itself on child structs, so the fields of each of those child types are collected.

Comment thread pkg/spec/gitops_validate.go Outdated
Comment on lines +110 to +111
// levenshtein computes the edit distance between two strings.
func levenshtein(a, b string) int {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could move this into pkg/str if we ever use it elsewhere

@sgress454

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds detection of unknown/extraneous keys in GitOps YAML parsing. Introduces ParseUnknownKeyError (filename, path, field, suggestion), a GitOpsOptions type with AllowUnknownKeys, and updates GitOpsFromFile to accept options. Implements reflection-based validators in gitops_validate.go (recursive key checks and Levenshtein suggestions). Adds --allow-unknown-keys CLI flag to treat unknown keys as warnings. Extensive tests added for detection, suggestions, and option-driven behavior.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Detect unknown keys in GitOps (phase 1)' clearly summarizes the main change—adding unknown key detection functionality to GitOps with the phase designation indicating it's part of a larger effort.
Linked Issues check ✅ Passed The PR successfully implements the linked issue #40496 requirement to validate and report unrecognized YAML keys, including suggestions via Levenshtein distance, with the --allow-unknown-keys flag for optional warning-only mode.
Out of Scope Changes check ✅ Passed All changes are within scope—introducing unknown key detection in GitOps sections, adding the --allow-unknown-keys flag, and supporting infrastructure (validation helpers, tests). The PR explicitly defers org_settings and settings sections to a follow-up as appropriate.
Description check ✅ Passed The PR description is comprehensive and covers key details including related issue, implementation details, feature description, testing performed, and relevant checklist items completed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch worktree-sgress454/unknown-keys-gitops

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/fleetctl/fleetctl/gitops.go`:
- Around line 228-232: The pre-parse call to extractControlsForNoTeam is
invoking spec.GitOpsFromFile without passing options, so the
--allow-unknown-keys flag (flAllowUnknownKeys) isn't honored; update the code to
construct and pass the same gitOpsOpts (or a shared spec.GitOpsOptions with
AllowUnknownKeys set from flAllowUnknownKeys) into the extractControlsForNoTeam
path and any other pre-parse calls that call spec.GitOpsFromFile so that
GitOpsFromFile receives the options consistently (refer to gitOpsOpts,
flAllowUnknownKeys, extractControlsForNoTeam, and spec.GitOpsFromFile).

In `@pkg/spec/gitops.go`:
- Around line 1571-1574: The unknown-key validation currently only runs against
the top-level softwareRaw; update the loading logic that unmarshals files
referenced by software.packages[].path to also call validateRawKeys on each
package's raw YAML before unmarshaling (use reflect.TypeFor[Package]() and
include the package file path in the filePath context), or alternatively iterate
softwareRaw.packages and validate each package raw map with validateRawKeys so
extraneous keys in package files are caught; ensure you append any returned
errors to multiError just like the top-level validation.
- Around line 931-939: processControlsPathIfNeeded currently reassigns the local
pointer variable controlsFilePath instead of updating the caller's string value,
so downstream code still uses the original path; change the assignment to update
the pointed-to value (e.g. ensure controlsFilePath is non-nil and set
*controlsFilePath = resolveApplyRelativePath(filepath.Dir(*controlsFilePath),
*controlsTop.Path) or assign the result to a local newPath and then set
*controlsFilePath = newPath) so subsequent reads
(os.ReadFile(*controlsFilePath)) and parseControls use the resolved external
controls file path; keep using the existing symbols processControlsPathIfNeeded,
controlsTop.Path, resolveApplyRelativePath and filepath.Dir.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 275a2711-c388-41e9-85bd-7ef2d3959c38

📥 Commits

Reviewing files that changed from the base of the PR and between 83a0df3 and 8ac73ba.

📒 Files selected for processing (6)
  • changes/40496-detect-unknown-fields
  • cmd/fleetctl/fleetctl/gitops.go
  • pkg/spec/gitops.go
  • pkg/spec/gitops_test.go
  • pkg/spec/gitops_validate.go
  • pkg/spec/gitops_validate_test.go

Comment thread cmd/fleetctl/fleetctl/gitops.go Outdated
Comment thread pkg/spec/gitops.go Outdated
Comment thread pkg/spec/gitops.go Outdated
Comment thread pkg/spec/gitops_validate.go Outdated

@iansltx iansltx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still need to review gitops_test, gitops_validate, and gitops_validate_test, but here's a review queue flush to get things started.

Comment thread pkg/spec/gitops.go
Comment thread pkg/spec/gitops.go
Comment thread pkg/spec/gitops.go Outdated
Comment thread pkg/spec/gitops.go

@iansltx iansltx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the remaining files are sizable, planning on flushing the review queue after each file if I have any feedback. 3 files to go.

Comment thread pkg/spec/gitops.go Outdated
}
errPrefix := fmt.Sprintf("failed to parse policy install_software %q: ", policy.Name)
wrapErr := func(err error) error {
return fmt.Errorf("%s: %w", errPrefix, err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we have two :s here.

@iansltx iansltx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gitops_test.go reviewed. Two files to go.

Comment thread pkg/spec/gitops_test.go
Comment thread pkg/spec/gitops_test.go Outdated
Comment thread pkg/spec/gitops_test.go Outdated

@iansltx iansltx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback from gitops_validate.go. Re-reviewing latest changes on gitops.go and gitops_test.go next.

Comment thread pkg/spec/gitops_validate.go Outdated
Comment thread pkg/spec/gitops_validate.go Outdated
Comment thread pkg/spec/gitops_validate.go Outdated

var errs []error
for i, elem := range data {
elemPath := append(append([]string(nil), path...), fmt.Sprintf("[%d]", i))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same slices.Clone advice as above.

Comment thread pkg/spec/gitops_validate.go Outdated
func validateYAMLKeys(yamlBytes []byte, targetType reflect.Type, filePath string, keysPath []string) []error {
var data any
if err := YamlUnmarshal(yamlBytes, &data); err != nil {
return nil // parse errors already caught by the struct unmarshal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not following what's going on here? Is there a test to explain why this works?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was that anything that calls this has already unmarshalled into a struct, and if there were parser errors there we'd have already caught them, so catching them here would mean a double log. In practice we always bail on parser errors rather than continuing, so it's unlikely we'd reach this spot. See

fleet/pkg/spec/gitops.go

Lines 1184 to 1190 in 19f9009

var pathLabels []*Label
if err := YamlUnmarshal(fileBytes, &pathLabels); err != nil {
multiError = multierror.Append(multiError, MaybeParseTypeError(*item.Path, []string{"labels"}, err))
continue
}
// Validate unknown keys in path-referenced labels file.
multiError = multierror.Append(multiError, validateYAMLKeys(fileBytes, reflect.TypeFor[[]Label](), *item.Path, []string{"labels"})...)
for example.

Putting it that way, I guess it's better to return the error here and get someone bugging us about the double log than it is to risk someone calling this before parsing to a struct and thinking that bad YML worked? I can fix.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any automated test paths that would catch the double log?

Thinking that we can add the extra error'ing here and see how this feels during QA. Either we've covered all cases upstream (which means no double-logs?) or the double-logging is some level of annoying and we back it back out. Either way it'll be a quick tweak during QA.

(and with the above I'm implying we need to call something out in the test plan for this so we try to hit this particular edge case)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same page, I updated to return errors (and updated the tests that checked it didn't). I check all upstream cases and they all bail out early, so we should never see the error, and if we do then it's an indication of something we should probably fix upstream.

sgress454 and others added 2 commits March 6, 2026 12:19
Co-authored-by: Ian Littman <iansltx@gmail.com>

@iansltx iansltx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to go; apologies for the delay. Did some additional fiddling with tests locally to make sure things behaved the way I expected, and they did.

@sgress454
sgress454 merged commit d5eee80 into main Mar 6, 2026
76 of 78 checks passed
@sgress454
sgress454 deleted the worktree-sgress454/unknown-keys-gitops branch March 6, 2026 22:16
@coderabbitai coderabbitai Bot mentioned this pull request Mar 24, 2026
37 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

YAML validation: Extraneous keys

2 participants