a7df9ffc6c
- 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).
223 lines
5.9 KiB
Go
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
|
|
}
|
|
|