Help us shape the future of Go! Take the 2026 Go Developer Survey

configsync

package
v1.4.1 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package configsync mirrors user-editable DB rows to YAML sidecar files on disk. Per-app sidecar: {apps_dir}/{slug}/simpledeploy.yml Global sidecar: {data_dir}/config.yml

Writers are debounced (500ms) so rapid successive calls coalesce into one write. Importers perform idempotent upserts, restoring config after a DB wipe.

Index

Constants

View Source
const Version = 1

Version is the sidecar schema version written to every file.

Variables

This section is empty.

Functions

func RunFirstBootSeedIfNeeded

func RunFirstBootSeedIfNeeded(ctx context.Context, db *store.Store, s *Syncer, cfg *config.Config) error

RunFirstBootSeedIfNeeded checks system_meta for the seeded marker. If absent, writes per-app + global FS files from current DB state and records the marker. Idempotent.

Types

type APIKeyEntry

type APIKeyEntry struct {
	Username  string     `yaml:"username"`
	Name      string     `yaml:"name"`
	ExpiresAt *time.Time `yaml:"expires_at,omitempty"`
}

APIKeyEntry mirrors store.APIKeyRecord. Key hash lives in GlobalSecrets.

type APIKeySecretsEntry

type APIKeySecretsEntry struct {
	KeyHash   string     `yaml:"key_hash"`
	Username  string     `yaml:"username"`
	Name      string     `yaml:"name"`
	ExpiresAt *time.Time `yaml:"expires_at,omitempty"`
}

type AccessEntry

type AccessEntry struct {
	Username string `yaml:"username" json:"username"`
}

AccessEntry records a user who has explicit access to this app.

type AlertRuleEntry

type AlertRuleEntry struct {
	Metric      string  `yaml:"metric" json:"metric"`
	Operator    string  `yaml:"operator" json:"operator"`
	Threshold   float64 `yaml:"threshold" json:"threshold"`
	DurationSec int     `yaml:"duration_sec" json:"duration_sec"`
	Webhook     string  `yaml:"webhook" json:"webhook"` // Webhook.Name
	Enabled     bool    `yaml:"enabled" json:"enabled"`
}

AlertRuleEntry is a portable alert rule (webhook referenced by name, not ID).

type AppMeta

type AppMeta struct {
	Slug        string `yaml:"slug" json:"slug"`
	DisplayName string `yaml:"display_name" json:"display_name"`
}

AppMeta holds identifying info about the app.

type AppSecrets

type AppSecrets struct {
	Version       int                  `yaml:"version"`
	Slug          string               `yaml:"slug"`
	BackupConfigs []BackupSecretsEntry `yaml:"backup_configs,omitempty"`
}

AppSecrets is {apps_dir}/<slug>/simpledeploy.secrets.yml. Mode 0600.

type AppSidecar

type AppSidecar struct {
	Version       int                 `yaml:"version"`
	App           AppMeta             `yaml:"app"`
	AlertRules    []AlertRuleEntry    `yaml:"alert_rules,omitempty"`
	BackupConfigs []BackupConfigEntry `yaml:"backup_configs,omitempty"`
	Access        []AccessEntry       `yaml:"access,omitempty"`
}

AppSidecar is the per-app YAML sidecar written to {apps_dir}/{slug}/simpledeploy.yml.

type BackupConfigEntry

type BackupConfigEntry struct {
	ID             string `yaml:"id" json:"id"` // UUID, correlates with secrets file entry
	Strategy       string `yaml:"strategy" json:"strategy"`
	Target         string `yaml:"target" json:"target"`
	ScheduleCron   string `yaml:"schedule_cron" json:"schedule_cron"`
	RetentionMode  string `yaml:"retention_mode" json:"retention_mode"`
	RetentionCount int    `yaml:"retention_count" json:"retention_count"`
	RetentionDays  *int   `yaml:"retention_days" json:"retention_days"`
	VerifyUpload   bool   `yaml:"verify_upload" json:"verify_upload"`
	PreHooks       string `yaml:"pre_hooks,omitempty" json:"pre_hooks,omitempty"`
	PostHooks      string `yaml:"post_hooks,omitempty" json:"post_hooks,omitempty"`
	Paths          string `yaml:"paths,omitempty" json:"paths,omitempty"`
}

