1
0
forked from wrenn/wrenn

Add USER, COPY, ENV persistence to template build system

Implement three new recipe commands for the admin template builder:

- USER <name>: creates the user (adduser + passwordless sudo), switches
  execution context so subsequent RUN/START commands run as that user
  via su wrapping. Last USER becomes the template's default_user.

- COPY <src> <dst>: copies files from an uploaded build archive
  (tar/tar.gz/zip) into the sandbox. Source paths validated against
  traversal. Ownership set to the current USER.

- ENV persistence: accumulated env vars stored in templates.default_env
  (JSONB) and injected via PostInit when sandboxes are created from the
  template, mirroring Docker's image metadata approach.

Supporting changes:
- Pre-build creates wrenn-user as default (via USER command)
- WORKDIR now creates the directory if it doesn't exist (mkdir -p)
- Per-step progress updates (ProgressFunc callback) for live UI
- Multipart form support on POST /v1/admin/builds for archive upload
- Proto: default_user/default_env fields on Create/ResumeSandboxRequest
- Host agent: SetDefaults calls PostInitWithDefaults on envd
- Control plane: reads template defaults, passes on sandbox create/resume
- Frontend: file upload widget, recipe copy button, keyword colors for
  USER/COPY, fixed Svelte whitespace stripping in step display
- Admin panel defaults to /admin/templates instead of /admin/hosts
- Migration adds default_user and default_env to templates and
  template_builds tables
This commit is contained in:
2026-04-12 02:10:01 +06:00
parent f6c3dc0801
commit 75af2a4f66
24 changed files with 866 additions and 183 deletions

View File

