Skip to content

VPP/in-house app managed configuration bug fixes and integration test - #45452

Merged
jkatz01 merged 10 commits into
mainfrom
iosmac-integration-test-and-fix-stack-regressions
May 14, 2026
Merged

VPP/in-house app managed configuration bug fixes and integration test#45452
jkatz01 merged 10 commits into
mainfrom
iosmac-integration-test-and-fix-stack-regressions

Conversation

@jkatz01

@jkatz01 jkatz01 commented May 14, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #43973

Fixes a few bugs regarding managed app configurations. Includes a few cherry-picks for commits that were supposed to be merged, but got overwritten by subsequent PRs that didn't get pushed properly while working with the github stack.
Also includes some more integration test cases.

Checklist for submitter

If some of the following don't apply, delete the relevant line.

Testing

Summary by CodeRabbit

  • New Features

    • Improved error handling for VPP managed app configurations that reference unresolvable Fleet variables.
  • Bug Fixes

    • Fixed cleanup of VPP app configuration data during team deletion.
    • In-house app configurations now apply only to their specific installer instead of sibling installers.

Review Change Stack

jkatz01 added 9 commits May 13, 2026 16:48
Verify the three-state contract on SaveInHouseAppUpdates (nil =
leave unchanged, empty = clear, non-empty = set) and that each
case applies to both the iOS row and its iPadOS sibling.

Also reflow the VPPAppTeam/VPPAppStoreApp Configuration comments
so the JSON-vs-XML note sits on its own line.
The singular PATCH /software/titles/{id}/package endpoint targets
one software title. iOS and iPadOS rows for the same .ipa live
under different title_ids, so silently mirroring the configuration
across siblings on update would mutate a title the user didn't edit.

Drop installerIDsForInHouseAppSibling and write only to the
installer ID in the payload. The insert path keeps the dual-row
write because a single .ipa upload genuinely owns both rows.

Tests updated to match: the CRUD three-state semantics now assert
against the targeted row only, and testInHouseAppConfigSiblingRows
keeps the insert-time propagation check but drops the obsolete
update-time propagation assertion.
InsertHostVPPSoftwareInstall runs activateNextUpcomingActivity in the
same transaction as the upcoming-activity insert, so a host that's
missing the IDP mapping referenced by the stored config surfaces
ErrUnresolvableAppConfigVar synchronously on the install POST. The
error wasn't translated, so it bubbled to a 500. Wrap it as a
BadRequestError with a user-facing message at the service-layer entry
that both the admin and self-service install endpoints fan into.

Test exercises the path against an iOS host with no IDP mapping and
also asserts the failed install leaves no upcoming_activities row
behind.
The Contains check is the load-bearing assertion: if the field type
regresses from json.RawMessage to []byte, encoding/json base64-encodes
the bytes and the literal substring would no longer be on the wire.
NotContains was looking for the base64 of a synthesized payload that
doesn't match what would actually get encoded (HTML-escaped wrap bytes
on iOS, server-renormalized whitespace on Android), so it would miss
the regression anyway.
drainVerifyCmds was used in two places: inside installAndCaptureCmd
and in the self-service subtest. The self-service call ran against a
freshly enrolled host with nothing pending, so it was dead. With that
gone the helper only had one caller; inline the loop into
installAndCaptureCmd and remove the closure.
@codecov

codecov Bot commented May 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.22%. Comparing base (d96355a) to head (ec7024e).
⚠️ Report is 31 commits behind head on main.

Files with missing lines Patch % Lines
ee/server/service/software_installers.go 0.00% 4 Missing ⚠️
server/datastore/mysql/in_house_apps.go 0.00% 1 Missing and 2 partials ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main   #45452       +/-   ##
===========================================
- Coverage   66.77%   55.22%   -11.55%     
===========================================
  Files        2729     2731        +2     
  Lines      218648   218611       -37     
  Branches    10751    10751               
===========================================
- Hits       146004   120733    -25271     
- Misses      59454    86631    +27177     
+ Partials    13190    11247     -1943     
Flag Coverage Δ
backend 54.99% <0.00%> (-13.62%) ⬇️

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.

Mirrors the scheduled-update orchestration (refetch → version
comparison → auto-update dispatch) for a VPP app with a managed
Configuration, asserts the resulting InstallApplication command
carries the Configuration bytes and the activity is recorded with
from_auto_update=true.

Lives next to the other VPP managed-configuration subtests rather
than alongside TestVPPAppScheduledUpdates so the Configuration
coverage stays grouped.
@jkatz01
jkatz01 marked this pull request as ready for review May 14, 2026 14:55
@jkatz01
jkatz01 requested a review from a team as a code owner May 14, 2026 14:55

