### Summary: Generates a json schema for valid GitOps yaml files, to be used with [yaml-language-server](https://github.com/redhat-developer/yaml-language-server) for IDE integration. This PR includes the actual generated file, so it can be used without running the tool. All files are in `/tools/gitops-autto-complete`, so nothing else gets affected. #### What it adds: - Complete json schema that defines valid GitOps yaml files and can be integrated with [yaml-language-server](https://github.com/redhat-developer/yaml-language-server). - Auto-completion, error checking, type checking, descriptions. - Defines all keys for osquery options/flags (based on `server/fleet/agent_options_generated.go`). - Additional validation: required keys, strings that must be enclosed in quotation marks, path support. - Additional data: descriptions from code comments, notices for fields that don't reset if null or empty. #### Limitations: - Some structs and data are duplicated into the tool and will inevitebly mismatch over time, because the structs used for gitops are not sufficient for the schema generation: - Some fields use an interface/any type that so can't be used for the schema generation. - Some important details are not encoded in the type or struct tags for gitops fields at all. - Some details (like required fields) are encoded in the Validate() interface, but the IDE integration cannot run Go code. - Doesn't work with all yaml file types used for gitops (like a yaml file that specifies multiple software packages), only the default/fleet level files. This will require having a subsection of the schema for each type of file, and some way to detect what it actually is (maybe specifying the schema in the file itself). - Requires manual setup to integrate with IDE, it's not an easy to use extension currently. #### Dependencies: - `invopop/jsonschema` reflects Fleet's GitOps structs into the schema. - `santhosh-tekuri/jsonschema/v6` validates the test fixtures against that schema. - `ghodss/yaml` decodes the fixture YAML the way fleetctl does. https://github.com/user-attachments/assets/b6ffacd0-e602-41a2-b46a-7d10300c6ea5 ## Testing - [x] QA'd all new/changed functionality manually - I have been using and working on this for the past week so it's in a pretty good state, but some descriptions or keys are probably still missing. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added GitOps YAML auto-completion powered by a comprehensive JSON Schema. * Added validation for GitOps configuration structure, supported fields, data types, required combinations, and unknown keys. * Added support for external file references using `path` and `paths` in supported sections. * Added clearer guidance for deprecated fields and special field behaviors. * **Bug Fixes** * Improved detection of incorrectly typed values and invalid configuration shapes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
130 lines
5.2 KiB
Go
130 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/spec"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/invopop/jsonschema"
|
|
)
|
|
|
|
// GitOpsSpec spells out the top-level GitOps keys with Fleet's typed structs, since
|
|
// spec.GitOps has no json tags and would reflect to PascalCase keys.
|
|
type GitOpsSpec struct {
|
|
Name string `json:"name,omitempty"`
|
|
OrgSettings *spec.GitOpsOrgSettings `json:"org_settings,omitempty"`
|
|
TeamSettings *spec.GitOpsFleetSettings `json:"settings,omitempty"`
|
|
AgentOptions *fleet.AgentOptions `json:"agent_options,omitempty"`
|
|
Controls ControlsWithTypes `json:"controls"`
|
|
Policies []*spec.GitOpsPolicySpec `json:"policies,omitempty"`
|
|
Reports []*spec.Query `json:"reports,omitempty"`
|
|
Software spec.GitOpsSoftware `json:"software"`
|
|
Labels []*fleet.LabelSpec `json:"labels,omitempty"`
|
|
CustomHostVitals []spec.GitOpsCustomHostVital `json:"custom_host_vitals,omitempty"`
|
|
}
|
|
|
|
// ControlsWithTypes covers `controls:` with real types. spec.GitOpsControls types
|
|
// most keys as `any` so yamlls can't complete them, and leaks an internal Defined field.
|
|
type ControlsWithTypes struct {
|
|
AndroidEnabledAndConfigured bool `json:"android_enabled_and_configured"`
|
|
WindowsEnabledAndConfigured bool `json:"windows_enabled_and_configured"`
|
|
EnableDiskEncryption bool `json:"enable_disk_encryption"`
|
|
EnableRecoveryLockPassword bool `json:"enable_recovery_lock_password"`
|
|
WindowsRequireBitLockerPIN bool `json:"windows_require_bitlocker_pin"`
|
|
|
|
NameTemplate string `json:"name_template"`
|
|
|
|
MacOSUpdates *fleet.AppleOSUpdateSettings `json:"macos_updates"`
|
|
IOSUpdates *fleet.AppleOSUpdateSettings `json:"ios_updates"`
|
|
IPadOSUpdates *fleet.AppleOSUpdateSettings `json:"ipados_updates"`
|
|
WindowsUpdates *fleet.WindowsUpdates `json:"windows_updates"`
|
|
|
|
MacOSSetup *fleet.MacOSSetup `json:"macos_setup" renameto:"setup_experience"`
|
|
AppleAccountProvisioning *fleet.AppleAccountProvisioning `json:"apple_account_provisioning"`
|
|
Scripts []fleet.BaseItem `json:"scripts"`
|
|
|
|
MacOSSettings *fleet.MacOSSettings `json:"macos_settings" renameto:"apple_settings"`
|
|
WindowsSettings *fleet.WindowsSettings `json:"windows_settings"`
|
|
AndroidSettings *fleet.AndroidSettings `json:"android_settings"`
|
|
|
|
// Remaining keys accept any value for now.
|
|
MacOSMigration any `json:"macos_migration"`
|
|
WindowsMigrationEnabled any `json:"windows_migration_enabled"`
|
|
EnableTurnOnWindowsMDMManually any `json:"enable_turn_on_windows_mdm_manually"`
|
|
WindowsEntraTenantIDs any `json:"windows_entra_tenant_ids"`
|
|
WindowsEntraClientIDs any `json:"windows_entra_client_ids"`
|
|
AppleRequireHardwareAttestation any `json:"apple_require_hardware_attestation"`
|
|
}
|
|
|
|
func goTypeToJSON(name string) string {
|
|
switch name {
|
|
case "bool":
|
|
return "boolean"
|
|
case "string":
|
|
return "string"
|
|
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64":
|
|
return "integer"
|
|
case "float32", "float64":
|
|
return "number"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func typeMapper(goType reflect.Type) *jsonschema.Schema {
|
|
packagePath := goType.PkgPath()
|
|
|
|
// json.RawMessage reflects to a bare `true` schema that yamlls won't complete.
|
|
// In GitOps these blobs are objects, so type them as such.
|
|
if packagePath == "encoding/json" && goType.Name() == "RawMessage" {
|
|
return &jsonschema.Schema{Type: "object"}
|
|
}
|
|
|
|
// fleet.Duration embeds time.Duration, so invopop emits a self-referential $def
|
|
// that overflows yamlls' resolver. It marshals to a string like "24h".
|
|
if strings.HasSuffix(packagePath, "server/fleet") && goType.Name() == "Duration" {
|
|
return &jsonschema.Schema{Type: "string"}
|
|
}
|
|
|
|
// optjson.Bool/String/Int/Slice[T]/Any[T] marshal to their Value, not the
|
|
// internal {Set, Valid, Value} struct.
|
|
if strings.Contains(packagePath, "pkg/optjson") {
|
|
valueField, ok := goType.FieldByName("Value")
|
|
if ok {
|
|
return schemaForType(valueField.Type)
|
|
}
|
|
|
|
// BoolOr[T]/StringOr[T] marshal to a scalar or an object, and their generic
|
|
// $def names can't resolve as a $ref, so express both arms as an anyOf.
|
|
scalar := "boolean"
|
|
_, hasString := goType.FieldByName("String")
|
|
if hasString {
|
|
scalar = "string"
|
|
}
|
|
return &jsonschema.Schema{AnyOf: []*jsonschema.Schema{{Type: scalar}, {Type: "object"}}}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func schemaForType(goType reflect.Type) *jsonschema.Schema {
|
|
for goType.Kind() == reflect.Pointer {
|
|
goType = goType.Elem()
|
|
}
|
|
switch goType.Kind() {
|
|
case reflect.Bool:
|
|
return &jsonschema.Schema{Type: "boolean"}
|
|
case reflect.String:
|
|
return &jsonschema.Schema{Type: "string"}
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return &jsonschema.Schema{Type: "integer"}
|
|
case reflect.Float32, reflect.Float64:
|
|
return &jsonschema.Schema{Type: "number"}
|
|
case reflect.Slice, reflect.Array:
|
|
return &jsonschema.Schema{Type: "array", Items: schemaForType(goType.Elem())}
|
|
default:
|
|
return &jsonschema.Schema{Type: "object"}
|
|
}
|
|
}
|