<!-- 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 -->
190 lines
9.0 KiB
Markdown
190 lines
9.0 KiB
Markdown
## 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_data` config object with a sub-key per dataset.
|
|
- Both `POST /api/v1/fleet/config` and `PATCH /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
|
|
`uptime` and `vulnerabilities`, but the internal dataset names are `uptime`
|
|
and `cve`. 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 `HistoricalDataSettings` struct to `server/fleet/app.go`:
|
|
```go
|
|
type HistoricalDataSettings struct {
|
|
Uptime bool `json:"uptime"` // dataset "uptime"
|
|
Vulnerabilities bool `json:"vulnerabilities"` // dataset "cve"
|
|
}
|
|
```
|
|
Both fields **without** `omitempty` so an explicit `false` round-trips
|
|
through GitOps overwrite mode (matches `EnableHostUsers` /
|
|
`EnableSoftwareInventory`).
|
|
- Add `HistoricalData HistoricalDataSettings` to `Features` with JSON tag
|
|
`historical_data` (no `omitempty`).
|
|
- Update the existing "WARNING: account in the Features Clone implementation"
|
|
comment block. `HistoricalDataSettings` is value-type-only so
|
|
`clone := *f` already deep-copies it; verify in unit test.
|
|
- Because `Team.Config` embeds `Features`, `historical_data` is automatically
|
|
available per-fleet with no schema change.
|
|
|
|
### Mapping helper
|
|
|
|
- Add a method on `HistoricalDataSettings`:
|
|
```go
|
|
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)
|
|
}
|
|
}
|
|
```
|
|
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.
|
|
|
|
### Defaults
|
|
|
|
- `Features.ApplyDefaults()` sets both sub-fields `true`.
|
|
`ApplyDefaultsForNewInstalls()` already delegates to `ApplyDefaults()`; no
|
|
separate change needed.
|
|
- The existing pre-unmarshal `ApplyDefaults` priming on both read paths
|
|
(`server/datastore/mysql/app_configs.go` and
|
|
`server/datastore/mysql/teams.go`) means existing rows whose stored
|
|
JSON omits `historical_data` read back with both sub-fields `true`.
|
|
- A small backfill is added to migration `20260423161823_AddHostSCDData`
|
|
to set `features.historical_data.{uptime,vulnerabilities}` to `true` on
|
|
every `app_config_json` row and every `teams.config` row. This is
|
|
required because earlier migrations using `updateAppConfigJSON` (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 to `false`. The backfill runs in the same migration
|
|
that creates `host_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 because `ModifyTeam` takes a parsed `TeamPayload` rather
|
|
than raw bytes. Add a `TeamPayloadFeatures` payload-only subset of
|
|
`Features` containing just `HistoricalData *HistoricalDataPayload`,
|
|
and a `HistoricalDataPayload` whose sub-fields are `optjson.Bool`
|
|
(matching the `optjson.Bool`-based partial-PATCH pattern already used
|
|
by `mdm.enable_disk_encryption` on this endpoint). Sub-keys whose
|
|
`Valid` is `false` are left untouched; sub-keys with explicit values
|
|
flip the stored fleet config. Other `features` sub-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 `Features` unmarshal that already handles other Features fields.
|
|
`ApplySpecOptions.Overwrite=true` is the same risk profile as every
|
|
other `Features` field today; documented in the GitOps YAML reference.
|
|
|
|
### Activities
|
|
|
|
- Add two activity types:
|
|
```go
|
|
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"`
|
|
}
|
|
```
|
|
Activity-type strings: `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_name` `nil`; per-fleet emits with both populated.
|
|
- The `dataset` payload uses the **config key** (`vulnerabilities`, not
|
|
`cve`) 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
|
|
a `features.historical_data` shape 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 for
|
|
`features.historical_data`.
|
|
|
|
## Impact
|
|
|
|
- **One backfill migration.** `Features` is stored as a JSON blob in
|
|
`app_config_json` and `teams.config`. Migration `20260423161823` (which
|
|
also creates `host_scd_data`) writes
|
|
`features.historical_data.{uptime,vulnerabilities} = true` on every
|
|
existing row. The `ApplyDefaults`-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. `EnableStrictDecoding` rejects unknown fields;
|
|
`historical_data` is 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 the
|
|
`Enabled(dataset)` helper into `Service.CollectDatasets` and adds a
|
|
per-fleet filter on `FindRecentlySeenHostIDs`. 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_data` rows, 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/true` defaults; the `global AND fleet` rule is the only precedence
|
|
mechanic. Cron-side enforcement is the consumer's concern.
|
|
- **`DeviceFeatures` surfacing.** The device-endpoint subset of app config
|
|
does not expose `historical_data`. This setting controls server-side
|
|
rollup collection; nothing about device behavior changes.
|