More fixes.

This commit is contained in:
Victor Lyuboslavsky 2026-04-20 16:07:18 -05:00
parent 94ef65240d
commit 33ee0e401a
No known key found for this signature in database
2 changed files with 198 additions and 62 deletions

View file

@ -29,57 +29,10 @@ func (ds *Datastore) NewAndroidHost(ctx context.Context, host *fleet.AndroidHost
}
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
// We use node_key as a unique identifier for the host table row. It matches: android/{enterpriseSpecificID}.
stmt := `
INSERT INTO hosts (
node_key,
hostname,
computer_name,
platform,
os_version,
build,
memory,
team_id,
hardware_serial,
cpu_type,
hardware_model,
hardware_vendor,
detail_updated_at,
label_updated_at,
uuid
) VALUES (
:node_key,
:hostname,
:computer_name,
:platform,
:os_version,
:build,
:memory,
:team_id,
:hardware_serial,
:cpu_type,
:hardware_model,
:hardware_vendor,
:detail_updated_at,
:label_updated_at,
:uuid
) ON DUPLICATE KEY UPDATE
hostname = VALUES(hostname),
computer_name = VALUES(computer_name),
platform = VALUES(platform),
os_version = VALUES(os_version),
build = VALUES(build),
memory = VALUES(memory),
team_id = VALUES(team_id),
hardware_serial = VALUES(hardware_serial),
cpu_type = VALUES(cpu_type),
hardware_model = VALUES(hardware_model),
hardware_vendor = VALUES(hardware_vendor),
detail_updated_at = VALUES(detail_updated_at),
label_updated_at = VALUES(label_updated_at),
uuid = VALUES(uuid)
`
result, err := sqlx.NamedExecContext(ctx, tx, stmt, map[string]interface{}{
// If the Fleet Android agent already orbit-enrolled this device, a hosts row exists
// keyed by uuid = enterpriseSpecificId. Reuse it instead of inserting a duplicate.
// (platform = '' covers agents that didn't send platform on orbit enroll.)
params := map[string]any{
"node_key": host.NodeKey,
"hostname": host.Hostname,
"computer_name": host.ComputerName,
@ -95,21 +48,112 @@ func (ds *Datastore) NewAndroidHost(ctx context.Context, host *fleet.AndroidHost
"detail_updated_at": host.DetailUpdatedAt,
"label_updated_at": host.LabelUpdatedAt,
"uuid": host.UUID,
})
if err != nil {
return ctxerr.Wrap(ctx, err, "new Android host")
}
id, _ := result.LastInsertId()
if id == 0 {
// This was an UPDATE, not an INSERT, so we need to get the host ID
var hostID uint
err := sqlx.GetContext(ctx, tx, &hostID, `SELECT id FROM hosts WHERE node_key = ?`, host.NodeKey)
var existingID uint
err := sqlx.GetContext(ctx, tx, &existingID,
`SELECT id FROM hosts WHERE uuid = ? AND (platform = 'android' OR platform = '') ORDER BY id LIMIT 1`,
host.UUID,
)
notExist := errors.Is(err, sql.ErrNoRows)
if err != nil && !notExist {
return ctxerr.Wrap(ctx, err, "check for existing orbit-enrolled Android host")
}
if notExist {
// No orbit-enrolled host for this uuid. Insert as usual.
// We use node_key as a unique identifier for the host table row. It matches: android/{enterpriseSpecificID}.
insertStmt := `
INSERT INTO hosts (
node_key,
hostname,
computer_name,
platform,
os_version,
build,
memory,
team_id,
hardware_serial,
cpu_type,
hardware_model,
hardware_vendor,
detail_updated_at,
label_updated_at,
uuid
) VALUES (
:node_key,
:hostname,
:computer_name,
:platform,
:os_version,
:build,
:memory,
:team_id,
:hardware_serial,
:cpu_type,
:hardware_model,
:hardware_vendor,
:detail_updated_at,
:label_updated_at,
:uuid
) ON DUPLICATE KEY UPDATE
hostname = VALUES(hostname),
computer_name = VALUES(computer_name),
platform = VALUES(platform),
os_version = VALUES(os_version),
build = VALUES(build),
memory = VALUES(memory),
team_id = VALUES(team_id),
hardware_serial = VALUES(hardware_serial),
cpu_type = VALUES(cpu_type),
hardware_model = VALUES(hardware_model),
hardware_vendor = VALUES(hardware_vendor),
detail_updated_at = VALUES(detail_updated_at),
label_updated_at = VALUES(label_updated_at),
uuid = VALUES(uuid)
`
result, err := sqlx.NamedExecContext(ctx, tx, insertStmt, params)
if err != nil {
return ctxerr.Wrap(ctx, err, "get host ID after update")
return ctxerr.Wrap(ctx, err, "new Android host")
}
id, _ := result.LastInsertId()
if id == 0 {
// This was an UPDATE, not an INSERT, so we need to get the host ID
var hostID uint
err := sqlx.GetContext(ctx, tx, &hostID, `SELECT id FROM hosts WHERE node_key = ?`, host.NodeKey)
if err != nil {
return ctxerr.Wrap(ctx, err, "get host ID after update")
}
host.Host.ID = hostID
} else {
host.Host.ID = uint(id) // nolint:gosec
}
host.Host.ID = hostID
} else {
host.Host.ID = uint(id) // nolint:gosec
// Orbit-enrolled Android host already exists; update it in place so both
// enrollment paths converge on a single hosts row.
params["id"] = existingID
updateStmt := `
UPDATE hosts SET
node_key = :node_key,
hostname = :hostname,
computer_name = :computer_name,
platform = :platform,
os_version = :os_version,
build = :build,
memory = :memory,
team_id = :team_id,
hardware_serial = :hardware_serial,
cpu_type = :cpu_type,
hardware_model = :hardware_model,
hardware_vendor = :hardware_vendor,
detail_updated_at = :detail_updated_at,
label_updated_at = :label_updated_at,
uuid = :uuid
WHERE id = :id`
if _, err := sqlx.NamedExecContext(ctx, tx, updateStmt, params); err != nil {
return ctxerr.Wrap(ctx, err, "update existing orbit-enrolled Android host")
}
host.Host.ID = existingID
}
host.Device.HostID = host.Host.ID

View file

@ -28,6 +28,7 @@ func TestAndroid(t *testing.T) {
fn func(t *testing.T, ds *Datastore)
}{
{"NewAndroidHost", testNewAndroidHost},
{"NewAndroidHostDedupesOrbitEnrolled", testNewAndroidHostDedupesOrbitEnrolled},
{"UpdateAndroidHost", testUpdateAndroidHost},
{"AndroidMDMStats", testAndroidMDMStats},
{"AndroidHostStorageData", testAndroidHostStorageData},
@ -124,6 +125,97 @@ func testNewAndroidHost(t *testing.T, ds *Datastore) {
require.Empty(t, lbls)
}
// testNewAndroidHostDedupesOrbitEnrolled covers the duplicate-Android-hosts fix.
// The Fleet Android agent enrolls first via /api/fleet/orbit/enroll,
// then later the AMAPI pubsub flow delivers a STATUS_REPORT that lands in
// NewAndroidHost. The dedupe works whether the agent also sends
// platform="android" (newer agents) or leaves it blank (older agents).
func testNewAndroidHostDedupesOrbitEnrolled(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
cases := []struct {
name string
platform string
}{
{"agent sends no platform", ""},
{"agent sends platform=android", "android"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ctx := testCtx()
enterpriseSpecificID := strings.ToUpper(uuid.New().String())
orbitHost, err := ds.EnrollOrbit(ctx,
fleet.WithEnrollOrbitMDMEnabled(true),
fleet.WithEnrollOrbitHostInfo(fleet.OrbitHostInfo{
HardwareUUID: enterpriseSpecificID,
HardwareSerial: enterpriseSpecificID,
Platform: tc.platform,
Hostname: "Samsung TestDevice",
ComputerName: "Samsung TestDevice",
HardwareModel: "TestModel",
}),
fleet.WithEnrollOrbitNodeKey(uuid.New().String()),
)
require.NoError(t, err)
require.NotZero(t, orbitHost.ID)
// Orbit enroll alone does not write an android_devices row; AndroidHostLite misses.
_, err = ds.AndroidHostLite(ctx, enterpriseSpecificID)
require.True(t, fleet.IsNotFound(err),
"before AMAPI arrives there is no android_devices row, so AndroidHostLite should miss")
// Simulate the AMAPI pubsub path calling NewAndroidHost. The fix makes
// NewAndroidHost find the existing orbit-enrolled hosts row by uuid and
// reuse it instead of inserting a duplicate.
newHost := createAndroidHost(enterpriseSpecificID)
returned, err := ds.NewAndroidHost(ctx, newHost, false)
require.NoError(t, err)
require.NotNil(t, returned)
require.Equal(t, orbitHost.ID, returned.Host.ID,
"NewAndroidHost must reuse the orbit-enrolled hosts row, not insert a duplicate")
// AndroidHostLite now finds the host via the newly-created android_devices row.
androidHost, err := ds.AndroidHostLite(ctx, enterpriseSpecificID)
require.NoError(t, err)
require.NotNil(t, androidHost)
require.Equal(t, orbitHost.ID, androidHost.Host.ID)
require.Equal(t, enterpriseSpecificID, androidHost.Host.UUID)
// Exactly one hosts row and one android_devices row for this device.
var hostCount, deviceCount int
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &hostCount,
`SELECT COUNT(*) FROM hosts WHERE uuid = ?`, enterpriseSpecificID))
require.Equal(t, 1, hostCount)
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &deviceCount,
`SELECT COUNT(*) FROM android_devices WHERE enterprise_specific_id = ?`, enterpriseSpecificID))
require.Equal(t, 1, deviceCount)
// Subsequent orbit re-enroll (agent node-key wipe, reinstall) stays idempotent.
_, err = ds.EnrollOrbit(ctx,
fleet.WithEnrollOrbitMDMEnabled(true),
fleet.WithEnrollOrbitHostInfo(fleet.OrbitHostInfo{
HardwareUUID: enterpriseSpecificID,
HardwareSerial: enterpriseSpecificID,
Platform: tc.platform,
Hostname: "Samsung TestDevice",
ComputerName: "Samsung TestDevice",
HardwareModel: "TestModel",
}),
fleet.WithEnrollOrbitNodeKey(uuid.New().String()),
)
require.NoError(t, err)
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &hostCount,
`SELECT COUNT(*) FROM hosts WHERE uuid = ?`, enterpriseSpecificID))
require.Equal(t, 1, hostCount)
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &deviceCount,
`SELECT COUNT(*) FROM android_devices WHERE enterprise_specific_id = ?`, enterpriseSpecificID))
require.Equal(t, 1, deviceCount)
})
}
}
func createAndroidHost(enterpriseSpecificID string) *fleet.AndroidHost {
// Device ID needs to be unique per device
deviceID := md5ChecksumBytes([]byte(enterpriseSpecificID))[:16]