diff --git a/cmd/fleetctl/fleetctl/hosts_test.go b/cmd/fleetctl/fleetctl/hosts_test.go index 41d125310f..0b02d71085 100644 --- a/cmd/fleetctl/fleetctl/hosts_test.go +++ b/cmd/fleetctl/fleetctl/hosts_test.go @@ -67,6 +67,9 @@ func TestHostsTransferByHosts(t *testing.T) { ds.ListHostsLiteByIDsFunc = func(ctx context.Context, ids []uint) ([]*fleet.Host, error) { return nil, nil } + ds.ListMDMAndroidUUIDsToHostIDsFunc = func(ctx context.Context, hostIDs []uint) (map[string]uint, error) { + return map[string]uint{}, nil + } assert.Equal(t, "", RunAppForTest(t, []string{"hosts", "transfer", "--team", "team1", "--hosts", "host1"})) assert.True(t, ds.AddHostsToTeamFuncInvoked) diff --git a/server/datastore/mysql/android.go b/server/datastore/mysql/android.go index 68ade001b3..90a257768d 100644 --- a/server/datastore/mysql/android.go +++ b/server/datastore/mysql/android.go @@ -1903,3 +1903,40 @@ func (ds *Datastore) updateAndroidAppConfigurationTx(ctx context.Context, tx sql } return nil } + +func (ds *Datastore) ListMDMAndroidUUIDsToHostIDs(ctx context.Context, hostIDs []uint) (map[string]uint, error) { + if len(hostIDs) == 0 { + return nil, nil + } + + stmt := ` +SELECT + h.id AS id, h.uuid AS uuid +FROM + hosts h + JOIN android_devices ad ON ad.host_id = h.id +WHERE + h.id IN (?) AND + h.platform = 'android' +` + + stmt, args, err := sqlx.In(stmt, hostIDs) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "prepare statement arguments") + } + + var rows []struct { + ID uint `db:"id"` + UUID string `db:"uuid"` + } + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, args...); err != nil { + return nil, ctxerr.Wrap(ctx, err, "list mdm android uuids to host ids") + } + + results := make(map[string]uint, len(rows)) + for _, r := range rows { + results[r.UUID] = r.ID + } + + return results, nil +} diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go index 9c01b3eb4e..858fa1451c 100644 --- a/server/datastore/mysql/software_installers.go +++ b/server/datastore/mysql/software_installers.go @@ -3113,17 +3113,18 @@ func (ds *Datastore) getIncludedHostUUIDMapForSoftware(ctx context.Context, tx s FROM hosts h JOIN android_devices ad ON ad.enterprise_specific_id = h.uuid + JOIN vpp_apps_teams vat ON vat.team_id <=> h.team_id AND vat.id = ? WHERE EXISTS (%s) - AND platform = 'android' + AND h.platform = 'android' `, filter) var queryResults []struct { UUID string `db:"uuid"` AppliedPolicyID *string `db:"applied_policy_id"` } - if err := sqlx.SelectContext(ctx, tx, &queryResults, stmt, softwareID, softwareID, softwareID); err != nil { - return nil, ctxerr.Wrap(ctx, err, "listing host uuids included in software scope") + if err := sqlx.SelectContext(ctx, tx, &queryResults, stmt, softwareID, softwareID, softwareID, softwareID); err != nil { + return nil, ctxerr.Wrap(ctx, err, "listing hosts included in software scope") } res := make(map[string]string, len(queryResults)) diff --git a/server/datastore/mysql/vpp.go b/server/datastore/mysql/vpp.go index b94d592a9b..622a2bb428 100644 --- a/server/datastore/mysql/vpp.go +++ b/server/datastore/mysql/vpp.go @@ -633,7 +633,7 @@ func (ds *Datastore) InsertVPPAppWithTeam(ctx context.Context, app *fleet.VPPApp } } - if vppToken != nil { + if vppToken != nil && app.Platform != fleet.AndroidPlatform { vppTokenID = &vppToken.ID } @@ -2250,6 +2250,7 @@ FROM ( 0 AS count_host_updated_after_labels, vpp_apps_teams.adam_id AS installable_id FROM vpp_apps_teams + JOIN hosts ON hosts.id = ? AND hosts.team_id <=> vpp_apps_teams.team_id LEFT JOIN vpp_app_team_labels ON vpp_app_team_labels.vpp_app_team_id = vpp_apps_teams.id WHERE vpp_app_team_labels.id IS NULL AND vpp_apps_teams.platform = 'android' @@ -2264,6 +2265,7 @@ FROM ( FROM vpp_app_team_labels vatl LEFT JOIN vpp_apps_teams ON vpp_apps_teams.id = vatl.vpp_app_team_id + JOIN hosts ON hosts.id = ? AND hosts.team_id <=> vpp_apps_teams.team_id LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id AND lm.host_id = ? WHERE vatl.exclude = 0 AND vpp_apps_teams.platform = 'android' @@ -2300,6 +2302,7 @@ FROM ( FROM vpp_app_team_labels vatl LEFT JOIN vpp_apps_teams ON vpp_apps_teams.id = vatl.vpp_app_team_id + JOIN hosts ON hosts.id = ? AND hosts.team_id <=> vpp_apps_teams.team_id LEFT OUTER JOIN labels lbl ON lbl.id = vatl.label_id LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id AND lm.host_id = ? @@ -2311,7 +2314,7 @@ FROM ( AND count_host_labels = 0) t; ` - err = sqlx.SelectContext(ctx, ds.reader(ctx), &applicationIDs, stmt, hostID, hostID, hostID) + err = sqlx.SelectContext(ctx, ds.reader(ctx), &applicationIDs, stmt, hostID, hostID, hostID, hostID, hostID, hostID) if err != nil { return nil, ctxerr.Wrap(ctx, err, "get in android apps in scope for host") } diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index b0710f498f..bfcc64b9fb 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -2408,6 +2408,8 @@ type Datastore interface { // DeleteAndroidAppConfiguration removes an Android app configuration. DeleteAndroidAppConfiguration(ctx context.Context, adamID string, globalOrTeamID uint) error + ListMDMAndroidUUIDsToHostIDs(ctx context.Context, hostIDs []uint) (map[string]uint, error) + // ///////////////////////////////////////////////////////////////////////////// // SCIM diff --git a/server/mdm/android/android.go b/server/mdm/android/android.go index 55eb9f91c3..4b07161ed8 100644 --- a/server/mdm/android/android.go +++ b/server/mdm/android/android.go @@ -76,3 +76,5 @@ type MDMAndroidPolicyRequest struct { AppliedPolicyVersion sql.Null[int64] `db:"applied_policy_version"` PolicyVersion sql.Null[int64] `db:"policy_version"` } + +const AppStatusAvailable = "AVAILABLE" diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index a541790ffb..48556c1271 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -1567,6 +1567,8 @@ type UpdateAndroidAppConfigurationFunc func(ctx context.Context, config *fleet.A type DeleteAndroidAppConfigurationFunc func(ctx context.Context, adamID string, globalOrTeamID uint) error +type ListMDMAndroidUUIDsToHostIDsFunc func(ctx context.Context, hostIDs []uint) (map[string]uint, error) + type CreateScimUserFunc func(ctx context.Context, user *fleet.ScimUser) (uint, error) type ScimUserByIDFunc func(ctx context.Context, id uint) (*fleet.ScimUser, error) @@ -4014,6 +4016,9 @@ type DataStore struct { DeleteAndroidAppConfigurationFunc DeleteAndroidAppConfigurationFunc DeleteAndroidAppConfigurationFuncInvoked bool + ListMDMAndroidUUIDsToHostIDsFunc ListMDMAndroidUUIDsToHostIDsFunc + ListMDMAndroidUUIDsToHostIDsFuncInvoked bool + CreateScimUserFunc CreateScimUserFunc CreateScimUserFuncInvoked bool @@ -9616,6 +9621,13 @@ func (s *DataStore) DeleteAndroidAppConfiguration(ctx context.Context, adamID st return s.DeleteAndroidAppConfigurationFunc(ctx, adamID, globalOrTeamID) } +func (s *DataStore) ListMDMAndroidUUIDsToHostIDs(ctx context.Context, hostIDs []uint) (map[string]uint, error) { + s.mu.Lock() + s.ListMDMAndroidUUIDsToHostIDsFuncInvoked = true + s.mu.Unlock() + return s.ListMDMAndroidUUIDsToHostIDsFunc(ctx, hostIDs) +} + func (s *DataStore) CreateScimUser(ctx context.Context, user *fleet.ScimUser) (uint, error) { s.mu.Lock() s.CreateScimUserFuncInvoked = true diff --git a/server/service/hosts.go b/server/service/hosts.go index 498be96a77..8e34420fc0 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -1109,6 +1109,23 @@ func (svc *Service) AddHostsToTeam(ctx context.Context, teamID *uint, hostIDs [] } } + // If there are any Android hosts, update their available apps. + androidUUIDs, err := svc.ds.ListMDMAndroidUUIDsToHostIDs(ctx, hostIDs) + if err != nil { + return err + } + + if len(androidUUIDs) > 0 { + enterprise, err := svc.ds.GetEnterprise(ctx) + if err != nil { + return ctxerr.Wrap(ctx, err, "get android enterprise") + } + + if err := worker.QueueBulkSetAndroidAppsAvailableForHosts(ctx, svc.ds, svc.logger, androidUUIDs, enterprise.Name()); err != nil { + return ctxerr.Wrap(ctx, err, "queue bulk set available android apps for hosts job") + } + } + return svc.createTransferredHostsActivity(ctx, teamID, hostIDs, nil) } diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go index fbed195aa3..d4d6dbdbf1 100644 --- a/server/service/hosts_test.go +++ b/server/service/hosts_test.go @@ -847,6 +847,9 @@ func TestHostAuth(t *testing.T) { ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) { return false, nil } + ds.ListMDMAndroidUUIDsToHostIDsFunc = func(ctx context.Context, hostIDs []uint) (map[string]uint, error) { + return map[string]uint{}, nil + } testCases := []struct { name string diff --git a/server/service/integration_android_software_test.go b/server/service/integration_android_software_test.go index 4d9ffb9954..93e64a0560 100644 --- a/server/service/integration_android_software_test.go +++ b/server/service/integration_android_software_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "slices" "strings" "testing" "time" @@ -91,8 +92,25 @@ func (s *integrationMDMTestSuite) TestAndroidAppsSelfService() { ) s.Assert().Contains(extractServerErrorText(r.Body), "Couldn't add software. The application ID isn't available in Play Store. Please find ID on the Play Store and try again.") + amapiConfig := struct { + AppIDsToNames map[string]string + EnterprisesPoliciesPatchValidator func(policyName string, policy *androidmanagement.Policy, opts androidmgmt.PoliciesPatchOpts) + }{ + AppIDsToNames: map[string]string{}, + EnterprisesPoliciesPatchValidator: func(policyName string, policy *androidmanagement.Policy, opts androidmgmt.PoliciesPatchOpts) {}, + } + s.androidAPIClient.EnterprisesApplicationsFunc = func(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error) { - return &androidmanagement.Application{IconUrl: "https://example.com/1.jpg", Title: "Test App"}, nil + title := amapiConfig.AppIDsToNames[packageName] + + return &androidmanagement.Application{IconUrl: "https://example.com/1.jpg", Title: title}, nil + } + + s.androidAPIClient.EnterprisesPoliciesPatchFunc = func(ctx context.Context, policyName string, policy *androidmanagement.Policy, opts androidmgmt.PoliciesPatchOpts) (*androidmanagement.Policy, error) { + + amapiConfig.EnterprisesPoliciesPatchValidator(policyName, policy, opts) + + return &androidmanagement.Policy{}, nil } // Valid application ID format, but wrong platform specified: should fail @@ -193,6 +211,7 @@ func (s *integrationMDMTestSuite) TestAndroidAppsSelfService() { // Should have hit the android API endpoint s.Assert().True(s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked) + s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked = false s.DoJSON( "PATCH", @@ -211,7 +230,108 @@ func (s *integrationMDMTestSuite) TestAndroidAppsSelfService() { return nil }) - // Test Android app configurations + // Add some apps to a different team. They shouldn't be sent to our existing host + var newTeamResp teamResponse + s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1")}}, http.StatusOK, &newTeamResp) + team := newTeamResp.Team + // Add Android app + androidAppNewTeam := &fleet.VPPApp{ + VPPAppTeam: fleet.VPPAppTeam{ + VPPAppID: fleet.VPPAppID{ + AdamID: "com.my.cool.app", + Platform: fleet.AndroidPlatform, + }, + }, + Name: "My cool app", + BundleIdentifier: "com.my.cool.app", + IconURL: "https://example.com/images/3", + } + s.DoJSON( + "POST", + "/api/latest/fleet/software/app_store_apps", + &addAppStoreAppRequest{AppStoreID: androidAppNewTeam.AdamID, Platform: fleet.AndroidPlatform, TeamID: &team.ID}, + http.StatusOK, + &addAppResp, + ) + + // New app should not show up in "No team" library + s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitles, "team_id", fmt.Sprint(0)) + s.Assert().Len(listSWTitles.SoftwareTitles, 1) + s.Assert().Equal(androidApp.AdamID, listSWTitles.SoftwareTitles[0].AppStoreApp.AppStoreID) // just the app we had before + s.Assert().Empty(listSWTitles.SoftwareTitles[0].AppStoreApp.Version) + + // New app SHOULD show up in our new team library + s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitles, "team_id", fmt.Sprint(team.ID)) + s.Assert().Len(listSWTitles.SoftwareTitles, 1) + s.Assert().Equal(androidAppNewTeam.AdamID, listSWTitles.SoftwareTitles[0].AppStoreApp.AppStoreID) + s.Assert().Empty(listSWTitles.SoftwareTitles[0].AppStoreApp.Version) + + androidAppNewTeam2 := &fleet.VPPApp{ + VPPAppTeam: fleet.VPPAppTeam{ + VPPAppID: fleet.VPPAppID{ + AdamID: "com.my.cool.app.two", + Platform: fleet.AndroidPlatform, + }, + }, + Name: "My cool app 2", + BundleIdentifier: "com.my.cool.app.two", + IconURL: "https://example.com/images/4", + } + + amapiConfig.AppIDsToNames[androidAppNewTeam2.AdamID] = androidAppNewTeam2.Name + + s.DoJSON( + "POST", + "/api/latest/fleet/software/app_store_apps", + &addAppStoreAppRequest{AppStoreID: androidAppNewTeam2.AdamID, Platform: fleet.AndroidPlatform, TeamID: &team.ID}, + http.StatusOK, + &addAppResp, + ) + + s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitles, "team_id", fmt.Sprint(team.ID)) + s.Assert().Len(listSWTitles.SoftwareTitles, 2) + s.Assert().True(slices.ContainsFunc(listSWTitles.SoftwareTitles, func(t fleet.SoftwareTitleListResult) bool { + return t.AppStoreApp.AppStoreID == androidAppNewTeam.AdamID || t.AppStoreApp.AppStoreID == androidAppNewTeam2.AdamID + })) + + s.lastActivityMatches(fleet.ActivityAddedAppStoreApp{}.ActivityName(), + fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %s, "platform": "%s", "self_service": true}`, + team.Name, androidAppNewTeam2.Name, addAppResp.TitleID, androidAppNewTeam2.AdamID, fmt.Sprint(team.ID), androidAppNewTeam2.Platform), 0) + + s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked = false + s.runWorkerUntilDone() + // We shouldn't have hit the AMAPI, since there are no hosts in the team + s.Assert().False(s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked) + s.Assert().False(s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked) + + amapiConfig.EnterprisesPoliciesPatchValidator = func(policyName string, policy *androidmanagement.Policy, opts androidmgmt.PoliciesPatchOpts) { + var appIDs []string + for _, a := range policy.Applications { + appIDs = append(appIDs, a.PackageName) + } + + s.Assert().ElementsMatch(appIDs, []string{androidAppNewTeam.AdamID, androidAppNewTeam2.AdamID}) + s.Assert().Contains(policyName, host1.UUID) + } + + // Transfer a host to the team + s.DoJSON("POST", "/api/latest/fleet/hosts/transfer", addHostsToTeamRequest{ + TeamID: &team.ID, + HostIDs: []uint{host1.ID}, + }, http.StatusOK, &addHostsToTeamResponse{}) + + s.runWorkerUntilDone() + s.Assert().True(s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked) + + // Transfer host back to "No team" + s.DoJSON("POST", "/api/latest/fleet/hosts/transfer", addHostsToTeamRequest{ + TeamID: nil, + HostIDs: []uint{host1.ID}, + }, http.StatusOK, &addHostsToTeamResponse{}) + + // ========================================= + // Android app configurations + // ========================================= // Title with no configuration should omit it from response var getAppResp map[string]any @@ -236,6 +356,8 @@ func (s *integrationMDMTestSuite) TestAndroidAppsSelfService() { IconURL: "https://example.com/images/2", } + amapiConfig.AppIDsToNames[androidAppWithConfig.AdamID] = androidAppWithConfig.Name + // Add Android app var appWithConfigResp addAppStoreAppResponse s.DoJSON( @@ -253,7 +375,7 @@ func (s *integrationMDMTestSuite) TestAndroidAppsSelfService() { // Verify that activity includes configuration s.lastActivityMatches(fleet.ActivityAddedAppStoreApp{}.ActivityName(), fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %s, "platform": "%s", "self_service": true,"configuration": %s}`, - "", "Test App", appWithConfigResp.TitleID, androidAppWithConfig.AdamID, "null", androidAppWithConfig.Platform, androidAppWithConfig.Configuration), 0) + "", androidAppWithConfig.Name, appWithConfigResp.TitleID, androidAppWithConfig.AdamID, "null", androidAppWithConfig.Platform, androidAppWithConfig.Configuration), 0) // Should see it in host software library s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host1.ID), nil, http.StatusOK, &getHostSw, "available_for_install", "true") @@ -294,7 +416,7 @@ func (s *integrationMDMTestSuite) TestAndroidAppsSelfService() { // Verify that configuration changed and last activity is correct s.lastActivityMatches(fleet.ActivityEditedAppStoreApp{}.ActivityName(), fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_icon_url":"https://example.com/1.jpg", "software_title_id": %d, "app_store_id": "%s", "team_id": %s, "software_display_name":"", "platform": "%s", "self_service": true,"configuration": %s}`, - "", "Test App", appWithConfigResp.TitleID, androidAppWithConfig.AdamID, "null", androidAppWithConfig.Platform, newConfig), 0) + "", androidAppWithConfig.Name, appWithConfigResp.TitleID, androidAppWithConfig.AdamID, "null", androidAppWithConfig.Platform, newConfig), 0) } func (s *integrationMDMTestSuite) TestAndroidSetupExperienceSoftware() { diff --git a/server/worker/software_worker.go b/server/worker/software_worker.go index 91dfddfe85..42e6afdb62 100644 --- a/server/worker/software_worker.go +++ b/server/worker/software_worker.go @@ -31,10 +31,11 @@ func (v *SoftwareWorker) Name() string { } const ( - makeAndroidAppsAvailableForHostTask SoftwareWorkerTask = "make_android_apps_available_for_host" - makeAndroidAppAvailableTask SoftwareWorkerTask = "make_android_app_available" - runAndroidSetupExperienceTask SoftwareWorkerTask = "run_android_setup_experience" - bulkSetAndroidAppsAvailableForHostTask SoftwareWorkerTask = "bulk_set_android_apps_available_for_host" + makeAndroidAppsAvailableForHostTask SoftwareWorkerTask = "make_android_apps_available_for_host" + makeAndroidAppAvailableTask SoftwareWorkerTask = "make_android_app_available" + runAndroidSetupExperienceTask SoftwareWorkerTask = "run_android_setup_experience" + bulkSetAndroidAppsAvailableForHostTask SoftwareWorkerTask = "bulk_set_android_apps_available_for_host" + bulkSetAndroidAppsAvailableForHostsTask SoftwareWorkerTask = "bulk_set_android_apps_available_for_hosts" ) type softwareWorkerArgs struct { @@ -59,7 +60,8 @@ type softwareWorkerArgs struct { // AppConfigChanged indicates if the android app configuration changed as part // of the action that triggered this task. - AppConfigChanged bool `json:"app_config_changed,omitempty"` + AppConfigChanged bool `json:"app_config_changed,omitempty"` + UUIDsToIDs map[string]uint `json:"uuids_to_ids,omitempty"` } func (v *SoftwareWorker) Run(ctx context.Context, argsJSON json.RawMessage) error { @@ -105,8 +107,16 @@ func (v *SoftwareWorker) Run(ctx context.Context, argsJSON json.RawMessage) erro ), "running %s task", bulkSetAndroidAppsAvailableForHostTask) + case bulkSetAndroidAppsAvailableForHostsTask: + return ctxerr.Wrapf(ctx, v.bulkSetAndroidAppsAvailableForHosts( + ctx, + args.UUIDsToIDs, + args.EnterpriseName, + ), "running %s task", bulkSetAndroidAppsAvailableForHostsTask) + default: return ctxerr.Errorf(ctx, "unknown task: %v", args.Task) + } } @@ -456,3 +466,61 @@ func QueueBulkSetAndroidAppsAvailableForHost( level.Debug(logger).Log("job_id", job.ID, "job_name", softwareWorkerJobName, "task", args.Task) return nil } + +func (v *SoftwareWorker) bulkSetAndroidAppsAvailableForHosts(ctx context.Context, uuidsToIDs map[string]uint, enterpriseName string) error { + // for each host + // get the set of self-service apps that are in scope for it + for uuid, hostID := range uuidsToIDs { + androidHost, err := v.Datastore.AndroidHostLiteByHostUUID(ctx, uuid) + if err != nil { + return ctxerr.Wrapf(ctx, err, "get android host by host UUID %s", uuid) + } + + appIDs, err := v.Datastore.GetAndroidAppsInScopeForHost(ctx, hostID) + if err != nil { + return ctxerr.WrapWithData(ctx, err, "get android apps in scope for host", map[string]any{"host_id": hostID}) + } + + configsByAppID, err := v.Datastore.BulkGetAndroidAppConfigurations(ctx, appIDs, ptr.ValOrZero(androidHost.TeamID)) + if err != nil { + return ctxerr.Wrap(ctx, err, "bulk get android app configurations") + } + + appPolicies, err := buildApplicationPolicyWithConfig(ctx, appIDs, configsByAppID, "AVAILABLE") + if err != nil { + return ctxerr.Wrap(ctx, err, "building application policies with config") + } + + err = v.AndroidModule.SetAppsForAndroidPolicy(ctx, enterpriseName, appPolicies, map[string]string{uuid: uuid}) + + if err != nil { + return ctxerr.WrapWithData(ctx, err, "set apps for android policy", map[string]any{"host_id": hostID}) + } + + } + + return nil + +} + +func QueueBulkSetAndroidAppsAvailableForHosts( + ctx context.Context, + ds fleet.Datastore, + logger kitlog.Logger, + uuidsToIDs map[string]uint, + enterpriseName string) error { + + args := &softwareWorkerArgs{ + Task: bulkSetAndroidAppsAvailableForHostsTask, + UUIDsToIDs: uuidsToIDs, + EnterpriseName: enterpriseName, + } + + job, err := QueueJob(ctx, ds, softwareWorkerJobName, args) + if err != nil { + return ctxerr.Wrap(ctx, err, "queueing job") + } + + level.Debug(logger).Log("job_id", job.ID, "job_name", softwareWorkerJobName, "task", args.Task) + return nil +}