Skip to content

Fix gitops dry-run to catch manual_agent_install + macos_script conflict#44432

Merged
sharon-fdm merged 4 commits into
mainfrom
34464
May 4, 2026
Merged

Fix gitops dry-run to catch manual_agent_install + macos_script conflict#44432
sharon-fdm merged 4 commits into
mainfrom
34464

Conversation

@sharon-fdm
Copy link
Copy Markdown
Collaborator

@sharon-fdm sharon-fdm commented Apr 29, 2026

Related issue: Resolves #34464

Checklist for submitter

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

  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.

  • Timeouts are implemented and retries are limited to avoid infinite loops

Testing

  • Added/updated automated tests
  • QA'd all new/changed functionality manually

What

GitOps --dry-run was succeeding when macos_manual_agent_install was set to true and a macos_script was configured under setup_experience, but the actual GitOps run would fail with:

Couldn't add setup experience script. To add script, first disable macos_manual_agent_install.

Why

The manual_agent_install conflict validation only existed server-side in ee/server/service/setup_experience.go:SetSetupExperienceScript(). The script upload call (uploadMacOSSetupScript()) was gated by !opts.DryRun in server/service/client.go, so during dry-run the upload was skipped entirely and the validation never fired.

Fix

Added client-side validation in server/service/client.go at the point where the YAML-parsed MacOSSetup struct is processed — before the script file is validated and loaded. This check runs for both dry-run and real runs, catching the conflict early. Two code paths were fixed:

  1. Team path (~line 803): Checks setup.ManualAgentInstall.Value when setup.Script.Value is set
  2. No-team path (~line 2603): Checks macOSSetup.ManualAgentInstall.Value when macOSSetup.Script.Value is set

How I reproduced the issue locally

Prerequisites

  • MySQL and Redis running via Docker: docker compose up -d mysql_test redis

Steps

  1. Wrote an integration test (TestDryRunMacOSSetupScriptWithManualAgentInstallConflict) that:

    • Creates a GitOps user and fleetctl config
    • Creates a bootstrap package server serving testdata/signed.pkg
    • Creates a .sh script file with echo "setup script"
    • Creates a global config YAML (minimal server settings)
    • Creates a team config YAML with macos_manual_agent_install: true, macos_script: <path>, and macos_bootstrap_package: <url>
    • Runs fleetctl gitops --dry-run and asserts it fails
    • Runs fleetctl gitops (no dry-run) and asserts it fails
  2. Ran the test before the fix — confirmed the bug:

    Dry-run error: <nil>                          ← BUG: should have failed
    Real run error: ...status 422...first disable macos_manual_agent_install  ← correctly fails
    
  3. Applied the fix and re-ran — both dry-run and real run now fail with the macos_manual_agent_install conflict error.

Test command

MYSQL_TEST=1 REDIS_TEST=1 go test -v \
  -run TestIntegrationsEnterpriseGitops/TestDryRunMacOSSetupScriptWithManualAgentInstallConflict \
  ./cmd/fleetctl/integrationtest/gitops/... -count=1 -timeout 600s

Both sub-tests (team and no-team paths) pass. All related existing tests continue to pass:

  • TestMacOSSetup, TestMacOSSetupScriptWithFleetSecret, TestDeletingNoTeamYAML, TestDisallowSoftwareSetupExperience

Summary by CodeRabbit

  • Bug Fixes

    • GitOps dry-run now correctly fails when a macOS setup configuration combines manual agent installation with a provided setup script, preventing false-positive dry-run success.
  • Tests

    • Added unit and integration regression tests to verify dry-run and real-run rejection of conflicting macOS setup configurations for both team-scoped and unassigned host scopes.

