domain

package
v0.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 8, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildComposeEnv

func BuildComposeEnv(projectName, envName string, ports PortMap) []string

BuildComposeEnv builds the environment variables for running docker compose commands. Includes COMPOSE_PROJECT_NAME and port mappings (e.g., REDIS_PORT=6421). The returned slice is suitable for passing to ComputeAccess.Exec.

func ComposeProjectName

func ComposeProjectName(projectName, envName string) string

ComposeProjectName returns the compose project name for this environment. Sanitizes to match Docker Compose requirements: lowercase alphanumeric, hyphens, underscores.

func ExecuteCoreHook

func ExecuteCoreHook(ctx context.Context, hookScript string, declaredOutputs []string, env []string, workdir string, stderr io.Writer) (map[string]string, error)

ExecuteCoreHook runs a core service hook script and captures KEY=VALUE outputs. Both stdout and stderr stream to the terminal in real-time for visibility. KEY=VALUE lines on stdout are redacted (value replaced with ********) before display. stdout is also captured and parsed for KEY=VALUE pairs. Returns the captured outputs, validated against declaredOutputs.

func GenerateComposeFile

func GenerateComposeFile(cfg *ProjectConfig, manifest *Manifest) ([]byte, error)

GenerateComposeFile generates a Docker Compose YAML file from service definitions. Only services with a `start` command are included. nginx is always added implicitly. The compose file contains ALL startable services — autostart controls which are started on create, not which are generated.

func GenerateErrorPages

func GenerateErrorPages(envName string) map[string][]byte

GenerateErrorPages returns a map of filename → HTML content for nginx error pages.

func GenerateNginxConfig

func GenerateNginxConfig(cfg *ProjectConfig, manifest *Manifest, domain string) ([]byte, error)

GenerateNginxConfig generates an nginx configuration file from the manifest and service definitions. Each service gets its own server block with explicit hostname matching.

Enabled services get full proxy blocks with a custom 502 error page. Disabled services (known but not started) get a friendly status page telling the user how to start the service. Unknown subdomains hit the default server with a 404 page.

func Hostname

func Hostname() string

Hostname returns the machine hostname for audit logging.

func ParseComposeFile

func ParseComposeFile(path string) (map[string]InfraService, error)

ParseComposeFile reads a docker compose YAML file and extracts infrastructure service definitions including service names, images, and port mappings.

func RenderEnvFileContent

func RenderEnvFileContent(env map[string]string) []byte

RenderEnvFileContent renders a map of env vars to KEY=VALUE format content. Keys are sorted for deterministic output.

func RenderEnvMap

func RenderEnvMap(envMap map[string]string, ctx *TemplateContext) (map[string]string, error)

RenderEnvMap renders all template variables in a map of env vars.

func RenderTemplate

func RenderTemplate(tmpl string, ctx *TemplateContext) (string, error)

