refactor: remove JobTemplate production code
Remove all JobTemplate model, store, handler, migrations, and wiring. Replaced by Application Definition system. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -21,7 +21,6 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App encapsulates the entire application lifecycle.
|
|
||||||
type App struct {
|
type App struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
logger *zap.Logger
|
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 {
|
func initHTTPServer(cfg *config.Config, db *gorm.DB, slurmClient *slurm.Client, logger *zap.Logger) *http.Server {
|
||||||
jobSvc := service.NewJobService(slurmClient, logger)
|
jobSvc := service.NewJobService(slurmClient, logger)
|
||||||
clusterSvc := service.NewClusterService(slurmClient, logger)
|
clusterSvc := service.NewClusterService(slurmClient, logger)
|
||||||
templateStore := store.NewTemplateStore(db)
|
|
||||||
|
|
||||||
jobH := handler.NewJobHandler(jobSvc, logger)
|
jobH := handler.NewJobHandler(jobSvc, logger)
|
||||||
clusterH := handler.NewClusterHandler(clusterSvc, 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
|
addr := ":" + cfg.ServerPort
|
||||||
|
|
||||||
|
|||||||
@@ -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"})
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"` // 运行时间限制
|
|
||||||
}
|
|
||||||
@@ -25,16 +25,17 @@ type ClusterHandler interface {
|
|||||||
GetDiag(c *gin.Context)
|
GetDiag(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemplateHandler interface {
|
type ApplicationHandler interface {
|
||||||
ListTemplates(c *gin.Context)
|
ListApplications(c *gin.Context)
|
||||||
CreateTemplate(c *gin.Context)
|
CreateApplication(c *gin.Context)
|
||||||
GetTemplate(c *gin.Context)
|
GetApplication(c *gin.Context)
|
||||||
UpdateTemplate(c *gin.Context)
|
UpdateApplication(c *gin.Context)
|
||||||
DeleteTemplate(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.
|
// 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)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(gin.Recovery())
|
r.Use(gin.Recovery())
|
||||||
@@ -59,12 +60,13 @@ func NewRouter(jobH JobHandler, clusterH ClusterHandler, templateH TemplateHandl
|
|||||||
|
|
||||||
v1.GET("/diag", clusterH.GetDiag)
|
v1.GET("/diag", clusterH.GetDiag)
|
||||||
|
|
||||||
templates := v1.Group("/templates")
|
apps := v1.Group("/applications")
|
||||||
templates.GET("", templateH.ListTemplates)
|
apps.GET("", appH.ListApplications)
|
||||||
templates.POST("", templateH.CreateTemplate)
|
apps.POST("", appH.CreateApplication)
|
||||||
templates.GET("/:id", templateH.GetTemplate)
|
apps.GET("/:id", appH.GetApplication)
|
||||||
templates.PUT("/:id", templateH.UpdateTemplate)
|
apps.PUT("/:id", appH.UpdateApplication)
|
||||||
templates.DELETE("/:id", templateH.DeleteTemplate)
|
apps.DELETE("/:id", appH.DeleteApplication)
|
||||||
|
apps.POST("/:id/submit", appH.SubmitApplication)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@@ -95,12 +97,13 @@ func registerPlaceholderRoutes(v1 *gin.RouterGroup) {
|
|||||||
|
|
||||||
v1.GET("/diag", notImplemented)
|
v1.GET("/diag", notImplemented)
|
||||||
|
|
||||||
templates := v1.Group("/templates")
|
apps := v1.Group("/applications")
|
||||||
templates.GET("", notImplemented)
|
apps.GET("", notImplemented)
|
||||||
templates.POST("", notImplemented)
|
apps.POST("", notImplemented)
|
||||||
templates.GET("/:id", notImplemented)
|
apps.GET("/:id", notImplemented)
|
||||||
templates.PUT("/:id", notImplemented)
|
apps.PUT("/:id", notImplemented)
|
||||||
templates.DELETE("/:id", notImplemented)
|
apps.DELETE("/:id", notImplemented)
|
||||||
|
apps.POST("/:id/submit", notImplemented)
|
||||||
}
|
}
|
||||||
|
|
||||||
func notImplemented(c *gin.Context) {
|
func notImplemented(c *gin.Context) {
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ func TestAllRoutesRegistered(t *testing.T) {
|
|||||||
{"GET", "/api/v1/partitions"},
|
{"GET", "/api/v1/partitions"},
|
||||||
{"GET", "/api/v1/partitions/:name"},
|
{"GET", "/api/v1/partitions/:name"},
|
||||||
{"GET", "/api/v1/diag"},
|
{"GET", "/api/v1/diag"},
|
||||||
{"GET", "/api/v1/templates"},
|
{"GET", "/api/v1/applications"},
|
||||||
{"POST", "/api/v1/templates"},
|
{"POST", "/api/v1/applications"},
|
||||||
{"GET", "/api/v1/templates/:id"},
|
{"GET", "/api/v1/applications/:id"},
|
||||||
{"PUT", "/api/v1/templates/:id"},
|
{"PUT", "/api/v1/applications/:id"},
|
||||||
{"DELETE", "/api/v1/templates/:id"},
|
{"DELETE", "/api/v1/applications/:id"},
|
||||||
|
{"POST", "/api/v1/applications/:id/submit"},
|
||||||
}
|
}
|
||||||
|
|
||||||
routeMap := map[string]bool{}
|
routeMap := map[string]bool{}
|
||||||
@@ -74,7 +75,7 @@ func TestRegisteredPathReturns501(t *testing.T) {
|
|||||||
{"GET", "/api/v1/nodes"},
|
{"GET", "/api/v1/nodes"},
|
||||||
{"GET", "/api/v1/partitions"},
|
{"GET", "/api/v1/partitions"},
|
||||||
{"GET", "/api/v1/diag"},
|
{"GET", "/api/v1/diag"},
|
||||||
{"GET", "/api/v1/templates"},
|
{"GET", "/api/v1/applications"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ep := range endpoints {
|
for _, ep := range endpoints {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS job_templates;
|
|
||||||
@@ -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;
|
|
||||||
@@ -40,5 +40,5 @@ func NewGormDB(dsn string, zapLogger *zap.Logger, gormLevel string) (*gorm.DB, e
|
|||||||
|
|
||||||
// AutoMigrate runs GORM auto-migration for all models.
|
// AutoMigrate runs GORM auto-migration for all models.
|
||||||
func AutoMigrate(db *gorm.DB) error {
|
func AutoMigrate(db *gorm.DB) error {
|
||||||
return db.AutoMigrate(&model.JobTemplate{})
|
return db.AutoMigrate(&model.Application{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user