Deploy a fresh Stalwart and ihasmail, linked, in one command
deploy stands up Stalwart 0.16, ihasmail and (for a mail host) Caddy as a compose project: completes Stalwart's bootstrap over x:Bootstrap, links ihasmail over the private network, requests certificates for both Caddy (TLS-ALPN-01) and Stalwart (HTTP-01 through Caddy), makes the auto-ban safe behind the proxy, and proves the link by signing in through the webmail. --local gives a loopback-only pair. certs retries Stalwart's certificate; destroy removes a deployment. e2e/public.sh runs the whole mail-host path against Pebble with no internet involved.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// Package docker drives the docker CLI. The CLI rather than the Engine API,
|
||||
// because compose is the thing being driven and the CLI is how it ships: the
|
||||
// deployment the tool leaves behind is one an operator manages with the same
|
||||
// commands.
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Output runs docker with args and returns its standard output.
|
||||
func Output(ctx context.Context, args ...string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, "docker", args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
if msg == "" {
|
||||
msg = err.Error()
|
||||
}
|
||||
return "", fmt.Errorf("docker %s: %s", strings.Join(args, " "), msg)
|
||||
}
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
// Versions returns the engine and compose versions, which is also the check
|
||||
// that both are installed and this user may use them.
|
||||
func Versions(ctx context.Context) (engine, compose string, err error) {
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
return "", "", errors.New("docker is not installed, or not on PATH")
|
||||
}
|
||||
if engine, err = Output(ctx, "version", "--format", "{{.Server.Version}}"); err != nil {
|
||||
return "", "", fmt.Errorf("cannot talk to the Docker daemon -- is it running, and may this user use it? (%w)", err)
|
||||
}
|
||||
if compose, err = Output(ctx, "compose", "version", "--short"); err != nil {
|
||||
return engine, "", fmt.Errorf("the docker compose plugin is not installed (%w)", err)
|
||||
}
|
||||
return engine, compose, nil
|
||||
}
|
||||
|
||||
// ProjectLeftovers lists containers, volumes and networks already labelled
|
||||
// with a compose project name. Any at all means an earlier run of the same
|
||||
// project, and its volumes would hand a "fresh" deployment an old server.
|
||||
func ProjectLeftovers(ctx context.Context, project string) ([]string, error) {
|
||||
filter := "label=com.docker.compose.project=" + project
|
||||
var found []string
|
||||
for _, kind := range []struct{ name, format string }{
|
||||
{"container", "{{.Names}}"},
|
||||
{"volume", "{{.Name}}"},
|
||||
{"network", "{{.Name}}"},
|
||||
} {
|
||||
args := []string{kind.name, "ls", "--filter", filter, "--format", kind.format}
|
||||
if kind.name == "container" {
|
||||
args = []string{"ps", "-a", "--filter", filter, "--format", kind.format}
|
||||
}
|
||||
out, err := Output(ctx, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, line := range strings.Fields(out) {
|
||||
found = append(found, kind.name+" "+line)
|
||||
}
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
|
||||
// Compose runs docker compose against one deployment directory.
|
||||
type Compose struct {
|
||||
Dir string
|
||||
Files []string // extra -f files after compose.yaml, e.g. the bootstrap override
|
||||
Env []string // added to the environment compose interpolates from
|
||||
Out io.Writer
|
||||
}
|
||||
|
||||
// Run runs a compose command with its output passed through: pulling images
|
||||
// takes long enough that silence would look like a hang. Everything but a pull
|
||||
// is quiet, because without a terminal compose prints each container's every
|
||||
// state change twice and the tool already says what step it is on.
|
||||
func (c Compose) Run(ctx context.Context, args ...string) error {
|
||||
full := []string{"compose", "--project-directory", c.Dir, "-f", c.Dir + "/compose.yaml"}
|
||||
if len(args) > 0 && args[0] != "pull" {
|
||||
full = append(full, "--progress", "quiet")
|
||||
}
|
||||
for _, f := range c.Files {
|
||||
full = append(full, "-f", f)
|
||||
}
|
||||
full = append(full, args...)
|
||||
cmd := exec.CommandContext(ctx, "docker", full...)
|
||||
cmd.Env = append(os.Environ(), c.Env...)
|
||||
cmd.Stdout, cmd.Stderr = c.Out, c.Out
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("docker compose %s: %w", strings.Join(args, " "), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Running reports whether a service has a running container.
|
||||
func (c Compose) Running(ctx context.Context, service string) bool {
|
||||
out, err := Output(ctx, "compose", "--project-directory", c.Dir, "-f", c.Dir+"/compose.yaml",
|
||||
"ps", "--status", "running", "--services")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, s := range strings.Fields(out) {
|
||||
if s == service {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Logs returns the last lines of one service's log, for a failure report.
|
||||
func (c Compose) Logs(ctx context.Context, service string, lines int) string {
|
||||
out, err := Output(ctx, "compose", "--project-directory", c.Dir, "-f", c.Dir+"/compose.yaml",
|
||||
"logs", "--no-color", "--tail", fmt.Sprint(lines), service)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SystemCABundle reads the CA bundle out of an image, so a private CA can be
|
||||
// added to the roots the image already trusts rather than replacing them.
|
||||
func SystemCABundle(ctx context.Context, image string) ([]byte, error) {
|
||||
out, err := Output(ctx, "run", "--rm", "--entrypoint", "cat", image, "/etc/ssl/certs/ca-certificates.crt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(out + "\n"), nil
|
||||
}
|
||||
Reference in New Issue
Block a user