docker

package
v1.60.0 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package docker shows what the Docker daemon runs next to the projects. It keeps one connection for the whole cockpit: the container list is fetched once, then the daemon's event stream keeps the cache fresh, so a hundred projects cost one list call and not a compose invocation each. Containers are matched to a project through the compose labels, and the join key is the compose working directory, not the compose project name, because the name is a normalised folder name and two folders may share it.

A machine without a reachable daemon is a normal state, not an error: the cache answers empty, the surfaces leave the docker parts out, and the watcher quietly keeps trying.

Index

Constants

View Source
const ActionsSettingKey = "docker-compose-actions"

The compose buttons are configuration, not code. What used to be two wired in commands is a list somebody edits: every entry carries an icon, a label, the command line it wraps, how long it may take and whether it asks before it starts. The list is one JSON value in the flat settings store, and its order is the order of the buttons.

That key has three states, not two. Not set at all means the defaults below, which is also why nothing writes them at first start: a later version may improve the list, and a copy of today's one sitting in everybody's settings would freeze it forever. Set and empty means somebody took every button away, which is a real answer and stays one. Telling those two apart needs Lookup, Get answers "" for both.

View Source
const DefaultIcon = "command"

DefaultIcon is what an entry that names nothing we know gets. A row without a picture reads as a broken page, so there is no such thing as no icon.

View Source
const HostSettingKey = "docker-host"

HostSettingKey is the settings store key naming the docker host. An empty value means not set, the resolution then falls through to the environment, the docker context and the known socket paths.

View Source
const LinkRulesSettingKey = "docker-link-rules"

A container is reachable in two ways and only one of them is a published port. The other is a reverse proxy in front of it, which publishes nothing and routes by host name; what that host is stands in the container's own labels, because that is how the proxy learned it.

Which label, and how to read it, is configuration and not code. Every ecosystem writes that down differently, and a cockpit that grows a parser per convention grows forever, so this file knows exactly one thing: a label may carry the address a container answers on. A rule says which label, a regular expression says where the address sits in its value, and the default list below covers the common case so a normal setup configures nothing.

What is deliberately out of reach is a convention carried in an environment variable, nginx-proxy's VIRTUAL_HOST being the known one: the whole docker integration is one container list plus an event stream, and reading a container's environment costs an inspect per container. Labels come with the list, so a rule over labels costs nothing at all.

The key has three states like the compose commands next to it, and only Lookup tells them apart: not set means DefaultLinkRules, which is never written at first start so a later version may improve them, set means what is stored, and an empty list means somebody took every rule away and wants the published ports alone.

Variables

View Source
var IconNames = []string{"start", "stop", "restart", "build", "pull", "purge", "command"}

IconNames is the vocabulary an entry's icon comes from. They are our own words for what a command does, not the name of a glyph in whatever icon set the pages happen to use: which picture a word gets is the render layer's table alone, so the set can be swapped without touching a stored setting.

View Source
var LinkSchemes = []string{"", "http", "https"}

LinkSchemes is what a rule may pin its links to. The empty one comes first because it is the usual answer.

Functions

func CompileLogPattern added in v1.59.0

func CompileLogPattern(pattern string) (*regexp.Regexp, error)

CompileLogPattern compiles a --grep pattern the way the formatter matches it, as a case insensitive regular expression. The handlers that spawn a log shell compile it too, so a broken pattern is refused where it was typed instead of failing inside the spawned pipeline. An empty pattern is no pattern.

func ComposeFile

func ComposeFile(dir string) (string, bool)

ComposeFile answers the compose file a directory carries, the way the compose CLI finds it.

func ComposeLogsCommand

func ComposeLogsCommand(host, filter string) string

ComposeLogsCommand is the same for a whole stack: every service of the compose project in one stream, run from the stack's own directory, which is how compose knows which project it is about.

func EncodeActions

func EncodeActions(list []Action) string

EncodeActions writes the list back the way the store keeps it.

func EncodeLinkRules

func EncodeLinkRules(list []LinkRule) string

EncodeLinkRules writes the list back the way the store keeps it.

func ExecCommand

func ExecCommand(host, container string) string

ExecCommand is the one line a shell types to get into a container: docker exec with a tty, bash when the image has it, sh otherwise.

