fleet/cmd/fleetctl/generate_gitops_test.go
Scott Gress d716265641
Add "generate-gitops" command (#28555)
For #27476

# Checklist for submitter

- [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/Committing-Changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)

# Details

This PR adds a new command `generate-gitops` to the `fleetctl` tool. The
purpose of this command is to output GitOps-ready files that can then be
used with `fleetctl-gitops`.

The general usage of the command is:

```
fleectl generate-gitops --dir /path/to/dir/to/add/files/to
```

By default, the outputted files will not contain sensitive data, but
will instead add comments where the data needs to be replaced by a user.
In cases where sensitive data is redacted, the tool outputs warnings to
the user indicating which keys need to be updated.

The tool uses existing APIs to gather data for use in generating
configuration files. In some cases new API client methods needed to be
added to support the tool:

* ListConfigurationProfiles
* GetProfileContents
* GetScriptContents
* GetSoftwareTitleByID

Additionally, the response for the /api/latest/fleet/software/batch
endpoint was updated slightly to return `HashSHA256` for the software
installers. This allows policies that automatically install software to
refer to that software by hash.

Other options that we may or may not choose to document at this time:

* `--insecure`: outputs sensitive data in plaintext instead of leaving
comments
* `--print`: prints the output to stdout instead of writing files
* `--key`: outputs the value at a keypath to stdout, e.g. `--key
agent_options.config`
* `--team`: only generates config for the specified team name
* `--force`: overwrites files in the given directory (defaults to false,
which errors if the dir is not empty)

# Technical notes

The command is implemented using a `GenerateGitopsCommand` type which
holds some state (like a list of software and scripts encountered) as
well as a Fleet client instance (which may be a mock instance for tests)
and the CLI context (containing things like flags and output writers).
The actual "action" of the CLI command calls the `Run()` method of the
`GenerateGitopsCommand` var, which delegates most of the work to other
methods like `generateOrgSettings()`, `generateControls()`, etc.

Wherever possible, the subroutines use reflection to translate Go struct
fields into JSON property names. This guarantees that the correct keys
are written to config files, and protects against the unlikely event of
keys changing.

When sensitive data is encountered, the subroutines call `AddComment()`
to get a new token to add to the config files. These tokens are replaced
with comments like `# TODO - Add your enrollment secrets here` in the
final output.

# Known issues / TODOs:

* The `macos_setup` configuration is not output by this tool yet. More
planning is required for this. In the meantime, if the tool detects that
`macos_setup` is configured on the server, it outputs a key with an
invalid value and prints a warning to the user that they'll need to
configure it themselves.
* `yara_rules` are not output yet. The tool adds a warning that if you
have Yara rules (which you can only upload via GitOps right now) that
you'll have to migrate them manually. Supporting this will require a new
API that we'll have to discuss the authz for, so punting on it for now.
* Fleet maintained apps are not supported by GitOps yet (coming in
https://github.com/fleetdm/fleet/issues/24469). In the meantime, this
tool will output a `fleet_maintained_apps` key and trigger a warning,
and GitOps will fail if that key is present.

---------

Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
Co-authored-by: Noah Talerman <47070608+noahtalerman@users.noreply.github.com>
2025-05-06 15:25:44 -05:00

948 lines
27 KiB
Go

// filepath: cmd/fleetctl/generate_gitops_test.go
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/ghodss/yaml"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
type MockClient struct {
IsFree bool
}
func (c *MockClient) GetAppConfig() (*fleet.EnrichedAppConfig, error) {
b, err := os.ReadFile("./testdata/generateGitops/appConfig.json")
if err != nil {
return nil, err
}
var appConfig fleet.EnrichedAppConfig
if err := json.Unmarshal(b, &appConfig); err != nil {
return nil, err
}
if c.IsFree == true {
appConfig.License.Tier = fleet.TierFree
}
return &appConfig, nil
}
func (MockClient) GetEnrollSecretSpec() (*fleet.EnrollSecretSpec, error) {
spec := &fleet.EnrollSecretSpec{
Secrets: []*fleet.EnrollSecret{
{
Secret: "some-secret-number-one",
},
{
Secret: "some-secret-number-two",
},
},
}
return spec, nil
}
func (MockClient) ListTeams(query string) ([]fleet.Team, error) {
b, err := os.ReadFile("./testdata/generateGitops/teamConfig.json")
if err != nil {
return nil, err
}
var config fleet.TeamConfig
if err := json.Unmarshal(b, &config); err != nil {
return nil, err
}
teams := []fleet.Team{
{
ID: 1,
Name: "Team A",
Config: config,
Secrets: []*fleet.EnrollSecret{
{
Secret: "some-team-secret",
},
},
},
}
return teams, nil
}
func (MockClient) ListScripts(query string) ([]*fleet.Script, error) {
switch query {
case "team_id=1":
return []*fleet.Script{{
ID: 2,
TeamID: ptr.Uint(1),
Name: "Script B.ps1",
ScriptContentID: 2,
}}, nil
case "team_id=0":
return []*fleet.Script{{
ID: 3,
TeamID: ptr.Uint(0),
Name: "Script Z.ps1",
ScriptContentID: 3,
}}, nil
default:
return nil, fmt.Errorf("unexpected query: %s", query)
}
}
func (MockClient) ListConfigurationProfiles(teamID *uint) ([]*fleet.MDMConfigProfilePayload, error) {
if teamID == nil {
return []*fleet.MDMConfigProfilePayload{
{
ProfileUUID: "global-macos-mobileconfig-profile-uuid",
Name: "Global MacOS MobileConfig Profile",
Platform: "darwin",
Identifier: "com.example.global-macos-mobileconfig-profile",
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{
LabelName: "Label A",
}, {
LabelName: "Label B",
}},
},
{
ProfileUUID: "global-macos-json-profile-uuid",
Name: "Global MacOS JSON Profile",
Platform: "darwin",
Identifier: "com.example.global-macos-json-profile",
LabelsExcludeAny: []fleet.ConfigurationProfileLabel{{
LabelName: "Label C",
}},
},
{
ProfileUUID: "global-windows-profile-uuid",
Name: "Global Windows Profile",
Platform: "windows",
Identifier: "com.example.global-windows-profile",
LabelsIncludeAny: []fleet.ConfigurationProfileLabel{{
LabelName: "Label D",
}},
},
}, nil
}
if *teamID == 1 {
return []*fleet.MDMConfigProfilePayload{
{
ProfileUUID: "test-mobileconfig-profile-uuid",
Name: "Team MacOS MobileConfig Profile",
Platform: "darwin",
Identifier: "com.example.team-macos-mobileconfig-profile",
},
}, nil
}
if *teamID == 0 {
return nil, nil
}
return nil, fmt.Errorf("unexpected team ID: %v", *teamID)
}
func (MockClient) GetScriptContents(scriptID uint) ([]byte, error) {
if scriptID == 2 {
return []byte("pop goes the weasel!"), nil
}
if scriptID == 3 {
return []byte("#!/usr/bin/env pwsh\necho \"Hello from Script B!\""), nil
}
return nil, errors.New("script not found")
}
func (MockClient) GetProfileContents(profileID string) ([]byte, error) {
switch profileID {
case "global-macos-mobileconfig-profile-uuid":
return []byte("<xml>global macos mobileconfig profile</xml>"), nil
case "global-macos-json-profile-uuid":
return []byte(`{"profile": "global macos json profile"}`), nil
case "global-windows-profile-uuid":
return []byte("<xml>global windows profile</xml>"), nil
case "test-mobileconfig-profile-uuid":
return []byte("<xml>test mobileconfig profile</xml>"), nil
}
return nil, errors.New("profile not found")
}
func (MockClient) GetTeam(teamID uint) (*fleet.Team, error) {
if teamID == 1 {
b, err := os.ReadFile("./testdata/generateGitops/teamConfig.json")
if err != nil {
return nil, err
}
var config fleet.TeamConfig
if err := json.Unmarshal(b, &config); err != nil {
return nil, err
}
return &fleet.Team{
ID: 1,
Name: "Test Team",
Config: config,
Secrets: []*fleet.EnrollSecret{
{
Secret: "some-team-secret",
},
},
}, nil
}
return nil, errors.New("team not found")
}
func (MockClient) ListSoftwareTitles(query string) ([]fleet.SoftwareTitleListResult, error) {
switch query {
case "available_for_install=1&team_id=1":
return []fleet.SoftwareTitleListResult{
{
ID: 1,
Name: "My Software Package",
Versions: []fleet.SoftwareVersion{{
ID: 1,
Version: "1.0.0",
}, {
ID: 2,
Version: "2.0.0",
}},
HashSHA256: ptr.String("software-package-hash"),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
Name: "my-software.pkg",
Platform: "darwin",
},
},
{
ID: 2,
Name: "My App Store App",
Versions: []fleet.SoftwareVersion{{
ID: 3,
Version: "5.6.7",
}, {
ID: 4,
Version: "8.9.10",
}},
AppStoreApp: &fleet.SoftwarePackageOrApp{
AppStoreID: "com.example.team-software",
},
HashSHA256: ptr.String("app-store-app-hash"),
},
}, nil
case "available_for_install=1&team_id=0":
return []fleet.SoftwareTitleListResult{}, nil
default:
return nil, fmt.Errorf("unexpected query: %s", query)
}
}
func (MockClient) GetPolicies(teamID *uint) ([]*fleet.Policy, error) {
if teamID == nil {
return []*fleet.Policy{
{
PolicyData: fleet.PolicyData{
ID: 1,
Name: "Global Policy",
Query: "SELECT * FROM global_policy WHERE id = 1",
Resolution: ptr.String("Do a global thing"),
Description: "This is a global policy",
Platform: "darwin",
LabelsIncludeAny: []fleet.LabelIdent{{
LabelName: "Label A",
}, {
LabelName: "Label B",
}},
},
InstallSoftware: &fleet.PolicySoftwareTitle{
SoftwareTitleID: 1,
},
},
}, nil
}
return []*fleet.Policy{
{
PolicyData: fleet.PolicyData{
ID: 1,
Name: "Team Policy",
Query: "SELECT * FROM team_policy WHERE id = 1",
Resolution: ptr.String("Do a team thing"),
Description: "This is a team policy",
Platform: "linux,windows",
},
RunScript: &fleet.PolicyScript{
ID: 1,
},
},
}, nil
}
func (MockClient) GetQueries(teamID *uint, name *string) ([]fleet.Query, error) {
if teamID == nil {
return []fleet.Query{
{
ID: 1,
Name: "Global Query",
Query: "SELECT * FROM global_query WHERE id = 1",
Description: "This is a global query",
Platform: "darwin",
Interval: 3600,
ObserverCanRun: true,
AutomationsEnabled: true,
LabelsIncludeAny: []fleet.LabelIdent{{
LabelName: "Label A",
}, {
LabelName: "Label B",
}},
MinOsqueryVersion: "1.2.3",
Logging: "stdout",
},
}, nil
}
return []fleet.Query{
{
ID: 1,
Name: "Team Query",
Query: "SELECT * FROM team_query WHERE id = 1",
Description: "This is a team query",
Platform: "linux,windows",
Interval: 1800,
ObserverCanRun: false,
AutomationsEnabled: true,
MinOsqueryVersion: "4.5.6",
Logging: "stderr",
},
}, nil
}
//nolint:gocritic // ignore captLocal
func (MockClient) GetSoftwareTitleByID(ID uint, teamID *uint) (*fleet.SoftwareTitle, error) {
switch ID {
case 1:
if *teamID != 1 {
return nil, errors.New("team ID mismatch")
}
return &fleet.SoftwareTitle{
ID: 1,
SoftwarePackage: &fleet.SoftwareInstaller{
LabelsIncludeAny: []fleet.SoftwareScopeLabel{{
LabelName: "Label A",
}, {
LabelName: "Label B",
}},
PreInstallQuery: "SELECT * FROM pre_install_query",
InstallScript: "foo",
PostInstallScript: "bar",
UninstallScript: "baz",
SelfService: true,
Platform: "darwin",
},
}, nil
case 2:
if *teamID != 1 {
return nil, errors.New("team ID mismatch")
}
return &fleet.SoftwareTitle{
ID: 2,
AppStoreApp: &fleet.VPPAppStoreApp{
LabelsExcludeAny: []fleet.SoftwareScopeLabel{{
LabelName: "Label C",
}, {
LabelName: "Label D",
}},
},
}, nil
default:
return nil, errors.New("software title not found")
}
}
func (MockClient) GetLabels() ([]*fleet.LabelSpec, error) {
return []*fleet.LabelSpec{{
Name: "Label A",
Platform: "linux,macos",
Description: "Label A description",
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
Query: "SELECT * FROM osquery_info",
}, {
Name: "Label B",
Description: "Label B description",
LabelMembershipType: fleet.LabelMembershipTypeManual,
Hosts: []string{"host1", "host2"},
}}, nil
}
func (MockClient) Me() (*fleet.User, error) {
return &fleet.User{
ID: 1,
Name: "Test User",
Email: "test@example.com",
GlobalRole: ptr.String("admin"),
}, nil
}
func compareDirs(t *testing.T, sourceDir, targetDir string) {
err := filepath.WalkDir(sourceDir, func(srcPath string, d os.DirEntry, walkErr error) error {
if d.IsDir() {
return nil
}
relPath, err := filepath.Rel(sourceDir, srcPath)
require.NoError(t, err, "Error getting relative path: %v", err)
tgtPath := filepath.Join(targetDir, relPath)
targetInfo, err := os.Stat(tgtPath)
require.NoError(t, err, "Error getting target file info: %v", err)
require.False(t, targetInfo.IsDir(), "Expected file but found directory: %s", tgtPath)
srcData, err := os.ReadFile(srcPath)
require.NoError(t, err, "Error reading source file: %v", err)
tgtData, err := os.ReadFile(tgtPath)
require.NoError(t, err, "Error reading target file: %v", err)
require.Equal(t, string(srcData), string(tgtData), "File contents do not match for %s", relPath)
return nil
})
if err != nil {
t.Fatalf("Error walking source directory: %v", err)
}
}
func TestGenerateGitops(t *testing.T) {
fleetClient := &MockClient{}
action := createGenerateGitopsAction(fleetClient)
buf := new(bytes.Buffer)
tempDir := os.TempDir() + "/" + uuid.New().String()
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
flagSet.String("dir", tempDir, "")
cliContext := cli.NewContext(&cli.App{
Name: "test",
Usage: "test",
Writer: buf,
ErrWriter: buf,
}, flagSet, nil)
err := action(cliContext)
require.NoError(t, err, buf.String())
compareDirs(t, "./testdata/generateGitops/test_dir_premium", tempDir)
t.Cleanup(func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Fatalf("failed to remove temp dir: %v", err)
}
})
}
func TestGenerateGitopsFree(t *testing.T) {
fleetClient := &MockClient{}
fleetClient.IsFree = true
action := createGenerateGitopsAction(fleetClient)
buf := new(bytes.Buffer)
tempDir := os.TempDir() + "/" + uuid.New().String()
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
flagSet.String("dir", tempDir, "")
cliContext := cli.NewContext(&cli.App{
Name: "test",
Usage: "test",
Writer: buf,
ErrWriter: buf,
}, flagSet, nil)
err := action(cliContext)
require.NoError(t, err, buf.String())
compareDirs(t, "./testdata/generateGitops/test_dir_free", tempDir)
t.Cleanup(func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Fatalf("failed to remove temp dir: %v", err)
}
})
}
func TestGenerateOrgSettings(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
}
// Generate the org settings.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
orgSettingsRaw, err := cmd.generateOrgSettings()
require.NoError(t, err)
require.NotNil(t, orgSettingsRaw)
var orgSettings map[string]interface{}
b, err := yaml.Marshal(orgSettingsRaw)
require.NoError(t, err)
fmt.Println("Org settings raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &orgSettings)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedOrgSettings.yaml")
require.NoError(t, err)
var expectedAppConfig map[string]interface{}
err = yaml.Unmarshal(b, &expectedAppConfig)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedAppConfig, orgSettings)
}
func TestGenerateOrgSettingsInsecure(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
flagSet.Bool("insecure", true, "Output sensitive information in plaintext.")
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, flagSet, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
}
// Generate the org settings.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
orgSettingsRaw, err := cmd.generateOrgSettings()
require.NoError(t, err)
require.NotNil(t, orgSettingsRaw)
var orgSettings map[string]interface{}
b, err := yaml.Marshal(orgSettingsRaw)
require.NoError(t, err)
fmt.Println("Org settings raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &orgSettings)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedOrgSettings-insecure.yaml")
require.NoError(t, err)
var expectedAppConfig map[string]interface{}
err = yaml.Unmarshal(b, &expectedAppConfig)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedAppConfig, orgSettings)
}
func TestGenerateTeamSettings(t *testing.T) {
// Get the test team.
fleetClient := &MockClient{}
team, err := fleetClient.GetTeam(1)
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: nil,
}
// Generate the org settings.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
TeamSettingsRaw, err := cmd.generateTeamSettings("team.yml", team)
require.NoError(t, err)
require.NotNil(t, TeamSettingsRaw)
var TeamSettings map[string]interface{}
b, err := yaml.Marshal(TeamSettingsRaw)
require.NoError(t, err)
fmt.Println("Team settings raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &TeamSettings)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamSettings.yaml")
require.NoError(t, err)
var expectedAppConfig map[string]interface{}
err = yaml.Unmarshal(b, &expectedAppConfig)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedAppConfig, TeamSettings)
}
func TestGenerateTeamSettingsInsecure(t *testing.T) {
// Get the test team.
fleetClient := &MockClient{}
team, err := fleetClient.GetTeam(1)
require.NoError(t, err)
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
flagSet.Bool("insecure", true, "Output sensitive information in plaintext.")
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, flagSet, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: nil,
}
// Generate the org settings.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
TeamSettingsRaw, err := cmd.generateTeamSettings("team.yml", team)
require.NoError(t, err)
require.NotNil(t, TeamSettingsRaw)
var TeamSettings map[string]interface{}
b, err := yaml.Marshal(TeamSettingsRaw)
require.NoError(t, err)
fmt.Println("Team settings raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &TeamSettings)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamSettings-insecure.yaml")
require.NoError(t, err)
var expectedAppConfig map[string]interface{}
err = yaml.Unmarshal(b, &expectedAppConfig)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedAppConfig, TeamSettings)
}
func TestGenerateControls(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
ScriptList: make(map[uint]string),
}
// Generate global controls.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
mdmConfig := fleet.TeamMDM{
EnableDiskEncryption: appConfig.MDM.EnableDiskEncryption.Value,
MacOSUpdates: appConfig.MDM.MacOSUpdates,
IOSUpdates: appConfig.MDM.IOSUpdates,
IPadOSUpdates: appConfig.MDM.IPadOSUpdates,
WindowsUpdates: appConfig.MDM.WindowsUpdates,
MacOSSetup: appConfig.MDM.MacOSSetup,
}
controlsRaw, err := cmd.generateControls(nil, "", &mdmConfig)
require.NoError(t, err)
require.NotNil(t, controlsRaw)
var controls map[string]interface{}
b, err := yaml.Marshal(controlsRaw)
require.NoError(t, err)
fmt.Println("Controls raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &controls)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedGlobalControls.yaml")
require.NoError(t, err)
var expectedControls map[string]interface{}
err = yaml.Unmarshal(b, &expectedControls)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedControls, controls)
// Generate controls for a team.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
controlsRaw, err = cmd.generateControls(ptr.Uint(1), "some_team", nil)
require.NoError(t, err)
require.NotNil(t, controlsRaw)
b, err = yaml.Marshal(controlsRaw)
require.NoError(t, err)
fmt.Println("Controls raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &controls)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamControls.yaml")
require.NoError(t, err)
err = yaml.Unmarshal(b, &expectedControls)
require.NoError(t, err)
if fileContents, ok := cmd.FilesToWrite["lib/profiles/global-macos-mobileconfig-profile.mobileconfig"]; ok {
require.Equal(t, "<xml>global macos mobileconfig profile</xml>", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/profiles/global-macos-json-profile.json"]; ok {
require.Equal(t, `{"profile": "global macos json profile"}`, fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/profiles/global-windows-profile.xml"]; ok {
require.Equal(t, "<xml>global windows profile</xml>", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/some_team/profiles/team-macos-mobileconfig-profile.mobileconfig"]; ok {
require.Equal(t, "<xml>test mobileconfig profile</xml>", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/some_team/scripts/Script B.ps1"]; ok {
require.Equal(t, "pop goes the weasel!", fileContents)
} else {
t.Fatalf("Expected file not found")
}
// Compare.
require.Equal(t, expectedControls, controls)
}
func TestGenerateSoftware(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
SoftwareList: make(map[uint]Software),
}
softwareRaw, err := cmd.generateSoftware("team.yml", 1, "some-team")
require.NoError(t, err)
require.NotNil(t, softwareRaw)
var software map[string]interface{}
b, err := yaml.Marshal(softwareRaw)
require.NoError(t, err)
fmt.Println("software raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &software)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamSoftware.yaml")
require.NoError(t, err)
var expectedSoftware map[string]interface{}
err = yaml.Unmarshal(b, &expectedSoftware)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedSoftware, software)
if fileContents, ok := cmd.FilesToWrite["lib/some-team/scripts/my-software-package-darwin-install"]; ok {
require.Equal(t, "foo", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/some-team/scripts/my-software-package-darwin-postinstall"]; ok {
require.Equal(t, "bar", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/some-team/scripts/my-software-package-darwin-uninstall"]; ok {
require.Equal(t, "baz", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/some-team/queries/my-software-package-darwin-preinstallquery.yml"]; ok {
require.Equal(t, []map[string]interface{}{{
"query": "SELECT * FROM pre_install_query",
}}, fileContents)
} else {
t.Fatalf("Expected file not found")
}
}
func TestGeneratePolicies(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
SoftwareList: map[uint]Software{
1: {
Hash: "team-software-hash",
Comment: "__TEAM_SOFTWARE_COMMENT_TOKEN__",
},
},
ScriptList: map[uint]string{
1: "/path/to/script1.sh",
},
}
policiesRaw, err := cmd.generatePolicies(nil, "default.yml")
require.NoError(t, err)
require.NotNil(t, policiesRaw)
var policies []map[string]interface{}
b, err := yaml.Marshal(policiesRaw)
require.NoError(t, err)
fmt.Println("policies raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &policies)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedGlobalPolicies.yaml")
require.NoError(t, err)
var expectedPolicies []map[string]interface{}
err = yaml.Unmarshal(b, &expectedPolicies)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedPolicies, policies)
// Generate policies for a team.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
policiesRaw, err = cmd.generatePolicies(ptr.Uint(1), "some_team")
require.NoError(t, err)
require.NotNil(t, policiesRaw)
b, err = yaml.Marshal(policiesRaw)
require.NoError(t, err)
fmt.Println("policies raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &policies)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamPolicies.yaml")
require.NoError(t, err)
err = yaml.Unmarshal(b, &expectedPolicies)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedPolicies, policies)
}
func TestGenerateQueries(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
}
queriesRaw, err := cmd.generateQueries(nil)
require.NoError(t, err)
require.NotNil(t, queriesRaw)
var queries []map[string]interface{}
b, err := yaml.Marshal(queriesRaw)
require.NoError(t, err)
fmt.Println("queries raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &queries)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedGlobalQueries.yaml")
require.NoError(t, err)
var expectedQueries []map[string]interface{}
err = yaml.Unmarshal(b, &expectedQueries)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedQueries, queries)
// Generate queries for a team.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
queriesRaw, err = cmd.generateQueries(ptr.Uint(1))
require.NoError(t, err)
require.NotNil(t, queriesRaw)
b, err = yaml.Marshal(queriesRaw)
require.NoError(t, err)
fmt.Println("queries raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &queries)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamQueries.yaml")
require.NoError(t, err)
err = yaml.Unmarshal(b, &expectedQueries)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedQueries, queries)
}
func TestGenerateLabels(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
}
labelsRaw, err := cmd.generateLabels()
require.NoError(t, err)
require.NotNil(t, labelsRaw)
var labels []map[string]interface{}
b, err := yaml.Marshal(labelsRaw)
require.NoError(t, err)
fmt.Println("labels raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &labels)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedLabels.yaml")
require.NoError(t, err)
var expectedlabels []map[string]interface{}
err = yaml.Unmarshal(b, &expectedlabels)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedlabels, labels)
}