Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION}" -o /bin/dr ./cmd/docker-release/

FROM alpine:3.21
FROM alpine:3.21 AS runtime
RUN apk add --no-cache ca-certificates
COPY --from=builder /bin/dr /usr/local/bin/dr
LABEL org.opencontainers.image.title="docker-release"
Expand Down
44 changes: 37 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
VERSION ?= $(shell v=$$(git tag --points-at HEAD 2>/dev/null | head -1); echo $${v:-dev})
IMAGE ?= malico/docker-release
NGINX_IMAGE ?= $(IMAGE)-nginx

# Bump type passed as a goal: make publish major|minor|fix
BUMP := $(filter major minor fix,$(MAKECMDGOALS))

.PHONY: dev dev-remove test build publish tag buildx-builder \
.PHONY: dev dev-remove test build build-regular build-nginx publish tag buildx-builder \
major minor fix print-version \
up-nginx up-angie up-traefik up-nginx-proxy up-caddy up-haproxy \
down-nginx down-angie down-traefik down-nginx-proxy down-caddy down-haproxy
up-nginx up-nginx-bundled up-angie up-traefik up-nginx-proxy up-caddy up-haproxy \
down-nginx down-nginx-bundled down-angie down-traefik down-nginx-proxy down-caddy down-haproxy

build: buildx-builder
docker buildx build \
--builder docker-release-builder \
--platform linux/amd64,linux/arm64 \
build: build-regular build-nginx

build-regular:
docker build \
--build-arg VERSION=$(VERSION) \
-t $(IMAGE):$(VERSION) \
-t $(IMAGE):latest \
.

build-nginx:
docker build \
-f dockerfiles/nginx.Dockerfile \
--build-arg BASE_IMAGE=$(IMAGE):$(VERSION) \
--build-arg VERSION=$(VERSION) \
-t $(NGINX_IMAGE):$(VERSION) \
-t $(NGINX_IMAGE):latest \
.

tag:
@test "$(VERSION)" != "dev" || (echo "ERROR: set VERSION=x.y.z"; exit 1)
@git fetch --tags origin 2>/dev/null; \
Expand Down Expand Up @@ -76,11 +86,23 @@ else
docker buildx build \
--builder docker-release-builder \
--platform linux/amd64,linux/arm64 \
--target runtime \
--build-arg VERSION=$(VERSION) \
-t $(IMAGE):$(VERSION) \
-t $(IMAGE):latest \
$${major:+-t $(IMAGE):$$major} \
--push \
. && \
docker buildx build \
--builder docker-release-builder \
--platform linux/amd64,linux/arm64 \
-f dockerfiles/nginx.Dockerfile \
--build-arg BASE_IMAGE=$(IMAGE):$(VERSION) \
--build-arg VERSION=$(VERSION) \
-t $(NGINX_IMAGE):$(VERSION) \
-t $(NGINX_IMAGE):latest \
$${major:+-t $(NGINX_IMAGE):$$major} \
--push \
. && $(MAKE) tag
endif

Expand All @@ -101,10 +123,15 @@ dev-remove:

test:
go test ./...
go test -tags bundled_nginx ./cmd/docker-release

up-nginx:
docker compose -f tests/nginx/docker-compose.yml up --build

up-nginx-bundled:
$(MAKE) build-regular IMAGE=docker-release VERSION=local
docker compose -f tests/nginx-bundled/docker-compose.yml up --build

up-angie:
docker compose -f tests/angie/docker-compose.yml up --build

Expand All @@ -123,6 +150,9 @@ up-haproxy:
down-nginx:
docker compose -f tests/nginx/docker-compose.yml down -v

down-nginx-bundled:
docker compose -f tests/nginx-bundled/docker-compose.yml down -v

down-angie:
docker compose -f tests/angie/docker-compose.yml down -v

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ Zero-downtime deploys for Docker Compose. Starts new containers, waits for healt

Your proxy still serves all traffic. `docker-release` only manages the container lifecycle and proxy config.

## Images

| Image | Use |
|---|---|
| `malico/docker-release` | controller-only sidecar |
| `malico/docker-release-nginx` | draft bundled Nginx + controller |

## Pick Your Proxy

| Proxy | Guide |
Expand Down
187 changes: 187 additions & 0 deletions cmd/docker-release/bundled.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//go:build bundled_nginx

package main

import (
"context"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"

"github.com/malico/docker-release/internal/health"
)

