<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** For #44077 # Details * Adds `historical_data` key to app and team config (and gitops) with `uptime` and `vulnerabilities` subkeys. Keys default to `true`, meaning "collect this data" * Adds `enabled_historical_dataset` and `disabled_historical_dataset` activities when these values are flipped via GitOps or the config APIs The majority of the file changes in here are GitOps test files that need to be updated to have the new config in them. **This PR does _not_ implement using these configs to actually disable data collection or purge data; that will come in a follow-up PR (as well as the front-end)** # 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, unreleased ## Testing - [X] Added/updated automated tests - [ ] QA'd all new/changed functionality manually #### Defaults - [X] Fresh install: `GET /api/v1/fleet/config` returns `features.historical_data.uptime: true` and `features.historical_data.vulnerabilities: true` - [X] Created a new fleet via `POST /api/v1/fleet/teams`, then `GET /api/v1/fleet/fleets/{id}` returns `features.historical_data.uptime: true` and `features.historical_data.vulnerabilities: true` #### Global PATCH (`POST /api/v1/fleet/config`) - [X] PATCHed `{"features": {"historical_data": {"vulnerabilities": false}}}` — `vulnerabilities` flipped to `false`, `uptime` unchanged at `true` - [X] PATCHed `{"features": {"historical_data": {"uptime": false, "vulnerabilities": true}}}` — both values applied as sent - [X] PATCHed `{"features": {"historical_data": {"vulnerabilites": false}}}` (typo in sub-key) — request rejected with 4xx, stored config unchanged #### Fleet PATCH (`PATCH /api/v1/fleet/fleets/{id}`) - [X] PATCHed a fleet with `{"features": {"historical_data": {"uptime": false}}}` — fleet's `uptime` flipped to `false`, `vulnerabilities` unchanged - [X] Subsequent `GET /api/v1/fleet/fleets/{id}` returns the toggled values under `features.historical_data` (storage shape is symmetric with global) - [X] PATCHed a fleet with `{"features": {"enable_host_users": false}}` (a non-`historical_data` features sub-field) — request returned 200 but the fleet's `enable_host_users` is unchanged (silently ignored, per existing endpoint convention) #### GitOps — global (`fleetctl gitops -f global.yml`) - [X] Applied a YAML with `features.historical_data: {uptime: true, vulnerabilities: false}` — `vulnerabilities` is `false` after apply, `uptime` is `true` - [X] Applied a YAML whose `org_settings` omits `features` entirely — both sub-keys are `true` after apply (defaults injected even if previously disabled) - [X] Applied a YAML where `historical_data` only contains `uptime: false` — `uptime: false` is honored, `vulnerabilities` defaults to `true` - [X] Disabled `vulnerabilities` via the API, then ran `fleetctl gitops` with a YAML that doesn't pin it — `vulnerabilities` flips back to `true` (this is intentional; gitops is the source of truth) #### GitOps — fleet - [X] Applied a fleet YAML with `features.historical_data: {uptime: false}` — that fleet has `uptime: false`, `vulnerabilities: true` after apply - [X] Applied a fleet YAML whose `team_settings.features` omits `historical_data` — both sub-keys are `true` after apply - [X] Applied a fleet YAML that omits `features` entirely — both sub-keys are `true` after apply #### `fleetctl apply` (legacy, partial-merge) - [ ] Disabled `vulnerabilities` via the API, then ran `fleetctl apply` with a YAML that doesn't mention `historical_data` — `vulnerabilities` is still `false` (apply leaves omitted fields alone) #### Activities — global - [X] After PATCHing global to disable `vulnerabilities`, the latest activity is `disabled_historical_dataset` with payload `{"dataset": "vulnerabilities", "fleet_id": null, "fleet_name": null}` - [X] After PATCHing global with both sub-keys flipping in one request, two activities are emitted (one per sub-key) - [X] After PATCHing global with the same values that are already stored, zero new activities are emitted - [X] After re-enabling a previously disabled dataset, the activity type is `enabled_historical_dataset` #### Activities — per fleet - [X] After PATCHing fleet `workstations` to disable `uptime`, the activity is `disabled_historical_dataset` with payload `{"dataset": "uptime", "fleet_id": <workstations id>, "fleet_name": "workstations"}` - [X] Toggling the same dataset on two different fleets produces two distinct activities, one per fleet - [X] After a fleet PATCH with the same values already stored, zero new activities are emitted For unreleased bug fixes in a release candidate, one of: - [X] Confirmed that the fix is not expected to adversely impact load test results - [ ] Alerted the release DRI if additional load testing is needed ## New Fleet configuration settings - [ ] Setting(s) is/are explicitly excluded from GitOps If you didn't check the box above, follow this checklist for GitOps-enabled settings: - [X] Verified that the setting is exported via `fleetctl generate-gitops` - [X] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - https://github.com/fleetdm/fleet/pull/44703 - [X] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [X] Verified that any relevant UI is disabled when GitOps mode is enabled <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Historical-data controls: per-org and per-team toggles for uptime and vulnerability time‑series, with defaults applied when keys are omitted and enable/disable activities emitted on changes. * **Bug Fixes** * Partial updates and PATCH/GitOps flows preserve unspecified historical-data sub-keys instead of clearing them. * **Tests** * Expanded unit and integration tests covering defaults, partial PATCH/GitOps behavior, idempotency, and activity emission. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
9.0 KiB
9.0 KiB
Why
Issue #44077 ("Allow disabling data collection for charts") adds admin-visible switches to disable per-dataset historical-data collection for the dashboard charts. The frontend, cron gating, and data-drop work are tracked separately; this change delivers the GitOps / API surface those consumers depend on:
- A new
features.historical_dataconfig object with a sub-key per dataset. - Both
POST /api/v1/fleet/configandPATCH /api/v1/fleet/teams/{id}accept the new keys with standard PATCH-merge semantics. - GitOps (global YAML and fleet YAML) accepts the new keys.
- A pair of audit activities fire when a dataset is enabled or disabled.
- A small mapping helper makes the "config key vs internal dataset name"
translation explicit and centralized — the issue mandates the config keys
uptimeandvulnerabilities, but the internal dataset names areuptimeandcve. The mismatch needs a single, greppable mapping point so callers don't hardcode the translation.
Default for both sub-keys is true. Existing deployments upgrade with both
datasets enabled, preserving current behavior.
What Changes
Data shape
- Add
HistoricalDataSettingsstruct toserver/fleet/app.go:Both fields withouttype HistoricalDataSettings struct { Uptime bool `json:"uptime"` // dataset "uptime" Vulnerabilities bool `json:"vulnerabilities"` // dataset "cve" }omitemptyso an explicitfalseround-trips through GitOps overwrite mode (matchesEnableHostUsers/EnableSoftwareInventory). - Add
HistoricalData HistoricalDataSettingstoFeatureswith JSON taghistorical_data(noomitempty). - Update the existing "WARNING: account in the Features Clone implementation"
comment block.
HistoricalDataSettingsis value-type-only soclone := *falready deep-copies it; verify in unit test. - Because
Team.ConfigembedsFeatures,historical_datais automatically available per-fleet with no schema change.
Mapping helper
- Add a method on
HistoricalDataSettings:The cron consumer (separate change) will use this. The mapping lives once, on the type that owns the data, with a safelist switch that prevents string interpolation into JSON paths and provides a clear error for unknown datasets.func (h HistoricalDataSettings) Enabled(dataset string) (bool, error) { switch dataset { case "uptime": return h.Uptime, nil case "cve": return h.Vulnerabilities, nil default: return false, fmt.Errorf("unknown dataset %q", dataset) } }
Defaults
Features.ApplyDefaults()sets both sub-fieldstrue.ApplyDefaultsForNewInstalls()already delegates toApplyDefaults(); no separate change needed.- The existing pre-unmarshal
ApplyDefaultspriming on both read paths (server/datastore/mysql/app_configs.goandserver/datastore/mysql/teams.go) means existing rows whose stored JSON omitshistorical_dataread back with both sub-fieldstrue. - A small backfill is added to migration
20260423161823_AddHostSCDDatato setfeatures.historical_data.{uptime,vulnerabilities}totrueon everyapp_config_jsonrow and everyteams.configrow. This is required because earlier migrations usingupdateAppConfigJSON(and the inline TeamConfig round-trip pattern) re-marshal the whole struct on save, and would otherwise stamp the new field's zero value (false) into stored JSON the moment it appeared in Go — silently degrading the upgrade default tofalse. The backfill runs in the same migration that createshost_scd_data(the chart data table) so it's conceptually grouped with the rest of the chart-disabling work, and this code lands in 4.85.0 before any deployment exposes the toggle to admins.
API surface
POST /api/v1/fleet/config(ModifyAppConfig) accepts the new keys. PATCH-merge works for free: the endpoint unmarshals raw JSON into the existing config, and Go's JSON decoder recurses into nested structs, only touching fields present in the payload.PATCH /api/v1/fleet/fleets/{id}(ModifyTeam) accepts the same{features: {historical_data: {...}}}shape, but the wiring is different becauseModifyTeamtakes a parsedTeamPayloadrather than raw bytes. Add aTeamPayloadFeaturespayload-only subset ofFeaturescontaining justHistoricalData *HistoricalDataPayload, and aHistoricalDataPayloadwhose sub-fields areoptjson.Bool(matching theoptjson.Bool-based partial-PATCH pattern already used bymdm.enable_disk_encryptionon this endpoint). Sub-keys whoseValidisfalseare left untouched; sub-keys with explicit values flip the stored fleet config. Otherfeaturessub-fields (enable_host_users,enable_software_inventory,additional_queries,detail_query_overrides) remain writable per-fleet only via/spec/fleets. Storage and read-back shapes are identical to global (features.historical_data); only the request decoding is structurally different.- GitOps (global config YAML and fleet YAML) accepts the keys via the
same
Featuresunmarshal that already handles other Features fields.ApplySpecOptions.Overwrite=trueis the same risk profile as every otherFeaturesfield today; documented in the GitOps YAML reference.
Activities
- Add two activity types:
Activity-type strings:
type ActivityTypeEnabledHistoricalDataset struct { Dataset string `json:"dataset"` // config key: "uptime" or "vulnerabilities" FleetID *uint `json:"fleet_id"` // nil for global FleetName *string `json:"fleet_name"` // nil for global } type ActivityTypeDisabledHistoricalDataset struct { Dataset string `json:"dataset"` FleetID *uint `json:"fleet_id"` FleetName *string `json:"fleet_name"` }enabled_historical_dataset/disabled_historical_dataset. - One activity per sub-field that flipped per request. No-op PATCH (same
values back) emits zero activities. Global emits with
fleet_id/fleet_namenil; per-fleet emits with both populated. - The
datasetpayload uses the config key (vulnerabilities, notcve) since the audit log is admin-facing and admins see config keys in YAML and (eventually) the UI.
Docs
docs/REST API/rest-api.md— global config and fleet-modify sections gain afeatures.historical_datashape and example payload.docs/Configuration/yaml-files.md— global and fleet sections document the keys, defaults, and the GitOps overwrite-on-omit behavior.docs/Contributing/reference/audit-logs.md— entries for both new activity types with payload field documentation and global + fleet-scoped examples.
Capabilities
Added Capabilities
chart-historical-data-settings— describes the config shape, defaults, PATCH-merge semantics, GitOps overwrite behavior, dataset-name ↔ config-key mapping, fleet-scoped settings inheritance, and audit activity emission forfeatures.historical_data.
Impact
- One backfill migration.
Featuresis stored as a JSON blob inapp_config_jsonandteams.config. Migration20260423161823(which also createshost_scd_data) writesfeatures.historical_data.{uptime,vulnerabilities} = trueon every existing row. TheApplyDefaults-before-unmarshal pattern already in the storage read paths covers fresh installs and any row whose stored JSON simply omits the key. - No API version bump. New keys in an existing JSON body are
backwards-compatible.
EnableStrictDecodingrejects unknown fields;historical_datais known after this change. - Consumer changes deferred. The cron gating that uses the helper, the data-drop on disable, the dashboard "data collection disabled" empty state, and the Advanced / Fleet Settings UI checkboxes are all separate changes. This change ships the API contract those consumers depend on.
Out of Scope
- Cron gating in
server/chart/. A separate change wires theEnabled(dataset)helper intoService.CollectDatasetsand adds a per-fleet filter onFindRecentlySeenHostIDs. This change ships the helper but does not call it. - Data drop on disable. The issue requires that disabling a dataset
globally truncates its
host_scd_datarows, and disabling per-fleet scrubs that fleet's contributions. This is a separate change. - Frontend UI. "Disable hosts active" / "Disable vulnerabilities" checkboxes on Advanced + Fleet Settings, the confirmation dialog, and the dashboard empty-state messaging are all separate changes.
- Per-dataset defaults other than
true. A future expensive/opt-in dataset would need its own design conversation. - Tri-state "inherit from global" on the fleet value. Plain bool with
true/truedefaults; theglobal AND fleetrule is the only precedence mechanic. Cron-side enforcement is the consumer's concern. DeviceFeaturessurfacing. The device-endpoint subset of app config does not exposehistorical_data. This setting controls server-side rollup collection; nothing about device behavior changes.