mirror of
https://github.com/fleetdm/fleet
synced 2026-05-21 16:08:47 +00:00
For #24473 # Checklist for submitter <!-- Note that API documentation changes are now addressed by the product design 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. ## Details This PR adds the ability to manage labels via GitOps. Usage is as follows: * If a top-level `labels:` key is provided in the global YAML file provided to GitOps, then any labels in this list will be created (if using a new name) or updated (if using an existing name). * If no top-level `labels:` key is provided, no changes will be made to labels. This allows backwards-compatibility; customers won't blow away all of their labels if they don't immediately use `labels:` in their YAML Additionally, some new validation has been added so that label usage is checked prior to application. This means that when the gitops command is run, it will verify that any labels referenced elsewhere in the YAML (e.g. by software installers or mdm profiles) exist, and will bail with an error message if they don't. ## Testing **Test label deletion** 1. Add some labels via the UI 2. Run `fleetctl gitops --dry-run` with a default.yml file _without_ `labels:` in it, and verify that it doesn't say it will update or delete any labels 2. Run `fleetctl gitops` with a default.yml file _without_ `labels:` in it, and verify that it doesn't modify or remove your labels 4. Run `fleetctl gitops --dry-run` with a default.yml file with `labels:` in it and nothing underneath, and verify that it says that it will delete your labels 4. Run `fleetctl gitops` with a default.yml file with `labels:` in it and nothing underneath, and verify that it removes all your labels **Test label create/update** 1. Add a label "foo" via the UI 2. Run `fleetctl gitops --dry-run` with a default.yml file with two `labels:` in it, one named "foo" and one named "bar". Verify that the output says that one label will be created and one will be updated. 2. Run `fleetctl gitops` with a default.yml file with two `labels:` in it, one named "foo" and one named "bar". Verify that the two labels now exist in the UI with the configuration you specified. **Test label usage** 1. Add a label "foo" in the UI. 1. Run `fleetctl gitops --dry-run` with a default.yml file _without_ `labels:` in it, where a software installer or mdm profile uses the "foo" label via `labels_include_any`. Verify that the output doesn't complain about unknown labels. 1. Run `fleetctl gitops --dry-run` with a default.yml file _with_ `labels:` in it with nothing underneath, and a software installer or mdm profile uses the "foo" label via `labels_include_any`. Verify that the output complains about unknown label "foo" 1. Run `fleetctl gitops --dry-run` with a default.yml file _with_ `labels:` in it with a "foo" label defined underneath, and a software installer or mdm profile uses the "foo" label via `labels_include_any`. Verify that the output doesn't complain about unknown labels.
631 lines
20 KiB
Go
631 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/spec"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/service"
|
|
"github.com/urfave/cli/v2"
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
const filenameMaxLength = 255
|
|
|
|
type LabelUsage struct {
|
|
Name string
|
|
Type string
|
|
}
|
|
|
|
func gitopsCommand() *cli.Command {
|
|
var (
|
|
flFilenames cli.StringSlice
|
|
flDryRun bool
|
|
flDeleteOtherTeams bool
|
|
)
|
|
return &cli.Command{
|
|
Name: "gitops",
|
|
Usage: "Synchronize Fleet configuration with provided file. This command is intended to be used in a GitOps workflow.",
|
|
UsageText: `fleetctl gitops [options]`,
|
|
Flags: []cli.Flag{
|
|
&cli.StringSliceFlag{
|
|
Name: "f",
|
|
Required: true,
|
|
EnvVars: []string{"FILENAME"},
|
|
Destination: &flFilenames,
|
|
Usage: "The file(s) with the GitOps configuration.",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "delete-other-teams",
|
|
EnvVars: []string{"DELETE_OTHER_TEAMS"},
|
|
Destination: &flDeleteOtherTeams,
|
|
Usage: "Delete other teams not present in the GitOps configuration",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "dry-run",
|
|
EnvVars: []string{"DRY_RUN"},
|
|
Destination: &flDryRun,
|
|
Usage: "Do not apply the file(s), just validate",
|
|
},
|
|
configFlag(),
|
|
contextFlag(),
|
|
debugFlag(),
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
totalFilenames := len(flFilenames.Value())
|
|
if totalFilenames == 0 {
|
|
return errors.New("-f must be specified")
|
|
}
|
|
for _, flFilename := range flFilenames.Value() {
|
|
if strings.TrimSpace(flFilename) == "" {
|
|
return errors.New("file name cannot be empty")
|
|
}
|
|
if len(filepath.Base(flFilename)) > filenameMaxLength {
|
|
return fmt.Errorf("file name must be less than %d characters: %s", filenameMaxLength, filepath.Base(flFilename))
|
|
}
|
|
}
|
|
|
|
// Check license
|
|
fleetClient, err := clientFromCLI(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
appConfig, err := fleetClient.GetAppConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if appConfig.License == nil {
|
|
return errors.New("no license struct found in app config")
|
|
}
|
|
logf := func(format string, a ...interface{}) {
|
|
_, _ = fmt.Fprintf(c.App.Writer, format, a...)
|
|
}
|
|
|
|
// We need the controls from no-team.yml to apply them when applying the global app config.
|
|
noTeamControls, noTeamPresent, err := extractControlsForNoTeam(flFilenames, appConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("extracting controls from no-team.yml: %w", err)
|
|
}
|
|
|
|
var originalABMConfig []any
|
|
var originalVPPConfig []any
|
|
var teamNames []string
|
|
var teamDryRunAssumptions *fleet.TeamSpecsDryRunAssumptions
|
|
var abmTeams, vppTeams []string
|
|
var hasMissingABMTeam, hasMissingVPPTeam, usesLegacyABMConfig bool
|
|
|
|
// we keep track of team software installers and scripts for correct policy application
|
|
teamsSoftwareInstallers := make(map[string][]fleet.SoftwarePackageResponse)
|
|
teamsVPPApps := make(map[string][]fleet.VPPAppResponse)
|
|
teamsScripts := make(map[string][]fleet.ScriptResponse)
|
|
|
|
// We keep track of the secrets to check if duplicates exist during dry run
|
|
secrets := make(map[string]struct{})
|
|
// We keep track of the environment FLEET_SECRET_* variables
|
|
allFleetSecrets := make(map[string]string)
|
|
// Keep track of which labels we'd have after this gitops run.
|
|
var proposedLabelNames []string
|
|
|
|
// Parsed config and filename pair
|
|
type ConfigFile struct {
|
|
Config *spec.GitOps
|
|
Filename string
|
|
IsGlobalConfig bool
|
|
}
|
|
|
|
// Load all configs in before processing them
|
|
configs := make([]ConfigFile, 0, len(flFilenames.Value()))
|
|
|
|
// We only want to have one global config loaded
|
|
globalConfigLoaded := false
|
|
|
|
for _, flFilename := range flFilenames.Value() {
|
|
baseDir := filepath.Dir(flFilename)
|
|
config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig, logf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
isGlobalConfig := config.TeamName == nil
|
|
if isGlobalConfig {
|
|
if globalConfigLoaded {
|
|
return errors.New("only one global config file may be provided to fleetctl gitops")
|
|
}
|
|
globalConfigLoaded = true
|
|
}
|
|
configFile := ConfigFile{Config: config, Filename: flFilename, IsGlobalConfig: isGlobalConfig}
|
|
if isGlobalConfig {
|
|
// If it's a global file, put it at the beginning
|
|
// of the array so it gets processed first
|
|
configs = append([]ConfigFile{configFile}, configs...)
|
|
} else {
|
|
configs = append(configs, configFile)
|
|
}
|
|
}
|
|
|
|
for _, configFile := range configs {
|
|
config := configFile.Config
|
|
flFilename := configFile.Filename
|
|
isGlobalConfig := configFile.IsGlobalConfig
|
|
|
|
if isGlobalConfig {
|
|
if noTeamControls.Set() && config.Controls.Set() {
|
|
return errors.New("'controls' cannot be set on both global config and on no-team.yml")
|
|
}
|
|
if !noTeamControls.Defined && !config.Controls.Defined {
|
|
if appConfig.License.IsPremium() {
|
|
return errors.New("'controls' must be set on global config or no-team.yml")
|
|
}
|
|
return errors.New("'controls' must be set on global config")
|
|
}
|
|
if !config.Controls.Set() {
|
|
config.Controls = noTeamControls
|
|
}
|
|
|
|
// If config.Labels is nil, it means we plan on deleting all existing labels.
|
|
if config.Labels == nil {
|
|
proposedLabelNames = make([]string, 0)
|
|
} else if len(config.Labels) > 0 {
|
|
// If config.Labels is populated, get the names it contains.
|
|
proposedLabelNames = make([]string, len(config.Labels))
|
|
for i, l := range config.Labels {
|
|
proposedLabelNames[i] = l.Name
|
|
}
|
|
}
|
|
} else if !appConfig.License.IsPremium() {
|
|
logf("[!] skipping team config %s since teams are only supported for premium Fleet users\n", flFilename)
|
|
continue
|
|
}
|
|
|
|
// If we haven't populated this list yet, it means we're either doing team-level GitOps only,
|
|
// or a global YAML was provided with no `labels:` key in it (meaning "keep existing labels").
|
|
// In either case we'll get the list of label names from the db so we can ensure that we're
|
|
// not attempting to apply non-existent labels to other entities.
|
|
if proposedLabelNames == nil {
|
|
proposedLabelNames = make([]string, 0)
|
|
persistedLabels, err := fleetClient.GetLabels()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, persistedLabel := range persistedLabels {
|
|
if persistedLabel.LabelType == fleet.LabelTypeRegular {
|
|
proposedLabelNames = append(proposedLabelNames, persistedLabel.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gather stats on where labels are used in the this gitops config,
|
|
// so we can bail if any of the referenced labels wouldn't exist
|
|
// after this run (either because they'd be deleted, never existed
|
|
// in the first place).
|
|
labelsUsed := getLabelUsage(config)
|
|
|
|
// Check if any used labels are not in the proposed labels list.
|
|
// If there are, we'll bail out with helpful error messages.
|
|
unknownLabelsUsed := false
|
|
for labelUsed := range labelsUsed {
|
|
if slices.Index(proposedLabelNames, labelUsed) == -1 {
|
|
for _, labelUser := range labelsUsed[labelUsed] {
|
|
logf("[!] Unknown label '%s' is referenced by %s '%s'\n", labelUsed, labelUser.Type, labelUser.Name)
|
|
}
|
|
unknownLabelsUsed = true
|
|
}
|
|
}
|
|
if unknownLabelsUsed {
|
|
return errors.New("Please create the missing labels, or update your settings to not refer to these labels.")
|
|
}
|
|
|
|
// Special handling for tokens is required because they link to teams (by
|
|
// name.) Because teams can be created/deleted during the same gitops run, we
|
|
// grab some information to help us determine allowed/restricted actions and
|
|
// when to perform the associations.
|
|
|
|
if isGlobalConfig && totalFilenames > 1 && !(totalFilenames == 2 && noTeamPresent) && appConfig.License.IsPremium() {
|
|
abmTeams, hasMissingABMTeam, usesLegacyABMConfig, err = checkABMTeamAssignments(config, fleetClient)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
vppTeams, hasMissingVPPTeam, err = checkVPPTeamAssignments(config, fleetClient)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// if one of the teams assigned to an ABM token doesn't exist yet, we need to
|
|
// submit the configs without the ABM default team set. We'll set those
|
|
// separately later when the teams are already created.
|
|
if hasMissingABMTeam {
|
|
if mdm, ok := config.OrgSettings["mdm"]; ok {
|
|
if mdmMap, ok := mdm.(map[string]any); ok {
|
|
if appleBM, ok := mdmMap["apple_business_manager"]; ok {
|
|
if bmSettings, ok := appleBM.([]any); ok {
|
|
originalABMConfig = bmSettings
|
|
}
|
|
}
|
|
|
|
// If team is not found, we need to remove the AppleBMDefaultTeam from
|
|
// the global config, and then apply it after teams are processed
|
|
mdmMap["apple_business_manager"] = nil
|
|
mdmMap["apple_bm_default_team"] = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
if hasMissingVPPTeam {
|
|
if mdm, ok := config.OrgSettings["mdm"]; ok {
|
|
if mdmMap, ok := mdm.(map[string]any); ok {
|
|
if vpp, ok := mdmMap["volume_purchasing_program"]; ok {
|
|
if vppSettings, ok := vpp.([]any); ok {
|
|
originalVPPConfig = vppSettings
|
|
}
|
|
}
|
|
|
|
// If team is not found, we need to remove the VPP config from
|
|
// the global config, and then apply it after teams are processed
|
|
mdmMap["volume_purchasing_program"] = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if flDryRun {
|
|
incomingSecrets := fleetClient.GetGitOpsSecrets(config)
|
|
for _, secret := range incomingSecrets {
|
|
if _, ok := secrets[secret]; ok {
|
|
return fmt.Errorf("duplicate enroll secret found in %s", flFilename)
|
|
}
|
|
secrets[secret] = struct{}{}
|
|
}
|
|
}
|
|
|
|
err = fleetClient.SaveEnvSecrets(allFleetSecrets, config.FleetSecrets, flDryRun)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
assumptions, err := fleetClient.DoGitOps(c.Context, config, flFilename, logf, flDryRun, teamDryRunAssumptions, appConfig,
|
|
teamsSoftwareInstallers, teamsVPPApps, teamsScripts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if config.TeamName != nil {
|
|
teamNames = append(teamNames, *config.TeamName)
|
|
} else {
|
|
teamDryRunAssumptions = assumptions
|
|
}
|
|
}
|
|
|
|
// if there were assignments to tokens, and some of the teams were missing at that time, submit a separate patch request to set them now.
|
|
if len(abmTeams) > 0 && hasMissingABMTeam {
|
|
if err = applyABMTokenAssignmentIfNeeded(c, teamNames, abmTeams, originalABMConfig, usesLegacyABMConfig, flDryRun,
|
|
fleetClient); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(vppTeams) > 0 && hasMissingVPPTeam {
|
|
if err = applyVPPTokenAssignmentIfNeeded(c, teamNames, vppTeams, originalVPPConfig, flDryRun, fleetClient); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if flDeleteOtherTeams && appConfig.License.IsPremium() { // skip team deletion for non-premium users
|
|
teams, err := fleetClient.ListTeams("")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, team := range teams {
|
|
if !slices.Contains(teamNames, team.Name) {
|
|
if slices.Contains(abmTeams, team.Name) {
|
|
if usesLegacyABMConfig {
|
|
return fmt.Errorf("apple_bm_default_team %s cannot be deleted", team.Name)
|
|
}
|
|
return fmt.Errorf("apple_business_manager team %s cannot be deleted", team.Name)
|
|
}
|
|
if slices.Contains(vppTeams, team.Name) {
|
|
return fmt.Errorf("volume_purchasing_program team %s cannot be deleted", team.Name)
|
|
}
|
|
if flDryRun {
|
|
_, _ = fmt.Fprintf(c.App.Writer, "[!] would delete team %s\n", team.Name)
|
|
} else {
|
|
_, _ = fmt.Fprintf(c.App.Writer, "[-] deleting team %s\n", team.Name)
|
|
if err := fleetClient.DeleteTeam(team.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if flDryRun {
|
|
_, _ = fmt.Fprintf(c.App.Writer, "[!] gitops dry run succeeded\n")
|
|
} else {
|
|
_, _ = fmt.Fprintf(c.App.Writer, "[!] gitops succeeded\n")
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// Merge sets of label names.
|
|
func concatLabels(labelArrays ...[]string) []string {
|
|
var result []string
|
|
for _, arr := range labelArrays {
|
|
result = append(result, arr...)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Given a set of referenced labels and info about who is using them, update a provided usage map.
|
|
func updateLabelUsage(labels []string, ident string, usageType string, currentUsage map[string][]LabelUsage) {
|
|
for _, label := range labels {
|
|
var usage []LabelUsage
|
|
if _, ok := currentUsage[label]; !ok {
|
|
currentUsage[label] = make([]LabelUsage, 0)
|
|
}
|
|
usage = currentUsage[label]
|
|
usage = append(usage, LabelUsage{
|
|
Name: ident,
|
|
Type: usageType,
|
|
})
|
|
currentUsage[label] = usage
|
|
}
|
|
}
|
|
|
|
// Create a map of label name -> who is using that label.
|
|
// This will be used to determine if any non-existent labels are being referenced.
|
|
func getLabelUsage(config *spec.GitOps) map[string][]LabelUsage {
|
|
result := make(map[string][]LabelUsage)
|
|
|
|
// Get profile label usage
|
|
for _, osSettingName := range []interface{}{config.Controls.MacOSSettings, config.Controls.WindowsSettings} {
|
|
if osSettings, ok := getCustomSettings(osSettingName); ok {
|
|
for _, setting := range osSettings {
|
|
labels := concatLabels(setting.LabelsIncludeAny, setting.LabelsIncludeAll, setting.LabelsExcludeAny)
|
|
updateLabelUsage(labels, setting.Path, "MDM Profile", result)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get software package installer label usage
|
|
for _, setting := range config.Software.Packages {
|
|
labels := concatLabels(setting.LabelsIncludeAny, setting.LabelsExcludeAny)
|
|
updateLabelUsage(labels, setting.URL, "Software Package", result)
|
|
}
|
|
|
|
// Get app store app installer label usage
|
|
for _, setting := range config.Software.AppStoreApps {
|
|
labels := concatLabels(setting.LabelsIncludeAny, setting.LabelsExcludeAny)
|
|
updateLabelUsage(labels, setting.AppStoreID, "App Store App", result)
|
|
}
|
|
|
|
// TODO -- Get query label usage
|
|
|
|
return result
|
|
}
|
|
|
|
func getCustomSettings(osSettings interface{}) ([]fleet.MDMProfileSpec, bool) {
|
|
if settingsMap, ok := osSettings.(fleet.WithMDMProfileSpecs); ok {
|
|
return settingsMap.GetMDMProfileSpecs(), true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func extractControlsForNoTeam(flFilenames cli.StringSlice, appConfig *fleet.EnrichedAppConfig) (spec.GitOpsControls, bool, error) {
|
|
for _, flFilename := range flFilenames.Value() {
|
|
if filepath.Base(flFilename) == "no-team.yml" {
|
|
if !appConfig.License.IsPremium() {
|
|
// Message is printed in the next flFilenames loop to avoid printing it multiple times
|
|
break
|
|
}
|
|
baseDir := filepath.Dir(flFilename)
|
|
config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig, func(format string, a ...interface{}) {})
|
|
if err != nil {
|
|
return spec.GitOpsControls{}, false, err
|
|
}
|
|
return config.Controls, true, nil
|
|
}
|
|
}
|
|
return spec.GitOpsControls{}, false, nil
|
|
}
|
|
|
|
// checkABMTeamAssignments validates the spec, and finds if:
|
|
//
|
|
// 1. The user is using the legacy apple_bm_default_team config.
|
|
// 2. All teams assigned to ABM tokens already exist.
|
|
// 3. Performs validations according to the spec for both the new and the
|
|
// deprecated key used for this setting.
|
|
func checkABMTeamAssignments(config *spec.GitOps, fleetClient *service.Client) (
|
|
abmTeams []string, missingTeam bool, usesLegacyConfig bool, err error,
|
|
) {
|
|
if mdm, ok := config.OrgSettings["mdm"]; ok {
|
|
if mdmMap, ok := mdm.(map[string]any); ok {
|
|
appleBMDT, hasLegacyConfig := mdmMap["apple_bm_default_team"]
|
|
appleBM, hasNewConfig := mdmMap["apple_business_manager"]
|
|
|
|
if hasLegacyConfig && hasNewConfig {
|
|
return nil, false, false, errors.New(fleet.AppleABMDefaultTeamDeprecatedMessage)
|
|
}
|
|
|
|
abmToks, err := fleetClient.CountABMTokens()
|
|
if err != nil {
|
|
return nil, false, false, err
|
|
}
|
|
|
|
if hasLegacyConfig && abmToks > 1 {
|
|
return nil, false, false, errors.New(fleet.AppleABMDefaultTeamDeprecatedMessage)
|
|
}
|
|
|
|
if !hasLegacyConfig && !hasNewConfig {
|
|
return nil, false, false, nil
|
|
}
|
|
|
|
teams, err := fleetClient.ListTeams("")
|
|
if err != nil {
|
|
return nil, false, false, err
|
|
}
|
|
teamNames := map[string]struct{}{}
|
|
for _, tm := range teams {
|
|
teamNames[tm.Name] = struct{}{}
|
|
}
|
|
|
|
if hasLegacyConfig {
|
|
if appleBMDefaultTeam, ok := appleBMDT.(string); ok {
|
|
// normalize for Unicode support
|
|
appleBMDefaultTeam = norm.NFC.String(appleBMDefaultTeam)
|
|
abmTeams = append(abmTeams, appleBMDefaultTeam)
|
|
usesLegacyConfig = true
|
|
if _, ok = teamNames[appleBMDefaultTeam]; !ok {
|
|
missingTeam = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if hasNewConfig {
|
|
if settingMap, ok := appleBM.([]any); ok {
|
|
for _, item := range settingMap {
|
|
if cfg, ok := item.(map[string]any); ok {
|
|
for _, teamConfigKey := range []string{"macos_team", "ios_team", "ipados_team"} {
|
|
if team, ok := cfg[teamConfigKey].(string); ok && team != "" {
|
|
// normalize for Unicode support
|
|
team = norm.NFC.String(team)
|
|
abmTeams = append(abmTeams, team)
|
|
if _, ok := teamNames[team]; !ok {
|
|
missingTeam = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return abmTeams, missingTeam, usesLegacyConfig, nil
|
|
}
|
|
|
|
func applyABMTokenAssignmentIfNeeded(
|
|
ctx *cli.Context,
|
|
teamNames []string,
|
|
abmTeamNames []string,
|
|
originalMDMConfig []any,
|
|
usesLegacyConfig bool,
|
|
flDryRun bool,
|
|
fleetClient *service.Client,
|
|
) error {
|
|
if usesLegacyConfig && len(abmTeamNames) > 1 {
|
|
return errors.New(fleet.AppleABMDefaultTeamDeprecatedMessage)
|
|
}
|
|
|
|
if usesLegacyConfig && len(abmTeamNames) == 0 {
|
|
return errors.New("using legacy config without any ABM teams defined")
|
|
}
|
|
|
|
var appConfigUpdate map[string]map[string]any
|
|
if usesLegacyConfig {
|
|
appleBMDefaultTeam := abmTeamNames[0]
|
|
if !slices.Contains(teamNames, appleBMDefaultTeam) {
|
|
return fmt.Errorf("apple_bm_default_team team %q not found in team configs", appleBMDefaultTeam)
|
|
}
|
|
appConfigUpdate = map[string]map[string]any{
|
|
"mdm": {
|
|
"apple_bm_default_team": appleBMDefaultTeam,
|
|
},
|
|
}
|
|
} else {
|
|
for _, abmTeam := range abmTeamNames {
|
|
if !slices.Contains(teamNames, abmTeam) {
|
|
return fmt.Errorf("apple_business_manager team %q not found in team configs", abmTeam)
|
|
}
|
|
}
|
|
|
|
appConfigUpdate = map[string]map[string]any{
|
|
"mdm": {
|
|
"apple_business_manager": originalMDMConfig,
|
|
},
|
|
}
|
|
}
|
|
|
|
if flDryRun {
|
|
_, _ = fmt.Fprint(ctx.App.Writer, "[!] would apply ABM teams\n")
|
|
return nil
|
|
}
|
|
_, _ = fmt.Fprintf(ctx.App.Writer, "[+] applying ABM teams\n")
|
|
if err := fleetClient.ApplyAppConfig(appConfigUpdate, fleet.ApplySpecOptions{}); err != nil {
|
|
return fmt.Errorf("applying fleet config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkVPPTeamAssignments(config *spec.GitOps, fleetClient *service.Client) (
|
|
vppTeams []string, missingTeam bool, err error,
|
|
) {
|
|
if mdm, ok := config.OrgSettings["mdm"]; ok {
|
|
if mdmMap, ok := mdm.(map[string]any); ok {
|
|
teams, err := fleetClient.ListTeams("")
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
teamNames := map[string]struct{}{}
|
|
for _, tm := range teams {
|
|
teamNames[tm.Name] = struct{}{}
|
|
}
|
|
|
|
if vpp, ok := mdmMap["volume_purchasing_program"]; ok {
|
|
if vppInterfaces, ok := vpp.([]any); ok {
|
|
for _, item := range vppInterfaces {
|
|
if itemMap, ok := item.(map[string]any); ok {
|
|
if teams, ok := itemMap["teams"].([]any); ok {
|
|
for _, team := range teams {
|
|
if teamStr, ok := team.(string); ok {
|
|
// normalize for Unicode support
|
|
normalizedTeam := norm.NFC.String(teamStr)
|
|
vppTeams = append(vppTeams, normalizedTeam)
|
|
if _, ok := teamNames[normalizedTeam]; !ok {
|
|
missingTeam = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return vppTeams, missingTeam, nil
|
|
}
|
|
|
|
func applyVPPTokenAssignmentIfNeeded(
|
|
ctx *cli.Context,
|
|
teamNames []string,
|
|
vppTeamNames []string,
|
|
originalVPPConfig []any,
|
|
flDryRun bool,
|
|
fleetClient *service.Client,
|
|
) error {
|
|
var appConfigUpdate map[string]map[string]any
|
|
for _, vppTeam := range vppTeamNames {
|
|
if !fleet.IsReservedTeamName(vppTeam) && !slices.Contains(teamNames, vppTeam) {
|
|
return fmt.Errorf("volume_purchasing_program team %s not found in team configs", vppTeam)
|
|
}
|
|
}
|
|
|
|
appConfigUpdate = map[string]map[string]any{
|
|
"mdm": {
|
|
"volume_purchasing_program": originalVPPConfig,
|
|
},
|
|
}
|
|
|
|
if flDryRun {
|
|
_, _ = fmt.Fprint(ctx.App.Writer, "[!] would apply volume_purchasing_program teams\n")
|
|
return nil
|
|
}
|
|
_, _ = fmt.Fprintf(ctx.App.Writer, "[+] applying volume_purchasing_program teams\n")
|
|
if err := fleetClient.ApplyAppConfig(appConfigUpdate, fleet.ApplySpecOptions{}); err != nil {
|
|
return fmt.Errorf("applying fleet config for volume_purchasing_program teams: %w", err)
|
|
}
|
|
return nil
|
|
}
|