mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Add fleetctl new command (#41909)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #41345 # Details This PR: * Adds a new `fleetctl new` command which creates a starter GitOps repo file structure * Adds support for file globs for the `configuration_profiles:` key in GitOps, to support its use in the `fleetctl new` templates. This involved moving the `BaseItem` type and `SupportsFileInclude` interface into the `fleet` package so that the `MDMProfileSpec` type could implement the interface and do glob expansion. # 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] added unit and intg tests for globbing profiles - [ ] added tests for `fleetctl new` - [X] QA'd all new/changed functionality manually - [X] `fleetctl new` with no args prompted for org name and created a new `it-and-security` folder under current folder w/ correct files - [X] `fleetctl new --dir /tmp/testnew` created correct files under `/tmp/testnew` - [X] `fleetctl new --dir /tmp/testexisting --force` with an existing `/tmp/testexisting` folder created correct files under `/tmp/testexisting` - [X] `fleetctl new --org-name=foo` created correct files under `it-and-security` without prompting for org name - [X] `paths:` in `configuration_profiles` picks up multiple matching profiles - [X] `paths:` + `path:` in `configuration_profiles` will error if the same profile is picked up twice <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added `fleetctl new` command to initialize GitOps repository structure via CLI. * Added glob pattern support for `configuration_profiles` field, enabling flexible profile selection. * **Chores** * Updated CLI dependencies to support enhanced user interactions. * Removed legacy website generator configuration files. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
8154fa9c57
commit
91362ba2ca
56 changed files with 752 additions and 241 deletions
2
changes/41345-add-fleetctl-new
Normal file
2
changes/41345-add-fleetctl-new
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Added `fleetctl new` command to initialize a GitOps folder
|
||||
- Added glob support for `configuration_profiles`
|
||||
|
|
@ -74,6 +74,7 @@ func CreateApp(
|
|||
runScriptCommand(),
|
||||
gitopsCommand(),
|
||||
generateGitopsCommand(),
|
||||
newCommand(),
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
|
|
|||
182
cmd/fleetctl/fleetctl/new.go
Normal file
182
cmd/fleetctl/fleetctl/new.go
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
package fleetctl
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
//go:embed all:templates/new
|
||||
var newTemplateFS embed.FS
|
||||
|
||||
func renderTemplate(content []byte, vars map[string]string) ([]byte, error) {
|
||||
tmpl, err := template.New("").Delims("<%=", "%>").Parse(string(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buf strings.Builder
|
||||
if err := tmpl.Execute(&buf, vars); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(buf.String()), nil
|
||||
}
|
||||
|
||||
func printNextSteps(w io.Writer) {
|
||||
fmt.Fprintln(w, "")
|
||||
fmt.Fprintln(w, "Next steps:")
|
||||
fmt.Fprintln(w, "")
|
||||
fmt.Fprintln(w, " 1. Create a repository on GitHub or GitLab and push this directory to it.")
|
||||
fmt.Fprintln(w, "")
|
||||
fmt.Fprintln(w, " 2. Create a Fleet GitOps user and get an API token:")
|
||||
fmt.Fprintln(w, " fleetctl user create --name GitOps --email gitops@example.com \\")
|
||||
fmt.Fprintln(w, " --password <password> --global-role gitops --api-only")
|
||||
fmt.Fprintln(w, "")
|
||||
fmt.Fprintln(w, " 3. Add FLEET_URL and FLEET_API_TOKEN as secrets (GitHub) or CI/CD variables (GitLab).")
|
||||
}
|
||||
|
||||
func newCommand() *cli.Command {
|
||||
var (
|
||||
orgName string
|
||||
outputDir string
|
||||
force bool
|
||||
)
|
||||
return &cli.Command{
|
||||
Name: "new",
|
||||
Usage: "Create a new Fleet GitOps repository structure",
|
||||
UsageText: "fleetctl new [options]",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "org-name",
|
||||
Usage: "The name of your organization",
|
||||
Destination: &orgName,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "dir",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Output directory path",
|
||||
Value: "it-and-security",
|
||||
Destination: &outputDir,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Write files into an existing directory",
|
||||
Destination: &force,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if !force {
|
||||
if _, err := os.Stat(outputDir); err == nil {
|
||||
return fmt.Errorf("%s already exists; use --force to write into an existing directory", outputDir)
|
||||
}
|
||||
}
|
||||
|
||||
cleanOrgName := func(name string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r < 0x20 || r == 0x7f {
|
||||
return -1 // strip control characters
|
||||
}
|
||||
return r
|
||||
}, strings.TrimSpace(name))
|
||||
}
|
||||
validateOrgName := func(name string) error {
|
||||
name = cleanOrgName(name)
|
||||
if name == "" {
|
||||
return errors.New("organization name is required")
|
||||
}
|
||||
if len(name) > 255 {
|
||||
return errors.New("organization name must be 255 characters or fewer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if orgName == "" {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Organization name",
|
||||
Default: "My organization",
|
||||
Validate: func(s string) error { return validateOrgName(s) },
|
||||
}
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("prompt failed: %w", err)
|
||||
}
|
||||
orgName = cleanOrgName(result)
|
||||
} else {
|
||||
orgName = cleanOrgName(orgName)
|
||||
if err := validateOrgName(orgName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal through YAML to get a properly escaped scalar value.
|
||||
yamlOrgName, err := yaml.Marshal(orgName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling org name: %w", err)
|
||||
}
|
||||
|
||||
vars := map[string]string{
|
||||
"org_name": strings.TrimSpace(string(yamlOrgName)),
|
||||
}
|
||||
|
||||
templateRoot := "templates/new"
|
||||
|
||||
err = fs.WalkDir(newTemplateFS, templateRoot, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(templateRoot, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Strip .template from filenames (e.g. foo.template.yml -> foo.yml).
|
||||
relPath = strings.Replace(relPath, ".template.", ".", 1)
|
||||
outPath := filepath.Join(outputDir, relPath)
|
||||
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(outPath, 0o755)
|
||||
}
|
||||
|
||||
content, err := newTemplateFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading template %s: %w", path, err)
|
||||
}
|
||||
|
||||
content, err = renderTemplate(content, vars)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rendering template %s: %w", path, err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(outPath, content, 0o644)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating GitOps directory structure: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(c.App.Writer, "Created new Fleet GitOps repository at %s\n", outputDir)
|
||||
fmt.Fprintf(c.App.Writer, "Organization name: %s\n", orgName)
|
||||
|
||||
printNextSteps(c.App.Writer)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
241
cmd/fleetctl/fleetctl/new_test.go
Normal file
241
cmd/fleetctl/fleetctl/new_test.go
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
package fleetctl
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const testAppVersion = "fleet-v4.83.0"
|
||||
|
||||
// runNewCommand runs the "new" command with the given args and returns stdout and any error.
|
||||
func runNewCommand(t *testing.T, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
buf := &strings.Builder{}
|
||||
app := &cli.App{
|
||||
Name: "fleetctl",
|
||||
Version: testAppVersion,
|
||||
Writer: buf,
|
||||
ErrWriter: buf,
|
||||
Commands: []*cli.Command{newCommand()},
|
||||
}
|
||||
cliArgs := append([]string{"fleetctl", "new"}, args...)
|
||||
err := app.Run(cliArgs)
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func TestNewBasicFileStructure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
outDir := filepath.Join(dir, "out")
|
||||
|
||||
output, err := runNewCommand(t, "--org-name", `Acme "Corp" \ Inc`, "--dir", outDir)
|
||||
require.NoError(t, err, output)
|
||||
|
||||
t.Run("has expected files", func(t *testing.T) {
|
||||
// Spot-check key files exist.
|
||||
expectedFiles := []string{
|
||||
"default.yml",
|
||||
".gitignore",
|
||||
".github/workflows/workflow.yml",
|
||||
".github/fleet-gitops/action.yml",
|
||||
".gitlab-ci.yml",
|
||||
"README.md",
|
||||
"fleets/workstations.yml",
|
||||
"labels/apple-silicon-macos-hosts.yml",
|
||||
"platforms/macos/policies/all-software-updates-installed.yml",
|
||||
}
|
||||
for _, f := range expectedFiles {
|
||||
path := filepath.Join(outDir, f)
|
||||
_, err := os.Stat(path)
|
||||
assert.NoError(t, err, "expected file %s to exist", f)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("strips .template. from output filenames", func(t *testing.T) {
|
||||
// .template. should be stripped from output filenames.
|
||||
err = filepath.Walk(outDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
assert.NotContains(t, info.Name(), ".template.", "file %s should not contain .template.", path)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("replaces and escapes org_name template var", func(t *testing.T) {
|
||||
content, err := os.ReadFile(filepath.Join(outDir, "default.yml"))
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), `Acme "Corp" \ Inc`)
|
||||
assert.NotContains(t, string(content), "<%=")
|
||||
|
||||
// Verify the output is valid YAML that round-trips correctly.
|
||||
var parsed map[string]any
|
||||
require.NoError(t, yaml.Unmarshal(content, &parsed))
|
||||
orgSettings, _ := parsed["org_settings"].(map[string]any)
|
||||
orgInfo, _ := orgSettings["org_info"].(map[string]any)
|
||||
assert.Equal(t, `Acme "Corp" \ Inc`, orgInfo["org_name"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewOrgNameYAMLQuoting(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
outDir := filepath.Join(dir, "out")
|
||||
|
||||
// A colon followed by a space is special in YAML, so yaml.Marshal
|
||||
// wraps the value in quotes to produce valid output.
|
||||
_, err := runNewCommand(t, "--org-name", "Ops: IT & Security", "--dir", outDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(outDir, "default.yml"))
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), `'Ops: IT & Security'`)
|
||||
|
||||
var parsed map[string]any
|
||||
require.NoError(t, yaml.Unmarshal(content, &parsed))
|
||||
orgSettings, _ := parsed["org_settings"].(map[string]any)
|
||||
orgInfo, _ := orgSettings["org_info"].(map[string]any)
|
||||
assert.Equal(t, "Ops: IT & Security", orgInfo["org_name"])
|
||||
}
|
||||
|
||||
func TestNewTemplateStripping(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
outDir := filepath.Join(dir, "out")
|
||||
|
||||
_, err := runNewCommand(t, "--org-name", "Test", "--dir", outDir)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNewDirFlag(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
outDir := filepath.Join(dir, "custom-dir")
|
||||
|
||||
output, err := runNewCommand(t, "--org-name", "Test", "--dir", outDir)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, output, "custom-dir")
|
||||
|
||||
_, err = os.Stat(filepath.Join(outDir, "default.yml"))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNewExistingDirWithoutForce(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
outDir := filepath.Join(dir, "existing")
|
||||
require.NoError(t, os.Mkdir(outDir, 0o755))
|
||||
|
||||
_, err := runNewCommand(t, "--org-name", "Test", "--dir", outDir)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already exists")
|
||||
assert.Contains(t, err.Error(), "--force")
|
||||
// Verify no default.yml was created.
|
||||
_, err = os.Stat(filepath.Join(outDir, "default.yml"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewExistingDirWithForce(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
outDir := filepath.Join(dir, "existing")
|
||||
require.NoError(t, os.Mkdir(outDir, 0o755))
|
||||
|
||||
_, err := runNewCommand(t, "--org-name", "Test", "--dir", outDir, "--force")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(filepath.Join(outDir, "default.yml"))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNewOrgNameValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
orgName string
|
||||
wantErr string // empty means no error expected
|
||||
checkOutput func(t *testing.T, outDir string)
|
||||
}{
|
||||
{
|
||||
name: "only control characters",
|
||||
orgName: "\x01\x02\x03",
|
||||
wantErr: "organization name is required",
|
||||
},
|
||||
{
|
||||
name: "too long",
|
||||
orgName: strings.Repeat("a", 256),
|
||||
wantErr: "255 characters",
|
||||
},
|
||||
{
|
||||
name: "at max length",
|
||||
orgName: strings.Repeat("a", 255),
|
||||
},
|
||||
{
|
||||
name: "control characters stripped",
|
||||
orgName: "ACME\x00Corp",
|
||||
checkOutput: func(t *testing.T, outDir string) {
|
||||
content, err := os.ReadFile(filepath.Join(outDir, "default.yml"))
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "ACMECorp")
|
||||
assert.NotContains(t, string(content), "\x00")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only whitespace",
|
||||
orgName: " ",
|
||||
wantErr: "organization name is required",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
outDir := filepath.Join(t.TempDir(), "out")
|
||||
_, err := runNewCommand(t, "--org-name", tt.orgName, "--dir", outDir)
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
if tt.checkOutput != nil {
|
||||
tt.checkOutput(t, outDir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOutputMessages(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
outDir := filepath.Join(dir, "out")
|
||||
|
||||
output, err := runNewCommand(t, "--org-name", "Test Org", "--dir", outDir)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, output, "Created new Fleet GitOps repository")
|
||||
assert.Contains(t, output, "Organization name: Test Org")
|
||||
assert.Contains(t, output, "Next steps:")
|
||||
}
|
||||
|
||||
func TestRenderTemplate(t *testing.T) {
|
||||
vars := map[string]string{
|
||||
"name": "Fleet",
|
||||
"version": "4.83.0",
|
||||
}
|
||||
|
||||
t.Run("replaces known vars", func(t *testing.T) {
|
||||
result, err := renderTemplate([]byte(`app: <%= .name %> v<%= .version %>`), vars)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "app: Fleet v4.83.0", string(result))
|
||||
})
|
||||
|
||||
t.Run("unknown vars produce no value", func(t *testing.T) {
|
||||
result, err := renderTemplate([]byte(`<%= .unknown %>`), vars)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "<no value>", string(result))
|
||||
})
|
||||
|
||||
t.Run("handles no vars", func(t *testing.T) {
|
||||
result, err := renderTemplate([]byte("no vars here"), vars)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "no vars here", string(result))
|
||||
})
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ runs:
|
|||
run: |
|
||||
FLEET_URL="${FLEET_URL%/}"
|
||||
FLEET_VERSION="$(curl "$FLEET_URL/api/v1/fleet/version" --header "Authorization: Bearer $FLEET_API_TOKEN" --fail --silent | jq --raw-output '.version')"
|
||||
DEFAULT_FLEETCTL_VERSION="4.80.2"
|
||||
DEFAULT_FLEETCTL_VERSION="latest"
|
||||
|
||||
# Decide which fleetctl version to install:
|
||||
# If the server returns a clean version (e.g. 4.74.0), use that.
|
||||
|
|
@ -56,4 +56,4 @@ runs:
|
|||
env:
|
||||
FLEET_DRY_RUN_ONLY: ${{ inputs.dry-run-only }}
|
||||
FLEET_DELETE_OTHER_FLEETS: ${{ inputs.delete-other-fleets }}
|
||||
run: ./.github/fleet-gitops/gitops.sh
|
||||
run: ./.github/fleet-gitops/gitops.sh
|
||||
4
website/generators/gitops/templates/github/fleet-gitops/gitops.sh.template → cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/gitops.sh
vendored
Normal file → Executable file
4
website/generators/gitops/templates/github/fleet-gitops/gitops.sh.template → cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/gitops.sh
vendored
Normal file → Executable file
|
|
@ -26,7 +26,7 @@ fi
|
|||
# FLEET_SSO_METADATA=$( sed '2,$s/^/ /' <<< "${FLEET_MDM_SSO_METADATA}")
|
||||
# FLEET_MDM_SSO_METADATA=$( sed '2,$s/^/ /' <<< "${FLEET_MDM_SSO_METADATA}")
|
||||
|
||||
# Copy/pasting raw SSO metadata into GitHub secrets will result in malformed yaml.
|
||||
# Copy/pasting raw SSO metadata into GitHub secrets will result in malformed yaml.
|
||||
# Adds spaces to all but the first line of metadata keeps the multiline string in bounds.
|
||||
|
||||
if compgen -G "$FLEET_GITOPS_DIR"/fleets/*.yml > /dev/null; then
|
||||
|
|
@ -56,4 +56,4 @@ if [ "$FLEET_DRY_RUN_ONLY" = true ]; then
|
|||
fi
|
||||
|
||||
# Real run
|
||||
$FLEETCTL gitops "${args[@]}"
|
||||
$FLEETCTL gitops "${args[@]}"
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
name: 'Apply latest configuration to Fleet'
|
||||
name: "Apply latest configuration to Fleet"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**'
|
||||
- "**"
|
||||
pull_request:
|
||||
paths:
|
||||
- '**'
|
||||
- "**"
|
||||
workflow_dispatch: # allows manual triggering
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # Nightly 6AM UTC
|
||||
- cron: "0 6 * * *" # Nightly 6AM UTC
|
||||
|
||||
# Prevent concurrent runs of this workflow.
|
||||
concurrency:
|
||||
|
|
@ -41,11 +41,10 @@ jobs:
|
|||
with:
|
||||
# Run GitOps in dry-run mode for pull requests.
|
||||
dry-run-only: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
|
||||
|
||||
|
||||
# These environment variables can be set as repository secrets,
|
||||
# alongside any other environment variables mentioned in your .yml files.
|
||||
env:
|
||||
|
||||
###########################################################
|
||||
# The server URL where Fleet is running.
|
||||
#
|
||||
|
|
@ -60,7 +59,7 @@ jobs:
|
|||
# will have to take action to restore MDM functionality (due
|
||||
# to the way Apple's device management protocol works.)
|
||||
###########################################################
|
||||
FLEET_URL: ${{ secrets.FLEET_URL && secrets.FLEET_URL || "https://fleet.example.com" }}
|
||||
FLEET_URL: ${{ secrets.FLEET_URL && secrets.FLEET_URL || 'https://fleet.example.com' }}
|
||||
|
||||
###########################################################
|
||||
# A Fleet API token that your CI/CD builds will use
|
||||
|
|
@ -74,5 +73,3 @@ jobs:
|
|||
# or that never expires.)
|
||||
###########################################################
|
||||
FLEET_API_TOKEN: ${{ secrets.FLEET_API_TOKEN }}
|
||||
|
||||
|
||||
|
|
@ -24,4 +24,4 @@ fleet-gitops:
|
|||
npm install -g fleetctl
|
||||
fi
|
||||
- fleetctl config set --address $FLEET_URL --token $FLEET_API_TOKEN
|
||||
- ./.github/fleet-gitops/gitops.sh
|
||||
- ./.github/fleet-gitops/gitops.sh
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Fleet
|
||||
# Fleet
|
||||
|
||||
These files allow you to configure, patch, and secure computing devices for your organization.
|
||||
|
||||
|
|
@ -1,28 +1,27 @@
|
|||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
#
|
||||
# default.yml
|
||||
#
|
||||
#
|
||||
# Use this global manifest (`default.yml`) to configure
|
||||
# top-level settings for your organization as a whole, and
|
||||
# controls/reports/etc that apply to all of your fleets.
|
||||
# controls/reports/etc that apply to all of your fleets.
|
||||
#
|
||||
# To see all supported options, check out:
|
||||
# • https://fleetdm.com/docs/configuration/yaml-files
|
||||
#
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
|
||||
org_settings:
|
||||
org_settings:
|
||||
org_info:
|
||||
###########################################################
|
||||
# The name of your organization is displayed to end users
|
||||
# during the setup experience for new hardware, and to admins
|
||||
# in the Fleet UI.
|
||||
#
|
||||
#
|
||||
# Read more:
|
||||
# • https://fleetdm.com/docs/configuration/yaml-files#org-info
|
||||
###########################################################
|
||||
org_name: "My organization"
|
||||
# ^TODO: Replace with the name of your organization
|
||||
org_name: <%= .org_name %>
|
||||
|
||||
server_settings:
|
||||
###########################################################
|
||||
|
|
@ -36,7 +35,7 @@ org_settings:
|
|||
|
||||
###########################################################
|
||||
# Uncomment to use single sign-on (SSO) for admins accessing Fleet.
|
||||
#
|
||||
#
|
||||
# Read more:
|
||||
# • https://fleetdm.com/docs/configuration/yaml-files#sso-settings
|
||||
# • https://fleetdm.com/docs/deploy/single-sign-on-sso
|
||||
|
|
@ -48,7 +47,7 @@ org_settings:
|
|||
|
||||
mdm:
|
||||
###########################################################
|
||||
# Uncomment to use single-sign on (SSO) to authenticate
|
||||
# Uncomment to use single sign-on (SSO) to authenticate
|
||||
# new computers enrolling in Fleet during end user setup.
|
||||
#
|
||||
# Read more:
|
||||
|
|
@ -59,7 +58,7 @@ org_settings:
|
|||
# idp_name: "Okta" e.g. "Entra", "Okta", "Google Workspace", etc. (Displayed to end users.)
|
||||
# metadata_url: "https://okta.com/replace-this-url" # This must exactly match the "IdP metadata URL" provided by your identity provider (IdP) when setting up this integration.
|
||||
# entity_id: "fleet-end-users" # This must exactly match the "Entity ID" field you chose when setting up this integration in your identity provider (IdP).
|
||||
|
||||
|
||||
###########################################################
|
||||
# Uncomment when you are ready to start using zero-touch enrollment
|
||||
# for Apple devices via Apple Business Manager (ABM).
|
||||
|
|
@ -70,7 +69,7 @@ org_settings:
|
|||
###########################################################
|
||||
# apple_business_manager:
|
||||
# - macos_fleet: "💻 Workstations" # Where new macOS devices from ABM will appear
|
||||
|
||||
|
||||
###########################################################
|
||||
# Uncomment to start using Apple's volume purchase program (VPP)
|
||||
# for making software available from the App Store and managing
|
||||
|
|
@ -90,7 +89,7 @@ controls:
|
|||
# Uncomment when you are ready to migrate Macs to Fleet
|
||||
# from a different MDM server, especially devices running
|
||||
# macOS ≤v26.
|
||||
#
|
||||
#
|
||||
# For more information about what this does, see:
|
||||
# • https://fleetdm.com/docs/configuration/yaml-files#macos-migration
|
||||
# • https://fleetdm.com/guides/mdm-migration#end-user-workflow
|
||||
|
|
@ -127,4 +126,3 @@ controls:
|
|||
###########################################################
|
||||
labels:
|
||||
- paths: ./labels/*.yml
|
||||
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
#
|
||||
# personal-mobile-devices.yml
|
||||
#
|
||||
#
|
||||
# Use this fleet manifest to configure controls, software,
|
||||
# and settings that apply only to computing devices (hosts)
|
||||
# in this particular fleet.
|
||||
#
|
||||
#
|
||||
# > Note: By convention, the "📱🔐 Personal mobile devices"
|
||||
# > fleet is where employee-owned iPhone and Android phones
|
||||
# > are enrolled, for example as part of a BYOD ("bring your
|
||||
|
|
@ -20,7 +20,7 @@ name: "📱🔐 Personal mobile devices"
|
|||
controls:
|
||||
setup_experience:
|
||||
###########################################################
|
||||
# Uncomment to use single-sign on (SSO) to authenticate
|
||||
# Uncomment to use single-sign on (SSO) to authenticate
|
||||
# end users enrolling their personal device.
|
||||
#
|
||||
# Read more:
|
||||
|
|
@ -72,7 +72,7 @@ controls:
|
|||
# Apps to make available from the Apple App Store or Google Play
|
||||
###########################################################
|
||||
software:
|
||||
app_store_apps:
|
||||
app_store_apps:
|
||||
###########################################################
|
||||
# iOS apps
|
||||
###########################################################
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
#
|
||||
# workstations.yml
|
||||
#
|
||||
#
|
||||
# Use this fleet manifest to configure controls, software,
|
||||
# automations, policies, and reports that apply only to
|
||||
# computing devices (hosts) in this particular fleet.
|
||||
#
|
||||
#
|
||||
# > Note: By convention, the "💻 Workstations" fleet is
|
||||
# > where all company-owned laptops, PCs, and other
|
||||
# > productivity endpoints (i.e. computers) are enrolled.
|
||||
|
|
@ -19,21 +19,24 @@ controls:
|
|||
setup_experience:
|
||||
###########################################################
|
||||
# Configure the macOS setup experience
|
||||
#
|
||||
#
|
||||
# (Optional) edit the automatic enrollment profile referenced
|
||||
# here to change which items are skipped during macOS setup
|
||||
# and other aspects of the end user's experience during
|
||||
# their first few minutes with their new Mac.
|
||||
#
|
||||
# Note: This requires activating MDM in your Fleet instance.
|
||||
#
|
||||
# For more, see:
|
||||
# • https://fleetdm.com/guides/apple-mdm-setup
|
||||
# • https://fleetdm.com/docs/configuration/yaml-files#macos-setup
|
||||
# • https://developer.apple.com/documentation/devicemanagement/profile
|
||||
# • https://support.apple.com/guide/deployment/automated-device-enrollment-management-dep73069dd57/web
|
||||
###########################################################
|
||||
apple_setup_assistant: ../platforms/macos/enrollment-profiles/automatic-enrollment.dep.json
|
||||
|
||||
# apple_setup_assistant: ../platforms/macos/enrollment-profiles/automatic-enrollment.dep.json
|
||||
|
||||
###########################################################
|
||||
# Uncomment to use single-sign on (SSO) to authenticate
|
||||
# Uncomment to use single-sign on (SSO) to authenticate
|
||||
# end users during first-time setup of new computers.
|
||||
#
|
||||
# Read more:
|
||||
|
|
@ -44,19 +47,23 @@ controls:
|
|||
|
||||
###########################################################
|
||||
# Configuration profiles
|
||||
#
|
||||
#
|
||||
# Note: This requires activating MDM in your Fleet instance.
|
||||
#
|
||||
# For more, see:
|
||||
# • https://fleetdm.com/guides/apple-mdm-setup
|
||||
# • https://fleetdm.com/guides/windows-mdm-setup
|
||||
# • https://fleetdm.com/docs/configuration/yaml-files#apple-settings-and-windows-settings
|
||||
#
|
||||
# Note: Instead of including all profiles with `paths`,
|
||||
# you can also switch to using `path` and including each
|
||||
# specific configuration profile one by one, which allows
|
||||
# for scoping using labels. For example:
|
||||
# Profiles can be scoped using labels. For example:
|
||||
# ```
|
||||
# - path: ../platforms/macos/configuration-profiles/1password-managed-settings.mobileconfig
|
||||
# labels_include_any:
|
||||
# - "Macs with 1Password installed"
|
||||
# ```
|
||||
# - paths: ../platforms/windows/configuration-profiles/*.xml
|
||||
# labels_include_all:
|
||||
# - "x86-based Windows hosts"
|
||||
# ```
|
||||
###########################################################
|
||||
apple_settings:
|
||||
configuration_profiles:
|
||||
|
|
@ -65,7 +72,7 @@ controls:
|
|||
windows_settings:
|
||||
configuration_profiles:
|
||||
- paths: ../platforms/windows/configuration-profiles/*.xml
|
||||
|
||||
|
||||
###########################################################
|
||||
# Managed disk encryption
|
||||
#
|
||||
|
|
@ -98,7 +105,7 @@ controls:
|
|||
|
||||
###########################################################
|
||||
# Script library
|
||||
#
|
||||
#
|
||||
# Note: You probably don't need to change the next few lines.
|
||||
#
|
||||
# > To make a script available for use with Fleet for helpdesk
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
critical: false
|
||||
description: This Mac may have outdated system software, which could lead to security vulnerabilities, performance issues, and incompatibility with other systems.
|
||||
resolution: |
|
||||
Please take some time and run all available updates from Software Update ( > System Settings > Software Update) and all available app store updates ( > App Store > Updates).
|
||||
|
||||
Please take some time and run all available updates from Software Update ( > System Settings > Software Update) and all available app store updates ( > App Store > Updates).
|
||||
|
||||
If you see the message "This feature isn't available with the Apple Account you're currently using." you will need to delete and re-install the app.
|
||||
platform: darwin
|
||||
platform: darwin
|
||||
2
go.mod
2
go.mod
|
|
@ -94,6 +94,7 @@ require (
|
|||
github.com/kolide/launcher v1.0.12
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/macadmins/osquery-extension v1.3.2
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/micromdm/micromdm v1.9.0
|
||||
github.com/micromdm/nanolib v0.2.0
|
||||
|
|
@ -226,6 +227,7 @@ require (
|
|||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/containerd/containerd/api v1.8.0 // indirect
|
||||
|
|
|
|||
9
go.sum
9
go.sum
|
|
@ -201,6 +201,12 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
|
|||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
|
||||
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
|
|
@ -591,6 +597,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
|
|||
github.com/macadmins/osquery-extension v1.3.2 h1:X6x4jD6Te9uYW1s7LQPjrux5G6WC78UF/6p4qFw2Mb8=
|
||||
github.com/macadmins/osquery-extension v1.3.2/go.mod h1:/4WhG7sh9qyEi2WkacxOUJAmVciiDFmT468MbkiXBfE=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
||||
|
|
@ -1021,6 +1029,7 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
|||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
|
|||
|
|
@ -160,13 +160,8 @@ func YamlUnmarshal(yamlBytes []byte, out any) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type BaseItem struct {
|
||||
Path *string `json:"path"`
|
||||
Paths *string `json:"paths"`
|
||||
}
|
||||
|
||||
type GitOpsControls struct {
|
||||
BaseItem
|
||||
fleet.BaseItem
|
||||
MacOSUpdates any `json:"macos_updates"`
|
||||
IOSUpdates any `json:"ios_updates"`
|
||||
IPadOSUpdates any `json:"ipados_updates"`
|
||||
|
|
@ -184,10 +179,10 @@ type GitOpsControls struct {
|
|||
AndroidEnabledAndConfigured any `json:"android_enabled_and_configured"`
|
||||
AndroidSettings any `json:"android_settings"`
|
||||
|
||||
EnableDiskEncryption any `json:"enable_disk_encryption"`
|
||||
EnableRecoveryLockPassword any `json:"enable_recovery_lock_password"`
|
||||
RequireBitLockerPIN any `json:"windows_require_bitlocker_pin,omitempty"`
|
||||
Scripts []BaseItem `json:"scripts"`
|
||||
EnableDiskEncryption any `json:"enable_disk_encryption"`
|
||||
EnableRecoveryLockPassword any `json:"enable_recovery_lock_password"`
|
||||
RequireBitLockerPIN any `json:"windows_require_bitlocker_pin,omitempty"`
|
||||
Scripts []fleet.BaseItem `json:"scripts"`
|
||||
|
||||
Defined bool
|
||||
}
|
||||
|
|
@ -202,7 +197,7 @@ func (c GitOpsControls) Set() bool {
|
|||
}
|
||||
|
||||
type Policy struct {
|
||||
BaseItem
|
||||
fleet.BaseItem
|
||||
GitOpsPolicySpec
|
||||
}
|
||||
|
||||
|
|
@ -233,12 +228,12 @@ type PolicyInstallSoftware struct {
|
|||
}
|
||||
|
||||
type Query struct {
|
||||
BaseItem
|
||||
fleet.BaseItem
|
||||
fleet.QuerySpec
|
||||
}
|
||||
|
||||
type Label struct {
|
||||
BaseItem
|
||||
fleet.BaseItem
|
||||
fleet.LabelSpec
|
||||
}
|
||||
|
||||
|
|
@ -268,29 +263,10 @@ func (l *Label) UnmarshalJSON(data []byte) error {
|
|||
}
|
||||
|
||||
type SoftwarePackage struct {
|
||||
BaseItem
|
||||
fleet.BaseItem
|
||||
fleet.SoftwarePackageSpec
|
||||
}
|
||||
|
||||
// SupportsFileInclude is implemented by types that embed BaseItem and can
|
||||
// reference external files via path/paths fields.
|
||||
type SupportsFileInclude interface {
|
||||
GetBaseItem() BaseItem
|
||||
SetBaseItem(v BaseItem)
|
||||
}
|
||||
|
||||
// GetBaseItem returns the current BaseItem value.
|
||||
// Types that embed BaseItem inherit this method via promotion.
|
||||
func (b *BaseItem) GetBaseItem() BaseItem {
|
||||
return *b
|
||||
}
|
||||
|
||||
// SetBaseItem sets the BaseItem value.
|
||||
// Types that embed BaseItem inherit this method via promotion.
|
||||
func (b *BaseItem) SetBaseItem(v BaseItem) {
|
||||
*b = v
|
||||
}
|
||||
|
||||
func (spec SoftwarePackage) HydrateToPackageLevel(packageLevel fleet.SoftwarePackageSpec) (fleet.SoftwarePackageSpec, error) {
|
||||
if spec.Icon.Path != "" || spec.InstallScript.Path != "" || spec.UninstallScript.Path != "" ||
|
||||
spec.PostInstallScript.Path != "" || spec.URL != "" || spec.SHA256 != "" || spec.PreInstallQuery.Path != "" {
|
||||
|
|
@ -575,7 +551,7 @@ const (
|
|||
)
|
||||
|
||||
func parseOrgSettings(raw json.RawMessage, result *GitOps, baseDir string, filePath string, multiError *multierror.Error) *multierror.Error {
|
||||
var orgSettingsTop BaseItem
|
||||
var orgSettingsTop fleet.BaseItem
|
||||
if err := json.Unmarshal(raw, &orgSettingsTop); err != nil {
|
||||
return multierror.Append(multiError, MaybeParseTypeError(filePath, []string{"org_settings"}, err))
|
||||
}
|
||||
|
|
@ -596,7 +572,7 @@ func parseOrgSettings(raw json.RawMessage, result *GitOps, baseDir string, fileP
|
|||
multiError, fmt.Errorf("failed to expand environment in file %s: %v", *orgSettingsTop.Path, err),
|
||||
)
|
||||
} else {
|
||||
var pathOrgSettings BaseItem
|
||||
var pathOrgSettings fleet.BaseItem
|
||||
if err := YamlUnmarshal(fileBytes, &pathOrgSettings); err != nil {
|
||||
noError = false
|
||||
multiError = multierror.Append(
|
||||
|
|
@ -631,7 +607,7 @@ func parseOrgSettings(raw json.RawMessage, result *GitOps, baseDir string, fileP
|
|||
}
|
||||
|
||||
func parseTeamSettings(raw json.RawMessage, result *GitOps, baseDir string, filePath string, multiError *multierror.Error) *multierror.Error {
|
||||
var teamSettingsTop BaseItem
|
||||
var teamSettingsTop fleet.BaseItem
|
||||
if err := json.Unmarshal(raw, &teamSettingsTop); err != nil {
|
||||
return multierror.Append(multiError, MaybeParseTypeError(filePath, []string{"settings"}, err))
|
||||
}
|
||||
|
|
@ -652,7 +628,7 @@ func parseTeamSettings(raw json.RawMessage, result *GitOps, baseDir string, file
|
|||
multiError, fmt.Errorf("failed to expand environment in file %s: %v", *teamSettingsTop.Path, err),
|
||||
)
|
||||
} else {
|
||||
var pathTeamSettings BaseItem
|
||||
var pathTeamSettings fleet.BaseItem
|
||||
if err := YamlUnmarshal(fileBytes, &pathTeamSettings); err != nil {
|
||||
noError = false
|
||||
multiError = multierror.Append(
|
||||
|
|
@ -847,7 +823,7 @@ func parseAgentOptions(top map[string]json.RawMessage, result *GitOps, baseDir s
|
|||
} else if !ok {
|
||||
return multierror.Append(multiError, errors.New("'agent_options' is required"))
|
||||
}
|
||||
var agentOptionsTop BaseItem
|
||||
var agentOptionsTop fleet.BaseItem
|
||||
if err := json.Unmarshal(agentOptionsRaw, &agentOptionsTop); err != nil {
|
||||
multiError = multierror.Append(multiError, MaybeParseTypeError(filePath, []string{"agent_options"}, err))
|
||||
} else {
|
||||
|
|
@ -865,7 +841,7 @@ func parseAgentOptions(top map[string]json.RawMessage, result *GitOps, baseDir s
|
|||
multiError, fmt.Errorf("failed to expand environment in file %s: %v", *agentOptionsTop.Path, err),
|
||||
)
|
||||
} else {
|
||||
var pathAgentOptions BaseItem
|
||||
var pathAgentOptions fleet.BaseItem
|
||||
if err := YamlUnmarshal(fileBytes, &pathAgentOptions); err != nil {
|
||||
return multierror.Append(
|
||||
multiError, MaybeParseTypeError(*agentOptionsTop.Path, []string{"agent_options"}, err),
|
||||
|
|
@ -965,8 +941,17 @@ func parseControls(top map[string]json.RawMessage, result *GitOps, logFn Logf, y
|
|||
return multierror.Append(multiError, MaybeParseTypeError(controlsFilePath, []string{"controls", "macos_settings"}, err))
|
||||
}
|
||||
|
||||
// Expand globs in profile paths.
|
||||
var errs []error
|
||||
macOSSettings.CustomSettings, errs = expandBaseItems(macOSSettings.CustomSettings, controlsDir, "profile", GlobExpandOptions{
|
||||
AllowedExtensions: map[string]bool{".mobileconfig": true, ".json": true},
|
||||
LogFn: logFn,
|
||||
})
|
||||
multiError = multierror.Append(multiError, errs...)
|
||||
// Then resolve the paths to absolute and find Fleet secrets in the profile files.
|
||||
for i := range macOSSettings.CustomSettings {
|
||||
err := resolveAndUpdateProfilePathToAbsolute(controlsDir, &macOSSettings.CustomSettings[i], result)
|
||||
|
||||
err := resolveAndUpdateProfilePath(&macOSSettings.CustomSettings[i], result)
|
||||
if err != nil {
|
||||
return multierror.Append(multiError, err)
|
||||
}
|
||||
|
|
@ -991,8 +976,16 @@ func parseControls(top map[string]json.RawMessage, result *GitOps, logFn Logf, y
|
|||
return multierror.Append(multiError, MaybeParseTypeError(controlsFilePath, []string{"controls", "windows_settings"}, err))
|
||||
}
|
||||
if windowsSettings.CustomSettings.Valid {
|
||||
var errs []error
|
||||
windowsSettings.CustomSettings.Value, errs = expandBaseItems(windowsSettings.CustomSettings.Value, controlsDir, "profile", GlobExpandOptions{
|
||||
AllowedExtensions: map[string]bool{".xml": true},
|
||||
|
||||
LogFn: logFn,
|
||||
})
|
||||
multiError = multierror.Append(multiError, errs...)
|
||||
|
||||
for i := range windowsSettings.CustomSettings.Value {
|
||||
err := resolveAndUpdateProfilePathToAbsolute(controlsDir, &windowsSettings.CustomSettings.Value[i], result)
|
||||
err := resolveAndUpdateProfilePath(&windowsSettings.CustomSettings.Value[i], result)
|
||||
if err != nil {
|
||||
return multierror.Append(multiError, err)
|
||||
}
|
||||
|
|
@ -1020,8 +1013,14 @@ func parseControls(top map[string]json.RawMessage, result *GitOps, logFn Logf, y
|
|||
}
|
||||
|
||||
if androidSettings.CustomSettings.Valid {
|
||||
var errs []error
|
||||
androidSettings.CustomSettings.Value, errs = expandBaseItems(androidSettings.CustomSettings.Value, controlsDir, "profile", GlobExpandOptions{
|
||||
AllowedExtensions: map[string]bool{".json": true},
|
||||
LogFn: logFn,
|
||||
})
|
||||
multiError = multierror.Append(multiError, errs...)
|
||||
for i := range androidSettings.CustomSettings.Value {
|
||||
err := resolveAndUpdateProfilePathToAbsolute(controlsDir, &androidSettings.CustomSettings.Value[i], result)
|
||||
err := resolveAndUpdateProfilePath(&androidSettings.CustomSettings.Value[i], result)
|
||||
if err != nil {
|
||||
return multierror.Append(multiError, err)
|
||||
}
|
||||
|
|
@ -1091,18 +1090,16 @@ func processControlsPathIfNeeded(controlsTop GitOpsControls, result *GitOps, con
|
|||
return errs
|
||||
}
|
||||
|
||||
func resolveAndUpdateProfilePathToAbsolute(controlsDir string, profile *fleet.MDMProfileSpec, result *GitOps) error {
|
||||
resolvedPath := resolveApplyRelativePath(controlsDir, profile.Path)
|
||||
// We switch to absolute path so that we don't have to keep track of the base directory.
|
||||
// This is useful because controls section can come from either the global config file or the no-team file.
|
||||
func resolveAndUpdateProfilePath(profile *fleet.MDMProfileSpec, result *GitOps) error {
|
||||
// Path has already been resolved by expandBaseItems; just ensure it's absolute.
|
||||
var err error
|
||||
profile.Path, err = filepath.Abs(resolvedPath)
|
||||
profile.Path, err = filepath.Abs(profile.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve profile path %s: %v", resolvedPath, err)
|
||||
return fmt.Errorf("failed to resolve profile path %s: %v", profile.Path, err)
|
||||
}
|
||||
fileBytes, err := os.ReadFile(resolvedPath)
|
||||
fileBytes, err := os.ReadFile(profile.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read profile file %s: %v", resolvedPath, err)
|
||||
return fmt.Errorf("failed to read profile file %s: %v", profile.Path, err)
|
||||
}
|
||||
err = LookupEnvSecrets(string(fileBytes), result.FleetSecrets)
|
||||
if err != nil {
|
||||
|
|
@ -1194,7 +1191,7 @@ func expandGlobPattern(pattern string, baseDir string, entityType string, opts G
|
|||
// problems in one pass.
|
||||
func expandBaseItems[T any, PT interface {
|
||||
*T
|
||||
SupportsFileInclude
|
||||
fleet.SupportsFileInclude
|
||||
}](inputEntities []T, baseDir string, entityType string, opts GlobExpandOptions) ([]T, []error) {
|
||||
opts.setDefaults()
|
||||
var result []T
|
||||
|
|
@ -1233,7 +1230,7 @@ func expandBaseItems[T any, PT interface {
|
|||
}
|
||||
seenBasenames[base] = *baseItem.Path
|
||||
}
|
||||
PT(&entity).SetBaseItem(BaseItem{Path: &resolved})
|
||||
PT(&entity).SetBaseItem(fleet.BaseItem{Path: &resolved})
|
||||
result = append(result, entity)
|
||||
// Glob -- expand and add files to result.
|
||||
case hasPaths:
|
||||
|
|
@ -1260,8 +1257,8 @@ func expandBaseItems[T any, PT interface {
|
|||
}
|
||||
seenBasenames[base] = *baseItem.Paths
|
||||
}
|
||||
var newItem T
|
||||
PT(&newItem).SetBaseItem(BaseItem{Path: &p})
|
||||
newItem := entity // clone to preserve non-BaseItem fields (e.g. labels)
|
||||
PT(&newItem).SetBaseItem(fleet.BaseItem{Path: &p})
|
||||
result = append(result, newItem)
|
||||
}
|
||||
}
|
||||
|
|
@ -1270,7 +1267,7 @@ func expandBaseItems[T any, PT interface {
|
|||
return result, errs
|
||||
}
|
||||
|
||||
func resolveScriptPaths(input []BaseItem, baseDir string, logFn Logf) ([]BaseItem, []error) {
|
||||
func resolveScriptPaths(input []fleet.BaseItem, baseDir string, logFn Logf) ([]fleet.BaseItem, []error) {
|
||||
return expandBaseItems(input, baseDir, "script", GlobExpandOptions{
|
||||
AllowedExtensions: allowedScriptExtensions,
|
||||
RequireUniqueBasenames: true,
|
||||
|
|
@ -1502,7 +1499,7 @@ func parsePolicies(top map[string]json.RawMessage, result *GitOps, baseDir strin
|
|||
return multiError
|
||||
}
|
||||
|
||||
func parsePolicyRunScript(baseDir string, parentFilePath string, teamName *string, policy *Policy, scripts []BaseItem) error {
|
||||
func parsePolicyRunScript(baseDir string, parentFilePath string, teamName *string, policy *Policy, scripts []fleet.BaseItem) error {
|
||||
if policy.RunScript == nil {
|
||||
policy.ScriptID = ptr.Uint(0) // unset the script
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -646,7 +646,7 @@ func TestInvalidGitOpsYaml(t *testing.T) {
|
|||
config = getConfig([]string{"settings"})
|
||||
config += fmt.Sprintf("%s:\n path: %s\n", "settings", tmpFile.Name())
|
||||
_, err = gitOpsFromString(t, config)
|
||||
assert.ErrorContains(t, err, "expected type spec.BaseItem but got array")
|
||||
assert.ErrorContains(t, err, "expected type fleet.BaseItem but got array")
|
||||
|
||||
// Invalid secrets 1
|
||||
config = getConfig([]string{"settings"})
|
||||
|
|
@ -818,7 +818,7 @@ func TestInvalidGitOpsYaml(t *testing.T) {
|
|||
config = getConfig([]string{"org_settings"})
|
||||
config += fmt.Sprintf("%s:\n path: %s\n", "org_settings", tmpFile.Name())
|
||||
_, err = gitOpsFromString(t, config)
|
||||
assert.ErrorContains(t, err, "expected type spec.BaseItem but got array")
|
||||
assert.ErrorContains(t, err, "expected type fleet.BaseItem but got array")
|
||||
|
||||
// Invalid secrets 1
|
||||
config = getConfig([]string{"org_settings"})
|
||||
|
|
@ -873,7 +873,7 @@ func TestInvalidGitOpsYaml(t *testing.T) {
|
|||
config = getConfig([]string{"agent_options"})
|
||||
config += fmt.Sprintf("%s:\n path: %s\n", "agent_options", tmpFile.Name())
|
||||
_, err = gitOpsFromString(t, config)
|
||||
assert.ErrorContains(t, err, "expected type spec.BaseItem but got array")
|
||||
assert.ErrorContains(t, err, "expected type fleet.BaseItem but got array")
|
||||
|
||||
// Invalid controls
|
||||
config = getConfig([]string{"controls"})
|
||||
|
|
@ -1793,7 +1793,7 @@ func TestExpandBaseItems(t *testing.T) {
|
|||
require.NoError(t, os.WriteFile(filepath.Join(dir, "b.yml"), []byte(""), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "c.txt"), []byte(""), 0o644))
|
||||
|
||||
items := []BaseItem{{Paths: ptr.String("*.yml")}}
|
||||
items := []fleet.BaseItem{{Paths: ptr.String("*.yml")}} //nolint:modernize
|
||||
result, errs := expandBaseItems(items, dir, "test", GlobExpandOptions{})
|
||||
require.Empty(t, errs)
|
||||
require.Len(t, result, 2)
|
||||
|
|
@ -1811,7 +1811,7 @@ func TestExpandBaseItems(t *testing.T) {
|
|||
require.NoError(t, os.WriteFile(filepath.Join(dir, "top.yml"), []byte(""), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subdir, "nested.yml"), []byte(""), 0o644))
|
||||
|
||||
items := []BaseItem{{Paths: ptr.String("**/*.yml")}}
|
||||
items := []fleet.BaseItem{{Paths: ptr.String("**/*.yml")}} //nolint:modernize
|
||||
result, errs := expandBaseItems(items, dir, "test", GlobExpandOptions{})
|
||||
require.Empty(t, errs)
|
||||
require.Len(t, result, 2)
|
||||
|
|
@ -1826,9 +1826,9 @@ func TestExpandBaseItems(t *testing.T) {
|
|||
require.NoError(t, os.WriteFile(filepath.Join(dir, "glob1.yaml"), []byte(""), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "glob2.yaml"), []byte(""), 0o644))
|
||||
|
||||
items := []BaseItem{
|
||||
{Path: ptr.String("single.yml")},
|
||||
{Paths: ptr.String("*.yaml")},
|
||||
items := []fleet.BaseItem{
|
||||
{Path: ptr.String("single.yml")}, //nolint:modernize
|
||||
{Paths: ptr.String("*.yaml")}, //nolint:modernize
|
||||
}
|
||||
result, errs := expandBaseItems(items, dir, "test", GlobExpandOptions{})
|
||||
require.Empty(t, errs)
|
||||
|
|
@ -1840,28 +1840,28 @@ func TestExpandBaseItems(t *testing.T) {
|
|||
|
||||
t.Run("paths_without_glob_error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []BaseItem{{Paths: ptr.String("foo.yml")}}
|
||||
items := []fleet.BaseItem{{Paths: ptr.String("foo.yml")}} //nolint:modernize
|
||||
_, errs := expandBaseItems(items, "/tmp", "test", GlobExpandOptions{})
|
||||
requireErrorContains(t, errs, `does not contain glob characters`)
|
||||
})
|
||||
|
||||
t.Run("path_with_glob_error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []BaseItem{{Path: ptr.String("*.yml")}}
|
||||
items := []fleet.BaseItem{{Path: ptr.String("*.yml")}} //nolint:modernize
|
||||
_, errs := expandBaseItems(items, "/tmp", "test", GlobExpandOptions{})
|
||||
requireErrorContains(t, errs, `contains glob characters`)
|
||||
})
|
||||
|
||||
t.Run("both_path_and_paths_error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []BaseItem{{Path: ptr.String("foo.yml"), Paths: ptr.String("*.yml")}}
|
||||
items := []fleet.BaseItem{{Path: ptr.String("foo.yml"), Paths: ptr.String("*.yml")}} //nolint:modernize
|
||||
_, errs := expandBaseItems(items, "/tmp", "test", GlobExpandOptions{})
|
||||
requireErrorContains(t, errs, `cannot have both "path" and "paths"`)
|
||||
})
|
||||
|
||||
t.Run("inline_items_passed_through", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []BaseItem{{}}
|
||||
items := []fleet.BaseItem{{}}
|
||||
result, errs := expandBaseItems(items, "/tmp", "test", GlobExpandOptions{})
|
||||
require.Empty(t, errs)
|
||||
require.Len(t, result, 1)
|
||||
|
|
@ -1871,7 +1871,7 @@ func TestExpandBaseItems(t *testing.T) {
|
|||
|
||||
t.Run("require_file_reference_error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []BaseItem{{}}
|
||||
items := []fleet.BaseItem{{}}
|
||||
_, errs := expandBaseItems(items, "/tmp", "test", GlobExpandOptions{
|
||||
RequireFileReference: true,
|
||||
})
|
||||
|
|
@ -1885,7 +1885,7 @@ func TestExpandBaseItems(t *testing.T) {
|
|||
logFn := func(format string, args ...any) {
|
||||
warnings = append(warnings, fmt.Sprintf(format, args...))
|
||||
}
|
||||
items := []BaseItem{{Paths: ptr.String("*.yml")}}
|
||||
items := []fleet.BaseItem{{Paths: ptr.String("*.yml")}} //nolint:modernize
|
||||
result, errs := expandBaseItems(items, dir, "test", GlobExpandOptions{LogFn: logFn})
|
||||
require.Empty(t, errs)
|
||||
assert.Empty(t, result)
|
||||
|
|
@ -1903,7 +1903,7 @@ func TestExpandBaseItems(t *testing.T) {
|
|||
require.NoError(t, os.WriteFile(filepath.Join(sub1, "dup.yml"), []byte(""), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(sub2, "dup.yml"), []byte(""), 0o644))
|
||||
|
||||
items := []BaseItem{{Paths: ptr.String("**/*.yml")}}
|
||||
items := []fleet.BaseItem{{Paths: ptr.String("**/*.yml")}} //nolint:modernize
|
||||
_, errs := expandBaseItems(items, dir, "test", GlobExpandOptions{
|
||||
RequireUniqueBasenames: true,
|
||||
})
|
||||
|
|
@ -1918,9 +1918,9 @@ func TestExpandBaseItems(t *testing.T) {
|
|||
require.NoError(t, os.WriteFile(filepath.Join(dir, "item.yml"), []byte(""), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(sub, "item.yml"), []byte(""), 0o644))
|
||||
|
||||
items := []BaseItem{
|
||||
{Path: ptr.String("item.yml")},
|
||||
{Paths: ptr.String("sub/*.yml")},
|
||||
items := []fleet.BaseItem{
|
||||
{Path: ptr.String("item.yml")}, //nolint:modernize
|
||||
{Paths: ptr.String("sub/*.yml")}, //nolint:modernize
|
||||
}
|
||||
_, errs := expandBaseItems(items, dir, "test", GlobExpandOptions{
|
||||
RequireUniqueBasenames: true,
|
||||
|
|
@ -1941,7 +1941,7 @@ func TestExpandBaseItems(t *testing.T) {
|
|||
warnings = append(warnings, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
items := []BaseItem{{Paths: ptr.String("*")}}
|
||||
items := []fleet.BaseItem{{Paths: ptr.String("*")}} //nolint:modernize
|
||||
result, errs := expandBaseItems(items, dir, "test", GlobExpandOptions{
|
||||
AllowedExtensions: map[string]bool{".sh": true},
|
||||
LogFn: logFn,
|
||||
|
|
@ -1959,7 +1959,7 @@ func TestExpandBaseItems(t *testing.T) {
|
|||
require.NoError(t, os.WriteFile(filepath.Join(dir, "a.yml"), []byte(""), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "m.yml"), []byte(""), 0o644))
|
||||
|
||||
items := []BaseItem{{Paths: ptr.String("*.yml")}}
|
||||
items := []fleet.BaseItem{{Paths: ptr.String("*.yml")}} //nolint:modernize
|
||||
result, errs := expandBaseItems(items, dir, "test", GlobExpandOptions{})
|
||||
require.Empty(t, errs)
|
||||
require.Len(t, result, 3)
|
||||
|
|
@ -1970,7 +1970,7 @@ func TestExpandBaseItems(t *testing.T) {
|
|||
|
||||
t.Run("multiple_errors_collected", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []BaseItem{{Path: ptr.String("*.yml")}, {Paths: ptr.String("noglob.yml")}}
|
||||
items := []fleet.BaseItem{{Path: ptr.String("*.yml")}, {Paths: ptr.String("noglob.yml")}} //nolint:modernize
|
||||
_, errs := expandBaseItems(items, "", "test", GlobExpandOptions{})
|
||||
require.Len(t, errs, 2)
|
||||
assert.Contains(t, errs[0].Error(), `contains glob characters`)
|
||||
|
|
@ -1986,7 +1986,7 @@ func TestResolveScriptPaths(t *testing.T) {
|
|||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "script.sh"), []byte("#!/bin/bash"), 0o644))
|
||||
|
||||
items := []BaseItem{{Path: ptr.String("script.sh")}}
|
||||
items := []fleet.BaseItem{{Path: ptr.String("script.sh")}} //nolint:modernize
|
||||
result, errs := resolveScriptPaths(items, dir, nopLogf)
|
||||
require.Empty(t, errs)
|
||||
require.Len(t, result, 1)
|
||||
|
|
@ -1999,7 +1999,7 @@ func TestResolveScriptPaths(t *testing.T) {
|
|||
require.NoError(t, os.WriteFile(filepath.Join(dir, "a.sh"), []byte("#!/bin/bash"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "b.sh"), []byte("#!/bin/bash"), 0o644))
|
||||
|
||||
items := []BaseItem{{Paths: ptr.String("*.sh")}}
|
||||
items := []fleet.BaseItem{{Paths: ptr.String("*.sh")}} //nolint:modernize
|
||||
result, errs := resolveScriptPaths(items, dir, nopLogf)
|
||||
require.Empty(t, errs)
|
||||
require.Len(t, result, 2)
|
||||
|
|
@ -2007,7 +2007,7 @@ func TestResolveScriptPaths(t *testing.T) {
|
|||
|
||||
t.Run("inline_not_allowed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []BaseItem{{}}
|
||||
items := []fleet.BaseItem{{}}
|
||||
_, errs := resolveScriptPaths(items, "/tmp", nopLogf)
|
||||
require.NotEmpty(t, errs)
|
||||
assert.Contains(t, errs[0].Error(), `no "path" or "paths" field`)
|
||||
|
|
@ -2193,6 +2193,127 @@ func TestGitOpsGlobScripts(t *testing.T) {
|
|||
assert.Equal(t, filepath.Join(scriptsDir, "gamma.ps1"), *result.Controls.Scripts[2].Path)
|
||||
}
|
||||
|
||||
func TestGitOpsGlobProfiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("macos_profiles", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
profilesDir := filepath.Join(dir, "profiles")
|
||||
require.NoError(t, os.MkdirAll(profilesDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "alpha.mobileconfig"), []byte("<plist></plist>"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "beta.json"), []byte("{}"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "gamma.mobileconfig"), []byte("<plist></plist>"), 0o644))
|
||||
|
||||
config := getGlobalConfig([]string{"controls"})
|
||||
config += `controls:
|
||||
apple_settings:
|
||||
configuration_profiles:
|
||||
- paths: profiles/*.mobileconfig
|
||||
- path: profiles/beta.json
|
||||
`
|
||||
yamlPath := filepath.Join(dir, "gitops.yml")
|
||||
require.NoError(t, os.WriteFile(yamlPath, []byte(config), 0o644))
|
||||
|
||||
result, err := GitOpsFromFile(yamlPath, dir, nil, nopLogf)
|
||||
require.NoError(t, err)
|
||||
macSettings, ok := result.Controls.MacOSSettings.(fleet.MacOSSettings)
|
||||
require.True(t, ok)
|
||||
require.Len(t, macSettings.CustomSettings, 3)
|
||||
|
||||
// Glob results come first (sorted), then the explicit path
|
||||
assert.Contains(t, macSettings.CustomSettings[0].Path, "alpha.mobileconfig")
|
||||
assert.Contains(t, macSettings.CustomSettings[1].Path, "gamma.mobileconfig")
|
||||
assert.Contains(t, macSettings.CustomSettings[2].Path, "beta.json")
|
||||
})
|
||||
|
||||
t.Run("windows_profiles", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
profilesDir := filepath.Join(dir, "profiles")
|
||||
require.NoError(t, os.MkdirAll(profilesDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "alpha.xml"), []byte("<xml/>"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "beta.xml"), []byte("<xml/>"), 0o644))
|
||||
|
||||
config := getGlobalConfig([]string{"controls"})
|
||||
config += `controls:
|
||||
windows_settings:
|
||||
configuration_profiles:
|
||||
- paths: profiles/*.xml
|
||||
`
|
||||
yamlPath := filepath.Join(dir, "gitops.yml")
|
||||
require.NoError(t, os.WriteFile(yamlPath, []byte(config), 0o644))
|
||||
|
||||
result, err := GitOpsFromFile(yamlPath, dir, nil, nopLogf)
|
||||
require.NoError(t, err)
|
||||
winSettings, ok := result.Controls.WindowsSettings.(fleet.WindowsSettings)
|
||||
require.True(t, ok)
|
||||
require.True(t, winSettings.CustomSettings.Valid)
|
||||
require.Len(t, winSettings.CustomSettings.Value, 2)
|
||||
|
||||
assert.Contains(t, winSettings.CustomSettings.Value[0].Path, "alpha.xml")
|
||||
assert.Contains(t, winSettings.CustomSettings.Value[1].Path, "beta.xml")
|
||||
})
|
||||
|
||||
t.Run("android_profiles", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
profilesDir := filepath.Join(dir, "profiles")
|
||||
require.NoError(t, os.MkdirAll(profilesDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "skip.xml"), []byte("<xml/>"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "beta.json"), []byte("{}"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "skip.txt"), []byte("nope"), 0o644))
|
||||
|
||||
config := getGlobalConfig([]string{"controls"})
|
||||
config += `controls:
|
||||
android_settings:
|
||||
configuration_profiles:
|
||||
- paths: profiles/*
|
||||
`
|
||||
yamlPath := filepath.Join(dir, "gitops.yml")
|
||||
require.NoError(t, os.WriteFile(yamlPath, []byte(config), 0o644))
|
||||
|
||||
result, err := GitOpsFromFile(yamlPath, dir, nil, nopLogf)
|
||||
require.NoError(t, err)
|
||||
androidSettings, ok := result.Controls.AndroidSettings.(fleet.AndroidSettings)
|
||||
require.True(t, ok)
|
||||
require.True(t, androidSettings.CustomSettings.Valid)
|
||||
require.Len(t, androidSettings.CustomSettings.Value, 1)
|
||||
|
||||
// Sorted alphabetically by path
|
||||
assert.Contains(t, androidSettings.CustomSettings.Value[0].Path, "beta.json")
|
||||
})
|
||||
|
||||
t.Run("macos_profiles_with_labels", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
profilesDir := filepath.Join(dir, "profiles")
|
||||
require.NoError(t, os.MkdirAll(profilesDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "a.mobileconfig"), []byte("<plist></plist>"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "b.mobileconfig"), []byte("<plist></plist>"), 0o644))
|
||||
|
||||
config := getGlobalConfig([]string{"controls"})
|
||||
config += `controls:
|
||||
apple_settings:
|
||||
configuration_profiles:
|
||||
- paths: profiles/*.mobileconfig
|
||||
labels_include_all:
|
||||
- MyLabel
|
||||
`
|
||||
yamlPath := filepath.Join(dir, "gitops.yml")
|
||||
require.NoError(t, os.WriteFile(yamlPath, []byte(config), 0o644))
|
||||
|
||||
result, err := GitOpsFromFile(yamlPath, dir, nil, nopLogf)
|
||||
require.NoError(t, err)
|
||||
macSettings, ok := result.Controls.MacOSSettings.(fleet.MacOSSettings)
|
||||
require.True(t, ok)
|
||||
require.Len(t, macSettings.CustomSettings, 2)
|
||||
for _, p := range macSettings.CustomSettings {
|
||||
assert.Equal(t, []string{"MyLabel"}, p.LabelsIncludeAll)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnknownKeyDetection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
|||
|
|
@ -679,7 +679,8 @@ func NewMDMConfigProfilePayloadFromAndroid(cp *MDMAndroidConfigProfile) *MDMConf
|
|||
// MDMProfileSpec represents the spec used to define configuration
|
||||
// profiles via yaml files.
|
||||
type MDMProfileSpec struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Paths string `json:"paths,omitempty"`
|
||||
|
||||
// Deprecated: the Labels field is now deprecated, it is superseded by
|
||||
// LabelsIncludeAll, so any value set via this field will be transferred to
|
||||
|
|
@ -700,6 +701,39 @@ type MDMProfileSpec struct {
|
|||
LabelsExcludeAny []string `json:"labels_exclude_any,omitempty"`
|
||||
}
|
||||
|
||||
// Implement the SupportsFileInclude interface so that MDMProfileSpec
|
||||
// can support globs in GitOps.
|
||||
|
||||
// GetBaseItem converts MDMProfileSpec's string Path/Paths to a BaseItem.
|
||||
// Nil pointers are returned for empty strings.
|
||||
func (p *MDMProfileSpec) GetBaseItem() BaseItem {
|
||||
var b BaseItem
|
||||
if p.Path != "" {
|
||||
path := p.Path
|
||||
b.Path = &path
|
||||
}
|
||||
if p.Paths != "" {
|
||||
paths := p.Paths
|
||||
b.Paths = &paths
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// SetBaseItem updates MDMProfileSpec's Path/Paths from a BaseItem.
|
||||
// Nil pointers become empty strings.
|
||||
func (p *MDMProfileSpec) SetBaseItem(v BaseItem) {
|
||||
if v.Path != nil {
|
||||
p.Path = *v.Path
|
||||
} else {
|
||||
p.Path = ""
|
||||
}
|
||||
if v.Paths != nil {
|
||||
p.Paths = *v.Paths
|
||||
} else {
|
||||
p.Paths = ""
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface to add backwards
|
||||
// compatibility to previous ways to define profile specs.
|
||||
func (p *MDMProfileSpec) UnmarshalJSON(data []byte) error {
|
||||
|
|
|
|||
27
server/fleet/spec.go
Normal file
27
server/fleet/spec.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package fleet
|
||||
|
||||
// BaseItem provides path/paths fields for types that can reference external
|
||||
// files in GitOps YAML configurations.
|
||||
type BaseItem struct {
|
||||
Path *string `json:"path"`
|
||||
Paths *string `json:"paths"`
|
||||
}
|
||||
|
||||
// SupportsFileInclude is implemented by types that can reference external
|
||||
// files via path/paths fields in GitOps YAML.
|
||||
type SupportsFileInclude interface {
|
||||
GetBaseItem() BaseItem
|
||||
SetBaseItem(v BaseItem)
|
||||
}
|
||||
|
||||
// GetBaseItem returns the current BaseItem value.
|
||||
// Types that embed BaseItem inherit this method via promotion.
|
||||
func (b *BaseItem) GetBaseItem() BaseItem {
|
||||
return *b
|
||||
}
|
||||
|
||||
// SetBaseItem sets the BaseItem value.
|
||||
// Types that embed BaseItem inherit this method via promotion.
|
||||
func (b *BaseItem) SetBaseItem(v BaseItem) {
|
||||
*b = v
|
||||
}
|
||||
|
|
@ -135,6 +135,7 @@ github.com/fleetdm/fleet/v4/server/fleet/WindowsUpdates GracePeriodDays optjson.
|
|||
github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSSettings fleet.MacOSSettings
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []fleet.MDMProfileSpec
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Paths string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAll []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAny []string
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Paths string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAll []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAny []string
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ github.com/fleetdm/fleet/v4/server/fleet/WindowsUpdates GracePeriodDays optjson.
|
|||
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM MacOSSettings fleet.MacOSSettings
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []fleet.MDMProfileSpec
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Paths string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAll []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAny []string
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ github.com/fleetdm/fleet/v4/server/fleet/WindowsUpdates GracePeriodDays optjson.
|
|||
github.com/fleetdm/fleet/v4/server/fleet/TeamMDM MacOSSettings fleet.MacOSSettings
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MacOSSettings CustomSettings []fleet.MDMProfileSpec
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Path string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Paths string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec Labels []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAll []string
|
||||
github.com/fleetdm/fleet/v4/server/fleet/MDMProfileSpec LabelsIncludeAny []string
|
||||
|
|
|
|||
3
website/.sailsrc
vendored
3
website/.sailsrc
vendored
|
|
@ -1,8 +1,7 @@
|
|||
{
|
||||
"generators": {
|
||||
"modules": {
|
||||
"landing-page": "./generators/landing-page",
|
||||
"gitops": "./generators/gitops"
|
||||
"landing-page": "./generators/landing-page"
|
||||
}
|
||||
},
|
||||
"_generatedWith": {
|
||||
|
|
|
|||
17
website/generators/gitops/README.md
vendored
17
website/generators/gitops/README.md
vendored
|
|
@ -1,17 +0,0 @@
|
|||
To test:
|
||||
|
||||
`cd website/`
|
||||
|
||||
Next:
|
||||
|
||||
`rm -rf /tmp/it-and-security/ && sails generate gitops && mv it-and-security /tmp/it-and-security`
|
||||
|
||||
Last:
|
||||
|
||||
If you use VS Code or Cursor:
|
||||
|
||||
`code /tmp/it-and-security/`
|
||||
|
||||
If you use Sublime:
|
||||
|
||||
`subl /tmp/it-and-security/`
|
||||
93
website/generators/gitops/index.js
vendored
93
website/generators/gitops/index.js
vendored
|
|
@ -1,93 +0,0 @@
|
|||
module.exports = {
|
||||
|
||||
targets: {
|
||||
|
||||
'./it-and-security/README.md': { copy: './README.md.template' },
|
||||
'./it-and-security/.github': { folder: {} },
|
||||
'./it-and-security/.github/fleet-gitops': { folder: {} },
|
||||
'./it-and-security/.github/fleet-gitops/action.yml': { copy: './github/fleet-gitops/action.yml.template' },
|
||||
'./it-and-security/.github/fleet-gitops/gitops.sh': { copy: './github/fleet-gitops/gitops.sh.template' },
|
||||
'./it-and-security/.github/workflows': { folder: {} },
|
||||
'./it-and-security/.github/workflows/workflow.yml': { copy: './github/workflows/workflow.yml.template' },
|
||||
'./it-and-security/.gitlab-ci.yml': { copy: './gitlab-ci.yml.template' },
|
||||
'./it-and-security/.gitignore': { copy: './gitignore.template' },
|
||||
'./it-and-security/default.yml': { copy: './default.yml.template' },
|
||||
'./it-and-security/fleets/': { folder: {} },
|
||||
'./it-and-security/fleets/workstations.yml': { copy: './fleets/workstations.yml.template' },
|
||||
'./it-and-security/fleets/personal-mobile-devices.yml': { copy: './fleets/personal-mobile-devices.yml.template' },
|
||||
'./it-and-security/labels/': { folder: {} },
|
||||
'./it-and-security/labels/apple-silicon-macos-hosts.yml': { copy: './labels/apple-silicon-macos-hosts.yml.template' },
|
||||
'./it-and-security/labels/x86-based-windows-hosts.yml': { copy: './labels/x86-based-windows-hosts.yml.template' },
|
||||
'./it-and-security/labels/arm-based-windows-hosts.yml': { copy: './labels/arm-based-windows-hosts.yml.template' },
|
||||
'./it-and-security/labels/debian-based-linux-hosts.yml': { copy: './labels/debian-based-linux-hosts.yml.template' },
|
||||
'./it-and-security/platforms/': { folder: {} },
|
||||
'./it-and-security/platforms/linux': { folder: {} },
|
||||
'./it-and-security/platforms/linux/policies/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/linux/reports/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/linux/scripts/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/linux/software/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/windows': { folder: {} },
|
||||
'./it-and-security/platforms/windows/configuration-profiles/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/windows/policies/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/windows/reports/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/windows/scripts/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/windows/software/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/macos': { folder: {} },
|
||||
'./it-and-security/platforms/macos/configuration-profiles/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/macos/declaration-profiles/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/macos/enrollment-profiles/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/macos/enrollment-profiles/automatic-enrollment.dep.json': { copy: './platforms/macos/enrollment-profiles/automatic-enrollment.dep.json.template' },
|
||||
'./it-and-security/platforms/macos/commands/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/macos/policies/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/macos/policies/all-software-updates-installed.yml': { copy: './platforms/macos/policies/all-software-updates-installed.yml' },
|
||||
'./it-and-security/platforms/macos/reports/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/macos/scripts/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/macos/software/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/ios': { folder: {} },
|
||||
'./it-and-security/platforms/ios/configuration-profiles/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/ios/declaration-profiles/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/ipados': { folder: {} },
|
||||
'./it-and-security/platforms/ipados/configuration-profiles/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/ipados/declaration-profiles/.gitkeep': { copy: './gitkeep.template' },
|
||||
// './it-and-security/platforms/tvos': { folder: {} },
|
||||
// './it-and-security/platforms/tvos/configuration-profiles/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/android': { folder: {} },
|
||||
'./it-and-security/platforms/android/configuration-profiles/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/android/managed-app-configurations/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/all/': { folder: {} },
|
||||
'./it-and-security/platforms/all/icons/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/all/reports/.gitkeep': { copy: './gitkeep.template' },
|
||||
'./it-and-security/platforms/all/policies/.gitkeep': { copy: './gitkeep.template' },
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
// • e.g. create a folder:
|
||||
// ```
|
||||
// './hey_look_a_folder': { folder: {} }
|
||||
// ```
|
||||
//
|
||||
// • e.g. create a dynamically-named file relative to `scope.rootPath`
|
||||
// (defined by the `filename` scope variable).
|
||||
//
|
||||
// The `template` helper reads the specified template, making the
|
||||
// entire scope available to it (uses underscore/JST/ejs syntax).
|
||||
// Then the file is copied into the specified destination (on the left).
|
||||
// ```
|
||||
// './:filename': { template: 'example.template.js' },
|
||||
// ```
|
||||
//
|
||||
// • See https://sailsjs.com/docs/concepts/extending-sails/generators for more documentation.
|
||||
// (Or visit https://sailsjs.com/support and talk to a maintainer of a core or community generator.)
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The absolute path to the `templates` for this generator
|
||||
* (for use with the `template` and `copy` builtins)
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
templatesDirectory: require('path').resolve(__dirname, './templates')
|
||||
|
||||
};
|
||||
Loading…
Reference in a new issue