You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by di...@apache.org on 2026/05/18 00:31:43 UTC
(airavata-custos) 03/09: Implemented service layer for compute allocations, allocation resources and mapping
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 3f6e4b96fc51ad61fae8cad56ea816542711b5b5
Author: DImuthuUpe <di...@gmail.com>
AuthorDate: Sat May 16 20:38:43 2026 -0400
Implemented service layer for compute allocations, allocation resources and mapping
---
docs/API-Docs.md | 302 +++++++++++++++++++++
.../migrations/000003_compute_allocations.down.sql | 19 ++
.../migrations/000003_compute_allocations.up.sql | 47 ++++
...4_compute_allocation_resource_mappings.down.sql | 18 ++
...004_compute_allocation_resource_mappings.up.sql | 32 +++
internal/server/server.go | 111 ++++++++
.../compute_allocation_resource_mapping_store.go | 102 +++++++
.../store/compute_allocation_resource_store.go | 83 ++++++
internal/store/compute_allocation_store.go | 96 +++++++
internal/store/store.go | 46 ++++
pkg/models/allocation.go | 30 +-
pkg/service/compute_allocation.go | 126 +++++++++
pkg/service/compute_allocation_resource.go | 98 +++++++
pkg/service/compute_allocation_resource_mapping.go | 119 ++++++++
pkg/service/service.go | 48 +++-
15 files changed, 1250 insertions(+), 27 deletions(-)
diff --git a/docs/API-Docs.md b/docs/API-Docs.md
index dacb5364d..f509a3000 100644
--- a/docs/API-Docs.md
+++ b/docs/API-Docs.md
@@ -269,6 +269,286 @@ Retrieve a project by its ID.
---
+## Compute Clusters
+
+A compute cluster represents a physical or logical HPC resource (e.g. a
+Slurm cluster) where allocations can be provisioned.
+
+### `POST /compute-clusters`
+
+Create a new compute cluster.
+
+**Required fields:** `name`
+**Optional fields:** `id` (auto-generated if omitted)
+
+`name` must be unique across compute clusters.
+
+**Request**
+
+```json
+{ "name": "Delta" }
+```
+
+**Response 201**
+
+```json
+{
+ "id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1",
+ "name": "Delta"
+}
+```
+
+**Errors**
+
+- `400` — `name` is required.
+- `409` — a compute cluster with this `name` already exists.
+
+---
+
+### `GET /compute-clusters`
+
+List all compute clusters.
+
+**Response 200**
+
+```json
+[
+ { "id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "name": "Delta" },
+ { "id": "1d4e6a3b-7c8f-49b2-bd34-7c1f9a4e5d10", "name": "Phoenix" }
+]
+```
+
+---
+
+### `GET /compute-clusters/{id}`
+
+Retrieve a single compute cluster by its ID.
+
+**Errors**
+
+- `404` — no compute cluster matches the supplied ID.
+
+---
+
+## Compute Allocations
+
+A compute allocation grants a project a budget of Service Units (SUs) on a
+specific compute cluster for a bounded time window.
+
+### `POST /compute-allocations`
+
+Create a new compute allocation.
+
+**Required fields:** `project_id`, `name`, `compute_cluster_id`
+**Optional fields:** `id`, `status` (defaults to `ACTIVE`), `initial_su_amount`, `start_time`, `end_time`
+
+Both `project_id` and `compute_cluster_id` must reference existing records.
+`status` is one of `ACTIVE`, `INACTIVE`, `DELETED`.
+
+**Request**
+
+```json
+{
+ "project_id": "3a8c2e7b-9d1f-4f5a-bc02-7a4d9e6c1bb1",
+ "name": "Q2 2026 Climate Run",
+ "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1",
+ "initial_su_amount": 100000,
+ "start_time": "2026-04-01T00:00:00Z",
+ "end_time": "2026-06-30T23:59:59Z"
+}
+```
+
+**Response 201**
+
+```json
+{
+ "id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56",
+ "project_id": "3a8c2e7b-9d1f-4f5a-bc02-7a4d9e6c1bb1",
+ "name": "Q2 2026 Climate Run",
+ "status": "ACTIVE",
+ "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1",
+ "initial_su_amount": 100000,
+ "start_time": "2026-04-01T00:00:00Z",
+ "end_time": "2026-06-30T23:59:59Z"
+}
+```
+
+**Errors**
+
+- `400` — required field missing, or `project_id` / `compute_cluster_id` does not exist.
+
+---
+
+### `GET /compute-allocations/{id}`
+
+Retrieve a compute allocation by its ID.
+
+**Errors**
+
+- `404` — no compute allocation matches the supplied ID.
+
+---
+
+## Compute Allocation Resources
+
+A compute allocation resource describes a hardware capability (e.g.
+`GPU B200`, `CPU`) that can be attached to one or more allocations.
+
+### `POST /compute-allocation-resources`
+
+Create a new compute allocation resource.
+
+**Required fields:** `name`, `resource_type`
+**Optional fields:** `id`, `resource_amount`
+
+**Request**
+
+```json
+{
+ "name": "GPU B200",
+ "resource_type": "GPU",
+ "resource_amount": 8
+}
+```
+
+**Response 201**
+
+```json
+{
+ "id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff",
+ "name": "GPU B200",
+ "resource_type": "GPU",
+ "resource_amount": 8
+}
+```
+
+**Errors**
+
+- `400` — `name` or `resource_type` is missing.
+
+---
+
+### `GET /compute-allocation-resources`
+
+List all compute allocation resources.
+
+**Response 200**
+
+```json
+[
+ {
+ "id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff",
+ "name": "GPU B200",
+ "resource_type": "GPU",
+ "resource_amount": 8
+ }
+]
+```
+
+---
+
+### `GET /compute-allocation-resources/{id}`
+
+Retrieve a compute allocation resource by its ID.
+
+**Errors**
+
+- `404` — no resource matches the supplied ID.
+
+---
+
+## Compute Allocation ↔ Resource Mappings
+
+A many-to-many join: an allocation can have many resources attached, and a
+resource can be attached to many allocations. Mappings are unique per
+(allocation, resource) pair, and are cascade-deleted when either parent is
+removed.
+
+### `POST /compute-allocations/{id}/resources`
+
+Attach an existing resource to a compute allocation.
+
+**Path parameters:** `{id}` — the compute allocation ID.
+**Required body fields:** `compute_allocation_resource_id`
+
+**Request**
+
+```json
+{ "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff" }
+```
+
+**Response 201**
+
+```json
+{
+ "id": "7e1d2c3b-4a5f-4b6c-9d8e-0011223344ff",
+ "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56",
+ "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff"
+}
+```
+
+**Errors**
+
+- `400` — `compute_allocation_resource_id` missing, or either the allocation or the resource does not exist.
+- `409` — this resource is already attached to the allocation.
+
+---
+
+### `DELETE /compute-allocations/{id}/resources/{resourceId}`
+
+Detach a resource from a compute allocation.
+
+**Response 204** — empty body on success.
+
+**Errors**
+
+- `404` — no such mapping exists.
+
+---
+
+### `GET /compute-allocations/{id}/resources`
+
+List every compute allocation resource currently attached to the given
+compute allocation.
+
+**Response 200**
+
+```json
+[
+ {
+ "id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff",
+ "name": "GPU B200",
+ "resource_type": "GPU",
+ "resource_amount": 8
+ }
+]
+```
+
+---
+
+### `GET /compute-allocation-resources/{id}/allocations`
+
+List every compute allocation that has the given resource attached.
+
+**Response 200**
+
+```json
+[
+ {
+ "id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56",
+ "project_id": "3a8c2e7b-9d1f-4f5a-bc02-7a4d9e6c1bb1",
+ "name": "Q2 2026 Climate Run",
+ "status": "ACTIVE",
+ "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1",
+ "initial_su_amount": 100000,
+ "start_time": "2026-04-01T00:00:00Z",
+ "end_time": "2026-06-30T23:59:59Z"
+ }
+]
+```
+
+---
+
## End-to-end example
```bash
@@ -289,6 +569,28 @@ PROJ_ID=$(curl -s -X POST $BASE/projects \
-d "{\"title\":\"Climate Simulation 2026\",\"origination\":\"ACCESS\",\"originated_id\":\"ACCESS-PRJ-9000\",\"project_pi_id\":\"$USER_ID\"}" \
| jq -r .id)
+CLUSTER_ID=$(curl -s -X POST $BASE/compute-clusters \
+ -H 'Content-Type: application/json' \
+ -d '{"name":"Delta"}' | jq -r .id)
+
+ALLOC_ID=$(curl -s -X POST $BASE/compute-allocations \
+ -H 'Content-Type: application/json' \
+ -d "{\"project_id\":\"$PROJ_ID\",\"name\":\"Q2 2026 Climate Run\",\"compute_cluster_id\":\"$CLUSTER_ID\",\"initial_su_amount\":100000}" \
+ | jq -r .id)
+
+RES_ID=$(curl -s -X POST $BASE/compute-allocation-resources \
+ -H 'Content-Type: application/json' \
+ -d '{"name":"GPU B200","resource_type":"GPU","resource_amount":8}' | jq -r .id)
+
+# 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
+
+# Bidirectional lookups.
+curl -s $BASE/compute-allocations/$ALLOC_ID/resources | jq
+curl -s $BASE/compute-allocation-resources/$RES_ID/allocations | jq
+
curl -s $BASE/projects/$PROJ_ID | jq
```
diff --git a/internal/db/migrations/000003_compute_allocations.down.sql b/internal/db/migrations/000003_compute_allocations.down.sql
new file mode 100644
index 000000000..71fc3aadf
--- /dev/null
+++ b/internal/db/migrations/000003_compute_allocations.down.sql
@@ -0,0 +1,19 @@
+-- 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.
+
+DROP TABLE IF EXISTS compute_allocation_resources;
+DROP TABLE IF EXISTS compute_allocations;
diff --git a/internal/db/migrations/000003_compute_allocations.up.sql b/internal/db/migrations/000003_compute_allocations.up.sql
new file mode 100644
index 000000000..68144ad61
--- /dev/null
+++ b/internal/db/migrations/000003_compute_allocations.up.sql
@@ -0,0 +1,47 @@
+-- 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.
+
+CREATE TABLE IF NOT EXISTS compute_allocations
+(
+ id VARCHAR(255) NOT NULL,
+ project_id VARCHAR(255) NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ status VARCHAR(64) NOT NULL,
+ compute_cluster_id VARCHAR(255) NOT NULL,
+ initial_su_amount BIGINT NOT NULL DEFAULT 0,
+ start_time TIMESTAMP(6) NOT NULL,
+ end_time TIMESTAMP(6) NOT NULL,
+ created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+ updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
+ PRIMARY KEY (id),
+ KEY idx_compute_allocations_project (project_id),
+ KEY idx_compute_allocations_cluster (compute_cluster_id),
+ CONSTRAINT fk_compute_allocations_project FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE RESTRICT,
+ CONSTRAINT fk_compute_allocations_cluster FOREIGN KEY (compute_cluster_id) REFERENCES compute_clusters (id) ON DELETE RESTRICT
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+
+CREATE TABLE IF NOT EXISTS compute_allocation_resources
+(
+ id VARCHAR(255) NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ resource_type VARCHAR(64) NOT NULL,
+ resource_amount BIGINT NOT NULL DEFAULT 0,
+ created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+ updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
+ PRIMARY KEY (id),
+ KEY idx_compute_allocation_resources_type (resource_type)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
diff --git a/internal/db/migrations/000004_compute_allocation_resource_mappings.down.sql b/internal/db/migrations/000004_compute_allocation_resource_mappings.down.sql
new file mode 100644
index 000000000..cfceaf2fd
--- /dev/null
+++ b/internal/db/migrations/000004_compute_allocation_resource_mappings.down.sql
@@ -0,0 +1,18 @@
+-- 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.
+
+DROP TABLE IF EXISTS compute_allocation_resource_mappings;
diff --git a/internal/db/migrations/000004_compute_allocation_resource_mappings.up.sql b/internal/db/migrations/000004_compute_allocation_resource_mappings.up.sql
new file mode 100644
index 000000000..877cc0ba7
--- /dev/null
+++ b/internal/db/migrations/000004_compute_allocation_resource_mappings.up.sql
@@ -0,0 +1,32 @@
+-- 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.
+
+CREATE TABLE IF NOT EXISTS compute_allocation_resource_mappings
+(
+ id VARCHAR(255) NOT NULL,
+ compute_allocation_id VARCHAR(255) NOT NULL,
+ compute_allocation_resource_id VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+ PRIMARY KEY (id),
+ UNIQUE KEY uq_carm_pair (compute_allocation_id, compute_allocation_resource_id),
+ KEY idx_carm_allocation (compute_allocation_id),
+ KEY idx_carm_resource (compute_allocation_resource_id),
+ CONSTRAINT fk_carm_allocation FOREIGN KEY (compute_allocation_id)
+ REFERENCES compute_allocations (id) ON DELETE CASCADE,
+ CONSTRAINT fk_carm_resource FOREIGN KEY (compute_allocation_resource_id)
+ REFERENCES compute_allocation_resources (id) ON DELETE CASCADE
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
diff --git a/internal/server/server.go b/internal/server/server.go
index 47679bee4..5207c724d 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -63,6 +63,18 @@ func (s *Server) routes() {
s.mux.HandleFunc("POST /compute-clusters", s.createComputeCluster)
s.mux.HandleFunc("GET /compute-clusters", s.listComputeClusters)
s.mux.HandleFunc("GET /compute-clusters/{id}", s.getComputeCluster)
+
+ s.mux.HandleFunc("POST /compute-allocations", s.createComputeAllocation)
+ s.mux.HandleFunc("GET /compute-allocations/{id}", s.getComputeAllocation)
+
+ s.mux.HandleFunc("POST /compute-allocation-resources", s.createComputeAllocationResource)
+ s.mux.HandleFunc("GET /compute-allocation-resources", s.listComputeAllocationResources)
+ s.mux.HandleFunc("GET /compute-allocation-resources/{id}", s.getComputeAllocationResource)
+
+ s.mux.HandleFunc("GET /compute-allocations/{id}/resources", s.listResourcesForAllocation)
+ s.mux.HandleFunc("POST /compute-allocations/{id}/resources", s.attachResourceToAllocation)
+ s.mux.HandleFunc("DELETE /compute-allocations/{id}/resources/{resourceId}", s.detachResourceFromAllocation)
+ s.mux.HandleFunc("GET /compute-allocation-resources/{id}/allocations", s.listAllocationsForResource)
}
func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
@@ -170,6 +182,105 @@ func (s *Server) listComputeClusters(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, clusters)
}
+func (s *Server) createComputeAllocation(w http.ResponseWriter, r *http.Request) {
+ var a models.ComputeAllocation
+ if err := decodeJSON(r, &a); err != nil {
+ writeError(w, http.StatusBadRequest, err)
+ return
+ }
+ created, err := s.svc.CreateComputeAllocation(r.Context(), &a)
+ if err != nil {
+ writeServiceError(w, err)
+ return
+ }
+ writeJSON(w, http.StatusCreated, created)
+}
+
+func (s *Server) getComputeAllocation(w http.ResponseWriter, r *http.Request) {
+ a, err := s.svc.GetComputeAllocation(r.Context(), r.PathValue("id"))
+ if err != nil {
+ writeServiceError(w, err)
+ return
+ }
+ writeJSON(w, http.StatusOK, a)
+}
+
+func (s *Server) createComputeAllocationResource(w http.ResponseWriter, r *http.Request) {
+ var res models.ComputeAllocationResource
+ if err := decodeJSON(r, &res); err != nil {
+ writeError(w, http.StatusBadRequest, err)
+ return
+ }
+ created, err := s.svc.CreateComputeAllocationResource(r.Context(), &res)
+ if err != nil {
+ writeServiceError(w, err)
+ return
+ }
+ writeJSON(w, http.StatusCreated, created)
+}
+
+func (s *Server) getComputeAllocationResource(w http.ResponseWriter, r *http.Request) {
+ res, err := s.svc.GetComputeAllocationResource(r.Context(), r.PathValue("id"))
+ if err != nil {
+ writeServiceError(w, err)
+ return
+ }
+ writeJSON(w, http.StatusOK, res)
+}
+
+func (s *Server) listComputeAllocationResources(w http.ResponseWriter, r *http.Request) {
+ resources, err := s.svc.ListComputeAllocationResources(r.Context())
+ if err != nil {
+ writeServiceError(w, err)
+ return
+ }
+ writeJSON(w, http.StatusOK, resources)
+}
+
+type attachResourceRequest struct {
+ ComputeAllocationResourceID string `json:"compute_allocation_resource_id"`
+}
+
+func (s *Server) attachResourceToAllocation(w http.ResponseWriter, r *http.Request) {
+ var body attachResourceRequest
+ if err := decodeJSON(r, &body); err != nil {
+ writeError(w, http.StatusBadRequest, err)
+ return
+ }
+ mapping, err := s.svc.AttachResourceToAllocation(r.Context(), r.PathValue("id"), body.ComputeAllocationResourceID)
+ if err != nil {
+ writeServiceError(w, err)
+ return
+ }
+ writeJSON(w, http.StatusCreated, 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)
+ return
+ }
+ w.WriteHeader(http.StatusNoContent)
+}
+
+func (s *Server) listResourcesForAllocation(w http.ResponseWriter, r *http.Request) {
+ resources, err := s.svc.ListResourcesForAllocation(r.Context(), r.PathValue("id"))
+ if err != nil {
+ writeServiceError(w, err)
+ return
+ }
+ writeJSON(w, http.StatusOK, resources)
+}
+
+func (s *Server) listAllocationsForResource(w http.ResponseWriter, r *http.Request) {
+ allocs, err := s.svc.ListAllocationsForResource(r.Context(), r.PathValue("id"))
+ if err != nil {
+ writeServiceError(w, err)
+ return
+ }
+ writeJSON(w, http.StatusOK, allocs)
+}
+
// LoggingMiddleware logs every request once it completes.
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/store/compute_allocation_resource_mapping_store.go b/internal/store/compute_allocation_resource_mapping_store.go
new file mode 100644
index 000000000..ca89bbf06
--- /dev/null
+++ b/internal/store/compute_allocation_resource_mapping_store.go
@@ -0,0 +1,102 @@
+// 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.
+
+package store
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+
+ "github.com/jmoiron/sqlx"
+
+ "github.com/apache/airavata-custos/pkg/models"
+)
+
+type mysqlComputeAllocationResourceMappingStore struct {
+ db *sqlx.DB
+}
+
+// NewComputeAllocationResourceMappingStore returns a MySQL-backed
+// ComputeAllocationResourceMappingStore.
+func NewComputeAllocationResourceMappingStore(db *sqlx.DB) ComputeAllocationResourceMappingStore {
+ return &mysqlComputeAllocationResourceMappingStore{db: db}
+}
+
+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
+ FROM compute_allocation_resource_mappings
+ WHERE compute_allocation_id = ? AND compute_allocation_resource_id = ?`,
+ allocationID, resourceID)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &m, nil
+}
+
+func (s *mysqlComputeAllocationResourceMappingStore) FindResourcesByAllocation(ctx context.Context, allocationID string) ([]models.ComputeAllocationResource, error) {
+ var resources []models.ComputeAllocationResource
+ err := s.db.SelectContext(ctx, &resources,
+ `SELECT r.id, r.name, r.resource_type, r.resource_amount
+ FROM compute_allocation_resources r
+ JOIN compute_allocation_resource_mappings m
+ ON m.compute_allocation_resource_id = r.id
+ WHERE m.compute_allocation_id = ?
+ ORDER BY r.name`, allocationID)
+ if err != nil {
+ return nil, err
+ }
+ return resources, nil
+}
+
+func (s *mysqlComputeAllocationResourceMappingStore) FindAllocationsByResource(ctx context.Context, resourceID string) ([]models.ComputeAllocation, error) {
+ var allocs []models.ComputeAllocation
+ err := s.db.SelectContext(ctx, &allocs,
+ `SELECT a.id, a.project_id, a.name, a.status, a.compute_cluster_id,
+ a.initial_su_amount, a.start_time, a.end_time
+ FROM compute_allocations a
+ JOIN compute_allocation_resource_mappings m
+ ON m.compute_allocation_id = a.id
+ WHERE m.compute_allocation_resource_id = ?
+ ORDER BY a.name`, resourceID)
+ if err != nil {
+ return nil, err
+ }
+ return allocs, nil
+}
+
+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)
+ return err
+}
+
+func (s *mysqlComputeAllocationResourceMappingStore) DeleteByPair(ctx context.Context, tx *sql.Tx, allocationID, resourceID string) error {
+ _, err := tx.ExecContext(ctx,
+ `DELETE FROM compute_allocation_resource_mappings
+ WHERE compute_allocation_id = ? AND compute_allocation_resource_id = ?`,
+ allocationID, resourceID)
+ return err
+}
diff --git a/internal/store/compute_allocation_resource_store.go b/internal/store/compute_allocation_resource_store.go
new file mode 100644
index 000000000..efbd507a2
--- /dev/null
+++ b/internal/store/compute_allocation_resource_store.go
@@ -0,0 +1,83 @@
+// 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.
+
+package store
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+
+ "github.com/jmoiron/sqlx"
+
+ "github.com/apache/airavata-custos/pkg/models"
+)
+
+type mysqlComputeAllocationResourceStore struct {
+ db *sqlx.DB
+}
+
+// NewComputeAllocationResourceStore returns a MySQL-backed
+// ComputeAllocationResourceStore.
+func NewComputeAllocationResourceStore(db *sqlx.DB) ComputeAllocationResourceStore {
+ return &mysqlComputeAllocationResourceStore{db: db}
+}
+
+func (s *mysqlComputeAllocationResourceStore) FindByID(ctx context.Context, id string) (*models.ComputeAllocationResource, error) {
+ var r models.ComputeAllocationResource
+ err := s.db.GetContext(ctx, &r,
+ `SELECT id, name, resource_type, resource_amount FROM compute_allocation_resources WHERE id = ?`, id)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &r, nil
+}
+
+func (s *mysqlComputeAllocationResourceStore) List(ctx context.Context) ([]models.ComputeAllocationResource, error) {
+ var resources []models.ComputeAllocationResource
+ err := s.db.SelectContext(ctx, &resources,
+ `SELECT id, name, resource_type, resource_amount FROM compute_allocation_resources ORDER BY name`)
+ if err != nil {
+ return nil, err
+ }
+ return resources, nil
+}
+
+func (s *mysqlComputeAllocationResourceStore) Create(ctx context.Context, tx *sql.Tx, r *models.ComputeAllocationResource) error {
+ _, err := tx.ExecContext(ctx,
+ `INSERT INTO compute_allocation_resources (id, name, resource_type, resource_amount)
+ VALUES (?, ?, ?, ?)`,
+ r.ID, r.Name, r.ResourceType, r.ResourceAmount)
+ return err
+}
+
+func (s *mysqlComputeAllocationResourceStore) Update(ctx context.Context, tx *sql.Tx, r *models.ComputeAllocationResource) error {
+ _, err := tx.ExecContext(ctx,
+ `UPDATE compute_allocation_resources
+ SET name = ?, resource_type = ?, resource_amount = ?
+ WHERE id = ?`,
+ r.Name, r.ResourceType, r.ResourceAmount, r.ID)
+ return err
+}
+
+func (s *mysqlComputeAllocationResourceStore) Delete(ctx context.Context, tx *sql.Tx, id string) error {
+ _, err := tx.ExecContext(ctx, `DELETE FROM compute_allocation_resources WHERE id = ?`, id)
+ return err
+}
diff --git a/internal/store/compute_allocation_store.go b/internal/store/compute_allocation_store.go
new file mode 100644
index 000000000..9bfc5c922
--- /dev/null
+++ b/internal/store/compute_allocation_store.go
@@ -0,0 +1,96 @@
+// 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.
+
+package store
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+
+ "github.com/jmoiron/sqlx"
+
+ "github.com/apache/airavata-custos/pkg/models"
+)
+
+type mysqlComputeAllocationStore struct {
+ db *sqlx.DB
+}
+
+// NewComputeAllocationStore returns a MySQL-backed ComputeAllocationStore.
+func NewComputeAllocationStore(db *sqlx.DB) ComputeAllocationStore {
+ return &mysqlComputeAllocationStore{db: db}
+}
+
+const computeAllocationColumns = `id, project_id, name, status, compute_cluster_id, initial_su_amount, start_time, end_time`
+
+func (s *mysqlComputeAllocationStore) FindByID(ctx context.Context, id string) (*models.ComputeAllocation, error) {
+ var a models.ComputeAllocation
+ err := s.db.GetContext(ctx, &a,
+ `SELECT `+computeAllocationColumns+` FROM compute_allocations WHERE id = ?`, id)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &a, nil
+}
+
+func (s *mysqlComputeAllocationStore) FindByProject(ctx context.Context, projectID string) ([]models.ComputeAllocation, error) {
+ var allocs []models.ComputeAllocation
+ err := s.db.SelectContext(ctx, &allocs,
+ `SELECT `+computeAllocationColumns+` FROM compute_allocations WHERE project_id = ?`, projectID)
+ if err != nil {
+ return nil, err
+ }
+ return allocs, nil
+}
+
+func (s *mysqlComputeAllocationStore) FindByCluster(ctx context.Context, clusterID string) ([]models.ComputeAllocation, error) {
+ var allocs []models.ComputeAllocation
+ err := s.db.SelectContext(ctx, &allocs,
+ `SELECT `+computeAllocationColumns+` FROM compute_allocations WHERE compute_cluster_id = ?`, clusterID)
+ if err != nil {
+ return nil, err
+ }
+ return allocs, nil
+}
+
+func (s *mysqlComputeAllocationStore) Create(ctx context.Context, tx *sql.Tx, a *models.ComputeAllocation) error {
+ _, err := tx.ExecContext(ctx,
+ `INSERT INTO compute_allocations (`+computeAllocationColumns+`)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
+ a.ID, a.ProjectID, a.Name, string(a.Status), a.ComputeClusterID, a.InitialSUAmount, a.StartTime, a.EndTime)
+ return err
+}
+
+func (s *mysqlComputeAllocationStore) Update(ctx context.Context, tx *sql.Tx, a *models.ComputeAllocation) error {
+ _, err := tx.ExecContext(ctx,
+ `UPDATE compute_allocations
+ SET project_id = ?, name = ?, status = ?, compute_cluster_id = ?,
+ initial_su_amount = ?, start_time = ?, end_time = ?
+ WHERE id = ?`,
+ a.ProjectID, a.Name, string(a.Status), a.ComputeClusterID,
+ a.InitialSUAmount, a.StartTime, a.EndTime, a.ID)
+ return err
+}
+
+func (s *mysqlComputeAllocationStore) Delete(ctx context.Context, tx *sql.Tx, id string) error {
+ _, err := tx.ExecContext(ctx, `DELETE FROM compute_allocations WHERE id = ?`, id)
+ return err
+}
diff --git a/internal/store/store.go b/internal/store/store.go
index c9a6525a8..013f948d1 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -85,3 +85,49 @@ type ProjectStore interface {
// Delete removes a project by ID within the provided transaction.
Delete(ctx context.Context, tx *sql.Tx, id string) error
}
+
+// ComputeAllocationStore defines persistence operations for compute allocations.
+type ComputeAllocationStore interface {
+ // FindByID returns the allocation with the given ID, or nil if not found.
+ FindByID(ctx context.Context, id string) (*models.ComputeAllocation, error)
+ // FindByProject returns all allocations attached to the given project.
+ FindByProject(ctx context.Context, projectID string) ([]models.ComputeAllocation, error)
+ // FindByCluster returns all allocations attached to the given compute cluster.
+ FindByCluster(ctx context.Context, clusterID string) ([]models.ComputeAllocation, error)
+ // Create inserts a new allocation within the provided transaction.
+ Create(ctx context.Context, tx *sql.Tx, a *models.ComputeAllocation) error
+ // Update replaces mutable fields of an existing allocation within the provided transaction.
+ Update(ctx context.Context, tx *sql.Tx, a *models.ComputeAllocation) error
+ // Delete removes an allocation by ID within the provided transaction.
+ Delete(ctx context.Context, tx *sql.Tx, id string) error
+}
+
+// ComputeAllocationResourceStore defines persistence operations for compute
+// allocation resources (CPU, GPU, etc.).
+type ComputeAllocationResourceStore interface {
+ // FindByID returns the resource with the given ID, or nil if not found.
+ FindByID(ctx context.Context, id string) (*models.ComputeAllocationResource, error)
+ // List returns all compute allocation resources.
+ List(ctx context.Context) ([]models.ComputeAllocationResource, error)
+ // Create inserts a new resource within the provided transaction.
+ Create(ctx context.Context, tx *sql.Tx, r *models.ComputeAllocationResource) error
+ // Update replaces mutable fields of an existing resource within the provided transaction.
+ Update(ctx context.Context, tx *sql.Tx, r *models.ComputeAllocationResource) error
+ // Delete removes a resource by ID within the provided transaction.
+ Delete(ctx context.Context, tx *sql.Tx, id string) error
+}
+
+// ComputeAllocationResourceMappingStore defines persistence operations for
+// the join table linking compute allocations and compute allocation resources.
+type ComputeAllocationResourceMappingStore interface {
+ // 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.
+ FindResourcesByAllocation(ctx context.Context, allocationID string) ([]models.ComputeAllocationResource, error)
+ // FindAllocationsByResource returns every allocation that has the given resource attached.
+ 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
+ // 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 e3c2c5d94..8d879af1a 100644
--- a/pkg/models/allocation.go
+++ b/pkg/models/allocation.go
@@ -16,27 +16,27 @@ type ComputeCluster struct {
}
type ComputeAllocation struct {
- ID string `json:"id"`
- ProjectID string `json:"project_id"`
- Name string `json:"name"`
- Status AllocationStatus `json:"status"` // ACTIVE, INACTIVE, DELETED, etc.
- ComputeClusterID string `json:"compute_cluster_id"` // The ID of the compute cluster where the allocation is provisioned.
- InitialSUAmount int64 `json:"initial_su_amount"` // SUs allocated at the time of allocation creation.
- StartTime time.Time `json:"start_time"`
- EndTime time.Time `json:"end_time"`
+ ID string `json:"id" db:"id"`
+ ProjectID string `json:"project_id" db:"project_id"`
+ Name string `json:"name" db:"name"`
+ Status AllocationStatus `json:"status" db:"status"` // ACTIVE, INACTIVE, DELETED, etc.
+ ComputeClusterID string `json:"compute_cluster_id" db:"compute_cluster_id"` // The ID of the compute cluster where the allocation is provisioned.
+ InitialSUAmount int64 `json:"initial_su_amount" db:"initial_su_amount"` // SUs allocated at the time of allocation creation.
+ StartTime time.Time `json:"start_time" db:"start_time"`
+ EndTime time.Time `json:"end_time" db:"end_time"`
}
type ComputeAllocationResource struct {
- ID string `json:"id"`
- Name string `json:"name"` // A human-readable name for the resource, e.g., "GPU B200", "CPU", "GPU RTX6000", etc.
- ResourceType string `json:"resource_type"` // CPU, GPU, etc.
- ResourceAmount int64 `json:"resource_amount"` // Number of CPUs, GPUs, etc. allocated.
+ ID string `json:"id" db:"id"`
+ Name string `json:"name" db:"name"` // A human-readable name for the resource, e.g., "GPU B200", "CPU", "GPU RTX6000", etc.
+ ResourceType string `json:"resource_type" db:"resource_type"` // CPU, GPU, etc.
+ ResourceAmount int64 `json:"resource_amount" db:"resource_amount"` // Number of CPUs, GPUs, etc. allocated.
}
type ComputeAllocationResourceMapping struct {
- ID string `json:"id"`
- ComputeAllocationID string `json:"compute_allocation_id"`
- ComputeAllocationResourceID string `json:"compute_allocation_resource_id"`
+ 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"`
}
type ComputeAllocationResourceRate struct {
diff --git a/pkg/service/compute_allocation.go b/pkg/service/compute_allocation.go
new file mode 100644
index 000000000..e51a624e4
--- /dev/null
+++ b/pkg/service/compute_allocation.go
@@ -0,0 +1,126 @@
+// 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.
+
+package service
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+
+ "github.com/apache/airavata-custos/pkg/models"
+)
+
+// CreateComputeAllocation persists a new compute allocation. The referenced
+// project and compute cluster must already exist. If alloc.ID is empty a new
+// UUID is generated; if Status is empty it defaults to ACTIVE.
+func (s *Service) CreateComputeAllocation(ctx context.Context, alloc *models.ComputeAllocation) (*models.ComputeAllocation, error) {
+ if alloc == nil {
+ return nil, fmt.Errorf("%w: compute allocation is nil", ErrInvalidInput)
+ }
+ if alloc.Name == "" {
+ return nil, fmt.Errorf("%w: compute allocation name is required", ErrInvalidInput)
+ }
+ if alloc.ProjectID == "" {
+ return nil, fmt.Errorf("%w: project_id is required", ErrInvalidInput)
+ }
+ if alloc.ComputeClusterID == "" {
+ return nil, fmt.Errorf("%w: compute_cluster_id is required", ErrInvalidInput)
+ }
+ if alloc.ID == "" {
+ alloc.ID = newID()
+ }
+ if alloc.Status == "" {
+ alloc.Status = models.ACTIVE
+ }
+
+ if proj, err := s.projs.FindByID(ctx, alloc.ProjectID); err != nil {
+ return nil, fmt.Errorf("lookup project: %w", err)
+ } else if proj == nil {
+ return nil, fmt.Errorf("%w: project %q not found", ErrInvalidInput, alloc.ProjectID)
+ }
+
+ if cluster, err := s.clusters.FindByID(ctx, alloc.ComputeClusterID); err != nil {
+ return nil, fmt.Errorf("lookup compute cluster: %w", err)
+ } else if cluster == nil {
+ return nil, fmt.Errorf("%w: compute cluster %q not found", ErrInvalidInput, alloc.ComputeClusterID)
+ }
+
+ if err := s.inTx(ctx, func(tx *sql.Tx) error {
+ return s.allocs.Create(ctx, tx, alloc)
+ }); err != nil {
+ return nil, fmt.Errorf("create compute allocation: %w", err)
+ }
+ return alloc, nil
+}
+
+// GetComputeAllocation retrieves a compute allocation by its ID. Returns
+// ErrNotFound when no allocation matches.
+func (s *Service) GetComputeAllocation(ctx context.Context, id string) (*models.ComputeAllocation, error) {
+ a, err := s.allocs.FindByID(ctx, id)
+ if err != nil {
+ return nil, fmt.Errorf("get compute allocation: %w", err)
+ }
+ if a == nil {
+ return nil, ErrNotFound
+ }
+ return a, nil
+}
+
+// ListComputeAllocationsByProject returns every compute allocation attached to a project.
+func (s *Service) ListComputeAllocationsByProject(ctx context.Context, projectID string) ([]models.ComputeAllocation, error) {
+ allocs, err := s.allocs.FindByProject(ctx, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("list compute allocations by project: %w", err)
+ }
+ return allocs, nil
+}
+
+// ListComputeAllocationsByCluster returns every compute allocation attached to a cluster.
+func (s *Service) ListComputeAllocationsByCluster(ctx context.Context, clusterID string) ([]models.ComputeAllocation, error) {
+ allocs, err := s.allocs.FindByCluster(ctx, clusterID)
+ if err != nil {
+ return nil, fmt.Errorf("list compute allocations by cluster: %w", err)
+ }
+ return allocs, nil
+}
+
+// UpdateComputeAllocation persists changes to an existing compute allocation.
+func (s *Service) UpdateComputeAllocation(ctx context.Context, alloc *models.ComputeAllocation) error {
+ if alloc == nil || alloc.ID == "" {
+ return fmt.Errorf("%w: compute allocation id is required", ErrInvalidInput)
+ }
+ if err := s.inTx(ctx, func(tx *sql.Tx) error {
+ return s.allocs.Update(ctx, tx, alloc)
+ }); err != nil {
+ return fmt.Errorf("update compute allocation: %w", err)
+ }
+ return nil
+}
+
+// DeleteComputeAllocation removes a compute allocation by ID.
+func (s *Service) DeleteComputeAllocation(ctx context.Context, id string) error {
+ if id == "" {
+ return fmt.Errorf("%w: compute allocation id is required", ErrInvalidInput)
+ }
+ if err := s.inTx(ctx, func(tx *sql.Tx) error {
+ return s.allocs.Delete(ctx, tx, id)
+ }); err != nil {
+ return fmt.Errorf("delete compute allocation: %w", err)
+ }
+ return nil
+}
diff --git a/pkg/service/compute_allocation_resource.go b/pkg/service/compute_allocation_resource.go
new file mode 100644
index 000000000..433f019d5
--- /dev/null
+++ b/pkg/service/compute_allocation_resource.go
@@ -0,0 +1,98 @@
+// 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.
+
+package service
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+
+ "github.com/apache/airavata-custos/pkg/models"
+)
+
+// CreateComputeAllocationResource persists a new compute allocation resource.
+// If resource.ID is empty a new UUID is generated.
+func (s *Service) CreateComputeAllocationResource(ctx context.Context, resource *models.ComputeAllocationResource) (*models.ComputeAllocationResource, error) {
+ if resource == nil {
+ return nil, fmt.Errorf("%w: compute allocation resource is nil", ErrInvalidInput)
+ }
+ if resource.Name == "" {
+ return nil, fmt.Errorf("%w: resource name is required", ErrInvalidInput)
+ }
+ if resource.ResourceType == "" {
+ return nil, fmt.Errorf("%w: resource_type is required", ErrInvalidInput)
+ }
+ if resource.ID == "" {
+ resource.ID = newID()
+ }
+
+ if err := s.inTx(ctx, func(tx *sql.Tx) error {
+ return s.resources.Create(ctx, tx, resource)
+ }); err != nil {
+ return nil, fmt.Errorf("create compute allocation resource: %w", err)
+ }
+ return resource, nil
+}
+
+// GetComputeAllocationResource retrieves a compute allocation resource by its ID.
+// Returns ErrNotFound when no resource matches.
+func (s *Service) GetComputeAllocationResource(ctx context.Context, id string) (*models.ComputeAllocationResource, error) {
+ r, err := s.resources.FindByID(ctx, id)
+ if err != nil {
+ return nil, fmt.Errorf("get compute allocation resource: %w", err)
+ }
+ if r == nil {
+ return nil, ErrNotFound
+ }
+ return r, nil
+}
+
+// ListComputeAllocationResources returns every compute allocation resource.
+func (s *Service) ListComputeAllocationResources(ctx context.Context) ([]models.ComputeAllocationResource, error) {
+ resources, err := s.resources.List(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("list compute allocation resources: %w", err)
+ }
+ return resources, nil
+}
+
+// UpdateComputeAllocationResource persists changes to an existing resource.
+func (s *Service) UpdateComputeAllocationResource(ctx context.Context, resource *models.ComputeAllocationResource) error {
+ if resource == nil || resource.ID == "" {
+ return fmt.Errorf("%w: compute allocation resource id is required", ErrInvalidInput)
+ }
+ if err := s.inTx(ctx, func(tx *sql.Tx) error {
+ return s.resources.Update(ctx, tx, resource)
+ }); err != nil {
+ return fmt.Errorf("update compute allocation resource: %w", err)
+ }
+ return nil
+}
+
+// DeleteComputeAllocationResource removes a compute allocation resource by ID.
+func (s *Service) DeleteComputeAllocationResource(ctx context.Context, id string) error {
+ if id == "" {
+ return fmt.Errorf("%w: compute allocation resource id is required", ErrInvalidInput)
+ }
+ if err := s.inTx(ctx, func(tx *sql.Tx) error {
+ return s.resources.Delete(ctx, tx, id)
+ }); err != nil {
+ return fmt.Errorf("delete compute allocation resource: %w", err)
+ }
+ return nil
+}
diff --git a/pkg/service/compute_allocation_resource_mapping.go b/pkg/service/compute_allocation_resource_mapping.go
new file mode 100644
index 000000000..84c4b52e0
--- /dev/null
+++ b/pkg/service/compute_allocation_resource_mapping.go
@@ -0,0 +1,119 @@
+// 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.
+
+package service
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+
+ "github.com/apache/airavata-custos/pkg/models"
+)
+
+// 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) {
+ 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 alloc, err := s.allocs.FindByID(ctx, allocationID); err != nil {
+ return nil, fmt.Errorf("lookup compute allocation: %w", err)
+ } else if alloc == nil {
+ return nil, fmt.Errorf("%w: compute allocation %q not found", ErrInvalidInput, allocationID)
+ }
+
+ if res, err := s.resources.FindByID(ctx, resourceID); err != nil {
+ return nil, fmt.Errorf("lookup compute allocation resource: %w", err)
+ } else if res == nil {
+ return nil, fmt.Errorf("%w: compute allocation resource %q not found", ErrInvalidInput, resourceID)
+ }
+
+ if existing, err := s.resourceMappings.FindByPair(ctx, allocationID, resourceID); err != nil {
+ return nil, fmt.Errorf("lookup mapping: %w", err)
+ } else if existing != nil {
+ return nil, fmt.Errorf("%w: resource %q already attached to allocation %q", ErrAlreadyExists, resourceID, allocationID)
+ }
+
+ mapping := &models.ComputeAllocationResourceMapping{
+ ID: newID(),
+ ComputeAllocationID: allocationID,
+ ComputeAllocationResourceID: resourceID,
+ }
+ if err := s.inTx(ctx, func(tx *sql.Tx) error {
+ return s.resourceMappings.Create(ctx, tx, mapping)
+ }); err != nil {
+ return nil, fmt.Errorf("attach resource to allocation: %w", err)
+ }
+ return mapping, nil
+}
+
+// DetachResourceFromAllocation removes the link between a compute allocation
+// and a compute allocation resource. Returns ErrNotFound when no such mapping
+// exists.
+func (s *Service) DetachResourceFromAllocation(ctx context.Context, allocationID, resourceID string) error {
+ if allocationID == "" || resourceID == "" {
+ return fmt.Errorf("%w: allocation and resource ids are required", ErrInvalidInput)
+ }
+
+ existing, err := s.resourceMappings.FindByPair(ctx, allocationID, resourceID)
+ if err != nil {
+ return fmt.Errorf("lookup mapping: %w", err)
+ }
+ if existing == nil {
+ return ErrNotFound
+ }
+
+ if err := s.inTx(ctx, func(tx *sql.Tx) error {
+ return s.resourceMappings.DeleteByPair(ctx, tx, allocationID, resourceID)
+ }); err != nil {
+ return fmt.Errorf("detach resource from allocation: %w", err)
+ }
+ return nil
+}
+
+// ListResourcesForAllocation returns every compute allocation resource
+// attached to the given compute allocation.
+func (s *Service) ListResourcesForAllocation(ctx context.Context, allocationID string) ([]models.ComputeAllocationResource, error) {
+ if allocationID == "" {
+ return nil, fmt.Errorf("%w: compute_allocation_id is required", ErrInvalidInput)
+ }
+ resources, err := s.resourceMappings.FindResourcesByAllocation(ctx, allocationID)
+ if err != nil {
+ return nil, fmt.Errorf("list resources for allocation: %w", err)
+ }
+ return resources, nil
+}
+
+// ListAllocationsForResource returns every compute allocation that has the
+// given compute allocation resource attached.
+func (s *Service) ListAllocationsForResource(ctx context.Context, resourceID string) ([]models.ComputeAllocation, error) {
+ if resourceID == "" {
+ return nil, fmt.Errorf("%w: compute_allocation_resource_id is required", ErrInvalidInput)
+ }
+ allocs, err := s.resourceMappings.FindAllocationsByResource(ctx, resourceID)
+ if err != nil {
+ return nil, fmt.Errorf("list allocations for resource: %w", err)
+ }
+ return allocs, nil
+}
diff --git a/pkg/service/service.go b/pkg/service/service.go
index b5926858e..e249fbe7e 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -33,30 +33,54 @@ import (
// mutating operation in a transaction so callers do not need to manage
// *sql.Tx themselves.
type Service struct {
- db *sqlx.DB
- orgs store.OrganizationStore
- users store.UserStore
- projs store.ProjectStore
- clusters store.ComputeClusterStore
+ db *sqlx.DB
+ orgs store.OrganizationStore
+ users store.UserStore
+ projs store.ProjectStore
+ clusters store.ComputeClusterStore
+ allocs store.ComputeAllocationStore
+ resources store.ComputeAllocationResourceStore
+ resourceMappings store.ComputeAllocationResourceMappingStore
}
// New constructs a Service backed by the supplied database handle.
// Stores are instantiated internally using the default MySQL implementations.
func New(database *sqlx.DB) *Service {
return &Service{
- db: database,
- orgs: store.NewOrganizationStore(database),
- users: store.NewUserStore(database),
- projs: store.NewProjectStore(database),
- clusters: store.NewComputeClusterStore(database),
+ db: database,
+ orgs: store.NewOrganizationStore(database),
+ users: store.NewUserStore(database),
+ projs: store.NewProjectStore(database),
+ clusters: store.NewComputeClusterStore(database),
+ allocs: store.NewComputeAllocationStore(database),
+ resources: store.NewComputeAllocationResourceStore(database),
+ resourceMappings: store.NewComputeAllocationResourceMappingStore(database),
}
}
// NewWithStores constructs a Service from explicit stores. Useful for tests
// within this module — stores are an internal type and cannot be supplied by
// external callers.
-func NewWithStores(database *sqlx.DB, orgs store.OrganizationStore, users store.UserStore, projs store.ProjectStore, clusters store.ComputeClusterStore) *Service {
- return &Service{db: database, orgs: orgs, users: users, projs: projs, clusters: clusters}
+func NewWithStores(
+ database *sqlx.DB,
+ orgs store.OrganizationStore,
+ users store.UserStore,
+ projs store.ProjectStore,
+ clusters store.ComputeClusterStore,
+ allocs store.ComputeAllocationStore,
+ resources store.ComputeAllocationResourceStore,
+ resourceMappings store.ComputeAllocationResourceMappingStore,
+) *Service {
+ return &Service{
+ db: database,
+ orgs: orgs,
+ users: users,
+ projs: projs,
+ clusters: clusters,
+ allocs: allocs,
+ resources: resources,
+ resourceMappings: resourceMappings,
+ }
}
// inTx runs fn inside a database transaction managed by the Service.