type bundledProxy struct {
name string
args []string
}

func bundledProxyFromEnv() (*bundledProxy, error) {
v := strings.ToLower(strings.TrimSpace(os.Getenv("DR_BUNDLED_PROXY")))
switch v {
case "", "none":
return nil, nil
case "nginx":
return &bundledProxy{name: "nginx", args: []string{"nginx", "-g", "daemon off;"}}, nil
default:
return nil, fmt.Errorf("unknown DR_BUNDLED_PROXY %q", os.Getenv("DR_BUNDLED_PROXY"))
}
}

func runWithBundledProxy(ctx context.Context, proxy *bundledProxy, run func(context.Context) error) error {
if proxy == nil {
return run(ctx)
}

_ = health.ClearReady()
_ = clearBundledProxyStarted(proxy)
if err := prepareBundledProxyConfig(proxy); err != nil {
return err
}

runCtx, cancel := context.WithCancel(ctx)
defer cancel()

controllerErr := make(chan error, 1)
go func() {
controllerErr <- run(runCtx)
}()

if err := waitForInitialConfigs(ctx, controllerErr); err != nil {
cancel()
if ctx.Err() != nil {
return nil
}
return err
}

cmd := exec.Command(proxy.args[0], proxy.args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
cancel()
return fmt.Errorf("starting bundled %s: %w", proxy.name, err)
}
if err := markBundledProxyStarted(proxy); err != nil {
cancel()
stopBundledProxy(cmd, proxy, nil)
return err
}
slog.Info("started bundled proxy", "component", "main", "proxy", proxy.name)

proxyErr := make(chan error, 1)
go func() {
proxyErr <- cmd.Wait()
}()

select {
case err := <-controllerErr:
cancel()
stopBundledProxy(cmd, proxy, proxyErr)
return err
case err := <-proxyErr:
cancel()
<-controllerErr
if err != nil {
return fmt.Errorf("bundled %s exited: %w", proxy.name, err)
}
return fmt.Errorf("bundled %s exited", proxy.name)
case <-ctx.Done():
cancel()
stopBundledProxy(cmd, proxy, proxyErr)
if err := <-controllerErr; err != nil {
return err
}
return nil
}
}

func prepareBundledProxyConfig(proxy *bundledProxy) error {
switch proxy.name {
case "nginx":
return prepareBundledNginxConfig()
default:
return nil
}
}

func bundledProxyStartedPath(proxy *bundledProxy) string {
return filepath.Join("/run/docker-release", proxy.name+".started")
}

func clearBundledProxyStarted(proxy *bundledProxy) error {
err := os.Remove(bundledProxyStartedPath(proxy))
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}

func markBundledProxyStarted(proxy *bundledProxy) error {
path := bundledProxyStartedPath(proxy)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
return os.WriteFile(path, nil, 0o644)
}

func waitForInitialConfigs(ctx context.Context, controllerErr <-chan error) error {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()

for {
if health.IsReady() {
return nil
}

select {
case err := <-controllerErr:
if err != nil {
return err
}
return fmt.Errorf("controller exited before bundled proxy started")
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}

func stopBundledProxy(cmd *exec.Cmd, proxy *bundledProxy, proxyErr <-chan error) {
if cmd.Process == nil {
return
}

if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
slog.Warn("could not stop bundled proxy", "component", "main", "proxy", proxy.name, "err", err)
return
}

if proxyErr == nil {
done := make(chan struct{})
go func() {
_ = cmd.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
_ = cmd.Process.Kill()
<-done
}
return
}

select {
case <-proxyErr:
return
case <-time.After(5 * time.Second):
_ = cmd.Process.Kill()
<-proxyErr
}
}
26 changes: 26 additions & 0 deletions cmd/docker-release/bundled_disabled.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//go:build !bundled_nginx

package main

import (
"context"
"fmt"
"os"
"strings"
)

type bundledProxy struct{}

func bundledProxyFromEnv() (*bundledProxy, error) {
v := strings.ToLower(strings.TrimSpace(os.Getenv("DR_BUNDLED_PROXY")))
switch v {
case "", "none":
return nil, nil
default:
return nil, fmt.Errorf("DR_BUNDLED_PROXY=%s requires a bundled dr build", v)
}
}

func runWithBundledProxy(ctx context.Context, _ *bundledProxy, run func(context.Context) error) error {
return run(ctx)
}
Loading