diff --git a/internal/app/app.go b/internal/app/app.go index fca0b69..d8d11f5 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -21,7 +21,6 @@ import ( "gorm.io/gorm" ) -// App encapsulates the entire application lifecycle. type App struct { cfg *config.Config logger *zap.Logger @@ -142,13 +141,14 @@ func initSlurmClient(cfg *config.Config) (*slurm.Client, error) { func initHTTPServer(cfg *config.Config, db *gorm.DB, slurmClient *slurm.Client, logger *zap.Logger) *http.Server { jobSvc := service.NewJobService(slurmClient, logger) clusterSvc := service.NewClusterService(slurmClient, logger) - templateStore := store.NewTemplateStore(db) - jobH := handler.NewJobHandler(jobSvc, logger) clusterH := handler.NewClusterHandler(clusterSvc, logger) - templateH := handler.NewTemplateHandler(templateStore, logger) - router := server.NewRouter(jobH, clusterH, templateH, logger) + appStore := store.NewApplicationStore(db) + appSvc := service.NewApplicationService(appStore, jobSvc, cfg.WorkDirBase, logger) + appH := handler.NewApplicationHandler(appSvc, logger) + + router := server.NewRouter(jobH, clusterH, appH, logger) addr := ":" + cfg.ServerPort diff --git a/internal/handler/template.go b/internal/handler/template.go deleted file mode 100644 index 8784d70..0000000 --- a/internal/handler/template.go +++ /dev/null @@ -1,146 +0,0 @@ -package handler - -import ( - "errors" - "strconv" - - "gcy_hpc_server/internal/model" - "gcy_hpc_server/internal/server" - "gcy_hpc_server/internal/store" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" - "gorm.io/gorm" -) - -type TemplateHandler struct { - store *store.TemplateStore - logger *zap.Logger -} - -func NewTemplateHandler(s *store.TemplateStore, logger *zap.Logger) *TemplateHandler { - return &TemplateHandler{store: s, logger: logger} -} - -func (h *TemplateHandler) ListTemplates(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - - if page < 1 { - page = 1 - } - if pageSize < 1 { - pageSize = 20 - } - - templates, total, err := h.store.List(c.Request.Context(), page, pageSize) - if err != nil { - h.logger.Error("failed to list templates", zap.Error(err)) - server.InternalError(c, err.Error()) - return - } - - server.OK(c, gin.H{ - "templates": templates, - "total": total, - "page": page, - "page_size": pageSize, - }) -} - -func (h *TemplateHandler) CreateTemplate(c *gin.Context) { - var req model.CreateTemplateRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Warn("invalid request body for create template", zap.Error(err)) - server.BadRequest(c, "invalid request body") - return - } - - if req.Name == "" || req.Script == "" { - h.logger.Warn("missing required fields for create template") - server.BadRequest(c, "name and script are required") - return - } - - id, err := h.store.Create(c.Request.Context(), &req) - if err != nil { - h.logger.Error("failed to create template", zap.Error(err)) - server.InternalError(c, err.Error()) - return - } - - h.logger.Info("template created", zap.Int64("id", id)) - server.Created(c, gin.H{"id": id}) -} - -func (h *TemplateHandler) GetTemplate(c *gin.Context) { - id, err := strconv.ParseInt(c.Param("id"), 10, 64) - if err != nil { - h.logger.Warn("invalid template id", zap.String("id", c.Param("id"))) - server.BadRequest(c, "invalid id") - return - } - - tmpl, err := h.store.GetByID(c.Request.Context(), id) - if err != nil { - h.logger.Error("failed to get template", zap.Int64("id", id), zap.Error(err)) - server.InternalError(c, err.Error()) - return - } - - if tmpl == nil { - h.logger.Warn("template not found", zap.Int64("id", id)) - server.NotFound(c, "template not found") - return - } - - server.OK(c, tmpl) -} - -func (h *TemplateHandler) UpdateTemplate(c *gin.Context) { - id, err := strconv.ParseInt(c.Param("id"), 10, 64) - if err != nil { - h.logger.Warn("invalid template id for update", zap.String("id", c.Param("id"))) - server.BadRequest(c, "invalid id") - return - } - - var req model.UpdateTemplateRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.logger.Warn("invalid request body for update template", zap.Int64("id", id), zap.Error(err)) - server.BadRequest(c, "invalid request body") - return - } - - if err := h.store.Update(c.Request.Context(), id, &req); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - h.logger.Warn("template not found for update", zap.Int64("id", id)) - server.NotFound(c, "template not found") - return - } - h.logger.Error("failed to update template", zap.Int64("id", id), zap.Error(err)) - server.InternalError(c, "failed to update template") - return - } - - h.logger.Info("template updated", zap.Int64("id", id)) - server.OK(c, gin.H{"message": "template updated"}) -} - -func (h *TemplateHandler) DeleteTemplate(c *gin.Context) { - id, err := strconv.ParseInt(c.Param("id"), 10, 64) - if err != nil { - h.logger.Warn("invalid template id for delete", zap.String("id", c.Param("id"))) - server.BadRequest(c, "invalid id") - return - } - - if err := h.store.Delete(c.Request.Context(), id); err != nil { - h.logger.Error("failed to delete template", zap.Int64("id", id), zap.Error(err)) - server.InternalError(c, err.Error()) - return - } - - h.logger.Info("template deleted", zap.Int64("id", id)) - server.OK(c, gin.H{"message": "template deleted"}) -} diff --git a/internal/handler/template_test.go b/internal/handler/template_test.go deleted file mode 100644 index 79951fc..0000000 --- a/internal/handler/template_test.go +++ /dev/null @@ -1,387 +0,0 @@ -package handler - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "gcy_hpc_server/internal/model" - "gcy_hpc_server/internal/server" - "gcy_hpc_server/internal/store" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "go.uber.org/zap/zaptest/observer" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -func setupTemplateHandler() (*TemplateHandler, *gorm.DB) { - db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) - db.AutoMigrate(&model.JobTemplate{}) - s := store.NewTemplateStore(db) - h := NewTemplateHandler(s, zap.NewNop()) - return h, db -} - -func setupTemplateRouter(h *TemplateHandler) *gin.Engine { - gin.SetMode(gin.TestMode) - r := gin.New() - v1 := r.Group("/api/v1") - templates := v1.Group("/templates") - templates.GET("", h.ListTemplates) - templates.POST("", h.CreateTemplate) - templates.GET("/:id", h.GetTemplate) - templates.PUT("/:id", h.UpdateTemplate) - templates.DELETE("/:id", h.DeleteTemplate) - return r -} - -func TestListTemplates_Success(t *testing.T) { - h, _ := setupTemplateHandler() - r := setupTemplateRouter(h) - - // Seed data - h.store.Create(context.Background(), &model.CreateTemplateRequest{Name: "test-tpl", Script: "echo hi", Partition: "normal", QOS: "high", CPUs: 4, Memory: "4GB"}) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/templates", nil) - r.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) - } - var resp server.APIResponse - json.Unmarshal(w.Body.Bytes(), &resp) - if !resp.Success { - t.Fatalf("expected success=true, got: %s", w.Body.String()) - } -} - -func TestCreateTemplate_Success(t *testing.T) { - h, _ := setupTemplateHandler() - r := setupTemplateRouter(h) - - body := `{"name":"my-tpl","description":"desc","script":"echo hello","partition":"gpu"}` - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/v1/templates", bytes.NewBufferString(body)) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - if w.Code != http.StatusCreated { - t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) - } - var resp server.APIResponse - json.Unmarshal(w.Body.Bytes(), &resp) - if !resp.Success { - t.Fatalf("expected success=true, got: %s", w.Body.String()) - } -} - -func TestCreateTemplate_MissingFields(t *testing.T) { - h, _ := setupTemplateHandler() - r := setupTemplateRouter(h) - - body := `{"name":"","script":""}` - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/v1/templates", bytes.NewBufferString(body)) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - if w.Code != http.StatusBadRequest { - t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) - } -} - -func TestGetTemplate_Success(t *testing.T) { - h, _ := setupTemplateHandler() - r := setupTemplateRouter(h) - - // Seed data - id, _ := h.store.Create(context.Background(), &model.CreateTemplateRequest{Name: "test-tpl", Script: "echo hi", Partition: "normal"}) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/templates/"+itoa(id), nil) - r.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) - } - var resp server.APIResponse - json.Unmarshal(w.Body.Bytes(), &resp) - if !resp.Success { - t.Fatalf("expected success=true, got: %s", w.Body.String()) - } -} - -func TestGetTemplate_NotFound(t *testing.T) { - h, _ := setupTemplateHandler() - r := setupTemplateRouter(h) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/templates/999", nil) - r.ServeHTTP(w, req) - - if w.Code != http.StatusNotFound { - t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String()) - } -} - -func TestGetTemplate_InvalidID(t *testing.T) { - h, _ := setupTemplateHandler() - r := setupTemplateRouter(h) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/templates/abc", nil) - r.ServeHTTP(w, req) - - if w.Code != http.StatusBadRequest { - t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) - } -} - -func TestUpdateTemplate_Success(t *testing.T) { - h, _ := setupTemplateHandler() - r := setupTemplateRouter(h) - - // Seed data - id, _ := h.store.Create(context.Background(), &model.CreateTemplateRequest{Name: "old", Script: "echo hi"}) - - body := `{"name":"updated"}` - w := httptest.NewRecorder() - req, _ := http.NewRequest("PUT", "/api/v1/templates/"+itoa(id), bytes.NewBufferString(body)) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) - } - var resp server.APIResponse - json.Unmarshal(w.Body.Bytes(), &resp) - if !resp.Success { - t.Fatalf("expected success=true, got: %s", w.Body.String()) - } -} - -func TestDeleteTemplate_Success(t *testing.T) { - h, _ := setupTemplateHandler() - r := setupTemplateRouter(h) - - // Seed data - id, _ := h.store.Create(context.Background(), &model.CreateTemplateRequest{Name: "to-delete", Script: "echo hi"}) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("DELETE", "/api/v1/templates/"+itoa(id), nil) - r.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) - } - var resp server.APIResponse - json.Unmarshal(w.Body.Bytes(), &resp) - if !resp.Success { - t.Fatalf("expected success=true, got: %s", w.Body.String()) - } -} - -// itoa converts int64 to string for URL path construction. -func itoa(id int64) string { - return fmt.Sprintf("%d", id) -} - -func setupTemplateHandlerWithObserver() (*TemplateHandler, *gorm.DB, *observer.ObservedLogs) { - core, recorded := observer.New(zapcore.DebugLevel) - l := zap.New(core) - db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) - db.AutoMigrate(&model.JobTemplate{}) - s := store.NewTemplateStore(db) - return NewTemplateHandler(s, l), db, recorded -} - -func TestTemplateLogging_CreateSuccess_LogsInfoWithID(t *testing.T) { - h, _, recorded := setupTemplateHandlerWithObserver() - r := setupTemplateRouter(h) - - body := `{"name":"log-tpl","script":"echo hi"}` - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/v1/templates", bytes.NewBufferString(body)) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - if w.Code != http.StatusCreated { - t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) - } - - entries := recorded.FilterMessage("template created").FilterLevelExact(zapcore.InfoLevel).All() - if len(entries) != 1 { - t.Fatalf("expected 1 info log for 'template created', got %d", len(entries)) - } - fields := entries[0].ContextMap() - if fields["id"] == nil { - t.Fatal("expected log entry to contain 'id' field") - } -} - -func TestTemplateLogging_UpdateSuccess_LogsInfoWithID(t *testing.T) { - h, _, recorded := setupTemplateHandlerWithObserver() - r := setupTemplateRouter(h) - - id, _ := h.store.Create(context.Background(), &model.CreateTemplateRequest{Name: "old", Script: "echo hi"}) - - body := `{"name":"updated"}` - w := httptest.NewRecorder() - req, _ := http.NewRequest("PUT", "/api/v1/templates/"+itoa(id), bytes.NewBufferString(body)) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) - } - - entries := recorded.FilterMessage("template updated").FilterLevelExact(zapcore.InfoLevel).All() - if len(entries) != 1 { - t.Fatalf("expected 1 info log for 'template updated', got %d", len(entries)) - } - fields := entries[0].ContextMap() - if fields["id"] == nil { - t.Fatal("expected log entry to contain 'id' field") - } -} - -func TestTemplateLogging_DeleteSuccess_LogsInfoWithID(t *testing.T) { - h, _, recorded := setupTemplateHandlerWithObserver() - r := setupTemplateRouter(h) - - id, _ := h.store.Create(context.Background(), &model.CreateTemplateRequest{Name: "to-delete", Script: "echo hi"}) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("DELETE", "/api/v1/templates/"+itoa(id), nil) - r.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) - } - - entries := recorded.FilterMessage("template deleted").FilterLevelExact(zapcore.InfoLevel).All() - if len(entries) != 1 { - t.Fatalf("expected 1 info log for 'template deleted', got %d", len(entries)) - } - fields := entries[0].ContextMap() - if fields["id"] == nil { - t.Fatal("expected log entry to contain 'id' field") - } -} - -func TestTemplateLogging_GetNotFound_LogsWarnWithID(t *testing.T) { - h, _, recorded := setupTemplateHandlerWithObserver() - r := setupTemplateRouter(h) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/templates/999", nil) - r.ServeHTTP(w, req) - - if w.Code != http.StatusNotFound { - t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String()) - } - - entries := recorded.FilterMessage("template not found").FilterLevelExact(zapcore.WarnLevel).All() - if len(entries) != 1 { - t.Fatalf("expected 1 warn log for 'template not found', got %d", len(entries)) - } - fields := entries[0].ContextMap() - if fields["id"] == nil { - t.Fatal("expected log entry to contain 'id' field") - } -} - -func TestTemplateLogging_CreateBadRequest_LogsWarn(t *testing.T) { - h, _, recorded := setupTemplateHandlerWithObserver() - r := setupTemplateRouter(h) - - body := `{"name":"","script":""}` - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/v1/templates", bytes.NewBufferString(body)) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - if w.Code != http.StatusBadRequest { - t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) - } - - warnEntries := recorded.FilterLevelExact(zapcore.WarnLevel).All() - if len(warnEntries) == 0 { - t.Fatal("expected at least 1 warn log for bad request") - } -} - -func TestTemplateLogging_InvalidID_LogsWarn(t *testing.T) { - h, _, recorded := setupTemplateHandlerWithObserver() - r := setupTemplateRouter(h) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/templates/abc", nil) - r.ServeHTTP(w, req) - - if w.Code != http.StatusBadRequest { - t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) - } - - warnEntries := recorded.FilterLevelExact(zapcore.WarnLevel).All() - if len(warnEntries) == 0 { - t.Fatal("expected at least 1 warn log for invalid id") - } -} - -func TestTemplateLogging_ListSuccess_NoInfoLog(t *testing.T) { - h, _, recorded := setupTemplateHandlerWithObserver() - r := setupTemplateRouter(h) - - // Seed data - h.store.Create(context.Background(), &model.CreateTemplateRequest{Name: "test-tpl", Script: "echo hi"}) - - // Reset recorded logs so the create log doesn't interfere - recorded.TakeAll() - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/templates", nil) - r.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) - } - - infoEntries := recorded.FilterLevelExact(zapcore.InfoLevel).All() - if len(infoEntries) != 0 { - t.Fatalf("expected 0 info logs for list success, got %d: %+v", len(infoEntries), infoEntries) - } -} - -func TestTemplateLogging_LogsDoNotContainTemplateContent(t *testing.T) { - h, _, recorded := setupTemplateHandlerWithObserver() - r := setupTemplateRouter(h) - - body := `{"name":"secret-name","script":"secret-script","partition":"secret-partition"}` - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/v1/templates", bytes.NewBufferString(body)) - req.Header.Set("Content-Type", "application/json") - r.ServeHTTP(w, req) - - if w.Code != http.StatusCreated { - t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) - } - - entries := recorded.All() - for _, e := range entries { - logStr := e.Message + " " + fmt.Sprintf("%v", e.ContextMap()) - if strings.Contains(logStr, "secret-name") || strings.Contains(logStr, "secret-script") || strings.Contains(logStr, "secret-partition") { - t.Fatalf("log entry contains sensitive template content: %s", logStr) - } - } -} diff --git a/internal/model/template.go b/internal/model/template.go deleted file mode 100644 index 496fac0..0000000 --- a/internal/model/template.go +++ /dev/null @@ -1,45 +0,0 @@ -package model - -import "time" - -// JobTemplate represents a saved job template. -type JobTemplate struct { - ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` // 模板 ID - Name string `json:"name" gorm:"uniqueIndex;size:255;not null"` // 模板名称 - Description string `json:"description,omitempty" gorm:"type:text"` // 模板描述 - Script string `json:"script" gorm:"type:text;not null"` // 作业脚本内容 - Partition string `json:"partition,omitempty" gorm:"size:255"` // 提交到的分区 - QOS string `json:"qos,omitempty" gorm:"column:qos;size:255"` // 使用的 QOS 策略 - CPUs int `json:"cpus,omitempty" gorm:"column:cpus"` // 请求的 CPU 核数 - Memory string `json:"memory,omitempty" gorm:"size:50"` // 请求的内存大小 - TimeLimit string `json:"time_limit,omitempty" gorm:"column:time_limit;size:50"` // 运行时间限制 - CreatedAt time.Time `json:"created_at"` // 创建时间 - UpdatedAt time.Time `json:"updated_at"` // 更新时间 -} - -// TableName specifies the database table name for GORM. -func (JobTemplate) TableName() string { return "job_templates" } - -// CreateTemplateRequest is the API request for creating a template. -type CreateTemplateRequest struct { - Name string `json:"name"` // 模板名称 (必填) - Description string `json:"description,omitempty"` // 模板描述 - Script string `json:"script"` // 作业脚本内容 (必填) - Partition string `json:"partition,omitempty"` // 提交到的分区 - QOS string `json:"qos,omitempty"` // 使用的 QOS 策略 - CPUs int `json:"cpus,omitempty"` // 请求的 CPU 核数 - Memory string `json:"memory,omitempty"` // 请求的内存大小 - TimeLimit string `json:"time_limit,omitempty"` // 运行时间限制 -} - -// UpdateTemplateRequest is the API request for updating a template. -type UpdateTemplateRequest struct { - Name string `json:"name,omitempty"` // 模板名称 - Description string `json:"description,omitempty"` // 模板描述 - Script string `json:"script,omitempty"` // 作业脚本内容 - Partition string `json:"partition,omitempty"` // 提交到的分区 - QOS string `json:"qos,omitempty"` // 使用的 QOS 策略 - CPUs int `json:"cpus,omitempty"` // 请求的 CPU 核数 - Memory string `json:"memory,omitempty"` // 请求的内存大小 - TimeLimit string `json:"time_limit,omitempty"` // 运行时间限制 -} diff --git a/internal/server/server.go b/internal/server/server.go index f6fac7f..8a3331c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -25,16 +25,17 @@ type ClusterHandler interface { GetDiag(c *gin.Context) } -type TemplateHandler interface { - ListTemplates(c *gin.Context) - CreateTemplate(c *gin.Context) - GetTemplate(c *gin.Context) - UpdateTemplate(c *gin.Context) - DeleteTemplate(c *gin.Context) +type ApplicationHandler interface { + ListApplications(c *gin.Context) + CreateApplication(c *gin.Context) + GetApplication(c *gin.Context) + UpdateApplication(c *gin.Context) + DeleteApplication(c *gin.Context) + SubmitApplication(c *gin.Context) } // NewRouter creates a Gin engine with all API v1 routes registered with real handlers. -func NewRouter(jobH JobHandler, clusterH ClusterHandler, templateH TemplateHandler, logger *zap.Logger) *gin.Engine { +func NewRouter(jobH JobHandler, clusterH ClusterHandler, appH ApplicationHandler, logger *zap.Logger) *gin.Engine { gin.SetMode(gin.ReleaseMode) r := gin.New() r.Use(gin.Recovery()) @@ -59,12 +60,13 @@ func NewRouter(jobH JobHandler, clusterH ClusterHandler, templateH TemplateHandl v1.GET("/diag", clusterH.GetDiag) - templates := v1.Group("/templates") - templates.GET("", templateH.ListTemplates) - templates.POST("", templateH.CreateTemplate) - templates.GET("/:id", templateH.GetTemplate) - templates.PUT("/:id", templateH.UpdateTemplate) - templates.DELETE("/:id", templateH.DeleteTemplate) + apps := v1.Group("/applications") + apps.GET("", appH.ListApplications) + apps.POST("", appH.CreateApplication) + apps.GET("/:id", appH.GetApplication) + apps.PUT("/:id", appH.UpdateApplication) + apps.DELETE("/:id", appH.DeleteApplication) + apps.POST("/:id/submit", appH.SubmitApplication) return r } @@ -95,12 +97,13 @@ func registerPlaceholderRoutes(v1 *gin.RouterGroup) { v1.GET("/diag", notImplemented) - templates := v1.Group("/templates") - templates.GET("", notImplemented) - templates.POST("", notImplemented) - templates.GET("/:id", notImplemented) - templates.PUT("/:id", notImplemented) - templates.DELETE("/:id", notImplemented) + apps := v1.Group("/applications") + apps.GET("", notImplemented) + apps.POST("", notImplemented) + apps.GET("/:id", notImplemented) + apps.PUT("/:id", notImplemented) + apps.DELETE("/:id", notImplemented) + apps.POST("/:id/submit", notImplemented) } func notImplemented(c *gin.Context) { diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 8bd8937..4cc5787 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -27,11 +27,12 @@ func TestAllRoutesRegistered(t *testing.T) { {"GET", "/api/v1/partitions"}, {"GET", "/api/v1/partitions/:name"}, {"GET", "/api/v1/diag"}, - {"GET", "/api/v1/templates"}, - {"POST", "/api/v1/templates"}, - {"GET", "/api/v1/templates/:id"}, - {"PUT", "/api/v1/templates/:id"}, - {"DELETE", "/api/v1/templates/:id"}, + {"GET", "/api/v1/applications"}, + {"POST", "/api/v1/applications"}, + {"GET", "/api/v1/applications/:id"}, + {"PUT", "/api/v1/applications/:id"}, + {"DELETE", "/api/v1/applications/:id"}, + {"POST", "/api/v1/applications/:id/submit"}, } routeMap := map[string]bool{} @@ -74,7 +75,7 @@ func TestRegisteredPathReturns501(t *testing.T) { {"GET", "/api/v1/nodes"}, {"GET", "/api/v1/partitions"}, {"GET", "/api/v1/diag"}, - {"GET", "/api/v1/templates"}, + {"GET", "/api/v1/applications"}, } for _, ep := range endpoints { diff --git a/internal/store/migrations/001_create_job_templates.down.sql b/internal/store/migrations/001_create_job_templates.down.sql deleted file mode 100644 index 9626210..0000000 --- a/internal/store/migrations/001_create_job_templates.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS job_templates; diff --git a/internal/store/migrations/001_create_job_templates.up.sql b/internal/store/migrations/001_create_job_templates.up.sql deleted file mode 100644 index 128ff1d..0000000 --- a/internal/store/migrations/001_create_job_templates.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE IF NOT EXISTS job_templates ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - description TEXT, - script TEXT NOT NULL, - partition VARCHAR(255), - qos VARCHAR(255), - cpus INT UNSIGNED, - memory VARCHAR(50), - time_limit VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY idx_name (name) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/internal/store/mysql.go b/internal/store/mysql.go index 2fe19f8..af0d378 100644 --- a/internal/store/mysql.go +++ b/internal/store/mysql.go @@ -40,5 +40,5 @@ func NewGormDB(dsn string, zapLogger *zap.Logger, gormLevel string) (*gorm.DB, e // AutoMigrate runs GORM auto-migration for all models. func AutoMigrate(db *gorm.DB) error { - return db.AutoMigrate(&model.JobTemplate{}) + return db.AutoMigrate(&model.Application{}) } diff --git a/internal/store/template_store.go b/internal/store/template_store.go deleted file mode 100644 index c5f0fa6..0000000 --- a/internal/store/template_store.go +++ /dev/null @@ -1,119 +0,0 @@ -package store - -import ( - "context" - "errors" - - "gorm.io/gorm" - - "gcy_hpc_server/internal/model" -) - -// TemplateStore provides CRUD operations for job templates via GORM. -type TemplateStore struct { - db *gorm.DB -} - -// NewTemplateStore creates a new TemplateStore. -func NewTemplateStore(db *gorm.DB) *TemplateStore { - return &TemplateStore{db: db} -} - -// List returns a paginated list of job templates and the total count. -func (s *TemplateStore) List(ctx context.Context, page, pageSize int) ([]model.JobTemplate, int, error) { - var templates []model.JobTemplate - var total int64 - - if err := s.db.WithContext(ctx).Model(&model.JobTemplate{}).Count(&total).Error; err != nil { - return nil, 0, err - } - - offset := (page - 1) * pageSize - if err := s.db.WithContext(ctx).Order("id DESC").Limit(pageSize).Offset(offset).Find(&templates).Error; err != nil { - return nil, 0, err - } - - return templates, int(total), nil -} - -// GetByID returns a single job template by ID. Returns nil, nil when not found. -func (s *TemplateStore) GetByID(ctx context.Context, id int64) (*model.JobTemplate, error) { - var t model.JobTemplate - err := s.db.WithContext(ctx).First(&t, id).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - if err != nil { - return nil, err - } - return &t, nil -} - -// Create inserts a new job template and returns the generated ID. -func (s *TemplateStore) Create(ctx context.Context, req *model.CreateTemplateRequest) (int64, error) { - t := &model.JobTemplate{ - Name: req.Name, - Description: req.Description, - Script: req.Script, - Partition: req.Partition, - QOS: req.QOS, - CPUs: req.CPUs, - Memory: req.Memory, - TimeLimit: req.TimeLimit, - } - if err := s.db.WithContext(ctx).Create(t).Error; err != nil { - return 0, err - } - return t.ID, nil -} - -// Update modifies an existing job template. Only non-empty/non-zero fields are updated. -func (s *TemplateStore) Update(ctx context.Context, id int64, req *model.UpdateTemplateRequest) error { - updates := map[string]interface{}{} - if req.Name != "" { - updates["name"] = req.Name - } - if req.Description != "" { - updates["description"] = req.Description - } - if req.Script != "" { - updates["script"] = req.Script - } - if req.Partition != "" { - updates["partition"] = req.Partition - } - if req.QOS != "" { - updates["qos"] = req.QOS - } - if req.CPUs > 0 { - updates["cpus"] = req.CPUs - } - if req.Memory != "" { - updates["memory"] = req.Memory - } - if req.TimeLimit != "" { - updates["time_limit"] = req.TimeLimit - } - - if len(updates) == 0 { - return nil // nothing to update - } - - result := s.db.WithContext(ctx).Model(&model.JobTemplate{}).Where("id = ?", id).Updates(updates) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound - } - return nil -} - -// Delete removes a job template by ID. Idempotent — returns nil even if the row doesn't exist. -func (s *TemplateStore) Delete(ctx context.Context, id int64) error { - result := s.db.WithContext(ctx).Delete(&model.JobTemplate{}, id) - if result.Error != nil { - return result.Error - } - return nil -} diff --git a/internal/store/template_store_test.go b/internal/store/template_store_test.go deleted file mode 100644 index d0f9703..0000000 --- a/internal/store/template_store_test.go +++ /dev/null @@ -1,205 +0,0 @@ -package store - -import ( - "context" - "testing" - - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" - - "gcy_hpc_server/internal/model" -) - -func newTestDB(t *testing.T) *gorm.DB { - t.Helper() - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - }) - if err != nil { - t.Fatalf("open sqlite: %v", err) - } - if err := db.AutoMigrate(&model.JobTemplate{}); err != nil { - t.Fatalf("auto migrate: %v", err) - } - return db -} - -func TestTemplateStore_List(t *testing.T) { - db := newTestDB(t) - s := NewTemplateStore(db) - - s.Create(context.Background(), &model.CreateTemplateRequest{Name: "job-1", Script: "echo 1"}) - s.Create(context.Background(), &model.CreateTemplateRequest{Name: "job-2", Script: "echo 2"}) - - templates, total, err := s.List(context.Background(), 1, 10) - if err != nil { - t.Fatalf("List() error = %v", err) - } - if total != 2 { - t.Errorf("total = %d, want 2", total) - } - if len(templates) != 2 { - t.Fatalf("len(templates) = %d, want 2", len(templates)) - } - // DESC order, so job-2 is first - if templates[0].Name != "job-2" { - t.Errorf("templates[0].Name = %q, want %q", templates[0].Name, "job-2") - } -} - -func TestTemplateStore_List_Page2(t *testing.T) { - db := newTestDB(t) - s := NewTemplateStore(db) - - for i := 0; i < 15; i++ { - s.Create(context.Background(), &model.CreateTemplateRequest{ - Name: "job-" + string(rune('A'+i)), Script: "echo", - }) - } - - templates, total, err := s.List(context.Background(), 2, 10) - if err != nil { - t.Fatalf("List() error = %v", err) - } - if total != 15 { - t.Errorf("total = %d, want 15", total) - } - if len(templates) != 5 { - t.Fatalf("len(templates) = %d, want 5", len(templates)) - } -} - -func TestTemplateStore_GetByID(t *testing.T) { - db := newTestDB(t) - s := NewTemplateStore(db) - - id, _ := s.Create(context.Background(), &model.CreateTemplateRequest{ - Name: "test-job", Script: "echo hi", Partition: "batch", QOS: "normal", CPUs: 2, Memory: "4G", - }) - - tpl, err := s.GetByID(context.Background(), id) - if err != nil { - t.Fatalf("GetByID() error = %v", err) - } - if tpl == nil { - t.Fatal("GetByID() returned nil") - } - if tpl.Name != "test-job" { - t.Errorf("Name = %q, want %q", tpl.Name, "test-job") - } - if tpl.CPUs != 2 { - t.Errorf("CPUs = %d, want 2", tpl.CPUs) - } -} - -func TestTemplateStore_GetByID_NotFound(t *testing.T) { - db := newTestDB(t) - s := NewTemplateStore(db) - - tpl, err := s.GetByID(context.Background(), 999) - if err != nil { - t.Fatalf("GetByID() error = %v, want nil", err) - } - if tpl != nil { - t.Fatal("GetByID() should return nil for not found") - } -} - -func TestTemplateStore_Create(t *testing.T) { - db := newTestDB(t) - s := NewTemplateStore(db) - - id, err := s.Create(context.Background(), &model.CreateTemplateRequest{ - Name: "new-job", Script: "echo", Partition: "gpu", - }) - if err != nil { - t.Fatalf("Create() error = %v", err) - } - if id == 0 { - t.Fatal("Create() returned id=0") - } -} - -func TestTemplateStore_Update(t *testing.T) { - db := newTestDB(t) - s := NewTemplateStore(db) - - id, _ := s.Create(context.Background(), &model.CreateTemplateRequest{ - Name: "old", Script: "echo", - }) - - err := s.Update(context.Background(), id, &model.UpdateTemplateRequest{ - Name: "updated", - Script: "echo new", - CPUs: 8, - }) - if err != nil { - t.Fatalf("Update() error = %v", err) - } - - tpl, _ := s.GetByID(context.Background(), id) - if tpl.Name != "updated" { - t.Errorf("Name = %q, want %q", tpl.Name, "updated") - } - if tpl.CPUs != 8 { - t.Errorf("CPUs = %d, want 8", tpl.CPUs) - } -} - -func TestTemplateStore_Update_Partial(t *testing.T) { - db := newTestDB(t) - s := NewTemplateStore(db) - - id, _ := s.Create(context.Background(), &model.CreateTemplateRequest{ - Name: "original", Script: "echo orig", Partition: "batch", - }) - - err := s.Update(context.Background(), id, &model.UpdateTemplateRequest{ - Name: "renamed", - }) - if err != nil { - t.Fatalf("Update() error = %v", err) - } - - tpl, _ := s.GetByID(context.Background(), id) - if tpl.Name != "renamed" { - t.Errorf("Name = %q, want %q", tpl.Name, "renamed") - } - // Script and Partition should be unchanged - if tpl.Script != "echo orig" { - t.Errorf("Script = %q, want %q", tpl.Script, "echo orig") - } - if tpl.Partition != "batch" { - t.Errorf("Partition = %q, want %q", tpl.Partition, "batch") - } -} - -func TestTemplateStore_Delete(t *testing.T) { - db := newTestDB(t) - s := NewTemplateStore(db) - - id, _ := s.Create(context.Background(), &model.CreateTemplateRequest{ - Name: "to-delete", Script: "echo", - }) - - err := s.Delete(context.Background(), id) - if err != nil { - t.Fatalf("Delete() error = %v", err) - } - - tpl, _ := s.GetByID(context.Background(), id) - if tpl != nil { - t.Fatal("Delete() did not remove the record") - } -} - -func TestTemplateStore_Delete_NotFound(t *testing.T) { - db := newTestDB(t) - s := NewTemplateStore(db) - - err := s.Delete(context.Background(), 999) - if err != nil { - t.Fatalf("Delete() should not error for non-existent record, got: %v", err) - } -}