@claude claude Bot left a comment

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR refactors VPP managed app configuration storage and lifecycle. The Configuration field in VPPAppStoreApp changes from raw bytes to json.RawMessage to support JSON-aware semantics. In-house app configuration sibling propagation is removed, so updates apply only to the specified installer row. VPP configuration rows are now cleaned up when teams are deleted. Service-layer error handling explicitly catches unresolvable Fleet variable substitution and returns a BadRequestError. Android tests are updated to use JSON semantic assertions. Two comprehensive integration tests validate wire-format correctness and end-to-end MDM command injection with variable resolution, clearing, platform isolation, and auto-update scenarios.

Possibly related PRs

  • fleetdm/fleet#44935: Introduces per-host Fleet variable substitution and apple_mdm.ErrUnresolvableAppConfigVar that this PR handles in the VPP install service path.
  • fleetdm/fleet#44934: Refactors InstallApplication command building to use the updated configuration fetch semantics that this PR validates through integration tests.
  • fleetdm/fleet#44936: Audits the iOS/iPadOS send path and datastore configuration behavior that this PR tests for correct wire format and host UUID substitution.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 'VPP/in-house app managed configuration bug fixes and integration test' is clear and directly reflects the main changes in the changeset—fixes for managed app configuration handling and new integration tests.
