mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #42180 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Enhanced GitOps exception handling for labels, secrets, and software with clearer enforcement and omission semantics. * Server-side prefetch of team software so omitted team software can preserve existing installers during validation. * Presence flags track whether top-level keys (labels, secrets, software) were provided versus omitted. * **Behavior Changes** * Omitted vs empty sections are now distinguished: omission can mean “no-op” or “delete-all” depending on exception settings. * GitOps YAML can define and manage labels directly; validations now reject YAML that includes keys marked as excepted. <!-- end of auto-generated comment: release notes by coderabbit.ai --> # Checklist for submitter If some of the following don't apply, delete the relevant line. - [X] 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. ## Testing - [X] Added/updated automated tests - [X] QA'd all new/changed functionality manually * **Labels** - [ ] Validated that with label exceptions off, omitting `labels:` key from default.yml clears all global labels - [ ] Validated that with label exceptions off, omitting `labels:` key from a fleet .yml clears all labels for that fleet - [ ] Validated that with label exceptions off, setting empty `labels:` key from default.yml clears all global labels - [ ] Validated that with label exceptions off, setting empty `labels:` key from a fleet .yml clears all labels for that fleet - [ ] Validated that with label exceptions on, omitting `labels:` key from default .yml leaves existing global labels as-is - [ ] Validated that with label exceptions on, omitting `labels:` key from a fleet .yml leaves existing labels as-is - [ ] Validated that with label exceptions on, setting `labels:` key on default .yml generates an error - [ ] Validated that with label exceptions on, setting `labels:` key on a fleet .yml generates an error - [ ] Validated that with label exceptions on, a policy using `labels_include_any` referencing an existing label succeeds without `labels:` key - [ ] Validated that with label exceptions on, a query using `labels_include_any` referencing an existing label succeeds without `labels:` key - [ ] Validated that with label exceptions on, an MDM profile using `labels_include_any` referencing an existing label succeeds without `labels:` key - [ ] Validated that with label exceptions on, a software package using `labels_include_any` referencing an existing label succeeds without `labels:` key (requires software exceptions off) - [ ] Validated that with label exceptions on, an app store app using `labels_include_any` referencing an existing label succeeds without `labels:` key (requires software exceptions off) - [ ] Validated that with label exceptions on, a fleet maintained app using `labels_include_any` referencing an existing label succeeds without `labels:` key (requires software exceptions off) * **Secrets** - [ ] Validated that with secrets exceptions off, omitting `secrets:` key from default.yml clears all global secrets - [ ] Validated that with secrets exceptions off, omitting `secrets:` key from a fleet .yml clears all secrets for that fleet - [ ] Validated that with secrets exceptions on, omitting `secrets:` key from default .yml leaves existing global secrets as-is - [ ] Validated that with secrets exceptions on, omitting `secrets:` key from a fleet .yml leaves existing secrets as-is - [ ] Validated that with secrets exceptions on, setting `secrets:` key on default .yml generates an error - [ ] Validated that with secrets exceptions on, setting `secrets:` key on a fleet .yml generates an error * **Software** - [ ] Validated that with software exceptions off, omitting `software:` key from no-team.yml/unassigned.yml clears all software for "no team" - [ ] Validated that with software exceptions off, omitting `software:` key from a fleet .yml clears all software for that fleet - [ ] Validated that with software exceptions off, setting empty `software:` key on a fleet .yml clears all software for that fleet - [ ] Validated that with software exceptions off, setting empty `software:` key on no-team.yml/unassigned.yml clears all software for "no team - [ ] Validated that with software exceptions on, omitting `software:` key from a fleet .yml leaves existing software as-is - [ ] Validated that with software exceptions on, setting `software:` key on a fleet .yml generates an error - [ ] Validated that with software exceptions on, omitting `software:` key from no-team.yml/unassigned.yml leaves existing software as-is for "no team" - [ ] Validated that with software exceptions on, setting `software:` key on no-team.yml/unassigned.yml generates an error - [ ] Validated that with software exceptions on, a policy using `install_software.hash_sha256` referencing an existing package succeeds without `software:` key - [ ] Validated that with software exceptions on, a policy using `install_software.app_store_id` referencing an existing VPP app succeeds without `software:` key - [ ] Validated that with software exceptions on, a patch policy using `fleet_maintained_app_slug` referencing an existing FMA succeeds without `software:` key - [ ] Validated that with software exceptions on, `setup_experience.software` referencing existing software succeeds without `software:` key (server-side validation fallback) - [ ] Validated that with software exceptions on, omitting `software:` from no-team.yml/unassigned.yml preserves existing no-team software - [ ] Validated that with software exceptions on, a policy in no-team.yml/unassigned.yml using `install_software.hash_sha256` referencing existing no-team software succeeds without `software:` key For unreleased bug fixes in a release candidate, one of: - [X] Confirmed that the fix is not expected to adversely impact load test results I don't think so. There is a bit of overhead when this feature is used since we have to fetch software from the server, but it would be done in a specific test, so even if there is an impact it should affect existing load testing, only new, specific tests.
1047 lines
40 KiB
Go
1047 lines
40 KiB
Go
package gitops
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl"
|
|
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
|
|
"github.com/fleetdm/fleet/v4/pkg/file"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/test"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
teamName = "Team Test"
|
|
)
|
|
|
|
func TestGitOpsTeamSoftwareInstallers(t *testing.T) {
|
|
testing_utils.StartSoftwareInstallerServer(t)
|
|
testing_utils.StartAndServeVPPServer(t)
|
|
|
|
cases := []struct {
|
|
file string
|
|
wantErr string
|
|
}{
|
|
{"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
|
|
{"testdata/gitops/team_software_installer_install_script_secret.yml", "environment variable \"FLEET_SECRET_NAME\" not set"},
|
|
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .zip, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1."},
|
|
{"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 513MiB"},
|
|
{"testdata/gitops/team_software_installer_valid.yml", ""},
|
|
{"testdata/gitops/team_software_installer_subdir.yml", ""},
|
|
{"testdata/gitops/subdir/team_software_installer_valid.yml", ""},
|
|
{"testdata/gitops/team_software_installer_valid_apply.yml", ""},
|
|
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
|
|
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries_apply.yml", "should have only one query."},
|
|
{"testdata/gitops/team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
|
|
{"testdata/gitops/team_software_installer_install_not_found.yml", "no such file or directory"},
|
|
{"testdata/gitops/team_software_installer_uninstall_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", "at least one of hash_sha256 or url is required for each software package"},
|
|
{"testdata/gitops/team_software_installer_no_url_multi.yml", "multi_missing_url.yml, list item #1"},
|
|
{
|
|
"testdata/gitops/team_software_installer_invalid_self_service_value.yml",
|
|
"Couldn't edit \"../../fleetctl/testdata/gitops/team_software_installer_invalid_self_service_value.yml\" at \"software.packages.self_service\", expected type bool but got string",
|
|
},
|
|
{
|
|
"testdata/gitops/team_software_installer_invalid_both_include_exclude.yml",
|
|
`only one of "labels_include_all", "labels_exclude_any" or "labels_include_any" can be specified`,
|
|
},
|
|
{"testdata/gitops/team_software_installer_valid_include.yml", ""},
|
|
{"testdata/gitops/team_software_installer_valid_exclude.yml", ""},
|
|
{"testdata/gitops/team_software_installer_valid_include_all.yml", ""},
|
|
{
|
|
"testdata/gitops/team_software_installer_invalid_unknown_label.yml",
|
|
"Please create the missing labels, or update your settings to not refer to these labels.",
|
|
},
|
|
// display_name tests
|
|
{"testdata/gitops/team_software_installer_with_display_name.yml", ""},
|
|
{"testdata/gitops/team_software_installer_display_name_too_long.yml", "display_name is too long (max 255 characters)"},
|
|
{"testdata/gitops/team_software_app_store_display_name_too_long.yml", "display_name is too long (max 255 characters)"},
|
|
// team tests for setup experience software/script
|
|
{"testdata/gitops/team_setup_software_valid.yml", ""},
|
|
{"testdata/gitops/team_setup_software_on_package.yml", ""},
|
|
{"testdata/gitops/team_setup_software_defined_in_conflicting_places.yml", " Setup experience may only be specified directly on software or within macos_setup, but not both."},
|
|
{"testdata/gitops/team_setup_software_defined_in_conflicting_places_vpp.yml", " Setup experience may only be specified directly on software or within macos_setup, but not both."},
|
|
{"testdata/gitops/team_setup_software_invalid_script.yml", "no_such_script.sh: no such file"},
|
|
{"testdata/gitops/team_setup_software_invalid_software_package.yml", "no_such_software.yml\" does not exist for that fleet"},
|
|
{"testdata/gitops/team_setup_software_invalid_vpp_app.yml", "\"no_such_app\" does not exist for that fleet"},
|
|
{"testdata/gitops/team_software_installer_valid_ipa.yml", ""},
|
|
{"testdata/gitops/team_software_installer_subdir_ipa.yml", ""},
|
|
}
|
|
for _, c := range cases {
|
|
c.file = filepath.Join("../../fleetctl", c.file)
|
|
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
tokExpire := time.Now().Add(time.Hour)
|
|
token, err := test.CreateVPPTokenEncoded(tokExpire, "fleet", "ca")
|
|
require.NoError(t, err)
|
|
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam, _ map[string]uint) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return []fleet.VPPAppResponse{}, nil
|
|
}
|
|
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
|
return nil
|
|
}
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{
|
|
ID: 1,
|
|
OrgName: "Fleet",
|
|
Location: "Earth",
|
|
RenewDate: tokExpire,
|
|
Token: string(token),
|
|
Teams: nil,
|
|
}, nil
|
|
}
|
|
|
|
ds.GetLabelSpecsFunc = func(ctx context.Context, filter fleet.TeamFilter) ([]*fleet.LabelSpec, error) {
|
|
return []*fleet.LabelSpec{
|
|
{
|
|
Name: "a",
|
|
Description: "A global label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
|
Hosts: []string{"host2", "host3"},
|
|
},
|
|
{
|
|
Name: "b",
|
|
Description: "Another label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
|
Query: "SELECT 1 from osquery_info",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
labelToIDs := map[string]uint{
|
|
fleet.BuiltinLabelMacOS14Plus: 1,
|
|
"a": 2,
|
|
"b": 3,
|
|
}
|
|
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
|
|
// for this test, recognize labels a and b (as well as the built-in macos 14+ one)
|
|
ret := make(map[string]uint)
|
|
for _, lbl := range names {
|
|
id, ok := labelToIDs[lbl]
|
|
if ok {
|
|
ret[lbl] = id
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
|
|
return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
|
|
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
return nil, nil
|
|
}
|
|
ds.InsertOrReplaceMDMConfigAssetFunc = func(ctx context.Context, asset fleet.MDMConfigAsset) error {
|
|
return nil
|
|
}
|
|
ds.HardDeleteMDMConfigAssetFunc = func(ctx context.Context, assetName fleet.MDMAssetName) error {
|
|
return nil
|
|
}
|
|
|
|
_, err = fleetctl.RunAppNoChecks([]string{"gitops", "-f", c.file})
|
|
if c.wantErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitOpsTeamSoftwareInstallersQueryEnv(t *testing.T) {
|
|
testing_utils.StartSoftwareInstallerServer(t)
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
|
|
t.Setenv("QUERY_VAR", "IT_WORKS")
|
|
|
|
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
|
if len(installers) != 0 && installers[0].PreInstallQuery != "select IT_WORKS" {
|
|
return fmt.Errorf("Missing env var, got %s", installers[0].PreInstallQuery)
|
|
}
|
|
return nil
|
|
}
|
|
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
|
return nil
|
|
}
|
|
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
|
|
return nil, nil
|
|
}
|
|
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
|
|
return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
|
|
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
_, err := fleetctl.RunAppNoChecks([]string{"gitops", "-f", "../../fleetctl/testdata/gitops/team_software_installer_valid_env_query.yml"})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestGitOpsNoTeamVPPPolicies(t *testing.T) {
|
|
testing_utils.StartAndServeVPPServer(t)
|
|
|
|
cases := []struct {
|
|
noTeamFile string
|
|
wantErr string
|
|
vppApps []fleet.VPPAppResponse
|
|
}{
|
|
{
|
|
noTeamFile: "testdata/gitops/subdir/no_team_vpp_policies_valid.yml",
|
|
vppApps: []fleet.VPPAppResponse{
|
|
{ // for more test coverage
|
|
Platform: fleet.MacOSPlatform,
|
|
},
|
|
{ // for more test coverage
|
|
TitleID: ptr.Uint(122),
|
|
Platform: fleet.MacOSPlatform,
|
|
},
|
|
{
|
|
TeamID: ptr.Uint(0),
|
|
TitleID: ptr.Uint(123),
|
|
AppStoreID: "1",
|
|
Platform: fleet.IOSPlatform,
|
|
},
|
|
{
|
|
TeamID: ptr.Uint(0),
|
|
TitleID: ptr.Uint(124),
|
|
AppStoreID: "1",
|
|
Platform: fleet.MacOSPlatform,
|
|
},
|
|
{
|
|
TeamID: ptr.Uint(0),
|
|
TitleID: ptr.Uint(125),
|
|
AppStoreID: "1",
|
|
Platform: fleet.IPadOSPlatform,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
c.noTeamFile = filepath.Join("../../fleetctl", c.noTeamFile)
|
|
t.Run(filepath.Base(c.noTeamFile), func(t *testing.T) {
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
tokExpire := time.Now().Add(time.Hour)
|
|
token, err := test.CreateVPPTokenEncoded(tokExpire, "fleet", "ca")
|
|
require.NoError(t, err)
|
|
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam, _ map[string]uint) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
|
return nil
|
|
}
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return c.vppApps, nil
|
|
}
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{
|
|
ID: 1,
|
|
OrgName: "Fleet",
|
|
Location: "Earth",
|
|
RenewDate: tokExpire,
|
|
Token: string(token),
|
|
Teams: nil,
|
|
}, nil
|
|
}
|
|
labelToIDs := map[string]uint{
|
|
fleet.BuiltinLabelMacOS14Plus: 1,
|
|
"a": 2,
|
|
"b": 3,
|
|
}
|
|
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
|
|
// for this test, recognize labels a and b (as well as the built-in macos 14+ one)
|
|
ret := make(map[string]uint)
|
|
for _, lbl := range names {
|
|
id, ok := labelToIDs[lbl]
|
|
if ok {
|
|
ret[lbl] = id
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
ds.LabelsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]*fleet.Label, error) {
|
|
return map[string]*fleet.Label{
|
|
"a": {
|
|
ID: 1,
|
|
Name: "a",
|
|
},
|
|
"b": {
|
|
ID: 2,
|
|
Name: "b",
|
|
},
|
|
}, nil
|
|
}
|
|
ds.SetAsideLabelsFunc = func(ctx context.Context, notOnTeamID *uint, names []string, user fleet.User) error {
|
|
return nil
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
ds.InsertOrReplaceMDMConfigAssetFunc = func(ctx context.Context, asset fleet.MDMConfigAsset) error {
|
|
return nil
|
|
}
|
|
ds.HardDeleteMDMConfigAssetFunc = func(ctx context.Context, assetName fleet.MDMAssetName) error {
|
|
return nil
|
|
}
|
|
ds.TeamLiteFunc = func(ctx context.Context, id uint) (*fleet.TeamLite, error) {
|
|
return &fleet.TeamLite{}, nil
|
|
}
|
|
|
|
t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
|
|
globalFile := "../../fleetctl/testdata/gitops/global_config_no_paths.yml"
|
|
dstPath := filepath.Join(filepath.Dir(c.noTeamFile), "no-team.yml")
|
|
t.Cleanup(func() {
|
|
os.Remove(dstPath)
|
|
})
|
|
err = file.Copy(c.noTeamFile, dstPath, 0o755)
|
|
require.NoError(t, err)
|
|
_, err = fleetctl.RunAppNoChecks([]string{"gitops", "-f", globalFile, "-f", dstPath})
|
|
if c.wantErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
|
|
testing_utils.StartSoftwareInstallerServer(t)
|
|
testing_utils.StartAndServeVPPServer(t)
|
|
|
|
cases := []struct {
|
|
noTeamFile string
|
|
wantErr string
|
|
}{
|
|
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
|
|
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .zip, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1."},
|
|
{"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 513MiB"},
|
|
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
|
|
{"testdata/gitops/no_team_software_installer_subdir.yml", ""},
|
|
{"testdata/gitops/subdir/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_uninstall_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", "at least one of hash_sha256 or url is required for each software package"},
|
|
{
|
|
"testdata/gitops/no_team_software_installer_invalid_self_service_value.yml",
|
|
"Couldn't edit \"../../fleetctl/testdata/gitops/no-team.yml\" at \"software.packages.self_service\", expected type bool but got string",
|
|
},
|
|
{
|
|
"testdata/gitops/no_team_software_installer_invalid_both_include_exclude.yml",
|
|
`only one of "labels_include_all", "labels_exclude_any" or "labels_include_any" can be specified`,
|
|
},
|
|
{"testdata/gitops/no_team_software_installer_valid_include.yml", ""},
|
|
{"testdata/gitops/no_team_software_installer_valid_exclude.yml", ""},
|
|
{"testdata/gitops/no_team_software_installer_valid_include_all.yml", ""},
|
|
{
|
|
"testdata/gitops/no_team_software_installer_invalid_unknown_label.yml",
|
|
"Please create the missing labels, or update your settings to not refer to these labels.",
|
|
},
|
|
// No team tests for setup experience software/script
|
|
{"testdata/gitops/no_team_setup_software_valid.yml", ""},
|
|
{"testdata/gitops/no_team_setup_software_invalid_script.yml", "no_such_script.sh: no such file"},
|
|
{"testdata/gitops/no_team_setup_software_invalid_software_package.yml", "no_such_software.yml\" does not exist for that fleet"},
|
|
{"testdata/gitops/no_team_setup_software_invalid_vpp_app.yml", "\"no_such_app\" does not exist for that fleet"},
|
|
{"testdata/gitops/no_team_software_installer_valid_ipa.yml", ""},
|
|
{"testdata/gitops/no_team_software_installer_subdir_ipa.yml", ""},
|
|
}
|
|
for _, c := range cases {
|
|
c.noTeamFile = filepath.Join("../../fleetctl", c.noTeamFile)
|
|
t.Run(filepath.Base(c.noTeamFile), func(t *testing.T) {
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
tokExpire := time.Now().Add(time.Hour)
|
|
token, err := test.CreateVPPTokenEncoded(tokExpire, "fleet", "ca")
|
|
require.NoError(t, err)
|
|
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam, _ map[string]uint) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
|
return nil
|
|
}
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return []fleet.VPPAppResponse{}, nil
|
|
}
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{
|
|
ID: 1,
|
|
OrgName: "Fleet",
|
|
Location: "Earth",
|
|
RenewDate: tokExpire,
|
|
Token: string(token),
|
|
Teams: nil,
|
|
}, nil
|
|
}
|
|
ds.GetLabelSpecsFunc = func(ctx context.Context, filter fleet.TeamFilter) ([]*fleet.LabelSpec, error) {
|
|
return []*fleet.LabelSpec{
|
|
{
|
|
Name: "a",
|
|
Description: "A global label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
|
Hosts: []string{"host2", "host3"},
|
|
},
|
|
{
|
|
Name: "b",
|
|
Description: "Another label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
|
Query: "SELECT 1 from osquery_info",
|
|
},
|
|
}, nil
|
|
}
|
|
ds.SetAsideLabelsFunc = func(ctx context.Context, notOnTeamID *uint, names []string, user fleet.User) error {
|
|
return nil
|
|
}
|
|
labelToIDs := map[string]uint{
|
|
fleet.BuiltinLabelMacOS14Plus: 1,
|
|
"a": 2,
|
|
"b": 3,
|
|
}
|
|
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
|
|
// for this test, recognize labels a and b (as well as the built-in macos 14+ one)
|
|
ret := make(map[string]uint)
|
|
for _, lbl := range names {
|
|
id, ok := labelToIDs[lbl]
|
|
if ok {
|
|
ret[lbl] = id
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
|
|
return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
ds.InsertOrReplaceMDMConfigAssetFunc = func(ctx context.Context, asset fleet.MDMConfigAsset) error {
|
|
return nil
|
|
}
|
|
ds.HardDeleteMDMConfigAssetFunc = func(ctx context.Context, assetName fleet.MDMAssetName) error {
|
|
return nil
|
|
}
|
|
ds.TeamLiteFunc = func(ctx context.Context, id uint) (*fleet.TeamLite, error) {
|
|
return &fleet.TeamLite{}, nil
|
|
}
|
|
|
|
t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
|
|
globalFile := "../../fleetctl/testdata/gitops/global_config_no_paths.yml"
|
|
if strings.HasPrefix(filepath.Base(c.noTeamFile), "no_team_setup_software") {
|
|
// the controls section is in the no-team test file, so use a global file without that section
|
|
globalFile = "../../fleetctl/testdata/gitops/global_config_no_paths_no_controls.yml"
|
|
}
|
|
dstPath := filepath.Join(filepath.Dir(c.noTeamFile), "no-team.yml")
|
|
t.Cleanup(func() {
|
|
os.Remove(dstPath)
|
|
})
|
|
err = file.Copy(c.noTeamFile, dstPath, 0o755)
|
|
require.NoError(t, err)
|
|
_, err = fleetctl.RunAppNoChecks([]string{"gitops", "-f", globalFile, "-f", dstPath})
|
|
if c.wantErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitOpsTeamVPPApps(t *testing.T) {
|
|
testing_utils.StartAndServeVPPServer(t)
|
|
|
|
cases := []struct {
|
|
file string
|
|
wantErr string
|
|
tokenExpiration time.Time
|
|
expectedLabels map[string]uint
|
|
}{
|
|
{"testdata/gitops/team_vpp_valid_app.yml", "", time.Now().Add(24 * time.Hour), map[string]uint{}},
|
|
{"testdata/gitops/team_vpp_valid_app_self_service.yml", "", time.Now().Add(24 * time.Hour), map[string]uint{}},
|
|
{"testdata/gitops/team_vpp_valid_empty.yml", "", time.Now().Add(24 * time.Hour), map[string]uint{}},
|
|
{"testdata/gitops/team_vpp_valid_empty.yml", "", time.Now().Add(-24 * time.Hour), map[string]uint{}},
|
|
{"testdata/gitops/team_vpp_valid_app.yml", "VPP token expired", time.Now().Add(-24 * time.Hour), map[string]uint{}},
|
|
{"testdata/gitops/team_vpp_invalid_app.yml", "app not available on vpp account", time.Now().Add(24 * time.Hour), map[string]uint{}},
|
|
{
|
|
"testdata/gitops/team_vpp_incorrect_type.yml", "Couldn't edit \"../../fleetctl/testdata/gitops/team_vpp_incorrect_type.yml\" at \"software.app_store_apps.app_store_id\", expected type string but got number",
|
|
time.Now().Add(24 * time.Hour),
|
|
map[string]uint{},
|
|
},
|
|
{"testdata/gitops/team_vpp_empty_adamid.yml", "software app store id required", time.Now().Add(24 * time.Hour), map[string]uint{}},
|
|
{
|
|
"testdata/gitops/team_vpp_valid_app_labels_exclude_any.yml", "", time.Now().Add(24 * time.Hour),
|
|
map[string]uint{"label 1": 1, "label 2": 2},
|
|
},
|
|
{
|
|
"testdata/gitops/team_vpp_valid_app_labels_include_any.yml", "", time.Now().Add(24 * time.Hour),
|
|
map[string]uint{"label 1": 1, "label 2": 2},
|
|
},
|
|
{
|
|
"testdata/gitops/team_vpp_valid_app_labels_include_all.yml", "", time.Now().Add(24 * time.Hour),
|
|
map[string]uint{"label 1": 1, "label 2": 2},
|
|
},
|
|
{
|
|
"testdata/gitops/team_vpp_invalid_app_labels_exclude_any.yml",
|
|
"Please create the missing labels, or update your settings to not refer to these labels.", time.Now().Add(24 * time.Hour),
|
|
map[string]uint{"label 1": 1, "label 2": 2},
|
|
},
|
|
{
|
|
"testdata/gitops/team_vpp_invalid_app_labels_include_any.yml",
|
|
"Please create the missing labels, or update your settings to not refer to these labels.", time.Now().Add(24 * time.Hour),
|
|
map[string]uint{"label 1": 1, "label 2": 2},
|
|
},
|
|
{
|
|
"testdata/gitops/team_vpp_invalid_app_labels_both.yml",
|
|
`only one of "labels_include_all", "labels_exclude_any" or "labels_include_any" can be specified for app store app`, time.Now().Add(24 * time.Hour),
|
|
map[string]uint{},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
c.file = filepath.Join("../../fleetctl", c.file)
|
|
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
token, err := test.CreateVPPTokenEncoded(c.tokenExpiration, "fleet", "ca")
|
|
require.NoError(t, err)
|
|
|
|
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam, _ map[string]uint) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
|
return nil
|
|
}
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return []fleet.VPPAppResponse{}, nil
|
|
}
|
|
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
return &fleet.VPPTokenDB{
|
|
ID: 1,
|
|
OrgName: "Fleet",
|
|
Location: "Earth",
|
|
RenewDate: c.tokenExpiration,
|
|
Token: string(token),
|
|
Teams: nil,
|
|
}, nil
|
|
}
|
|
|
|
ds.GetLabelSpecsFunc = func(ctx context.Context, filter fleet.TeamFilter) ([]*fleet.LabelSpec, error) {
|
|
return []*fleet.LabelSpec{
|
|
{
|
|
Name: "label 1",
|
|
Description: "A global label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
|
Hosts: []string{"host2", "host3"},
|
|
},
|
|
{
|
|
Name: "label 2",
|
|
Description: "Another label",
|
|
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
|
Query: "SELECT 1 from osquery_info",
|
|
},
|
|
}, nil
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
|
|
found := make(map[string]uint)
|
|
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
|
|
for _, l := range names {
|
|
if id, ok := c.expectedLabels[l]; ok {
|
|
found[l] = id
|
|
}
|
|
}
|
|
return found, nil
|
|
}
|
|
ds.LabelsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]*fleet.Label, error) {
|
|
found2 := make(map[string]*fleet.Label)
|
|
for _, l := range names {
|
|
if id, ok := c.expectedLabels[l]; ok {
|
|
found2[l] = &fleet.Label{
|
|
ID: id,
|
|
Name: l,
|
|
}
|
|
}
|
|
}
|
|
return found2, nil
|
|
}
|
|
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
return nil, nil
|
|
}
|
|
ds.InsertOrReplaceMDMConfigAssetFunc = func(ctx context.Context, asset fleet.MDMConfigAsset) error {
|
|
return nil
|
|
}
|
|
ds.HardDeleteMDMConfigAssetFunc = func(ctx context.Context, assetName fleet.MDMAssetName) error {
|
|
return nil
|
|
}
|
|
|
|
_, err = fleetctl.RunAppNoChecks([]string{"gitops", "-f", c.file})
|
|
|
|
if c.wantErr == "" {
|
|
require.NoError(t, err)
|
|
if len(c.expectedLabels) > 0 {
|
|
require.True(t, ds.LabelIDsByNameFuncInvoked)
|
|
require.True(t, ds.LabelsByNameFuncInvoked)
|
|
}
|
|
|
|
require.Equal(t, c.expectedLabels, found)
|
|
} else {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGitOpsTeamVPPAndApp tests the flow where a new team is created with VPP apps.
|
|
// GitOps must first create the team, then assign VPP token to it, and only then add VPP apps.
|
|
func TestGitOpsTeamVPPAndApp(t *testing.T) {
|
|
testing_utils.StartAndServeVPPServer(t)
|
|
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
renewDate := time.Now().Add(24 * time.Hour)
|
|
token, err := test.CreateVPPTokenEncoded(renewDate, "fleet", "ca")
|
|
require.NoError(t, err)
|
|
|
|
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
|
|
return []fleet.VPPAppResponse{}, nil
|
|
}
|
|
ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// The following mocks are key to this test.
|
|
vppToken := &fleet.VPPTokenDB{
|
|
ID: 1,
|
|
OrgName: "Fleet",
|
|
Location: "Earth",
|
|
RenewDate: renewDate,
|
|
Token: string(token),
|
|
Teams: nil,
|
|
}
|
|
tokensByTeams := make(map[uint]*fleet.VPPTokenDB)
|
|
ds.UpdateVPPTokenTeamsFunc = func(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) {
|
|
for _, teamID := range teams {
|
|
tokensByTeams[teamID] = vppToken
|
|
}
|
|
return vppToken, nil
|
|
}
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return []*fleet.VPPTokenDB{vppToken}, nil
|
|
}
|
|
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
|
|
if teamID == nil {
|
|
return vppToken, nil
|
|
}
|
|
token, ok := tokensByTeams[*teamID]
|
|
if !ok {
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
return token, nil
|
|
}
|
|
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
|
|
return []uint{}, nil
|
|
}
|
|
ds.InsertOrReplaceMDMConfigAssetFunc = func(ctx context.Context, asset fleet.MDMConfigAsset) error {
|
|
return nil
|
|
}
|
|
ds.HardDeleteMDMConfigAssetFunc = func(ctx context.Context, assetName fleet.MDMAssetName) error {
|
|
return nil
|
|
}
|
|
ds.TeamLiteFunc = func(ctx context.Context, id uint) (*fleet.TeamLite, error) {
|
|
return &fleet.TeamLite{}, nil
|
|
}
|
|
|
|
buf, err := fleetctl.RunAppNoChecks([]string{
|
|
"gitops", "-f", "../../fleetctl/testdata/gitops/global_config_vpp.yml", "-f",
|
|
"../../fleetctl/testdata/gitops/team_vpp_valid_app.yml",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, ds.UpdateVPPTokenTeamsFuncInvoked)
|
|
assert.True(t, ds.GetVPPTokenByTeamIDFuncInvoked)
|
|
assert.True(t, ds.SetTeamVPPAppsFuncInvoked)
|
|
assert.Contains(t, buf.String(), fmt.Sprintf(fleetctl.ReapplyingTeamForVPPAppsMsg, teamName))
|
|
}
|
|
|
|
func TestGitOpsVPP(t *testing.T) {
|
|
global := func(mdm string) string {
|
|
return fmt.Sprintf(`
|
|
controls:
|
|
queries:
|
|
policies:
|
|
agent_options:
|
|
software:
|
|
org_settings:
|
|
server_settings:
|
|
server_url: "https://foo.example.com"
|
|
org_info:
|
|
org_name: GitOps Test
|
|
secrets:
|
|
- secret: "global"
|
|
mdm:
|
|
%s
|
|
`, mdm)
|
|
}
|
|
|
|
team := func(name string) string {
|
|
return fmt.Sprintf(`
|
|
name: %s
|
|
team_settings:
|
|
secrets:
|
|
- secret: "%s-secret"
|
|
agent_options:
|
|
controls:
|
|
policies:
|
|
queries:
|
|
software:
|
|
`, name, name)
|
|
}
|
|
|
|
workstations := team("💻 Workstations")
|
|
iosTeam := team("📱🏢 Company-owned iPhones")
|
|
ipadTeam := team("🔳🏢 Company-owned iPads")
|
|
|
|
cases := []struct {
|
|
name string
|
|
cfgs []string
|
|
dryRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
|
|
realRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
|
|
}{
|
|
{
|
|
name: "new key all valid",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Fleet Device Management Inc.
|
|
teams:
|
|
- "💻 Workstations"
|
|
- "📱🏢 Company-owned iPhones"
|
|
- "🔳🏢 Company-owned iPads"`),
|
|
workstations,
|
|
iosTeam,
|
|
ipadTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.Contains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(
|
|
t,
|
|
appCfg.MDM.VolumePurchasingProgram.Value,
|
|
[]fleet.MDMAppleVolumePurchasingProgramInfo{
|
|
{
|
|
Location: "Fleet Device Management Inc.",
|
|
Teams: []string{
|
|
"💻 Workstations",
|
|
"📱🏢 Company-owned iPhones",
|
|
"🔳🏢 Company-owned iPads",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
assert.Contains(t, out, "[!] gitops succeeded")
|
|
},
|
|
},
|
|
{
|
|
name: "new key multiple elements",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Acme Inc.
|
|
teams:
|
|
- "💻 Workstations"
|
|
- location: Fleet Device Management Inc.
|
|
teams:
|
|
- "📱🏢 Company-owned iPhones"
|
|
- "🔳🏢 Company-owned iPads"`),
|
|
workstations,
|
|
iosTeam,
|
|
ipadTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.Contains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(
|
|
t,
|
|
appCfg.MDM.VolumePurchasingProgram.Value,
|
|
[]fleet.MDMAppleVolumePurchasingProgramInfo{
|
|
{
|
|
Location: "Acme Inc.",
|
|
Teams: []string{
|
|
"💻 Workstations",
|
|
},
|
|
},
|
|
{
|
|
Location: "Fleet Device Management Inc.",
|
|
Teams: []string{
|
|
"📱🏢 Company-owned iPhones",
|
|
"🔳🏢 Company-owned iPads",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
assert.Contains(t, out, "[!] gitops succeeded")
|
|
},
|
|
},
|
|
{
|
|
name: "using an undefined team errors",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Fleet Device Management Inc.
|
|
teams:
|
|
- "💻 Workstations"
|
|
- "📱🏢 Company-owned iPhones"
|
|
- "🔳🏢 Company-owned iPads"`),
|
|
workstations,
|
|
ipadTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.ErrorContains(t, err, "volume_purchasing_program team 📱🏢 Company-owned iPhones not found in team configs")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.ErrorContains(t, err, "volume_purchasing_program team 📱🏢 Company-owned iPhones not found in team configs")
|
|
},
|
|
},
|
|
{
|
|
name: "no team is supported",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Fleet Device Management Inc.
|
|
teams:
|
|
- "💻 Workstations"
|
|
- "📱🏢 Company-owned iPhones"
|
|
- "No team"`),
|
|
workstations,
|
|
iosTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.Contains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(
|
|
t,
|
|
appCfg.MDM.VolumePurchasingProgram.Value,
|
|
[]fleet.MDMAppleVolumePurchasingProgramInfo{
|
|
{
|
|
Location: "Fleet Device Management Inc.",
|
|
Teams: []string{
|
|
"💻 Workstations",
|
|
"📱🏢 Company-owned iPhones",
|
|
"No team",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
assert.Contains(t, out, "[!] gitops succeeded")
|
|
},
|
|
},
|
|
{
|
|
name: "all teams is supported",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Fleet Device Management Inc.
|
|
teams:
|
|
- "All teams"`),
|
|
workstations,
|
|
iosTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.Contains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(
|
|
t,
|
|
appCfg.MDM.VolumePurchasingProgram.Value,
|
|
[]fleet.MDMAppleVolumePurchasingProgramInfo{
|
|
{
|
|
Location: "Fleet Device Management Inc.",
|
|
Teams: []string{
|
|
"All teams",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
assert.Contains(t, out, "[!] gitops succeeded")
|
|
},
|
|
},
|
|
{
|
|
name: "not provided teams defaults to no team",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Fleet Device Management Inc.
|
|
teams:`),
|
|
workstations,
|
|
ipadTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.Contains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(
|
|
t,
|
|
appCfg.MDM.VolumePurchasingProgram.Value,
|
|
[]fleet.MDMAppleVolumePurchasingProgramInfo{
|
|
{
|
|
Location: "Fleet Device Management Inc.",
|
|
Teams: nil,
|
|
},
|
|
},
|
|
)
|
|
assert.Contains(t, out, "[!] gitops succeeded")
|
|
},
|
|
},
|
|
{
|
|
name: "non existent location fails",
|
|
cfgs: []string{
|
|
global(`
|
|
volume_purchasing_program:
|
|
- location: Does not exist
|
|
teams:`),
|
|
workstations,
|
|
ipadTeam,
|
|
},
|
|
dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.ErrorContains(t, err, "token with location Does not exist doesn't exist")
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.NotContains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
|
|
assert.ErrorContains(t, err, "token with location Does not exist doesn't exist")
|
|
assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
|
|
assert.NotContains(t, out, "[!] gitops dry run succeeded")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ds, savedAppConfigPtr, savedTeams := testing_utils.SetupFullGitOpsPremiumServer(t)
|
|
// No existing labels — this test doesn't test label behavior.
|
|
ds.GetLabelSpecsFunc = func(ctx context.Context, filter fleet.TeamFilter) ([]*fleet.LabelSpec, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
|
return []*fleet.VPPTokenDB{{Location: "Fleet Device Management Inc."}, {Location: "Acme Inc."}}, nil
|
|
}
|
|
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Foo Inc."}}, nil
|
|
}
|
|
ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
|
|
return 1, nil
|
|
}
|
|
|
|
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
|
|
var res []*fleet.TeamSummary
|
|
for _, tm := range savedTeams {
|
|
res = append(res, &fleet.TeamSummary{Name: (*tm).Name, ID: (*tm).ID})
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
|
|
return nil
|
|
}
|
|
|
|
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
|
|
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
|
|
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
|
|
return nil, nil
|
|
}
|
|
ds.TeamLiteFunc = func(ctx context.Context, id uint) (*fleet.TeamLite, error) {
|
|
return &fleet.TeamLite{}, nil
|
|
}
|
|
|
|
args := []string{"gitops"}
|
|
for _, cfg := range tt.cfgs {
|
|
if cfg != "" {
|
|
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
|
require.NoError(t, err)
|
|
_, err = tmpFile.WriteString(cfg)
|
|
require.NoError(t, err)
|
|
args = append(args, "-f", tmpFile.Name())
|
|
}
|
|
}
|
|
|
|
// Dry run
|
|
out, err := fleetctl.RunAppNoChecks(append(args, "--dry-run"))
|
|
tt.dryRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
|
|
if t.Failed() {
|
|
t.FailNow()
|
|
}
|
|
|
|
// Real run
|
|
out, err = fleetctl.RunAppNoChecks(args)
|
|
tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
|
|
|
|
// Second real run, now that all the teams are saved
|
|
out, err = fleetctl.RunAppNoChecks(args)
|
|
tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
|
|
})
|
|
}
|
|
}
|