BackupConfigEntry mirrors store.BackupConfig without DB IDs. target_config_enc lives in the secrets sidecar, correlated by ID (UUID).

type BackupSecretsEntry

type BackupSecretsEntry struct {
	ID              string `yaml:"id"`
	TargetConfigEnc string `yaml:"target_config_enc"`
}

type DBBackupSecretsEntry

type DBBackupSecretsEntry struct {
	TargetConfigEnc string `yaml:"target_config_enc"`
}

type GlobalSecrets

type GlobalSecrets struct {
	Version    int                    `yaml:"version"`
	Users      []UserSecretsEntry     `yaml:"users,omitempty"`
	APIKeys    []APIKeySecretsEntry   `yaml:"api_keys,omitempty"`
	Registries []RegistrySecretsEntry `yaml:"registries,omitempty"`
	Webhooks   []WebhookSecretsEntry  `yaml:"webhooks,omitempty"`
	DBBackup   *DBBackupSecretsEntry  `yaml:"db_backup,omitempty"`
}

GlobalSecrets is {data_dir}/secrets.yml. Mode 0600.

type GlobalSidecar

type GlobalSidecar struct {
	Version        int               `yaml:"version"`
	Users          []UserEntry       `yaml:"users,omitempty"`
	APIKeys        []APIKeyEntry     `yaml:"api_keys,omitempty"`
	Registries     []RegistryEntry   `yaml:"registries,omitempty"`
	Webhooks       []WebhookEntry    `yaml:"webhooks,omitempty"`
	DBBackupConfig map[string]string `yaml:"db_backup_config,omitempty"`
}

GlobalSidecar is the global YAML sidecar written to {data_dir}/config.yml.

type LoadedApp

type LoadedApp struct {
	Slug    string
	Sidecar *AppSidecar
	Secrets *AppSecrets
}

LoadedApp aggregates an app's sidecar and secrets read from disk. Either field may be nil if the corresponding file is absent.

type LoadedGlobal

type LoadedGlobal struct {
	Sidecar *GlobalSidecar
	Secrets *GlobalSecrets
}

LoadedGlobal aggregates the global sidecar and secrets read from disk. Either field may be nil if the corresponding file is absent.

type RedactedGlobalSidecar

type RedactedGlobalSidecar struct {
	Version          int                `yaml:"version"`
	Users            []RedactedUser     `yaml:"users,omitempty"`
	Registries       []RedactedRegistry `yaml:"registries,omitempty"`
	Webhooks         []RedactedWebhook  `yaml:"webhooks,omitempty"`
	DBBackupSchedule string             `yaml:"db_backup_schedule,omitempty"`
	DBBackupTarget   string             `yaml:"db_backup_target,omitempty"`
}

DEPRECATED: Redundant with FS-authoritative config.yml (which is already non-secret). Kept for gitsync-pushed snapshot compatibility. Remove once gitsync is migrated to push config.yml directly. See plan docs/superpowers/plans/2026-04-27-fs-authoritative-state.md.

RedactedGlobalSidecar is a git-safe view of global config. Stored at {apps_dir}/_global.yml. Contains NO secrets: no password hashes, no api-key hashes, no encrypted credentials, no webhook URLs. Used by gitsync for portable config sharing; never used for DR.

type RedactedRegistry

type RedactedRegistry struct {
	ID   string `yaml:"id"`
	Name string `yaml:"name"`
	URL  string `yaml:"url"`
}

RedactedRegistry holds non-secret registry fields (URL is the registry hostname, not a secret).

type RedactedUser

type RedactedUser struct {
	Username    string `yaml:"username"`
	Role        string `yaml:"role"`
	DisplayName string `yaml:"display_name,omitempty"`
	Email       string `yaml:"email,omitempty"`
}

RedactedUser holds non-secret user fields.

type RedactedWebhook

type RedactedWebhook struct {
	Name string `yaml:"name"`
	Type string `yaml:"type"`
}