Description check ✅ Passed The PR description identifies the related issue (#43973), completes the testing checklist sections with 'Added/updated automated tests' and 'QA'd functionality' checked, but omits several uncompleted sections like changes files, input validation, database migrations, and fleetd compatibility checks.
Linked Issues check ✅ Passed The PR delivers backend integration tests and code changes addressing issue #43973 objectives: fixed configuration encoding via json.RawMessage, implemented three-state SaveInHouseAppUpdates semantics with single-row scoping, added error handling for unresolvable Fleet variables, verified team deletion cleanup, and included comprehensive MDM integration tests covering InstallApplication commands, auto-updates, variable resolution, and platform-specific behavior.
Out of Scope Changes check ✅ Passed All changes are directly related to the bug fixes and integration testing objectives in issue #43973: configuration type corrections, in-house app update semantics, VPP app configuration handling, test coverage expansion, and database schema updates for team cleanup.

✏️ 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 iosmac-integration-test-and-fix-stack-regressions

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ee/server/service/software_installers.go`:
- Around line 1531-1538: The code currently converts
apple_mdm.ErrUnresolvableAppConfigVar from InsertHostVPPSoftwareInstall into a
400 without undoing a prior AssociateAssets call, leaving the host assigned a
VPP license; fix by either validating the managed app configuration before
calling AssociateAssets or, if keeping the current call order, explicitly
disassociate the assets when InsertHostVPPSoftwareInstall returns errors.Is(err,
apple_mdm.ErrUnresolvableAppConfigVar): call the corresponding disassociation
routine for the same asset IDs you passed to AssociateAssets (undo the external
assignment) and only then return the fleet.BadRequestError; reference
InsertHostVPPSoftwareInstall, AssociateAssets, and
apple_mdm.ErrUnresolvableAppConfigVar when making the change.
🪄 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: 0b61ec77-0e71-48de-8c56-5acd0f7700cd

📥 Commits

Reviewing files that changed from the base of the PR and between f263583 and ec7024e.

📒 Files selected for processing (8)
  • ee/server/service/software_installers.go
  • server/datastore/mysql/in_house_apps.go
  • server/datastore/mysql/in_house_apps_test.go
  • server/datastore/mysql/teams.go
  • server/datastore/mysql/vpp_test.go
  • server/fleet/vpp.go
  • server/service/integration_android_software_test.go
  • server/service/integration_apple_vpp_config_test.go
💤 Files with no reviewable changes (1)
  • server/datastore/mysql/in_house_apps_test.go

Comment on lines 1531 to +1538
err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, vppApp.VPPAppID, cmdUUID, eventID, opts)
if err != nil {
if errors.Is(err, apple_mdm.ErrUnresolvableAppConfigVar) {
return "", &fleet.BadRequestError{
Message: "Couldn't install. The managed app configuration references Fleet variables that can't be resolved for this host.",
InternalErr: err,
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Avoid consuming a VPP license on the new 400 path.

By the time InsertHostVPPSoftwareInstall returns apple_mdm.ErrUnresolvableAppConfigVar, AssociateAssets may already have succeeded above. This branch now turns that into a 400 without undoing the external assignment, so the host can consume a VPP license with no queued install or upcoming activity. Validate the managed config before AssociateAssets, or explicitly disassociate on this error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/server/service/software_installers.go` around lines 1531 - 1538, The code
currently converts apple_mdm.ErrUnresolvableAppConfigVar from
InsertHostVPPSoftwareInstall into a 400 without undoing a prior AssociateAssets
call, leaving the host assigned a VPP license; fix by either validating the
managed app configuration before calling AssociateAssets or, if keeping the
current call order, explicitly disassociate the assets when
InsertHostVPPSoftwareInstall returns errors.Is(err,
apple_mdm.ErrUnresolvableAppConfigVar): call the corresponding disassociation
routine for the same asset IDs you passed to AssociateAssets (undo the external
assignment) and only then return the fleet.BadRequestError; reference
InsertHostVPPSoftwareInstall, AssociateAssets, and
apple_mdm.ErrUnresolvableAppConfigVar when making the change.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Looks like this is a real issue, but it's out of scope for this PR:

// TODO(mna): should we associate the device (give the license) only when the
// upcoming activity is ready to run? I don't think so, because then it could
// fail when it's ready to run which is probably a worse UX as once enqueued
// you expect it to succeed. But eventually, we should do better management
// of the licenses, e.g. if the upcoming activity gets cancelled, it should
// release the reserved license.
//
// But the command is definitely not enqueued now, only when activating the
// activity.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: aggregate-result

Failed stage: Check for failures [❌]

Failed test name: integration-enterprise-mysql9.5.0

Failure summary:

The action failed because the workflow’s status-aggregation step detected multiple test jobs with
status files containing fail and then exited with code 1.
Failed jobs reported in the log:
-
integration-enterprise-mysql9.5.0
- service-mysql9.5.0
- integration-core-mysql8.0.44
-
integration-enterprise-mysql8.0.44
- integration-mdm-mysql8.0.44
- integration-core-mysql9.5.0
-
integration-mdm-mysql9.5.0
- service-mysql8.0.44
- mysql-mysql8.0.44

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

159:  Redirecting to blob download url: https://productionresultssa18.blob.core.windows.net/actions-results/83bf3e9d-c401-463f-9603-12d1645987a1/workflow-job-run-e6d09803-027e-5018-b350-7cc5d2600583/artifacts/74d5f586529e00f2e0070daf5977778508efac1c8d6e2c5066d3e0bd8d50a574.zip
160:  Starting download of artifact to: /home/runner/work/fleet/fleet/integration-mdm-mysql9.5.0-status
161:  Artifact download completed successfully.
162:  Extracting artifact entry: /home/runner/work/fleet/fleet/main-mysql8.0.44-status/status
163:  Extracting artifact entry: /home/runner/work/fleet/fleet/integration-core-mysql8.0.44-status/status
164:  Extracting artifact entry: /home/runner/work/fleet/fleet/integration-mdm-mysql9.5.0-status/status
165:  Artifact download completed successfully.
166:  Artifact download completed successfully.
167:  Artifact download completed successfully.
168:  Extracting artifact entry: /home/runner/work/fleet/fleet/scripts-status/status
169:  Artifact download completed successfully.
170:  Extracting artifact entry: /home/runner/work/fleet/fleet/integration-enterprise-mysql9.5.0-status/status
171:  Artifact download completed successfully.
172:  Total of 18 artifact(s) downloaded
173:  Download artifact has finished successfully
174:  ##[group]Run failed_tests=""
175:  �[36;1mfailed_tests=""�[0m
176:  �[36;1mstatus_count=0�[0m
177:  �[36;1m# Find all status files (they are in directories like 'fleetctl-mysql8.0.44-status/status')�[0m
178:  �[36;1mfor status_file in $(find ./ -type f -name 'status'); do�[0m
179:  �[36;1m  status_count=$((status_count + 1))�[0m
180:  �[36;1m  # Extract test name from parent directory (e.g., 'fleetctl-mysql8.0.44-status')�[0m
181:  �[36;1m  test_dir=$(basename $(dirname "$status_file"))�[0m
182:  �[36;1m  # Remove '-status' suffix to get the test name�[0m
183:  �[36;1m  test_name="${test_dir%-status}"�[0m
184:  �[36;1m  status_content=$(cat "$status_file")�[0m
185:  �[36;1m  echo "Processing: $status_file (Test: $test_name) with status content: $status_content"�[0m
186:  �[36;1m  if grep -q "fail" "$status_file"; then�[0m
187:  �[36;1m    echo "  ❌ Test failed: $test_name"�[0m
188:  �[36;1m    failed_tests="${failed_tests}${test_name}, "�[0m
189:  �[36;1m  else�[0m
190:  �[36;1m    echo "  ✅ Test passed: $test_name"�[0m
191:  �[36;1m  fi�[0m
192:  �[36;1mdone�[0m
193:  �[36;1mif [[ $status_count -eq 0 ]]; then�[0m
194:  �[36;1m  echo "❌ ERROR: No status files found! This indicates a workflow issue."�[0m
195:  �[36;1m  exit 1�[0m
196:  �[36;1mfi�[0m
197:  �[36;1mif [[ -n "$failed_tests" ]]; then�[0m
198:  �[36;1m  echo "❌ One or more test jobs failed: ${failed_tests%, }"�[0m
199:  �[36;1m  exit 1�[0m
200:  �[36;1mfi�[0m
201:  �[36;1mecho "✅ All test jobs succeeded."�[0m
202:  shell: /usr/bin/bash --noprofile --norc -e -o pipefail {0}
203:  ##[endgroup]
204:  Processing: ./mysql-mysql9.5.0-status/status (Test: mysql-mysql9.5.0) with status content: success
205:  ✅ Test passed: mysql-mysql9.5.0
206:  Processing: ./integration-enterprise-mysql9.5.0-status/status (Test: integration-enterprise-mysql9.5.0) with status content: fail
207:  ❌ Test failed: integration-enterprise-mysql9.5.0
208:  Processing: ./service-mysql9.5.0-status/status (Test: service-mysql9.5.0) with status content: fail
209:  ❌ Test failed: service-mysql9.5.0
210:  Processing: ./integration-core-mysql8.0.44-status/status (Test: integration-core-mysql8.0.44) with status content: fail
211:  ❌ Test failed: integration-core-mysql8.0.44
212:  Processing: ./integration-enterprise-mysql8.0.44-status/status (Test: integration-enterprise-mysql8.0.44) with status content: fail
213:  ❌ Test failed: integration-enterprise-mysql8.0.44
214:  Processing: ./fleetctl-mysql8.0.44-status/status (Test: fleetctl-mysql8.0.44) with status content: success
215:  ✅ Test passed: fleetctl-mysql8.0.44
216:  Processing: ./main-mysql9.5.0-status/status (Test: main-mysql9.5.0) with status content: success
217:  ✅ Test passed: main-mysql9.5.0
218:  Processing: ./integration-mdm-mysql8.0.44-status/status (Test: integration-mdm-mysql8.0.44) with status content: fail
219:  ❌ Test failed: integration-mdm-mysql8.0.44
220:  Processing: ./fast-status/status (Test: fast) with status content: success
221:  ✅ Test passed: fast
222:  Processing: ./integration-core-mysql9.5.0-status/status (Test: integration-core-mysql9.5.0) with status content: fail
223:  ❌ Test failed: integration-core-mysql9.5.0
224:  Processing: ./main-mysql8.0.44-status/status (Test: main-mysql8.0.44) with status content: success
225:  ✅ Test passed: main-mysql8.0.44
226:  Processing: ./integration-mdm-mysql9.5.0-status/status (Test: integration-mdm-mysql9.5.0) with status content: fail
227:  ❌ Test failed: integration-mdm-mysql9.5.0
228:  Processing: ./scripts-status/status (Test: scripts) with status content: success
229:  ✅ Test passed: scripts
230:  Processing: ./fleetctl-mysql9.5.0-status/status (Test: fleetctl-mysql9.5.0) with status content: success
231:  ✅ Test passed: fleetctl-mysql9.5.0
232:  Processing: ./vuln-mysql8.0.44-status/status (Test: vuln-mysql8.0.44) with status content: success
233:  ✅ Test passed: vuln-mysql8.0.44
234:  Processing: ./service-mysql8.0.44-status/status (Test: service-mysql8.0.44) with status content: fail
235:  ❌ Test failed: service-mysql8.0.44
236:  Processing: ./mysql-mysql8.0.44-status/status (Test: mysql-mysql8.0.44) with status content: fail
237:  ❌ Test failed: mysql-mysql8.0.44
238:  Processing: ./vuln-mysql9.5.0-status/status (Test: vuln-mysql9.5.0) with status content: success
239:  ✅ Test passed: vuln-mysql9.5.0
240:  ❌ One or more test jobs failed: integration-enterprise-mysql9.5.0, service-mysql9.5.0, integration-core-mysql8.0.44, integration-enterprise-mysql8.0.44, integration-mdm-mysql8.0.44, integration-core-mysql9.5.0, integration-mdm-mysql9.5.0, service-mysql8.0.44, mysql-mysql8.0.44
241:  ##[error]Process completed with exit code 1.
242:  Post job cleanup.

@jkatz01
jkatz01 merged commit 4f59f39 into main May 14, 2026
40 of 56 checks passed
@jkatz01
jkatz01 deleted the iosmac-integration-test-and-fix-stack-regressions branch May 14, 2026 17:51
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.

IOSMAC: Integration tests + QA checklist

2 participants