Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions tools/gitops-auto-complete/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# gitops-auto-complete

Generates a JSON schema from Fleet's GitOps Go structs so
[yaml-language-server](https://github.com/redhat-developer/yaml-language-server) can
offer completion, hover docs, and validation while you write GitOps YAML.

## How to use

### Build the schema

The tool is a separate Go module, so run it from its own directory:

```bash
cd tools/gitops-auto-complete
go run . generated-schema.json
```

The argument is the output file, or omit it to print to stdout. Re-run it whenever
the relevant Fleet structs change.

### Set up with yaml-language-server

Point yaml-language-server at the generated file. It's used by Neovim, the VS Code
YAML extension, and others. There are two ways to do this:

- Map it to your GitOps files with the `yaml.schemas` setting, which maps a schema
path to file globs.
- Or add a modeline to the top of a single file:

```yaml
# yaml-language-server: $schema=/absolute/path/to/generated-schema.json
```

### Neovim and lazy.nvim example

```lua
{
"neovim/nvim-lspconfig",
dependencies = {
{ "mason-org/mason.nvim", opts = {} },
"mason-org/mason-lspconfig.nvim",
},
config = function()
vim.lsp.config("yamlls", {
settings = {
yaml = {
schemas = {
-- schema file -> which YAML files it applies to
["/absolute/path/to/generated-schema.json"] = {
"**/default.yml",
"**/teams/*.yml",
"**/fleets/*.yml",
},
},
},
},
})
vim.lsp.enable("yamlls")
end,
}
```

Install the server once (`:MasonInstall yaml-language-server`), reload, and open a
GitOps file. Hover a key with `K` to see its type and docs.

## How it works

Reflects a `GitOpsSpec` struct that mirrors the real top-level GitOps keys, such as
`org_settings`, `controls`, `software`, and `policies`, reusing Fleet's own types for
each section, via [`invopop/jsonschema`](https://github.com/invopop/jsonschema). It
then post-processes the result so the schema matches how GitOps files are written:
file-path references, legacy key aliases, required fields for an item, and field docs
pulled from Go comments.

It's a separate Go module with a `replace` back to the repo, so it builds from inside
the repo without adding dependencies to the root `go.mod`.

## Known limitations

- The schema is filename-agnostic, but Fleet applies some keys differently by file.
For example, `agent_options` and `reports` are rejected in `no-team.yml` and the
unassigned file. The schema still accepts them there, so that mistake shows up at
`fleetctl` apply time, not in the editor.
- `GitOpsSpec` and `ControlsWithTypes` are hand-written mirrors of `spec.GitOps` and
`spec.GitOpsControls`, because those spec structs are untyped or untagged and reflect
poorly. `TestControlsKeysCoverSpec` catches a controls-key drift, but a new top-level
key has to be added to `GitOpsSpec` by hand, as `custom_host_vitals` was.
120 changes: 120 additions & 0 deletions tools/gitops-auto-complete/extra_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package main

// Data tables that Fleet's Go structs don't express but GitOps YAML relies on:
// declarative apply notes, path-reference support, and installer-reference keys.

// gitops sends a fully materialized config, so omitting a key normally resets it.
// These hover notes cover the keys that instead keep their value.
const (
declarativeKeepAlways = "GitOps: kept unchanged when omitted, null, or empty."
declarativeKeepUnlessEmpty = "GitOps: kept unchanged when omitted or null; cleared when set to an empty value."
declarativeKeepOnOmit = "GitOps: kept unchanged when omitted; cleared when set to null or empty."
)

// declarativeExceptions maps a gitops key (dotted path) to its hover note. Only the
// exceptions to the reset-on-omit default are listed, verified against a live apply.
var declarativeExceptions = map[string]string{
// Google service-account credentials, preserved so a re-apply need not resend
// the secret. UI GitOps mode is merged onto the existing config.
"org_settings.integrations.google_calendar.api_key_json": declarativeKeepAlways,
"org_settings.integrations.google_workspace.api_key_json": declarativeKeepAlways,
"org_settings.gitops": declarativeKeepAlways,

// host_expiry is the documented exception that isn't reset when omitted, but
// an explicit empty object still resets it. Applies at team and org level.
"settings.host_expiry_settings": declarativeKeepUnlessEmpty,
"org_settings.host_expiry_settings": declarativeKeepUnlessEmpty,

// Label host membership: omitting keeps the current members, an explicit empty
// list clears them. From the docs and label parsing.
"labels.hosts": declarativeKeepOnOmit,
}

// pathReferenceDefinitions are the $defs that accept a `path` (one external file) in
// place of inline content. pathsReferenceDefinitions additionally accept `paths` (a
// single glob string). Defs whose Go type embeds fleet.BaseItem (reports, scripts,
// configuration_profiles) already get both from reflection, so they aren't listed.
var pathReferenceDefinitions = []string{
"GitOpsOrgSettings", "GitOpsFleetSettings", "AgentOptions", "ControlsWithTypes",
"SoftwarePackageSpec",
}

var pathsReferenceDefinitions = []string{
"GitOpsPolicySpec", "LabelSpec",
}

// requiredKeyRule gates a $def: an item is valid if it has all the keys of any one of
// its validKeyCombinations. So {{"a"},{"b"}} means "a or b" and {{"a","b"}} means
// "a and b". Fleet enforces these at gitops apply time in validation code rather than
// struct tags. Where an item can also be a file reference, path/paths are listed as
// their own combinations.
type requiredKeyRule struct {
definition string
message string
validKeyCombinations [][]string
}

var requiredKeys = []requiredKeyRule{
{
definition: "SoftwarePackageSpec",
message: "A package must set one of: url, hash_sha256, or path.",
validKeyCombinations: [][]string{
{"url"},
{"hash_sha256"},
{"path"},
},
},
{
definition: "TeamSpecAppStoreApp",
message: "An app_store_apps entry must set app_store_id.",
validKeyCombinations: [][]string{{"app_store_id"}},
},
{
definition: "MaintainedAppSpec",
message: "A fleet_maintained_apps entry must set slug.",
validKeyCombinations: [][]string{{"slug"}},
},
{
definition: "LabelSpec",
message: "A label must set name (or reference a file with path/paths).",
validKeyCombinations: [][]string{
{"name"},
{"path"},
{"paths"},
},
},
{
definition: "GitOpsPolicySpec",
message: "A policy must set name (or reference a file with path/paths).",
validKeyCombinations: [][]string{
{"name"},
{"path"},
{"paths"},
},
},
{
definition: "Query",
message: "A report must set name and query (or reference a file with path/paths).",
validKeyCombinations: [][]string{
{"name", "query"},
{"path"},
{"paths"},
},
},
{
definition: "YaraRule",
message: "A yara_rules entry must set path.",
validKeyCombinations: [][]string{{"path"}},
},
}

// strictStringKeys are the keys kept as strict strings, so a wrong-typed value like
// `url: 12345` is caught. `path`/`paths` are excluded so they stay nullable.
var strictStringKeys = map[string][]string{
"SoftwarePackageSpec": {"url", "hash_sha256"},
"TeamSpecAppStoreApp": {"app_store_id"},
"MaintainedAppSpec": {"slug"},
"LabelSpec": {"name"},
"GitOpsPolicySpec": {"name"},
"Query": {"name", "query"},
}
Loading
Loading