From 652229c67f9e83b659f890a5ae6da2aef82d062e Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 28 Mar 2026 13:13:45 +0300 Subject: [PATCH] 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. --- .dockerignore | 9 +++ Dockerfile | 5 +- go.mod | 40 +++++------ go.sum | 125 ++++++++++++++++++++++++++++++++++ internal/deployer/deployer.go | 75 +++++++++++++++++++- internal/docker/client.go | 4 +- internal/docker/container.go | 62 ++++++++++------- internal/docker/image.go | 45 ++++++------ internal/docker/network.go | 18 ++--- 9 files changed, 301 insertions(+), 82 deletions(-) create mode 100644 .dockerignore create mode 100644 go.sum diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..057d540 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +node_modules +web/node_modules +web/build +data +*.md +plans/ +.claude/ +.dockerignore diff --git a/Dockerfile b/Dockerfile index 4e819aa..8b914db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,9 +15,6 @@ RUN apk add --no-cache git ca-certificates WORKDIR /build COPY go.mod go.sum ./ - -# The project requires Go 1.25 (transitive dep from Docker SDK -> otelhttp). -# GOTOOLCHAIN=auto lets Go 1.24 download the required toolchain automatically. ENV GOTOOLCHAIN=auto RUN go mod download @@ -25,7 +22,7 @@ COPY . . # Copy built frontend into the expected embed location. COPY --from=frontend-builder /build/web/build ./web/build -RUN GOTOOLCHAIN=auto CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /docker-watcher ./cmd/server +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /docker-watcher ./cmd/server # Stage 3: Minimal runtime image FROM alpine:3.19 diff --git a/go.mod b/go.mod index b2bb51c..54c9737 100644 --- a/go.mod +++ b/go.mod @@ -2,52 +2,52 @@ module github.com/alexei/docker-watcher go 1.24.0 +toolchain go1.25.0 + require ( github.com/coreos/go-oidc/v3 v3.11.0 - github.com/docker/docker v25.0.7+incompatible - github.com/docker/go-connections v0.5.0 github.com/go-chi/chi/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 + github.com/moby/moby/api v1.54.0 + github.com/moby/moby/client v0.3.0 github.com/robfig/cron/v3 v3.0.1 - golang.org/x/crypto v0.31.0 + golang.org/x/crypto v0.28.0 golang.org/x/oauth2 v0.25.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.34.5 ) require ( - github.com/Microsoft/go-winio v0.4.14 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/sys/atomicwriter v0.1.0 // indirect - github.com/moby/term v0.5.2 // indirect - github.com/morikuni/aec v1.1.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pkg/errors v0.8.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/time v0.9.0 // indirect - gotest.tools/v3 v3.5.2 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/tools v0.22.0 // indirect modernc.org/libc v1.55.3 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect ) + +// Prevent the +incompatible monorepo from being pulled (conflicts with moby/moby/client submodule). +replace github.com/moby/moby => github.com/moby/moby/client v0.3.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4374b63 --- /dev/null +++ b/go.sum @@ -0,0 +1,125 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= +github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= +github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= +github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= +modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index d192412..5a43297 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -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). diff --git a/internal/docker/client.go b/internal/docker/client.go index f66d09a..5b29d40 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -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) } diff --git a/internal/docker/container.go b/internal/docker/container.go index bafa887..c835766 100644 --- a/internal/docker/container.go +++ b/internal/docker/container.go @@ -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) diff --git a/internal/docker/image.go b/internal/docker/image.go index 0e12c7e..bcc2793 100644 --- a/internal/docker/image.go +++ b/internal/docker/image.go @@ -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:]) + } } } diff --git a/internal/docker/network.go b/internal/docker/network.go index a36deeb..240c05e 100644 --- a/internal/docker/network.go +++ b/internal/docker/network.go @@ -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) }