Documentation
¶
Index ¶
- Constants
- Variables
- func AddGroupTag(fileName string, groupName string, serviceNames []string) error
- func AddServiceFragment(fileName string, serviceName string, fragment []byte) error
- func ApplyEnvEdit(filePath string, ops []EnvEditOp) error
- func ApplyHealthcheck(fileName string, serviceName string, t HealthcheckTemplate, port string) error
- func ApplyServiceFragment(fileName string, serviceName string, fragment []byte) error
- func ComposeFileArgs(composeFile string) []string
- func Deduplicate(input []string) []string
- func DefaultGenericPort(svc types.ServiceConfig) string
- func DeleteService(fileName string, serviceName string) error
- func DockerComposePs(composeFile string) (string, error)
- func DockerMemTotal() (int64, error)
- func DockerSystemDf() (string, error)
- func EditorCommand(path string) *exec.Cmd
- func EnsureBackupStore(composeDir string) error
- func ExtractServiceFragment(fileName string, serviceName string) ([]byte, error)
- func GetComposeFileName(source ComposeSource) (winner string, candidates []string, err error)
- func IsValidServiceName(s string) bool
- func ListComposeFiles(dir string) ([]string, error)
- func NextRestartPolicy(current string) string
- func ParseContainers(output string) ([]apptypes.DockerContainer, error)
- func ReadConfigFile(fileName string) (*types.Project, error)
- func ReadConfigFileExt(fileName string) (*types.Project, string, bool, error)
- func RemoveGroupTag(fileName string, groupName string) error
- func RenameGroupTag(fileName string, oldName string, newName string) error
- func ReplaceFileAtomically(fileName string, contents []byte) error
- func ReplaceFileAtomicallyWithMode(fileName string, contents []byte, newFileMode os.FileMode) error
- func RestoreBackup(sourceFile, backupName string) error
- func RunDockerCompose(action string, target string, isGroup bool, composeFile string, ...) error
- func SecretHint(key string) bool
- func SetGroupMembers(fileName string, groupName string, members []string) error
- func SetRestartPolicy(fileName string, serviceName string, policy string) error
- func SnapshotFile(fileName string) error
- func StreamDockerLogs(target string, isGroup bool, composeFile string, members []string) (<-chan string, context.CancelFunc, error)
- func URLHost(cfg config.Config, env func(string) string) string
- func ValidateComposeCandidate(dir string, contents []byte) error
- func WriteNewComposeFile(fileName string, serviceName string, image string) error
- type BackupEntry
- type ComposeSource
- type DiskUsage
- type DockerState
- type DockerStatsContainer
- type DockerStatus
- type EnvEditOp
- type HealthcheckTemplate
- type HostInfo
- type Remedy
- type ServiceURL
- type URLSource
Constants ¶
const FallbackEditor = "vi"
FallbackEditor is used when neither $VISUAL nor $EDITOR is set. POSIX requires vi, so it is the one editor that can be assumed present.
const MaxBackupsPerSource = 500
MaxBackupsPerSource caps how many past copies the store keeps for a single source file. Compose files are a few KB, so 500 copies stay under a megabyte; the cap exists only to stop a churny file from growing the folder forever. Pruning keeps the newest entries and drops the oldest, on insert.
Variables ¶
var ErrNoComposeFile = errors.New("no compose file found")
ErrNoComposeFile is returned by GetComposeFileName when none of the candidate file names exist in the directory it looked in. The bootstrap flow uses it to distinguish "no file yet" from other load errors and offer to create one.
var ErrServiceRenamed = errors.New("renaming a service is not supported")
ErrServiceRenamed is returned when an edited fragment comes back under a different service name. Renaming is not supported: other services may point at the old name in depends_on:, and a rename that leaves those dangling is worse than a refusal.
var HealthcheckCatalog = []HealthcheckTemplate{ { Name: "PostgreSQL", Matches: []string{"postgres", "postgresql"}, Test: []string{"CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432"}, Interval: "30s", Timeout: "5s", Retries: 3, StartPeriod: "10s", }, { Name: "MariaDB", Matches: []string{"mariadb"}, Test: []string{"CMD-SHELL", "healthcheck.sh --connect --innodb_initialized"}, Interval: "30s", Timeout: "5s", Retries: 3, StartPeriod: "10s", }, { Name: "Redis", Matches: []string{"redis"}, Test: []string{"CMD-SHELL", "redis-cli -h 127.0.0.1 ping | grep PONG"}, Interval: "30s", Timeout: "5s", Retries: 3, StartPeriod: "10s", }, { Name: "nginx", Matches: []string{"nginx"}, Test: []string{"CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null 2>&1"}, Interval: "30s", Timeout: "5s", Retries: 3, StartPeriod: "10s", }, { Name: "Generic HTTP", Test: []string{"CMD-SHELL", "wget -qO- http://127.0.0.1:%s/ >/dev/null 2>&1"}, Interval: "30s", Timeout: "5s", Retries: 3, StartPeriod: "10s", Generic: true, }, }
HealthcheckCatalog is every template offered, in the order high-confidence image-specific probes are checked, ending with the generic fallback. Every probe here ships in the image it targets, needs no authentication, and omits start_interval - accepted by this app's own parser (compose-go v2.12.1) but not guaranteed to be by a user's docker compose CLI older than 2.20.2, so the app's own validation would pass a file the user's own tooling then rejects. Omitting it costs nothing.
Functions ¶
func AddGroupTag ¶
AddGroupTag tags each of the given services with groupName in the compose file at fileName, preserving the file's existing formatting and comments as much as possible. It's idempotent: a service that already carries the tag is left unchanged.
func AddServiceFragment ¶
AddServiceFragment inserts a new service into the compose file at fileName.
It is ApplyServiceFragment's opposite number: same fragment shape, same validation, same atomic write, but it refuses when the name is already taken instead of when it is absent. Insertion is at the end of the services: mapping, which is where a reader expects a new entry and the only position that never reorders the user's file.
func ApplyEnvEdit ¶
ApplyEnvEdit applies a series of edits to an .env file, preserving formatting. Comments, blank lines, key order, quoting, export prefixes, CRLF and BOM survive.
func ApplyHealthcheck ¶
func ApplyHealthcheck(fileName string, serviceName string, t HealthcheckTemplate, port string) error
ApplyHealthcheck inserts or replaces the healthcheck: mapping under serviceName in the compose file at fileName. port is only used by templates with Generic set - it is substituted into Test's CMD-SHELL string - and is otherwise ignored.
The healthcheck is built as a yaml.Node and inserted directly into the service's existing value node, the same read-modify-write shape utils.SetGroupMembers uses, rather than round-tripping through a whole- service fragment: a healthcheck is one mapping under the service, not the service itself.
func ApplyServiceFragment ¶
ApplyServiceFragment parses an edited fragment and writes it back over serviceName in the compose file.
The file is left untouched unless the fragment parses, is shaped like a service, and the whole resulting document still loads as compose. That last check is what makes editing a fragment safer than editing the file by hand.
func ComposeFileArgs ¶
ComposeFileArgs starts the argument list for a `docker compose` invocation, pinning it to composeFile when one is known.
Every docker call in the app goes through this so the UI and the commands can never disagree about which file is in play: the panel shows the file the app resolved, and `--file` makes docker act on that same one. Before this, each side resolved independently - identically, but only because both looked in the current directory. A `--file` flag pointing somewhere else would have split them, which is why this landed first.
An empty composeFile leaves the flag off and lets docker resolve the file itself. That is the bootstrap state (no file loaded yet), where there is nothing to pin to and no panel making a claim to contradict.
--file is compose-level, so it belongs between `compose` and the subcommand: `docker compose --file X up -d`, never `docker compose up --file X`.
func DefaultGenericPort ¶
func DefaultGenericPort(svc types.ServiceConfig) string
DefaultGenericPort is the container-internal port the generic template's port field prefills with: the first ports: target if the service publishes one, else 80 - the common case for a bare web image.
func DeleteService ¶ added in v0.5.0
DeleteService removes serviceName's entry from the services: mapping in fileName - AddServiceFragment's opposite number, minus a fragment to parse.
The removal goes through the same validate-by-reload every other writer in this file uses, plus an explicit dependents check ValidateComposeCandidate alone cannot cover - see ensureNoDependents.
func DockerComposePs ¶
Executes `docker compose ps` scoped to composeFile (see ComposeFileArgs) and returns the raw JSON output. Using `docker compose ps` (rather than `docker ps`) means each entry already carries the compose service name in its "Service" field, so callers never need to guess it from the container name.
The output shape depends on the Docker Compose version: newer releases emit a single JSON array, older ones emit NDJSON (one object per line). ParseContainers accepts both.
func DockerMemTotal ¶
DockerMemTotal runs `docker info --format '{{.MemTotal}}'` and returns the total memory in bytes.
func DockerSystemDf ¶
DockerSystemDf runs `docker system df --format json` and returns the raw NDJSON output.
func EditorCommand ¶
EditorCommand builds the command that opens path in the user's editor.
$VISUAL wins over $EDITOR by long-standing convention: $EDITOR may be a line editor for use on a dumb terminal, while $VISUAL is the full-screen one, and we are handing over a full terminal.
The value is split on whitespace rather than run through a shell, so EDITOR="code --wait" works. A shell would also make every character the user has in that variable executable at the moment we hand it the terminal, which buys nothing here.
func EnsureBackupStore ¶
EnsureBackupStore creates the sidecar backup folder next to the resolved compose file. It is idempotent: calling it again is a no-op. The folder is local to the stack directory, not in the config dir and not under ~/.local/share, so it moves with the project and stays discoverable.
func ExtractServiceFragment ¶
ExtractServiceFragment returns the YAML for one service, as a single-key mapping exactly as it appears in the compose file:
web:
image: nginx:alpine
ports:
- "8085:80"
The service name is kept as the top-level key for two reasons. It gives the user the context they would have in the real file, and it gives callers somewhere to put an explanatory header comment that cannot leak back in: comments above the key attach to the key node, and ApplyServiceFragment only ever takes the value.
func GetComposeFileName ¶
func GetComposeFileName(source ComposeSource) (winner string, candidates []string, err error)
GetComposeFileName resolves which compose file to use for source. It returns the winner plus every candidate that exists, in priority order (so the winner is candidates[0]), because the winner is the whole story only when it is the only one: with several present, the UI can say which others were in the running.
Paths are returned as they will be used: joined with source.Dir, so the result can go straight to docker, to the YAML writers and to the footer.
func IsValidServiceName ¶
IsValidServiceName reports whether s is a legal Compose service name: letters, digits, hyphen and underscore only. Shared by every "make a service" flow (servicefieldsstep, addservicemodal) so the rule can never drift between them - see D7 in docs/plans/image-search.md for why this function is shared while the UI step it validates for is deliberately not.
func ListComposeFiles ¶
ListComposeFiles returns the names of the YAML files in dir, sorted. It is the directory scan behind the Files page's file picker: any *.yaml or *.yml file is a candidate, because --file accepts any name, not just the four auto-detected ones - the picker is a way to choose, not a resolution order, so it is not limited to GetComposeFileName's canonical names.
func NextRestartPolicy ¶ added in v0.5.0
NextRestartPolicy returns the policy after current in restartPolicyCycle, wrapping around. "no" is treated the same as "": both are the cycle's unset state, since a service written by hand with restart: no means exactly what an absent key means. A value outside the cycle (hand-edited, or a future compose keyword this app does not know) resets to the first entry rather than guessing where it belongs.
func ParseContainers ¶
func ParseContainers(output string) ([]apptypes.DockerContainer, error)
ParseContainers turns `docker compose ps --format json` output into a slice of containers. Newer Docker Compose versions emit a single JSON array; older ones emit NDJSON (one object per line). Both are accepted so the app doesn't pin a minimum compose version.
func ReadConfigFileExt ¶
ReadConfigFileExt returns the project, the resolved .env path, and whether it was loaded. The .env path is resolved relative to the compose file's directory (compose-go semantics). It may exist but not be loaded if COMPOSE_DISABLE_ENV_FILE is set.
func RemoveGroupTag ¶
RemoveGroupTag strips groupName from every service in the compose file at fileName that carries it. A service's profiles key is removed entirely, rather than left as an empty list, once its last tag is gone.
func RenameGroupTag ¶
RenameGroupTag replaces every profiles entry equal to oldName with newName in the compose file at fileName. Other profiles and every other key are untouched. Nothing else in a compose file references a profile by name (unlike service names, which depends_on references - which is why service renames are refused), so a rename cannot leave dangling references. Returns an error when no service carries oldName, which catches the file having changed since the caller last synced it.
func ReplaceFileAtomically ¶
ReplaceFileAtomically writes contents to fileName by way of a temporary file that is renamed into place, creating the file if it doesn't exist.
Writing over the file directly truncates it first, so anything that fails after that point - a full disk, a crash, a killed process - leaves the user with a half-written or empty compose file and no copy of the original. Rename, by contrast, is atomic within a filesystem: a reader, including `docker compose` itself, sees either the old file or the complete new one.
Permissions of an existing file are carried over to the replacement.
func ReplaceFileAtomicallyWithMode ¶
ReplaceFileAtomicallyWithMode is like ReplaceFileAtomically but accepts an explicit mode for new files. The mode is applied before the write, not after the rename, so a mode like 0600 is never briefly world-readable.
If the file exists, its mode is preserved (resolving symlinks first). If the file is a symlink, it is resolved and the target is written through. If the symlink is dangling, an error is returned without creating a file.
func RestoreBackup ¶ added in v0.4.0
RestoreBackup writes the named .bak back over sourceFile. It goes through ReplaceFileAtomically, so the file being overwritten is snapshotted first and the restore is itself undoable: restoring compose.yaml@T2 leaves a new backup of the current compose.yaml (the one about to be replaced) as the next-newest entry.
The .bak name must belong to sourceFile's slug dir; an unknown or mismatched name is an error so a caller cannot accidentally write bytes from another source's folder over this one.
func RunDockerCompose ¶
func RunDockerCompose(action string, target string, isGroup bool, composeFile string, members []string) error
RunDockerCompose runs a `docker compose` action scoped either to a single service or to an explicit set of services (a group's members).
composeFile is the file the app resolved and is showing in its UI; it is passed to docker as --file so the command acts on the same file the panels describe. Empty means "let docker resolve it", which is only correct before a file is loaded - see ComposeFileArgs.
Remove uses `rm -fs` rather than `down`: `down` also tears down the project's shared network, which would affect services outside the selected set.
func SecretHint ¶
SecretHint reports whether a value under this env key should be masked by default when something other than the Env page renders it. Key name only — value-shape heuristics are deliberately not used.
It is a hint, not a guarantee: the Env page masks everything regardless, and this exists so the next panel to render arbitrary env values does the safe thing without its author having to think about it.
func SetGroupMembers ¶
SetGroupMembers reconciles the compose file so that exactly the named services carry groupName as a profile tag. Services in members that lack the tag get it added; services not in members that carry it get it removed. A service whose profiles key becomes empty is cleaned up the same way RemoveGroupTag does.
The reconciliation runs in a single read-modify-write pass rather than composing AddGroupTag and RemoveGroupTag, which would each open and close the file separately and leave a crash window with a half-applied edit.
func SetRestartPolicy ¶ added in v0.5.0
SetRestartPolicy writes policy into serviceName's restart: field in the compose file at fileName, through the same read-modify-write shape ApplyHealthcheck uses. An empty policy removes the key entirely rather than writing restart: "" - compose has no such value, and dropping the key is what "no policy" looks like in a file a human reads afterward.
func SnapshotFile ¶
SnapshotFile captures the pre-write state of fileName into the backup store, so a bad write can later be undone. It is called from inside the atomic write, before the file is replaced, so it only ever runs for writes the app has already committed to.
A brand-new file has nothing to snapshot, so it is a no-op that still succeeds. If the file's content matches the most recent entry already in the store, the snapshot is skipped too (nothing new to preserve).
If the backup cannot be taken, the error is returned and the write is refused: no write happens without a retained copy of what it replaces.
func StreamDockerLogs ¶
func StreamDockerLogs(target string, isGroup bool, composeFile string, members []string) (<-chan string, context.CancelFunc, error)
StreamDockerLogs starts `docker compose logs -f` for a single service or for the named members of a group, scoped to composeFile (see ComposeFileArgs), and streams each output line over the returned channel. The channel is closed when the process exits (or is cancelled). Call the returned CancelFunc to kill the process and stop the stream - this is the first long-lived subprocess in the app, so tearing it down is the caller's responsibility.
Unlike RunDockerCompose, which captures CombinedOutput() once and returns, this reads stdout+stderr incrementally on a goroutine so the TUI can render lines as they arrive.
func URLHost ¶
URLHost is the host part of every service URL the app builds, in order:
- config.URLHost - the user's explicit answer, wins always
- SSH_CONNECTION[2] - the address this SSH client used to get here, measured rather than guessed
- "localhost" - running locally, which is then correct
env is passed in (rather than reading os.Getenv directly) so this stays a table test; AppModel is the one real caller and reads the environment once, at startup, since the answer cannot change during a run.
func ValidateComposeCandidate ¶
ValidateComposeCandidate reports whether contents would load as a compose file, without touching whatever is currently on disk.
The candidate is written into dir rather than a system temp directory because compose resolves relative paths - build contexts, env_file: - from the compose file's own location, so validating anywhere else would reject files that are perfectly fine, and accept ones that aren't.
func WriteNewComposeFile ¶
WriteNewComposeFile creates a brand-new compose file at fileName with a top-level services mapping, optionally pre-seeded with one service. It refuses to overwrite an existing file: the caller is expected to have already shown a validation error in the modal in that case, so we surface os.ErrExist to make the failure mode explicit.
Types ¶
type BackupEntry ¶ added in v0.4.0
type BackupEntry struct {
// Source is "compose" for any source file whose basename is not
// ".env", and ".env" for the .env file. It is derived from the live
// file's basename, not from the slug folder, so a merged list can
// label each row by what it is.
Source string
// Name is the .bak filename, e.g. 20260811T091530.ab12cd34.bak.
Name string
// Timestamp is the UTC write time parsed from the filename prefix.
Timestamp time.Time
// SHA8 is the content hash (8 hex) parsed from the filename.
SHA8 string
// Path is the absolute path to the .bak, for restore.
Path string
}
BackupEntry describes one stored version of a source file.
func ListBackups ¶ added in v0.4.0
func ListBackups(sourceFile string) ([]BackupEntry, error)
ListBackups returns the stored versions of sourceFile, newest first. A source whose slug folder does not exist - never written, or brand-new - yields an empty, non-error slice, so a reader need not special-case a missing directory. The timestamp and SHA-8 come from the filename, which SnapshotFile wrote, so no file is opened to learn them.
type ComposeSource ¶
type ComposeSource struct {
// File is an exact path from --file. It skips resolution entirely: the
// user named the file, so there is nothing to pick between.
File string
// Dir is the directory from --dir to resolve in. Empty means the current
// directory.
Dir string
}
ComposeSource is where the app was told to look for a compose file: the zero value is "resolve one in the current directory", which is what it does when no flag is given.
File and Dir never both apply - main.go rejects the combination - so this is two spellings of one answer rather than a search path.
type DiskUsage ¶
type DiskUsage struct {
Type string // "Images", "Containers", "Local Volumes", "Build Cache"
TotalCount int
Active int
Size int64 // bytes
Reclaimable int64 // bytes
}
DiskUsage represents one row of `docker system df`.
func ParseSystemDf ¶
ParseSystemDf parses the NDJSON output of `docker system df --format json`. It accepts both a JSON array and NDJSON (one object per line).
type DockerState ¶
type DockerState int
DockerState is which of the five states a preflight probe found. See docs/plans/docker-preflight.md for the research behind the five.
const ( DockerOK DockerState = iota DockerNotInstalled DockerComposeMissing DockerDaemonUnreachable DockerPermissionDenied )
type DockerStatsContainer ¶
type DockerStatsContainer struct {
Container string `json:"Container"`
ID string `json:"ID"`
Name string `json:"Name"`
CPUPerc string `json:"CPUPerc"`
MemPerc string `json:"MemPerc"`
MemUsage string `json:"MemUsage"`
NetIO string `json:"NetIO"`
BlockIO string `json:"BlockIO"`
PIDs string `json:"PIDs"`
}
DockerStatsContainer holds the raw output of `docker stats --no-stream --format json`. Field names match the Docker CLI output exactly.
func DockerStats ¶
func DockerStats() ([]DockerStatsContainer, error)
DockerStats executes `docker stats --no-stream --format json` and returns the parsed stats for all running containers. The output is NDJSON (one JSON object per line).
type DockerStatus ¶
type DockerStatus struct {
State DockerState
EngineVersion string // "29.6.0", when known
ComposeVersion string // "5.1.4", when known
Endpoint string // DOCKER_HOST or the active context's endpoint
ComposeV1Found bool // docker-compose (hyphen) is on PATH
Raw string // the failing probe's output, for the modal's detail line
}
DockerStatus is the whole answer: the state, plus the facts worth reporting alongside it.
func DockerPreflight ¶
func DockerPreflight() DockerStatus
DockerPreflight runs the real probes and classifies them.
type EnvEditOp ¶
type EnvEditOp struct {
Type string // "set" (add or update key), "delete"
Key string // The variable name
Value string // For "set" operations
}
EnvEditOp describes a mutation to apply to the .env file.
type HealthcheckTemplate ¶
type HealthcheckTemplate struct {
Name string // shown in the picker
Matches []string // image substrings, e.g. {"postgres", "postgresql"}
Test []string // CMD/CMD-SHELL and its argument
Interval string
Timeout string
Retries uint64
StartPeriod string
// Generic asks the picker to collect the container-internal port before
// building the test - the one field this catalog has, deliberately kept
// to a single row rather than growing options per template.
Generic bool
}
HealthcheckTemplate is one row of the catalog: a probe the maintainers of a specific image ship, or (Generic) a best-effort HTTP probe for anything else. The catalog stays small and does not grow into a service directory (docs/plans/healthcheck-insertion.md, "The catalog stays at the 4+1 rows above") - every row is a correctness claim about an image, and a wrong one produces a container stuck at unhealthy forever.
func TemplatesFor ¶
func TemplatesFor(image string) []HealthcheckTemplate
TemplatesFor orders the catalog for a service: image-matched templates first (in catalog order), the generic fallback last. A service with no matching image-specific template still gets the generic one - it is never filtered out, only ever sorted to the end.
type HostInfo ¶
HostInfo is enough about the machine to pick a remediation family: the OS, and on Linux, the distro's own idea of what it is and what it resembles.
func DetectHost ¶
func DetectHost() HostInfo
DetectHost reads runtime.GOOS and, on Linux, /etc/os-release. A missing or unreadable file is not an error - it yields the generic family, same as any other unrecognised distro.
type Remedy ¶
type Remedy struct {
Summary string // "The Docker daemon is not running."
Steps []string // shell lines, printed verbatim, never executed
Note string // optional: the Snap/context/V1/rootless caveats
DocsURL string
}
Remedy is what the user should do about a DockerStatus on this machine: one sentence of what is wrong, and the exact command that fixes it - see D2 in docs/plans/docker-preflight.md for why Steps is printed, never run.
func RemedyFor ¶
func RemedyFor(status DockerStatus, host HostInfo) Remedy
RemedyFor returns what the user should do about status on host. See D7: the states the app can fix in one line - 2, 3 and 4 - get a command; state 1 gets one only where the distro's own package manager carries docker directly (fedora, arch, suse), because the debian family's real answer is Docker's own multi-step repo setup, which this app does not embed (D2) - so it links out instead, same as the generic family.
type ServiceURL ¶
ServiceURL is the address a service is reachable at, plus how the app worked it out and, when the guess is shaky, why.
func ResolveURL ¶
func ResolveURL(svc types.ServiceConfig, host string) (ServiceURL, bool)
ResolveURL works out the service's main URL. ok is false when the service publishes nothing and declares nothing - the caller omits the row entirely rather than rendering it empty. host is passed in, never read from the environment here, so this whole function is a table test - see URLHost for where SSH_CONNECTION is actually read.
Source Files
¶
- ApplyEnvEdit.go
- AtomicWrite.go
- BackupStore.go
- ComposeArgs.go
- ComposeName.go
- Deduplicate.go
- DockerCompose.go
- DockerComposePs.go
- DockerLogs.go
- DockerMemTotal.go
- DockerPreflight.go
- DockerStats.go
- DockerSystemDf.go
- Editor.go
- GetComposeFileName.go
- GroupTags.go
- HealthcheckTemplate.go
- HostInfo.go
- ListComposeFiles.go
- ParseContainers.go
- ReadConfigFile.go
- RestartPolicy.go
- SecretHint.go
- ServiceFragment.go
- ServiceURL.go