RenderTemplate replaces {{var}} placeholders in a string with values from the context. Supported variable patterns:

  • {{services.<name>.port}} — allocated port for an application service
  • {{infrastructure.<name>.port}} — allocated port for an infrastructure service
  • {{provisioner.<service>.<OUTPUT>}} — output value from a provisioner service
  • {{env.name}} — the current environment name
  • {{store.<key>}} — value from the persistent key-value store
  • {{proxy.url.<service>}} — full URL for a service (https://{env}--{service}.{domain})
  • {{proxy.domain}} — the proxy domain (e.g., "preview.airgoods.com")

func ResolveEnvironmentFromCwd

func ResolveEnvironmentFromCwd(cwd string, environments map[string]*EnvironmentEntry) (string, error)

ResolveEnvironmentFromCwd determines the environment name by checking if the current working directory is inside any known worktree path from state.

func SanitizeName

func SanitizeName(name string) string

SanitizeName replaces characters not safe for use in database names, file paths, and Docker Compose project names. Lowercase alphanumeric, hyphens, and underscores only.

func TopologicalSort

func TopologicalSort(services map[string]ServiceConfig) ([]string, error)

TopologicalSort performs Kahn's algorithm to produce a topological ordering of services based on their dependsOn relationships. Returns an error if a cycle is detected or a dependency references an unknown service.

func ValidateConfig

func ValidateConfig(cfg *ProjectConfig) error

ValidateConfig performs deep validation of a ProjectConfig. It checks structural correctness, port collisions, dependency references, and template variable references. It does NOT check file system paths — use ValidateConfigWithFS for that.

func ValidateConfigWithFS

func ValidateConfigWithFS(cfg *ProjectConfig, projectRoot string, fileExists func(string) bool) error

ValidateConfigWithFS performs all config validation plus file system checks. projectRoot is the directory containing previewctl.yaml. fileExists is a function that checks if a path exists (allows testing).

func WriteManifest

func WriteManifest(m *Manifest, w io.Writer) error

WriteManifest encodes a manifest as JSON to the given writer.

Types

type AuditEntry

type AuditEntry struct {
	Timestamp  time.Time `json:"timestamp"`
	Step       string    `json:"step"`
	Action     string    `json:"action"` // "executed", "skipped", "verified", "verify_failed", "invalidated", "failed"
	Machine    string    `json:"machine"`
	DurationMs int64     `json:"durationMs,omitempty"`
	Message    string    `json:"message,omitempty"`
	Error      string    `json:"error,omitempty"`
}

AuditEntry is an append-only log entry recording what happened during lifecycle operations.

type CommandHook

type CommandHook struct {
	Command    string `yaml:"command,omitempty"`
	AllowCache *bool  `yaml:"allow_cache,omitempty"`
}

CommandHook is a command-valued hook that can be configured either as a legacy scalar string or as an object with hook metadata.

func (CommandHook) CacheAllowed

func (h CommandHook) CacheAllowed() bool

CacheAllowed reports whether completed checkpoints may skip this hook. Hooks are cacheable by default for backward compatibility.

func (CommandHook) IsConfigured

func (h CommandHook) IsConfigured() bool

IsConfigured reports whether this hook has a command to run.

func (*CommandHook) UnmarshalYAML

func (h *CommandHook) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML accepts both:

runner:
  after: ./script.sh

and:

runner:
  after:
    command: ./script.sh
    allow_cache: false

type ComposeConfig

type ComposeConfig struct {
	Autostart []string     `yaml:"autostart"`       // services started on create (proxy is always implicit if enabled)
	Image     string       `yaml:"image"`           // base Docker image for app containers (e.g., "node:20")
	Proxy     *ProxyConfig `yaml:"proxy,omitempty"` // reverse proxy configuration
}

ComposeConfig defines how previewctl generates and manages Docker Compose for application services in remote mode.

type ComputeAccess

type ComputeAccess interface {
	// WriteFile writes content to a path relative to the compute root.
	WriteFile(ctx context.Context, relPath string, data []byte, mode os.FileMode) error

	// ReadFile reads content from a path relative to the compute root.
	ReadFile(ctx context.Context, relPath string) ([]byte, error)

	// Exec runs a command in the compute root directory.
	// Stderr streams to the configured writer. Stdout is captured and returned silently.
	Exec(ctx context.Context, command string, env []string) (stdout string, err error)

	// VerboseExec runs a command and tees stdout to stderr for real-time visibility.
	// Use for user-defined hooks and build commands where output should always be visible.
	VerboseExec(ctx context.Context, command string, env []string) (stdout string, err error)

	// Root returns the compute root path (local path or remote working dir).
	Root() string
}

ComputeAccess provides uniform access to a compute location. Local mode wraps filesystem operations; remote mode wraps SSH/SCP.

func NewDomainLocalComputeAccess

func NewDomainLocalComputeAccess(root string) ComputeAccess

func NewDomainSSHComputeAccess

func NewDomainSSHComputeAccess(host, user, root string) ComputeAccess

NewDomainSSHComputeAccess creates a ComputeAccess backed by SSH to a remote host. This is the direct-mode constructor (backward compatible).

func NewDomainSSHComputeAccessWithOpts

func NewDomainSSHComputeAccessWithOpts(opts SSHComputeAccessOpts) ComputeAccess

NewDomainSSHComputeAccessWithOpts creates a ComputeAccess with full SSH options.

type ComputeAccessInfo

type ComputeAccessInfo struct {
	Type            string            `json:"type"`                      // "local" or "ssh"
	Path            string            `json:"path,omitempty"`            // local worktree path or remote root
	Host            string            `json:"host,omitempty"`            // VM hostname (ssh)
	User            string            `json:"user,omitempty"`            // SSH user
	ManagedWorktree bool              `json:"managedWorktree,omitempty"` // true = created by previewctl
	Metadata        map[string]string `json:"metadata,omitempty"`        // hook-provided metadata (e.g., "vm_zone", "gcp_project")
}

ComputeAccessInfo stores how to reach the compute location so environments can be reconnected across CLI invocations.

type ComputeHooks

type ComputeHooks struct {
	Create  string     `yaml:"create"`
	Destroy string     `yaml:"destroy"`
	Outputs []string   `yaml:"outputs,omitempty"`
	SSH     *SSHConfig `yaml:"ssh,omitempty"`
}

ComputeHooks defines lifecycle hooks for compute resources.

type ComputePort

type ComputePort interface {
	// Create sets up compute resources for an environment.
	// branch is the target branch to use/create.
	// baseBranch is the branch to create from (empty = use branch as-is).
	Create(ctx context.Context, envName string, branch string, baseBranch string) (*ComputeResources, error)

	// Start starts per-environment services (infra containers, etc).
	Start(ctx context.Context, envName string, ports PortMap) error

	// Stop stops services without destroying data or resources.
	Stop(ctx context.Context, envName string) error

	// Destroy tears down all compute resources.
	Destroy(ctx context.Context, envName string) error

	// IsRunning checks if environment compute resources are active.
	IsRunning(ctx context.Context, envName string) (bool, error)

	// DetectBranch returns the current git branch for a worktree path.
	DetectBranch(ctx context.Context, worktreePath string) (string, error)
}

ComputePort manages the compute substrate for an environment. Local: git worktree + docker compose for per-env infrastructure. Preview: VM provisioning + full compose stack. Sandbox: isolated VM with network policies.

type ComputeResources

type ComputeResources struct {
	WorktreePath string `json:"worktreePath,omitempty"` // local mode
	VMId         string `json:"vmId,omitempty"`         // preview/sandbox mode
	ExternalIP   string `json:"externalIp,omitempty"`   // preview/sandbox mode
}

ComputeResources holds the result of creating compute resources for an environment.

type CoreResetOpts

type CoreResetOpts struct {
	// NoPropagate skips regenerating remote manifest/env/compose files and
	// restarting dependent services after a successful reset. State is still
	// updated with fresh provisioner outputs. Use this when you want to batch
	// several resets and run a single SyncRemote at the end.
	NoPropagate bool
}

CoreResetOpts configures the behavior of CoreReset.

type DestroyOptions added in v0.0.5

type DestroyOptions struct {
	// Force continues past destroy-hook failures and always removes the
	// environment from the state store. Useful when the create lifecycle
	// failed and left state in an inconsistent shape that hooks can't handle.
	Force bool
}

DestroyOptions configures Destroy behavior.

type DomainLocalComputeAccess

type DomainLocalComputeAccess struct {
	// contains filtered or unexported fields
}

func (*DomainLocalComputeAccess) Exec

func (l *DomainLocalComputeAccess) Exec(ctx context.Context, command string, env []string) (string, error)

func (*DomainLocalComputeAccess) ReadFile

func (l *DomainLocalComputeAccess) ReadFile(_ context.Context, relPath string) ([]byte, error)

func (*DomainLocalComputeAccess) Root

func (l *DomainLocalComputeAccess) Root() string

func (*DomainLocalComputeAccess) SetStderr

func (l *DomainLocalComputeAccess) SetStderr(w io.Writer)

func (*DomainLocalComputeAccess) VerboseExec

func (l *DomainLocalComputeAccess) VerboseExec(ctx context.Context, command string, env []string) (string, error)

func (*DomainLocalComputeAccess) WriteFile

func (l *DomainLocalComputeAccess) WriteFile(_ context.Context, relPath string, data []byte, mode os.FileMode) error

type DomainSSHComputeAccess

type DomainSSHComputeAccess struct {
	// contains filtered or unexported fields
}

DomainSSHComputeAccess implements ComputeAccess over SSH. It uses the system ssh binary. Lives in domain to avoid circular imports.

Two connection modes:

  • ProxyCommand mode: uses -o ProxyCommand=... (cloud-agnostic, no local SSH config needed)
  • Direct mode: uses user@host (requires SSH config or direct access)

func (*DomainSSHComputeAccess) EnsureRootWritable

func (s *DomainSSHComputeAccess) EnsureRootWritable(ctx context.Context) error

EnsureRootWritable reassigns ownership of the remote root tree to the current SSH user and relaxes its mode so subsequent host-side tools (pnpm, turbo, etc.) can create and chmod files regardless of who touched the tree last.

This exists because OS Login hands each actor a distinct UID; files created by one actor (CI service account, another human) are owned by that UID, and chmod(2) on those inodes fails for anyone but the owner even when the mode is world-writable. Call this at the top of any operation that writes into the remote root from a potentially different actor than the last writer.

Idempotent and best-effort: failures are swallowed (`|| true`) because the common case is already-correctly-owned, and we don't want to block the happy path on an unrelated permission blip.

func (*DomainSSHComputeAccess) Exec

func (s *DomainSSHComputeAccess) Exec(ctx context.Context, command string, env []string) (string, error)

func (*DomainSSHComputeAccess) Host

func (s *DomainSSHComputeAccess) Host() string

Host returns the SSH host.

func (*DomainSSHComputeAccess) ProxyCommand

func (s *DomainSSHComputeAccess) ProxyCommand() string

ProxyCommand returns the configured proxy command, if any.

func (*DomainSSHComputeAccess) ReadFile

func (s *DomainSSHComputeAccess) ReadFile(ctx context.Context, relPath string) ([]byte, error)

func (*DomainSSHComputeAccess) Root

func (s *DomainSSHComputeAccess) Root() string

func (*DomainSSHComputeAccess) SSHArgs

func (s *DomainSSHComputeAccess) SSHArgs() []string

sshArgs returns the SSH arguments for building external SSH commands (e.g., for syscall.Exec).

func (*DomainSSHComputeAccess) SetStderr

func (s *DomainSSHComputeAccess) SetStderr(w io.Writer)

func (*DomainSSHComputeAccess) User

func (s *DomainSSHComputeAccess) User() string

User returns the SSH user.

func (*DomainSSHComputeAccess) VerboseExec

func (s *DomainSSHComputeAccess) VerboseExec(ctx context.Context, command string, env []string) (string, error)

func (*DomainSSHComputeAccess) WriteFile

func (s *DomainSSHComputeAccess) WriteFile(ctx context.Context, relPath string, data []byte, _ os.FileMode) error

type EnvironmentDetail

type EnvironmentDetail struct {
	Entry        *EnvironmentEntry `json:"entry"`
	InfraRunning bool              `json:"infraRunning"`
}

EnvironmentDetail is an enriched view with live infrastructure checks.

type EnvironmentEntry

type EnvironmentEntry struct {
	Name               string                       `json:"name"`
	Mode               EnvironmentMode              `json:"mode"`
	Branch             string                       `json:"branch"`
	Status             EnvironmentStatus            `json:"status"`
	CreatedAt          time.Time                    `json:"createdAt"`
	UpdatedAt          time.Time                    `json:"updatedAt"`
	Ports              PortMap                      `json:"ports"`
	ProvisionerOutputs map[string]map[string]string `json:"provisionerOutputs"`
	Compute            *ComputeAccessInfo           `json:"compute,omitempty"`
	Env                map[string]string            `json:"env,omitempty"`             // persistent key-value store for hooks
	EnabledServices    []string                     `json:"enabledServices,omitempty"` // services currently enabled (seeded from autostart, updated by service start/stop)
	Steps              map[string]*StepRecord       `json:"steps,omitempty"`
	AuditLog           []AuditEntry                 `json:"auditLog,omitempty"`
}

EnvironmentEntry is a tracked environment persisted in state.

func (*EnvironmentEntry) AppendAudit

func (e *EnvironmentEntry) AppendAudit(entry AuditEntry)

AppendAudit adds an audit log entry.

func (*EnvironmentEntry) DisableService

func (e *EnvironmentEntry) DisableService(name string)

DisableService removes a service from the enabled set.

func (*EnvironmentEntry) EnableService

func (e *EnvironmentEntry) EnableService(name string)

EnableService adds a service to the enabled set (idempotent).

func (*EnvironmentEntry) GetEnv

func (e *EnvironmentEntry) GetEnv(key string) (string, bool)

GetEnv reads a value from the environment's persistent store.

func (*EnvironmentEntry) InvalidateStep

func (e *EnvironmentEntry) InvalidateStep(name, reason string) bool

InvalidateStep marks a completed step as invalidated so it will re-run. Returns true if the step was actually invalidated (was previously completed).

func (*EnvironmentEntry) InvalidateStepsFrom

func (e *EnvironmentEntry) InvalidateStepsFrom(stepName string, orderedSteps []string)

InvalidateStepsFrom removes checkpoint records for the named step and all steps that come after it in the given ordered step list.

func (*EnvironmentEntry) IsManagedWorktree

func (e *EnvironmentEntry) IsManagedWorktree() bool

IsManagedWorktree returns whether the worktree was created by previewctl.

func (*EnvironmentEntry) IsServiceEnabled

func (e *EnvironmentEntry) IsServiceEnabled(name string) bool

IsServiceEnabled returns true if the service is in the enabled set.

func (*EnvironmentEntry) SetEnv

func (e *EnvironmentEntry) SetEnv(key, value string)

SetEnv sets a key-value pair in the environment's persistent store.

func (*EnvironmentEntry) SetStepRecord

func (e *EnvironmentEntry) SetStepRecord(rec *StepRecord)

SetStepRecord records a step completion/failure.

func (*EnvironmentEntry) StepCompleted

func (e *EnvironmentEntry) StepCompleted(name string) bool

StepCompleted returns true if the named step has a "completed" record.

func (*EnvironmentEntry) StepOutputs

func (e *EnvironmentEntry) StepOutputs(name string) map[string]any

StepOutputs returns the outputs map for a completed step, or nil.

func (*EnvironmentEntry) WorktreePath

func (e *EnvironmentEntry) WorktreePath() string

WorktreePath returns the worktree path from ComputeAccessInfo, or empty string.

type EnvironmentMode

type EnvironmentMode string

EnvironmentMode represents the deployment mode of an environment.

const (
	ModeLocal   EnvironmentMode = "local"
	ModePreview EnvironmentMode = "preview"
	ModeSandbox EnvironmentMode = "sandbox"
)

type EnvironmentStatus

type EnvironmentStatus string

EnvironmentStatus represents the lifecycle status of an environment.

const (
	StatusCreating    EnvironmentStatus = "creating"
	StatusProvisioned EnvironmentStatus = "provisioned"
	StatusRunning     EnvironmentStatus = "running"
	StatusStopped     EnvironmentStatus = "stopped"
	StatusError       EnvironmentStatus = "error"
)

type InfraService

type InfraService struct {
	Name   string
	Image  string
	Port   int
	EnvVar string // e.g., "REDIS_PORT" (extracted from ${REDIS_PORT:-6379} patterns)
}

InfraService holds parsed infrastructure service information from a compose file.

type InfrastructureConfig

type InfrastructureConfig struct {
	ComposeFile string `yaml:"compose_file"`
}

InfrastructureConfig holds infrastructure configuration.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager orchestrates environment lifecycle by coordinating all ports.

func NewManager

func NewManager(deps ManagerDeps) *Manager

NewManager creates a new Manager with the given dependencies.

func (*Manager) Attach

func (m *Manager) Attach(ctx context.Context, envName string, worktreePath string) (*EnvironmentEntry, error)

Attach creates a preview environment using an existing worktree.

func (*Manager) BuildProvisionerStepOrder

func (m *Manager) BuildProvisionerStepOrder() []string

BuildProvisionerStepOrder returns the canonical step order for the provisioner phase.

func (*Manager) BuildRunnerStepOrder

func (m *Manager) BuildRunnerStepOrder() []string

BuildRunnerStepOrder returns the canonical step order for the runner phase.

func (*Manager) BuildSSHComputeAccess

func (m *Manager) BuildSSHComputeAccess(entry *EnvironmentEntry) ComputeAccess

buildSSHComputeAccess constructs SSH compute access from an environment entry. It resolves the SSH config template from the project config using store values, or falls back to stored proxy_command in metadata, or direct host connection.

func (*Manager) CoreInit

func (m *Manager) CoreInit(ctx context.Context, svcName string) error

func (*Manager) CoreReset

func (m *Manager) CoreReset(ctx context.Context, svcName, envName string, opts CoreResetOpts) error

func (*Manager) Destroy

func (m *Manager) Destroy(ctx context.Context, envName string, opts DestroyOptions) error

func (*Manager) DryRunStep

func (m *Manager) DryRunStep(ctx context.Context, envName, stepName string) (string, error)

DryRunStep shows a diff of what a step would change. For generation steps, compares the current file on the VM with what would be generated. For other steps, describes what would happen.

func (*Manager) GetEnvironment

func (m *Manager) GetEnvironment(ctx context.Context, envName string) (*EnvironmentEntry, error)

GetEnvironment returns a single environment entry from state, or nil if not found.

func (*Manager) Init

func (m *Manager) Init(ctx context.Context, envName, branch, baseBranch string) (*EnvironmentEntry, error)

Init creates a new environment end-to-end: provisions then runs. branch is the target branch. baseBranch is the branch to create from (empty = use branch as-is).

func (*Manager) List

func (m *Manager) List(ctx context.Context) ([]*EnvironmentEntry, error)

func (*Manager) PrintStep

func (m *Manager) PrintStep(ctx context.Context, envName, stepName string) (string, error)

PrintStep generates and returns the full content for a step without executing it.

func (*Manager) Provision

func (m *Manager) Provision(ctx context.Context, envName, branch, baseBranch, fromStep string) (*EnvironmentEntry, error)

Provision runs the provisioner phase only. Does NOT run the runner. fromStep invalidates that step and all subsequent steps, forcing re-execution.

func (*Manager) ProvisionAttach

func (m *Manager) ProvisionAttach(ctx context.Context, envName, worktreePath, fromStep string) (*EnvironmentEntry, error)

ProvisionAttach runs the provisioner phase on an existing worktree.

func (*Manager) Reconcile

func (m *Manager) Reconcile(ctx context.Context, envName string, dryRun ...bool) (*ReconcileReport, error)

Reconcile verifies all runner steps and re-executes any whose side effects are missing. Hook-owned steps are skipped since previewctl can't verify user-defined hooks.

Progress is reported via the ProgressReporter for each step:

  • StepStarted: "Verifying <step>..."
  • StepCompleted: verification passed or heal succeeded
  • StepFailed: heal failed
  • StepSkipped: hook-owned or never completed

func (*Manager) Run

func (m *Manager) Run(ctx context.Context, manifestPath, fromStep string) error

Run reads a manifest and executes the runner phase only. Stateless — does not persist state.

func (*Manager) RunCoreHook

func (m *Manager) RunCoreHook(ctx context.Context, svcName, action, envName string) (map[string]string, error)

func (*Manager) RunStep

func (m *Manager) RunStep(ctx context.Context, envName, stepName string) error

RunStep executes a single runner-phase step in isolation. Loads the environment from state, reconstructs compute access, reads the manifest, and runs only the specified step (always executes, ignoring cache).

func (*Manager) RunSteps

func (m *Manager) RunSteps(ctx context.Context, envName string, stepNames []string) error

RunSteps executes a sequence of runner steps in order, reusing a single SSH connection and step registry. All steps are forced (cache bypassed).

func (*Manager) SaveEnvironment

func (m *Manager) SaveEnvironment(ctx context.Context, envName string, entry *EnvironmentEntry) error

SaveEnvironment persists an environment entry to state.

func (*Manager) SetNoCache

func (m *Manager) SetNoCache(v bool)

SetNoCache sets whether checkpoint caching is disabled.

func (*Manager) SetStatus

func (m *Manager) SetStatus(ctx context.Context, envName string, status EnvironmentStatus) error

SetStatus updates an environment's status.

func (*Manager) Status

func (m *Manager) Status(ctx context.Context, envName string) (*EnvironmentDetail, error)

func (*Manager) SyncRemote

func (m *Manager) SyncRemote(ctx context.Context, envName string) error

SyncRemote regenerates every VM-side artifact we know how to regenerate (manifest, env files, compose, nginx) from current state and restarts every currently-enabled service. Useful as a recovery tool when the VM has drifted from state (e.g., a propagation failed mid-flight after a reset).

No-op for local environments. Unlike `refresh`, SyncRemote does not rerun runner_before / build_services / runner_deploy — it only reconciles what could have changed as a result of state mutations (creds, store values, port allocations, enabled-service set).

type ManagerDeps

type ManagerDeps struct {
	Compute     ComputePort
	Networking  NetworkingPort
	State       StatePort
	Progress    ProgressReporter
	Config      *ProjectConfig
	ProjectRoot string
	NoCache     bool
}

ManagerDeps holds the dependencies for creating a Manager.

type Manifest

type Manifest struct {
	Version            int                          `json:"version"`
	EnvName            string                       `json:"env_name"`
	ProjectName        string                       `json:"project_name"`
	Branch             string                       `json:"branch"`
	Mode               string                       `json:"mode"`
	Ports              PortMap                      `json:"ports"`
	ProvisionerOutputs map[string]map[string]string `json:"provisioner_outputs,omitempty"`
	Services           map[string]ManifestService   `json:"services,omitempty"`
	Infrastructure     *ManifestInfrastructure      `json:"infrastructure,omitempty"`
	EnabledServices    []string                     `json:"enabled_services,omitempty"`
}

Manifest is the single source of truth written to the compute location. It contains fully resolved values — no template variables.

func BuildManifest

func BuildManifest(
	cfg *ProjectConfig,
	envName, branch, mode string,
	ports PortMap,
	provisionerOutputs map[string]map[string]string,
	store map[string]string,
) (*Manifest, error)

BuildManifest resolves all template variables and builds a complete manifest.

func ReadManifest

func ReadManifest(r io.Reader) (*Manifest, error)

ReadManifest decodes a manifest from the given reader.

func (*Manifest) EnvFilePaths

func (m *Manifest) EnvFilePaths() map[string]map[string]string

EnvFilePaths returns a map of relative file paths to env var maps, grouping services that share the same env file path.

type ManifestInfrastructure

type ManifestInfrastructure struct {
	ComposeFile string `json:"compose_file"`
}

ManifestInfrastructure describes infrastructure configuration.

type ManifestService

type ManifestService struct {
	Path    string            `json:"path"`
	EnvFile string            `json:"env_file"`
	Env     map[string]string `json:"env,omitempty"`
}

ManifestService describes a service with fully resolved env vars.

type NetworkingPort

type NetworkingPort interface {
	// AllocatePorts returns deterministic port assignments for all services and infrastructure.
	AllocatePorts(envName string) (PortMap, error)

	// GetServiceURL returns the URL to reach a named service in the environment.
	GetServiceURL(envName string, service string) (string, error)
}

NetworkingPort handles port allocation and service URL resolution. Local: deterministic offset from base ports. Preview: reverse proxy with subdomain routing (future).

type NoopReporter

type NoopReporter struct{}

NoopReporter is a ProgressReporter that discards all events.

func (NoopReporter) OnStep

func (NoopReporter) OnStep(StepEvent)

func (NoopReporter) StderrWriter

func (NoopReporter) StderrWriter() io.Writer

type PortMap

type PortMap map[string]int

PortMap maps service names to allocated ports.

func AllocatePortBlock

func AllocatePortBlock(envName string, serviceNames []string) (PortMap, error)

AllocatePortBlock selects a block of ports for an environment and assigns one port per service name. Ports are checked for availability. Returns a PortMap with service names mapped to allocated ports.

type ProgressReporter

type ProgressReporter interface {
	OnStep(event StepEvent)
	// StderrWriter returns a writer for hook stderr output.
	// The reporter may indent or buffer this output for display.
	StderrWriter() io.Writer
}

ProgressReporter receives lifecycle step events from the manager. Inbound adapters implement this to render progress (CLI spinners, SSE, etc).

type ProjectConfig

type ProjectConfig struct {
	Version        int                      `yaml:"version"`
	Name           string                   `yaml:"name"`
	Provisioner    ProvisionerConfig        `yaml:"provisioner"`
	Infrastructure *InfrastructureConfig    `yaml:"infrastructure,omitempty"`
	Services       map[string]ServiceConfig `yaml:"services"`
	Runner         *RunnerConfig            `yaml:"runner,omitempty"`

	// Mode is the deployment mode (e.g., "local"). Set at load time, not from YAML.
	Mode string `yaml:"-"`

	// InfraServices is populated by parsing the compose file referenced in
	// Infrastructure.ComposeFile. It is not read from YAML directly.
	InfraServices map[string]InfraService `yaml:"-"`
}

ProjectConfig is the top-level previewctl.yaml configuration.

func LoadConfig

func LoadConfig(path string) (*ProjectConfig, error)

LoadConfig reads and parses a previewctl.yaml file.

func LoadConfigWithOverlay

func LoadConfigWithOverlay(basePath, mode string) (*ProjectConfig, error)

LoadConfigWithOverlay loads a base config and merges a mode-specific overlay if present.

func ParseConfig

func ParseConfig(data []byte) (*ProjectConfig, error)

ParseConfig parses YAML bytes into a ProjectConfig.

func (*ProjectConfig) ServiceNames

func (c *ProjectConfig) ServiceNames() []string

ServiceNames returns a sorted list of all service and infrastructure names that need port assignments.

type ProvisionerConfig

type ProvisionerConfig struct {
	Before   string                              `yaml:"before,omitempty"`
	After    string                              `yaml:"after,omitempty"`
	Compute  *ComputeHooks                       `yaml:"compute,omitempty"`
	Services map[string]ProvisionerServiceConfig `yaml:"services,omitempty"`
}

ProvisionerConfig holds managed provisioner services with hook-driven lifecycle.

type ProvisionerServiceConfig

type ProvisionerServiceConfig struct {
	Outputs []string `yaml:"outputs,omitempty"`
	Init    string   `yaml:"init,omitempty"`
	Seed    string   `yaml:"seed,omitempty"`
	Reset   string   `yaml:"reset,omitempty"`
	Destroy string   `yaml:"destroy,omitempty"`
}

ProvisionerServiceConfig defines a provisioner service managed by hooks.

type ProxyConfig

type ProxyConfig struct {
	Enabled *bool  `yaml:"enabled,omitempty"` // defaults to true if omitted
	Domain  string `yaml:"domain"`            // e.g., "preview.airgoods.com"
	Type    string `yaml:"type,omitempty"`    // "nginx" (default). Future: "traefik", "caddy"
}

ProxyConfig defines the reverse proxy that sits in front of preview services.

func (*ProxyConfig) IsEnabled

func (p *ProxyConfig) IsEnabled() bool

IsEnabled returns whether the proxy is enabled (defaults to true).

func (*ProxyConfig) ResolvedType

func (p *ProxyConfig) ResolvedType() string

ResolvedType returns the proxy type, defaulting to "nginx".

type ReconcileReport

type ReconcileReport struct {
	Results []ReconcileResult
	Healed  int
	Failed  int
	OK      int
	Skipped int
	NotRun  int
}

ReconcileReport is the full outcome of a reconcile run.

type ReconcileResult

type ReconcileResult struct {
	Step       string
	Action     string // "ok", "healed", "failed", "skipped", "not_run"
	Message    string
	DurationMs int64
}

ReconcileResult describes the outcome of reconciling a single step.

type RunnerConfig

type RunnerConfig struct {
	Before CommandHook `yaml:"before,omitempty"`
	// Build, when set, runs once during build_services in place of the
	// per-service Build loop. Intended for monorepo tools (turborepo, nx,
	// lerna) where one bulk command builds everything more efficiently
	// than N filtered invocations.
	Build   CommandHook    `yaml:"build,omitempty"`
	Deploy  CommandHook    `yaml:"deploy,omitempty"`
	Destroy CommandHook    `yaml:"destroy,omitempty"`
	After   CommandHook    `yaml:"after,omitempty"`
	Compose *ComposeConfig `yaml:"compose,omitempty"`
}

RunnerConfig holds runner lifecycle hooks.

type SSHComputeAccessOpts

type SSHComputeAccessOpts struct {
	Host         string // SSH host (direct mode) or logical hostname (proxy mode)
	User         string
	Root         string
	ProxyCommand string // when set, uses -o ProxyCommand=... instead of relying on SSH config
	IdentityFile string // path to SSH private key (optional)
}

SSHComputeAccessOpts configures SSH compute access creation.

type SSHConfig

type SSHConfig struct {
	// ProxyCommand is the SSH ProxyCommand used to tunnel into the VM.
	// Example: "gcloud compute start-iap-tunnel {{store.VM_NAME}} %p --listen-on-stdin --zone={{store.GCP_ZONE}} --project={{store.GCP_PROJECT}}"
	ProxyCommand string `yaml:"proxy_command"`
	// User is the SSH username. Example: "{{store.SSH_USER}}"
	User string `yaml:"user"`
	// UserCommand is a shell command that resolves the SSH username dynamically.
	// Useful when different users (CI vs human) SSH into the same VM.
	// Example: "gcloud compute os-login describe-profile --format='value(posixAccounts[0].username)'"
	// Takes precedence over User when set.
	UserCommand string `yaml:"user_command"`
	// IdentityFile is the path to the SSH private key. Supports ~ expansion.
	// Example: "~/.ssh/google_compute_engine"
	IdentityFile string `yaml:"identity_file"`
	// Root is the remote working directory. Defaults to "/app".
	Root string `yaml:"root,omitempty"`
}

SSHConfig defines how previewctl connects to remote compute via SSH. All fields support {{store.KEY}} template resolution.

type ServiceConfig

type ServiceConfig struct {
	Path      string            `yaml:"path"`
	Port      int               `yaml:"port,omitempty"` // fixed port — skips the port allocator when set
	Command   string            `yaml:"command,omitempty"`
	DependsOn []string          `yaml:"depends_on,omitempty"`
	Env       map[string]string `yaml:"env,omitempty"`
	EnvFile   string            `yaml:"env_file,omitempty"` // relative to path, defaults to ".env.local"
	Build     string            `yaml:"build,omitempty"`    // build command (run on host before container starts)
	Start     string            `yaml:"start,omitempty"`    // start command (run inside container). Required for compose generation.
	Proxy     []ServiceProxy    `yaml:"proxy,omitempty"`    // optional reverse proxy rules for nginx
}

ServiceConfig defines an application service.

func (ServiceConfig) ResolvedEnvFile

func (s ServiceConfig) ResolvedEnvFile() string

ResolvedEnvFile returns the env file path relative to the service path. Defaults to ".env.local" if not configured.

type ServiceProxy

type ServiceProxy struct {
	Path       string             `yaml:"path"`                  // source path the browser sends, e.g., "/api" or "/iapi"
	TargetPath string             `yaml:"target_path,omitempty"` // path rewritten to on the target service. Defaults to Path if omitted.
	To         ServiceProxyTarget `yaml:"to"`
}

ServiceProxy defines a reverse proxy rule on a service's subdomain. When configured, nginx generates a location block that proxies the given path to the target service's port (same-origin for IAP cookie compatibility).

func (*ServiceProxy) ResolvedTargetPath

func (p *ServiceProxy) ResolvedTargetPath() string

ResolvedTargetPath returns the target path, defaulting to Path if not set.

type ServiceProxyTarget

type ServiceProxyTarget struct {
	Service string `yaml:"service"` // target service name, resolved to its port at generation time
}

ServiceProxyTarget identifies the target service for a proxy rule.

type State

type State struct {
	Version      int                          `json:"version"`
	Environments map[string]*EnvironmentEntry `json:"environments"`
}

State is the top-level persisted state.

func NewState

func NewState() *State

NewState returns an initialized empty state.

type StatePort

type StatePort interface {
	// Load returns the full state.
	Load(ctx context.Context) (*State, error)

	// Save persists the full state.
	Save(ctx context.Context, state *State) error

	// GetEnvironment returns a single environment entry, or nil if not found.
	GetEnvironment(ctx context.Context, name string) (*EnvironmentEntry, error)

	// SetEnvironment creates or updates an environment entry.
	SetEnvironment(ctx context.Context, name string, entry *EnvironmentEntry) error

	// RemoveEnvironment deletes an environment entry.
	RemoveEnvironment(ctx context.Context, name string) error
}

StatePort persists previewctl state. File-based for POC; interface accommodates Postgres/etcd later.

type StepEvent

type StepEvent struct {
	Step    string // e.g. "allocate_ports", "create_worktree", "create_database"
	Status  StepStatus
	Message string // human-readable detail
	Error   error  // non-nil when Status == StepFailed
}

StepEvent is emitted by the manager at each lifecycle transition.

type StepOpts

type StepOpts struct {
	Name        string
	StartMsg    string
	CompleteMsg *string
	Fn          func() error
	Verify      VerifyFunc            // nil = pure, skip on checkpoint alone
	Outputs     func() map[string]any // capture outputs after success
	AllowCache  *bool                 // nil = true; false = always execute
}

StepOpts configures step execution behavior.

type StepRecord

type StepRecord struct {
	Name       string           `json:"name"`
	Status     StepRecordStatus `json:"status"`
	StartedAt  time.Time        `json:"startedAt"`
	FinishedAt time.Time        `json:"finishedAt"`
	DurationMs int64            `json:"durationMs"`
	Machine    string           `json:"machine"`
	Error      string           `json:"error,omitempty"`
	Outputs    map[string]any   `json:"outputs,omitempty"`
}

StepRecord is the checkpoint persisted for a single step execution.

type StepRecordStatus

type StepRecordStatus string

StepRecordStatus represents the persisted outcome of a step.

const (
	StepRecordCompleted   StepRecordStatus = "completed"
	StepRecordFailed      StepRecordStatus = "failed"
	StepRecordInvalidated StepRecordStatus = "invalidated"
)

type StepStatus

type StepStatus string

StepStatus represents the status of a lifecycle step.

const (
	StepStarted   StepStatus = "started"
	StepCompleted StepStatus = "completed"
	StepFailed    StepStatus = "failed"
	StepSkipped   StepStatus = "skipped"
	StepStreaming StepStatus = "streaming" // stop spinner, hook will stream its own output
)

type TemplateContext

type TemplateContext struct {
	ServicePorts       PortMap
	InfraPorts         PortMap
	ProvisionerOutputs map[string]map[string]string
	CurrentService     string            // set per-service during rendering, enables {{self.port}}
	EnvName            string            // environment name, enables {{env.name}}
	Store              map[string]string // persistent key-value store, enables {{store.KEY}}
	ProxyDomain        string            // proxy domain (e.g., "preview.airgoods.com"), enables {{proxy.*}}
}

TemplateContext holds the values available for template substitution.

type VMInfo

type VMInfo struct {
	ID         string
	ExternalIP string
	Status     string
}

VMInfo describes a provisioned VM (future).

type VMSpec

type VMSpec struct {
	MachineType string
	DiskSizeGB  int
	Image       string
	Region      string
}

VMSpec defines requirements for provisioning a VM (future).

type ValidationError

type ValidationError struct {
	Errors []string
}

ValidationError collects multiple validation issues.

func (*ValidationError) Error

func (e *ValidationError) Error() string

type VerifyFunc

type VerifyFunc func(ctx context.Context) error

VerifyFunc checks that a previously-completed step's side effects still hold.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL