Files
bri-sandbox-development-pla…/control-plane/internal/store/templates.go
T
Achmad a7df9ffc6c Slice 2: sandbox, template, environment, route CRUD
- store: add tables and CRUD for sandboxes (with services), templates
  (with services, clone-into-sandbox), environments (named key/value
  sets), and routes (per-sandbox <service>_url overrides).
- api: split into one file per resource. handleSandboxes/handleSandboxByID
  covers CRUD + 'clone from template' + 'deploy one service in a sandbox'
  (which merges the sandbox's env into the request, picks the port,
  and dispatches the deploy frame to the right node).
  handleTemplates/handleTemplateByID, handleEnvironments/handleEnvironmentByID,
  handlePushRoutes cover the rest. The control plane's repo->node
  resolution still lives in resolveNode (api-gateway -> gateway,
  everything else -> micro).
2026-06-24 03:59:02 +00:00

223 lines
5.9 KiB
Go

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
}