func FormatLogs added in v1.59.0

func FormatLogs(r io.Reader, w io.Writer, pattern string, context int) error

FormatLogs reads raw docker log lines from r and writes them formatted to w until the stream ends. Every output line is a single write, which is what keeps a followed log live when w is a pipe or a terminal.

func IsDefault

func IsDefault(list []Action) bool

IsDefault reports whether a list is exactly the default one. Saving that is the same thing as never having answered, so the caller stores nothing and lets the key stay absent, which is what keeps a later version free to improve the defaults for this install.

func IsDefaultLinkRules

func IsDefaultLinkRules(list []LinkRule) bool

IsDefaultLinkRules reports whether a list is exactly the default one. Saving that says nothing the absent key does not already say, so the caller stores nothing and leaves the key absent.

func LogsCommand

func LogsCommand(host, container, filter string) string

LogsCommand is the line a shell types to follow a container's output; when the container stops, the follow ends and the shell prompt is back. The stream runs through this binary's own log formatter, and a filter narrows it to the matching lines plus their context.

func NormalizeIcon

func NormalizeIcon(name string) string

NormalizeIcon answers the name to store for what somebody picked.

func Resolve

func Resolve(setting string) string

Resolve picks the docker host the cockpit talks to, in the order a docker CLI user expects: the cockpit's own docker-host setting when it is set, then DOCKER_HOST from the environment, then the endpoint of the current docker context, and finally the well known socket paths, first one that exists wins. An empty answer means no candidate was found, which is a normal state, not an error.

func SplitCommand

func SplitCommand(line string) ([]string, error)

SplitCommand splits a configured command line into argv the way a shell splits words, and no further: spaces separate, quotes group, a backslash takes the next character as it stands. Nothing is expanded, no variable, no pattern against the disk, no command inside another, because the line is never handed to a shell in the first place. What comes out of here reaches the program as arguments and can never become a command of its own.

func ValidateHost

func ValidateHost(host string) error

ValidateHost checks an address typed as the docker-host setting. It is stricter than NewClient on purpose: the same value travels to the docker CLI as DOCKER_HOST for every compose run and container shell, and the CLI reads unix:// and tcp:// but not http://, so an address only the API client could use would pass the form and then fail on every CLI surface.

Types

type Action

type Action struct {
	// ID names the entry in a request. It is stable across edits, so a page
	// rendered before a change still asks for the entry it showed.
	ID string `json:"id"`
	// Icon is one name out of IconNames, what the button and the menu row show.
	Icon string `json:"icon"`
	// Label is the words next to it.
	Label string `json:"label"`
	// Command is the command line, argv separated by spaces, quotes allowed.
	Command string `json:"command"`
	// Timeout is a Go duration ("10m"), enforced by the hold process.
	Timeout string `json:"timeout"`
	// Confirm makes the surfaces ask before the run starts.
	Confirm bool `json:"confirm,omitempty"`
}

Action is one configured compose command. Command is a command line, never a shell line: it is split into argv here and handed to the program directly, see SplitCommand.

func ActionByID

func ActionByID(list []Action, id string) (Action, bool)

ActionByID picks the entry a request names.

func Actions

func Actions(raw string, set bool) []Action

Actions turns what the settings store answered into the list of buttons. Pass both return values of Lookup: not set is the default list, set is what is stored, empty list included. Something stored that cannot be read at all is treated like never set, so a damaged value leaves a working cockpit.

func DecodeActions

func DecodeActions(raw string) ([]Action, error)

DecodeActions reads the stored JSON. An empty value is an empty list, not an error: that is what removing every entry leaves behind.

func DefaultActions

func DefaultActions() []Action

DefaultActions is the list an install that never touched the setting has: the two runs the cockpit always had, a build, and the destructive down that takes the volumes with it, which is the one that asks first.

func PurgeAction

func PurgeAction(composeProject string) Action

PurgeAction is the fixed down a project deletion runs, and the one command here that no setting reaches: a deletion takes the volumes with it, and it names the compose project explicitly, because compose otherwise derives the name from the directory and would quietly clear nothing while reporting success.

func (Action) Duration

func (a Action) Duration() time.Duration

