Allow enabling/disabling software inventory per-fleet via the API (#50481)
Resolves #45735. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [X] Added/updated automated tests - [x] QA'd all new/changed functionality manually. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Team settings can now enable or disable Software Inventory through API updates. * Partial updates preserve existing settings when the Software Inventory option is omitted. * Software Inventory configuration can be re-enabled after being disabled. * **Bug Fixes** * Invalid or null Software Inventory values are handled correctly without affecting global or Unassigned settings. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Added support for enabling/disabling software inventory per-fleet via `PATCH /api/v1/fleet/fleets/{id}` with `{"features": {"enable_software_inventory": <bool>}}`. The key follows PATCH-merge semantics: when omitted, the stored value is unchanged.
|
||||
@@ -503,6 +503,10 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
|
||||
}
|
||||
}
|
||||
|
||||
if payload.Features != nil && payload.Features.EnableSoftwareInventory.Valid {
|
||||
team.Config.Features.EnableSoftwareInventory = payload.Features.EnableSoftwareInventory.Value
|
||||
}
|
||||
|
||||
team, err = svc.ds.SaveTeam(ctx, team)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -65,11 +65,13 @@ type TeamPayload struct {
|
||||
// `features` shape so admins can use the same JSON path on both endpoints.
|
||||
//
|
||||
// Only the sub-fields defined here take effect; the broader Features
|
||||
// fields (enable_host_users, enable_software_inventory, additional_queries,
|
||||
// detail_query_overrides) remain settable per-fleet only via the
|
||||
// `/spec/fleets` GitOps path.
|
||||
// fields (enable_host_users, additional_queries, detail_query_overrides)
|
||||
// remain settable per-fleet only via the `/spec/fleets` GitOps path.
|
||||
type TeamPayloadFeatures struct {
|
||||
HistoricalData *HistoricalDataPayload `json:"historical_data"`
|
||||
// EnableSoftwareInventory uses optjson.Bool so a key omitted from a
|
||||
// PATCH body retains its current stored value (PATCH-merge semantics).
|
||||
EnableSoftwareInventory optjson.Bool `json:"enable_software_inventory"`
|
||||
HistoricalData *HistoricalDataPayload `json:"historical_data"`
|
||||
}
|
||||
|
||||
// HistoricalDataPayload is the per-sub-key partial-PATCH form of
|
||||
|
||||
@@ -1831,6 +1831,80 @@ func (s *integrationEnterpriseTestSuite) TestModifyTeamHistoricalData() {
|
||||
), "no disable historical data activity for ignored patch")
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestModifyTeamEnableSoftwareInventory() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a fleet — features.enable_software_inventory snapshots the global
|
||||
// config. `features` in the POST payload is ignored (pre-existing
|
||||
// behavior, consistent with historical_data), so send the OPPOSITE of the
|
||||
// global value to make the ignore observable.
|
||||
globalCfg, err := s.ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
globalEnabled := globalCfg.Features.EnableSoftwareInventory
|
||||
teamName := t.Name() + "softwareInventoryFleet"
|
||||
var createResp teamResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/teams", json.RawMessage(fmt.Sprintf(
|
||||
`{"name": %q, "features": {"enable_software_inventory": %t}}`, teamName, !globalEnabled,
|
||||
)), http.StatusOK, &createResp)
|
||||
require.Equal(t, globalEnabled, createResp.Team.Config.Features.EnableSoftwareInventory,
|
||||
"features is ignored on POST; new fleet snapshots the global config value")
|
||||
teamID := createResp.Team.ID
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, s.ds.DeleteTeam(ctx, teamID))
|
||||
})
|
||||
|
||||
// PATCH enable_software_inventory=false.
|
||||
var modResp teamResponse
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/fleets/%d", teamID),
|
||||
json.RawMessage(`{"features": {"enable_software_inventory": false}}`),
|
||||
http.StatusOK, &modResp)
|
||||
require.False(t, modResp.Team.Config.Features.EnableSoftwareInventory)
|
||||
|
||||
// PATCH other fields without the key — value retained (PATCH-merge semantics).
|
||||
newName := teamName + "renamed"
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/fleets/%d", teamID),
|
||||
json.RawMessage(fmt.Sprintf(`{"name": %q, "features": {"historical_data": {"uptime": false}}}`, newName)),
|
||||
http.StatusOK, &modResp)
|
||||
require.Equal(t, newName, modResp.Team.Name)
|
||||
require.False(t, modResp.Team.Config.Features.EnableSoftwareInventory,
|
||||
"enable_software_inventory preserved when omitted from PATCH")
|
||||
require.False(t, modResp.Team.Config.Features.HistoricalData.Uptime)
|
||||
|
||||
// Invalid value returns a 4xx error and does not change the setting.
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/fleets/%d", teamID),
|
||||
json.RawMessage(`{"features": {"enable_software_inventory": "yes"}}`),
|
||||
http.StatusBadRequest, &modResp)
|
||||
teamFeatures, err := s.ds.TeamFeatures(ctx, teamID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, teamFeatures.EnableSoftwareInventory,
|
||||
"setting unchanged after invalid value")
|
||||
|
||||
// `null` is treated as omitted — value retained.
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/fleets/%d", teamID),
|
||||
json.RawMessage(`{"features": {"enable_software_inventory": null}}`),
|
||||
http.StatusOK, &modResp)
|
||||
require.False(t, modResp.Team.Config.Features.EnableSoftwareInventory)
|
||||
|
||||
// Re-enable.
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/fleets/%d", teamID),
|
||||
json.RawMessage(`{"features": {"enable_software_inventory": true}}`),
|
||||
http.StatusOK, &modResp)
|
||||
require.True(t, modResp.Team.Config.Features.EnableSoftwareInventory)
|
||||
|
||||
// PATCH /fleets/0 (Unassigned) ignores `features` (pre-existing behavior,
|
||||
// consistent with historical_data); Unassigned hosts follow the global
|
||||
// config setting, so send the OPPOSITE of the global value and verify it
|
||||
// is unchanged.
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/fleets/0",
|
||||
json.RawMessage(fmt.Sprintf(`{"features": {"enable_software_inventory": %t}}`, !globalEnabled)),
|
||||
http.StatusOK, &modResp)
|
||||
appCfg, err := s.ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, globalEnabled, appCfg.Features.EnableSoftwareInventory,
|
||||
"global setting unchanged by Unassigned fleet PATCH")
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestAvailableTeams() {
|
||||
t := s.T()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user