Skip to content

Self service categories - install all - #46865

Merged
jkatz01 merged 12 commits into
feat/39018-self-service-categoriesfrom
46393-categories-install-all
Jun 5, 2026
Merged

Self service categories - install all#46865
jkatz01 merged 12 commits into
feat/39018-self-service-categoriesfrom
46393-categories-install-all

Conversation

@jkatz01

@jkatz01 jkatz01 commented Jun 4, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #46393

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.

  • 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

  • If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes

Testing

jkatz01 added 10 commits June 4, 2026 17:05
- remove the self-service install-all title cap (no limit on queued titles)
- endpoint signature interface{} -> any
- datastore test for GetSoftwareTitlesForInstallAll (status/inventory/label/category/team matrix, alpha order)
- consolidate integration coverage into TestInstallAllSelfServiceSoftware: install statuses, existing/incoming + concurrent activities (incl. double-queue), category/label/team scoping, multiple hosts, with VPP exercised in every run
- add TestSelfServiceInstallDoubleEnqueue characterizing the pre-existing double-enqueue
Backend:
- GetSoftwareTitlesForInstallAll returns BadRequestError for a nonexistent
  category_id (client-supplied bad param), not the wrapped not-found error.
- Fix a misleading comment: the len(InstalledVersions) skip means the title
  is already installed.

Tests:
- Datastore: assert BadRequestError for both nonexistent and cross-fleet
  categories; integration: nonexistent category now expects 400.
- Rename rollupName -> installAllActivityName and completeHead ->
  completeActivatedInstall.
- Inline the single-use mdmEnroll helper into newMDMHost.
- Drop the temporary TestSelfServiceInstallDoubleEnqueue.
- Replace the AppConfig-cache time.Sleep with an EventuallyWithT poll of
  GET /config so we wait on the actual condition, never a fixed duration.
@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.75000% with 13 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (feat/39018-self-service-categories@1036719). Learn more about missing BASE report.

Files with missing lines Patch % Lines
server/datastore/mysql/software_installers.go 83.33% 4 Missing and 3 partials ⚠️
server/service/software_installers.go 62.50% 5 Missing and 1 partial ⚠️
Additional details and impacted files
@@                          Coverage Diff                          @@
##             feat/39018-self-service-categories   #46865   +/-   ##
=====================================================================
  Coverage                                      ?   66.83%           
=====================================================================
  Files                                         ?     2807           
  Lines                                         ?   224009           
  Branches                                      ?    11328           
=====================================================================
  Hits                                          ?   149713           
  Misses                                        ?    60696           
  Partials                                      ?    13600           
Flag Coverage Δ
backend 68.55% <83.75%> (?)

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

☔ View full report in Codecov by Harness.
📢 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.

@jkatz01

jkatz01 commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

TestTranslateCPEToCVE/find_vulns_on_cpes is failing but unrelated, it's fixed on main but not on the feature branch so I think it's fine for now.

@jkatz01
jkatz01 marked this pull request as ready for review June 4, 2026 23:15
@jkatz01
jkatz01 requested a review from a team as a code owner June 4, 2026 23:15

@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.

@jkatz01

jkatz01 commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

@claude review once

@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.

I didn't find any bugs in the new install-all flow, but this PR adds a sizeable new self-service endpoint with VPP/in-house/package branches and label/category/team scoping — worth a human pass before merging into the feature branch.

Extended reasoning...

Overview

This PR adds a new "install all" self-service software flow on top of the in-progress self-service categories feature branch (feat/39018-self-service-categories). It introduces:

  • A new device endpoint POST /api/_version_/fleet/device/{token}/software/install_all (with optional category_id query param)
  • A new EE service method SelfServiceInstallAllSoftwareTitles (~120 lines) that branches across VPP apps, in-house apps, and package installers, with per-title label scoping and pre-flight checks
  • A new datastore method GetSoftwareTitlesForInstallAll that filters self-service titles by status, inventory, category, and team scoping
  • A new ActivityTypeInstalledAllSelfServiceSoftware activity and roll-up
  • Exposing the previously-unexported InstallerAvailableForInstallForTeamAndTitleID for reuse
  • ~500 lines of integration test coverage and a focused datastore test

Security risks

The endpoint is authenticated via device token (same model as the existing single-title self-service install), and category/team scoping is validated in the datastore layer. Label scoping (include-any / exclude-any) is applied per title at queue time. The main attack surface is the "queue every available title" fan-out from a compromised device token, but that's already implicit in the per-title self-service endpoint that existed before. No new SQL injection / authz bypass / data exposure surface jumped out.

