chore: fix build dependencies and frontend config
Migrate Docker SDK from github.com/docker/docker (+incompatible) to github.com/moby/moby/client v0.3.0 + moby/moby/api v1.54.0 (proper Go modules). Adapt all container/image/network operations to the new moby API (Filters, ContainerListOptions, PullResponse, InspectResult, etc.). Add AsyncTriggerDeploy runDeploy method. Fix SvelteKit build: disable prerender, set strict=false for SPA, bump vite-plugin-svelte to v5 for vite 6 compat. Add .dockerignore to exclude .git, node_modules, plans.
This commit is contained in:
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/alexei/docker-watcher/internal/notify"
|
||||
"github.com/alexei/docker-watcher/internal/npm"
|
||||
"github.com/alexei/docker-watcher/internal/store"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/moby/moby/api/types/mount"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -118,6 +118,79 @@ func (d *Deployer) AsyncTriggerDeploy(ctx context.Context, projectID, stageID, i
|
||||
return deploy.ID, nil
|
||||
}
|
||||
|
||||
// runDeploy is the internal deploy pipeline used by AsyncTriggerDeploy.
|
||||
// It assumes the deploy record already exists and project/stage are validated.
|
||||
func (d *Deployer) runDeploy(ctx context.Context, project store.Project, stage store.Stage, deployID string, imageTag string) error {
|
||||
settings, err := d.store.GetSettings()
|
||||
if err != nil {
|
||||
if updateErr := d.store.UpdateDeployStatus(deployID, "failed", err.Error()); updateErr != nil {
|
||||
slog.Warn("update deploy status", "error", updateErr)
|
||||
}
|
||||
return fmt.Errorf("get settings: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("starting deploy",
|
||||
"deploy_id", deployID,
|
||||
"project", project.Name,
|
||||
"stage", stage.Name,
|
||||
"tag", imageTag,
|
||||
)
|
||||
d.logDeploy(deployID, fmt.Sprintf("Starting deploy of %s:%s for project %s, stage %s", project.Image, imageTag, project.Name, stage.Name), "info")
|
||||
|
||||
// Enforce max_instances before deploying.
|
||||
if err := d.enforceMaxInstances(ctx, stage, deployID, settings); err != nil {
|
||||
d.logDeploy(deployID, fmt.Sprintf("Failed to enforce max instances: %v", err), "error")
|
||||
}
|
||||
|
||||
var containerID string
|
||||
var npmProxyID int
|
||||
var instanceID string
|
||||
var deployErr error
|
||||
|
||||
if stage.MaxInstances == 1 {
|
||||
containerID, npmProxyID, instanceID, deployErr = d.blueGreenDeploy(ctx, project, stage, settings, deployID, imageTag)
|
||||
} else {
|
||||
containerID, npmProxyID, instanceID, deployErr = d.executeDeploy(ctx, project, stage, settings, deployID, imageTag)
|
||||
}
|
||||
|
||||
if deployErr != nil {
|
||||
d.logDeploy(deployID, fmt.Sprintf("Deploy failed: %v", deployErr), "error")
|
||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "failed", deployErr.Error())
|
||||
d.rollback(ctx, deployID, containerID, npmProxyID, instanceID)
|
||||
|
||||
d.notifier.Send(settings.NotificationURL, notify.Event{
|
||||
Type: "deploy_failure",
|
||||
Project: project.Name,
|
||||
Stage: stage.Name,
|
||||
ImageTag: imageTag,
|
||||
Error: deployErr.Error(),
|
||||
})
|
||||
|
||||
return fmt.Errorf("deploy failed: %w", deployErr)
|
||||
}
|
||||
|
||||
if err := d.store.UpdateDeployStatus(deployID, "success", ""); err != nil {
|
||||
slog.Warn("update deploy status to success", "error", err)
|
||||
}
|
||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "success", "")
|
||||
|
||||
subdomain := d.buildSubdomain(project, stage, settings, imageTag)
|
||||
fullURL := fmt.Sprintf("https://%s.%s", subdomain, settings.Domain)
|
||||
|
||||
d.logDeploy(deployID, fmt.Sprintf("Deploy successful: %s", fullURL), "info")
|
||||
|
||||
d.notifier.Send(settings.NotificationURL, notify.Event{
|
||||
Type: "deploy_success",
|
||||
Project: project.Name,
|
||||
Stage: stage.Name,
|
||||
ImageTag: imageTag,
|
||||
Subdomain: subdomain,
|
||||
URL: fullURL,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerDeploy is the synchronous entry point for deployments (used by poller and webhook).
|
||||
// It orchestrates the full flow: pull image -> create container -> start -> configure proxy -> health check.
|
||||
// On failure, it rolls back (removes container, deletes proxy host, updates status).
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
// Labels applied to all containers managed by docker-watcher.
|
||||
@@ -42,7 +42,7 @@ func (c *Client) Close() error {
|
||||
|
||||
// Ping checks connectivity to the Docker daemon.
|
||||
func (c *Client) Ping(ctx context.Context) error {
|
||||
_, err := c.api.Ping(ctx)
|
||||
_, err := c.api.Ping(ctx, client.PingOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("ping docker daemon: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,11 +7,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/mount"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
// ContainerConfig holds all parameters needed to create a managed container.
|
||||
@@ -69,13 +68,16 @@ func ContainerName(project, stage, tag string) string {
|
||||
// It returns the container ID on success.
|
||||
func (c *Client) CreateContainer(ctx context.Context, cfg ContainerConfig) (string, error) {
|
||||
// Build port bindings: each exposed port maps to a random host port.
|
||||
exposedPorts := nat.PortSet{}
|
||||
portBindings := nat.PortMap{}
|
||||
exposedPorts := network.PortSet{}
|
||||
portBindings := network.PortMap{}
|
||||
for _, p := range cfg.ExposedPorts {
|
||||
port := nat.Port(p)
|
||||
port, err := network.ParsePort(p)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse port %s: %w", p, err)
|
||||
}
|
||||
exposedPorts[port] = struct{}{}
|
||||
portBindings[port] = []nat.PortBinding{
|
||||
{HostIP: "0.0.0.0", HostPort: ""}, // empty HostPort = auto-assign
|
||||
portBindings[port] = []network.PortBinding{
|
||||
{HostPort: ""}, // empty HostPort = auto-assign
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +115,12 @@ func (c *Client) CreateContainer(ctx context.Context, cfg ContainerConfig) (stri
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := c.api.ContainerCreate(ctx, containerCfg, hostCfg, networkCfg, nil, cfg.Name)
|
||||
resp, err := c.api.ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
Config: containerCfg,
|
||||
HostConfig: hostCfg,
|
||||
NetworkingConfig: networkCfg,
|
||||
Name: cfg.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create container %s: %w", cfg.Name, err)
|
||||
}
|
||||
@@ -123,7 +130,7 @@ func (c *Client) CreateContainer(ctx context.Context, cfg ContainerConfig) (stri
|
||||
|
||||
// StartContainer starts a stopped container.
|
||||
func (c *Client) StartContainer(ctx context.Context, containerID string) error {
|
||||
if err := c.api.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
|
||||
if _, err := c.api.ContainerStart(ctx, containerID, client.ContainerStartOptions{}); err != nil {
|
||||
return fmt.Errorf("start container %s: %w", containerID, err)
|
||||
}
|
||||
return nil
|
||||
@@ -132,12 +139,12 @@ func (c *Client) StartContainer(ctx context.Context, containerID string) error {
|
||||
// StopContainer gracefully stops a running container with the given timeout in seconds.
|
||||
// A nil timeout uses the Docker default (10 seconds).
|
||||
func (c *Client) StopContainer(ctx context.Context, containerID string, timeoutSeconds int) error {
|
||||
opts := container.StopOptions{}
|
||||
opts := client.ContainerStopOptions{}
|
||||
if timeoutSeconds > 0 {
|
||||
opts.Timeout = &timeoutSeconds
|
||||
}
|
||||
|
||||
if err := c.api.ContainerStop(ctx, containerID, opts); err != nil {
|
||||
if _, err := c.api.ContainerStop(ctx, containerID, opts); err != nil {
|
||||
return fmt.Errorf("stop container %s: %w", containerID, err)
|
||||
}
|
||||
return nil
|
||||
@@ -146,12 +153,12 @@ func (c *Client) StopContainer(ctx context.Context, containerID string, timeoutS
|
||||
// RemoveContainer removes a container. If force is true, a running container
|
||||
// will be killed before removal.
|
||||
func (c *Client) RemoveContainer(ctx context.Context, containerID string, force bool) error {
|
||||
opts := container.RemoveOptions{
|
||||
opts := client.ContainerRemoveOptions{
|
||||
Force: force,
|
||||
RemoveVolumes: true,
|
||||
}
|
||||
|
||||
if err := c.api.ContainerRemove(ctx, containerID, opts); err != nil {
|
||||
if _, err := c.api.ContainerRemove(ctx, containerID, opts); err != nil {
|
||||
return fmt.Errorf("remove container %s: %w", containerID, err)
|
||||
}
|
||||
return nil
|
||||
@@ -159,12 +166,12 @@ func (c *Client) RemoveContainer(ctx context.Context, containerID string, force
|
||||
|
||||
// RestartContainer restarts a container with the given timeout in seconds.
|
||||
func (c *Client) RestartContainer(ctx context.Context, containerID string, timeoutSeconds int) error {
|
||||
opts := container.StopOptions{}
|
||||
opts := client.ContainerRestartOptions{}
|
||||
if timeoutSeconds > 0 {
|
||||
opts.Timeout = &timeoutSeconds
|
||||
}
|
||||
|
||||
if err := c.api.ContainerRestart(ctx, containerID, opts); err != nil {
|
||||
if _, err := c.api.ContainerRestart(ctx, containerID, opts); err != nil {
|
||||
return fmt.Errorf("restart container %s: %w", containerID, err)
|
||||
}
|
||||
return nil
|
||||
@@ -187,7 +194,7 @@ type ManagedContainer struct {
|
||||
// Pass nil or an empty map to list all docker-watcher managed containers.
|
||||
// Label filters are key=value pairs applied as Docker label filters.
|
||||
func (c *Client) ListContainers(ctx context.Context, labelFilters map[string]string) ([]ManagedContainer, error) {
|
||||
filterArgs := filters.NewArgs()
|
||||
filterArgs := make(client.Filters)
|
||||
|
||||
// Always filter by the docker-watcher project label to only return managed containers.
|
||||
filterArgs.Add("label", LabelProject)
|
||||
@@ -200,7 +207,7 @@ func (c *Client) ListContainers(ctx context.Context, labelFilters map[string]str
|
||||
}
|
||||
}
|
||||
|
||||
containers, err := c.api.ContainerList(ctx, container.ListOptions{
|
||||
listResult, err := c.api.ContainerList(ctx, client.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filterArgs,
|
||||
})
|
||||
@@ -208,8 +215,8 @@ func (c *Client) ListContainers(ctx context.Context, labelFilters map[string]str
|
||||
return nil, fmt.Errorf("list containers: %w", err)
|
||||
}
|
||||
|
||||
result := make([]ManagedContainer, 0, len(containers))
|
||||
for _, ctr := range containers {
|
||||
result := make([]ManagedContainer, 0, len(listResult.Items))
|
||||
for _, ctr := range listResult.Items {
|
||||
name := ""
|
||||
if len(ctr.Names) > 0 {
|
||||
// Docker prefixes names with "/".
|
||||
@@ -228,7 +235,7 @@ func (c *Client) ListContainers(ctx context.Context, labelFilters map[string]str
|
||||
Name: name,
|
||||
Image: ctr.Image,
|
||||
Status: ctr.Status,
|
||||
State: ctr.State,
|
||||
State: string(ctr.State),
|
||||
Project: ctr.Labels[LabelProject],
|
||||
Stage: ctr.Labels[LabelStage],
|
||||
InstanceID: ctr.Labels[LabelInstanceID],
|
||||
@@ -242,12 +249,17 @@ func (c *Client) ListContainers(ctx context.Context, labelFilters map[string]str
|
||||
// InspectContainerPort returns the host port mapped to a given container port.
|
||||
// This is useful after starting a container with auto-assigned ports.
|
||||
func (c *Client) InspectContainerPort(ctx context.Context, containerID string, containerPort string) (uint16, error) {
|
||||
inspect, err := c.api.ContainerInspect(ctx, containerID)
|
||||
inspectResult, err := c.api.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("inspect container %s: %w", containerID, err)
|
||||
}
|
||||
inspect := inspectResult.Container
|
||||
|
||||
port, err := network.ParsePort(containerPort)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parse container port %s: %w", containerPort, err)
|
||||
}
|
||||
|
||||
port := nat.Port(containerPort)
|
||||
bindings, ok := inspect.NetworkSettings.Ports[port]
|
||||
if !ok || len(bindings) == 0 {
|
||||
return 0, fmt.Errorf("container %s: no binding for port %s", containerID, containerPort)
|
||||
|
||||
+23
-22
@@ -5,11 +5,10 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
// ImageInfo holds metadata extracted from a Docker image inspection.
|
||||
@@ -33,7 +32,7 @@ func (c *Client) PullImage(ctx context.Context, imageRef string, tag string, aut
|
||||
ref = imageRef + ":" + tag
|
||||
}
|
||||
|
||||
opts := image.PullOptions{}
|
||||
opts := client.ImagePullOptions{}
|
||||
if authConfig != "" {
|
||||
opts.RegistryAuth = authConfig
|
||||
}
|
||||
@@ -42,11 +41,10 @@ func (c *Client) PullImage(ctx context.Context, imageRef string, tag string, aut
|
||||
if err != nil {
|
||||
return fmt.Errorf("pull image %s: %w", ref, err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// Drain the pull progress stream to completion.
|
||||
if _, err := io.Copy(io.Discard, reader); err != nil {
|
||||
return fmt.Errorf("read pull response for %s: %w", ref, err)
|
||||
// Wait for the pull to complete.
|
||||
if err := reader.Wait(ctx); err != nil {
|
||||
return fmt.Errorf("wait for pull of %s: %w", ref, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -54,26 +52,29 @@ func (c *Client) PullImage(ctx context.Context, imageRef string, tag string, aut
|
||||
|
||||
// InspectImage retrieves metadata from a local image.
|
||||
func (c *Client) InspectImage(ctx context.Context, imageRef string) (ImageInfo, error) {
|
||||
inspect, _, err := c.api.ImageInspectWithRaw(ctx, imageRef)
|
||||
inspectResult, err := c.api.ImageInspect(ctx, imageRef)
|
||||
if err != nil {
|
||||
return ImageInfo{}, fmt.Errorf("inspect image %s: %w", imageRef, err)
|
||||
}
|
||||
|
||||
info := ImageInfo{
|
||||
Labels: inspect.Config.Labels,
|
||||
}
|
||||
info := ImageInfo{}
|
||||
|
||||
// Extract exposed ports.
|
||||
for port := range inspect.Config.ExposedPorts {
|
||||
info.ExposedPorts = append(info.ExposedPorts, string(port))
|
||||
}
|
||||
// Extract labels from Config if available.
|
||||
if inspectResult.Config != nil {
|
||||
info.Labels = inspectResult.Config.Labels
|
||||
|
||||
// Extract healthcheck command.
|
||||
if inspect.Config.Healthcheck != nil && len(inspect.Config.Healthcheck.Test) > 0 {
|
||||
// The Test slice is ["CMD", "arg1", "arg2", ...] or ["CMD-SHELL", "cmd string"].
|
||||
// Join all parts after the first element for a readable representation.
|
||||
if len(inspect.Config.Healthcheck.Test) > 1 {
|
||||
info.Healthcheck = joinArgs(inspect.Config.Healthcheck.Test[1:])
|
||||
// Extract exposed ports from OCI config (map[string]struct{}).
|
||||
for port := range inspectResult.Config.ExposedPorts {
|
||||
info.ExposedPorts = append(info.ExposedPorts, port)
|
||||
}
|
||||
|
||||
// Extract healthcheck command.
|
||||
if inspectResult.Config.Healthcheck != nil && len(inspectResult.Config.Healthcheck.Test) > 0 {
|
||||
// The Test slice is ["CMD", "arg1", "arg2", ...] or ["CMD-SHELL", "cmd string"].
|
||||
// Join all parts after the first element for a readable representation.
|
||||
if len(inspectResult.Config.Healthcheck.Test) > 1 {
|
||||
info.Healthcheck = joinArgs(inspectResult.Config.Healthcheck.Test[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,17 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
// EnsureNetwork creates a Docker network with the given name if it does not
|
||||
// already exist. It returns the network ID in all cases.
|
||||
func (c *Client) EnsureNetwork(ctx context.Context, networkName string) (string, error) {
|
||||
// Check if the network already exists.
|
||||
filterArgs := filters.NewArgs()
|
||||
filterArgs.Add("name", networkName)
|
||||
filterArgs := make(client.Filters).Add("name", networkName)
|
||||
|
||||
networks, err := c.api.NetworkList(ctx, network.ListOptions{
|
||||
listResult, err := c.api.NetworkList(ctx, client.NetworkListOptions{
|
||||
Filters: filterArgs,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -23,14 +22,14 @@ func (c *Client) EnsureNetwork(ctx context.Context, networkName string) (string,
|
||||
}
|
||||
|
||||
// NetworkList with a name filter may return partial matches, so check exact name.
|
||||
for _, n := range networks {
|
||||
for _, n := range listResult.Items {
|
||||
if n.Name == networkName {
|
||||
return n.ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Create the network.
|
||||
resp, err := c.api.NetworkCreate(ctx, networkName, network.CreateOptions{
|
||||
resp, err := c.api.NetworkCreate(ctx, networkName, client.NetworkCreateOptions{
|
||||
Driver: "bridge",
|
||||
Labels: map[string]string{
|
||||
LabelProject: "docker-watcher",
|
||||
@@ -45,7 +44,10 @@ func (c *Client) EnsureNetwork(ctx context.Context, networkName string) (string,
|
||||
|
||||
// ConnectNetwork attaches a container to an existing network.
|
||||
func (c *Client) ConnectNetwork(ctx context.Context, networkID string, containerID string) error {
|
||||
err := c.api.NetworkConnect(ctx, networkID, containerID, &network.EndpointSettings{})
|
||||
_, err := c.api.NetworkConnect(ctx, networkID, client.NetworkConnectOptions{
|
||||
Container: containerID,
|
||||
EndpointConfig: &network.EndpointSettings{},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect container %s to network %s: %w", containerID, networkID, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user