package store import ( "database/sql" "time" ) // Template is a reusable sandbox configuration. Cloning a template into // a sandbox materializes a sandbox row + sandbox_services from the // template rows. type Template struct { ID string `json:"id"` Name string `json:"name"` GatewayBranch string `json:"gatewayBranch,omitempty"` CreatedAt int64 `json:"createdAt"` UpdatedAt int64 `json:"updatedAt"` Services []TemplateService `json:"services"` } // TemplateService is one microservice entry in a template. type TemplateService struct { ID string `json:"id"` TemplateID string `json:"templateId"` Repo string `json:"repo"` Branch string `json:"branch,omitempty"` EnvID string `json:"envId,omitempty"` HostPort int `json:"hostPort,omitempty"` UseOCP bool `json:"useOcp"` } // CreateTemplate persists a template and its services. Caller assigns IDs. func (s *Store) CreateTemplate(t Template) error { now := time.Now().UnixMilli() if t.CreatedAt == 0 { t.CreatedAt = now } t.UpdatedAt = now tx, err := s.db.Begin() if err != nil { return err } defer tx.Rollback() if _, err := tx.Exec( `INSERT INTO templates(id, name, gateway_branch, created_at, updated_at) VALUES(?,?,?,?,?)`, t.ID, t.Name, t.GatewayBranch, t.CreatedAt, t.UpdatedAt, ); err != nil { return err } for i := range t.Services { svc := &t.Services[i] if svc.ID == "" { svc.ID = t.ID + "-" + svc.Repo svc.TemplateID = t.ID } if err := insertTemplateService(tx, *svc); err != nil { return err } } return tx.Commit() } // UpdateTemplate replaces the template's mutable fields. Services are // replaced wholesale. func (s *Store) UpdateTemplate(t Template) error { now := time.Now().UnixMilli() t.UpdatedAt = now tx, err := s.db.Begin() if err != nil { return err } defer tx.Rollback() res, err := tx.Exec( `UPDATE templates SET name=?, gateway_branch=?, updated_at=? WHERE id=?`, t.Name, t.GatewayBranch, t.UpdatedAt, t.ID) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return ErrNotFound } if _, err := tx.Exec(`DELETE FROM template_services WHERE template_id=?`, t.ID); err != nil { return err } for i := range t.Services { svc := &t.Services[i] svc.TemplateID = t.ID if svc.ID == "" { svc.ID = t.ID + "-" + svc.Repo } if err := insertTemplateService(tx, *svc); err != nil { return err } } return tx.Commit() } // GetTemplate returns one template with its services. ErrNotFound if missing. func (s *Store) GetTemplate(id string) (*Template, error) { row := s.db.QueryRow( `SELECT id, name, COALESCE(gateway_branch,''), created_at, updated_at FROM templates WHERE id=?`, id) var t Template err := row.Scan(&t.ID, &t.Name, &t.GatewayBranch, &t.CreatedAt, &t.UpdatedAt) if err == sql.ErrNoRows { return nil, ErrNotFound } if err != nil { return nil, err } if err := s.loadTemplateServices(&t); err != nil { return nil, err } return &t, nil } // ListTemplates returns all templates, newest first. func (s *Store) ListTemplates() ([]Template, error) { rows, err := s.db.Query( `SELECT id, name, COALESCE(gateway_branch,''), created_at, updated_at FROM templates ORDER BY created_at DESC`) if err != nil { return nil, err } defer rows.Close() var out []Template for rows.Next() { var t Template if err := rows.Scan(&t.ID, &t.Name, &t.GatewayBranch, &t.CreatedAt, &t.UpdatedAt); err != nil { return nil, err } out = append(out, t) } if err := rows.Err(); err != nil { return nil, err } for i := range out { if err := s.loadTemplateServices(&out[i]); err != nil { return nil, err } } return out, nil } // DeleteTemplate removes the template and its services. ErrNotFound if missing. func (s *Store) DeleteTemplate(id string) error { tx, err := s.db.Begin() if err != nil { return err } defer tx.Rollback() if _, err := tx.Exec(`DELETE FROM template_services WHERE template_id=?`, id); err != nil { return err } res, err := tx.Exec(`DELETE FROM templates WHERE id=?`, id) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return ErrNotFound } return tx.Commit() } // CloneTemplateIntoSandbox materializes a sandbox from a template. The // caller assigns the new sandbox id and name. The gateway_branch and // each service are copied; hostPort is preserved as-is (the caller may // then update it to claim a fresh port via SetSandboxServicePort). func (s *Store) CloneTemplateIntoSandbox(templateID string, sb Sandbox) error { t, err := s.GetTemplate(templateID) if err != nil { return err } sb.GatewayBranch = t.GatewayBranch sb.Services = make([]SandboxService, 0, len(t.Services)) for _, ts := range t.Services { sb.Services = append(sb.Services, SandboxService{ SandboxID: sb.ID, Repo: ts.Repo, Branch: ts.Branch, EnvID: ts.EnvID, HostPort: ts.HostPort, UseOCP: ts.UseOCP, }) } return s.CreateSandbox(sb) } func (s *Store) loadTemplateServices(t *Template) error { rows, err := s.db.Query( `SELECT id, template_id, repo, COALESCE(branch,''), COALESCE(env_id,''), COALESCE(host_port,0), COALESCE(use_ocp,0) FROM template_services WHERE template_id=? ORDER BY repo`, t.ID) if err != nil { return err } defer rows.Close() for rows.Next() { var svc TemplateService var useOCP int if err := rows.Scan(&svc.ID, &svc.TemplateID, &svc.Repo, &svc.Branch, &svc.EnvID, &svc.HostPort, &useOCP); err != nil { return err } svc.UseOCP = useOCP == 1 t.Services = append(t.Services, svc) } return rows.Err() } func insertTemplateService(tx *sql.Tx, svc TemplateService) error { _, err := tx.Exec( `INSERT INTO template_services(id, template_id, repo, branch, env_id, host_port, use_ocp) VALUES(?,?,?,?,?,?,?)`, svc.ID, svc.TemplateID, svc.Repo, svc.Branch, nullString(svc.EnvID), nullInt(svc.HostPort), boolInt(svc.UseOCP), ) return err }