Skip to content

Add fleetctl new command#41909

Merged
sgress454 merged 35 commits intomainfrom
worktree-fleetctl-new
Mar 20, 2026
Merged

Add fleetctl new command#41909
sgress454 merged 35 commits intomainfrom
worktree-fleetctl-new

Conversation

@sgress454
Copy link
Contributor

@sgress454 sgress454 commented Mar 17, 2026

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.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

Testing

  • Added/updated automated tests
    • added unit and intg tests for globbing profiles
    • added tests for fleetctl new
  • QA'd all new/changed functionality manually
    • fleetctl new with no args prompted for org name and created a new it-and-security folder under current folder w/ correct files
    • fleetctl new --dir /tmp/testnew created correct files under /tmp/testnew
    • fleetctl new --dir /tmp/testexisting --force with an existing /tmp/testexisting folder created correct files under /tmp/testexisting
    • fleetctl new --org-name=foo created correct files under it-and-security without prompting for org name
    • paths: in configuration_profiles picks up multiple matching profiles
    • 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.

Copilot AI review requested due to automatic review settings March 17, 2026 21:57
@sgress454 sgress454 requested a review from a team as a code owner March 17, 2026 21:57
@fleet-release fleet-release requested a review from eashaw March 17, 2026 21:57
@lukeheath
Copy link
Member

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 18, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 18, 2026

Walkthrough

The PR introduces the fleetctl new CLI command to initialize a GitOps repository structure from embedded templates. It adds glob pattern support to the configuration_profiles field. The implementation includes template rendering with placeholder substitution, organization name input validation, YAML escaping, and Fleetctl version resolution. The BaseItem and SupportsFileInclude types are migrated from pkg/spec/gitops.go to server/fleet/spec.go as public API types, with MDMProfileSpec updated to support both Path and Paths fields for glob expansion. Various template files are updated to use placeholders and improve documentation.

Possibly related PRs

  • #41141: Modifies the same GitOps path-expansion machinery (BaseItem/SupportsFileInclude and glob expansion in pkg/spec/gitops.go) to add glob pattern support for different entities.
  • #41183: Modifies pkg/spec/gitops.go and alters GitOps policy and struct definitions alongside other fields.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add fleetctl new command' clearly and concisely summarizes the main change in the PR.
Linked Issues check ✅ Passed The PR implements the core requirement from issue #41345: fleetctl new command generates a GitOps folder structure. Glob support for configuration_profiles was added as required for template functionality.
Out of Scope Changes check ✅ Passed Changes are within scope. The PR removes the Sails-based gitops generator (website/generators/gitops) since fleetctl now provides this functionality directly, which is a logical consequence of the PR's main objective.
Description check ✅ Passed The PR description includes a related issue reference, comprehensive details about the changes, and a mostly complete checklist with testing confirmation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fleetctl-new
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/spec/gitops.go (1)

1189-1205: ⚠️ Potential issue | 🟠 Major

path entries currently bypass AllowedExtensions checks.

paths glob expansion filters by extension, but on Line 1189+ direct path items skip that validation. This allows unsupported file types through and makes behavior inconsistent.

💡 Suggested patch
 		case hasPath:
 			if containsGlobMeta(*baseItem.Path) {
 				errs = append(errs, fmt.Errorf(`%s "path" %q contains glob characters; use "paths" for glob patterns`, entityType, *baseItem.Path))
 				continue
 			}
 			resolved := resolveApplyRelativePath(baseDir, *baseItem.Path)