…ict (#34464)

The gitops dry-run was succeeding when `macos_manual_agent_install` was
set to `true` and a `macos_script` was configured, but the actual gitops
run would fail with a 422 error. This happened because the
`manual_agent_install` validation only lived server-side in the script
upload handler, which was skipped entirely during dry-run.

Add client-side validation in both the team and no-team code paths that
checks for this conflict before attempting to validate/upload the script,
ensuring dry-run and real runs behave consistently.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 29, 2026

Codecov Report

❌ Patch coverage is 50.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.79%. Comparing base (b67ab72) to head (a4b8d5d).
⚠️ Report is 112 commits behind head on main.

Files with missing lines Patch % Lines
server/service/client.go 50.00% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #44432      +/-   ##
==========================================
+ Coverage   66.75%   66.79%   +0.03%     
==========================================
  Files        2633     2639       +6     
  Lines      211745   212957    +1212     
  Branches     9424     9424              
==========================================
+ Hits       141355   142236     +881     
- Misses      57547    57755     +208     
- Partials    12843    12966     +123     
Flag Coverage Δ
backend 68.56% <50.00%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Covers both team and no-team code paths in the gitops client-side
validation, ensuring 100% patch coverage of the new validation logic
without requiring MySQL or Redis.
@sharon-fdm
Copy link
Copy Markdown
Collaborator Author

@claude review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — minimal bug fix that mirrors the existing server-side conflict check at the client-side validation step so dry-run surfaces the same error.

Extended reasoning...

Overview

The PR adds two early-exit conflict checks in server/service/client.go (team path ~L803, no-team path ~L2603) that error out when both macos_manual_agent_install is true and macos_script is set. This mirrors the existing server-side validation at ee/server/service/setup_experience.go:SetSetupExperienceScript(), which previously was unreachable during --dry-run because the script upload was gated by !opts.DryRun. The PR also adds a unit test in gitops_test.go and an enterprise integration test, plus a changes-file entry.

Security risks

None apparent. The change is purely defensive — it returns an error earlier rather than allowing a misconfiguration to slip through dry-run. No new code paths, network calls, file reads, or auth logic are introduced; the script file is no longer read when the conflict is present, which is fine since the operation is going to fail anyway.

Level of scrutiny

Low. This is a one-line conflict guard duplicated in two places with matching tests. The error messages reuse the existing wording (with a minor scope hint added for the team/unassigned case). No behavioral change for valid configurations.

Other factors

  • Bug hunters found no issues.
  • Codecov flags 50% patch coverage, but the missed lines are the new error-return branches; both are exercised by the new tests in gitops_test.go and the integration suite, so coverage is effectively present even if the tool didn't pick up the branch counts on this run.
  • The new error string uses an ASCII apostrophe while the server-side message uses a curly apostrophe — purely cosmetic, no test relies on it.

@sharon-fdm sharon-fdm marked this pull request as ready for review April 29, 2026 20:55
@sharon-fdm sharon-fdm requested a review from a team as a code owner April 29, 2026 20:55
Copilot AI review requested due to automatic review settings April 29, 2026 20:55
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@qodo-code-review
Copy link
Copy Markdown

ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes GitOps --dry-run validation so it surfaces the macos_manual_agent_install + macos_script setup experience conflict that previously only failed during a real (non-dry-run) GitOps run.

Changes:

  • Added client-side conflict validation in the GitOps apply path for both team and no-team (unassigned) macOS setup experience script handling.
  • Added/updated regression tests (unit + enterprise integration) to ensure both dry-run and real runs fail on the conflict.
  • Added a changelog entry describing the dry-run behavior fix.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 3 comments.

File Description
server/service/client.go Adds client-side validation to fail early when macos_script is set alongside macos_manual_agent_install.
cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go Adds enterprise integration regression test covering dry-run and real run for team + no-team paths.
cmd/fleetctl/fleetctl/gitops_test.go Adds unit regression test verifying conflict failure in both dry-run and real run.
changes/34464-gitops-dry-run-manual-agent-install-script-conflict Documents the user-visible behavior change for GitOps dry-run conflict detection.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/service/client.go Outdated
Comment thread server/service/client.go Outdated
Comment thread server/service/client.go
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ddebda41-18b1-4ed8-88ca-64ee60e67245

📥 Commits

Reviewing files that changed from the base of the PR and between 380edba and a4b8d5d.

📒 Files selected for processing (1)
  • server/service/client.go

Walkthrough

Adds validation to GitOps processing that detects when a macOS setup-experience provides a script while macos_manual_agent_install is enabled and returns an immediate error instead of attempting script validation or upload. The guard is applied in both the team-scoped and no-team (unassigned hosts) flows in the service client. A unit test and an enterprise integration test were added to assert that both --dry-run and real fleetctl gitops runs fail with the expected macos_manual_agent_install conflict.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 PR title clearly and specifically describes the main bug fix: catching a conflict between manual_agent_install and macos_script in gitops dry-run, which aligns with the core changes.
Description check ✅ Passed The PR description comprehensively covers the what, why, and how of the fix, includes testing details, reproduction steps, and completes all relevant checklist items for security, testing, and changes files.
Linked Issues check ✅ Passed The PR fully addresses issue #34464 by adding client-side validation to detect the manual_agent_install + macos_script conflict in both dry-run and real GitOps runs, matching the expected behavior described in the issue.
Out of Scope Changes check ✅ Passed All code changes are focused on the specific conflict validation: a changes file entry, client-side validation in two code paths, and regression tests. No unrelated changes detected.

✏️ 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 34464

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

…essages

Match the server-side Unicode curly apostrophe (U+2019) in the client-side
validation messages, addressing PR review feedback about wording consistency
between dry-run and real-run error output.
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — small, well-scoped guard added in two places to mirror the existing server-side validation during gitops dry-run; integration test properly covers the regression and reviewer feedback has been addressed.

Extended reasoning...

Overview

This PR adds a client-side guard in server/service/client.go (team path at line 803-808 and no-team path at line 2603-2608) so that fleetctl gitops --dry-run catches the macos_manual_agent_install: true + macos_script: <path> conflict that previously only surfaced in real runs (because the script upload was gated by !opts.DryRun). It also adds a unit test in gitops_test.go and an enterprise integration test in gitops_enterprise_integration_test.go covering both team and no-team paths.

Security risks

None. This is a CLI-side validation that fails earlier with the same error users would already see on a real run; it widens no permissions, exposes no data, and accepts no new untrusted input. The error string interpolates the team key via %q (safe quoting).

Level of scrutiny

Low. The production change is ~10 lines of guard logic added immediately before existing validateMacOSSetupScript calls, following the same pattern. There is no logic forking, no new dependency, no API surface change, and no impact on real-run behavior (which already errored out via the server). The author has already addressed Copilot's concerns about apostrophe/Unicode consistency and explained the rationale for keeping the contextual suffixes.

Other factors

A bug-hunter inline comment flags that the unit test substring assertion ("macos_manual_agent_install") is too loose because the unit-test team YAML omits macos_bootstrap_package, so a pre-existing server-side validator emits an error containing the same substring — meaning the unit test would still pass even if the new client-side guard were reverted. This is a legitimate test-quality nit, not a production bug. The integration test correctly includes macos_bootstrap_package and properly isolates the regression scenario, so the fix is meaningfully covered. The nit is low-impact and can be addressed at the author's discretion.

Comment on lines +6533 to +6537
require.Error(t, err)
assert.Contains(t, err.Error(), "macos_manual_agent_install")

// Real run should fail
_, err = RunAppNoChecks([]string{"gitops", "-f", teamFile.Name()})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The new unit test TestGitOpsMacOSSetupScriptWithManualAgentInstallConflict asserts on the substring "macos_manual_agent_install", but the team YAML at lines 6516-6528 (and the no-team YAML at 6561-6568) omit macos_bootstrap_package, so the pre-existing server-side validator at server/fleet/app.go:582 returns an error that also contains that substring ("Couldn't enable macos_manual_agent_install. To use this option, first specify a bootstrap package."). As a result, the assertions on lines 6533/6537 (team) and 6573/6577 (no-team) would still pass even if the new client-side guard at server/service/client.go:803-808 were removed — defeating the regression-test purpose. To tighten this, either include macos_bootstrap_package in the YAMLs (matching the integration test) or assert on a more specific substring like "first disable macos_manual_agent_install".

Extended reasoning...

What the bug is

The new unit test TestGitOpsMacOSSetupScriptWithManualAgentInstallConflict is intended to be a regression guard for #34464, which added a client-side check at server/service/client.go:803-808 (team) and :2603-2608 (no-team) so that gitops --dry-run catches the macos_manual_agent_install + macos_script conflict. However, the unit test's assertion is too loose: assert.Contains(t, err.Error(), "macos_manual_agent_install") matches any error mentioning that field, not specifically the new conflict error.

The specific code path that triggers the false-negative

The team YAML at lines 6516-6528 sets:

controls:
  setup_experience:
    macos_manual_agent_install: true
    macos_script: %s

It does not set macos_bootstrap_package. The pre-existing server-side validator at server/fleet/app.go:582-584 is:

if mos.ManualAgentInstall.Valid && mos.ManualAgentInstall.Value && (!mos.BootstrapPackage.Valid || mos.BootstrapPackage.Value == "") {
    return NewInvalidArgumentError("setup_experience.macos_manual_agent_install", `Couldn't enable macos_manual_agent_install. To use this option, first specify a bootstrap package.`)
}

This validator is invoked from ee/server/service/teams.go:1240 inside ApplyTeamSpecs (before the dry-run branch), so it fires on both dry-run and real runs. The returned error string contains the substring "macos_manual_agent_install".

Step-by-step proof (hypothetically reverting the fix)

  1. Imagine the new client-side guard at server/service/client.go:803-808 is removed.
  2. validateMacOSSetupScript only does os.ReadFile on the script — succeeds (the test creates a real temp file).
  3. The flow proceeds to ApplyTeams (with DryRun=true).
  4. applyTeamSpecs at ee/server/service/teams.go:1240 calls spec.MDM.MacOSSetup.Validate().
  5. Validate() returns the pre-existing "Couldn't enable macos_manual_agent_install..." error because BootstrapPackage is empty.
  6. The test reaches assert.Contains(t, err.Error(), "macos_manual_agent_install") — and it PASSES because the bootstrap-package error string also contains macos_manual_agent_install.

So the test passes today both because of the new client-side check and because of an unrelated server-side validation. If the new check were ever reverted/reordered, the unit test would still go green and silently lose its regression value.

Why existing code doesn't prevent it

The integration test at gitops_enterprise_integration_test.go:4798 correctly includes macos_bootstrap_package: %s in its team template, so it does properly isolate the new behavior at the integration layer. The unit test simply did not match that pattern.

Note: the no-team subtest is partially mitigated for a different reason — no-team controls are not folded into mdmAppConfig server-side, so MacOSSetup.Validate() isn't invoked for the no-team config. But the team subtest is still genuinely deficient, and tightening both keeps the assertions consistent.

Impact

This is a test-quality / regression-risk issue, not a production bug. The runtime fix in client.go is correct and the integration test does cover end-to-end behavior. But the unit test, despite its name (...WithManualAgentInstallConflict), is not a strict regression test for the new client-side guard.

How to fix

Either of these is sufficient (the integration test does the first):

  1. Add macos_bootstrap_package: <some-url> to the team YAML so the bootstrap-package validator does not fire, leaving only the new client-side check as the source of error.
  2. Tighten the assertion to a more discriminating substring that only the new check produces, e.g. assert.Contains(t, err.Error(), "first disable macos_manual_agent_install") (or "Couldn't add setup experience script").

The same applies to the no-team subtest assertions on lines 6573/6577.

@sharon-fdm sharon-fdm merged commit beca71e into main May 4, 2026
60 checks passed
@sharon-fdm sharon-fdm deleted the 34464 branch May 4, 2026 19:32
@coderabbitai coderabbitai Bot mentioned this pull request May 5, 2026
6 tasks
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.

Gitops dry-run for "macos_setup" succeeds when "manual_agent_install" is set to true and a script is configured

3 participants