RedactedWebhook holds non-secret webhook fields (no URL, no headers, no template).

type RegistryEntry

type RegistryEntry struct {
	ID   string `yaml:"id"`
	Name string `yaml:"name"`
	URL  string `yaml:"url"`
}

RegistryEntry mirrors store.Registry. Encrypted creds live in GlobalSecrets.

type RegistrySecretsEntry

type RegistrySecretsEntry struct {
	ID          string `yaml:"id"`
	UsernameEnc string `yaml:"username_enc"`
	PasswordEnc string `yaml:"password_enc"`
}

type SidecarWriteHook

type SidecarWriteHook func(path string, reason string)

SidecarWriteHook is invoked after every successful sidecar write with the absolute path of the file written and a short reason string.

type Syncer

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

Syncer writes and reads config sidecar files, debouncing frequent writes.

func New

func New(st *store.Store, appsDir, dataDir string) *Syncer

New creates a Syncer. It does not start any background work until Schedule* is called.

func (*Syncer) ApplyAppSidecar

func (s *Syncer) ApplyAppSidecar(slug string, loaded *LoadedApp) error

ApplyAppSidecar reconciles per-app DB rows to match the FS-loaded sidecar for the given slug. The app row must already exist. Performs full-replace for alert_rules, backup_configs, and user_app_access. Updates apps.name from sidecar display_name. Alert rules referencing unknown webhooks are dropped with a log line (rather than failing the whole apply).

loaded.Sidecar must be non-nil. loaded.Secrets may be nil.

func (*Syncer) ApplyGlobalSidecar

func (s *Syncer) ApplyGlobalSidecar(loaded *LoadedGlobal) error

ApplyGlobalSidecar reconciles global DB rows (users, api_keys, registries, webhooks, db_backup_config) to match the FS-loaded global sidecar.

CRITICAL DR safety: if loaded.Secrets == nil (secrets file missing) we run only the non-secret diff and leave secret columns (password_hash, key_hash, *_enc, webhook URL/headers/template, db backup target_config_enc) untouched. This prevents a missing secrets.yml from wiping the admin's password and locking everyone out.

Full-replace semantics: rows present in the DB but absent from the sidecar are deleted (users, api_keys, registries, webhooks). db_backup_config is upserted (single config row, not a set).

func (*Syncer) AppsDir

func (s *Syncer) AppsDir() string

AppsDir returns the apps directory the syncer was configured with.

func (*Syncer) ArchiveDir

func (s *Syncer) ArchiveDir() string

ArchiveDir returns the directory where tombstones are stored.

func (*Syncer) Close

func (s *Syncer) Close() error

Close flushes all pending debounced writes and stops all timers. Safe to call multiple times.

func (*Syncer) DataDir

func (s *Syncer) DataDir() string

DataDir returns the data directory the syncer was configured with.

func (*Syncer) DeleteAppSidecar

func (s *Syncer) DeleteAppSidecar(slug string) error

DeleteAppSidecar removes {apps_dir}/{slug}/simpledeploy.yml if it exists. Safe to call even if the file or directory is gone. Ignores not-exist errors.

func (*Syncer) DeleteTombstone

func (s *Syncer) DeleteTombstone(slug string) error

DeleteTombstone removes the tombstone for a slug. Missing file is not an error.

func (*Syncer) ImportAppSidecar

func (s *Syncer) ImportAppSidecar(data *AppSidecar) error

ImportAppSidecar performs idempotent upserts for all rows in data into the store. Existing alert_rules and backup_configs for the app are replaced wholesale. The caller must ensure the app exists in the DB before calling.

func (*Syncer) ImportAppSidecarIfMissing

func (s *Syncer) ImportAppSidecarIfMissing(slug string) (bool, error)

ImportAppSidecarIfMissing reads the per-app sidecar and imports it only when the app's DB-side state (alert rules + backup configs + access grants) is empty. Returns (imported bool, err error). Missing sidecar -> (false, nil). If any of the three is non-empty, treat as "DB has state" and skip import. The app row must already exist before calling.