Duration is how long the entry may run.

func (Action) Resolve

func (a Action) Resolve(dir, root string) ([]string, time.Duration, error)

Resolve is the one place a configured entry becomes a run: the argv the program is started with and the time it may take. dir is the stack directory the command runs in, root the project it belongs to.

A program named relatively (./deploy.sh, ../ops/up.sh) is looked up from the stack directory upwards to the project root and handed on absolute, because detach.Start resolves the program before it sets the working directory: a relative path would be answered against the cockpit's own directory, which is somewhere else entirely.

type Client

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

Client is a minimal Docker Engine API client over one host, enough for a container list, the lifecycle actions, logs and the event stream. It keeps no request timeout of its own, the event stream is a request that never ends, so every call runs under the caller's context.

func NewClient

func NewClient(host string) (*Client, error)

NewClient prepares a client for host, which is a docker style endpoint: unix:// for a socket, tcp:// or http:// for a plain TCP daemon, https:// for a TLS one.

func (*Client) Containers

func (c *Client) Containers(ctx context.Context) ([]Container, error)

Containers lists every container, running or not, reduced to what the cache carries.

func (*Client) Events

func (c *Client) Events(ctx context.Context) (io.ReadCloser, error)

Events opens the daemon's event stream, filtered to container events. The caller owns the body and ends the stream by cancelling the context.

func (*Client) Host

func (c *Client) Host() string

Host answers the endpoint the client talks to.

func (*Client) Ping

func (c *Client) Ping(ctx context.Context) error

Ping asks the daemon whether it is there at all.

func (*Client) Restart

func (c *Client) Restart(ctx context.Context, id string) error

Restart restarts a container.

func (*Client) Start

func (c *Client) Start(ctx context.Context, id string) error

Start starts a container. A container already running answers 304, which the request layer treats as success.

func (*Client) Stop

func (c *Client) Stop(ctx context.Context, id string) error

Stop stops a container, with the daemon's default grace period.

type ComposeOptions

type ComposeOptions struct {
	// Dir is where the command runs, Label the name the run reports under.
	Dir   string
	Label string
	// Root is the project the stack belongs to, the ceiling a relatively named
	// program is searched up to.
	Root string
	// Action is the configured entry to run.
	Action Action
	// Quiet keeps a run that went through silent. The project deletion brings
	// the stacks down as a step of itself, where the row disappearing is the
	// word to the user and only a failure has something left to say.
	Quiet bool
}

ComposeOptions describe one compose run.

type ComposeRecord

type ComposeRecord struct {
	ID string `json:"id"`
	// Dir is the compose directory, which is also what a second run in the
	// same place is refused against.
	Dir string `json:"dir"`
	// Label is the name the run reports under, the project it belongs to.
	Label string `json:"label"`
	// Action is the label of the configured entry that started it, the word
	// every surface says about the run.
	Action string `json:"action"`
	// Argv is what really ran, kept so the output view can show the line.
	Argv []string `json:"argv"`
	// Timeout is what the hold process enforces, kept so a run that hit it can
	// say after how long.
	Timeout time.Duration `json:"timeout"`
	// Quiet keeps a run that went through silent, see ComposeOptions.
	Quiet bool `json:"quiet,omitempty"`
	// PID is the hold process. Whether the run is still going is decided by
	// its lock file alone.
	PID       int       `json:"pid"`
	StartedAt time.Time `json:"startedAt"`
	// Cancelled marks a run somebody called off, so the end reads as that and
	// not as a run that ended without a result.
	Cancelled bool `json:"cancelled,omitempty"`
	// Finished and everything below it are written once, when the run is over
	// and has been reported. An entry carrying it is never taken up again.
	Finished bool      `json:"finished,omitempty"`
	EndedAt  time.Time `json:"endedAt,omitempty"`
	// Exited says the run wrote an exit code at all; Exit is that code. A run
	// without one did not end by its own decision.
	Exited  bool   `json:"exited,omitempty"`
	Exit    int    `json:"exit,omitempty"`
	Failure string `json:"failure,omitempty"`
}

ComposeRecord is one compose run as it exists outside this process.

type ComposeRun

