This is an automated email from the ASF dual-hosted git repository. DImuthuUpe pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit 13ef537fd8b5a7438fc654f4ddd2084aac944b70 Author: DImuthuUpe <[email protected]> AuthorDate: Tue May 19 21:12:58 2026 -0400 Updating account resource limits when the allocation resource mapping is created --- .../internal/subscribers/account.go | 60 ++++++++++++++++++++++ .../internal/subscribers/members.go | 4 +- .../internal/subscribers/subscriber.go | 1 + docs/API-Docs.md | 56 +++++++++++++++++--- ...te_allocation_resource_mapping_amounts.down.sql | 20 ++++++++ ...pute_allocation_resource_mapping_amounts.up.sql | 20 ++++++++ internal/server/server.go | 24 ++++++++- .../compute_allocation_resource_mapping_store.go | 33 ++++++++++-- internal/store/store.go | 4 ++ pkg/models/allocation.go | 10 ++-- pkg/service/compute_allocation_resource_mapping.go | 50 ++++++++++++++++-- 11 files changed, 262 insertions(+), 20 deletions(-) diff --git a/connectors/SLURM/Association-Mapper/internal/subscribers/account.go b/connectors/SLURM/Association-Mapper/internal/subscribers/account.go index 2b872d54c..70a0f49e6 100644 --- a/connectors/SLURM/Association-Mapper/internal/subscribers/account.go +++ b/connectors/SLURM/Association-Mapper/internal/subscribers/account.go @@ -59,3 +59,63 @@ func (a *AssociationSubscriber) SubscribeToComputeAllocationDeletion(computeAcco func (a *AssociationSubscriber) SubscribeToComputeAllocationUpdate(computeAccount models.ComputeAllocation) { slog.Info("Received compute allocation update event", "account", computeAccount) } + +func (a *AssociationSubscriber) SubscribeToComputeAllocationResourceMappingCreation(mapping models.ComputeAllocationResourceMapping) { + slog.Info("Received compute allocation resource mapping creation event", "mapping", mapping) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + allocation, err := a.coreService.GetComputeAllocation(ctx, mapping.ComputeAllocationID) + if err != nil { + slog.Error("Failed to get compute allocation for resource mapping creation", "error", err) + return + } + + cluster, err := a.coreService.GetComputeCluster(ctx, allocation.ComputeClusterID) + if err != nil { + slog.Error("Failed to get compute cluster for resource mapping creation", "error", err) + return + } + + resource, err := a.coreService.GetComputeAllocationResource(ctx, mapping.ComputeAllocationResourceID) + if err != nil { + slog.Error("Failed to get compute allocation resource for resource mapping creation", "error", err) + return + } + + grpTres := []client.TRES{} + + if mapping.ResourceAmount > 0 { + grpTres = append(grpTres, client.TRES{ + Type: resource.ResourceType, + Count: mapping.ResourceAmount, + }) + } + + grpTresMins := []client.TRES{} + if mapping.ResourceTime > 0 { + grpTresMins = append(grpTresMins, client.TRES{ + Type: resource.ResourceType, + Count: mapping.ResourceTime, + }) + } + + limits := client.AssocLimits{ + GrpTRES: grpTres, + GrpTRESMins: grpTresMins, + } + + association := client.Association{ + Account: allocation.Name, + Cluster: cluster.Name, + Limits: limits, + } + + err = a.slurmClient.UpsertAssociation(association) + if err != nil { + slog.Error("Failed to upsert association for membership resource override creation", "error", err) + } else { + slog.Info("Successfully upserted association for membership resource override creation", "association", association) + } +} diff --git a/connectors/SLURM/Association-Mapper/internal/subscribers/members.go b/connectors/SLURM/Association-Mapper/internal/subscribers/members.go index 687ecd8d5..dfcc4ee70 100644 --- a/connectors/SLURM/Association-Mapper/internal/subscribers/members.go +++ b/connectors/SLURM/Association-Mapper/internal/subscribers/members.go @@ -118,7 +118,7 @@ func (a *AssociationSubscriber) SubscribeToComputeAllocationMembershipResourceOv if allocationResource.ResourceType == "GrpTRES" { grpTres = append(grpTres, client.TRES{ - Type: allocationResource.Name, + Type: allocationResource.ResourceType, Count: override.OverriddenResourceAmount, // override.OverriddenResourceAmount is the SU amount, but SLURM needs the actual resource amount (e.g., number of CPU hours), so we need to convert it using the rate for the resource }) } @@ -127,7 +127,7 @@ func (a *AssociationSubscriber) SubscribeToComputeAllocationMembershipResourceOv if allocationResource.ResourceType == "GrpTRESMins" { grpTresMins = append(grpTresMins, client.TRES{ - Type: allocationResource.Name, + Type: allocationResource.ResourceType, Count: override.OverriddenResourceAmount, }) } diff --git a/connectors/SLURM/Association-Mapper/internal/subscribers/subscriber.go b/connectors/SLURM/Association-Mapper/internal/subscribers/subscriber.go index 17b65cce8..308928a4b 100644 --- a/connectors/SLURM/Association-Mapper/internal/subscribers/subscriber.go +++ b/connectors/SLURM/Association-Mapper/internal/subscribers/subscriber.go @@ -24,4 +24,5 @@ func (a *AssociationSubscriber) RegisterSubscribers() { a.eventBus.SubscribeComputeAllocationUpdated(a.SubscribeToComputeAllocationUpdate) a.eventBus.SubscribeComputeAllocationMembershipCreated(a.SubscribeToComputeAllocationMembershipCreation) a.eventBus.SubscribeComputeAllocationMembershipResourceOverrideCreated(a.SubscribeToComputeAllocationMembershipResourceOverrideCreation) + a.eventBus.SubscribeComputeAllocationResourceMappingCreated(a.SubscribeToComputeAllocationResourceMappingCreation) } diff --git a/docs/API-Docs.md b/docs/API-Docs.md index b571c8255..325a219bc 100644 --- a/docs/API-Docs.md +++ b/docs/API-Docs.md @@ -619,15 +619,21 @@ removed. ### `POST /compute-allocations/{id}/resources` -Attach an existing resource to a compute allocation. +Attach an existing resource to a compute allocation, recording the amount of +the resource and the wall-clock time granted to the allocation. **Path parameters:** `{id}` — the compute allocation ID. **Required body fields:** `compute_allocation_resource_id` +**Optional body fields:** `resource_amount` (int64, default `0`), `resource_time` (int64, default `0`). Both must be non-negative. **Request** ```json -{ "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff" } +{ + "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", + "resource_amount": 24, + "resource_time": 1440 +} ``` **Response 201** @@ -636,17 +642,55 @@ Attach an existing resource to a compute allocation. { "id": "7e1d2c3b-4a5f-4b6c-9d8e-0011223344ff", "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56", - "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff" + "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", + "resource_amount": 24, + "resource_time": 1440 } ``` **Errors** -- `400` — `compute_allocation_resource_id` missing, or either the allocation or the resource does not exist. +- `400` — `compute_allocation_resource_id` missing, `resource_amount` / `resource_time` negative, or either the allocation or the resource does not exist. - `409` — this resource is already attached to the allocation. --- +### `PUT /compute-allocations/{id}/resources/{resourceId}` + +Update the `resource_amount` and `resource_time` recorded on an existing +(allocation, resource) mapping. + +**Path parameters:** `{id}` — the compute allocation ID; `{resourceId}` — the compute allocation resource ID. +**Required body fields:** `resource_amount`, `resource_time` (both non-negative int64). + +**Request** + +```json +{ + "resource_amount": 48, + "resource_time": 2880 +} +``` + +**Response 200** + +```json +{ + "id": "7e1d2c3b-4a5f-4b6c-9d8e-0011223344ff", + "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56", + "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff", + "resource_amount": 48, + "resource_time": 2880 +} +``` + +**Errors** + +- `400` — either id missing, or `resource_amount` / `resource_time` negative. +- `404` — no such mapping exists. + +--- + ### `DELETE /compute-allocations/{id}/resources/{resourceId}` Detach a resource from a compute allocation. @@ -1276,7 +1320,7 @@ RES_ID=$(curl -s -X POST $BASE/compute-allocation-resources \ # Attach the resource to the allocation. curl -s -X POST $BASE/compute-allocations/$ALLOC_ID/resources \ -H 'Content-Type: application/json' \ - -d "{\"compute_allocation_resource_id\":\"$RES_ID\"}" | jq + -d "{\"compute_allocation_resource_id\":\"$RES_ID\",\"resource_amount\":24,\"resource_time\":1440}" | jq # Define a rate for the resource. curl -s -X POST $BASE/compute-allocation-resource-rates \ @@ -1329,7 +1373,7 @@ OVERRIDE_ID=$(curl -s -X POST $BASE/compute-allocation-membership-resource-overr -d "{ \"compute_allocation_membership_id\":\"$MEMBERSHIP_ID\", \"compute_allocation_resource_id\":\"$RES_ID\", - \"overridden_resource_amount\":10000 + \"overridden_resource_amount\":10 }" | jq -r .id) # Bump the override amount. diff --git a/internal/db/migrations/000012_compute_allocation_resource_mapping_amounts.down.sql b/internal/db/migrations/000012_compute_allocation_resource_mapping_amounts.down.sql new file mode 100644 index 000000000..b792b9027 --- /dev/null +++ b/internal/db/migrations/000012_compute_allocation_resource_mapping_amounts.down.sql @@ -0,0 +1,20 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +ALTER TABLE compute_allocation_resource_mappings + DROP COLUMN resource_time, + DROP COLUMN resource_amount; diff --git a/internal/db/migrations/000012_compute_allocation_resource_mapping_amounts.up.sql b/internal/db/migrations/000012_compute_allocation_resource_mapping_amounts.up.sql new file mode 100644 index 000000000..120491635 --- /dev/null +++ b/internal/db/migrations/000012_compute_allocation_resource_mapping_amounts.up.sql @@ -0,0 +1,20 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +ALTER TABLE compute_allocation_resource_mappings + ADD COLUMN resource_amount BIGINT NOT NULL DEFAULT 0 AFTER compute_allocation_resource_id, + ADD COLUMN resource_time BIGINT NOT NULL DEFAULT 0 AFTER resource_amount; diff --git a/internal/server/server.go b/internal/server/server.go index 960799fd8..6af71d2df 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -81,6 +81,7 @@ func (s *Server) routes() { s.mux.HandleFunc("GET /compute-allocations/{id}/resources", s.listResourcesForAllocation) s.mux.HandleFunc("POST /compute-allocations/{id}/resources", s.attachResourceToAllocation) + s.mux.HandleFunc("PUT /compute-allocations/{id}/resources/{resourceId}", s.updateAllocationResourceMapping) s.mux.HandleFunc("DELETE /compute-allocations/{id}/resources/{resourceId}", s.detachResourceFromAllocation) s.mux.HandleFunc("GET /compute-allocation-resources/{id}/allocations", s.listAllocationsForResource) @@ -366,6 +367,8 @@ func (s *Server) listComputeAllocationResources(w http.ResponseWriter, r *http.R type attachResourceRequest struct { ComputeAllocationResourceID string `json:"compute_allocation_resource_id"` + ResourceAmount int64 `json:"resource_amount"` + ResourceTime int64 `json:"resource_time"` } func (s *Server) attachResourceToAllocation(w http.ResponseWriter, r *http.Request) { @@ -374,7 +377,7 @@ func (s *Server) attachResourceToAllocation(w http.ResponseWriter, r *http.Reque writeError(w, http.StatusBadRequest, err) return } - mapping, err := s.svc.AttachResourceToAllocation(r.Context(), r.PathValue("id"), body.ComputeAllocationResourceID) + mapping, err := s.svc.AttachResourceToAllocation(r.Context(), r.PathValue("id"), body.ComputeAllocationResourceID, body.ResourceAmount, body.ResourceTime) if err != nil { writeServiceError(w, err) return @@ -382,6 +385,25 @@ func (s *Server) attachResourceToAllocation(w http.ResponseWriter, r *http.Reque writeJSON(w, http.StatusCreated, mapping) } +type updateAllocationResourceMappingRequest struct { + ResourceAmount int64 `json:"resource_amount"` + ResourceTime int64 `json:"resource_time"` +} + +func (s *Server) updateAllocationResourceMapping(w http.ResponseWriter, r *http.Request) { + var body updateAllocationResourceMappingRequest + if err := decodeJSON(r, &body); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + mapping, err := s.svc.UpdateAllocationResourceMapping(r.Context(), r.PathValue("id"), r.PathValue("resourceId"), body.ResourceAmount, body.ResourceTime) + if err != nil { + writeServiceError(w, err) + return + } + writeJSON(w, http.StatusOK, mapping) +} + func (s *Server) detachResourceFromAllocation(w http.ResponseWriter, r *http.Request) { if err := s.svc.DetachResourceFromAllocation(r.Context(), r.PathValue("id"), r.PathValue("resourceId")); err != nil { writeServiceError(w, err) diff --git a/internal/store/compute_allocation_resource_mapping_store.go b/internal/store/compute_allocation_resource_mapping_store.go index ca89bbf06..9f05bf70b 100644 --- a/internal/store/compute_allocation_resource_mapping_store.go +++ b/internal/store/compute_allocation_resource_mapping_store.go @@ -37,10 +37,25 @@ func NewComputeAllocationResourceMappingStore(db *sqlx.DB) ComputeAllocationReso return &mysqlComputeAllocationResourceMappingStore{db: db} } +func (s *mysqlComputeAllocationResourceMappingStore) FindByID(ctx context.Context, id string) (*models.ComputeAllocationResourceMapping, error) { + var m models.ComputeAllocationResourceMapping + err := s.db.GetContext(ctx, &m, + `SELECT id, compute_allocation_id, compute_allocation_resource_id, resource_amount, resource_time + FROM compute_allocation_resource_mappings + WHERE id = ?`, id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + return &m, nil +} + func (s *mysqlComputeAllocationResourceMappingStore) FindByPair(ctx context.Context, allocationID, resourceID string) (*models.ComputeAllocationResourceMapping, error) { var m models.ComputeAllocationResourceMapping err := s.db.GetContext(ctx, &m, - `SELECT id, compute_allocation_id, compute_allocation_resource_id + `SELECT id, compute_allocation_id, compute_allocation_resource_id, resource_amount, resource_time FROM compute_allocation_resource_mappings WHERE compute_allocation_id = ? AND compute_allocation_resource_id = ?`, allocationID, resourceID) @@ -87,9 +102,19 @@ func (s *mysqlComputeAllocationResourceMappingStore) FindAllocationsByResource(c func (s *mysqlComputeAllocationResourceMappingStore) Create(ctx context.Context, tx *sql.Tx, m *models.ComputeAllocationResourceMapping) error { _, err := tx.ExecContext(ctx, `INSERT INTO compute_allocation_resource_mappings - (id, compute_allocation_id, compute_allocation_resource_id) - VALUES (?, ?, ?)`, - m.ID, m.ComputeAllocationID, m.ComputeAllocationResourceID) + (id, compute_allocation_id, compute_allocation_resource_id, resource_amount, resource_time) + VALUES (?, ?, ?, ?, ?)`, + m.ID, m.ComputeAllocationID, m.ComputeAllocationResourceID, m.ResourceAmount, m.ResourceTime) + return err +} + +func (s *mysqlComputeAllocationResourceMappingStore) Update(ctx context.Context, tx *sql.Tx, m *models.ComputeAllocationResourceMapping) error { + _, err := tx.ExecContext(ctx, + `UPDATE compute_allocation_resource_mappings + SET resource_amount = ?, + resource_time = ? + WHERE id = ?`, + m.ResourceAmount, m.ResourceTime, m.ID) return err } diff --git a/internal/store/store.go b/internal/store/store.go index 8066d87e8..b066df428 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -140,6 +140,8 @@ type ComputeAllocationResourceStore interface { // ComputeAllocationResourceMappingStore defines persistence operations for // the join table linking compute allocations and compute allocation resources. type ComputeAllocationResourceMappingStore interface { + // FindByID returns the mapping with the given ID, or nil if it does not exist. + FindByID(ctx context.Context, id string) (*models.ComputeAllocationResourceMapping, error) // FindByPair returns the mapping for a (allocation, resource) pair, or nil if absent. FindByPair(ctx context.Context, allocationID, resourceID string) (*models.ComputeAllocationResourceMapping, error) // FindResourcesByAllocation returns every resource attached to the given allocation. @@ -148,6 +150,8 @@ type ComputeAllocationResourceMappingStore interface { FindAllocationsByResource(ctx context.Context, resourceID string) ([]models.ComputeAllocation, error) // Create inserts a new mapping within the provided transaction. Create(ctx context.Context, tx *sql.Tx, m *models.ComputeAllocationResourceMapping) error + // Update replaces mutable fields of an existing mapping within the provided transaction. + Update(ctx context.Context, tx *sql.Tx, m *models.ComputeAllocationResourceMapping) error // DeleteByPair removes the mapping for a (allocation, resource) pair within the provided transaction. DeleteByPair(ctx context.Context, tx *sql.Tx, allocationID, resourceID string) error } diff --git a/pkg/models/allocation.go b/pkg/models/allocation.go index ca95595b5..582bce606 100644 --- a/pkg/models/allocation.go +++ b/pkg/models/allocation.go @@ -33,17 +33,21 @@ type ComputeAllocation struct { EndTime time.Time `json:"end_time" db:"end_time"` } +// Typically store the a paritition information type ComputeAllocationResource struct { ID string `json:"id" db:"id"` - Name string `json:"name" db:"name"` // resource / partition name, e.g., "cpu", "gpu", etc. - ResourceType string `json:"resource_type" db:"resource_type"` // GrpTRES, GrpTRESMins - ResourceAmount int64 `json:"resource_amount" db:"resource_amount"` // Number of CPUs, GPUs, time in minutes, or other unit depending on the resource type. + Name string `json:"name" db:"name"` // resource / partition name, e.g., "cpu-01", "gpu-01", "gpu-interactive", etc. + ResourceType string `json:"resource_type" db:"resource_type"` // cpu, gpu + ResourceAmount int64 `json:"resource_amount" db:"resource_amount"` // Number of CPUs, GPUs. } +// Store the association amount for a parition and allocation type ComputeAllocationResourceMapping struct { ID string `json:"id" db:"id"` ComputeAllocationID string `json:"compute_allocation_id" db:"compute_allocation_id"` ComputeAllocationResourceID string `json:"compute_allocation_resource_id" db:"compute_allocation_resource_id"` + ResourceAmount int64 `json:"resource_amount" db:"resource_amount"` // Amount of the resource allocated to this allocation (e.g., number of CPUs, GPUs). + ResourceTime int64 `json:"resource_time" db:"resource_time"` // Wall-clock time in minutes that the allocated amount is granted for. } type ComputeAllocationResourceRate struct { diff --git a/pkg/service/compute_allocation_resource_mapping.go b/pkg/service/compute_allocation_resource_mapping.go index d0c96014c..ce5f4ef18 100644 --- a/pkg/service/compute_allocation_resource_mapping.go +++ b/pkg/service/compute_allocation_resource_mapping.go @@ -27,16 +27,23 @@ import ( ) // AttachResourceToAllocation links a compute allocation resource to a compute -// allocation. Both entities must already exist. The link is idempotent — if -// the same (allocation, resource) pair is already linked, ErrAlreadyExists is -// returned. -func (s *Service) AttachResourceToAllocation(ctx context.Context, allocationID, resourceID string) (*models.ComputeAllocationResourceMapping, error) { +// allocation, recording the resource amount and wall-clock time granted to +// the allocation. Both entities must already exist. The link is idempotent — +// if the same (allocation, resource) pair is already linked, ErrAlreadyExists +// is returned. +func (s *Service) AttachResourceToAllocation(ctx context.Context, allocationID, resourceID string, resourceAmount, resourceTime int64) (*models.ComputeAllocationResourceMapping, error) { if allocationID == "" { return nil, fmt.Errorf("%w: compute_allocation_id is required", ErrInvalidInput) } if resourceID == "" { return nil, fmt.Errorf("%w: compute_allocation_resource_id is required", ErrInvalidInput) } + if resourceAmount < 0 { + return nil, fmt.Errorf("%w: resource_amount must be non-negative", ErrInvalidInput) + } + if resourceTime < 0 { + return nil, fmt.Errorf("%w: resource_time must be non-negative", ErrInvalidInput) + } if alloc, err := s.allocs.FindByID(ctx, allocationID); err != nil { return nil, fmt.Errorf("lookup compute allocation: %w", err) @@ -60,6 +67,8 @@ func (s *Service) AttachResourceToAllocation(ctx context.Context, allocationID, ID: newID(), ComputeAllocationID: allocationID, ComputeAllocationResourceID: resourceID, + ResourceAmount: resourceAmount, + ResourceTime: resourceTime, } if err := s.inTx(ctx, func(tx *sql.Tx) error { return s.resourceMappings.Create(ctx, tx, mapping) @@ -71,6 +80,39 @@ func (s *Service) AttachResourceToAllocation(ctx context.Context, allocationID, return mapping, nil } +// UpdateAllocationResourceMapping updates the resource_amount and +// resource_time recorded against an existing (allocation, resource) mapping. +func (s *Service) UpdateAllocationResourceMapping(ctx context.Context, allocationID, resourceID string, resourceAmount, resourceTime int64) (*models.ComputeAllocationResourceMapping, error) { + if allocationID == "" || resourceID == "" { + return nil, fmt.Errorf("%w: allocation and resource ids are required", ErrInvalidInput) + } + if resourceAmount < 0 { + return nil, fmt.Errorf("%w: resource_amount must be non-negative", ErrInvalidInput) + } + if resourceTime < 0 { + return nil, fmt.Errorf("%w: resource_time must be non-negative", ErrInvalidInput) + } + + existing, err := s.resourceMappings.FindByPair(ctx, allocationID, resourceID) + if err != nil { + return nil, fmt.Errorf("lookup mapping: %w", err) + } + if existing == nil { + return nil, ErrNotFound + } + + existing.ResourceAmount = resourceAmount + existing.ResourceTime = resourceTime + if err := s.inTx(ctx, func(tx *sql.Tx) error { + return s.resourceMappings.Update(ctx, tx, existing) + }); err != nil { + return nil, fmt.Errorf("update allocation resource mapping: %w", err) + } + + s.eventBus.Publish(events.ComputeAllocationResourceMappingUpdateEvent, existing) + return existing, nil +} + // DetachResourceFromAllocation removes the link between a compute allocation // and a compute allocation resource. Returns ErrNotFound when no such mapping // exists.
