| /** | |
| * | |
| * 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" | |
| "gorm.io/gorm" | |
| "github.com/apache/airavata/internal/auth" | |
| "github.com/apache/airavata/internal/httpx" | |
| dto "github.com/apache/airavata/api/process/dto" | |
| model "github.com/apache/airavata/api/process/model" | |
| "github.com/apache/airavata/api/process/repository" | |
| ) | |
| // TaskService manages one kind of task as a sub-resource of its process. | |
| // | |
| // The four kinds are the same service with a different payload — scope by process, | |
| // authorise against that process, order by task_order — so the rules are written once | |
| // and instantiated per kind. What differs is supplied at construction: how a request | |
| // is applied to a row, and what the kind is called in an error message. | |
| // | |
| // Unlike the rest of this package, reads here are owner-scoped. Process and status | |
| // reads are open for compatibility with clients the Java service already had; these | |
| // endpoints are new, and a task carries filesystem paths and shell commands, so there | |
| // is no reason to expose them more widely than the run they belong to. | |
| type TaskService[T any, Req any] struct { | |
| db *gorm.DB | |
| tasks *repository.TaskRepository[T] | |
| processes *repository.ProcessRepository | |
| // name is how the kind is named in a not-found message, e.g. "Data staging task". | |
| name string | |
| // apply copies a validated request onto a row. | |
| apply func(dst *T, src *Req) | |
| // stamp records which process a new row belongs to. | |
| stamp func(dst *T, processID string) | |
| } | |
| // ListForProcess returns every task of one process, in execution order. | |
| func (s *TaskService[T, Req]) ListForProcess(ctx context.Context, processID string) ([]T, error) { | |
| if _, err := s.requireProcess(ctx, processID); err != nil { | |
| return nil, err | |
| } | |
| return s.tasks.FindByProcessID(ctx, processID) | |
| } | |
| // Get returns one task of one process. | |
| func (s *TaskService[T, Req]) Get(ctx context.Context, processID, taskID string) (*T, error) { | |
| if _, err := s.requireProcess(ctx, processID); err != nil { | |
| return nil, err | |
| } | |
| return s.requireTask(ctx, s.tasks, processID, taskID) | |
| } | |
| // Create adds a task to a process. | |
| func (s *TaskService[T, Req]) Create(ctx context.Context, processID string, req *Req) (*T, error) { | |
| var out *T | |
| err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { | |
| if _, err := s.requireProcessTx(ctx, tx, processID); err != nil { | |
| return err | |
| } | |
| var task T | |
| s.stamp(&task, processID) | |
| s.apply(&task, req) | |
| if err := s.tasks.WithTx(tx).Save(ctx, &task); err != nil { | |
| return err | |
| } | |
| out = &task | |
| return nil | |
| }) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return out, nil | |
| } | |
| // Update changes a task of a process. | |
| // | |
| // The process it belongs to is left alone: both ids come from the path, so a task can | |
| // be edited but never moved to a different run. | |
| func (s *TaskService[T, Req]) Update(ctx context.Context, processID, taskID string, req *Req) (*T, error) { | |
| var out *T | |
| err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { | |
| if _, err := s.requireProcessTx(ctx, tx, processID); err != nil { | |
| return err | |
| } | |
| tasks := s.tasks.WithTx(tx) | |
| task, err := s.requireTask(ctx, tasks, processID, taskID) | |
| if err != nil { | |
| return err | |
| } | |
| s.apply(task, req) | |
| if err := tasks.Save(ctx, task); err != nil { | |
| return err | |
| } | |
| out = task | |
| return nil | |
| }) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return out, nil | |
| } | |
| // Delete removes a task from a process. | |
| func (s *TaskService[T, Req]) Delete(ctx context.Context, processID, taskID string) error { | |
| if _, err := s.requireProcess(ctx, processID); err != nil { | |
| return err | |
| } | |
| task, err := s.requireTask(ctx, s.tasks, processID, taskID) | |
| if err != nil { | |
| return err | |
| } | |
| return s.tasks.Delete(ctx, task) | |
| } | |
| // requireProcess loads the process and checks the caller may act on it: its owner, or | |
| // an admin. A process nobody can name is a 404; one that is not theirs is a 403. | |
| func (s *TaskService[T, Req]) requireProcess(ctx context.Context, processID string) (*model.Process, error) { | |
| return s.requireProcessWith(ctx, s.processes, processID) | |
| } | |
| func (s *TaskService[T, Req]) requireProcessTx(ctx context.Context, tx *gorm.DB, processID string) (*model.Process, error) { | |
| return s.requireProcessWith(ctx, s.processes.WithTx(tx), processID) | |
| } | |
| func (s *TaskService[T, Req]) requireProcessWith(ctx context.Context, processes *repository.ProcessRepository, processID string) (*model.Process, error) { | |
| principal, err := auth.RequireAuthenticated(ctx) | |
| if err != nil { | |
| return nil, err | |
| } | |
| proc, err := processes.FindByID(ctx, processID) | |
| if err != nil { | |
| return nil, notFoundAs(err, "Process not found: %s", processID) | |
| } | |
| if !proc.OwnedBy(principal.Name) && !principal.IsAdmin() { | |
| return nil, httpx.Forbidden("Access denied: you may only work with tasks of your own processes") | |
| } | |
| return proc, nil | |
| } | |
| func (s *TaskService[T, Req]) requireTask(ctx context.Context, tasks *repository.TaskRepository[T], processID, taskID string) (*T, error) { | |
| task, err := tasks.FindByIDAndProcessID(ctx, taskID, processID) | |
| if err != nil { | |
| return nil, notFoundAs(err, "%s not found: %s in process %s", s.name, taskID, processID) | |
| } | |
| return task, nil | |
| } | |
| // The four instantiations. | |
| type ( | |
| DataStagingTaskService = TaskService[model.DataStagingTask, dto.DataStagingTaskRequest] | |
| JobSubmissionTaskService = TaskService[model.JobSubmissionTask, dto.JobSubmissionTaskRequest] | |
| JobMonitoringTaskService = TaskService[model.JobMonitoringTask, dto.JobMonitoringTaskRequest] | |
| InteractiveCommandTaskService = TaskService[model.InteractiveCommandTask, dto.InteractiveCommandTaskRequest] | |
| ) | |
| // NewDataStagingTaskService returns a staging task service. | |
| func NewDataStagingTaskService(db *gorm.DB, tasks *repository.DataStagingTaskRepository, processes *repository.ProcessRepository) *DataStagingTaskService { | |
| return &DataStagingTaskService{ | |
| db: db, tasks: tasks, processes: processes, | |
| name: "Data staging task", | |
| apply: dto.ApplyDataStagingTaskRequest, | |
| stamp: func(t *model.DataStagingTask, processID string) { t.ProcessID = &processID }, | |
| } | |
| } | |
| // NewJobSubmissionTaskService returns a submission task service. | |
| func NewJobSubmissionTaskService(db *gorm.DB, tasks *repository.JobSubmissionTaskRepository, processes *repository.ProcessRepository) *JobSubmissionTaskService { | |
| return &JobSubmissionTaskService{ | |
| db: db, tasks: tasks, processes: processes, | |
| name: "Job submission task", | |
| apply: dto.ApplyJobSubmissionTaskRequest, | |
| stamp: func(t *model.JobSubmissionTask, processID string) { t.ProcessID = &processID }, | |
| } | |
| } | |
| // NewJobMonitoringTaskService returns a monitoring task service. | |
| func NewJobMonitoringTaskService(db *gorm.DB, tasks *repository.JobMonitoringTaskRepository, processes *repository.ProcessRepository) *JobMonitoringTaskService { | |
| return &JobMonitoringTaskService{ | |
| db: db, tasks: tasks, processes: processes, | |
| name: "Job monitoring task", | |
| apply: dto.ApplyJobMonitoringTaskRequest, | |
| stamp: func(t *model.JobMonitoringTask, processID string) { t.ProcessID = &processID }, | |
| } | |
| } | |
| // NewInteractiveCommandTaskService returns a command task service. | |
| func NewInteractiveCommandTaskService(db *gorm.DB, tasks *repository.InteractiveCommandTaskRepository, processes *repository.ProcessRepository) *InteractiveCommandTaskService { | |
| return &InteractiveCommandTaskService{ | |
| db: db, tasks: tasks, processes: processes, | |
| name: "Interactive command task", | |
| apply: dto.ApplyInteractiveCommandTaskRequest, | |
| stamp: func(t *model.InteractiveCommandTask, processID string) { t.ProcessID = &processID }, | |
| } | |
| } |