type ComposeRun struct {
	ID     string
	Dir    string
	Label  string
	Action string
	Quiet  bool
	Failed bool
}

ComposeRun is a finished compose run: which project it reported under, what it ran, and whether it failed.

type Container

type Container struct {
	ID      string
	Name    string
	Image   string
	State   string
	Status  string
	Health  string
	Project string
	Service string
	// WorkingDir is the compose working directory label, the join key to a
	// cockpit project. Empty for containers compose did not start.
	WorkingDir string
	Ports      []Port
	// Labels is what the daemon reported about the container, kept whole
	// because a reverse proxy in front of it writes its address in here and
	// which label that is, is configuration (see links.go). It rides along
	// with the list, so reading it costs no call of its own.
	Labels  map[string]string
	Created int64
}

Container is one entry of the cached list, reduced to what the surfaces show and the join needs.

func (Container) DisplayName

func (c Container) DisplayName() string

DisplayName is what the chip shows: the compose service, the name a person gave the thing, with the container name as the fallback.

func (Container) PortsLabel

func (c Container) PortsLabel() string

PortsLabel joins the published ports for the status line.

func (Container) Running

func (c Container) Running() bool

Running reports whether the container is up, the one distinction the status color makes.

func (Container) Unwell

func (c Container) Unwell() bool

Unwell reports the states worth an error color: a failing healthcheck or a daemon that gave up on the container.

type Link struct {
	Scheme string
	Host   string
	Port   int
	Path   string
}

Link is one address a browser can open for a container. The two ways a container is reachable share the shape:

A published port is the address this page was reached on, at that port: Host is empty, which the client reads as its own location, and the scheme follows the container side of the mapping, a service listening on 443 speaks TLS.

A route a link rule read out of the labels is a host of its own, with the path it is routed under when it has one, and usually no port at all: the proxy answers on the scheme default. Its Scheme is whatever the rule pins, and empty, the usual answer, means the scheme of the page the link is opened from. Only the browser knows that one: what terminates TLS may sit above the proxy, where neither a label nor this server can see it.

func (Link) Address

func (l Link) Address() string

Address is the link as a person reads it, which is what the rule preview on the settings page shows: a route is its host and path, a published port is the port alone, and a scheme only appears where the link pins one.

type LinkMatcher

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

LinkMatcher is the configured rules ready to be applied, compiled once and then asked per container: a page has many containers and a rule is a regular expression. Rules that do not validate are left out here, so nothing downstream has to think about them.

func NewLinkMatcher

func NewLinkMatcher(rules []LinkRule) LinkMatcher

NewLinkMatcher compiles what is configured, in the order it is configured.

func (m LinkMatcher) Links(c Container) []Link

Links answers every address a browser can open for one container: the routes its labels declare first, because where a route exists it is the address a person wants, then the published tcp ports, which are docker's own truth and never a rule.

func (LinkMatcher) Routes

func (m LinkMatcher) Routes(labels map[string]string) []Link

Routes answers the addresses the rules read out of one container's labels, deduplicated and in a stable order. It is what the container chips, the menus and the preview on the settings page all go through, so what a rule promises there is what it does everywhere.

type LinkRule

type LinkRule struct {
	// Label is the label to read, with * standing for any run of characters,
	// so one rule covers a whole family of them (a router per host name).
	// Label keys are matched case insensitively, the way the tools that write
	// them treat their own spelling.
	Label string `json:"label"`
	// Pattern is a regular expression over the label's value with named
	// captures: host is the address, path and port are optional. Every match
	// in the value counts, not only the first, and the host capture may name
	// several addresses separated by commas. Empty means the whole value is
	// the host.
	Pattern string `json:"pattern,omitempty"`
	// Scheme pins the link to http or https. Empty is the useful answer
	// whenever the proxy in front of the app is the proxy in front of the
	// cockpit: the link then carries no scheme and is opened under the one
	// the page itself was reached over. What terminates TLS may well sit
	// above the proxy, where no label of the routed container can see it.
	Scheme string `json:"scheme,omitempty"`
	// Unless is a key=value label that switches this rule off for a
	// container, which is how an opt-out is expressed without this code
	// knowing any proxy's name for it.
	Unless string `json:"unless,omitempty"`
}

