From 91362ba2cacf4fe418d8873e8787ed02770e336c Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Fri, 20 Mar 2026 17:27:27 -0500 Subject: [PATCH] Add fleetctl new command (#41909) **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 ## 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. --- changes/41345-add-fleetctl-new | 2 + cmd/fleetctl/fleetctl/fleetctl.go | 1 + cmd/fleetctl/fleetctl/new.go | 182 +++++++++++++ cmd/fleetctl/fleetctl/new_test.go | 241 ++++++++++++++++++ .../.github/fleet-gitops/action.template.yml | 4 +- .../new/.github/fleet-gitops/gitops.sh | 4 +- .../.github/workflows/workflow.template.yml | 15 +- .../fleetctl/templates/new/.gitignore | 0 .../templates/new/.gitlab-ci.template.yml | 2 +- .../fleetctl/fleetctl/templates/new/README.md | 2 +- .../templates/new/default.template.yml | 22 +- .../personal-mobile-devices.template.yml | 8 +- .../new/fleets/workstations.template.yml | 35 ++- .../apple-silicon-macos-hosts.template.yml | 0 .../arm-based-windows-hosts.template.yml | 0 .../debian-based-linux-hosts.template.yml | 0 .../x86-based-windows-hosts.template.yml | 0 .../new/platforms/all/icons/.gitkeep | 0 .../new/platforms/all/policies/.gitkeep | 0 .../new/platforms/all/reports/.gitkeep | 0 .../android/configuration-profiles/.gitkeep | 0 .../managed-app-configurations/.gitkeep | 0 .../ios/configuration-profiles/.gitkeep | 0 .../ios/declaration-profiles/.gitkeep | 0 .../ipados/configuration-profiles/.gitkeep | 0 .../ipados/declaration-profiles/.gitkeep | 0 .../new/platforms/linux/policies/.gitkeep | 0 .../new/platforms/linux/reports/.gitkeep | 0 .../new/platforms/linux/scripts/.gitkeep | 0 .../new/platforms/linux/software/.gitkeep | 0 .../new/platforms/macos/commands/.gitkeep | 0 .../macos/configuration-profiles/.gitkeep | 0 .../macos/declaration-profiles/.gitkeep | 0 .../automatic-enrollment.dep.json | 0 ...ll-software-updates-installed.template.yml | 6 +- .../new/platforms/macos/reports/.gitkeep | 0 .../new/platforms/macos/scripts/.gitkeep | 0 .../new/platforms/macos/software/.gitkeep | 0 .../windows/configuration-profiles/.gitkeep | 0 .../new/platforms/windows/policies/.gitkeep | 0 .../new/platforms/windows/reports/.gitkeep | 0 .../new/platforms/windows/scripts/.gitkeep | 0 .../new/platforms/windows/software/.gitkeep | 0 go.mod | 2 + go.sum | 9 + pkg/spec/gitops.go | 109 ++++---- pkg/spec/gitops_test.go | 169 ++++++++++-- server/fleet/mdm.go | 36 ++- server/fleet/spec.go | 27 ++ .../generated_files/appconfig.txt | 1 + .../generated_files/mdmprofilespec.txt | 1 + .../generated_files/teamconfig.txt | 1 + .../cloner-check/generated_files/teammdm.txt | 1 + website/.sailsrc | 3 +- website/generators/gitops/README.md | 17 -- website/generators/gitops/index.js | 93 ------- 56 files changed, 752 insertions(+), 241 deletions(-) create mode 100644 changes/41345-add-fleetctl-new create mode 100644 cmd/fleetctl/fleetctl/new.go create mode 100644 cmd/fleetctl/fleetctl/new_test.go rename website/generators/gitops/templates/github/fleet-gitops/action.yml.template => cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/action.template.yml (96%) rename website/generators/gitops/templates/github/fleet-gitops/gitops.sh.template => cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/gitops.sh (97%) mode change 100644 => 100755 rename website/generators/gitops/templates/github/workflows/workflow.yml.template => cmd/fleetctl/fleetctl/templates/new/.github/workflows/workflow.template.yml (93%) rename website/generators/gitops/templates/gitignore.template => cmd/fleetctl/fleetctl/templates/new/.gitignore (100%) rename website/generators/gitops/templates/gitlab-ci.yml.template => cmd/fleetctl/fleetctl/templates/new/.gitlab-ci.template.yml (96%) rename website/generators/gitops/templates/README.md.template => cmd/fleetctl/fleetctl/templates/new/README.md (99%) rename website/generators/gitops/templates/default.yml.template => cmd/fleetctl/fleetctl/templates/new/default.template.yml (95%) rename website/generators/gitops/templates/fleets/personal-mobile-devices.yml.template => cmd/fleetctl/fleetctl/templates/new/fleets/personal-mobile-devices.template.yml (97%) rename website/generators/gitops/templates/fleets/workstations.yml.template => cmd/fleetctl/fleetctl/templates/new/fleets/workstations.template.yml (90%) rename website/generators/gitops/templates/labels/apple-silicon-macos-hosts.yml.template => cmd/fleetctl/fleetctl/templates/new/labels/apple-silicon-macos-hosts.template.yml (100%) rename website/generators/gitops/templates/labels/arm-based-windows-hosts.yml.template => cmd/fleetctl/fleetctl/templates/new/labels/arm-based-windows-hosts.template.yml (100%) rename website/generators/gitops/templates/labels/debian-based-linux-hosts.yml.template => cmd/fleetctl/fleetctl/templates/new/labels/debian-based-linux-hosts.template.yml (100%) rename website/generators/gitops/templates/labels/x86-based-windows-hosts.yml.template => cmd/fleetctl/fleetctl/templates/new/labels/x86-based-windows-hosts.template.yml (100%) rename website/generators/gitops/templates/gitkeep.template => cmd/fleetctl/fleetctl/templates/new/platforms/all/icons/.gitkeep (100%) create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/all/policies/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/all/reports/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/android/configuration-profiles/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/android/managed-app-configurations/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/ios/configuration-profiles/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/ios/declaration-profiles/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/ipados/configuration-profiles/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/ipados/declaration-profiles/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/linux/policies/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/linux/reports/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/linux/scripts/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/linux/software/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/macos/commands/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/macos/configuration-profiles/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/macos/declaration-profiles/.gitkeep rename website/generators/gitops/templates/platforms/macos/enrollment-profiles/automatic-enrollment.dep.json.template => cmd/fleetctl/fleetctl/templates/new/platforms/macos/enrollment-profiles/automatic-enrollment.dep.json (100%) rename website/generators/gitops/templates/platforms/macos/policies/all-software-updates-installed.yml => cmd/fleetctl/fleetctl/templates/new/platforms/macos/policies/all-software-updates-installed.template.yml (88%) create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/macos/reports/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/macos/scripts/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/macos/software/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/windows/configuration-profiles/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/windows/policies/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/windows/reports/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/windows/scripts/.gitkeep create mode 100644 cmd/fleetctl/fleetctl/templates/new/platforms/windows/software/.gitkeep create mode 100644 server/fleet/spec.go delete mode 100644 website/generators/gitops/README.md delete mode 100644 website/generators/gitops/index.js diff --git a/changes/41345-add-fleetctl-new b/changes/41345-add-fleetctl-new new file mode 100644 index 0000000000..aa62e3b3d1 --- /dev/null +++ b/changes/41345-add-fleetctl-new @@ -0,0 +1,2 @@ +- Added `fleetctl new` command to initialize a GitOps folder +- Added glob support for `configuration_profiles` diff --git a/cmd/fleetctl/fleetctl/fleetctl.go b/cmd/fleetctl/fleetctl/fleetctl.go index c1f0c5c31d..4a97a750db 100644 --- a/cmd/fleetctl/fleetctl/fleetctl.go +++ b/cmd/fleetctl/fleetctl/fleetctl.go @@ -74,6 +74,7 @@ func CreateApp( runScriptCommand(), gitopsCommand(), generateGitopsCommand(), + newCommand(), } return app } diff --git a/cmd/fleetctl/fleetctl/new.go b/cmd/fleetctl/fleetctl/new.go new file mode 100644 index 0000000000..672bae27fc --- /dev/null +++ b/cmd/fleetctl/fleetctl/new.go @@ -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 --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 + }, + } +} diff --git a/cmd/fleetctl/fleetctl/new_test.go b/cmd/fleetctl/fleetctl/new_test.go new file mode 100644 index 0000000000..2f225a6af9 --- /dev/null +++ b/cmd/fleetctl/fleetctl/new_test.go @@ -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, "", 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)) + }) +} diff --git a/website/generators/gitops/templates/github/fleet-gitops/action.yml.template b/cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/action.template.yml similarity index 96% rename from website/generators/gitops/templates/github/fleet-gitops/action.yml.template rename to cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/action.template.yml index af453deca5..b483b24d00 100644 --- a/website/generators/gitops/templates/github/fleet-gitops/action.yml.template +++ b/cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/action.template.yml @@ -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 \ No newline at end of file + run: ./.github/fleet-gitops/gitops.sh diff --git a/website/generators/gitops/templates/github/fleet-gitops/gitops.sh.template b/cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/gitops.sh old mode 100644 new mode 100755 similarity index 97% rename from website/generators/gitops/templates/github/fleet-gitops/gitops.sh.template rename to cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/gitops.sh index 408d1677f9..45caa6c81c --- a/website/generators/gitops/templates/github/fleet-gitops/gitops.sh.template +++ b/cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/gitops.sh @@ -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[@]}" \ No newline at end of file +$FLEETCTL gitops "${args[@]}" diff --git a/website/generators/gitops/templates/github/workflows/workflow.yml.template b/cmd/fleetctl/fleetctl/templates/new/.github/workflows/workflow.template.yml similarity index 93% rename from website/generators/gitops/templates/github/workflows/workflow.yml.template rename to cmd/fleetctl/fleetctl/templates/new/.github/workflows/workflow.template.yml index f19b76daf5..334d49e6ff 100644 --- a/website/generators/gitops/templates/github/workflows/workflow.yml.template +++ b/cmd/fleetctl/fleetctl/templates/new/.github/workflows/workflow.template.yml @@ -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 }} - - \ No newline at end of file diff --git a/website/generators/gitops/templates/gitignore.template b/cmd/fleetctl/fleetctl/templates/new/.gitignore similarity index 100% rename from website/generators/gitops/templates/gitignore.template rename to cmd/fleetctl/fleetctl/templates/new/.gitignore diff --git a/website/generators/gitops/templates/gitlab-ci.yml.template b/cmd/fleetctl/fleetctl/templates/new/.gitlab-ci.template.yml similarity index 96% rename from website/generators/gitops/templates/gitlab-ci.yml.template rename to cmd/fleetctl/fleetctl/templates/new/.gitlab-ci.template.yml index 1120f12985..f141f69600 100644 --- a/website/generators/gitops/templates/gitlab-ci.yml.template +++ b/cmd/fleetctl/fleetctl/templates/new/.gitlab-ci.template.yml @@ -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 \ No newline at end of file + - ./.github/fleet-gitops/gitops.sh diff --git a/website/generators/gitops/templates/README.md.template b/cmd/fleetctl/fleetctl/templates/new/README.md similarity index 99% rename from website/generators/gitops/templates/README.md.template rename to cmd/fleetctl/fleetctl/templates/new/README.md index eda8bbdcd7..1db7414d2d 100644 --- a/website/generators/gitops/templates/README.md.template +++ b/cmd/fleetctl/fleetctl/templates/new/README.md @@ -1,4 +1,4 @@ -# Fleet +# Fleet These files allow you to configure, patch, and secure computing devices for your organization. diff --git a/website/generators/gitops/templates/default.yml.template b/cmd/fleetctl/fleetctl/templates/new/default.template.yml similarity index 95% rename from website/generators/gitops/templates/default.yml.template rename to cmd/fleetctl/fleetctl/templates/new/default.template.yml index 278db35cbf..e1038a90f8 100644 --- a/website/generators/gitops/templates/default.yml.template +++ b/cmd/fleetctl/fleetctl/templates/new/default.template.yml @@ -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 - diff --git a/website/generators/gitops/templates/fleets/personal-mobile-devices.yml.template b/cmd/fleetctl/fleetctl/templates/new/fleets/personal-mobile-devices.template.yml similarity index 97% rename from website/generators/gitops/templates/fleets/personal-mobile-devices.yml.template rename to cmd/fleetctl/fleetctl/templates/new/fleets/personal-mobile-devices.template.yml index 304c8f762a..c6c3c887cc 100644 --- a/website/generators/gitops/templates/fleets/personal-mobile-devices.yml.template +++ b/cmd/fleetctl/fleetctl/templates/new/fleets/personal-mobile-devices.template.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 ########################################################### diff --git a/website/generators/gitops/templates/fleets/workstations.yml.template b/cmd/fleetctl/fleetctl/templates/new/fleets/workstations.template.yml similarity index 90% rename from website/generators/gitops/templates/fleets/workstations.yml.template rename to cmd/fleetctl/fleetctl/templates/new/fleets/workstations.template.yml index fda996624a..7bffa076a2 100644 --- a/website/generators/gitops/templates/fleets/workstations.yml.template +++ b/cmd/fleetctl/fleetctl/templates/new/fleets/workstations.template.yml @@ -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 diff --git a/website/generators/gitops/templates/labels/apple-silicon-macos-hosts.yml.template b/cmd/fleetctl/fleetctl/templates/new/labels/apple-silicon-macos-hosts.template.yml similarity index 100% rename from website/generators/gitops/templates/labels/apple-silicon-macos-hosts.yml.template rename to cmd/fleetctl/fleetctl/templates/new/labels/apple-silicon-macos-hosts.template.yml diff --git a/website/generators/gitops/templates/labels/arm-based-windows-hosts.yml.template b/cmd/fleetctl/fleetctl/templates/new/labels/arm-based-windows-hosts.template.yml similarity index 100% rename from website/generators/gitops/templates/labels/arm-based-windows-hosts.yml.template rename to cmd/fleetctl/fleetctl/templates/new/labels/arm-based-windows-hosts.template.yml diff --git a/website/generators/gitops/templates/labels/debian-based-linux-hosts.yml.template b/cmd/fleetctl/fleetctl/templates/new/labels/debian-based-linux-hosts.template.yml similarity index 100% rename from website/generators/gitops/templates/labels/debian-based-linux-hosts.yml.template rename to cmd/fleetctl/fleetctl/templates/new/labels/debian-based-linux-hosts.template.yml diff --git a/website/generators/gitops/templates/labels/x86-based-windows-hosts.yml.template b/cmd/fleetctl/fleetctl/templates/new/labels/x86-based-windows-hosts.template.yml similarity index 100% rename from website/generators/gitops/templates/labels/x86-based-windows-hosts.yml.template rename to cmd/fleetctl/fleetctl/templates/new/labels/x86-based-windows-hosts.template.yml diff --git a/website/generators/gitops/templates/gitkeep.template b/cmd/fleetctl/fleetctl/templates/new/platforms/all/icons/.gitkeep similarity index 100% rename from website/generators/gitops/templates/gitkeep.template rename to cmd/fleetctl/fleetctl/templates/new/platforms/all/icons/.gitkeep diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/all/policies/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/all/policies/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/all/reports/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/all/reports/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/android/configuration-profiles/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/android/configuration-profiles/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/android/managed-app-configurations/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/android/managed-app-configurations/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/ios/configuration-profiles/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/ios/configuration-profiles/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/ios/declaration-profiles/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/ios/declaration-profiles/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/ipados/configuration-profiles/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/ipados/configuration-profiles/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/ipados/declaration-profiles/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/ipados/declaration-profiles/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/linux/policies/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/linux/policies/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/linux/reports/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/linux/reports/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/linux/scripts/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/linux/scripts/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/linux/software/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/linux/software/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/macos/commands/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/macos/commands/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/macos/configuration-profiles/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/macos/configuration-profiles/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/macos/declaration-profiles/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/macos/declaration-profiles/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/website/generators/gitops/templates/platforms/macos/enrollment-profiles/automatic-enrollment.dep.json.template b/cmd/fleetctl/fleetctl/templates/new/platforms/macos/enrollment-profiles/automatic-enrollment.dep.json similarity index 100% rename from website/generators/gitops/templates/platforms/macos/enrollment-profiles/automatic-enrollment.dep.json.template rename to cmd/fleetctl/fleetctl/templates/new/platforms/macos/enrollment-profiles/automatic-enrollment.dep.json diff --git a/website/generators/gitops/templates/platforms/macos/policies/all-software-updates-installed.yml b/cmd/fleetctl/fleetctl/templates/new/platforms/macos/policies/all-software-updates-installed.template.yml similarity index 88% rename from website/generators/gitops/templates/platforms/macos/policies/all-software-updates-installed.yml rename to cmd/fleetctl/fleetctl/templates/new/platforms/macos/policies/all-software-updates-installed.template.yml index 85053221d4..2eb2a76457 100644 --- a/website/generators/gitops/templates/platforms/macos/policies/all-software-updates-installed.yml +++ b/cmd/fleetctl/fleetctl/templates/new/platforms/macos/policies/all-software-updates-installed.template.yml @@ -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 \ No newline at end of file + platform: darwin diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/macos/reports/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/macos/reports/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/macos/scripts/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/macos/scripts/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/macos/software/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/macos/software/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/windows/configuration-profiles/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/windows/configuration-profiles/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/windows/policies/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/windows/policies/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/windows/reports/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/windows/reports/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/windows/scripts/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/windows/scripts/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/fleetctl/fleetctl/templates/new/platforms/windows/software/.gitkeep b/cmd/fleetctl/fleetctl/templates/new/platforms/windows/software/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/go.mod b/go.mod index 76b84e8d94..3d56f5035d 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 862e0eaae6..6fbad5ff53 100644 --- a/go.sum +++ b/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= diff --git a/pkg/spec/gitops.go b/pkg/spec/gitops.go index 60181e1893..208f5a4fda 100644 --- a/pkg/spec/gitops.go +++ b/pkg/spec/gitops.go @@ -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 diff --git a/pkg/spec/gitops_test.go b/pkg/spec/gitops_test.go index 15441cf621..f485021611 100644 --- a/pkg/spec/gitops_test.go +++ b/pkg/spec/gitops_test.go @@ -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(""), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "beta.json"), []byte("{}"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "gamma.mobileconfig"), []byte(""), 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(""), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "beta.xml"), []byte(""), 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(""), 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(""), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(profilesDir, "b.mobileconfig"), []byte(""), 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() diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 139995c46f..9304c86e63 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -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 { diff --git a/server/fleet/spec.go b/server/fleet/spec.go new file mode 100644 index 0000000000..4180a3c3c9 --- /dev/null +++ b/server/fleet/spec.go @@ -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 +} diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt index 93b681634c..6e3ea4e408 100644 --- a/tools/cloner-check/generated_files/appconfig.txt +++ b/tools/cloner-check/generated_files/appconfig.txt @@ -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 diff --git a/tools/cloner-check/generated_files/mdmprofilespec.txt b/tools/cloner-check/generated_files/mdmprofilespec.txt index 3c190ca329..2dea58833e 100644 --- a/tools/cloner-check/generated_files/mdmprofilespec.txt +++ b/tools/cloner-check/generated_files/mdmprofilespec.txt @@ -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 diff --git a/tools/cloner-check/generated_files/teamconfig.txt b/tools/cloner-check/generated_files/teamconfig.txt index dddee046ca..99cf158509 100644 --- a/tools/cloner-check/generated_files/teamconfig.txt +++ b/tools/cloner-check/generated_files/teamconfig.txt @@ -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 diff --git a/tools/cloner-check/generated_files/teammdm.txt b/tools/cloner-check/generated_files/teammdm.txt index bbe7e26c4d..3340ed963e 100644 --- a/tools/cloner-check/generated_files/teammdm.txt +++ b/tools/cloner-check/generated_files/teammdm.txt @@ -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 diff --git a/website/.sailsrc b/website/.sailsrc index f6f62e1f79..469767ed23 100644 --- a/website/.sailsrc +++ b/website/.sailsrc @@ -1,8 +1,7 @@ { "generators": { "modules": { - "landing-page": "./generators/landing-page", - "gitops": "./generators/gitops" + "landing-page": "./generators/landing-page" } }, "_generatedWith": { diff --git a/website/generators/gitops/README.md b/website/generators/gitops/README.md deleted file mode 100644 index a8889ae0b9..0000000000 --- a/website/generators/gitops/README.md +++ /dev/null @@ -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/` diff --git a/website/generators/gitops/index.js b/website/generators/gitops/index.js deleted file mode 100644 index d06b66af5a..0000000000 --- a/website/generators/gitops/index.js +++ /dev/null @@ -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') - -};