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 <[email protected]>
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.

Reply via email to