Skip to content

Install Fleet android agent on device enrollment. - #36050

Merged
getvictor merged 3 commits into
mainfrom
victor/35434-install-android-agent
Nov 21, 2025
Merged

Install Fleet android agent on device enrollment.#36050
getvictor merged 3 commits into
mainfrom
victor/35434-install-android-agent

Conversation

@getvictor

@getvictor getvictor commented Nov 20, 2025

Copy link
Copy Markdown
Member

Related issue: Resolves #35434

Feature is largely behind feature flag FLEET_DEV_ANDROID_AGENT_PACKAGE
Set it like: export FLEET_DEV_ANDROID_AGENT_PACKAGE=com.fleetdm.agent.private.victor

Rough set up:

  1. Change the applicationId of your Android app in build.gradle.kts:
    defaultConfig {
        applicationId = "com.fleetdm.agent.private.you"
  1. Build a release version of your app (use dummy signing key). Build -> Generate Signed App Bundle or APK ...
  2. Get the super secret Google Play URL like: go run tools/android/android.go --command enterprises.webTokens.create --enterprise_id 'XXXX'
  3. Upload your signed app.
  4. Wait ~10 minutes
  5. Enroll your Android device.
  6. The agent should start installing pretty soon. Check your Google Play in Work profile. Mine was pending for a while the last time I tried it and I restarted the device before it actually started installing.

@ksykulev you can use this Android service method for "notification":
AddFleetAgentToAndroidPolicy(ctx context.Context, enterpriseName string, hostConfigs map[string]AgentManagedConfiguration) error
You'll need to update AgentManagedConfiguration struct to define what to send down to the device. It includes the enroll secret, so I think we need to send it down every time just to be safe.

Checklist for submitter

  • Changes file will be updated when full feature is done.

Testing

  • QA'd all new/changed functionality manually

@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

The changes implement automated Fleet Agent installation on Android devices during MDM enrollment. A new struct carries host UUID and enroll secret data through the policy application pipeline, which is integrated into the software worker to apply Fleet Agent policies to host configurations.

Changes

Cohort / File(s) Summary
Core data structures and interfaces
server/mdm/android/android.go, server/mdm/android/service.go
New AgentManagedConfiguration struct with HostUUID and EnrollSecret fields; Service interface extended with AddFleetAgentToAndroidPolicy method for managing Fleet Agent policies.
Policy implementation
server/mdm/android/service/service.go
New AddFleetAgentToAndroidPolicy method applies Fleet Agent ApplicationPolicy with FORCE_INSTALLED install type and CERT_INSTALL delegated scope to per-host policies, gated by FLEET_DEV_ANDROID_AGENT_PACKAGE environment variable. Collects and joins per-host errors.
Software worker integration
server/worker/software_worker.go
When policy ID is "1", fetches Android host once at start, retrieves team's enroll secrets, validates at least one exists, and invokes AddFleetAgentToAndroidPolicy with host config data. Eliminates redundant host re-fetch.
Tooling utilities
tools/android/android.go
Normalizes enterprise_id by stripping "enterprises/" prefix; adds new enterprises.webTokens.create command handler to construct and create WebTokens with APPROVE_APPS permission.

Sequence Diagram

sequenceDiagram
    participant Worker as Software Worker
    participant AndroidSvc as Android Service
    participant MDMApi as MDM API
    
    Worker->>AndroidSvc: Fetch Android Host
    AndroidSvc-->>Worker: Host Data
    
    Worker->>AndroidSvc: Get Team Enroll Secrets
    AndroidSvc-->>Worker: Secrets List
    
    Note over Worker: Validate ≥1 Secret
    
    Worker->>AndroidSvc: AddFleetAgentToAndroidPolicy<br/>(hostConfigs with UUID, Secret)
    
    loop Per-Host Policy
        AndroidSvc->>AndroidSvc: Marshal ManagedConfig to JSON
        AndroidSvc->>MDMApi: EnterprisesPoliciesModifyPolicyApplications<br/>(FORCE_INSTALLED, CERT_INSTALL)
        MDMApi-->>AndroidSvc: Policy Applied
    end
    
    AndroidSvc-->>Worker: Errors (if any, joined)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Error handling pattern in service/service.go: Verify error collection and joining logic is robust across per-host operations
  • Environment variable gating: Confirm FLEET_DEV_ANDROID_AGENT_PACKAGE check is the intended control mechanism for this feature
  • Enroll secret validation in software_worker.go: Review the validation that at least one enroll secret exists and error handling for missing secrets
  • Data flow consistency: Ensure AgentManagedConfiguration fields (HostUUID, EnrollSecret) correctly propagate through the policy application pipeline
  • API call parameters in service/service.go: Validate that FORCE_INSTALLED, GRANT, and CERT_INSTALL are correct values for the MDM API

Suggested reviewers

  • sharon-fdm
  • mostlikelee
  • sgress454

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The PR description provides context for a feature behind a feature flag with setup instructions, but lacks formal adherence to the repository's PR template structure. Confirm whether changes file is required or deferred to final PR; verify all template sections are addressed before merging.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: installing the Fleet Android agent on device enrollment, which aligns with the primary objective.
Linked Issues check ✅ Passed Code changes implement the core requirements: new AgentManagedConfiguration struct [android.go], AddFleetAgentToAndroidPolicy method [service.go, service/service.go] with FORCE_INSTALLED policy, and enrollment-time installation via software_worker.go [#35434].
Out of Scope Changes check ✅ Passed All changes directly support Android agent enrollment installation. The tools/android/android.go changes add web token creation for Play Store URL generation, which supports the agent enrollment workflow.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch victor/35434-install-android-agent

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

Caution

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

⚠️ Outside diff range comments (1)
server/worker/software_worker.go (1)

168-168: Verify androidHost.DeviceID is non-nil before constructing device name.

The code constructs deviceName using androidHost.DeviceID, but there's no nil check. If Device or DeviceID is nil/empty, this will construct an invalid device name.

Apply this diff to add defensive checks:

+		if androidHost.Device == nil || androidHost.Device.DeviceID == "" {
+			return ctxerr.Errorf(ctx, "android host %s has no device ID", hostUUID)
+		}
 		device := &androidmanagement.Device{
 			PolicyName: policyName,
-			// State must be specified when updating a device, otherwise it fails with
-			// "Illegal state transition from ACTIVE to DEVICE_STATE_UNSPECIFIED"
-			//
-			// > Note that when calling enterprises.devices.patch, ACTIVE and
-			// > DISABLED are the only allowable values.

-			// TODO(ap): should we send whatever the previous state was? If it was DISABLED,
-			// we probably don't want to re-enable it by accident. Those are the only
-			// 2 valid states when patching a device.
 			State: "ACTIVE",
 		}
 		deviceName := fmt.Sprintf("%s/devices/%s", enterpriseName, androidHost.DeviceID)
🧹 Nitpick comments (1)
server/mdm/android/service/service.go (1)

902-902: Consider logging when the agent package is not configured.

The method silently returns when FLEET_DEV_ANDROID_AGENT_PACKAGE is not set. This could make debugging difficult when the agent isn't installed as expected.

Apply this diff to add debug logging:

 	var errs []error
-	if packageName := os.Getenv("FLEET_DEV_ANDROID_AGENT_PACKAGE"); packageName != "" {
+	packageName := os.Getenv("FLEET_DEV_ANDROID_AGENT_PACKAGE")
+	if packageName == "" {
+		level.Debug(svc.logger).Log("msg", "skipping fleet agent installation: FLEET_DEV_ANDROID_AGENT_PACKAGE not set")
+		return nil
+	}
+	{
 		for uuid, managedConfig := range hostConfigs {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21a13a7 and 83e4fd0.

📒 Files selected for processing (5)
  • server/mdm/android/android.go (1 hunks)
  • server/mdm/android/service.go (1 hunks)
  • server/mdm/android/service/service.go (1 hunks)
  • server/worker/software_worker.go (2 hunks)
  • tools/android/android.go (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

⚙️ CodeRabbit configuration file

When reviewing SQL queries that are added or modified, ensure that appropriate filtering criteria are applied—especially when a query is intended to return data for a specific entity (e.g., a single host). Check for missing WHERE clauses or incorrect filtering that could lead to incorrect or non-deterministic results (e.g., returning the first row instead of the correct one). Flag any queries that may return unintended results due to lack of precise scoping.

Files:

  • server/mdm/android/service.go
  • server/mdm/android/android.go
  • server/mdm/android/service/service.go
  • tools/android/android.go
  • server/worker/software_worker.go
🧠 Learnings (2)
📓 Common learnings
Learnt from: MagnusHJensen
Repo: fleetdm/fleet PR: 33805
File: server/service/integration_mdm_test.go:1248-1251
Timestamp: 2025-10-03T18:16:11.482Z
Learning: In server/service/integration_mdm_test.go, the helper createAppleMobileHostThenEnrollMDM(platform string) is exclusively for iOS/iPadOS hosts (mobile). Do not flag macOS model/behavior issues based on changes within this helper; macOS provisioning uses different helpers such as createHostThenEnrollMDM.
📚 Learning: 2025-10-03T18:16:11.482Z
Learnt from: MagnusHJensen
Repo: fleetdm/fleet PR: 33805
File: server/service/integration_mdm_test.go:1248-1251
Timestamp: 2025-10-03T18:16:11.482Z
Learning: In server/service/integration_mdm_test.go, the helper createAppleMobileHostThenEnrollMDM(platform string) is exclusively for iOS/iPadOS hosts (mobile). Do not flag macOS model/behavior issues based on changes within this helper; macOS provisioning uses different helpers such as createHostThenEnrollMDM.

Applied to files:

  • server/mdm/android/android.go
  • server/mdm/android/service/service.go
  • tools/android/android.go
  • server/worker/software_worker.go
🔇 Additional comments (7)
server/mdm/android/android.go (1)

52-55: LGTM!

The AgentManagedConfiguration struct is well-defined with appropriate JSON tags for serialization.

tools/android/android.go (2)

45-48: LGTM!

The enterprise ID normalization is a helpful addition that makes the tool more user-friendly.


168-194: LGTM for development tooling.

The hardcoded ParentFrameUrl on line 174 is acceptable since this is development tooling in the tools/ directory.

server/mdm/android/service.go (1)

26-26: LGTM!

The interface method signature correctly matches the implementation in service/service.go.

server/worker/software_worker.go (1)

107-111: LGTM!

Good optimization to fetch the Android host once at the start, avoiding redundant database calls.

server/mdm/android/service/service.go (2)

895-928: Assumption is valid and consistently applied throughout the Android MDM flow.

The host UUID to policy ID assumption holds true. The code in profiles.go explicitly documents: "using the host uuid as policy id, so we don't need to track the id mapping to the host." The caller in software_worker.go passes hostUUID as the map key when invoking AddFleetAgentToAndroidPolicy, and the implementation directly uses the map key (uuid) as the policyID in the policy name construction. This pattern is consistently applied across the Android MDM service methods.


913-918: No format issues identified—the code correctly marshals the managed configuration.

The verification confirms the implementation is correct:

  1. The ManagedConfiguration field is a RawMessage type, which stores raw JSON bytes
  2. The API expects a JSON object with string keys mapping to typed values
  3. json.Marshal(managedConfig) produces valid JSON: {"hostUUID": "...", "enrollSecret": "..."}, which matches the required format
  4. The AgentManagedConfiguration struct is properly defined with JSON tags for serialization

The code correctly marshals and passes the managed configuration to the Google Android Management API in the expected format.

Comment on lines +135 to +153
// Get enroll secrets for the host's team (nil means global/no team)
enrollSecrets, err := v.Datastore.GetEnrollSecrets(ctx, androidHost.Host.TeamID)
if err != nil {
return ctxerr.Wrapf(ctx, err, "get enroll secrets for team %v", androidHost.Host.TeamID)
}
if len(enrollSecrets) == 0 {
return ctxerr.Errorf(ctx, "no enroll secrets found for team %v", androidHost.Host.TeamID)
}
// Use the first enroll secret
enrollSecret := enrollSecrets[0].Secret
err = v.AndroidModule.AddFleetAgentToAndroidPolicy(ctx, enterpriseName, map[string]android.AgentManagedConfiguration{
hostUUID: {
HostUUID: hostUUID,
EnrollSecret: enrollSecret,
},
})
if err != nil {
return ctxerr.Wrapf(ctx, err, "add fleet agent to android policy for host %s", hostUUID)
}

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 | 🟠 Major

🧩 Analysis chain

Verify enroll secret selection logic and error handling.

Two concerns:

  1. Line 141: The code returns an error if no enroll secrets exist. Consider whether this should be a fatal error or if the flow should continue without agent installation (since the agent is still in development based on the env var gate).

  2. Line 144: The code always uses the first enroll secret. Verify this is the intended behavior, especially for teams with multiple secrets.

Run the following script to check how enroll secrets are typically selected in similar flows:


🏁 Script executed:

#!/bin/bash
# Description: Find patterns of enroll secret selection in the codebase

# Search for other locations that retrieve and use enroll secrets
rg -nP --type=go -C3 'GetEnrollSecrets.*\[0\]|enrollSecrets\[0\]' -g '!vendor/**'

# Search for any documented preference or ordering logic
rg -nP --type=go -C3 'enroll.*secret.*first|select.*enroll.*secret' -g '!vendor/**'

Length of output: 1098


I need to gather more information to verify these concerns thoroughly. Let me search for additional context:

Let me search the codebase directly for more context:

Now let me search the codebase directly for the GetEnrollSecrets implementation and android context:


🏁 Script executed:

#!/bin/bash
# Find GetEnrollSecrets function implementation

# Search for the GetEnrollSecrets method definition
rg -nP --type=go 'func.*GetEnrollSecrets' -g '!vendor/**'

# Also search for any interface definition
rg -nP --type=go 'GetEnrollSecrets.*\(' -g '!vendor/**' -B2 -A10 | head -100

Length of output: 7523


🏁 Script executed:

#!/bin/bash
# Search for Android agent environment variables and feature gates

rg -nP --type=go -i 'android|ENABLE_ANDROID' -g '!vendor/**' -C2 | head -150

Length of output: 8163


🏁 Script executed:

#!/bin/bash
# Look for how errors are typically handled when lists are empty in this codebase
# and see if line 141 error is consistent with codebase patterns

rg -nP --type=go 'len.*==.*0.*return.*Err' -g '!vendor/**' | head -30

Length of output: 0


Let me check the GetEnrollSecrets implementation and search for any feature gating:


🏁 Script executed:

#!/bin/bash
# Check the actual GetEnrollSecrets implementation
cat server/datastore/mysql/app_configs.go | sed -n '231,280p'

Length of output: 1657


🏁 Script executed:

#!/bin/bash
# Search for any environment variable or configuration gate for android feature in software_worker.go
rg -nP --type=go -B20 'makeAndroidAppsAvailableForHost|makeAndroidAppAvailable' server/worker/software_worker.go | head -80

Length of output: 2918


🏁 Script executed:

#!/bin/bash
# Check if there's any gate before calling makeAndroidAppsAvailableForHostTask
rg -nP --type=go 'enqueue.*android|makeAndroidApps' -B5 -A5 -g '!vendor/**' | head -100

Length of output: 4003


Address undefined enroll secret selection and inconsistent error handling.

  1. Line 141 (error handling): The code fails hard if no enroll secrets exist, but this pattern is inconsistent across the codebase. In server/service/microsoft_mdm.go:1358, missing secrets are logged as a warning and execution continues. Decide whether this should halt processing or continue gracefully for the Android agent flow.

  2. Line 144 (secret selection): GetEnrollSecrets (in server/datastore/mysql/app_configs.go:231) has no ORDER BY clause, so enrollSecrets[0] returns an arbitrary/undefined secret, not a deterministic "first" one. The separate AggregateEnrollSecretPerTeam method explicitly uses ORDER BY created_at DESC to select the most recent secret. Either:

    • Order GetEnrollSecrets results by created_at DESC to match the intended behavior, OR
    • Select a secret using explicit logic (e.g., call AggregateEnrollSecretPerTeam or filter results after retrieval)
🤖 Prompt for AI Agents
In server/worker/software_worker.go around lines 135 to 153, the code currently
returns an error if no enroll secrets are found and naively uses
enrollSecrets[0] which is non-deterministic; change handling to match the
project convention used in server/service/microsoft_mdm.go:1358 (log a warning
and continue rather than hard-failing) or explicitly decide to abort (but be
consistent across codebase); also make secret selection deterministic by either
calling AggregateEnrollSecretPerTeam to obtain the most recent secret or by
ensuring GetEnrollSecrets returns results ordered by created_at DESC (or
sort/filter the returned slice to pick the newest) and then use that chosen
secret when calling AddFleetAgentToAndroidPolicy; update the error paths to
log/wrap consistently with surrounding code.

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.

Interesting, but non-urgent. Noted in follow up issue: #36052

@codecov

codecov Bot commented Nov 20, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 35.71429% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.00%. Comparing base (2aac622) to head (937339f).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
server/mdm/android/service/service.go 15.00% 16 Missing and 1 partial ⚠️
server/worker/software_worker.go 54.54% 5 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #36050      +/-   ##
==========================================
+ Coverage   65.98%   66.00%   +0.01%     
==========================================
  Files        2122     2122              
  Lines      180706   180636      -70     
  Branches     7456     7491      +35     
==========================================
- Hits       119243   119220      -23     
+ Misses      50561    50512      -49     
- Partials    10902    10904       +2     
Flag Coverage Δ
backend 67.58% <35.71%> (-0.01%) ⬇️

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.

@getvictor
getvictor marked this pull request as ready for review November 20, 2025 15:53
@getvictor
getvictor requested a review from a team as a code owner November 20, 2025 15:53
AppliedPolicyVersion *int64 `db:"applied_policy_version"`
}

type AgentManagedConfiguration struct {

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.

We also need to send the fleet base URL

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.

Done: serverURL

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.

Android agent: install app during MDM enrollment

2 participants