Documentation
¶
Overview ¶
Package reconcile provides GitOps reconciliation functionality.
Index ¶
- Constants
- Variables
- func CheckCriticalContainerHealth(ctx context.Context, client *docker.Client, containers []string, ...) error
- func DriftAlertKey(item DriftItem) string
- func EnrichUnhealthyItems(ctx context.Context, client *docker.Client, report *DriftReport, ...)
- func ExecutePostSyncHooks(ctx context.Context, client *docker.Client, hooks []PostSyncHook, ...) error
- func IsUnderFUSEDeployPath(path string) bool
- func MergeTargetSecrets(secrets map[string]any, scope string) map[string]any
- func MinLen(s string, n int) int
- func ResolveHealthGateScope(scope string) (string, error)
- func SaveState(path string, state *DeployState) error
- func ShouldAlert(attemptCount, lastAlertedAttempt int) bool
- func TargetLockFile(baseLockDir string, t Target) string
- func TargetStagingDir(baseStagingDir string, t Target) string
- func TargetStateFile(baseStateDir string, t Target) string
- func ValidateDriftIgnoreRules(rules []DriftIgnoreRule) (warnings []string, err error)
- func ValidateProjectName(name string) error
- func ValidateRemotePath(path string) error
- func ValidateSOPSFile(path string) error
- func ValidateTargetName(name string) error
- type ActualService
- type AlertSender
- type ComposeFileResult
- type ComposeUpSummary
- type Config
- type ConfigField
- type ConfigReloaderFunc
- type ConfigSource
- type ContainerHealthResult
- type DeclaredService
- type DeployOps
- func (d *DeployOps) Backup(ctx context.Context, backupDir string, paths []string) (string, error)
- func (d *DeployOps) BackupRemote(ctx context.Context, host, backupDir string, remotePaths []string) (string, error)
- func (d *DeployOps) CheckSSHConnectivity(ctx context.Context, host string) error
- func (d *DeployOps) CleanupBackups(ctx context.Context, backupDir string, keep int) error
- func (d *DeployOps) ComposeUp(ctx context.Context, composeFile string) error
- func (d *DeployOps) ComposeUpIsolated(ctx context.Context, composeFiles []string, backupPath string) (*ComposeUpSummary, error)
- func (d *DeployOps) ComposeUpMultiple(ctx context.Context, composeFiles []string) error
- func (d *DeployOps) ComposeUpRemote(ctx context.Context, host, composeDir string) error
- func (d *DeployOps) DeployLocal(ctx context.Context, sourceDir, targetDir string, result *DeployResult, ...) error
- func (d *DeployOps) DeployLocalFile(ctx context.Context, sourceFile, targetFile string, result *DeployResult) error
- func (d *DeployOps) DeployRemote(ctx context.Context, sourceDir, targetHost, targetDir string, ...) error
- func (d *DeployOps) DeployRemoteFile(ctx context.Context, sourceFile, targetHost, targetFile string) error
- func (d *DeployOps) EnsureRemoteDir(ctx context.Context, host, dir string) error
- func (d *DeployOps) LatestVerifiedBackup(ctx context.Context, backupDir string) (string, error)
- func (d *DeployOps) RollbackFromBackupSet(ctx context.Context, set RollbackSet, backupPath string) error
- func (d *DeployOps) RollbackRemoteCompose(ctx context.Context, host, remoteComposeDir, backupPath string, ...) error
- func (d *DeployOps) SignalContainer(ctx context.Context, containerName, signal string) error
- func (d *DeployOps) SignalContainerRemote(ctx context.Context, host, containerName, signal string) error
- func (d *DeployOps) VerifyBackup(ctx context.Context, backupPath string) error
- func (d *DeployOps) VerifyContainerHealth(ctx context.Context, composeFile string) error
- type DeployResult
- type DeployState
- type DeployTarget
- type Deployer
- type DockerClientFunc
- type DriftIgnoreRule
- type DriftItem
- func FilterDebounced(currentItems []DriftItem, debounceItems map[string]time.Time, ...) []DriftItem
- func FilterIgnoredDriftItems(items []DriftItem, rules []DriftIgnoreRule) []DriftItem
- func ShouldAlertDrift(activeItems, alertCandidates []DriftItem, alertedItems map[string]time.Time, ...) (alertItems []DriftItem, resolvedKeys []string)
- type DriftReport
- type DriftType
- type Duration
- type GitOperations
- type GitOps
- func (g *GitOps) Clone(ctx context.Context, depth int) error
- func (g *GitOps) DiffFiles(ctx context.Context, fromCommit, toCommit string) ([]string, error)
- func (g *GitOps) GetCommitMessage(ctx context.Context) (string, error)
- func (g *GitOps) GetLatestCommit(ctx context.Context) (string, error)
- func (g *GitOps) IsDirty(ctx context.Context) (bool, error)
- func (g *GitOps) IsRepo(ctx context.Context) bool
- func (g *GitOps) Pull(ctx context.Context) (bool, string, string, error)
- func (g *GitOps) RemoteBranchExists(ctx context.Context, branch string) (bool, error)
- func (g *GitOps) Sync(ctx context.Context) (bool, string, string, error)
- type HealthCheckResult
- type PostSyncHook
- type Reconciler
- type ReconcilerOption
- func WithAlerter(alerter AlertSender) ReconcilerOption
- func WithDeployOps(deploy *DeployOps) ReconcilerOption
- func WithDockerClient(client *docker.Client) ReconcilerOption
- func WithDockerClientFunc(fn DockerClientFunc) ReconcilerOption
- func WithGitOperations(git GitOperations) ReconcilerOption
- func WithLockFile(path string) ReconcilerOption
- func WithSecretsDecryptor(sops SecretsDecryptor) ReconcilerOption
- type ReloadedConfig
- type RestartBreakerResult
- type RestartTrackingEntry
- type RollbackSet
- type SOPSOps
- func (s *SOPSOps) CheckAgeKey() error
- func (s *SOPSOps) Decrypt(ctx context.Context, file string) ([]byte, error)
- func (s *SOPSOps) DecryptFiles(ctx context.Context, files []string) (map[string]any, error)
- func (s *SOPSOps) DecryptToJSON(ctx context.Context, files []string) ([]byte, error)
- func (s *SOPSOps) DecryptToMap(ctx context.Context, file string) (map[string]any, error)
- type SecretsDecryptor
- type Target
- type TemplateOps
- type TemplateRenderer
Constants ¶
const ( GitCloneTimeout = 5 * time.Minute GitFetchTimeout = 2 * time.Minute GitLocalTimeout = 30 * time.Second )
Git operation timeouts
const ( HealthGateScopeCritical = "critical" HealthGateScopeDeclared = "declared" HealthGateScopeOff = "off" )
Health gate scope values (Config.HealthGateScope). Selects what the post-compose-up gate polls and rolls back on.
const ( DefaultMaxRetries = 3 InitialBackoff = 1 * time.Second )
SSH retry configuration
const ( SSHConnectTimeout = 5 * time.Second SSHTimeout = 30 * time.Second RemoteDeployTimeout = 5 * time.Minute RemoteCleanupTimeout = 10 * time.Second )
Deploy operation timeouts
const ComposeProjectLabel = "com.docker.compose.project"
ComposeProjectLabel is the label Docker Compose v2 sets on managed containers.
const ComposeServiceLabel = "com.docker.compose.service"
ComposeServiceLabel is the label Docker Compose v2 sets for the service name.
const DefaultBackupTimeout = 5 * time.Minute
DefaultBackupTimeout bounds backup creation + verification when no BackupTimeout is configured. Prevents the pre-deploy backup step from wedging the reconcile indefinitely (#319).
const DefaultComposeUpTimeout = 10 * time.Minute
DefaultComposeUpTimeout is the maximum time allowed for docker compose up.
const DefaultIncludeSubdir = "templates"
DefaultIncludeSubdir is the subdirectory (relative to the render source directory) that include/fromJsonFile reads are confined to by default. Secrets (SOPS files, age keys) and bosun.yaml live in the infra root, NOT under templates/, so confining reads here keeps them unreachable.
const DefaultLockDir = "/var/run/bosun"
DefaultLockDir is the default directory for per-target lock files.
const DefaultLockFile = "/var/run/bosun/reconcile.lock"
DefaultLockFile is the default path for the reconciliation lock file.
const DefaultStateDir = "/var/lib/bosun"
DefaultStateDir is the default directory for persistent deploy state. Uses /var/lib/bosun/ per FHS conventions for mutable application state.
const DefaultStateFile = "deploy-state.json"
DefaultStateFile is the default filename for the deploy state file.
const DefaultTargetName = "default"
DefaultTargetName is the name used for the implicit single-target backwards-compat target.
const FUSEDeployPathPrefix = "/mnt/user"
FUSEDeployPathPrefix is Unraid's user-share FUSE (shfs) mount prefix. Writes through this mount need extra time to settle before post-sync hooks (or other consumers, e.g. the `bosun doctor` check) read the files back — see resolveHookSettleDelay.
const HealthGatePollInterval = 5 * time.Second
HealthGatePollInterval is the default interval for polling container status.
const IsRepoCheckTimeout = 2 * time.Second
IsRepoCheckTimeout is the timeout for checking if a directory is a git repository.
const MaxAttempts = 3
MaxAttempts is the circuit breaker threshold — after this many consecutive failures on the same commit, stop retrying until a new commit or --force.
const MaxVerifyDecompressedBytes int64 = 10 << 30 // 10 GiB
MaxVerifyDecompressedBytes caps the TOTAL decompressed size that verifyArchiveIntegrity will read across an entire archive. A crafted gzip/tar member can decompress to tens of GB (a decompression bomb), spinning CPU past BackupTimeout during the integrity read-through (#240). The cap is a TOTAL across all members (not per-member) because a bomb can be split across many members each individually small — the total is the true bound on verify work. 10 GiB is far above any real bosun config backup (rendered compose/config files) yet small enough to reject a bomb well before it exhausts memory or CPU.
Variables ¶
var ( ErrDeployInvariantEmptyWrite = errors.New("deploy invariant: source has files but no writes recorded") ErrDeployInvariantStaleMtime = errors.New("deploy invariant: destination file has stale mtime") ErrDeployInvariantMissingFile = errors.New("deploy invariant: destination file missing") )
Sentinel errors returned by verifyDeployTarget. Callers branch via errors.Is.
var ErrAgeKeyNotFound = errors.New("age key not found")
ErrAgeKeyNotFound is returned when no age key is found for SOPS decryption.
var ErrAppdataInaccessible = errors.New("local appdata path is configured but inaccessible")
ErrAppdataInaccessible is returned when LocalAppdataPath is configured but the path cannot be accessed (e.g. mount is down).
var ErrBackupTooLarge = errors.New("backup archive exceeds maximum verifiable decompressed size")
ErrBackupTooLarge is returned when a backup archive decompresses past MaxVerifyDecompressedBytes during verification — a likely decompression bomb.
var ErrComposeDirMissing = errors.New("staging compose directory does not exist")
ErrComposeDirMissing is returned by ExtractDeclaredState when the staging compose directory does not exist on disk. This indicates a misconfigured staging path and is always treated as a fatal reconcile error — operators cannot opt out, because there is no way to distinguish "intentionally empty repo" from "wrong path."
var ErrComposeUnhealthy = errors.New("compose up completed with unhealthy containers")
ErrComposeUnhealthy indicates compose exited non-zero but all containers are running (some unhealthy). This is recoverable and should not trigger rollback.
var ErrNoDeclaredServices = errors.New("no declared services in staging compose directory")
ErrNoDeclaredServices is returned by ExtractDeclaredState when the staging compose directory exists but contains no parseable services. This is overridable via BOSUN_ALLOW_EMPTY_DECLARED_STATE=true for genuinely empty repos (early scaffolding, archive branches).
var ErrNotSOPSFile = errors.New("file is not SOPS-encrypted")
ErrNotSOPSFile is returned when a file is not a valid SOPS-encrypted file.
var ErrRollbackFailed = errors.New("deployment and rollback both failed")
ErrRollbackFailed indicates both deployment and rollback failed.
var ErrRollbackSucceeded = errors.New("deployment failed, rollback succeeded")
ErrRollbackSucceeded indicates deployment failed but rollback succeeded.
var ErrTransferIntegrity = errors.New("remote transfer integrity check failed")
ErrTransferIntegrity marks a remote tar-over-SSH transfer whose staged tree failed SHA-256 verification against the locally-built manifest — a truncated, partial, or misdirected transfer that must never be promoted to the live target (#334). It is RETRYABLE: retryWithBackoff stages into a fresh remote tmpDir on the next attempt, so a re-transfer is never overlaid on the dirty leftover of a failed one.
var ErrUnsafeChecksumPath = errors.New("unsafe filename for checksum manifest")
ErrUnsafeChecksumPath marks a staged filename that cannot be represented in a `sha256sum -c` manifest: busybox sha256sum lacks GNU's backslash escaping, so a newline or backslash in a name would corrupt the newline-delimited, space-separated manifest parse. Rejected at the local staging walk rather than trusted to format escaping (#334). The input is repo-controlled, so this guards against a pathological rendered filename, not a hostile actor.
Functions ¶
func CheckCriticalContainerHealth ¶ added in v0.26.0
func CheckCriticalContainerHealth( ctx context.Context, client *docker.Client, containers []string, timeout time.Duration, interval time.Duration, ) error
CheckCriticalContainerHealth polls critical containers via Docker API until all are healthy or the timeout expires. Returns nil if all containers pass, or an error listing which containers failed and why.
Health classification:
- "healthy": pass
- no healthcheck defined (empty Health field + running): pass
- "unhealthy": fail
- "starting" at timeout: fail
- missing or not running: fail
func DriftAlertKey ¶ added in v0.10.0
DriftAlertKey returns a deduplication key for a drift item in the format "service:type".
func EnrichUnhealthyItems ¶ added in v0.15.0
func EnrichUnhealthyItems(ctx context.Context, client *docker.Client, report *DriftReport, actual []ActualService)
EnrichUnhealthyItems inspects unhealthy containers to populate DriftItem.Actual with the last health check result (exit code + output). Only calls Docker Inspect for containers with DriftUnhealthy type, so the cost scales with the number of unhealthy containers, not total containers.
func ExecutePostSyncHooks ¶ added in v0.6.0
func ExecutePostSyncHooks(ctx context.Context, client *docker.Client, hooks []PostSyncHook, settleDelay time.Duration) error
ExecutePostSyncHooks runs matched hooks by performing the specified action on containers. settleDelay is a global pause before any hooks run (filesystem propagation).
func IsUnderFUSEDeployPath ¶ added in v0.37.6
IsUnderFUSEDeployPath reports whether path is FUSEDeployPathPrefix itself or nested under it. Checks the path-segment boundary (not a bare string prefix) so a lookalike sibling mount such as "/mnt/userdata" is not mistaken for Unraid's "/mnt/user" share.
func MergeTargetSecrets ¶ added in v0.30.0
MergeTargetSecrets creates a copy of the secrets map with per-target overrides applied. Keys under "targets.<scope>.*" override same-named top-level keys. The original map is not modified.
Example: if secrets contains {"db_password": "shared", "targets": {"unraid": {"db_password": "secret1"}}} and scope is "unraid", the result has {"db_password": "secret1", "targets": {...}}.
func ResolveHealthGateScope ¶ added in v0.39.0
ResolveHealthGateScope normalizes and validates a configured scope. Empty resolves to "critical". An unknown value returns an error naming the valid set so callers (the daemon env/config parse, and runHealthGate) can surface it rather than silently guessing.
func SaveState ¶ added in v0.3.0
func SaveState(path string, state *DeployState) error
SaveState atomically writes the deploy state to disk. Uses the pattern: write temp (same dir) → fsync temp → rename → fsync dir.
func ShouldAlert ¶ added in v0.6.0
ShouldAlert returns true if a failure alert should be sent for the current attempt count, given the last attempt that triggered an alert. Schedule: alert on attempt 1, 3, 10, 30, then every 30th attempt. Circuit breaker activation (attempt == MaxAttempts) always alerts.
func TargetLockFile ¶ added in v0.30.0
TargetLockFile returns the lock file path for a target. The default target uses the legacy path; named targets use reconcile-<name>.lock.
func TargetStagingDir ¶ added in v0.30.0
TargetStagingDir returns the staging directory for a target. The default target uses the base staging dir; named targets use <staging>/<name>/.
func TargetStateFile ¶ added in v0.30.0
TargetStateFile returns the state file path for a target. The default target uses the legacy path; named targets use deploy-state-<name>.json.
func ValidateDriftIgnoreRules ¶ added in v0.37.9
func ValidateDriftIgnoreRules(rules []DriftIgnoreRule) (warnings []string, err error)
ValidateDriftIgnoreRules validates drift_ignore rules -- from the config file or the BOSUN_DRIFT_IGNORE environment override -- against the implemented DriftType enum and glob syntax.
A rule with an unknown type or an invalid service glob is returned as an error, because such a rule silently never matches and leaves drift unreported; the offending rule's index is named so operators can find it. A rule that suppresses all drift (service "*" and type "*") is syntactically valid but disables drift detection entirely, so it is reported separately as a non-fatal warning: callers decide whether to treat it as an error (e.g. `bosun validate`, where it should block) or a loud warning (daemon startup, where an intentional full mute is allowed but never silent).
func ValidateProjectName ¶ added in v0.34.3
ValidateProjectName validates a docker compose project name for use in shell commands. Must start with alphanumeric and may contain alphanumeric, underscore, hyphen, or dot. Maximum length is 128 characters (docker compose's own limit). Rejects shell metacharacters that could enable command injection.
func ValidateRemotePath ¶ added in v0.34.3
ValidateRemotePath validates a filesystem path intended for use in SSH-executed shell commands. Accepts absolute paths (starting with /) and relative paths using alphanumeric, underscore, hyphen, dot, space, and forward slash. Spaces are permitted to support Unraid NAS paths (e.g. "/mnt/user/My Media/appdata"); shellquote.Join handles quoting at call sites. Rejects path traversal (../), shell metacharacters, and paths starting with "-" (CLI flag injection).
func ValidateSOPSFile ¶
ValidateSOPSFile checks if a file is a valid SOPS-encrypted file. Returns nil if valid, or an actionable error describing the problem.
func ValidateTargetName ¶ added in v0.30.0
ValidateTargetName checks that a target name is safe for use in filesystem paths. Rejects empty names, path traversal attempts, absolute paths, and special characters.
Types ¶
type ActualService ¶ added in v0.3.0
type ActualService struct {
Name string
ContainerName string // Full Docker container name (for inspect calls)
Image string
State string
Health string
}
ActualService represents the observed state of a running container.
func CollectActualState ¶ added in v0.3.0
func CollectActualState(ctx context.Context, client *docker.Client, projectName string) ([]ActualService, error)
CollectActualState queries Docker for running containers filtered by the compose project label to scope to bosun-managed services.
type AlertSender ¶
type AlertSender interface {
SendDeploySuccess(ctx context.Context, commit, target string, services []string, duration time.Duration) error
SendDeployFailure(ctx context.Context, commit, target, reason string, services []string, duration time.Duration) error
SendDeployRecovery(ctx context.Context, commit, target string, priorFailures int) error
SendUnhealthyContainers(ctx context.Context, target string, containers []string) error
SendRollbackSuccess(ctx context.Context, target, backupName string) error
SendRollbackFailure(ctx context.Context, target, reason string) error
}
AlertSender sends alerts for reconciliation events.
type ComposeFileResult ¶ added in v0.29.0
type ComposeFileResult struct {
File string // absolute path to the compose file
Success bool // true if compose up succeeded (including unhealthy-only)
RolledBack bool // true if the file was rolled back from backup after failure
Err error // nil on success, the compose-up error on failure
}
ComposeFileResult records the outcome of a single compose file's deployment.
type ComposeUpSummary ¶ added in v0.29.0
type ComposeUpSummary struct {
Results []ComposeFileResult
Succeeded int
Failed int
RolledBack int
}
ComposeUpSummary aggregates per-file compose up results.
type Config ¶
type Config struct {
// RepoURL is the git repository URL.
RepoURL string
// RepoBranch is the branch to track.
RepoBranch string
// RepoDir is the local directory for the cloned repository.
RepoDir string
// StagingDir is the directory for rendered templates.
StagingDir string
// BackupDir is the directory for configuration backups.
BackupDir string
// LogDir is the directory for log files.
LogDir string
// LockFile is the path to the reconciliation lock file.
LockFile string
// TargetName identifies the deployment target (e.g., "unraid", "pi", "default").
// Set by ConfigForTarget; used in alert messages and log context.
TargetName string
// TargetHost is empty for local deployment, or "user@host" for remote.
TargetHost string
// LocalAppdataPath is the path to appdata when running locally.
LocalAppdataPath string
// RemoteAppdataPath is the path to appdata on the remote host.
RemoteAppdataPath string
// Targets is the list of deployment targets. When empty, an implicit default
// target is synthesized from the flat config fields above (TargetHost,
// LocalAppdataPath, RemoteAppdataPath, ProjectName).
Targets []Target
// TargetsFromEnv records that Targets came from the BOSUN_TARGETS env var.
// Hot-reload must not override env-provided per-target config (env wins
// over the repo's bosun.yaml) — see applyTargetOverrides.
TargetsFromEnv bool
// DeployMode overrides automatic deploy mode detection.
// Valid values: "" (auto-detect), "local", "remote".
// When set, resolveDeployMode skips heuristics and uses the specified mode.
DeployMode string
// DryRun if true, only shows what would be done.
DryRun bool
// Force if true, runs deployment even if no changes detected. This is the
// human-supplied override (CLI --force, socket/TCP/HTTP trigger `force`):
// it bypasses the commit-unchanged skip AND the circuit breaker AND the
// deploy_paths allowlist gate. An unattended trigger must never set this.
Force bool
// ForceRedeployUnchanged bypasses only the commit-unchanged skip in
// shouldSkipDeploy, for triggers that need a redeploy despite the commit
// hash not moving (drift self-heal: image_mismatch/unhealthy drift on a
// running container doesn't change the declared commit). Unlike Force,
// it does NOT bypass the circuit breaker or the deploy_paths allowlist
// gate — an unattended self-heal loop must never override a human
// decision (a tripped breaker) or silently ignore path scoping.
ForceRedeployUnchanged bool
// Source identifies what triggered this reconciliation (e.g., "webhook:github", "poll", "cli").
Source string
// SecretsFiles is the list of SOPS-encrypted secret files to decrypt.
SecretsFiles []string
// SecretsScope is the key prefix for per-target secrets scoping.
// When set, keys under "targets.<scope>.*" in the decrypted secrets
// override same-named top-level keys for this target's template rendering.
SecretsScope string
// InfraSubDir is the subdirectory within the repo containing infrastructure configs.
// Use "." for repos where the root is the infrastructure (dedicated infra repos).
// Use a path like "infrastructure" for repos where infra is nested (e.g., dotfiles).
InfraSubDir string
// TemplateIncludeDir overrides the subtree that template include/fromJsonFile
// reads are confined to. Empty resolves to <infraDir>/templates (the default
// allowlist root). A relative value is joined to the infra directory; an
// absolute value is used as-is. Confining reads here keeps sibling SOPS files
// and bosun.yaml unreachable from templates. Configurable via
// BOSUN_TEMPLATE_INCLUDE_DIR or the template_include_dir config field.
TemplateIncludeDir string
// BackupsToKeep is the number of backups to retain.
BackupsToKeep int
// ProjectName is the docker compose project name for consistent container namespacing.
// All compose operations will use this name, ensuring --remove-orphans works correctly.
ProjectName string
// StateFile is the path to the deploy state file that tracks last successful deployment.
StateFile string
// HealthCheckTimeout is the maximum time to poll container health after
// compose up. Zero disables health verification entirely. Default 60s.
HealthCheckTimeout time.Duration
// HealthCheckInterval is how often to poll container health during
// post-deploy verification. Default 5s.
HealthCheckInterval time.Duration
// RestartBreakerEnabled controls whether the restart circuit breaker runs
// during drift checks. Default true.
RestartBreakerEnabled bool
// RestartThreshold is the restart count delta that trips the breaker. Default 5.
RestartThreshold int
// RestartWindow is the time window for measuring restart velocity. Default 10m.
RestartWindow time.Duration
// PostSyncHooks defines container restart actions triggered by file changes.
PostSyncHooks ConfigField[[]PostSyncHook]
// HookSettleDelay is a global pause after deploy but before any post-sync hooks run.
// Allows filesystem propagation on FUSE mounts (e.g., Unraid's shfs).
HookSettleDelay ConfigField[time.Duration]
// ContentHashSync if true, compares file content hashes before writing.
// Skips writes for unchanged files to avoid FUSE handle invalidation.
ContentHashSync bool
// RemoveOrphans if true, passes --remove-orphans to docker compose up.
// Removes containers belonging to services deleted from the compose file.
// Defaults to true.
RemoveOrphans ConfigField[bool]
// DeployPaths is an allowlist of glob patterns for deploy-relevant paths.
// When configured, commits that only touch files outside these patterns skip the pipeline.
DeployPaths ConfigField[[]string]
// DeploySyncPaths is an allowlist of glob patterns for deploy sync targets.
// When non-empty, only staging directory entries matching these patterns are deployed.
DeploySyncPaths ConfigField[[]string]
// DeploySyncExclude is a blocklist of glob patterns for deploy sync targets.
// Matching entries are excluded from deployment. Exclude wins over include.
DeploySyncExclude ConfigField[[]string]
// CriticalContainers is a list of container names that must be healthy after compose up.
// When configured, the health gate runs after startup grace period before state save.
// Empty list (default) skips the health gate entirely.
CriticalContainers ConfigField[[]string]
// DriftIgnore is a list of rules for suppressing known drift noise.
DriftIgnore ConfigField[[]DriftIgnoreRule]
// HealthGateTimeout is the maximum time to poll critical container health.
// Default 60s; non-positive disables the gate. Configurable via
// BOSUN_HEALTH_GATE_TIMEOUT.
HealthGateTimeout time.Duration
// HealthGateScope selects which containers the post-compose-up health gate
// polls and rolls back on: "critical" (default), "declared", or "off".
// Empty resolves to "critical". Configurable via BOSUN_HEALTH_GATE_SCOPE.
//
// - "critical": today's behavior — gate only on CriticalContainers members;
// an empty list skips the gate. A declared-but-non-critical service coming
// up unhealthy does NOT trigger rollback.
// - "declared": gate on ALL declared services (pollContainerHealth), with the
// #392 pre-existing-casualty exemption; any service THIS deploy made
// unhealthy triggers the same rollback branch. Opt-in because it adds
// flapping-healthcheck churn risk on top of PR #336's fail+retry+alert.
// - "off": no health gate at all.
HealthGateScope string
// OnFailure gates failure alert dispatch. When false, no failure alerts are sent.
// Defaults to true via DefaultConfig(). A bare Config{} leaves this false.
OnFailure bool
// OnSuccess gates success and recovery alert dispatch. When false, neither
// success nor recovery alerts are sent. Defaults to false.
OnSuccess bool
// ComposeUpTimeout is the maximum time allowed for docker compose up.
// Zero means use DefaultComposeUpTimeout (10 minutes).
ComposeUpTimeout time.Duration
// BackupTimeout bounds backup creation + verification.
// Zero means use DefaultBackupTimeout (5 minutes).
BackupTimeout time.Duration
// ConfigReloader loads project config from a directory path.
// Set by daemon/CLI to break the config→reconcile import cycle.
// When nil, config reload is skipped.
ConfigReloader ConfigReloaderFunc
// AllowEmptyDeclaredState relaxes the ErrNoDeclaredServices invariant when
// the staging compose directory exists but contains no parseable services.
// Use only for genuinely empty repos (early scaffolding, archive branches).
// Set via BOSUN_ALLOW_EMPTY_DECLARED_STATE=true. Default false.
// Note: ErrComposeDirMissing (compose dir does not exist at all) is always
// fatal regardless of this setting.
AllowEmptyDeclaredState bool
// SkipDeployInvariant disables the post-deploy mtime + WrittenFiles
// invariant check that runs between deploy sync and compose-up. Use for
// diagnostic or development scenarios only — silent-success deploys are
// the failure mode this guards against. Set via
// BOSUN_SKIP_DEPLOY_INVARIANT=true. Default false.
SkipDeployInvariant bool
}
Config holds the reconciliation configuration.
func DefaultConfig ¶
func DefaultConfig() *Config
DefaultConfig returns a Config with sensible defaults.
func (*Config) ConfigForTarget ¶ added in v0.30.0
ConfigForTarget returns a shallow copy of the base config with per-target fields (TargetHost, appdata paths, ProjectName, StagingDir, StateFile, LockFile, CriticalContainers, PostSyncHooks, DeploySyncPaths/Exclude) overridden from the given Target. This lets the existing pipeline run unchanged per-target - the daemon creates a ConfigForTarget copy before each reconciler instantiation.
func (*Config) ResolveTargets ¶ added in v0.30.0
ResolveTargets returns the effective target list for this config. When Targets is empty, it synthesizes a single implicit default target from the flat config fields for backwards compatibility. A lone target named `default` (case-insensitive) is the implicit default's configuration — its fields (project_name above all) are honored, not discarded (#391). A multi-target config that includes a `default` target is a hard error: silently dropping it would deploy that target project-less and collide.
type ConfigField ¶ added in v0.33.0
type ConfigField[T any] struct { Value T Source ConfigSource }
ConfigField tracks a configuration value alongside its origin. The zero value is safe: Value is T's zero value and Source is SourceDefault.
func EnvConfigField ¶ added in v0.33.0
func EnvConfigField[T any](v T) ConfigField[T]
EnvConfigField creates a ConfigField with SourceEnv.
func FileConfigField ¶ added in v0.33.0
func FileConfigField[T any](v T) ConfigField[T]
FileConfigField creates a ConfigField with SourceFile.
func NewConfigField ¶ added in v0.33.0
func NewConfigField[T any](v T) ConfigField[T]
NewConfigField creates a ConfigField with SourceDefault.
func (ConfigField[T]) FromEnv ¶ added in v0.33.0
func (f ConfigField[T]) FromEnv() bool
FromEnv returns true if the field was set by an environment variable.
func (*ConfigField[T]) SetFromEnv ¶ added in v0.33.0
func (f *ConfigField[T]) SetFromEnv(v T)
SetFromEnv sets the value and marks its source as environment.
func (*ConfigField[T]) SetFromFile ¶ added in v0.33.0
func (f *ConfigField[T]) SetFromFile(v T)
SetFromFile sets the value and marks its source as config file.
type ConfigReloaderFunc ¶ added in v0.12.1
type ConfigReloaderFunc func(dir string) (*ReloadedConfig, error)
ConfigReloaderFunc loads project config from a directory path. Returns nil ReloadedConfig if no config file is found (not an error).
type ConfigSource ¶ added in v0.33.0
type ConfigSource int
ConfigSource tracks where a configuration value originated. Environment variables always take precedence over config file values during hot-reload — fields with SourceEnv are never overwritten.
const ( // SourceDefault indicates the field holds its zero value or a hardcoded default. SourceDefault ConfigSource = iota // SourceFile indicates the field was loaded from bosun.yaml. SourceFile // SourceEnv indicates the field was set via an environment variable. SourceEnv )
type ContainerHealthResult ¶ added in v0.26.0
type ContainerHealthResult struct {
Name string
Status string // "healthy", "unhealthy", "starting", "no_healthcheck", "missing", "not_running"
Detail string // Human-readable detail (e.g. health log output)
}
ContainerHealthResult describes the health gate outcome for a single container.
type DeclaredService ¶ added in v0.3.0
DeclaredService represents a service that should be running per the manifests.
func ExtractDeclaredState ¶ added in v0.3.0
func ExtractDeclaredState(stagingDir string) ([]DeclaredService, error)
ExtractDeclaredState parses rendered compose files from a staging directory and returns the list of declared services with their images.
Returns ErrComposeDirMissing if the compose directory does not exist (always fatal). Returns ErrNoDeclaredServices if the directory exists but no services are declared (overridable). Other errors indicate I/O failures.
type DeployOps ¶
type DeployOps struct {
// DryRun if true, only shows what would be done without making changes.
DryRun bool
// ProjectName is the docker compose project name for consistent container namespacing.
ProjectName string
// ContentHashSync if true, skips writing files whose content has not changed.
// Reduces unnecessary FUSE handle invalidation on copy-on-write filesystems.
ContentHashSync bool
// RemoveOrphans if true, passes --remove-orphans to docker compose up.
// Removes containers belonging to services deleted from the compose file.
RemoveOrphans bool
// ComposeUpTimeout is the maximum time allowed for docker compose up.
// Zero means use DefaultComposeUpTimeout.
ComposeUpTimeout time.Duration
// contains filtered or unexported fields
}
DeployOps provides deployment operations including backup, file sync, and service management.
func NewDeployOps ¶
NewDeployOps creates a new DeployOps instance.
func (*DeployOps) BackupRemote ¶
func (d *DeployOps) BackupRemote(ctx context.Context, host, backupDir string, remotePaths []string) (string, error)
BackupRemote creates a backup from a remote host via SSH. Retries on transient SSH errors with exponential backoff.
func (*DeployOps) CheckSSHConnectivity ¶
CheckSSHConnectivity verifies SSH connectivity to a remote host. Returns nil if connection succeeds, error with actionable details otherwise.
func (*DeployOps) CleanupBackups ¶
CleanupBackups removes old backups, keeping only the most recent N VALID ones. A candidate counts toward retention only if it passes the SAME VerifyBackup used to select a rollback anchor — reusing that helper rather than a private, weaker notion of "good" so the two can never drift (a truncated-but-nonempty archive is "good" to a mere existence check yet junk to VerifyBackup). A corrupt or partial dir (missing, unlistable, or truncated archive) does NOT occupy a keep slot and is removed outright; otherwise it could evict an older known-good backup (#353). Verification shares ctx with backup creation, so a stuck tar/gzip read cannot wedge cleanup.
func (*DeployOps) ComposeUp ¶
ComposeUp runs docker compose up for the specified compose file. Uses the configured compose up timeout if the parent context has no deadline. Returns an error if compose up fails (caller should handle rollback).
func (*DeployOps) ComposeUpIsolated ¶ added in v0.29.0
func (d *DeployOps) ComposeUpIsolated(ctx context.Context, composeFiles []string, backupPath string) (*ComposeUpSummary, error)
ComposeUpIsolated runs compose up per-file with isolated failure handling. Phase 1: each file gets its own compose up (no --remove-orphans). On failure, the single file is rolled back from backup if available. Phase 2: a single orphan-reconciliation pass with all files and --remove-orphans.
func (*DeployOps) ComposeUpMultiple ¶ added in v0.2.8
ComposeUpMultiple runs docker compose up for multiple compose files. Uses the configured compose up timeout if the parent context has no deadline. Returns an error if compose up fails (caller should handle rollback).
func (*DeployOps) ComposeUpRemote ¶
ComposeUpRemote runs docker compose up on a remote host via SSH. Retries on transient SSH errors with exponential backoff.
func (*DeployOps) DeployLocal ¶
func (d *DeployOps) DeployLocal(ctx context.Context, sourceDir, targetDir string, result *DeployResult, prevManaged map[string]bool) error
DeployLocal syncs files locally using native Go file operations. Performs atomic copy: copies to temp directory first, then replaces target. Uses --delete semantics: removes files in target that don't exist in source.
func (*DeployOps) DeployLocalFile ¶
func (d *DeployOps) DeployLocalFile(ctx context.Context, sourceFile, targetFile string, result *DeployResult) error
DeployLocalFile syncs a single file locally using native Go file operations. Uses atomic copy via temp file. When ContentHashSync is enabled, skips writing if the file content has not changed.
func (*DeployOps) DeployRemote ¶
func (d *DeployOps) DeployRemote(ctx context.Context, sourceDir, targetHost, targetDir string, verifyChecksums bool) error
DeployRemote syncs files to a remote host using tar-over-SSH. Uses RemoteDeployTimeout if the parent context has no deadline. Retries on transient SSH errors with exponential backoff. Deployment is safe-by-ordering: tar to a temp dir, then a retain-old rename-swap that never deletes the live target before the replacement is in place (#343), with crash recovery for interrupted swaps.
When verifyChecksums is true, the staged tree is SHA-256 verified against a locally-built manifest AFTER the tar lands and BEFORE the swap to live: a truncated or misdirected transfer fails with ErrTransferIntegrity and retries into a fresh remote tmpDir (#334). Callers set it from a once-per-deploy `sha256sum` availability probe (remoteHasSha256sum); false degrades to the pre-#334 behavior of promoting whatever landed.
func (*DeployOps) DeployRemoteFile ¶
func (d *DeployOps) DeployRemoteFile(ctx context.Context, sourceFile, targetHost, targetFile string) error
DeployRemoteFile syncs a single file to a remote host using scp. Uses RemoteDeployTimeout if the parent context has no deadline. Retries on transient SSH errors with exponential backoff. Performs atomic copy: scp to temp file, then move to target.
func (*DeployOps) EnsureRemoteDir ¶
EnsureRemoteDir ensures a directory exists on a remote host via SSH. Uses SSHTimeout if the parent context has no deadline. Retries on transient SSH errors with exponential backoff.
func (*DeployOps) LatestVerifiedBackup ¶ added in v0.37.10
LatestVerifiedBackup returns the path to the most recent backup directory under backupDir whose archive passes VerifyBackup, scanning newest-first. It is the rollback-anchor fallback when a fresh backup fails: a deploy must never proceed without a verified anchor, so the caller uses this to recover a prior good backup (and aborts if none qualifies) (#240).
Verification runs under ctx; a caller deadline/cancellation aborts the scan.
func (*DeployOps) RollbackFromBackupSet ¶ added in v0.39.1
func (d *DeployOps) RollbackFromBackupSet(ctx context.Context, set RollbackSet, backupPath string) error
RollbackFromBackupSet restores a full managed tree from the backup archive at backupPath, then re-applies the restored compose files. Order:
- copy every backed-up managed file over its live counterpart
- (opt-in) delete live managed files absent from the backup
- `docker compose up` on the restored compose files (LAST)
A per-file failure does NOT abort the restore — failures are collected and returned via errors.Join, so one unreadable file cannot strand the rest of the tree half-restored. Returns errRollbackNotAttempted when there is no usable backup, mirroring the prior compose-only path.
func (*DeployOps) RollbackRemoteCompose ¶ added in v0.38.5
func (d *DeployOps) RollbackRemoteCompose(ctx context.Context, host, remoteComposeDir, backupPath string, deployErr error) error
RollbackRemoteCompose restores the remote compose dir from the backup anchor after a failed remote `docker compose up`, then re-deploys and restarts (#340). It is the remote counterpart to the local rollback path. The re-push goes through the SAME hardened DeployRemote, so the transfer integrity check (#334) covers the restored tree automatically.
deployErr is the original compose-up failure being recovered from; it is wrapped into the returned sentinel. RollbackRemoteCompose ALWAYS returns a non-nil error:
- ErrRollbackSucceeded (wrapping deployErr) when the backup was restored and re-applied cleanly, and
- ErrRollbackFailed (wrapping both errors) when any rollback step failed.
remoteComposeDir is the live remote compose dir the failed deploy just wrote — passed by the caller where it is in scope, never derived from r.lastComposeFiles (only deployLocal sets that).
func (*DeployOps) SignalContainer ¶
SignalContainer sends a signal to a Docker container.
func (*DeployOps) SignalContainerRemote ¶
func (d *DeployOps) SignalContainerRemote(ctx context.Context, host, containerName, signal string) error
SignalContainerRemote sends a signal to a Docker container on a remote host. Retries on transient SSH errors with exponential backoff.
func (*DeployOps) VerifyBackup ¶
VerifyBackup checks that a backup archive is valid and non-empty. The archive listing runs under ctx so a caller deadline or cancellation aborts verification rather than blocking on a large/growing archive (#319).
type DeployResult ¶ added in v0.12.0
type DeployResult struct {
// WrittenFiles contains relative paths of files that were written to disk.
WrittenFiles []string
// DeletedFiles contains relative paths of files removed from disk by
// removeStaleFiles's --delete-style pruning. Tracked separately from
// WrittenFiles because a deletion is not a write; callers that need "every
// file touched this deploy" (e.g. post-sync hook matching) combine both
// lists explicitly.
DeletedFiles []string
// ManagedFiles is the full set of files bosun deployed this run (every
// regular file in the source tree, not just the changed ones in
// WrittenFiles), as appdata-relative paths. Persisted to DeployState as the
// manifest that scopes the next reconcile's stale-file pruning.
ManagedFiles []string
}
DeployResult tracks which files were actually written or deleted during deployment. Used to inform post-sync hooks about actual on-disk changes.
func (*DeployResult) AddDeleted ¶ added in v0.37.6
func (r *DeployResult) AddDeleted(files ...string)
AddDeleted appends file paths to the result's deleted files list.
func (*DeployResult) AddManaged ¶ added in v0.37.1
func (r *DeployResult) AddManaged(files ...string)
AddManaged appends file paths to the result's managed-files manifest.
func (*DeployResult) AddWritten ¶ added in v0.12.0
func (r *DeployResult) AddWritten(files ...string)
AddWritten appends file paths to the result's written files list.
func (*DeployResult) PrefixLatest ¶ added in v0.32.2
func (r *DeployResult) PrefixLatest(snapshot int, prefix string)
PrefixLatest prepends prefix to all WrittenFiles entries added after the snapshot index. Call with len(r.WrittenFiles) before a DeployLocal call, then PrefixLatest after, to give the new entries context needed for hook glob matching.
func (*DeployResult) PrefixLatestDeleted ¶ added in v0.37.6
func (r *DeployResult) PrefixLatestDeleted(snapshot int, prefix string)
PrefixLatestDeleted prepends prefix to all DeletedFiles entries added after the snapshot index. Mirrors PrefixLatest so deleted paths get the same staging-relative prefix needed for hook glob matching.
type DeployState ¶ added in v0.3.0
type DeployState struct {
SchemaVersion int `json:"schema_version"`
LastDeployedCommit string `json:"last_deployed_commit,omitempty"`
DeployedAt time.Time `json:"deployed_at,omitempty"`
DeployCount int `json:"deploy_count,omitempty"`
Source string `json:"source,omitempty"`
LastAttemptedCommit string `json:"last_attempted_commit,omitempty"`
AttemptCount int `json:"attempt_count,omitempty"`
LastAlertedAttempt int `json:"last_alerted_attempt,omitempty"`
// Declared state snapshot from last successful deployment.
DeclaredServices []DeclaredService `json:"declared_services,omitempty"`
// Drift detection results from last check.
DriftCheckedAt time.Time `json:"drift_checked_at,omitempty"`
DriftItems []DriftItem `json:"drift_items,omitempty"`
// Drift alert deduplication state.
DriftAlertedAt time.Time `json:"drift_alerted_at,omitempty"`
DriftAlertedItems map[string]time.Time `json:"drift_alerted_items,omitempty"`
// Drift alert debounce state: tracks first-seen timestamps for items
// within their debounce window. Items graduate to the dedup layer
// when the debounce duration elapses, or are removed if drift resolves.
DriftDebounceItems map[string]time.Time `json:"drift_debounce_items,omitempty"`
// NeedsRedeploy indicates that a previous deploy partially succeeded
// (configs were synced to disk) but compose up failed. When true, the
// next reconcile will re-run the deploy pipeline even if there are no
// new git changes, bypassing the shouldSkipDeploy check.
NeedsRedeploy bool `json:"needs_redeploy,omitempty"`
// Health verification results from last post-deploy check.
HealthVerifiedAt time.Time `json:"health_verified_at,omitempty"`
HealthVerificationPassed bool `json:"health_verification_passed,omitempty"`
// Restart circuit breaker: tracks per-container restart counts to detect crash loops.
RestartTracking map[string]RestartTrackingEntry `json:"restart_tracking,omitempty"`
// DeployedFiles is the manifest of files bosun wrote on the last successful
// deploy, as appdata-relative paths (e.g. "authelia/configuration.yml").
// Stale-file pruning deletes only files in this set that are gone from the
// current source — never files bosun did not deploy (e.g. container runtime
// data). Empty on a fresh state file, which makes the first deploy prune
// nothing and simply seed the manifest.
DeployedFiles []string `json:"deployed_files,omitempty"`
}
DeployState tracks the last successful deployment and attempt history.
func LoadState ¶ added in v0.3.0
func LoadState(path string) *DeployState
LoadState reads the deploy state file. Returns zero state on missing or corrupt files — this is correct fail-open behavior (triggers a full deploy).
func (*DeployState) RecordDriftAlerts ¶ added in v0.39.8
func (s *DeployState) RecordDriftAlerts(items []DriftItem, alertedAt time.Time)
RecordDriftAlerts advances alert state after successful delivery. A service can have only one active critical drift type, so a delivered replacement retires the previous type without reporting a false resolution.
type DeployTarget ¶ added in v0.28.0
type DeployTarget struct {
// RelPath is the path relative to the staging subdirectory (e.g. "appdata/traefik", "compose").
// Used for source path construction and filter matching.
RelPath string
// TargetPath is the path relative to the appdata base directory on the deploy target.
// For appdata children, this strips the "appdata/" prefix (e.g. "traefik").
// For top-level entries, this matches RelPath (e.g. "compose").
TargetPath string
// IsDir indicates whether the target is a directory (true) or a single file (false).
IsDir bool
}
DeployTarget represents a discovered directory or file in the staging area that should be synced to the target host during deployment.
type Deployer ¶
type Deployer interface {
// Deploy syncs files to the target directory.
// For local deployment, host should be empty.
// For remote deployment, host should be "user@host".
Deploy(ctx context.Context, srcDir, host, dstDir string) error
// DeployFile syncs a single file to the target.
DeployFile(ctx context.Context, srcFile, host, dstFile string) error
// Backup creates a timestamped backup of the specified paths.
// For local backup, host should be empty.
// For remote backup, host should be "user@host".
Backup(ctx context.Context, host, backupDir string, paths []string) (string, error)
// EnsureDir ensures a directory exists.
// For local, host should be empty.
// For remote, host should be "user@host".
EnsureDir(ctx context.Context, host, dir string) error
// ComposeUp runs docker compose up for the specified compose file or directory.
// For local, host should be empty.
// For remote, host should be "user@host".
ComposeUp(ctx context.Context, host, composePath string) error
// SignalContainer sends a signal to a Docker container.
// For local, host should be empty.
// For remote, host should be "user@host".
SignalContainer(ctx context.Context, host, containerName, signal string) error
// CleanupBackups removes old backups, keeping only the most recent N valid ones.
CleanupBackups(ctx context.Context, backupDir string, keep int) error
}
Deployer handles file deployment.
type DockerClientFunc ¶ added in v0.3.0
DockerClientFunc returns a Docker client, or nil if unavailable.
type DriftIgnoreRule ¶ added in v0.29.1
type DriftIgnoreRule struct {
Service string `yaml:"service" json:"service"` // Glob pattern matching service name (e.g., "traefik", "*.monitoring")
Type string `yaml:"type" json:"type"` // Drift type to ignore: "missing", "image_mismatch", "unhealthy", or "*"
}
DriftIgnoreRule defines a pattern for suppressing known drift noise. Service supports glob patterns (filepath.Match) and Type can be a specific drift type or "*" to match all types.
type DriftItem ¶ added in v0.3.0
type DriftItem struct {
Service string `json:"service"`
Type DriftType `json:"type"`
Declared string `json:"declared,omitempty"`
Actual string `json:"actual,omitempty"`
}
DriftItem describes a single drift between declared and actual state.
func FilterDebounced ¶ added in v0.18.0
func FilterDebounced(currentItems []DriftItem, debounceItems map[string]time.Time, debounce time.Duration) []DriftItem
FilterDebounced filters drift items through the debounce layer. Items that have not persisted beyond the debounce duration are held back. Items that have persisted past the debounce window are returned (graduated) for normal dedup processing.
The debounceItems map is mutated in place:
- New items are added with the current timestamp
- Graduated items (past window) are removed
- Items no longer in currentItems are removed (resolved)
When debounce is zero (disabled), all items pass through unchanged.
func FilterIgnoredDriftItems ¶ added in v0.29.1
func FilterIgnoredDriftItems(items []DriftItem, rules []DriftIgnoreRule) []DriftItem
FilterIgnoredDriftItems returns only the drift items that do not match any ignore rule. This is the exported entry point for callers outside the reconcile package.
func ShouldAlertDrift ¶ added in v0.10.0
func ShouldAlertDrift(activeItems, alertCandidates []DriftItem, alertedItems map[string]time.Time, cooldown time.Duration) (alertItems []DriftItem, resolvedKeys []string)
ShouldAlertDrift compares active drift and alert candidates against previously alerted items, returning which candidates should trigger alerts and which services have resolved.
An alert candidate triggers if it is new (not in alertedItems) or if its cooldown has expired. A key is considered resolved only when its service is absent from activeItems; a missing/unhealthy type transition is still active drift for that service. alertCandidates may be a debounce-filtered subset of activeItems.
type DriftReport ¶ added in v0.3.0
type DriftReport struct {
CheckedAt time.Time
Items []DriftItem
IgnoredCount int // Number of items filtered out by ignore rules
}
DriftReport is the result of comparing declared vs actual state.
func CompareDrift ¶ added in v0.3.0
func CompareDrift(declared []DeclaredService, actual []ActualService) *DriftReport
CompareDrift compares declared services against actual running services and returns a drift report identifying any discrepancies.
func RunDriftCheck ¶ added in v0.3.0
func RunDriftCheck(ctx context.Context, client *docker.Client, stateFile, projectName string, ignoreRules []DriftIgnoreRule) (*DriftReport, error)
RunDriftCheck performs a full drift check: loads declared state from the state file and compares against actual Docker state. Ignore rules filter out known drift noise before reporting.
func (*DriftReport) DriftSummaries ¶ added in v0.15.0
func (r *DriftReport) DriftSummaries() []string
DriftSummaries returns a slice of "service:type" strings for each drift item. Suitable for structured log fields where the full list of drifting containers needs to be queryable (not just the count).
func (*DriftReport) HasCriticalDrift ¶ added in v0.3.0
func (r *DriftReport) HasCriticalDrift() bool
HasCriticalDrift returns true if drift includes missing or unhealthy services.
func (*DriftReport) HasDrift ¶ added in v0.3.0
func (r *DriftReport) HasDrift() bool
HasDrift returns true if any drift items were detected.
type DriftType ¶ added in v0.3.0
type DriftType string
DriftType classifies the kind of drift detected.
const ( // DriftMissing indicates a declared service is not running. DriftMissing DriftType = "missing" // DriftImageMismatch indicates a running service uses a different image than declared. DriftImageMismatch DriftType = "image_mismatch" // DriftUnhealthy indicates a declared service is running but unhealthy. DriftUnhealthy DriftType = "unhealthy" )
type Duration ¶ added in v0.8.0
Duration wraps time.Duration with YAML/JSON marshaling support. It accepts Go duration strings ("5s", "2m30s") and bare seconds ("5", "30").
func (Duration) IsZero ¶ added in v0.8.0
IsZero returns true if the duration is zero, supporting omitempty in YAML/JSON.
func (Duration) MarshalJSON ¶ added in v0.8.0
MarshalJSON writes the duration as a Go duration string.
func (Duration) MarshalYAML ¶ added in v0.8.0
MarshalYAML writes the duration as a Go duration string.
func (*Duration) UnmarshalJSON ¶ added in v0.8.0
UnmarshalJSON handles both string ("5s") and number (5 as seconds) formats.
type GitOperations ¶
type GitOperations interface {
// Sync clones or pulls depending on whether repo exists.
// Returns (changed, beforeCommit, afterCommit, error).
// For fresh clones, changed is always true.
Sync(ctx context.Context) (changed bool, before, after string, err error)
// IsRepo checks if the directory is a git repository.
// Uses the provided context for timeout control.
IsRepo(ctx context.Context) bool
// DiffFiles returns the list of changed file paths between two commits.
// Paths are relative to the repository root.
DiffFiles(ctx context.Context, fromCommit, toCommit string) ([]string, error)
}
GitOperations defines git sync operations.
type GitOps ¶
type GitOps struct {
// RepoURL is the git repository URL to clone.
RepoURL string
// Branch is the branch to checkout/track.
Branch string
// Dir is the local directory for the repository.
Dir string
}
GitOps represents git operations for the reconciliation workflow.
func (*GitOps) Clone ¶
Clone clones the repository with the specified depth. If depth is 0, a full clone is performed. Uses GitCloneTimeout if the parent context has no deadline.
func (*GitOps) DiffFiles ¶ added in v0.6.0
DiffFiles returns the list of changed file paths between two commits. If fromCommit is empty, returns all files in toCommit.
func (*GitOps) GetCommitMessage ¶
GetCommitMessage returns the commit message for the current HEAD.
func (*GitOps) GetLatestCommit ¶
GetLatestCommit returns the current HEAD commit hash.
func (*GitOps) IsRepo ¶
IsRepo checks if the directory is a git repository. Uses the provided context for timeout control.
func (*GitOps) Pull ¶
Pull fetches and resets to the remote branch. Returns (changed, beforeCommit, afterCommit, error). Uses GitFetchTimeout for network operations.
func (*GitOps) RemoteBranchExists ¶
RemoteBranchExists checks if a remote branch exists.
type HealthCheckResult ¶ added in v0.19.0
type HealthCheckResult struct {
Passed bool
Unhealthy []string
Duration time.Duration
Iterations int
}
HealthCheckResult holds the outcome of a post-deploy health poll.
type PostSyncHook ¶ added in v0.6.0
type PostSyncHook struct {
// Paths are glob patterns matched against changed files (relative to repo root).
Paths []string `json:"paths" yaml:"paths"`
// Action is the operation to perform: "restart" (default) or "exec".
Action string `json:"action" yaml:"action"`
// Container is the name of the container to act on.
Container string `json:"container" yaml:"container"`
// Command is the command to execute inside the container (for action: "exec").
// Accepts a list of strings (e.g., ["nginx", "-s", "reload"]).
Command []string `json:"command,omitempty" yaml:"command,omitempty"`
// Delay is an optional pause before executing this hook's action.
// Useful when a container needs extra time for config propagation.
Delay Duration `json:"delay,omitempty" yaml:"delay,omitempty"`
}
PostSyncHook defines a container action triggered when specific file paths change.
func EvaluatePostSyncHooks ¶ added in v0.6.0
func EvaluatePostSyncHooks(changedFiles []string, hooks []PostSyncHook) []PostSyncHook
EvaluatePostSyncHooks matches changed file paths against hook glob patterns and returns the hooks that should be executed. Deduplication keys on container+action, so a container can have both a restart and an exec hook fire from the same change set.
type Reconciler ¶
type Reconciler struct {
// contains filtered or unexported fields
}
Reconciler orchestrates the GitOps reconciliation workflow.
func NewReconciler ¶
func NewReconciler(cfg *Config, opts ...ReconcilerOption) *Reconciler
NewReconciler creates a new Reconciler with the given configuration.
func (*Reconciler) Run ¶
func (r *Reconciler) Run(ctx context.Context) (runErr error)
Run executes the full reconciliation workflow.
func (*Reconciler) SetRunOptions ¶ added in v0.3.0
func (r *Reconciler) SetRunOptions(source string, force bool)
SetRunOptions sets per-run options (source and force) on the reconciler config. This is called by the daemon before each Run() to pass trigger context.
type ReconcilerOption ¶
type ReconcilerOption func(*Reconciler)
ReconcilerOption is a functional option for configuring the Reconciler.
func WithAlerter ¶
func WithAlerter(alerter AlertSender) ReconcilerOption
WithAlerter sets the alert sender for notifications.
func WithDeployOps ¶
func WithDeployOps(deploy *DeployOps) ReconcilerOption
WithDeployOps sets the DeployOps implementation.
func WithDockerClient ¶ added in v0.3.0
func WithDockerClient(client *docker.Client) ReconcilerOption
WithDockerClient sets the Docker client for post-deploy verification.
func WithDockerClientFunc ¶ added in v0.3.0
func WithDockerClientFunc(fn DockerClientFunc) ReconcilerOption
WithDockerClientFunc sets a lazy Docker client provider for post-deploy verification.
func WithGitOperations ¶
func WithGitOperations(git GitOperations) ReconcilerOption
WithGitOperations sets the GitOperations implementation.
func WithLockFile ¶
func WithLockFile(path string) ReconcilerOption
WithLockFile sets the lock file path.
func WithSecretsDecryptor ¶
func WithSecretsDecryptor(sops SecretsDecryptor) ReconcilerOption
WithSecretsDecryptor sets the SecretsDecryptor implementation.
type ReloadedConfig ¶ added in v0.12.1
type ReloadedConfig struct {
PostSyncHooks []PostSyncHook
HookSettleDelay *time.Duration
DeployPaths []string
DeploySyncPaths []string
DeploySyncExclude []string
CriticalContainers []string
DriftIgnore []DriftIgnoreRule
OnFailure *bool
OnSuccess *bool
RemoveOrphans *bool
// ProjectName is the repo bosun.yaml's root-level project_name. When
// non-nil and non-empty, the default-target reconciler adopts it before
// the first deploy (#390); a lone `default` target's project_name wins
// over it.
ProjectName *string
// Targets is the reloaded target list from the repo's bosun.yaml.
// When non-nil, the daemon uses these targets for the next reconciliation cycle.
Targets []Target
}
ReloadedConfig holds the fields that can be reloaded from the repo's bosun.yaml.
type RestartBreakerResult ¶ added in v0.20.0
type RestartBreakerResult struct {
// Tripped contains service names that exceeded the restart threshold.
Tripped []string
// Resolved contains service names that stabilized after being tripped.
Resolved []string
// Updated is the new tracking state to persist.
Updated map[string]RestartTrackingEntry
}
RestartBreakerResult holds the outcome of a restart circuit breaker evaluation.
func RunRestartBreaker ¶ added in v0.20.0
func RunRestartBreaker( ctx context.Context, client *docker.Client, actual []ActualService, state *DeployState, threshold int, window time.Duration, ) (*RestartBreakerResult, error)
RunRestartBreaker performs restart circuit breaker evaluation and takes action on containers that exceed the restart threshold. Returns the result for alerting.
type RestartTrackingEntry ¶ added in v0.20.0
type RestartTrackingEntry struct {
RestartCount int `json:"restart_count"`
CheckedAt time.Time `json:"checked_at"`
Tripped bool `json:"tripped"`
TrippedAt time.Time `json:"tripped_at,omitempty"`
}
RestartTrackingEntry tracks the restart count for a container across drift checks.
type RollbackSet ¶ added in v0.39.1
type RollbackSet struct {
// Files are the LIVE absolute paths of every managed file this deploy wrote
// (DeployResult.ManagedFiles joined under the appdata root). Each is restored
// from its backed-up copy. A file ABSENT from the backup was added by the
// failed deploy; it is removed only when DeleteMissing is set.
Files []string
// ComposeFiles are the LIVE compose file paths to `docker compose up` after
// the tree is restored — the restart, run LAST so it acts on the fully
// reverted config.
ComposeFiles []string
// Root is the appdata directory every entry in Files must resolve within. It
// backstops the DELETE and restore paths: even though Files are derived from
// ManagedFiles (always inside appdata today), an explicit containment check
// means a future absolute-path-target regression can't silently reopen the
// filepath.Join prefix-drop footgun on a path we os.Remove.
Root string
// DeleteMissing removes live managed files absent from the backup (files the
// failed deploy added). Default false, and safe to enable ONLY against a
// fresh anchor — this deploy's own pre-deploy backup, tracked by the
// Reconciler's lastBackupIsFresh bool. A stale fallback anchor predates files
// a later legitimate deploy added, so deleting "missing" files against it
// would destroy real data (the #331 incident class).
DeleteMissing bool
}
RollbackSet describes a full managed-tree restore from a backup archive — the single chokepoint the health-gate rollback funnels through (#445). It widens rollback beyond the compose files: a failed deploy's appdata config writes are reverted too, so a rollback no longer leaves a hybrid tree (old compose paired with the failed deploy's new config).
ComposeUpIsolated keeps its OWN compose-only per-file rollback and does NOT go through this set — its rollback is a targeted restart of a single failed stack, not a whole-tree revert.
type SOPSOps ¶
type SOPSOps struct{}
SOPSOps provides SOPS decryption operations.
func (*SOPSOps) CheckAgeKey ¶
CheckAgeKey verifies that an age key is available for SOPS decryption. It checks in order:
- SOPS_AGE_KEY environment variable
- SOPS_AGE_KEY_FILE environment variable
- Default location: ~/.config/sops/age/keys.txt
Returns nil if a key is found, or an error with setup instructions if not.
func (*SOPSOps) Decrypt ¶
Decrypt decrypts a SOPS-encrypted file and returns the plaintext bytes as JSON. It first validates the file is SOPS-encrypted and checks that an age key is available. Uses go-sops library for in-process decryption - no external binary required.
func (*SOPSOps) DecryptFiles ¶
DecryptFiles decrypts multiple SOPS files and merges them into a single map. Later files override earlier ones for duplicate keys. This method implements the SecretsDecryptor interface.
func (*SOPSOps) DecryptToJSON ¶
DecryptToJSON decrypts files and returns merged JSON bytes.
type SecretsDecryptor ¶
type SecretsDecryptor interface {
// DecryptFiles decrypts multiple SOPS files and merges them into a single map.
// Later files override earlier ones for duplicate keys.
DecryptFiles(ctx context.Context, files []string) (map[string]any, error)
// CheckAgeKey verifies that an age key is available for SOPS decryption.
CheckAgeKey() error
}
SecretsDecryptor handles SOPS decryption.
type Target ¶ added in v0.30.0
type Target struct {
// Name identifies this target (e.g., "unraid", "pi"). Used in file paths and logs.
Name string `json:"name"`
// TargetHost is empty for local deployment, or "user@host" for remote.
TargetHost string `json:"target_host,omitempty"`
// LocalAppdataPath is the path to appdata when running locally.
LocalAppdataPath string `json:"local_appdata_path,omitempty"`
// RemoteAppdataPath is the path to appdata on the remote host.
RemoteAppdataPath string `json:"remote_appdata_path,omitempty"`
// ProjectName is the docker compose project name for this target.
ProjectName string `json:"project_name,omitempty"`
// StateFile overrides the derived state file path. When empty, derived from Name.
StateFile string `json:"state_file,omitempty"`
// StagingDir overrides the derived staging directory. When empty, derived from Name.
StagingDir string `json:"staging_dir,omitempty"`
// SecretsScope is the key prefix for per-target secrets (e.g., "unraid" -> targets.unraid.*).
SecretsScope string `json:"secrets_scope,omitempty"`
// CriticalContainers overrides the global list for this target.
CriticalContainers []string `json:"critical_containers,omitempty"`
// PostSyncHooks overrides the global hooks for this target.
PostSyncHooks []PostSyncHook `json:"post_sync_hooks,omitempty"`
// DeploySyncPaths overrides the global allowlist for this target.
DeploySyncPaths []string `json:"deploy_sync_paths,omitempty"`
// DeploySyncExclude overrides the global blocklist for this target.
DeploySyncExclude []string `json:"deploy_sync_exclude,omitempty"`
}
Target describes a single deployment target (a server/host to deploy to). Each target has its own host, appdata paths, project name, state file, staging directory, secrets scope, and operational overrides.
func ValidateAndSanitizeTargets ¶ added in v0.34.3
func ValidateAndSanitizeTargets(targets []Target, warn func(target, field string, err error)) []Target
ValidateAndSanitizeTargets validates security-sensitive fields on each target, clearing invalid fields with a warning rather than dropping the whole target. This mirrors the YAML-load semantics in extractTargets — a single bad field must not block deployments to that host.
The logger parameter receives structured warn entries; pass nil to suppress logging.
type TemplateOps ¶
type TemplateOps struct {
// Data is the template data available during rendering.
Data map[string]any
// IncludeDir is the allowlisted root for include/fromJsonFile reads: only
// files located within this subtree may be read, enumerating what MAY be
// read rather than blocklisting what may not. When empty, RenderDirectory
// defaults it to <sourceDir>/DefaultIncludeSubdir. When still empty at
// render time (e.g. a bare ExecuteTemplate call with no tree), validation
// is skipped (backwards-compatible for callers that supply no root).
IncludeDir string
}
TemplateOps provides template rendering operations using Go's text/template with sprig functions.
func NewTemplateOps ¶
func NewTemplateOps(data map[string]any) *TemplateOps
NewTemplateOps creates a new TemplateOps instance with the given data.
func (*TemplateOps) ExecuteTemplate ¶
func (t *TemplateOps) ExecuteTemplate(_ context.Context, templateFile, outputFile string) error
ExecuteTemplate renders a single template file using Go's text/template with sprig functions. Template data is passed directly to the template context. Templates can access data via {{ .key }} syntax and use sprig functions.
func (*TemplateOps) RenderDirectory ¶
func (t *TemplateOps) RenderDirectory(ctx context.Context, sourceDir, stagingDir, subDir string) error
RenderDirectory processes all .tmpl files in sourceDir and renders them to stagingDir. Non-template files are copied as-is.
type TemplateRenderer ¶
type TemplateRenderer interface {
// Render processes all .tmpl files in srcDir and renders them to dstDir.
// Non-template files are copied as-is.
Render(ctx context.Context, srcDir, dstDir string, secrets map[string]any) error
}
TemplateRenderer handles Go template rendering with Sprig functions.
Source Files
¶
- alerts.go
- backup.go
- compose.go
- config_reload.go
- configfield.go
- deploy.go
- discovery.go
- drift.go
- duration.go
- git.go
- health.go
- healthgate.go
- hooks.go
- interfaces.go
- lock_unix.go
- open_pinned_other.go
- pure.go
- reconcile.go
- remote_rollback.go
- rename_noreplace_linux.go
- restart_breaker.go
- rollback.go
- sops.go
- ssh.go
- state.go
- target.go
- template.go
- validation.go
- verify.go