func (*Syncer) ImportGlobal

func (s *Syncer) ImportGlobal(data *GlobalSidecar) error

ImportGlobal performs idempotent upserts for all rows in data into the store. Reads the sibling secrets.yml (if present) and correlates secrets to non-secret rows by their natural keys. Import order: webhooks -> users -> api_keys -> registries -> db_backup_config.

func (*Syncer) ImportGlobalIfEmpty

func (s *Syncer) ImportGlobalIfEmpty() (bool, error)

ImportGlobalIfEmpty imports the global sidecar only when the DB has zero users. Returns (imported bool, err error). If no sidecar exists -> (false, nil). If users table is non-empty -> (false, nil) without modifying DB.

func (*Syncer) ImportRedactedGlobal

func (s *Syncer) ImportRedactedGlobal(data *RedactedGlobalSidecar) error

ImportRedactedGlobal applies non-secret deltas from the redacted file to the DB. Preserves existing password hashes, encrypted registry credentials, and webhook URLs. Does NOT delete users/registries/webhooks that are absent from the file. Does NOT touch api_keys.

func (*Syncer) IsSelfWrite

func (s *Syncer) IsSelfWrite(path string, within time.Duration) bool

func (*Syncer) LoadAppFromFS

func (s *Syncer) LoadAppFromFS(slug string) (*LoadedApp, error)

LoadAppFromFS reads {apps_dir}/{slug}/simpledeploy.yml and the matching secrets sidecar. Missing files are reported as nil fields, not errors.

func (*Syncer) LoadGlobalFromFS

func (s *Syncer) LoadGlobalFromFS() (*LoadedGlobal, error)

LoadGlobalFromFS reads {data_dir}/config.yml and the global secrets sidecar. Missing files are reported as nil fields, not errors.

func (*Syncer) MarkSelfWrite

func (s *Syncer) MarkSelfWrite(path string)

MarkSelfWrite records that path was just written by the syncer itself. IsSelfWrite returns true if path was self-written within the given window (and consumes the entry so a manual edit immediately after would still be honored).

func (*Syncer) PruneOrphanSidecars

func (s *Syncer) PruneOrphanSidecars() ([]string, error)

PruneOrphanSidecars removes per-app sidecar files for slugs not present in the DB. Safe to call on every startup. Returns the list of slugs whose sidecars were removed. Skips hidden dirs and names starting with '_' or '.'.

func (*Syncer) ReadAppSecrets

func (s *Syncer) ReadAppSecrets(slug string) (*AppSecrets, error)

ReadAppSecrets reads the per-app secrets sidecar. Returns (nil, nil) if absent.

func (*Syncer) ReadAppSidecar

func (s *Syncer) ReadAppSidecar(slug string) (*AppSidecar, error)

ReadAppSidecar reads the app sidecar from disk. Returns (nil, nil) if the file does not exist. Unknown YAML keys are tolerated (logged as warnings).

func (*Syncer) ReadGlobal

func (s *Syncer) ReadGlobal() (*GlobalSidecar, error)

ReadGlobal reads the global sidecar from disk. Returns (nil, nil) if the file does not exist.

func (*Syncer) ReadGlobalSecrets

func (s *Syncer) ReadGlobalSecrets() (*GlobalSecrets, error)

ReadGlobalSecrets reads the global secrets sidecar. Returns (nil, nil) if absent.

func (*Syncer) ReadRedactedGlobal

func (s *Syncer) ReadRedactedGlobal() (*RedactedGlobalSidecar, error)

ReadRedactedGlobal reads the redacted global sidecar. Returns (nil, nil) if missing.

func (*Syncer) ReadTombstone

func (s *Syncer) ReadTombstone(slug string) (*Tombstone, error)

ReadTombstone reads the tombstone for a slug. Returns the parsed struct or an os error (use os.IsNotExist to detect a missing file).

func (*Syncer) ReconcileDBFromFS

func (s *Syncer) ReconcileDBFromFS(ctx context.Context) error

ReconcileDBFromFS scans the apps directory and applies each per-app sidecar to the DB, then applies the global sidecar. Missing files are skipped (not treated as deletions) so this is safe to run on every boot.