LinkRule is one convention for reading addresses out of a container's labels.

func DecodeLinkRules

func DecodeLinkRules(raw string) ([]LinkRule, error)

DecodeLinkRules reads the stored JSON. An empty value is an empty list, not an error: that is what removing every rule leaves behind.

func DefaultLinkRules

func DefaultLinkRules() []LinkRule

DefaultLinkRules is the list an install that never touched the setting has. It carries the one convention wide enough to be worth defaulting to, the docker labels of traefik: a router per name, its rule naming the hosts it answers for, and traefik.enable=false as the opt-out. The pattern reads every Host(...) of a rule and, when one directly follows, the Path or PathPrefix the host is pinned to, so a container routed under a prefix gets the address that reaches it.

A second entry belongs here only if it is as safe as this one. A rule that guesses wrong sends somebody to an address that does not exist, which is worse than the cockpit offering no link at all.

func LinkRules

func LinkRules(raw string, set bool) []LinkRule

LinkRules turns what the settings store answered into the rules in force. Pass both return values of Lookup. Something stored that cannot be read at all is treated like never set, so a damaged value leaves a working cockpit.

func (LinkRule) Validate

func (r LinkRule) Validate() error

Validate answers what makes a rule unusable, which is what the row it is edited in says under the field. A rule that does not validate is also the rule the matcher skips, so a page never breaks on one.

type Port

type Port struct {
	Public  int
	Private int
	// Proto is empty for tcp, the unspoken default everywhere.
	Proto string
}

Port is one published port mapping.

func (Port) Label

func (p Port) Label() string

Label writes the mapping the way docker ps does, public first.

type RunView

type RunView struct {
	ID        string
	Dir       string
	Project   string
	Action    string
	Command   string
	Running   bool
	StartedAt time.Time
	EndedAt   time.Time
	Exited    bool
	Exit      int
	Cancelled bool
	Failure   string
}

RunView is one compose run as a surface shows it: what it is, whether it is still going, and how it ended.

type Service

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

Service keeps the one docker connection of the whole cockpit: it lists the containers once, then follows the daemon's event stream and refreshes the cached list when something moved. Every surface reads the cache, nothing asks the daemon per request or per project.

func NewService

func NewService(stateDir string, hostSetting func() string) *Service

NewService prepares the watcher. hostSetting reads the docker-host setting on every round, so a changed setting reaches a running watcher; stateDir is where the compose runs are registered, so they can be picked up again after a restart.

func (*Service) AwaitCompose

func (s *Service) AwaitCompose(id string)

AwaitCompose blocks until the named run is over. A run this process knows nothing about is over as far as the caller is concerned, so it returns at once: the project deletion waits this way, and a deletion whose wait was cut short by a restart has no goroutine left to wait in anyway.

func (*Service) CLI

func (s *Service) CLI() bool

CLI reports whether the docker CLI is on the PATH, which compose and the container shells need; the list, the actions and the logs go through the API and do not.

func (*Service) CancelCompose

func (s *Service) CancelCompose(id string) error

CancelCompose ends a run that is still going. It goes at the hold process, never at this server: the run is detached, the server that asked for it may be gone, and the one thing that reaches it either way is its process group. The cancel is written down first, so the end reads as called off rather than as a run that stopped without saying why.

func (*Service) Client

func (s *Service) Client() (*Client, error)

Client answers a client for the host the watcher is connected to. While no daemon is reachable it answers the error the action handlers surface.

func (*Service) ComposeBusy

func (s *Service) ComposeBusy(dir string) bool

ComposeBusy reports whether a compose run is under way in dir.

func (*Service) ComposeBusyUnder

func (s *Service) ComposeBusyUnder(dir string) bool

ComposeBusyUnder reports whether a compose run is under way in dir or anywhere below it. A project deletion asks this about the whole project: a run in a subdirectory has the same right not to have the ground pulled from under it, and a run that is past its last container still holds its claim while it tears the rest of the stack down, which is precisely the moment the container list has stopped naming it.

func (*Service) ComposeDeadline

func (s *Service) ComposeDeadline(dir string) (deadline time.Time, ok bool)

