diff --git a/cmd/incusd/api_internal_recover.go b/cmd/incusd/api_internal_recover.go index 936fcc8953e..e9d1473ca70 100644 --- a/cmd/incusd/api_internal_recover.go +++ b/cmd/incusd/api_internal_recover.go @@ -80,6 +80,11 @@ func internalRecoverScan(ctx context.Context, s *state.State, userPools []api.St return err } + profileDevices, err := dbCluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return err + } + // Convert to map for lookups by project name later. projectProfiles = make(map[string][]*api.Profile) for _, profile := range profiles { @@ -87,7 +92,7 @@ func internalRecoverScan(ctx context.Context, s *state.State, userPools []api.St projectProfiles[profile.Project] = []*api.Profile{} } - apiProfile, err := profile.ToAPI(ctx, tx.Tx()) + apiProfile, err := profile.ToAPI(ctx, tx.Tx(), profileDevices) if err != nil { return err } diff --git a/cmd/incusd/instance_patch.go b/cmd/incusd/instance_patch.go index d47a20b71ac..de48b44b727 100644 --- a/cmd/incusd/instance_patch.go +++ b/cmd/incusd/instance_patch.go @@ -192,8 +192,13 @@ func instancePatch(d *Daemon, r *http.Request) response.Response { return err } + profileDevices, err := cluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return err + } + for _, profile := range profiles { - apiProfile, err := profile.ToAPI(ctx, tx.Tx()) + apiProfile, err := profile.ToAPI(ctx, tx.Tx(), profileDevices) if err != nil { return err } diff --git a/cmd/incusd/instance_post.go b/cmd/incusd/instance_post.go index ef1627cdca9..2932515bbb2 100644 --- a/cmd/incusd/instance_post.go +++ b/cmd/incusd/instance_post.go @@ -622,8 +622,13 @@ func migrateInstance(ctx context.Context, s *state.State, inst instance.Instance return err } + profileDevices, err := dbCluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return err + } + for _, profile := range rawProfiles { - apiProfile, err := profile.ToAPI(ctx, tx.Tx()) + apiProfile, err := profile.ToAPI(ctx, tx.Tx(), profileDevices) if err != nil { return err } diff --git a/cmd/incusd/instance_put.go b/cmd/incusd/instance_put.go index 0bb22c8e57f..3ef106807e0 100644 --- a/cmd/incusd/instance_put.go +++ b/cmd/incusd/instance_put.go @@ -138,8 +138,13 @@ func instancePut(d *Daemon, r *http.Request) response.Response { return err } + profileDevices, err := cluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return err + } + for _, profile := range profiles { - apiProfile, err := profile.ToAPI(ctx, tx.Tx()) + apiProfile, err := profile.ToAPI(ctx, tx.Tx(), profileDevices) if err != nil { return err } diff --git a/cmd/incusd/instances_get.go b/cmd/incusd/instances_get.go index 341607e73d1..bd8dceea30e 100644 --- a/cmd/incusd/instances_get.go +++ b/cmd/incusd/instances_get.go @@ -15,15 +15,12 @@ import ( "github.com/lxc/incus/v6/internal/server/cluster" "github.com/lxc/incus/v6/internal/server/db" dbCluster "github.com/lxc/incus/v6/internal/server/db/cluster" - "github.com/lxc/incus/v6/internal/server/db/query" "github.com/lxc/incus/v6/internal/server/instance" "github.com/lxc/incus/v6/internal/server/instance/instancetype" "github.com/lxc/incus/v6/internal/server/request" "github.com/lxc/incus/v6/internal/server/response" - "github.com/lxc/incus/v6/internal/server/state" "github.com/lxc/incus/v6/internal/version" "github.com/lxc/incus/v6/shared/api" - "github.com/lxc/incus/v6/shared/logger" localtls "github.com/lxc/incus/v6/shared/tls" "github.com/lxc/incus/v6/shared/util" ) @@ -215,33 +212,12 @@ func urlInstanceTypeDetect(r *http.Request) (instancetype.Type, error) { func instancesGet(d *Daemon, r *http.Request) response.Response { s := d.State() - for i := 0; i < 100; i++ { - result, err := doInstancesGet(s, r) - if err == nil { - return response.SyncResponse(true, result) - } - - if !query.IsRetriableError(err) { - logger.Debugf("DBERR: containersGet: error %q", err) - return response.SmartError(err) - } - // 100 ms may seem drastic, but we really don't want to thrash - // perhaps we should use a random amount - time.Sleep(100 * time.Millisecond) - } - - logger.Debugf("DBERR: containersGet, db is locked") - logger.Debugf(logger.GetStack()) - return response.InternalError(fmt.Errorf("DB is locked")) -} - -func doInstancesGet(s *state.State, r *http.Request) (any, error) { resultFullList := []*api.InstanceFull{} resultMu := sync.Mutex{} instanceType, err := urlInstanceTypeDetect(r) if err != nil { - return nil, err + return response.BadRequest(err) } // Parse the recursion field. @@ -254,7 +230,7 @@ func doInstancesGet(s *state.State, r *http.Request) (any, error) { filterStr := r.FormValue("filter") clauses, err := filter.Parse(filterStr, filter.QueryOperatorSet()) if err != nil { - return nil, fmt.Errorf("Invalid filter: %w", err) + return response.BadRequest(fmt.Errorf("Invalid filter: %w", err)) } mustLoadObjects := recursion > 0 || (recursion == 0 && clauses != nil && len(clauses.Clauses) > 0) @@ -264,7 +240,7 @@ func doInstancesGet(s *state.State, r *http.Request) (any, error) { allProjects := util.IsTrue(r.FormValue("all-projects")) if allProjects && projectName != "" { - return nil, api.StatusErrorf(http.StatusBadRequest, "Cannot specify a project when requesting all projects") + return response.BadRequest(fmt.Errorf("Cannot specify a project when requesting all projects")) } else if !allProjects && projectName == "" { projectName = api.ProjectDefaultName } @@ -297,12 +273,12 @@ func doInstancesGet(s *state.State, r *http.Request) (any, error) { return nil }) if err != nil { - return nil, err + return response.SmartError(err) } userHasPermission, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeInstance) if err != nil { - return nil, err + return response.InternalError(err) } // Removes instances the user doesn't have access to. @@ -429,7 +405,7 @@ func doInstancesGet(s *state.State, r *http.Request) (any, error) { for _, projectName := range filteredProjects { insts, err := instanceLoadNodeProjectAll(r.Context(), s, projectName, instanceType) if err != nil { - return nil, fmt.Errorf("Failed loading instances for project %q: %w", projectName, err) + return response.InternalError(fmt.Errorf("Failed loading instances for project %q: %w", projectName, err)) } for _, inst := range insts { @@ -499,7 +475,7 @@ func doInstancesGet(s *state.State, r *http.Request) (any, error) { if clauses != nil && len(clauses.Clauses) > 0 { resultFullList, err = instance.FilterFull(resultFullList, *clauses) if err != nil { - return nil, err + return response.SmartError(err) } } @@ -510,7 +486,7 @@ func doInstancesGet(s *state.State, r *http.Request) (any, error) { resultList = append(resultList, url.String()) } - return resultList, nil + return response.SyncResponse(true, resultList) } if recursion == 1 { @@ -519,10 +495,10 @@ func doInstancesGet(s *state.State, r *http.Request) (any, error) { resultList = append(resultList, &resultFullList[i].Instance) } - return resultList, nil + return response.SyncResponse(true, resultList) } - return resultFullList, nil + return response.SyncResponse(true, resultFullList) } // Fetch information about the containers on the given remote node, using the diff --git a/cmd/incusd/instances_post.go b/cmd/incusd/instances_post.go index ac108cea5a8..c41bfdfd839 100644 --- a/cmd/incusd/instances_post.go +++ b/cmd/incusd/instances_post.go @@ -991,6 +991,11 @@ func instancesPost(d *Daemon, r *http.Request) response.Response { return err } + dbProfileDevices, err := dbCluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return err + } + profilesByName := make(map[string]dbCluster.Profile, len(dbProfiles)) for _, dbProfile := range dbProfiles { profilesByName[dbProfile.Name] = dbProfile @@ -1002,7 +1007,7 @@ func instancesPost(d *Daemon, r *http.Request) response.Response { return fmt.Errorf("Requested profile %q doesn't exist", profileName) } - apiProfile, err := profile.ToAPI(ctx, tx.Tx()) + apiProfile, err := profile.ToAPI(ctx, tx.Tx(), dbProfileDevices) if err != nil { return err } diff --git a/cmd/incusd/profiles.go b/cmd/incusd/profiles.go index 6cae94b2f09..1a430b0b48a 100644 --- a/cmd/incusd/profiles.go +++ b/cmd/incusd/profiles.go @@ -189,32 +189,42 @@ func profilesGet(d *Daemon, r *http.Request) response.Response { } } - apiProfiles := make([]*api.Profile, 0, len(profiles)) - for _, profile := range profiles { - if !userHasPermission(auth.ObjectProfile(p.Name, profile.Name)) { - continue - } - - apiProfile, err := profile.ToAPI(ctx, tx.Tx()) + if recursion { + profileDevices, err := dbCluster.GetDevices(ctx, tx.Tx(), "profile") if err != nil { return err } - apiProfile.UsedBy, err = profileUsedBy(ctx, tx, profile) - if err != nil { - return err - } + apiProfiles := make([]*api.Profile, 0, len(profiles)) + for _, profile := range profiles { + if !userHasPermission(auth.ObjectProfile(p.Name, profile.Name)) { + continue + } - apiProfile.UsedBy = project.FilterUsedBy(s.Authorizer, r, apiProfile.UsedBy) - apiProfiles = append(apiProfiles, apiProfile) - } + apiProfile, err := profile.ToAPI(ctx, tx.Tx(), profileDevices) + if err != nil { + return err + } + + apiProfile.UsedBy, err = profileUsedBy(ctx, tx, profile) + if err != nil { + return err + } + + apiProfile.UsedBy = project.FilterUsedBy(s.Authorizer, r, apiProfile.UsedBy) + apiProfiles = append(apiProfiles, apiProfile) + } - if recursion { result = apiProfiles } else { - urls := make([]string, len(apiProfiles)) - for i, apiProfile := range apiProfiles { - urls[i] = apiProfile.URL(version.APIVersion, apiProfile.Project).String() + urls := make([]string, 0, len(profiles)) + for _, profile := range profiles { + if !userHasPermission(auth.ObjectProfile(p.Name, profile.Name)) { + continue + } + + apiProfile := api.Profile{Name: profile.Name} + urls = append(urls, apiProfile.URL(version.APIVersion, profile.Project).String()) } result = urls @@ -427,7 +437,12 @@ func profileGet(d *Daemon, r *http.Request) response.Response { return fmt.Errorf("Fetch profile: %w", err) } - resp, err = profile.ToAPI(ctx, tx.Tx()) + profileDevices, err := dbCluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return err + } + + resp, err = profile.ToAPI(ctx, tx.Tx(), profileDevices) if err != nil { return err } @@ -518,7 +533,7 @@ func profilePut(d *Daemon, r *http.Request) response.Response { return fmt.Errorf("Failed to retrieve profile %q: %w", name, err) } - profile, err = current.ToAPI(ctx, tx.Tx()) + profile, err = current.ToAPI(ctx, tx.Tx(), nil) if err != nil { return err } @@ -623,7 +638,7 @@ func profilePatch(d *Daemon, r *http.Request) response.Response { return fmt.Errorf("Failed to retrieve profile=%q: %w", name, err) } - profile, err = current.ToAPI(ctx, tx.Tx()) + profile, err = current.ToAPI(ctx, tx.Tx(), nil) if err != nil { return err } diff --git a/internal/server/backup/backup_config_utils.go b/internal/server/backup/backup_config_utils.go index 28eca35b224..0404e49b2ba 100644 --- a/internal/server/backup/backup_config_utils.go +++ b/internal/server/backup/backup_config_utils.go @@ -51,8 +51,14 @@ func ConfigToInstanceDBArgs(state *state.State, c *config.Config, projectName st return err } + // Get all the profile devices. + profileDevices, err := cluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return err + } + for _, profile := range profiles { - apiProfile, err := profile.ToAPI(ctx, tx.Tx()) + apiProfile, err := profile.ToAPI(ctx, tx.Tx(), profileDevices) if err != nil { return err } diff --git a/internal/server/db/cluster/instances.go b/internal/server/db/cluster/instances.go index e828009b8d6..98e3088be83 100644 --- a/internal/server/db/cluster/instances.go +++ b/internal/server/db/cluster/instances.go @@ -77,16 +77,23 @@ type InstanceFilter struct { } // ToAPI converts the database Instance to API type. -func (i *Instance) ToAPI(ctx context.Context, tx *sql.Tx) (*api.Instance, error) { +func (i *Instance) ToAPI(ctx context.Context, tx *sql.Tx, instanceDevices map[int][]Device, profileDevices map[int][]Device) (*api.Instance, error) { profiles, err := GetInstanceProfiles(ctx, tx, i.ID) if err != nil { return nil, err } + if profileDevices == nil { + profileDevices, err = GetDevices(ctx, tx, "profile") + if err != nil { + return nil, err + } + } + apiProfiles := make([]api.Profile, 0, len(profiles)) profileNames := make([]string, 0, len(profiles)) for _, p := range profiles { - apiProfile, err := p.ToAPI(ctx, tx) + apiProfile, err := p.ToAPI(ctx, tx, profileDevices) if err != nil { return nil, err } @@ -95,9 +102,18 @@ func (i *Instance) ToAPI(ctx context.Context, tx *sql.Tx) (*api.Instance, error) profileNames = append(profileNames, p.Name) } - devices, err := GetInstanceDevices(ctx, tx, i.ID) - if err != nil { - return nil, err + var devices map[string]Device + if instanceDevices != nil { + devices = map[string]Device{} + + for _, dev := range instanceDevices[i.ID] { + devices[dev.Name] = dev + } + } else { + devices, err = GetInstanceDevices(ctx, tx, i.ID) + if err != nil { + return nil, err + } } apiDevices := DevicesToAPI(devices) diff --git a/internal/server/db/cluster/profiles.go b/internal/server/db/cluster/profiles.go index 11ab73cdaee..dc59883b49b 100644 --- a/internal/server/db/cluster/profiles.go +++ b/internal/server/db/cluster/profiles.go @@ -52,15 +52,24 @@ type ProfileFilter struct { } // ToAPI returns a cluster Profile as an API struct. -func (p *Profile) ToAPI(ctx context.Context, tx *sql.Tx) (*api.Profile, error) { +func (p *Profile) ToAPI(ctx context.Context, tx *sql.Tx, profileDevices map[int][]Device) (*api.Profile, error) { config, err := GetProfileConfig(ctx, tx, p.ID) if err != nil { return nil, err } - devices, err := GetProfileDevices(ctx, tx, p.ID) - if err != nil { - return nil, err + var devices map[string]Device + if profileDevices != nil { + devices = map[string]Device{} + + for _, dev := range profileDevices[p.ID] { + devices[dev.Name] = dev + } + } else { + devices, err = GetProfileDevices(ctx, tx, p.ID) + if err != nil { + return nil, err + } } profile := &api.Profile{ diff --git a/internal/server/db/instances.go b/internal/server/db/instances.go index 675ff0fda79..cad70562540 100644 --- a/internal/server/db/instances.go +++ b/internal/server/db/instances.go @@ -534,6 +534,12 @@ func (c *ClusterTx) instanceProfilesFill(ctx context.Context, snapshotsMode bool return fmt.Errorf("Failed loading profiles: %w", err) } + // Get all the profile devices. + profileDevices, err := cluster.GetDevices(context.TODO(), c.Tx(), "profile") + if err != nil { + return fmt.Errorf("Failed loading profile devices: %w", err) + } + // Populate profilesByID map entry for referenced profiles. // This way we only call ToAPI() on the profiles actually referenced by the instances in // the list, which can reduce the number of queries run. @@ -543,7 +549,7 @@ func (c *ClusterTx) instanceProfilesFill(ctx context.Context, snapshotsMode bool continue } - profilesByID[profile.ID], err = profile.ToAPI(context.TODO(), c.tx) + profilesByID[profile.ID], err = profile.ToAPI(context.TODO(), c.tx, profileDevices) if err != nil { return err } diff --git a/internal/server/db/profiles.go b/internal/server/db/profiles.go index 7d3078c401d..fde0fc4e8a5 100644 --- a/internal/server/db/profiles.go +++ b/internal/server/db/profiles.go @@ -61,7 +61,7 @@ func (c *ClusterTx) GetProfile(ctx context.Context, project, name string) (int64 profile := profiles[0] id := int64(profile.ID) - result, err := profile.ToAPI(ctx, c.tx) + result, err := profile.ToAPI(ctx, c.tx, nil) if err != nil { return -1, nil, err } @@ -78,8 +78,14 @@ func (c *ClusterTx) GetProfiles(ctx context.Context, projectName string, profile return nil, err } + // Get all the profile devices. + profileDevices, err := cluster.GetDevices(ctx, c.Tx(), "profile") + if err != nil { + return nil, err + } + for i, profile := range dbProfiles { - apiProfile, err := profile.ToAPI(ctx, c.tx) + apiProfile, err := profile.ToAPI(ctx, c.tx, profileDevices) if err != nil { return nil, err } diff --git a/internal/server/db/query/transaction.go b/internal/server/db/query/transaction.go index e9d5dd3b931..b73e51a2c3f 100644 --- a/internal/server/db/query/transaction.go +++ b/internal/server/db/query/transaction.go @@ -10,9 +10,9 @@ import ( "github.com/lxc/incus/v6/shared/logger" ) -// Transaction executes the given function within a database transaction with a 10s context timeout. +// Transaction executes the given function within a database transaction with a 30s context timeout. func Transaction(ctx context.Context, db *sql.DB, f func(context.Context, *sql.Tx) error) error { - ctx, cancel := context.WithTimeout(ctx, time.Second*10) + ctx, cancel := context.WithTimeout(ctx, time.Second*30) defer cancel() tx, err := db.BeginTx(ctx, nil) diff --git a/internal/server/network/acl/acl_load.go b/internal/server/network/acl/acl_load.go index 05029734801..4cd04845c04 100644 --- a/internal/server/network/acl/acl_load.go +++ b/internal/server/network/acl/acl_load.go @@ -145,11 +145,19 @@ func UsedBy(s *state.State, aclProjectName string, usageFunc func(ctx context.Co return err } + // Get all the profile devices. + profileDevicesByID, err := cluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return err + } + for _, profile := range profiles { - profileDevices[profile.Name], err = cluster.GetProfileDevices(ctx, tx.Tx(), profile.ID) - if err != nil { - return err + devices := map[string]cluster.Device{} + for _, dev := range profileDevicesByID[profile.ID] { + devices[dev.Name] = dev } + + profileDevices[profile.Name] = devices } return nil diff --git a/internal/server/network/network_utils.go b/internal/server/network/network_utils.go index 3b24fb7c5d0..58d7a6af00d 100644 --- a/internal/server/network/network_utils.go +++ b/internal/server/network/network_utils.go @@ -188,17 +188,19 @@ func UsedBy(s *state.State, networkProjectName string, networkID int64, networkN // Look for profiles. Next cheapest to do. err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { + // Get all profiles profiles, err := cluster.GetProfiles(ctx, tx.Tx()) if err != nil { return err } - for _, profile := range profiles { - profileDevices, err := cluster.GetProfileDevices(ctx, tx.Tx(), profile.ID) - if err != nil { - return err - } + // Get all the profile devices. + profileDevices, err := cluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return err + } + for _, profile := range profiles { profileProject, err := cluster.GetProject(ctx, tx.Tx(), profile.Project) if err != nil { return err @@ -209,7 +211,12 @@ func UsedBy(s *state.State, networkProjectName string, networkID int64, networkN return err } - inUse, err := usedByProfileDevices(s, profileDevices, apiProfileProject, networkProjectName, networkName, networkType) + devices := map[string]cluster.Device{} + for _, dev := range profileDevices[profile.ID] { + devices[dev.Name] = dev + } + + inUse, err := usedByProfileDevices(s, devices, apiProfileProject, networkProjectName, networkName, networkType) if err != nil { return err } diff --git a/internal/server/project/permissions.go b/internal/server/project/permissions.go index 1d36fc28fce..80ecd0426fc 100644 --- a/internal/server/project/permissions.go +++ b/internal/server/project/permissions.go @@ -1256,9 +1256,14 @@ func fetchProject(tx *db.ClusterTx, projectName string, skipIfNoLimits bool) (*p return nil, fmt.Errorf("Fetch profiles from database: %w", err) } + dbProfileDevices, err := cluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return nil, fmt.Errorf("Fetch profile devices from database: %w", err) + } + profiles := make([]api.Profile, 0, len(dbProfiles)) for _, profile := range dbProfiles { - apiProfile, err := profile.ToAPI(ctx, tx.Tx()) + apiProfile, err := profile.ToAPI(ctx, tx.Tx(), dbProfileDevices) if err != nil { return nil, err } @@ -1271,9 +1276,14 @@ func fetchProject(tx *db.ClusterTx, projectName string, skipIfNoLimits bool) (*p return nil, fmt.Errorf("Fetch project instances from database: %w", err) } + dbInstanceDevices, err := cluster.GetDevices(ctx, tx.Tx(), "instance") + if err != nil { + return nil, fmt.Errorf("Fetch instance devices from database: %w", err) + } + instances := make([]api.Instance, 0, len(dbInstances)) for _, instance := range dbInstances { - apiInstance, err := instance.ToAPI(ctx, tx.Tx()) + apiInstance, err := instance.ToAPI(ctx, tx.Tx(), dbInstanceDevices, dbProfileDevices) if err != nil { return nil, fmt.Errorf("Failed to get API data for instance %q in project %q: %w", instance.Name, instance.Project, err) } diff --git a/internal/server/scriptlet/instance_placement.go b/internal/server/scriptlet/instance_placement.go index b00faf60b9b..213468e5b46 100644 --- a/internal/server/scriptlet/instance_placement.go +++ b/internal/server/scriptlet/instance_placement.go @@ -282,9 +282,14 @@ func InstancePlacementRun(ctx context.Context, l logger.Logger, s *state.State, } } + objectDevices, err := dbCluster.GetDevices(ctx, tx.Tx(), "instance") + if err != nil { + return err + } + // Convert the []Instances into []api.Instances. for _, obj := range objects { - instance, err := obj.ToAPI(ctx, tx.Tx()) + instance, err := obj.ToAPI(ctx, tx.Tx(), objectDevices, nil) if err != nil { return err } diff --git a/internal/server/storage/storage.go b/internal/server/storage/storage.go index 0e2ad61ea6f..74402af0e56 100644 --- a/internal/server/storage/storage.go +++ b/internal/server/storage/storage.go @@ -211,13 +211,14 @@ func UsedBy(ctx context.Context, s *state.State, pool Pool, firstOnly bool, memb return fmt.Errorf("Failed loading profiles: %w", err) } - for _, profile := range profiles { - profileDevices, err := cluster.GetProfileDevices(ctx, tx.Tx(), profile.ID) - if err != nil { - return fmt.Errorf("Failed loading profile devices: %w", err) - } + // Get all the profile devices. + profileDevices, err := cluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return fmt.Errorf("Failed loading profile devices: %w", err) + } - for _, device := range profileDevices { + for _, profile := range profiles { + for _, device := range profileDevices[profile.ID] { if device.Type != cluster.TypeDisk { continue } diff --git a/internal/server/storage/utils.go b/internal/server/storage/utils.go index 5dea055a11a..d6b4254c599 100644 --- a/internal/server/storage/utils.go +++ b/internal/server/storage/utils.go @@ -819,8 +819,14 @@ func VolumeUsedByProfileDevices(s *state.State, poolName string, projectName str return fmt.Errorf("Failed loading profiles: %w", err) } + // Get all the profile devices. + profileDevices, err := cluster.GetDevices(ctx, tx.Tx(), "profile") + if err != nil { + return fmt.Errorf("Failed loading profiles: %w", err) + } + for _, profile := range dbProfiles { - apiProfile, err := profile.ToAPI(ctx, tx.Tx()) + apiProfile, err := profile.ToAPI(ctx, tx.Tx(), profileDevices) if err != nil { return fmt.Errorf("Failed getting API Profile %q: %w", profile.Name, err) }