mirror of
https://github.com/fleetdm/fleet
synced 2026-05-09 02:01:09 +00:00
For #24473 This PR allows users to add / update / remove labels from queries via Gitops. ## Testing 1. Create a few labels in the UI 1. Create a global query "foo" in the UI without labels 2. Create a global query "bar" in the UI with labels 2. Create a global query "baz" in the UI with labels 4. Use `fleetctl gitops` with a global .yml file, and under `queries:` and "foo", "bar", "baz" and "boop". * Add labels to "foo" with `labels_include_any:` * Don't add `labels_include_any:` to "bar" * Add labels to "baz" with `labels_include_any:`, but different labels than what you added in the UI * Add labels to "boop" with `labels_include_any:` The expected outcome when viewing the queries in the UI (on the "edit query" screen) * Foo, Baz and Boop should have the labels specified in gitops * Bar should have no labels
634 lines
20 KiB
Go
634 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)
|
|
}
|
|
|
|
// Get query label usage
|
|
for _, query := range config.Queries {
|
|
updateLabelUsage(query.LabelsIncludeAny, query.Name, "Query", result)
|
|
}
|
|
|
|
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
|
|
}
|