diff --git a/internal/application/add/infer.go b/internal/application/add/infer.go index e8b5b12..02d812a 100644 --- a/internal/application/add/infer.go +++ b/internal/application/add/infer.go @@ -10,6 +10,13 @@ import ( "github.com/alyraffauf/cattery/internal/repository" ) +const ( + repositoryPathSeparator = "/" + platformLayerDirectoryPrefix = "_" + rootScopeName = "" + dotPathPrefix = "." +) + // inferContext bundles the read-only inputs of ownership inference so Infer // stays under the parameter limit. Targets are canonical absolute paths // resolved against the working directory before inference begins, so Infer @@ -129,15 +136,15 @@ func (location sourceLocation) sourcePath(target string) string { var builder strings.Builder if !location.scope.IsRoot() { builder.WriteString(location.scope.Group) - builder.WriteString("/") + builder.WriteString(repositoryPathSeparator) } if location.layer != deployment.LayerBase { - builder.WriteString("_") + builder.WriteString(platformLayerDirectoryPrefix) builder.WriteString(string(location.layer)) - builder.WriteString("/") + builder.WriteString(repositoryPathSeparator) } if location.kind == deployment.FileSecret { - builder.WriteString(repository.SecretDirectoryName + "/") + builder.WriteString(repository.SecretDirectoryName + repositoryPathSeparator) } builder.WriteString(target) return builder.String() @@ -160,7 +167,7 @@ func inferScope(request Request) (deployment.Scope, error) { } return deployment.NewScope(request.Group), nil } - return deployment.NewScope(""), nil + return deployment.NewScope(rootScopeName), nil } // inferLayer selects an explicit platform layer that must match the runtime @@ -204,7 +211,7 @@ func checkRepresentable(scope deployment.Scope, layer deployment.Layer, relative return failure.New(failure.InvalidInput, "add: target "+relative+" is a reserved metadata name; pass --group", nil) } - if strings.Contains(relative, "/") && !strings.HasPrefix(first, ".") { + if strings.Contains(relative, repositoryPathSeparator) && !strings.HasPrefix(first, dotPathPrefix) { return failure.New(failure.InvalidInput, "add: target "+relative+" is unrepresentable at the root base layer; pass --group", nil) } diff --git a/internal/application/initialize/service.go b/internal/application/initialize/service.go index 2452843..03298d0 100644 --- a/internal/application/initialize/service.go +++ b/internal/application/initialize/service.go @@ -215,10 +215,10 @@ func existingDirectory(path string) (bool, error) { func requireDirectory(path string) error { info, err := os.Stat(path) if err != nil { - return err + return failure.New(failure.Operational, "initialize: inspect directory "+path, err) } if !info.IsDir() { - return fmt.Errorf("initialize: %s is not a directory", path) + return failure.New(failure.InvalidInput, "initialize: "+path+" is not a directory", nil) } return nil } diff --git a/internal/application/inspect/status.go b/internal/application/inspect/status.go index 42028e4..2f3b063 100644 --- a/internal/application/inspect/status.go +++ b/internal/application/inspect/status.go @@ -212,7 +212,7 @@ func recordsConvergedGeneric[T interface{ Converged() bool }](records []T) bool // actionName returns the stable status name of one reconciliation action. // Unknown values are rendered explicitly instead of being mistaken for no-op. func actionName(action reconcile.Action) string { - names := [reconcile.ActionRetireAliasState + 1]string{ + names := map[reconcile.Action]string{ reconcile.ActionNoOp: "no-op", reconcile.ActionCorrectMode: "correct-mode", reconcile.ActionCreateTarget: "create-target", @@ -225,16 +225,17 @@ func actionName(action reconcile.Action) string { reconcile.ActionVerifyAlias: "verify-alias", reconcile.ActionRetireAliasState: "retire-alias-state", } - if int(action) < 0 || int(action) >= len(names) { + name, ok := names[action] + if !ok { return fmt.Sprintf("unknown-action-%d", action) } - return names[action] + return name } // reasonName returns the stable status name of one reconciliation reason. // Unknown values are rendered explicitly instead of being mistaken for no-change. func reasonName(reason reconcile.Reason) string { - names := [reconcile.ReasonAlreadyRetired + 1]string{ + names := map[reconcile.Reason]string{ reconcile.ReasonNoChange: "no-change", reconcile.ReasonModeCorrection: "mode-correction", reconcile.ReasonSourceChanged: "source-changed", @@ -254,8 +255,9 @@ func reasonName(reason reconcile.Reason) string { reconcile.ReasonInactivePlatform: "inactive-platform", reconcile.ReasonAlreadyRetired: "already-retired", } - if int(reason) < 0 || int(reason) >= len(names) { + name, ok := names[reason] + if !ok { return fmt.Sprintf("unknown-reason-%d", reason) } - return names[reason] + return name } diff --git a/internal/reconcile/source_snapshot.go b/internal/reconcile/source_snapshot.go index 1da2df2..4125511 100644 --- a/internal/reconcile/source_snapshot.go +++ b/internal/reconcile/source_snapshot.go @@ -12,6 +12,8 @@ import ( "github.com/alyraffauf/cattery/internal/secrets" ) +const sourceGrowthDetectionSlack int64 = 1 + // SourceObservation pairs a frozen source snapshot with the exact bytes read // during capture. The bytes are retained for the write phase and can be // explicitly cleared when the observation is no longer needed. @@ -104,7 +106,7 @@ func readVerifiedSource(path string, before pathsafe.Identity) ([]byte, pathsafe return nil, pathsafe.Identity{}, err } defer handle.Close() - data, err := io.ReadAll(io.LimitReader(handle, before.Size()+1)) + data, err := io.ReadAll(io.LimitReader(handle, before.Size()+sourceGrowthDetectionSlack)) if err != nil { return data, pathsafe.Identity{}, fmt.Errorf("reconcile: read source %s: %w", path, err) } diff --git a/internal/state/database.go b/internal/state/database.go index fb48bf5..5306e18 100644 --- a/internal/state/database.go +++ b/internal/state/database.go @@ -19,11 +19,13 @@ import ( // (PLAN.md Section 8.1). The directory is private to the owning user; the // database and lock files are read-write but never searchable by others. const ( - catteryDirectoryName = "cattery" - stateDatabaseFileName = "state.db" - stateLockFileName = "cattery.lock" - stateDirectoryMode os.FileMode = 0o700 - stateFileMode os.FileMode = 0o600 + catteryDirectoryName = "cattery" + stateDatabaseFileName = "state.db" + stateLockFileName = "cattery.lock" + stateDirectoryMode os.FileMode = 0o700 + stateFileMode os.FileMode = 0o600 + singleDatabaseConnection = 1 + busyTimeoutMilliseconds = 5000 ) // sqliteDriverName is the registration name modernc.org/sqlite uses. @@ -158,7 +160,7 @@ func openConnection(path string) (*sql.DB, error) { if err != nil { return nil, err } - conn.SetMaxOpenConns(1) + conn.SetMaxOpenConns(singleDatabaseConnection) if err := applyPragmas(conn); err != nil { _ = conn.Close() return nil, err @@ -178,7 +180,7 @@ func applyPragmas(conn *sql.DB) error { func sqlitePragmas() []string { return []string{ "PRAGMA foreign_keys = ON", - "PRAGMA busy_timeout = 5000", + fmt.Sprintf("PRAGMA busy_timeout = %d", busyTimeoutMilliseconds), "PRAGMA journal_mode = WAL", "PRAGMA synchronous = FULL", } diff --git a/internal/state/migrations.go b/internal/state/migrations.go index c2bf105..4034deb 100644 --- a/internal/state/migrations.go +++ b/internal/state/migrations.go @@ -17,6 +17,12 @@ var initialMigrationSQL string // PRAGMA user_version is managed against it. const currentSchemaVersion = 1 +const ( + lockingModeNormalSQL = "PRAGMA locking_mode = NORMAL" + lockingModeExclusiveSQL = "PRAGMA locking_mode = EXCLUSIVE" + userVersionSQL = "PRAGMA user_version" +) + // Migrate applies the embedded schema migration to the database when needed. // Re-running on a current database is a no-op. An unknown newer schema // (user_version greater than current) is rejected so a newer Cattery never @@ -51,14 +57,14 @@ func applyMigration(database *Database) error { if err := transaction.Commit(); err != nil { return err } - if _, err := database.conn.Exec("PRAGMA locking_mode = NORMAL"); err != nil { + if _, err := database.conn.Exec(lockingModeNormalSQL); err != nil { return fmt.Errorf("state: restore normal locking mode: %w", err) } return nil } func beginExclusive(database *Database) (*sql.Tx, error) { - if _, err := database.conn.Exec("PRAGMA locking_mode = EXCLUSIVE"); err != nil { + if _, err := database.conn.Exec(lockingModeExclusiveSQL); err != nil { return nil, fmt.Errorf("state: exclusive locking mode: %w", err) } return database.conn.Begin() @@ -66,7 +72,7 @@ func beginExclusive(database *Database) (*sql.Tx, error) { func readUserVersion(database *Database) (int, error) { var version int - if err := database.conn.QueryRow("PRAGMA user_version").Scan(&version); err != nil { + if err := database.conn.QueryRow(userVersionSQL).Scan(&version); err != nil { return 0, fmt.Errorf("state: read user_version: %w", err) } return version, nil