fa6d5bd3ba
Secrets defined once and applied to many workloads by scope (global or per-app), encrypted at rest and resolved into container env as a low-precedence default layer: global-shared < app-shared < image cfg.Env < workload_env. A workload with no applicable shared secrets is byte-identical to the prior workload_env-only behavior. - store: shared_secrets table + CRUD + ListApplicableSharedSecrets (enabled global + app, global-first), UNIQUE(scope,app_id,name). - plugin.ResolveSharedSecrets + integration into BuildWorkloadEnv (static/dockerfile) and image buildEnv; best-effort — a shared-secret store/decrypt error never fails a deploy, and values are never logged. - REST CRUD at /api/shared-secrets (reads authed, mutations AdminOnly); values encrypted at the boundary via crypto.Encrypt and never returned (only a has_value flag), mirroring workload_env. UNIQUE collisions 409. Compose is out of scope (YAML-defined env). Frontend rule UI is Phase 2. Reviewed: go + security APPROVE (0 CRITICAL/HIGH); two MEDIUMs fixed (translateSQLError -> 409, no driver-message leak). Deferred defense-in- depth: json:"-" on the model value + a description length cap.
495 lines
22 KiB
Go
495 lines
22 KiB
Go
package store
|
||
|
||
// Registry represents a container image registry.
|
||
type Registry struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
URL string `json:"url"`
|
||
Type string `json:"type"`
|
||
Token string `json:"token"`
|
||
Owner string `json:"owner"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// Settings holds global application configuration (single-row pattern).
|
||
type Settings struct {
|
||
Domain string `json:"domain"`
|
||
ServerIP string `json:"server_ip"` // Docker host IP (for NPM remote forwarding)
|
||
PublicIP string `json:"public_ip"` // Public-facing IP for DNS A records (e.g., NPM/proxy host)
|
||
Network string `json:"network"`
|
||
SubdomainPattern string `json:"subdomain_pattern"`
|
||
NotificationURL string `json:"notification_url"`
|
||
NotificationSecret string `json:"-"` // outgoing-webhook signing secret; never serialized directly
|
||
NpmURL string `json:"npm_url"`
|
||
NpmEmail string `json:"npm_email"`
|
||
NpmPassword string `json:"npm_password"`
|
||
PollingInterval string `json:"polling_interval"`
|
||
BaseVolumePath string `json:"base_volume_path"`
|
||
SSLCertificateID int `json:"ssl_certificate_id"`
|
||
StaleThresholdDays int `json:"stale_threshold_days"`
|
||
AllowedVolumePaths string `json:"allowed_volume_paths"` // JSON array of allowed absolute paths
|
||
WildcardDNS bool `json:"wildcard_dns"`
|
||
DNSProvider string `json:"dns_provider"`
|
||
CloudflareAPIToken string `json:"cloudflare_api_token"`
|
||
CloudflareZoneID string `json:"cloudflare_zone_id"`
|
||
NpmRemote bool `json:"npm_remote"`
|
||
NpmAccessListID int `json:"npm_access_list_id"`
|
||
ProxyProvider string `json:"proxy_provider"`
|
||
TraefikEntrypoint string `json:"traefik_entrypoint"`
|
||
TraefikCertResolver string `json:"traefik_cert_resolver"`
|
||
TraefikNetwork string `json:"traefik_network"`
|
||
TraefikAPIURL string `json:"traefik_api_url"`
|
||
ImagePruneThresholdMB int `json:"image_prune_threshold_mb"`
|
||
BackupEnabled bool `json:"backup_enabled"`
|
||
BackupIntervalHours int `json:"backup_interval_hours"`
|
||
BackupRetentionCount int `json:"backup_retention_count"`
|
||
// AutoBackupBeforeDeploy creates a "pre-deploy" Tinyforge DB backup
|
||
// at the start of every project deploy. Independent of BackupEnabled
|
||
// (which governs the periodic auto-backup cron).
|
||
AutoBackupBeforeDeploy bool `json:"auto_backup_before_deploy"`
|
||
StatsIntervalSeconds int `json:"stats_interval_seconds"` // 0 disables collection
|
||
StatsRetentionHours int `json:"stats_retention_hours"` // 0 disables collection
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// ContainerStatsSample is one persisted sample of container resource usage.
|
||
// Cumulative counters (network, block I/O) require differencing two samples
|
||
// to get rates; CPU is already a percent-since-previous-sample value.
|
||
type ContainerStatsSample struct {
|
||
ContainerID string `json:"container_id"`
|
||
OwnerType string `json:"owner_type"` // "instance" or "site"
|
||
OwnerID string `json:"owner_id"`
|
||
TS int64 `json:"ts"` // Unix seconds UTC
|
||
CPUPercent float64 `json:"cpu_percent"`
|
||
MemoryUsage int64 `json:"memory_usage"`
|
||
MemoryLimit int64 `json:"memory_limit"`
|
||
NetworkRxBytes int64 `json:"network_rx_bytes"`
|
||
NetworkTxBytes int64 `json:"network_tx_bytes"`
|
||
BlockReadBytes int64 `json:"block_read_bytes"`
|
||
BlockWriteBytes int64 `json:"block_write_bytes"`
|
||
}
|
||
|
||
// SystemStatsSample is one persisted host-level snapshot that aggregates
|
||
// workload usage across all containers plus daemon capacity + disk totals.
|
||
type SystemStatsSample struct {
|
||
TS int64 `json:"ts"` // Unix seconds UTC
|
||
NCPU int `json:"ncpu"`
|
||
MemoryTotal int64 `json:"memory_total"`
|
||
WorkloadCPUPercent float64 `json:"workload_cpu_percent"`
|
||
WorkloadMemUsage int64 `json:"workload_mem_usage"`
|
||
ContainersRunning int `json:"containers_running"`
|
||
DiskTotalBytes int64 `json:"disk_total_bytes"`
|
||
}
|
||
|
||
// Backup represents a backup metadata record.
|
||
type Backup struct {
|
||
ID string `json:"id"`
|
||
Filename string `json:"filename"`
|
||
SizeBytes int64 `json:"size_bytes"`
|
||
BackupType string `json:"backup_type"` // "manual" or "auto"
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
|
||
// DNSRecord tracks a DNS record managed by the application.
|
||
type DNSRecord struct {
|
||
ID string `json:"id"`
|
||
FQDN string `json:"fqdn"`
|
||
ProviderRecordID string `json:"provider_record_id"`
|
||
ConsumerType string `json:"consumer_type"` // "instance" or "standalone"
|
||
ConsumerID string `json:"consumer_id"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// ProxyRoute shapes one proxy-enabled container row for the Proxies
|
||
// page. The legacy field names (ProjectID, ProjectName, StageID,
|
||
// StageName, InstanceID) are retained verbatim for the existing
|
||
// frontend contract — after the workload-first cutover they map to:
|
||
//
|
||
// ProjectID/Name → workload id / workload name
|
||
// StageID/Name → containers.stage_id / containers.role
|
||
// InstanceID → container row id
|
||
// Source → "instance" for image/compose, "static_site" for static
|
||
//
|
||
// Renaming would require a coordinated frontend change; deferred.
|
||
type ProxyRoute struct {
|
||
Source string `json:"source"`
|
||
InstanceID string `json:"instance_id"`
|
||
ProjectID string `json:"project_id"`
|
||
ProjectName string `json:"project_name"`
|
||
StageID string `json:"stage_id"`
|
||
StageName string `json:"stage_name"`
|
||
ImageTag string `json:"image_tag"`
|
||
Subdomain string `json:"subdomain"`
|
||
Domain string `json:"domain"`
|
||
ContainerID string `json:"container_id"`
|
||
Port int `json:"port"`
|
||
ProxyRouteID string `json:"proxy_route_id"`
|
||
NpmProxyID int `json:"npm_proxy_id"`
|
||
Status string `json:"status"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
|
||
// WorkloadVolume is the plugin-shape equivalent of legacy Volume: a
|
||
// per-workload mount declaration. The Scope enum matches the existing
|
||
// VolumeScope contract so the legacy resolver can be reused once its
|
||
// project_id assumption is loosened.
|
||
type WorkloadVolume struct {
|
||
ID string `json:"id"`
|
||
WorkloadID string `json:"workload_id"`
|
||
Source string `json:"source"`
|
||
Target string `json:"target"`
|
||
Scope string `json:"scope"`
|
||
Name string `json:"name"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// WorkloadEnv is the plugin-shape equivalent of StageEnv: per-workload
|
||
// environment variable overrides, optionally encrypted at rest. Read by
|
||
// the Source plugin at deploy time, merged on top of source_config.env.
|
||
type WorkloadEnv struct {
|
||
ID string `json:"id"`
|
||
WorkloadID string `json:"workload_id"`
|
||
Key string `json:"key"`
|
||
Value string `json:"value"`
|
||
Encrypted bool `json:"encrypted"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// SharedSecret is an env var shared across workloads by scope. Resolved
|
||
// into a workload's container env as a low-precedence default (overridden
|
||
// by image cfg.Env and workload_env).
|
||
type SharedSecret struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"` // the env KEY
|
||
Value string `json:"value"` // ciphertext when Encrypted; never returned decrypted by the API
|
||
Encrypted bool `json:"encrypted"`
|
||
Scope string `json:"scope"` // global | app
|
||
AppID string `json:"app_id"` // set when scope == app; "" for global
|
||
Description string `json:"description"`
|
||
Enabled bool `json:"enabled"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// Shared-secret scope enum: a secret is either applied to every workload
|
||
// (global) or only to workloads whose app_id matches (app).
|
||
const (
|
||
SharedSecretScopeGlobal = "global"
|
||
SharedSecretScopeApp = "app"
|
||
)
|
||
|
||
// VolumeScope defines the sharing scope for a volume mount.
|
||
// Valid scopes: instance, stage, project, project_named, named, ephemeral.
|
||
type VolumeScope string
|
||
|
||
const (
|
||
VolumeScopeInstance VolumeScope = "instance"
|
||
VolumeScopeStage VolumeScope = "stage"
|
||
VolumeScopeProject VolumeScope = "project"
|
||
VolumeScopeProjectNamed VolumeScope = "project_named"
|
||
VolumeScopeNamed VolumeScope = "named"
|
||
VolumeScopeEphemeral VolumeScope = "ephemeral"
|
||
VolumeScopeAbsolute VolumeScope = "absolute"
|
||
)
|
||
|
||
// ValidVolumeScopes contains all valid scope values for validation.
|
||
var ValidVolumeScopes = []VolumeScope{
|
||
VolumeScopeInstance, VolumeScopeStage, VolumeScopeProject,
|
||
VolumeScopeProjectNamed, VolumeScopeNamed, VolumeScopeEphemeral,
|
||
VolumeScopeAbsolute,
|
||
}
|
||
|
||
// IsValidVolumeScope returns true if the given string is a valid scope.
|
||
func IsValidVolumeScope(s string) bool {
|
||
for _, v := range ValidVolumeScopes {
|
||
if string(v) == s {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// EventLog represents a persistent event log entry.
|
||
type EventLog struct {
|
||
ID int64 `json:"id"`
|
||
Source string `json:"source"`
|
||
WorkloadID string `json:"workload_id"` // "" = unscoped (non-deploy events)
|
||
Severity string `json:"severity"` // info, warn, error
|
||
Message string `json:"message"`
|
||
Metadata string `json:"metadata"` // JSON-encoded structured data
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
|
||
// EventTrigger is a filter+action rule evaluated against EventLog
|
||
// entries published on the bus. When all non-empty filters match, the
|
||
// trigger fires its configured action (webhook today, additional action
|
||
// types extensible via the ActionType enum).
|
||
//
|
||
// Filter fields use a comma-separated list shape for multi-value
|
||
// filters (severity, source) to keep the schema flat — empty string
|
||
// means "no filter on this dimension." FilterMessageRegex is a single
|
||
// regex evaluated against EventLog.Message.
|
||
//
|
||
// Loop-prevention: deliveries are recorded in webhook_deliveries (the
|
||
// existing audit trail). The dispatcher MUST NOT write to event_log
|
||
// or it will recurse.
|
||
type EventTrigger struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
FilterSeverity string `json:"filter_severity"` // comma list: "warn,error"; "" = any
|
||
FilterSource string `json:"filter_source"` // comma list: "logscan,deploy"; "" = any
|
||
FilterMessageRegex string `json:"filter_message_regex"` // "" = any
|
||
ActionType string `json:"action_type"` // "webhook" today
|
||
ActionTarget string `json:"action_target"` // URL for webhook
|
||
ActionSecret string `json:"action_secret"` // optional HMAC secret for signed delivery
|
||
Enabled bool `json:"enabled"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// EventTriggerActionType enumerates the supported action_type values.
|
||
// Adding a new action is additive — old triggers keep working, the
|
||
// dispatcher just learns a new branch.
|
||
const (
|
||
EventTriggerActionWebhook = "webhook"
|
||
)
|
||
|
||
// LogScanRule is one regex-based pattern the log scanner evaluates
|
||
// against container log lines. The (workload_id, overrides_id) pair
|
||
// implements the "global rule with optional per-workload override"
|
||
// pattern documented in docs/LOGSCAN_AND_TRIGGERS_TODO.md:
|
||
//
|
||
// - WorkloadID == "" && OverridesID == 0 → global rule, applies to
|
||
// every workload unless overridden.
|
||
// - WorkloadID != "" && OverridesID == 0 → workload-only addition.
|
||
// - WorkloadID != "" && OverridesID != 0 → override of the named
|
||
// global rule for one workload (Enabled=false to disable globally
|
||
// for this workload).
|
||
type LogScanRule struct {
|
||
ID int64 `json:"id"`
|
||
WorkloadID string `json:"workload_id"` // "" = global
|
||
OverridesID int64 `json:"overrides_id"` // 0 = not an override
|
||
Name string `json:"name"`
|
||
Pattern string `json:"pattern"` // regex, compiled at load
|
||
Severity string `json:"severity"` // info|warn|error
|
||
Streams string `json:"streams"` // all|stdout|stderr
|
||
CooldownSeconds int `json:"cooldown_seconds"`
|
||
Enabled bool `json:"enabled"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// Log scan stream filter values. "all" reads both streams; "stdout"
|
||
// or "stderr" filter to one. Used both for store validation and at
|
||
// docker-side log read time.
|
||
const (
|
||
LogScanStreamAll = "all"
|
||
LogScanStreamStdout = "stdout"
|
||
LogScanStreamStderr = "stderr"
|
||
)
|
||
|
||
// Log scan severity values mirror the event_log enum so a matched
|
||
// rule lands as an event_log row with the rule's severity verbatim.
|
||
const (
|
||
LogScanSeverityInfo = "info"
|
||
LogScanSeverityWarn = "warn"
|
||
LogScanSeverityError = "error"
|
||
)
|
||
|
||
// MetricAlertRule fires an event when a container metric breaches a
|
||
// threshold. Mirrors LogScanRule but evaluated against stats_samples
|
||
// instead of log lines.
|
||
type MetricAlertRule struct {
|
||
ID int64 `json:"id"`
|
||
WorkloadID string `json:"workload_id"` // "" = applies to all workloads
|
||
Name string `json:"name"`
|
||
Metric string `json:"metric"` // cpu_percent | memory_percent | memory_bytes
|
||
Comparator string `json:"comparator"` // gt | lt
|
||
Threshold float64 `json:"threshold"`
|
||
Severity string `json:"severity"` // info | warn | error
|
||
CooldownSeconds int `json:"cooldown_seconds"` // min seconds between fires per (rule,workload)
|
||
Enabled bool `json:"enabled"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// Metric-alert metric identifiers. cpu_percent + memory_percent are
|
||
// 0–100 ratios; memory_bytes is an absolute usage figure. Validated in
|
||
// the store on create/update.
|
||
const (
|
||
MetricCPUPercent = "cpu_percent"
|
||
MetricMemoryPercent = "memory_percent"
|
||
MetricMemoryBytes = "memory_bytes"
|
||
)
|
||
|
||
// Metric-alert comparators. gt fires when the value exceeds the
|
||
// threshold; lt when it falls below.
|
||
const (
|
||
MetricComparatorGT = "gt"
|
||
MetricComparatorLT = "lt"
|
||
)
|
||
|
||
// WorkloadKind enumerates the legacy discriminator values written into
|
||
// containers.workload_kind and workloads.kind. After the hard cutover the
|
||
// backing project / stack / static_site tables are gone — these constants
|
||
// are just strings used to filter the unified containers index in the UI.
|
||
//
|
||
// `build` is the dockerfile-source kind: a container built from a
|
||
// Dockerfile in a Git repo. Operationally it looks like a site (one
|
||
// container, one optional public face) but its origin is the build
|
||
// pipeline, not a static-asset extract. Dashboard filters that need to
|
||
// distinguish "I built this from source" from "I served files from a
|
||
// repo" should key on this value.
|
||
type WorkloadKind string
|
||
|
||
const (
|
||
WorkloadKindProject WorkloadKind = "project"
|
||
WorkloadKindStack WorkloadKind = "stack"
|
||
WorkloadKindSite WorkloadKind = "site"
|
||
WorkloadKindBuild WorkloadKind = "build"
|
||
)
|
||
|
||
// Workload is the unifying primitive that abstracts Project, Stack, and StaticSite.
|
||
// Each row is paired with exactly one project/stack/site via (Kind, RefID).
|
||
// Notification + webhook config moves here so it lives in one place across kinds.
|
||
//
|
||
// SourceKind / SourceConfig / TriggerKind / TriggerConfig / PublicFaces /
|
||
// ParentWorkloadID populate the unified plugin model from the Workload-first
|
||
// refactor. Existing rows keep these empty until they are explicitly migrated
|
||
// or replaced — the legacy Kind/RefID columns continue to point at
|
||
// project/stack/site rows in parallel during the cutover.
|
||
type Workload struct {
|
||
ID string `json:"id"`
|
||
Kind string `json:"kind"` // project | stack | site (legacy discriminator)
|
||
RefID string `json:"ref_id"`
|
||
Name string `json:"name"`
|
||
AppID string `json:"app_id"` // nullable; "" = unassigned (a.k.a. GroupID after rename)
|
||
SourceKind string `json:"source_kind"` // "" until plugin-mode populated
|
||
SourceConfig string `json:"source_config"` // JSON-encoded, decoded by the matching Source
|
||
TriggerKind string `json:"trigger_kind"`
|
||
TriggerConfig string `json:"trigger_config"` // JSON-encoded, decoded by the matching Trigger
|
||
PublicFaces string `json:"public_faces"` // JSON-encoded []PublicFace
|
||
ParentWorkloadID string `json:"parent_workload_id"` // "" = root; non-empty = stage chain
|
||
NotificationURL string `json:"notification_url"`
|
||
NotificationSecret string `json:"-"` // never serialized
|
||
WebhookSecret string `json:"-"` // URL-identifier secret; never serialized
|
||
WebhookSigningSecret string `json:"-"` // HMAC key; never serialized
|
||
WebhookRequireSignature bool `json:"webhook_require_signature"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// WorkloadNotification is one configured outbound notification route for
|
||
// a workload. Multiple rows per workload model the "one Slack channel
|
||
// for failures, one Discord webhook for successes" routing the legacy
|
||
// single notification_url column could not express.
|
||
//
|
||
// EventTypes is a comma-separated allow-list (e.g. "build_failure" or
|
||
// "deploy_success,deploy_failure"). An empty EventTypes means the row
|
||
// fires for every event type — the cheapest way to keep the existing
|
||
// single-destination behaviour expressible in the new shape.
|
||
//
|
||
// Secret round-trips through the same crypto envelope as other stored
|
||
// secrets; the API layer strips it from responses.
|
||
type WorkloadNotification struct {
|
||
ID string `json:"id"`
|
||
WorkloadID string `json:"workload_id"`
|
||
Name string `json:"name"`
|
||
URL string `json:"url"`
|
||
Secret string `json:"-"`
|
||
EventTypes string `json:"event_types"`
|
||
Enabled bool `json:"enabled"`
|
||
SortOrder int `json:"sort_order"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// Container is the normalized index of every Tinyforge-managed container.
|
||
// Replaces the project-specific Instance table after migration. Subdomain/
|
||
// proxy fields are hoisted as first-class columns because ListProxyRoutes,
|
||
// stale detection, and dashboard queries filter on them frequently.
|
||
//
|
||
// StageID is populated by the deployer for project containers so ListProxyRoutes
|
||
// survives stage renames; it stays empty for stack and site rows.
|
||
type Container struct {
|
||
ID string `json:"id"`
|
||
WorkloadID string `json:"workload_id"`
|
||
WorkloadKind string `json:"workload_kind"` // denormalized for filtered queries
|
||
Role string `json:"role"` // stage name (project), service name (stack), '' (site)
|
||
StageID string `json:"stage_id"` // project containers only; '' otherwise
|
||
ContainerID string `json:"container_id"` // Docker container ID; '' between create+start
|
||
ImageRef string `json:"image_ref"` // "image:tag" as scheduled
|
||
ImageTag string `json:"image_tag"` // just the tag, for ListProxyRoutes
|
||
Host string `json:"host"`
|
||
State string `json:"state"` // running | stopped | failed | removing | missing
|
||
Port int `json:"port"`
|
||
Subdomain string `json:"subdomain"`
|
||
ProxyRouteID string `json:"proxy_route_id"`
|
||
NpmProxyID int `json:"npm_proxy_id"`
|
||
LastSeenAt string `json:"last_seen_at"`
|
||
// ExtraJSON carries source-specific metadata that isn't promoted to a
|
||
// first-class column — currently per-face proxy route IDs for
|
||
// multi-face image deploys. Stored as a JSON object; '{}' on empty
|
||
// rows. Sources own the shape; consumers should tolerate unknown
|
||
// keys.
|
||
ExtraJSON string `json:"extra_json"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// Trigger is a first-class redeploy signal source. Triggers were embedded
|
||
// in workload rows (workload.trigger_kind / trigger_config) until the
|
||
// trigger-split refactor; they are now standalone records bound to
|
||
// workloads via WorkloadTriggerBinding so a single trigger (a webhook,
|
||
// registry watcher, schedule, git push) can fan out to many workloads.
|
||
//
|
||
// Webhook secrets live here, not on the workload — the inbound webhook
|
||
// URL identifies a trigger, which then resolves its bindings to decide
|
||
// which workloads to fire.
|
||
type Trigger struct {
|
||
ID string `json:"id"`
|
||
Kind string `json:"kind"` // registry | git | manual | schedule | log_scan | ...
|
||
Name string `json:"name"` // human-readable, unique
|
||
Config string `json:"config"` // JSON-encoded, decoded by the matching plugin
|
||
WebhookSecret string `json:"-"` // URL-identifier secret; never serialized
|
||
WebhookSigningSecret string `json:"-"` // HMAC key; never serialized
|
||
WebhookRequireSignature bool `json:"webhook_require_signature"`
|
||
// LastFiredAt is the RFC3339 wall-clock the scheduler last dispatched
|
||
// this trigger. Empty for never-fired or non-schedule triggers. The
|
||
// scheduler reads + writes this column to decide next-fire windows
|
||
// and to surface "last fired" on the trigger detail page.
|
||
LastFiredAt string `json:"last_fired_at,omitempty"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// WorkloadTriggerBinding joins a Workload to a Trigger. BindingConfig is
|
||
// the per-binding override applied on top of Trigger.Config (top-level
|
||
// JSON merge: binding fields win). Empty BindingConfig means "use the
|
||
// trigger's config verbatim". Enabled false skips the binding without
|
||
// deleting it (useful for paused stages).
|
||
type WorkloadTriggerBinding struct {
|
||
ID string `json:"id"`
|
||
WorkloadID string `json:"workload_id"`
|
||
TriggerID string `json:"trigger_id"`
|
||
BindingConfig string `json:"binding_config"` // JSON-encoded; "{}" = none
|
||
Enabled bool `json:"enabled"`
|
||
SortOrder int `json:"sort_order"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|
||
|
||
// App is an optional grouping of workloads (e.g., "my-saas" = web project + worker stack + redis stack).
|
||
// Schema lives here from day one so future UI work is unblocked, but no UI is wired in v1.
|
||
type App struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Description string `json:"description"`
|
||
CreatedAt string `json:"created_at"`
|
||
UpdatedAt string `json:"updated_at"`
|
||
}
|