func (*Syncer) ScheduleAppWrite

func (s *Syncer) ScheduleAppWrite(slug string)

ScheduleAppWrite schedules a debounced write of the app sidecar for slug.

func (*Syncer) ScheduleGlobalWrite

func (s *Syncer) ScheduleGlobalWrite()

ScheduleGlobalWrite schedules a debounced write of both global sidecars (config.yml and _global.yml) in a single 500ms window.

func (*Syncer) SetSidecarWriteHook

func (s *Syncer) SetSidecarWriteHook(h SidecarWriteHook)

SetSidecarWriteHook installs a callback invoked after each successful sidecar write. The hook receives the absolute file path. Panics from the hook are recovered so a buggy hook cannot corrupt writes.

func (*Syncer) WriteAppSecrets

func (s *Syncer) WriteAppSecrets(slug string, secrets *AppSecrets) error

WriteAppSecrets writes the per-app secrets sidecar at mode 0600.

func (*Syncer) WriteAppSidecar

func (s *Syncer) WriteAppSidecar(slug string) error

WriteAppSidecar reads the app and its config from the store and writes the sidecar pair atomically. Secrets file (mode 0600) is written FIRST; if that fails, the non-secret sidecar is not written. Non-secret sidecar is mode 0644.

func (*Syncer) WriteGlobal

func (s *Syncer) WriteGlobal() error

WriteGlobal reads global config from the store and writes the global sidecar pair atomically. Secrets file (mode 0600) is written FIRST; if that fails, the non-secret config.yml is not written. config.yml is mode 0644.

func (*Syncer) WriteGlobalSecrets

func (s *Syncer) WriteGlobalSecrets(g *GlobalSecrets) error

WriteGlobalSecrets writes the global secrets sidecar at mode 0600.

func (*Syncer) WriteRedactedGlobal

func (s *Syncer) WriteRedactedGlobal() error

WriteRedactedGlobal reads global config from the store and writes the redacted {apps_dir}/_global.yml. Contains no secrets.

func (*Syncer) WriteTombstone

func (s *Syncer) WriteTombstone(slug string, archivedAt time.Time) error

WriteTombstone builds a tombstone from current DB state for the slug and writes it atomically to {data_dir}/archive/<slug>.yml (mode 0644).

type Tombstone

type Tombstone struct {
	Version       int                 `yaml:"version" json:"version"`
	ArchivedAt    time.Time           `yaml:"archived_at" json:"archived_at"`
	App           AppMeta             `yaml:"app" json:"app"`
	AlertRules    []AlertRuleEntry    `yaml:"alert_rules,omitempty" json:"alert_rules,omitempty"`
	BackupConfigs []BackupConfigEntry `yaml:"backup_configs,omitempty" json:"backup_configs,omitempty"`
	Access        []AccessEntry       `yaml:"access,omitempty" json:"access,omitempty"`
}

Tombstone is the snapshot written to {data_dir}/archive/<slug>.yml when an app is archived (its dir disappeared from apps_dir).

type UserEntry

type UserEntry struct {
	Username    string `yaml:"username"`
	Role        string `yaml:"role"`
	DisplayName string `yaml:"display_name,omitempty"`
	Email       string `yaml:"email,omitempty"`
}

UserEntry mirrors store.User. Password hash lives in GlobalSecrets.

type UserSecretsEntry

type UserSecretsEntry struct {
	Username     string `yaml:"username"`
	PasswordHash string `yaml:"password_hash"`
}

type WebhookEntry

type WebhookEntry struct {
	Name string `yaml:"name"`
	Type string `yaml:"type"`
}

WebhookEntry mirrors store.Webhook. URL/headers/template live in GlobalSecrets.

type WebhookSecretsEntry

type WebhookSecretsEntry struct {
	Name             string `yaml:"name"`
	URL              string `yaml:"url"`
	HeadersJSON      string `yaml:"headers_json,omitempty"`
	TemplateOverride string `yaml:"template_override,omitempty"`
}

Jump to

Keyboard shortcuts

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