ComposeDeadline answers the latest moment the unfinished runs in dir or below it can still end on their own. Every run carries a timeout the hold process enforces, so its start plus that timeout plus a grace is as late as it gets, and a run still going past that is stuck. ok is false while nothing runs there, which is what lets a waiter tell a missing run from one that has all that time left.

func (*Service) ComposeRunByID

func (s *Service) ComposeRunByID(id string) (RunView, bool)

ComposeRunByID answers one run for a surface that shows it.

func (*Service) ComposeRunOutput

func (s *Service) ComposeRunOutput(id string) string

ComposeRunOutput reads what a run has written so far, the end of it when it grew past what anybody reads. It answers while the run goes and after it ended, which is the whole point of keeping the file.

func (*Service) ComposeRunsForDir

func (s *Service) ComposeRunsForDir(dir string) []RunView

ComposeRunsForDir answers the runs of one stack directory, newest first, the running one included.

func (*Service) Kick

func (s *Service) Kick()

Kick makes the watcher drop its connection and start over, which re-reads the setting. The settings handler calls it after a save.

func (*Service) LastComposeRun

func (s *Service) LastComposeRun(project string) (RunView, bool)

LastComposeRun answers the newest finished run of one project, which is what a notification about that project is about. It is asked per project and not as one global "the last run", because two projects can finish in the same moment and each one's news has to name its own run.

func (*Service) OnChange

func (s *Service) OnChange(fn func())

OnChange registers the one callback fired after the cached state moved, the web layer publishes its docker event from it. Set it before Run.

func (*Service) OnComposeDone

func (s *Service) OnComposeDone(fn func(run ComposeRun, err error, output string))

OnComposeDone registers the one callback every finished run reports through, the ones this process started and the ones it found already running. Set it before Recover.

func (*Service) Recover

func (s *Service) Recover()

Recover picks up the compose runs of an earlier server. What is still going gets its directory claimed and its busy mark back and is waited out; what finished while nobody was looking reports now, which is the notification the restart would otherwise have swallowed. A run that is over and was reported stays where it is, its output is what somebody still reads. Run it after OnComposeDone is set and before anything can start a run of its own.

func (*Service) Run

func (s *Service) Run(ctx context.Context)

Run is the watcher loop. It never returns before ctx ends; a machine without a daemon just cycles quietly through the retry pause.

func (*Service) RunCompose

func (s *Service) RunCompose(opts ComposeOptions) (string, error)

RunCompose starts one configured action in a directory and returns the id of the run once it is under way. The run is detached (see internal/detach): it lives on when this server goes away, its output goes into a file of its own, and its lock is what says it is still going. The event stream shows the containers move while it runs; the word at the end reaches OnComposeDone. It refuses a directory that is already running one, a command that cannot be read, a missing CLI, and a cockpit without a reachable daemon.

func (*Service) State

func (s *Service) State() State

State answers the current reading of the cache. Nil-receiver-safe like the editor intelligence service: web tests build a Server without the daemon wiring, and for them the daemon simply is not there.

type Stack

type Stack struct {
	Dir   string
	Label string
	// Project is the compose project name the daemon reports for the
	// containers of this directory, empty while none runs. It is what a
	// command has to name explicitly to reach exactly this stack: compose
	// otherwise derives the name from the directory it stands in.
	Project string
	Running int
	Total   int
}

Stack is one compose control point of a project: a directory compose runs in, with what currently runs out of it. Dir carries the project relative label ("" for the project root) next to the absolute path.

type State

type State struct {
	Available  bool
	Host       string
	Containers []Container
}

State is one reading of the cache: whether a daemon answers, which host it is, and what it runs.

func (State) ForDir

func (s State) ForDir(dir string) []Container

ForDir answers the containers whose compose working directory lies in dir, the per project view of the cache and the one list every surface renders, so the order below is the order the chips, the editor's grid and the menus all stand in. The comparison also tries the resolved path, because compose records the path the user stood in, which may reach the same directory through a symlink.

func (State) StacksForDir

func (s State) StacksForDir(dir string) []Stack

StacksForDir answers the compose control points inside dir: every distinct compose working directory of the project's containers, plus the project root while it carries a compose file of its own. A project without any of that answers none, which is the quiet everywhere rule.

Jump to

Keyboard shortcuts

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