mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Add "exceptions" GitOps config (#42013)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #42008 # Details Step one in https://github.com/fleetdm/fleet/issues/40171. This PR adds a new `exceptions` subsection to the current GitOps config, with boolean keys for software, secrets and labels. For existing instances a migration is included to set labels and secrets to `true`. For new instances, only `secrets` will be `true`. # 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, will put changelog in when more functionality is implemented. ## Testing - [X] Added/updated automated tests - [X] QA'd all new/changed functionality manually (https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [X] ran migration and verified that app config had `gitops.exceptions` with `software: false, secrets: true, labels: true` - [X] created a new instance and verified that that app config had `gitops.exceptions` with `software: false, secrets: true, labels: false` - [X] verified that the PATCH /config API works and can update exceptions independently of other config ## Database migrations - [X] Checked schema for all modified table for columns that will auto-update timestamps during migration. n/a - [X] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. n/a - [X] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). n/a ## New Fleet configuration settings - [X] Setting(s) is/are explicitly excluded from GitOps these will not be set in GitOps, since they're _about_ how GitOps works. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * GitOps configuration now supports exception settings for granular resource control. Administrators can configure which specific resource types (labels, software, and secrets) are included in or excluded from GitOps mode operations. * **Improvements** * Improved GitOps configuration handling to preserve exception settings during partial updates and system migrations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
7a6a95703f
commit
deec6aa904
19 changed files with 187 additions and 30 deletions
|
|
@ -375,9 +375,14 @@ func TestGitOpsBasicGlobalPremium(t *testing.T) {
|
|||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
// Set a GitOps UI mode to verify that applying GitOps config won't overwrite it.
|
||||
UIGitOpsMode: fleet.UIGitOpsModeConfig{
|
||||
GitOpsConfig: fleet.GitOpsConfig{
|
||||
GitopsModeEnabled: true,
|
||||
RepositoryURL: "https://didsomeonesaygitops.biz",
|
||||
Exceptions: fleet.GitOpsExceptions{
|
||||
Software: false,
|
||||
Secrets: true,
|
||||
Labels: true,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -577,8 +582,11 @@ software:
|
|||
assert.Empty(t, enrolledSecrets)
|
||||
|
||||
// GitOps should not overwrite GitOps UI Mode.
|
||||
assert.Equal(t, savedAppConfig.UIGitOpsMode.GitopsModeEnabled, true)
|
||||
assert.Equal(t, savedAppConfig.UIGitOpsMode.RepositoryURL, "https://didsomeonesaygitops.biz")
|
||||
assert.Equal(t, savedAppConfig.GitOpsConfig.GitopsModeEnabled, true)
|
||||
assert.Equal(t, savedAppConfig.GitOpsConfig.RepositoryURL, "https://didsomeonesaygitops.biz")
|
||||
assert.Equal(t, savedAppConfig.GitOpsConfig.Exceptions.Labels, true)
|
||||
assert.Equal(t, savedAppConfig.GitOpsConfig.Exceptions.Secrets, true)
|
||||
assert.Equal(t, savedAppConfig.GitOpsConfig.Exceptions.Software, false)
|
||||
|
||||
// Check MDM settings
|
||||
require.True(t, savedAppConfig.MDM.EnableDiskEncryption.Value)
|
||||
|
|
|
|||
|
|
@ -194,7 +194,12 @@
|
|||
"scripts": null,
|
||||
"gitops": {
|
||||
"gitops_mode_enabled": false,
|
||||
"repository_url": ""
|
||||
"repository_url": "",
|
||||
"exceptions": {
|
||||
"labels": false,
|
||||
"software": false,
|
||||
"secrets": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -166,7 +166,12 @@
|
|||
"scripts": null,
|
||||
"gitops": {
|
||||
"gitops_mode_enabled": false,
|
||||
"repository_url": ""
|
||||
"repository_url": "",
|
||||
"exceptions": {
|
||||
"labels": false,
|
||||
"software": false,
|
||||
"secrets": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -140,3 +140,7 @@ spec:
|
|||
gitops:
|
||||
gitops_mode_enabled: false
|
||||
repository_url: ""
|
||||
exceptions:
|
||||
labels: false
|
||||
software: false
|
||||
secrets: false
|
||||
|
|
|
|||
|
|
@ -166,3 +166,7 @@ spec:
|
|||
gitops:
|
||||
gitops_mode_enabled: false
|
||||
repository_url: ""
|
||||
exceptions:
|
||||
labels: false
|
||||
software: false
|
||||
secrets: false
|
||||
|
|
|
|||
|
|
@ -257,7 +257,12 @@
|
|||
},
|
||||
"gitops": {
|
||||
"gitops_mode_enabled": false,
|
||||
"repository_url": ""
|
||||
"repository_url": "",
|
||||
"exceptions": {
|
||||
"labels": false,
|
||||
"software": false,
|
||||
"secrets": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -219,3 +219,7 @@ spec:
|
|||
gitops:
|
||||
gitops_mode_enabled: false
|
||||
repository_url: ""
|
||||
exceptions:
|
||||
labels: false
|
||||
software: false
|
||||
secrets: false
|
||||
|
|
|
|||
|
|
@ -285,7 +285,12 @@
|
|||
},
|
||||
"gitops": {
|
||||
"gitops_mode_enabled": false,
|
||||
"repository_url": "https://github.com/fleetdm/fleet/tree/main/it-and-security"
|
||||
"repository_url": "https://github.com/fleetdm/fleet/tree/main/it-and-security",
|
||||
"exceptions": {
|
||||
"labels": false,
|
||||
"software": false,
|
||||
"secrets": false
|
||||
}
|
||||
},
|
||||
"scripts": []
|
||||
}
|
||||
|
|
@ -166,3 +166,7 @@ spec:
|
|||
gitops:
|
||||
gitops_mode_enabled: false
|
||||
repository_url: ""
|
||||
exceptions:
|
||||
labels: false
|
||||
software: false
|
||||
secrets: false
|
||||
|
|
|
|||
|
|
@ -166,3 +166,7 @@ spec:
|
|||
gitops:
|
||||
gitops_mode_enabled: false
|
||||
repository_url: ""
|
||||
exceptions:
|
||||
labels: false
|
||||
software: false
|
||||
secrets: false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20260323144117, Down_20260323144117)
|
||||
}
|
||||
|
||||
func Up_20260323144117(tx *sql.Tx) error {
|
||||
return updateAppConfigJSON(tx, func(config *fleet.AppConfig) error {
|
||||
// For existing instances, preserve current implicit behavior:
|
||||
// labels and secrets were already no-ops when omitted from GitOps.
|
||||
config.GitOpsConfig.Exceptions.Labels = true
|
||||
config.GitOpsConfig.Exceptions.Secrets = true
|
||||
config.GitOpsConfig.Exceptions.Software = false
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Down_20260323144117(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20260323144117(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// Apply the migration
|
||||
applyNext(t, db)
|
||||
|
||||
// Verify exceptions were set correctly for existing instance
|
||||
var rawAppConfig []byte
|
||||
err := db.QueryRow(`SELECT json_value FROM app_config_json WHERE id = 1`).Scan(&rawAppConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
var config fleet.AppConfig
|
||||
err = json.Unmarshal(rawAppConfig, &config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Existing instances should have labels and secrets excepted (preserving current behavior)
|
||||
require.True(t, config.GitOpsConfig.Exceptions.Labels)
|
||||
require.True(t, config.GitOpsConfig.Exceptions.Secrets)
|
||||
require.False(t, config.GitOpsConfig.Exceptions.Software)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -273,9 +273,16 @@ type DiskEncryptionConfig struct {
|
|||
BitLockerPINRequired bool
|
||||
}
|
||||
|
||||
type UIGitOpsModeConfig struct {
|
||||
GitopsModeEnabled bool `json:"gitops_mode_enabled"`
|
||||
RepositoryURL string `json:"repository_url"`
|
||||
type GitOpsExceptions struct {
|
||||
Labels bool `json:"labels"`
|
||||
Software bool `json:"software"`
|
||||
Secrets bool `json:"secrets"`
|
||||
}
|
||||
|
||||
type GitOpsConfig struct {
|
||||
GitopsModeEnabled bool `json:"gitops_mode_enabled"`
|
||||
RepositoryURL string `json:"repository_url"`
|
||||
Exceptions GitOpsExceptions `json:"exceptions"`
|
||||
}
|
||||
|
||||
func (c *AppConfig) MDMUrl() string {
|
||||
|
|
@ -673,7 +680,7 @@ type AppConfig struct {
|
|||
|
||||
MDM MDM `json:"mdm"`
|
||||
|
||||
UIGitOpsMode UIGitOpsModeConfig `json:"gitops"`
|
||||
GitOpsConfig GitOpsConfig `json:"gitops"`
|
||||
|
||||
// Scripts is a slice of script file paths.
|
||||
//
|
||||
|
|
@ -1064,6 +1071,10 @@ func (c *AppConfig) ApplyDefaultsForNewInstalls() {
|
|||
|
||||
c.Features.ApplyDefaultsForNewInstalls()
|
||||
|
||||
c.GitOpsConfig.Exceptions.Secrets = true
|
||||
c.GitOpsConfig.Exceptions.Labels = false
|
||||
c.GitOpsConfig.Exceptions.Software = false
|
||||
|
||||
c.ApplyDefaults()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Se
|
|||
Integrations: appConfig.Integrations,
|
||||
MDM: appConfig.MDM,
|
||||
Scripts: appConfig.Scripts,
|
||||
UIGitOpsMode: appConfig.UIGitOpsMode,
|
||||
GitOpsConfig: appConfig.GitOpsConfig,
|
||||
ConditionalAccess: appConfig.ConditionalAccess,
|
||||
},
|
||||
appConfigResponseFields: appConfigResponseFields{
|
||||
|
|
@ -809,7 +809,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
|
|||
appConfig.Integrations.GoogleCalendar = oldAppConfig.Integrations.GoogleCalendar
|
||||
}
|
||||
|
||||
gitopsModeEnabled, gitopsRepoURL := appConfig.UIGitOpsMode.GitopsModeEnabled, appConfig.UIGitOpsMode.RepositoryURL
|
||||
gitopsModeEnabled, gitopsRepoURL := appConfig.GitOpsConfig.GitopsModeEnabled, appConfig.GitOpsConfig.RepositoryURL
|
||||
if gitopsModeEnabled {
|
||||
if !lic.IsPremium() {
|
||||
return nil, fleet.NewInvalidArgumentError("UI GitOpsMode: ", ErrMissingLicense.Error())
|
||||
|
|
@ -826,7 +826,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
|
|||
}
|
||||
}
|
||||
|
||||
if oldAppConfig.UIGitOpsMode.GitopsModeEnabled != appConfig.UIGitOpsMode.GitopsModeEnabled {
|
||||
if oldAppConfig.GitOpsConfig.GitopsModeEnabled != appConfig.GitOpsConfig.GitopsModeEnabled {
|
||||
// generate the activity
|
||||
var act fleet.ActivityDetails
|
||||
if gitopsModeEnabled {
|
||||
|
|
|
|||
|
|
@ -676,10 +676,7 @@ func (c *Client) ApplyGroup(
|
|||
|
||||
// Keep any existing GitOps mode config rather than attempting to set via GitOps.
|
||||
if appconfig != nil {
|
||||
specs.AppConfig.(map[string]interface{})["gitops"] = fleet.UIGitOpsModeConfig{
|
||||
GitopsModeEnabled: appconfig.UIGitOpsMode.GitopsModeEnabled,
|
||||
RepositoryURL: appconfig.UIGitOpsMode.RepositoryURL,
|
||||
}
|
||||
specs.AppConfig.(map[string]any)["gitops"] = appconfig.GitOpsConfig
|
||||
}
|
||||
|
||||
if err := c.ApplyAppConfig(specs.AppConfig, opts.ApplySpecOptions); err != nil {
|
||||
|
|
|
|||
|
|
@ -7736,8 +7736,8 @@ func (s *integrationTestSuite) TestAppConfig() {
|
|||
assert.False(t, acResp.ActivityExpirySettings.ActivityExpiryEnabled)
|
||||
assert.Zero(t, acResp.ActivityExpirySettings.ActivityExpiryWindow)
|
||||
assert.False(t, acResp.ServerSettings.AIFeaturesDisabled)
|
||||
assert.False(t, acResp.UIGitOpsMode.GitopsModeEnabled)
|
||||
assert.Zero(t, acResp.UIGitOpsMode.RepositoryURL)
|
||||
assert.False(t, acResp.GitOpsConfig.GitopsModeEnabled)
|
||||
assert.Zero(t, acResp.GitOpsConfig.RepositoryURL)
|
||||
|
||||
// set the apple BM terms expired flag, and the enabled and configured flags,
|
||||
// we'll check again at the end of this test to make sure they weren't
|
||||
|
|
|
|||
|
|
@ -3334,7 +3334,51 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsModeConfig() {
|
|||
}`), http.StatusOK)
|
||||
config, err := s.ds.AppConfig(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://a.b.cc", config.UIGitOpsMode.RepositoryURL)
|
||||
assert.Equal(t, "https://a.b.cc", config.GitOpsConfig.RepositoryURL)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestGitOpsExceptionsConfig() {
|
||||
t := s.T()
|
||||
|
||||
// Enable GitOps mode first
|
||||
s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"gitops": { "gitops_mode_enabled": true, "repository_url": "https://example.com/repo" }
|
||||
}`), http.StatusOK)
|
||||
|
||||
// Set exceptions
|
||||
s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"gitops": { "exceptions": { "labels": true, "software": true, "secrets": false } }
|
||||
}`), http.StatusOK)
|
||||
|
||||
config, err := s.ds.AppConfig(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, config.GitOpsConfig.Exceptions.Labels)
|
||||
assert.True(t, config.GitOpsConfig.Exceptions.Software)
|
||||
assert.False(t, config.GitOpsConfig.Exceptions.Secrets)
|
||||
assert.True(t, config.GitOpsConfig.GitopsModeEnabled)
|
||||
assert.Equal(t, "https://example.com/repo", config.GitOpsConfig.RepositoryURL)
|
||||
|
||||
// Partial update — only change one exception, others should persist
|
||||
s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"gitops": { "exceptions": { "software": false } }
|
||||
}`), http.StatusOK)
|
||||
|
||||
config, err = s.ds.AppConfig(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, config.GitOpsConfig.Exceptions.Labels, "labels should persist")
|
||||
assert.False(t, config.GitOpsConfig.Exceptions.Software, "software should be updated")
|
||||
assert.False(t, config.GitOpsConfig.Exceptions.Secrets, "secrets should persist")
|
||||
assert.True(t, config.GitOpsConfig.GitopsModeEnabled)
|
||||
assert.Equal(t, "https://example.com/repo", config.GitOpsConfig.RepositoryURL)
|
||||
|
||||
// Verify exceptions appear in GET response
|
||||
var getResp appConfigResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &getResp)
|
||||
assert.True(t, getResp.GitOpsConfig.Exceptions.Labels)
|
||||
assert.False(t, getResp.GitOpsConfig.Exceptions.Software)
|
||||
assert.False(t, getResp.GitOpsConfig.Exceptions.Secrets)
|
||||
assert.True(t, getResp.GitOpsConfig.GitopsModeEnabled)
|
||||
assert.Equal(t, "https://example.com/repo", getResp.GitOpsConfig.RepositoryURL)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) assertAppleOSUpdatesDeclaration(teamID *uint, profileName string, expected *fleet.AppleOSUpdateSettings) {
|
||||
|
|
@ -12363,7 +12407,6 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
|
|||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "some install script",
|
||||
PreInstallQuery: "some pre install query",
|
||||
|
|
@ -12381,7 +12424,6 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
|
|||
}
|
||||
|
||||
s.uploadSoftwareInstaller(t, payload, http.StatusBadRequest, `Only one of "labels_include_all", "labels_include_any" or "labels_exclude_any" can be included.`)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -13786,7 +13828,6 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
|||
}
|
||||
res := s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusBadRequest)
|
||||
assert.Contains(t, extractServerErrorText(res.Body), `Only one of "labels_include_all", "labels_include_any" or "labels_exclude_any" can be included.`)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -193,9 +193,13 @@ github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server
|
|||
github.com/fleetdm/fleet/v4/server/fleet/CertificateTemplateSpec Name string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/CertificateTemplateSpec CertificateAuthorityName string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/CertificateTemplateSpec SubjectName string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/AppConfig UIGitOpsMode fleet.UIGitOpsModeConfig
|
||||
github.com/fleetdm/fleet/v4/server/fleet/UIGitOpsModeConfig GitopsModeEnabled bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/UIGitOpsModeConfig RepositoryURL string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/AppConfig GitOpsConfig fleet.GitOpsConfig
|
||||
github.com/fleetdm/fleet/v4/server/fleet/GitOpsConfig GitopsModeEnabled bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/GitOpsConfig RepositoryURL string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/GitOpsConfig Exceptions fleet.GitOpsExceptions
|
||||
github.com/fleetdm/fleet/v4/server/fleet/GitOpsExceptions Labels bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/GitOpsExceptions Software bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/GitOpsExceptions Secrets bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/AppConfig Scripts optjson.Slice[string]
|
||||
github.com/fleetdm/fleet/v4/server/fleet/AppConfig YaraRules []fleet.YaraRule
|
||||
github.com/fleetdm/fleet/v4/server/fleet/YaraRule Name string
|
||||
|
|
|
|||
Loading…
Reference in a new issue