blob: 81956f5318a885761befe1f955740afb25c37206 [file]
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 },
}
}