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.
