no team software gitops (#20847)
#20464 Adding gitops support for a top level `software` key to be used to manage installable software into "no team". - [ ] 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/Committing-Changes.md#changes-files) for more information. - [X] Added/updated tests - [X] Manual QA for all new/changed functionality --------- Co-authored-by: Victor Lyuboslavsky <victor.lyuboslavsky@gmail.com>
This commit is contained in:
co-authored by
Victor Lyuboslavsky
parent
7f95ae46a3
commit
a6a9a2e1c2
@@ -90,7 +90,7 @@ func applyCommand() *cli.Command {
|
||||
opts.TeamForPolicies = policiesTeamName
|
||||
}
|
||||
baseDir := filepath.Dir(flFilename)
|
||||
_, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, opts)
|
||||
_, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, nil, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func gitopsCommand() *cli.Command {
|
||||
secrets := make(map[string]struct{})
|
||||
for _, flFilename := range flFilenames.Value() {
|
||||
baseDir := filepath.Dir(flFilename)
|
||||
config, err := spec.GitOpsFromFile(flFilename, baseDir)
|
||||
config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ contexts:
|
||||
fmt.Sprintf(
|
||||
`
|
||||
controls:
|
||||
software:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
@@ -230,5 +231,4 @@ team_settings:
|
||||
for _, fileName := range teamFileNames {
|
||||
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", fileName})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const (
|
||||
orgName = "GitOps Test"
|
||||
)
|
||||
|
||||
func TestFilenameValidation(t *testing.T) {
|
||||
func TestFilenameGitOpsValidation(t *testing.T) {
|
||||
filename := strings.Repeat("a", filenameMaxLength+1)
|
||||
_, err := runAppNoChecks([]string{"gitops", "-f", filename})
|
||||
assert.ErrorContains(t, err, "file name must be less than")
|
||||
@@ -207,6 +207,9 @@ func TestBasicGlobalPremiumGitOps(t *testing.T) {
|
||||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
return &fleet.Job{}, nil
|
||||
}
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
||||
require.NoError(t, err)
|
||||
@@ -238,6 +241,7 @@ org_settings:
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets:
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -381,6 +385,7 @@ agent_options:
|
||||
name: ${TEST_TEAM_NAME}
|
||||
team_settings:
|
||||
secrets: ${TEST_SECRET}
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -538,6 +543,7 @@ func TestFullGlobalGitOps(t *testing.T) {
|
||||
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
|
||||
t.Setenv("ORG_NAME", orgName)
|
||||
t.Setenv("APPLE_BM_DEFAULT_TEAM", teamName)
|
||||
t.Setenv("SOFTWARE_INSTALLER_URL", fleetServerURL)
|
||||
file := "./testdata/gitops/global_config_no_paths.yml"
|
||||
|
||||
// Dry run should fail because Apple BM Default Team does not exist and premium license is not set
|
||||
@@ -834,6 +840,7 @@ agent_options:
|
||||
name: ${TEST_TEAM_NAME}
|
||||
team_settings:
|
||||
secrets: [{"secret":"${TEST_SECRET}"}]
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -1011,6 +1018,7 @@ org_settings:
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -1030,6 +1038,7 @@ agent_options:
|
||||
name: ${TEST_TEAM_NAME}
|
||||
team_settings:
|
||||
secrets: [{"secret":"${TEST_SECRET}"}]
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -1045,6 +1054,7 @@ agent_options:
|
||||
name: ${TEST_TEAM_NAME}
|
||||
team_settings:
|
||||
secrets: [{"secret":"${TEST_SECRET}"},{"secret":"globalSecret"}]
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -1222,7 +1232,7 @@ func TestTeamSofwareInstallersGitOps(t *testing.T) {
|
||||
{"testdata/gitops/team_software_installer_install_not_found.yml", "no such file or directory"},
|
||||
{"testdata/gitops/team_software_installer_post_install_not_found.yml", "no such file or directory"},
|
||||
{"testdata/gitops/team_software_installer_no_url.yml", "software URL is required"},
|
||||
{"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field TeamSpecSoftware.packages of type bool"},
|
||||
{"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field SoftwareSpec.packages of type bool"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
||||
@@ -1255,6 +1265,39 @@ func TestTeamSoftwareInstallersGitopsQueryEnv(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNoTeamSoftwareInstallersGitOps(t *testing.T) {
|
||||
startSoftwareInstallerServer(t)
|
||||
|
||||
cases := []struct {
|
||||
file string
|
||||
wantErr string
|
||||
}{
|
||||
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are publicy accessible to the internet."},
|
||||
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
|
||||
{"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 500 MB"},
|
||||
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
|
||||
{"testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
|
||||
{"testdata/gitops/no_team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
|
||||
{"testdata/gitops/no_team_software_installer_install_not_found.yml", "no such file or directory"},
|
||||
{"testdata/gitops/no_team_software_installer_post_install_not_found.yml", "no such file or directory"},
|
||||
{"testdata/gitops/no_team_software_installer_no_url.yml", "software URL is required"},
|
||||
{"testdata/gitops/no_team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field SoftwareSpec.packages of type bool"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
||||
setupFullGitOpsPremiumServer(t)
|
||||
|
||||
t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
|
||||
_, err := runAppNoChecks([]string{"gitops", "-f", c.file})
|
||||
if c.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamVPPAppsGitOps(t *testing.T) {
|
||||
config := &appleVPPConfigSrvConf{
|
||||
Assets: []vpp.Asset{
|
||||
|
||||
@@ -387,7 +387,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
|
||||
}
|
||||
// this only applies standard queries, the base directory is not used,
|
||||
// so pass in the current working directory.
|
||||
_, err = client.ApplyGroup(c.Context, specs, ".", logf, fleet.ApplyClientSpecOptions{})
|
||||
_, err = client.ApplyGroup(c.Context, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -187,3 +187,4 @@ org_settings:
|
||||
secrets: # These secrets are used to enroll hosts to the "All teams" team
|
||||
- secret: SampleSecret123
|
||||
- secret: ABC
|
||||
software:
|
||||
|
||||
+1
@@ -91,3 +91,4 @@ org_settings:
|
||||
databases_path: ""
|
||||
secrets:
|
||||
- secret: ABC
|
||||
software:
|
||||
@@ -97,3 +97,4 @@ org_settings:
|
||||
databases_path: ""
|
||||
secrets:
|
||||
- secret: ABC
|
||||
software:
|
||||
|
||||
+1
@@ -93,3 +93,4 @@ org_settings:
|
||||
databases_path: ""
|
||||
secrets:
|
||||
- secret: ABC
|
||||
software:
|
||||
@@ -91,3 +91,4 @@ org_settings:
|
||||
databases_path: ""
|
||||
secrets:
|
||||
- secret: ABC
|
||||
software:
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/notfound.sh
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/invalidtype.txt
|
||||
self_service: "not a boolean"
|
||||
@@ -0,0 +1,22 @@
|
||||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- install_script:
|
||||
path: lib/install_ruby.sh
|
||||
pre_install_query:
|
||||
path: lib/query_ruby.yml
|
||||
post_install_script:
|
||||
path: lib/post_install_ruby.sh
|
||||
@@ -0,0 +1,17 @@
|
||||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/notfound.deb
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/install_ruby.sh
|
||||
post_install_script:
|
||||
path: lib/notfound.sh
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/install_ruby.sh
|
||||
pre_install_query:
|
||||
path: lib/query_multiple.yml
|
||||
post_install_script:
|
||||
path: lib/post_install_ruby.sh
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/install_ruby.sh
|
||||
pre_install_query:
|
||||
path: lib/notfound.yml
|
||||
@@ -0,0 +1,17 @@
|
||||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/toolarge.deb
|
||||
@@ -0,0 +1,17 @@
|
||||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/invalidtype.txt
|
||||
@@ -0,0 +1,25 @@
|
||||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/install_ruby.sh
|
||||
pre_install_query:
|
||||
path: lib/query_ruby.yml
|
||||
post_install_script:
|
||||
path: lib/post_install_ruby.sh
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/other.deb
|
||||
self_service: true
|
||||
@@ -2957,8 +2957,8 @@ _Available in Fleet Premium._
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| --------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| team_id | number | query | The ID of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_name`. |
|
||||
| team_name | string | query | The name of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_id`. |
|
||||
| team_id | number | query | The ID of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_name`. Ommitting these parameters will add software to 'No Team'. |
|
||||
| team_name | string | query | The name of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_id`. Ommitting these parameters will add software to 'No Team'. |
|
||||
| dry_run | bool | query | If `true`, will validate the provided software packages and return any validation errors, but will not apply the changes. |
|
||||
| software | list | body | An array of software objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, and `post_install_script` - script that runs after software install. |
|
||||
|
||||
|
||||
@@ -580,25 +580,24 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
|
||||
const maxInstallerSizeBytes int64 = 1024 * 1024 * 500
|
||||
|
||||
func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName string, payloads []fleet.SoftwareInstallerPayload, dryRun bool) error {
|
||||
if tmName == "" {
|
||||
svc.authz.SkipAuthorization(ctx) // so that the error message is not replaced by "forbidden"
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("team_name", "must not be empty"))
|
||||
}
|
||||
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tm, err := svc.ds.TeamByName(ctx, tmName)
|
||||
if err != nil {
|
||||
// If this is a dry run, the team may not have been created yet
|
||||
if dryRun && fleet.IsNotFound(err) {
|
||||
return nil
|
||||
var teamID *uint
|
||||
if tmName != "" {
|
||||
tm, err := svc.ds.TeamByName(ctx, tmName)
|
||||
if err != nil {
|
||||
// If this is a dry run, the team may not have been created yet
|
||||
if dryRun && fleet.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
teamID = &tm.ID
|
||||
}
|
||||
|
||||
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: &tm.ID}, fleet.ActionWrite); err != nil {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionWrite); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "validating authorization")
|
||||
}
|
||||
|
||||
@@ -672,7 +671,7 @@ func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName strin
|
||||
}
|
||||
|
||||
installer := &fleet.UploadSoftwareInstallerPayload{
|
||||
TeamID: &tm.ID,
|
||||
TeamID: teamID,
|
||||
InstallScript: p.InstallScript,
|
||||
PreInstallQuery: p.PreInstallQuery,
|
||||
PostInstallScript: p.PostInstallScript,
|
||||
@@ -732,7 +731,7 @@ func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName strin
|
||||
}
|
||||
}
|
||||
|
||||
if err := svc.ds.BatchSetSoftwareInstallers(ctx, &tm.ID, installers); err != nil {
|
||||
if err := svc.ds.BatchSetSoftwareInstallers(ctx, teamID, installers); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "batch set software installers")
|
||||
}
|
||||
|
||||
|
||||
@@ -1246,7 +1246,7 @@ func (svc *Service) editTeamFromSpec(
|
||||
|
||||
if spec.Software != nil {
|
||||
if team.Config.Software == nil {
|
||||
team.Config.Software = &fleet.TeamSpecSoftware{}
|
||||
team.Config.Software = &fleet.SoftwareSpec{}
|
||||
}
|
||||
|
||||
if spec.Software.Packages.Set {
|
||||
|
||||
+15
-9
@@ -61,12 +61,12 @@ type GitOps struct {
|
||||
}
|
||||
|
||||
type GitOpsSoftware struct {
|
||||
Packages []*fleet.TeamSpecSoftwarePackage
|
||||
Packages []*fleet.SoftwarePackageSpec
|
||||
AppStoreApps []*fleet.TeamSpecAppStoreApp
|
||||
}
|
||||
|
||||
// GitOpsFromFile parses a GitOps yaml file.
|
||||
func GitOpsFromFile(filePath, baseDir string) (*GitOps, error) {
|
||||
func GitOpsFromFile(filePath, baseDir string, appConfig *fleet.EnrichedAppConfig) (*GitOps, error) {
|
||||
b, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %s: %w", filePath, err)
|
||||
@@ -96,18 +96,16 @@ func GitOpsFromFile(filePath, baseDir string) (*GitOps, error) {
|
||||
// Figure out if this is an org or team settings file
|
||||
teamRaw, teamOk := top["name"]
|
||||
teamSettingsRaw, teamSettingsOk := top["team_settings"]
|
||||
teamSoftware, teamSoftwareOk := top["software"]
|
||||
orgSettingsRaw, orgOk := top["org_settings"]
|
||||
if orgOk {
|
||||
if teamOk || teamSettingsOk || teamSoftwareOk {
|
||||
multiError = multierror.Append(multiError, errors.New("'org_settings' cannot be used with 'name', 'team_settings' or 'software'"))
|
||||
if teamOk || teamSettingsOk {
|
||||
multiError = multierror.Append(multiError, errors.New("'org_settings' cannot be used with 'name', 'team_settings'"))
|
||||
} else {
|
||||
multiError = parseOrgSettings(orgSettingsRaw, result, baseDir, multiError)
|
||||
}
|
||||
} else if teamOk && teamSettingsOk {
|
||||
multiError = parseName(teamRaw, result, multiError)
|
||||
multiError = parseTeamSettings(teamSettingsRaw, result, baseDir, multiError)
|
||||
multiError = parseSoftware(teamSoftware, result, baseDir, multiError)
|
||||
} else {
|
||||
multiError = multierror.Append(multiError, errors.New("either 'org_settings' or 'name' and 'team_settings' is required"))
|
||||
}
|
||||
@@ -118,6 +116,10 @@ func GitOpsFromFile(filePath, baseDir string) (*GitOps, error) {
|
||||
multiError = parsePolicies(top, result, baseDir, multiError)
|
||||
multiError = parseQueries(top, result, baseDir, multiError)
|
||||
|
||||
if appConfig != nil && appConfig.License.IsPremium() {
|
||||
multiError = parseSoftware(top, result, baseDir, multiError)
|
||||
}
|
||||
|
||||
return result, multiError.ErrorOrNil()
|
||||
}
|
||||
|
||||
@@ -523,11 +525,15 @@ func parseQueries(top map[string]json.RawMessage, result *GitOps, baseDir string
|
||||
return multiError
|
||||
}
|
||||
|
||||
func parseSoftware(softwareRaw json.RawMessage, result *GitOps, baseDir string, multiError *multierror.Error) *multierror.Error {
|
||||
var software fleet.TeamSpecSoftware
|
||||
func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir string, multiError *multierror.Error) *multierror.Error {
|
||||
softwareRaw, ok := top["software"]
|
||||
if !ok {
|
||||
return multierror.Append(multiError, errors.New("'software' is required"))
|
||||
}
|
||||
var software fleet.SoftwareSpec
|
||||
if len(softwareRaw) > 0 {
|
||||
if err := json.Unmarshal(softwareRaw, &software); err != nil {
|
||||
return multierror.Append(multiError, fmt.Errorf("failed to unmarshall software: %v", err))
|
||||
return multierror.Append(multiError, fmt.Errorf("failed to unmarshall softwarespec: %v", err))
|
||||
}
|
||||
}
|
||||
if software.AppStoreApps.Set {
|
||||
|
||||
@@ -53,7 +53,7 @@ func createTempFile(t *testing.T, pattern, contents string) (filePath string, ba
|
||||
|
||||
func gitOpsFromString(t *testing.T, s string) (*GitOps, error) {
|
||||
path, basePath := createTempFile(t, "", s)
|
||||
return GitOpsFromFile(path, basePath)
|
||||
return GitOpsFromFile(path, basePath, nil)
|
||||
}
|
||||
|
||||
func TestValidGitOpsYaml(t *testing.T) {
|
||||
@@ -108,7 +108,7 @@ func TestValidGitOpsYaml(t *testing.T) {
|
||||
t.Parallel()
|
||||
}
|
||||
|
||||
gitops, err := GitOpsFromFile(test.filePath, "./testdata")
|
||||
gitops, err := GitOpsFromFile(test.filePath, "./testdata", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
if test.isTeam {
|
||||
@@ -336,20 +336,20 @@ func TestMixingGlobalAndTeamConfig(t *testing.T) {
|
||||
config := getGlobalConfig(nil)
|
||||
config += "name: TeamName\n"
|
||||
_, err := gitOpsFromString(t, config)
|
||||
assert.ErrorContains(t, err, "'org_settings' cannot be used with 'name', 'team_settings' or 'software'")
|
||||
assert.ErrorContains(t, err, "'org_settings' cannot be used with 'name', 'team_settings'")
|
||||
|
||||
// Mixing org_settings and team_settings
|
||||
config = getGlobalConfig(nil)
|
||||
config += "team_settings:\n secrets: []\n"
|
||||
_, err = gitOpsFromString(t, config)
|
||||
assert.ErrorContains(t, err, "'org_settings' cannot be used with 'name', 'team_settings' or 'software'")
|
||||
assert.ErrorContains(t, err, "'org_settings' cannot be used with 'name', 'team_settings'")
|
||||
|
||||
// Mixing org_settings and team name and team_settings
|
||||
config = getGlobalConfig(nil)
|
||||
config += "name: TeamName\n"
|
||||
config += "team_settings:\n secrets: []\n"
|
||||
_, err = gitOpsFromString(t, config)
|
||||
assert.ErrorContains(t, err, "'org_settings' cannot be used with 'name', 'team_settings' or 'software'")
|
||||
assert.ErrorContains(t, err, "'org_settings' cannot be used with 'name', 'team_settings'")
|
||||
}
|
||||
|
||||
func TestInvalidGitOpsYaml(t *testing.T) {
|
||||
@@ -696,7 +696,7 @@ func TestGitOpsPaths(t *testing.T) {
|
||||
err = os.WriteFile(mainTmpFile.Name(), []byte(config), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = GitOpsFromFile(mainTmpFile.Name(), dir)
|
||||
_, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test a bad path
|
||||
@@ -709,7 +709,7 @@ func TestGitOpsPaths(t *testing.T) {
|
||||
err = os.WriteFile(mainTmpFile.Name(), []byte(config), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = GitOpsFromFile(mainTmpFile.Name(), dir)
|
||||
_, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil)
|
||||
assert.ErrorContains(t, err, "no such file or directory")
|
||||
|
||||
// Test a bad file -- cannot be unmarshalled
|
||||
@@ -744,7 +744,7 @@ func TestGitOpsPaths(t *testing.T) {
|
||||
}
|
||||
err = os.WriteFile(mainTmpFile.Name(), []byte(config), 0o644)
|
||||
require.NoError(t, err)
|
||||
_, err = GitOpsFromFile(mainTmpFile.Name(), dir)
|
||||
_, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil)
|
||||
assert.ErrorContains(t, err, "nested paths are not supported")
|
||||
},
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ type Group struct {
|
||||
Packs []*fleet.PackSpec
|
||||
Labels []*fleet.LabelSpec
|
||||
Policies []*fleet.PolicySpec
|
||||
Software []*fleet.SoftwarePackageSpec
|
||||
// This needs to be interface{} to allow for the patch logic. Otherwise we send a request that looks to the
|
||||
// server like the user explicitly set the zero values.
|
||||
AppConfig interface{}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
)
|
||||
|
||||
@@ -334,6 +335,19 @@ type SoftwarePackageOrApp struct {
|
||||
LastInstall *HostSoftwareInstall `json:"last_install"`
|
||||
}
|
||||
|
||||
type SoftwarePackageSpec struct {
|
||||
URL string `json:"url"`
|
||||
SelfService bool `json:"self_service"`
|
||||
PreInstallQuery TeamSpecSoftwareAsset `json:"pre_install_query"`
|
||||
InstallScript TeamSpecSoftwareAsset `json:"install_script"`
|
||||
PostInstallScript TeamSpecSoftwareAsset `json:"post_install_script"`
|
||||
}
|
||||
|
||||
type SoftwareSpec struct {
|
||||
Packages optjson.Slice[SoftwarePackageSpec] `json:"packages,omitempty"`
|
||||
AppStoreApps optjson.Slice[TeamSpecAppStoreApp] `json:"app_store_apps,omitempty"`
|
||||
}
|
||||
|
||||
// HostSoftwareInstall represents installation of software on a host from a
|
||||
// Fleet software installer.
|
||||
type HostSoftwareInstall struct {
|
||||
|
||||
+2
-15
@@ -155,7 +155,7 @@ type TeamConfig struct {
|
||||
Features Features `json:"features"`
|
||||
MDM TeamMDM `json:"mdm"`
|
||||
Scripts optjson.Slice[string] `json:"scripts,omitempty"`
|
||||
Software *TeamSpecSoftware `json:"software,omitempty"`
|
||||
Software *SoftwareSpec `json:"software,omitempty"`
|
||||
}
|
||||
|
||||
type TeamWebhookSettings struct {
|
||||
@@ -168,23 +168,10 @@ type TeamSpecSoftwareAsset struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type TeamSpecSoftware struct {
|
||||
Packages optjson.Slice[TeamSpecSoftwarePackage] `json:"packages,omitempty"`
|
||||
AppStoreApps optjson.Slice[TeamSpecAppStoreApp] `json:"app_store_apps,omitempty"`
|
||||
}
|
||||
|
||||
type TeamSpecAppStoreApp struct {
|
||||
AppStoreID string `json:"app_store_id"`
|
||||
}
|
||||
|
||||
type TeamSpecSoftwarePackage struct {
|
||||
URL string `json:"url"`
|
||||
SelfService bool `json:"self_service"`
|
||||
PreInstallQuery TeamSpecSoftwareAsset `json:"pre_install_query"`
|
||||
InstallScript TeamSpecSoftwareAsset `json:"install_script"`
|
||||
PostInstallScript TeamSpecSoftwareAsset `json:"post_install_script"`
|
||||
}
|
||||
|
||||
type TeamMDM struct {
|
||||
EnableDiskEncryption bool `json:"enable_disk_encryption"`
|
||||
MacOSUpdates AppleOSUpdateSettings `json:"macos_updates"`
|
||||
@@ -450,7 +437,7 @@ type TeamSpec struct {
|
||||
Scripts optjson.Slice[string] `json:"scripts"`
|
||||
WebhookSettings TeamSpecWebhookSettings `json:"webhook_settings"`
|
||||
Integrations TeamSpecIntegrations `json:"integrations"`
|
||||
Software *TeamSpecSoftware `json:"software,omitempty"`
|
||||
Software *SoftwareSpec `json:"software,omitempty"`
|
||||
}
|
||||
|
||||
type TeamSpecWebhookSettings struct {
|
||||
|
||||
+133
-91
@@ -394,6 +394,7 @@ func (c *Client) ApplyGroup(
|
||||
specs *spec.Group,
|
||||
baseDir string,
|
||||
logf func(format string, args ...interface{}),
|
||||
appconfig *fleet.EnrichedAppConfig,
|
||||
opts fleet.ApplyClientSpecOptions,
|
||||
) (map[string]uint, error) {
|
||||
logfn := func(format string, args ...interface{}) {
|
||||
@@ -610,90 +611,10 @@ func (c *Client) ApplyGroup(
|
||||
tmSoftwarePackages := extractTmSpecsSoftwarePackages(specs.Teams)
|
||||
tmSoftwarePackagesPayloads := make(map[string][]fleet.SoftwareInstallerPayload, len(tmScripts))
|
||||
for tmName, software := range tmSoftwarePackages {
|
||||
softwarePayloads := make([]fleet.SoftwareInstallerPayload, len(software))
|
||||
for i, si := range software {
|
||||
var qc string
|
||||
var err error
|
||||
if si.PreInstallQuery.Path != "" {
|
||||
queryFile := resolveApplyRelativePath(baseDir, si.PreInstallQuery.Path)
|
||||
rawSpec, err := os.ReadFile(queryFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading pre-install query: %w", err)
|
||||
}
|
||||
|
||||
rawSpecExpanded, err := spec.ExpandEnvBytes(rawSpec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't exit software (%s). Unable to expand environment variable in YAML file %s: %w", si.URL, queryFile, err)
|
||||
}
|
||||
|
||||
var top any
|
||||
|
||||
if err := yaml.Unmarshal(rawSpecExpanded, &top); err != nil {
|
||||
return nil, fmt.Errorf("Couldn't exit software (%s). Unable to expand environment variable in YAML file %s: %w", si.URL, queryFile, err)
|
||||
}
|
||||
|
||||
if _, ok := top.(map[any]any); ok {
|
||||
// Old apply format
|
||||
group, err := spec.GroupFromBytes(rawSpecExpanded)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Unable to parse pre-install apply format query YAML file %s: %w", si.URL, queryFile, err)
|
||||
}
|
||||
|
||||
if len(group.Queries) > 1 {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Pre-install query YAML file %s should have only one query.", si.URL, queryFile)
|
||||
}
|
||||
|
||||
if len(group.Queries) == 0 {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Pre-install query YAML file %s doesn't have a query defined.", si.URL, queryFile)
|
||||
}
|
||||
|
||||
qc = group.Queries[0].Query
|
||||
} else {
|
||||
// Gitops format
|
||||
var querySpecs []fleet.QuerySpec
|
||||
if err := yaml.Unmarshal(rawSpecExpanded, &querySpecs); err != nil {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Unable to parse pre-install query YAML file %s: %w", si.URL, queryFile, err)
|
||||
}
|
||||
|
||||
if len(querySpecs) > 1 {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Pre-install query YAML file %s should have only one query.", si.URL, queryFile)
|
||||
}
|
||||
|
||||
if len(querySpecs) == 0 {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Pre-install query YAML file %s doesn't have a query defined.", si.URL, queryFile)
|
||||
}
|
||||
|
||||
qc = querySpecs[0].Query
|
||||
}
|
||||
}
|
||||
|
||||
var ic []byte
|
||||
if si.InstallScript.Path != "" {
|
||||
installScriptFile := resolveApplyRelativePath(baseDir, si.InstallScript.Path)
|
||||
ic, err = os.ReadFile(installScriptFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Unable to read install script file %s: %w", si.URL, si.InstallScript.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
var pc []byte
|
||||
if si.PostInstallScript.Path != "" {
|
||||
postInstallScriptFile := resolveApplyRelativePath(baseDir, si.PostInstallScript.Path)
|
||||
pc, err = os.ReadFile(postInstallScriptFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Unable to read post-install script file %s: %w", si.URL, si.PostInstallScript.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
softwarePayloads[i] = fleet.SoftwareInstallerPayload{
|
||||
URL: si.URL,
|
||||
SelfService: si.SelfService,
|
||||
PreInstallQuery: qc,
|
||||
InstallScript: string(ic),
|
||||
PostInstallScript: string(pc),
|
||||
}
|
||||
softwarePayloads, err := buildSoftwarePackagesPayload(baseDir, software)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("applying software installers for team %q: %w", tmName, err)
|
||||
}
|
||||
|
||||
tmSoftwarePackagesPayloads[tmName] = softwarePayloads
|
||||
}
|
||||
|
||||
@@ -810,6 +731,95 @@ func (c *Client) ApplyGroup(
|
||||
return teamIDsByName, nil
|
||||
}
|
||||
|
||||
func buildSoftwarePackagesPayload(baseDir string, specs []fleet.SoftwarePackageSpec) ([]fleet.SoftwareInstallerPayload, error) {
|
||||
softwarePayloads := make([]fleet.SoftwareInstallerPayload, len(specs))
|
||||
for i, si := range specs {
|
||||
var qc string
|
||||
var err error
|
||||
if si.PreInstallQuery.Path != "" {
|
||||
queryFile := resolveApplyRelativePath(baseDir, si.PreInstallQuery.Path)
|
||||
rawSpec, err := os.ReadFile(queryFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading pre-install query: %w", err)
|
||||
}
|
||||
|
||||
rawSpecExpanded, err := spec.ExpandEnvBytes(rawSpec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't exit software (%s). Unable to expand environment variable in YAML file %s: %w", si.URL, queryFile, err)
|
||||
}
|
||||
|
||||
var top any
|
||||
|
||||
if err := yaml.Unmarshal(rawSpecExpanded, &top); err != nil {
|
||||
return nil, fmt.Errorf("Couldn't exit software (%s). Unable to expand environment variable in YAML file %s: %w", si.URL, queryFile, err)
|
||||
}
|
||||
|
||||
if _, ok := top.(map[any]any); ok {
|
||||
// Old apply format
|
||||
group, err := spec.GroupFromBytes(rawSpecExpanded)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Unable to parse pre-install apply format query YAML file %s: %w", si.URL, queryFile, err)
|
||||
}
|
||||
|
||||
if len(group.Queries) > 1 {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Pre-install query YAML file %s should have only one query.", si.URL, queryFile)
|
||||
}
|
||||
|
||||
if len(group.Queries) == 0 {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Pre-install query YAML file %s doesn't have a query defined.", si.URL, queryFile)
|
||||
}
|
||||
|
||||
qc = group.Queries[0].Query
|
||||
} else {
|
||||
// Gitops format
|
||||
var querySpecs []fleet.QuerySpec
|
||||
if err := yaml.Unmarshal(rawSpecExpanded, &querySpecs); err != nil {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Unable to parse pre-install query YAML file %s: %w", si.URL, queryFile, err)
|
||||
}
|
||||
|
||||
if len(querySpecs) > 1 {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Pre-install query YAML file %s should have only one query.", si.URL, queryFile)
|
||||
}
|
||||
|
||||
if len(querySpecs) == 0 {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Pre-install query YAML file %s doesn't have a query defined.", si.URL, queryFile)
|
||||
}
|
||||
|
||||
qc = querySpecs[0].Query
|
||||
}
|
||||
}
|
||||
|
||||
var ic []byte
|
||||
if si.InstallScript.Path != "" {
|
||||
installScriptFile := resolveApplyRelativePath(baseDir, si.InstallScript.Path)
|
||||
ic, err = os.ReadFile(installScriptFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Unable to read install script file %s: %w", si.URL, si.InstallScript.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
var pc []byte
|
||||
if si.PostInstallScript.Path != "" {
|
||||
postInstallScriptFile := resolveApplyRelativePath(baseDir, si.PostInstallScript.Path)
|
||||
pc, err = os.ReadFile(postInstallScriptFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't edit software (%s). Unable to read post-install script file %s: %w", si.URL, si.PostInstallScript.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
softwarePayloads[i] = fleet.SoftwareInstallerPayload{
|
||||
URL: si.URL,
|
||||
SelfService: si.SelfService,
|
||||
PreInstallQuery: qc,
|
||||
InstallScript: string(ic),
|
||||
PostInstallScript: string(pc),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return softwarePayloads, nil
|
||||
}
|
||||
|
||||
func extractAppCfgMacOSSetup(appCfg any) *fleet.MacOSSetup {
|
||||
asMap, ok := appCfg.(map[string]interface{})
|
||||
if !ok {
|
||||
@@ -1045,8 +1055,8 @@ func extractTmSpecsMDMCustomSettings(tmSpecs []json.RawMessage) map[string]profi
|
||||
return m
|
||||
}
|
||||
|
||||
func extractTmSpecsSoftwarePackages(tmSpecs []json.RawMessage) map[string][]fleet.TeamSpecSoftwarePackage {
|
||||
var m map[string][]fleet.TeamSpecSoftwarePackage
|
||||
func extractTmSpecsSoftwarePackages(tmSpecs []json.RawMessage) map[string][]fleet.SoftwarePackageSpec {
|
||||
var m map[string][]fleet.SoftwarePackageSpec
|
||||
for _, tm := range tmSpecs {
|
||||
var spec struct {
|
||||
Name string `json:"name"`
|
||||
@@ -1059,10 +1069,10 @@ func extractTmSpecsSoftwarePackages(tmSpecs []json.RawMessage) map[string][]flee
|
||||
spec.Name = norm.NFC.String(spec.Name)
|
||||
if spec.Name != "" && len(spec.Software) > 0 {
|
||||
if m == nil {
|
||||
m = make(map[string][]fleet.TeamSpecSoftwarePackage)
|
||||
m = make(map[string][]fleet.SoftwarePackageSpec)
|
||||
}
|
||||
var software fleet.TeamSpecSoftware
|
||||
var packages []fleet.TeamSpecSoftwarePackage
|
||||
var software fleet.SoftwareSpec
|
||||
var packages []fleet.SoftwarePackageSpec
|
||||
if err := json.Unmarshal(spec.Software, &software); err != nil {
|
||||
// ignore, will fail in apply team specs call
|
||||
continue
|
||||
@@ -1070,7 +1080,7 @@ func extractTmSpecsSoftwarePackages(tmSpecs []json.RawMessage) map[string][]flee
|
||||
if !software.Packages.Valid {
|
||||
// to be consistent with the AppConfig custom settings, set it to an
|
||||
// empty slice if the provided custom settings are present but empty.
|
||||
packages = []fleet.TeamSpecSoftwarePackage{}
|
||||
packages = []fleet.SoftwarePackageSpec{}
|
||||
} else {
|
||||
packages = software.Packages.Value
|
||||
}
|
||||
@@ -1096,7 +1106,7 @@ func extractTmSpecsSoftwareApps(tmSpecs []json.RawMessage) map[string][]fleet.Te
|
||||
if m == nil {
|
||||
m = make(map[string][]fleet.TeamSpecAppStoreApp)
|
||||
}
|
||||
var software fleet.TeamSpecSoftware
|
||||
var software fleet.SoftwareSpec
|
||||
var apps []fleet.TeamSpecAppStoreApp
|
||||
if err := json.Unmarshal(spec.Software, &software); err != nil {
|
||||
// ignore, will fail in apply team specs call
|
||||
@@ -1255,6 +1265,8 @@ func (c *Client) DoGitOps(
|
||||
}
|
||||
}
|
||||
group.AppConfig.(map[string]interface{})["scripts"] = scripts
|
||||
|
||||
group.Software = config.Software.Packages
|
||||
} else {
|
||||
team = make(map[string]interface{})
|
||||
team["name"] = *config.TeamName
|
||||
@@ -1413,7 +1425,7 @@ func (c *Client) DoGitOps(
|
||||
}
|
||||
|
||||
// Apply org settings, scripts, enroll secrets, and controls
|
||||
teamIDsByName, err := c.ApplyGroup(ctx, &group, baseDir, logf, fleet.ApplyClientSpecOptions{
|
||||
teamIDsByName, err := c.ApplyGroup(ctx, &group, baseDir, logf, appConfig, fleet.ApplyClientSpecOptions{
|
||||
ApplySpecOptions: fleet.ApplySpecOptions{
|
||||
DryRun: dryRun,
|
||||
},
|
||||
@@ -1449,9 +1461,39 @@ func (c *Client) DoGitOps(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.doGitOpsNoTeamSoftware(group, baseDir, appConfig, logFn, dryRun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return teamAssumptions, nil
|
||||
}
|
||||
|
||||
func (c *Client) doGitOpsNoTeamSoftware(specs spec.Group, baseDir string, appconfig *fleet.EnrichedAppConfig, logFn func(format string, args ...interface{}), dryRun bool) error {
|
||||
if len(specs.Teams) == 0 && appconfig != nil && appconfig.License.IsPremium() {
|
||||
packages := make([]fleet.SoftwarePackageSpec, 0, len(specs.Software))
|
||||
for _, software := range specs.Software {
|
||||
if software != nil {
|
||||
packages = append(packages, *software)
|
||||
}
|
||||
}
|
||||
payload, err := buildSoftwarePackagesPayload(baseDir, packages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("applying software installers: %w", err)
|
||||
}
|
||||
if err := c.ApplyNoTeamSoftwareInstallers(payload, fleet.ApplySpecOptions{DryRun: dryRun}); err != nil {
|
||||
return fmt.Errorf("applying software installers: %w", err)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
logFn("[+] would've applied 'No Team' software installers\n")
|
||||
} else {
|
||||
logFn("[+] applied 'No Team' software installers\n")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) doGitOpsPolicies(config *spec.GitOps, logFn func(format string, args ...interface{}), dryRun bool) error {
|
||||
// Get the ids and names of current policies to figure out which ones to delete
|
||||
policies, err := c.GetPolicies(config.TeamID)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
@@ -25,3 +27,12 @@ func (c *Client) ListSoftwareTitles(query string) ([]fleet.SoftwareTitleListResu
|
||||
}
|
||||
return responseBody.SoftwareTitles, nil
|
||||
}
|
||||
|
||||
func (c *Client) ApplyNoTeamSoftwareInstallers(softwareInstallers []fleet.SoftwareInstallerPayload, opts fleet.ApplySpecOptions) error {
|
||||
verb, path := "POST", "/api/latest/fleet/software/batch"
|
||||
query, err := url.ParseQuery(opts.RawQuery())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.authenticatedRequestWithQuery(map[string]interface{}{"software": softwareInstallers}, verb, path, nil, query.Encode())
|
||||
}
|
||||
|
||||
@@ -10161,7 +10161,7 @@ func (s *integrationEnterpriseTestSuite) TestApplyTeamsSoftwareConfig() {
|
||||
}
|
||||
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
|
||||
|
||||
wantSoftwarePackages := []fleet.TeamSpecSoftwarePackage{
|
||||
wantSoftwarePackages := []fleet.SoftwarePackageSpec{
|
||||
{
|
||||
URL: "http://foo.com",
|
||||
SelfService: true,
|
||||
@@ -10320,9 +10320,6 @@ func (s *integrationEnterpriseTestSuite) TestApplyTeamsSoftwareConfig() {
|
||||
func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
||||
t := s.T()
|
||||
|
||||
// a team name is required (we don't allow installers for "no team")
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{}, http.StatusBadRequest)
|
||||
|
||||
// non-existent team
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{}, http.StatusNotFound, "team_name", "foo")
|
||||
|
||||
@@ -10397,6 +10394,42 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &titlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(tm.ID)))
|
||||
require.Equal(t, 0, titlesResp.Count)
|
||||
require.Len(t, titlesResp.SoftwareTitles, 0)
|
||||
|
||||
//////////////////////////
|
||||
// Do a request with a valid URL with no team
|
||||
//////////////////////////
|
||||
softwareToInstall = []fleet.SoftwareInstallerPayload{
|
||||
{URL: srv.URL},
|
||||
}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusNoContent)
|
||||
|
||||
// check the application status on team 0
|
||||
titlesResp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &titlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(0)))
|
||||
require.Equal(t, 1, titlesResp.Count)
|
||||
require.Len(t, titlesResp.SoftwareTitles, 1)
|
||||
|
||||
// same payload doesn't modify anything
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusNoContent)
|
||||
newTitlesResp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &newTitlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(0)))
|
||||
require.Equal(t, titlesResp, newTitlesResp)
|
||||
|
||||
// setting self-service to true updates the software title metadata
|
||||
softwareToInstall[0].SelfService = true
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusNoContent)
|
||||
newTitlesResp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &newTitlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(0)))
|
||||
titlesResp.SoftwareTitles[0].SoftwarePackage.SelfService = ptr.Bool(true)
|
||||
require.Equal(t, titlesResp, newTitlesResp)
|
||||
|
||||
// empty payload cleans the software items
|
||||
softwareToInstall = []fleet.SoftwareInstallerPayload{}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusNoContent)
|
||||
titlesResp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &titlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(0)))
|
||||
require.Equal(t, 0, titlesResp.Count)
|
||||
require.Len(t, titlesResp.SoftwareTitles, 0)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerNewInstallRequestPlatformValidation() {
|
||||
|
||||
@@ -318,7 +318,7 @@ func (svc *Service) GetSoftwareInstallResults(ctx context.Context, resultUUID st
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type batchSetSoftwareInstallersRequest struct {
|
||||
TeamName string `json:"-" query:"team_name"`
|
||||
TeamName string `json:"-" query:"team_name,optional"`
|
||||
DryRun bool `json:"-" query:"dry_run,optional"` // if true, apply validation but do not save changes
|
||||
Software []fleet.SoftwareInstallerPayload `json:"software"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user