+			ext := strings.ToLower(filepath.Ext(resolved))
+			if !opts.AllowedExtensions[ext] {
+				errs = append(errs, fmt.Errorf(
+					`%s "path" %q has unsupported extension %q`,
+					entityType, *baseItem.Path, ext,
+				))
+				continue
+			}
 			// Check for duplicate filenames if requested.
 			if opts.RequireUniqueBasenames {
 				base := filepath.Base(resolved)
 				if existing, ok := seenBasenames[base]; ok {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/spec/gitops.go` around lines 1189 - 1205, The code path under the hasPath
case bypasses AllowedExtensions validation (while the paths glob branch performs
extension filtering); modify the hasPath handling to validate the resolved
path's file extension against the same AllowedExtensions logic used for glob
expansion: after resolveApplyRelativePath(baseDir, *baseItem.Path) and before
checking RequireUniqueBasenames or appending the entity, check whether the
resolved filename's extension is permitted (reuse the same extension-check
helper or the same list from opts.AllowedExtensions), and if not append an error
to errs and continue; keep the existing duplicate basename logic (seenBasenames)
and then call PT(&entity).SetBaseItem and append to result only when the
extension is allowed.
🧹 Nitpick comments (1)
pkg/spec/gitops_test.go (1)

2155-2274: Add a direct-path extension regression test for profile parsing.

These tests cover extension filtering for paths globs well, but not for single-file path entries. A small test for invalid direct path (for example Android .xml) would lock in expected behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/spec/gitops_test.go` around lines 2155 - 2274, The tests in
TestGitOpsGlobProfiles exercise extension filtering for globbed "paths" but
don't assert behavior for explicit single-file "path" entries; add a new subtest
inside TestGitOpsGlobProfiles that creates a profiles dir with a single-file
(e.g., an Android .xml), writes a gitops YAML using a direct "path:
profiles/foo.xml" under android_settings.configuration_profiles, calls
GitOpsFromFile, and asserts that result.Controls.AndroidSettings
(AndroidSettings.CustomSettings) either excludes the invalid extension or
reports expected invalid/empty Value—use the same patterns and helpers already
in the test (dir := t.TempDir(), require.NoError, GitOpsFromFile, assertions
against androidSettings.CustomSettings.Value and .Valid) to lock in regression
behavior. Ensure the new subtest name indicates it's for direct-path extension
validation and mirrors existing test structure (t.Run, t.Parallel()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/fleetctl/fleetctl/new.go`:
- Around line 56-60: The npm version lookup currently uses exec.Command which
can hang; change it to use exec.CommandContext with a context created by
context.WithTimeout (e.g., 3–5s) so the call is cancelled on slow networks, and
handle the context timeout error the same as the existing fallback to "latest";
update the block that currently calls exec.Command("npm", "view", "fleetctl",
"version") to create ctx, defer cancel, call exec.CommandContext(ctx, ...), then
keep the existing logic that trims output and checks semverPattern before
returning the version.

In `@cmd/fleetctl/fleetctl/templates/new/default.template.yml`:
- Line 50: Replace the typo "single-sign on" in the comment in
default.template.yml with the correct phrasing "single sign-on" so the end-user
guidance reads "Uncomment to use single sign-on (SSO) to authenticate". Locate
the comment containing the string "single-sign on" and update only the wording,
preserving the rest of the comment formatting and context.

In `@server/fleet/mdm.go`:
- Around line 681-682: MDMProfileSpecsMatch currently keys profile specs using
only the Path field causing collisions when Paths is used; update the matching
logic in MDMProfileSpecsMatch to derive a stable key that uses Paths when
non-empty (e.g., sort and join the Paths slice into a single string) else falls
back to Path, and use that key for map lookups and comparisons so entries with
empty Path and non-empty Paths no longer collide.

---

Outside diff comments:
In `@pkg/spec/gitops.go`:
- Around line 1189-1205: The code path under the hasPath case bypasses
AllowedExtensions validation (while the paths glob branch performs extension
filtering); modify the hasPath handling to validate the resolved path's file
extension against the same AllowedExtensions logic used for glob expansion:
after resolveApplyRelativePath(baseDir, *baseItem.Path) and before checking
RequireUniqueBasenames or appending the entity, check whether the resolved
filename's extension is permitted (reuse the same extension-check helper or the
same list from opts.AllowedExtensions), and if not append an error to errs and
continue; keep the existing duplicate basename logic (seenBasenames) and then
call PT(&entity).SetBaseItem and append to result only when the extension is
allowed.

---

Nitpick comments:
In `@pkg/spec/gitops_test.go`:
- Around line 2155-2274: The tests in TestGitOpsGlobProfiles exercise extension
filtering for globbed "paths" but don't assert behavior for explicit single-file
"path" entries; add a new subtest inside TestGitOpsGlobProfiles that creates a
profiles dir with a single-file (e.g., an Android .xml), writes a gitops YAML
using a direct "path: profiles/foo.xml" under
android_settings.configuration_profiles, calls GitOpsFromFile, and asserts that
result.Controls.AndroidSettings (AndroidSettings.CustomSettings) either excludes
the invalid extension or reports expected invalid/empty Value—use the same
patterns and helpers already in the test (dir := t.TempDir(), require.NoError,
GitOpsFromFile, assertions against androidSettings.CustomSettings.Value and
.Valid) to lock in regression behavior. Ensure the new subtest name indicates
it's for direct-path extension validation and mirrors existing test structure
(t.Run, t.Parallel()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 40d739b0-676f-4e00-8d62-fb97b894ee61

📥 Commits

Reviewing files that changed from the base of the PR and between 5444c8b and c0f39f7.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (55)
  • changes/41345-add-fleetctl-new
  • cmd/fleetctl/fleetctl/fleetctl.go
  • cmd/fleetctl/fleetctl/new.go
  • cmd/fleetctl/fleetctl/new_test.go
  • cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/action.template.yml
  • cmd/fleetctl/fleetctl/templates/new/.github/fleet-gitops/gitops.sh
  • cmd/fleetctl/fleetctl/templates/new/.github/workflows/workflow.template.yml
  • cmd/fleetctl/fleetctl/templates/new/.gitignore
  • cmd/fleetctl/fleetctl/templates/new/.gitlab-ci.template.yml
  • cmd/fleetctl/fleetctl/templates/new/README.md
  • cmd/fleetctl/fleetctl/templates/new/default.template.yml
  • cmd/fleetctl/fleetctl/templates/new/fleets/personal-mobile-devices.template.yml
  • cmd/fleetctl/fleetctl/templates/new/fleets/workstations.template.yml
  • cmd/fleetctl/fleetctl/templates/new/labels/apple-silicon-macos-hosts.template.yml
  • cmd/fleetctl/fleetctl/templates/new/labels/arm-based-windows-hosts.template.yml
  • cmd/fleetctl/fleetctl/templates/new/labels/debian-based-linux-hosts.template.yml
  • cmd/fleetctl/fleetctl/templates/new/labels/x86-based-windows-hosts.template.yml
  • cmd/fleetctl/fleetctl/templates/new/platforms/all/icons/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/all/policies/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/all/reports/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/android/configuration-profiles/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/android/managed-app-configurations/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/ios/configuration-profiles/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/ios/declaration-profiles/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/ipados/configuration-profiles/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/ipados/declaration-profiles/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/linux/policies/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/linux/reports/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/linux/scripts/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/linux/software/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/macos/commands/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/macos/configuration-profiles/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/macos/declaration-profiles/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/macos/enrollment-profiles/automatic-enrollment.dep.json
  • cmd/fleetctl/fleetctl/templates/new/platforms/macos/policies/all-software-updates-installed.template.yml
  • cmd/fleetctl/fleetctl/templates/new/platforms/macos/reports/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/macos/scripts/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/macos/software/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/windows/configuration-profiles/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/windows/policies/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/windows/reports/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/windows/scripts/.gitkeep
  • cmd/fleetctl/fleetctl/templates/new/platforms/windows/software/.gitkeep
  • go.mod
  • pkg/spec/gitops.go
  • pkg/spec/gitops_test.go
  • server/fleet/mdm.go
  • server/fleet/spec.go
  • tools/cloner-check/generated_files/appconfig.txt
  • tools/cloner-check/generated_files/mdmprofilespec.txt
  • tools/cloner-check/generated_files/teamconfig.txt
  • tools/cloner-check/generated_files/teammdm.txt
  • website/.sailsrc
  • website/generators/gitops/README.md
  • website/generators/gitops/index.js
💤 Files with no reviewable changes (2)
  • website/generators/gitops/README.md
  • website/generators/gitops/index.js

@lukeheath
Copy link
Member

@getvictor Nice example of different review bots finding different things.

Copy link
Member

@iansltx iansltx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed alphabetically through everything in cmd. Will pick up later.

Comment on lines +154 to +156
// Escape for safe inclusion in double-quoted YAML strings.
yamlOrgName := strings.ReplaceAll(orgName, `\`, `\\`)
yamlOrgName = strings.ReplaceAll(yamlOrgName, `"`, `\"`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like we should be using a templating/escaping library for this. Maybe this isn't possible because Go doesn't have one that operates like test/html template stdlib, and YAML escapers expect you to serialize (which we can't do because comments and field order). But this has my spidey senses tingling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could remove the quotes from the template and just do:
org_name: <%= org_name %>
and then use the YAML serializer on them if that feels better. I don't think any template library is gonna do it for us.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. One nit is it uses single quotes (when it uses quotes at all), where we default to double-quotes in all our examples. I could get around this by using JSON marshaling instead, but then it'd double-quote everything and do unnecessary escaping. So I'm leaving as-is.

outDir := filepath.Join(dir, "ctrl-only")
_, err := runNewCommand(t, "--org-name", "\x01\x02\x03", "--dir", outDir)
require.Error(t, err)
assert.Contains(t, err.Error(), "organization name is required")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like we could do table driven tests, including an assertion that the dir wasn't created etc., or was created with the correct org name.

Copy link
Member

@iansltx iansltx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete.

@@ -0,0 +1,27 @@
package fleet
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Feels moving this into the fleet megapackage is a (smallish) step backwards, but I understand why you did it (mdm.go). Probably more trouble than it's worth to extract it into its own thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't disagree. Maybe pkg/glob?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then again it's 30 lines of code and it'll probably never be more than that 🤔 . I think I'll leave it for now, and if this becomes a reason why we can't drop the fleet import from something we can move it out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and i couldn't think of a good name for it

Copy link
Member

@iansltx iansltx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the cleanup!

@sgress454 sgress454 merged commit 91362ba into main Mar 20, 2026
60 checks passed
@sgress454 sgress454 deleted the worktree-fleetctl-new branch March 20, 2026 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fleetctl new

5 participants