Level of scrutiny

Production-critical: this lands a new user-facing endpoint that queues MDM commands and software installs against real hosts, with multi-branch logic (VPP / in-house / package) and several scoping rules (category, team, label, MDM enrollment, inventory state) that all need to agree. The integration tests do cover the interesting cases (idempotency, queue coexistence, category/label/team scoping, the documented double-queue race for concurrent installs), but the combinatorics make a human review valuable.

Other factors

  • Bug-hunting agents found no defects
  • Patch coverage is 57.9% per Codecov; the EE service method is the largest gap (39%)
  • Targets a feature branch, not main, so blast radius is limited until that branch merges
  • One unrelated CI failure (TestTranslateCPEToCVE/find_vulns_on_cpes), already acknowledged in-thread as fixed on main
  • Author explicitly requested a bot review

return nil
}()
if queueErr != nil {
svc.logger.ErrorContext(ctx, "failed to enqueue software install", "title_id", title.ID, "err", queueErr)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like we log here but the device gets a 202 no matter how many failed, is that right?

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.

Yeah I went with just logging any errors that happen within the loop for this, because we wouldn't want to fail the whole thing even if one installer failed right? Is there maybe something more nuanced we can do like fail if the count of actually queued installs doesn't match the count coming in? I'll leave that for later.

I think I can replace this whole part of the loop with SelfServiceInstallSoftwareTitle and log the errors from that rather than return, since it is queuing installs one by one anyway. Downside is it uses the slightly more expensive GetSoftwareInstallerMetadataByTeamAndTitleID function.

I think I'll give that a try.

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.

Oh, looks like we actually do want an error message here. My bad for missing that.

POST /device/{token}/software/install_all — when one or more apps in the list are currently installing, updating, or uninstalling, verify the endpoint returns an appropriate error with a human-readable message.

POST /device/{token}/software/install_all — verify that no apps are queued or double-queued when the request is rejected due to an in-progress operation.

Verify the error response body includes enough detail to identify which app(s) are blocking the request (or at minimum that the message is actionable for the end user).

cc @Brajim20 heads up that we missed this, in case you get to QAing before this is fixed.

// queue each software install individually
var queuedCount uint
for _, title := range titles {
// log errors rather than return early

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well, we still return early for out of scope labels below (nit).

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.

Updated the comment to the one below. Now that it's using SelfServiceInstallSoftwareTitle this is actually correct because out of scope labels get treated as errors.

// Queue individual install activities for each title. If any errors occurred while
// queuing this title we log them and continue to the next software title.

@cdcme cdcme left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Couple of minor questions which could be dealt with later. Looks fantastic!

Undo exporting installerAvailableForInstallForTeamAndTitleID
@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: vuln-mysql8.0.44

Failure summary:

The action failed because at least one test job reported a failing status in its downloaded status
artifact files.
- The status aggregation script found ./vuln-mysql8.0.44-status/status contained
fail, so it marked vuln-mysql8.0.44 as failed and exited with code 1 (❌ One or more test jobs
failed: vuln-mysql8.0.44).

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

119:  Artifact download completed successfully.
120:  Extracting artifact entry: /home/runner/work/fleet/fleet/integration-mdm-mysql8.0.44-status/status
121:  Artifact download completed successfully.
122:  Extracting artifact entry: /home/runner/work/fleet/fleet/vuln-mysql8.0.44-status/status
123:  Artifact download completed successfully.
124:  Redirecting to blob download url: https://productionresultssa11.blob.core.windows.net/actions-results/2b7d854a-5b9f-490d-bb15-6192373bc3bb/workflow-job-run-f051017c-6bb5-5c21-b8ca-c9126c656c36/artifacts/121f22d84b5e61b860eb3979485c8f64ebda0f03ee595ec8d53e25cd87b57e3e.zip
125:  Starting download of artifact to: /home/runner/work/fleet/fleet/main-mysql8.0.44-status
126:  Redirecting to blob download url: https://productionresultssa11.blob.core.windows.net/actions-results/2b7d854a-5b9f-490d-bb15-6192373bc3bb/workflow-job-run-e990efae-0ff1-57f9-90d0-6470e8fc27f3/artifacts/023cd68f0028f26ec8a302741c14d1063773dfa881aec556f78d076f4e6e56a0.zip
127:  Starting download of artifact to: /home/runner/work/fleet/fleet/integration-enterprise-mysql8.0.44-status
128:  Extracting artifact entry: /home/runner/work/fleet/fleet/main-mysql8.0.44-status/status
129:  Artifact download completed successfully.
130:  Extracting artifact entry: /home/runner/work/fleet/fleet/integration-enterprise-mysql8.0.44-status/status
131:  Artifact download completed successfully.
132:  Total of 10 artifact(s) downloaded
133:  Download artifact has finished successfully
134:  ##[group]Run failed_tests=""
135:  �[36;1mfailed_tests=""�[0m
136:  �[36;1mstatus_count=0�[0m
137:  �[36;1m# Find all status files (they are in directories like 'fleetctl-mysql8.0.44-status/status')�[0m
138:  �[36;1mfor status_file in $(find ./ -type f -name 'status'); do�[0m
139:  �[36;1m  status_count=$((status_count + 1))�[0m
140:  �[36;1m  # Extract test name from parent directory (e.g., 'fleetctl-mysql8.0.44-status')�[0m
141:  �[36;1m  test_dir=$(basename $(dirname "$status_file"))�[0m
142:  �[36;1m  # Remove '-status' suffix to get the test name�[0m
143:  �[36;1m  test_name="${test_dir%-status}"�[0m
144:  �[36;1m  status_content=$(cat "$status_file")�[0m
145:  �[36;1m  echo "Processing: $status_file (Test: $test_name) with status content: $status_content"�[0m
146:  �[36;1m  if grep -q "fail" "$status_file"; then�[0m
147:  �[36;1m    echo "  ❌ Test failed: $test_name"�[0m
148:  �[36;1m    failed_tests="${failed_tests}${test_name}, "�[0m
149:  �[36;1m  else�[0m
150:  �[36;1m    echo "  ✅ Test passed: $test_name"�[0m
151:  �[36;1m  fi�[0m
152:  �[36;1mdone�[0m
153:  �[36;1mif [[ $status_count -eq 0 ]]; then�[0m
154:  �[36;1m  echo "❌ ERROR: No status files found! This indicates a workflow issue."�[0m
155:  �[36;1m  exit 1�[0m
156:  �[36;1mfi�[0m
157:  �[36;1mif [[ -n "$failed_tests" ]]; then�[0m
158:  �[36;1m  echo "❌ One or more test jobs failed: ${failed_tests%, }"�[0m
159:  �[36;1m  exit 1�[0m
160:  �[36;1mfi�[0m
161:  �[36;1mecho "✅ All test jobs succeeded."�[0m
162:  shell: /usr/bin/bash --noprofile --norc -e -o pipefail {0}
163:  ##[endgroup]
164:  Processing: ./integration-core-mysql8.0.44-status/status (Test: integration-core-mysql8.0.44) with status content: success
165:  ✅ Test passed: integration-core-mysql8.0.44
166:  Processing: ./vuln-mysql8.0.44-status/status (Test: vuln-mysql8.0.44) with status content: fail
167:  ❌ Test failed: vuln-mysql8.0.44
168:  Processing: ./service-mysql8.0.44-status/status (Test: service-mysql8.0.44) with status content: success
169:  ✅ Test passed: service-mysql8.0.44
170:  Processing: ./main-mysql8.0.44-status/status (Test: main-mysql8.0.44) with status content: success
171:  ✅ Test passed: main-mysql8.0.44
172:  Processing: ./fleetctl-mysql8.0.44-status/status (Test: fleetctl-mysql8.0.44) with status content: success
173:  ✅ Test passed: fleetctl-mysql8.0.44
174:  Processing: ./integration-enterprise-mysql8.0.44-status/status (Test: integration-enterprise-mysql8.0.44) with status content: success
175:  ✅ Test passed: integration-enterprise-mysql8.0.44
176:  Processing: ./fast-status/status (Test: fast) with status content: success
177:  ✅ Test passed: fast
178:  Processing: ./mysql-mysql8.0.44-status/status (Test: mysql-mysql8.0.44) with status content: success
179:  ✅ Test passed: mysql-mysql8.0.44
180:  Processing: ./integration-mdm-mysql8.0.44-status/status (Test: integration-mdm-mysql8.0.44) with status content: success
181:  ✅ Test passed: integration-mdm-mysql8.0.44
182:  Processing: ./scripts-status/status (Test: scripts) with status content: success
183:  ✅ Test passed: scripts
184:  ❌ One or more test jobs failed: vuln-mysql8.0.44
185:  ##[error]Process completed with exit code 1.
186:  Post job cleanup.

@jkatz01
jkatz01 merged commit d6b56d8 into feat/39018-self-service-categories Jun 5, 2026
32 of 34 checks passed
@jkatz01
jkatz01 deleted the 46393-categories-install-all branch June 5, 2026 17:43
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.

3 participants