@ -152,14 +152,16 @@ type TeamApiKey struct {
}
type Template struct {
Name string `json:"name"`
Type string `json:"type"`
Vcpus int32 `json:"vcpus"`
MemoryMb int32 `json:"memory_mb"`
SizeBytes int64 `json:"size_bytes"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
TeamID pgtype.UUID `json:"team_id"`
ID pgtype.UUID `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Vcpus int32 `json:"vcpus"`
MemoryMb int32 `json:"memory_mb"`
SizeBytes int64 `json:"size_bytes"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
TeamID pgtype.UUID `json:"team_id"`
ID pgtype.UUID `json:"id"`
DefaultUser string `json:"default_user"`
DefaultEnv []byte `json:"default_env"`
}
type TemplateBuild struct {
@ -183,6 +185,8 @@ type TemplateBuild struct {
TemplateID pgtype.UUID `json:"template_id"`
TeamID pgtype.UUID `json:"team_id"`
SkipPrePost bool `json:"skip_pre_post"`
DefaultUser string `json:"default_user"`
DefaultEnv []byte `json:"default_env"`
}
type User struct {

View File

@ -12,7 +12,7 @@ import (
)
const getTemplateBuild = `-- name: GetTemplateBuild :one
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post FROM template_builds WHERE id = $1
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env FROM template_builds WHERE id = $1
`
func (q *Queries) GetTemplateBuild(ctx context.Context, id pgtype.UUID) (TemplateBuild, error) {
@ -39,6 +39,8 @@ func (q *Queries) GetTemplateBuild(ctx context.Context, id pgtype.UUID) (Templat
&i.TemplateID,
&i.TeamID,
&i.SkipPrePost,
&i.DefaultUser,
&i.DefaultEnv,
)
return i, err
}
@ -46,7 +48,7 @@ func (q *Queries) GetTemplateBuild(ctx context.Context, id pgtype.UUID) (Templat
const insertTemplateBuild = `-- name: InsertTemplateBuild :one
INSERT INTO template_builds (id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, total_steps, template_id, team_id, skip_pre_post)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8, $9, $10, $11)
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env
`
type InsertTemplateBuildParams struct {
@ -99,12 +101,14 @@ func (q *Queries) InsertTemplateBuild(ctx context.Context, arg InsertTemplateBui
&i.TemplateID,
&i.TeamID,
&i.SkipPrePost,
&i.DefaultUser,
&i.DefaultEnv,
)
return i, err
}
const listTemplateBuilds = `-- name: ListTemplateBuilds :many
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post FROM template_builds ORDER BY created_at DESC
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env FROM template_builds ORDER BY created_at DESC
`
func (q *Queries) ListTemplateBuilds(ctx context.Context) ([]TemplateBuild, error) {
@ -137,6 +141,8 @@ func (q *Queries) ListTemplateBuilds(ctx context.Context) ([]TemplateBuild, erro
&i.TemplateID,
&i.TeamID,
&i.SkipPrePost,
&i.DefaultUser,
&i.DefaultEnv,
); err != nil {
return nil, err
}
@ -148,6 +154,23 @@ func (q *Queries) ListTemplateBuilds(ctx context.Context) ([]TemplateBuild, erro
return items, nil
}
const updateBuildDefaults = `-- name: UpdateBuildDefaults :exec
UPDATE template_builds
SET default_user = $2, default_env = $3
WHERE id = $1
`
type UpdateBuildDefaultsParams struct {
ID pgtype.UUID `json:"id"`
DefaultUser string `json:"default_user"`
DefaultEnv []byte `json:"default_env"`
}
func (q *Queries) UpdateBuildDefaults(ctx context.Context, arg UpdateBuildDefaultsParams) error {
_, err := q.db.Exec(ctx, updateBuildDefaults, arg.ID, arg.DefaultUser, arg.DefaultEnv)
return err
}
const updateBuildError = `-- name: UpdateBuildError :exec
UPDATE template_builds
SET error = $2, status = 'failed', completed_at = NOW()
@ -204,7 +227,7 @@ SET status = $2,
started_at = CASE WHEN $2 = 'running' AND started_at IS NULL THEN NOW() ELSE started_at END,
completed_at = CASE WHEN $2 IN ('success', 'failed', 'cancelled') THEN NOW() ELSE completed_at END
WHERE id = $1
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env
`
type UpdateBuildStatusParams struct {
@ -236,6 +259,8 @@ func (q *Queries) UpdateBuildStatus(ctx context.Context, arg UpdateBuildStatusPa
&i.TemplateID,
&i.TeamID,
&i.SkipPrePost,
&i.DefaultUser,
&i.DefaultEnv,
)
return i, err
}

View File

@ -45,7 +45,7 @@ func (q *Queries) DeleteTemplatesByTeam(ctx context.Context, teamID pgtype.UUID)
}
const getPlatformTemplateByName = `-- name: GetPlatformTemplateByName :one
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE team_id = '00000000-0000-0000-0000-000000000000' AND name = $1
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE team_id = '00000000-0000-0000-0000-000000000000' AND name = $1
`
// Check if a global (platform) template exists with the given name.
@ -61,12 +61,14 @@ func (q *Queries) GetPlatformTemplateByName(ctx context.Context, name string) (T
&i.CreatedAt,
&i.TeamID,
&i.ID,
&i.DefaultUser,
&i.DefaultEnv,
)
return i, err
}
const getTemplate = `-- name: GetTemplate :one
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE id = $1
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE id = $1
`
func (q *Queries) GetTemplate(ctx context.Context, id pgtype.UUID) (Template, error) {
@ -81,12 +83,14 @@ func (q *Queries) GetTemplate(ctx context.Context, id pgtype.UUID) (Template, er
&i.CreatedAt,
&i.TeamID,
&i.ID,
&i.DefaultUser,
&i.DefaultEnv,
)
return i, err
}
const getTemplateByName = `-- name: GetTemplateByName :one
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE team_id = $1 AND name = $2
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE team_id = $1 AND name = $2
`
type GetTemplateByNameParams struct {
@ -107,12 +111,14 @@ func (q *Queries) GetTemplateByName(ctx context.Context, arg GetTemplateByNamePa
&i.CreatedAt,
&i.TeamID,
&i.ID,
&i.DefaultUser,
&i.DefaultEnv,
)
return i, err
}
const getTemplateByTeam = `-- name: GetTemplateByTeam :one
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE name = $1 AND (team_id = $2 OR team_id = '00000000-0000-0000-0000-000000000000')
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE name = $1 AND (team_id = $2 OR team_id = '00000000-0000-0000-0000-000000000000')
`
type GetTemplateByTeamParams struct {
@ -133,24 +139,28 @@ func (q *Queries) GetTemplateByTeam(ctx context.Context, arg GetTemplateByTeamPa
&i.CreatedAt,
&i.TeamID,
&i.ID,
&i.DefaultUser,
&i.DefaultEnv,
)
return i, err
}
const insertTemplate = `-- name: InsertTemplate :one
INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id
INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id, default_user, default_env)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env
`
type InsertTemplateParams struct {
ID pgtype.UUID `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Vcpus int32 `json:"vcpus"`
MemoryMb int32 `json:"memory_mb"`
SizeBytes int64 `json:"size_bytes"`
TeamID pgtype.UUID `json:"team_id"`
ID pgtype.UUID `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Vcpus int32 `json:"vcpus"`
MemoryMb int32 `json:"memory_mb"`
SizeBytes int64 `json:"size_bytes"`
TeamID pgtype.UUID `json:"team_id"`
DefaultUser string `json:"default_user"`
DefaultEnv []byte `json:"default_env"`
}
func (q *Queries) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) {
@ -162,6 +172,8 @@ func (q *Queries) InsertTemplate(ctx context.Context, arg InsertTemplateParams)
arg.MemoryMb,
arg.SizeBytes,
arg.TeamID,
arg.DefaultUser,
arg.DefaultEnv,
)
var i Template
err := row.Scan(
@ -173,12 +185,14 @@ func (q *Queries) InsertTemplate(ctx context.Context, arg InsertTemplateParams)
&i.CreatedAt,
&i.TeamID,
&i.ID,
&i.DefaultUser,
&i.DefaultEnv,
)
return i, err
}
const listTemplates = `-- name: ListTemplates :many
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates ORDER BY created_at DESC
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates ORDER BY created_at DESC
`
func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
@ -199,6 +213,8 @@ func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
&i.CreatedAt,
&i.TeamID,
&i.ID,
&i.DefaultUser,
&i.DefaultEnv,
); err != nil {
return nil, err
}
@ -211,7 +227,7 @@ func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
}
const listTemplatesByTeam = `-- name: ListTemplatesByTeam :many
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') ORDER BY created_at DESC
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') ORDER BY created_at DESC
`
// Platform templates are visible to all teams.
@ -233,6 +249,8 @@ func (q *Queries) ListTemplatesByTeam(ctx context.Context, teamID pgtype.UUID) (
&i.CreatedAt,
&i.TeamID,
&i.ID,
&i.DefaultUser,
&i.DefaultEnv,
); err != nil {
return nil, err
}
@ -245,7 +263,7 @@ func (q *Queries) ListTemplatesByTeam(ctx context.Context, teamID pgtype.UUID) (
}
const listTemplatesByTeamAndType = `-- name: ListTemplatesByTeamAndType :many
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') AND type = $2 ORDER BY created_at DESC
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') AND type = $2 ORDER BY created_at DESC
`
type ListTemplatesByTeamAndTypeParams struct {
@ -272,6 +290,8 @@ func (q *Queries) ListTemplatesByTeamAndType(ctx context.Context, arg ListTempla
&i.CreatedAt,
&i.TeamID,
&i.ID,
&i.DefaultUser,
&i.DefaultEnv,
); err != nil {
return nil, err
}
@ -284,7 +304,7 @@ func (q *Queries) ListTemplatesByTeamAndType(ctx context.Context, arg ListTempla
}
const listTemplatesByTeamOnly = `-- name: ListTemplatesByTeamOnly :many
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE team_id = $1 ORDER BY created_at DESC
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE team_id = $1 ORDER BY created_at DESC
`
// List templates owned by a specific team (NOT including platform templates).
@ -306,6 +326,8 @@ func (q *Queries) ListTemplatesByTeamOnly(ctx context.Context, teamID pgtype.UUI
&i.CreatedAt,
&i.TeamID,
&i.ID,
&i.DefaultUser,
&i.DefaultEnv,
); err != nil {
return nil, err
}
@ -318,7 +340,7 @@ func (q *Queries) ListTemplatesByTeamOnly(ctx context.Context, teamID pgtype.UUI
}
const listTemplatesByType = `-- name: ListTemplatesByType :many
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE type = $1 ORDER BY created_at DESC
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE type = $1 ORDER BY created_at DESC
`
func (q *Queries) ListTemplatesByType(ctx context.Context, type_ string) ([]Template, error) {
@ -339,6 +361,8 @@ func (q *Queries) ListTemplatesByType(ctx context.Context, type_ string) ([]Temp
&i.CreatedAt,
&i.TeamID,
&i.ID,
&i.DefaultUser,
&i.DefaultEnv,
); err != nil {
return nil, err
}