fix(release): prepare v2.31 release blockers (#235)
* fix(release): prepare v2.31 release blockers * fix(docker): add volume listing context
- commit
- e02f6546f2612bb46eb3b987defdf24b6e53cd7f
- author
- bnema · · 2026-08-23 14:44 UTC
- committer
- GitHub · · 2026-08-23 14:44 UTC
- parent
- fdf5cec
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown - refs
- tag: v2.31.0
34 files · +580 -263
M .github/actions/setup-gordon/action.yml +1 -1
| @@ -83,7 +83,7 @@ runs: | |||
| 83 | 83 | curl -fsSL -o "${dest}" "${url}" | |
| 84 | 84 | ||
| 85 | 85 | # Verify checksum before extraction | |
| 86 | - | archive_escaped="$(printf '%s\n' "${archive}" | sed 's/[.[\\*^$()+?{|]/\\\\&/g')" | |
| 86 | + | archive_escaped="$(printf '%s\n' "${archive}" | sed 's/[.[\*^$()+?{|]/\\&/g')" | |
| 87 | 87 | checksum_lines="$(grep -E "^[0-9a-fA-F]{64}[[:space:]]+\\*?${archive_escaped}$" "${checksums}" || true)" | |
| 88 | 88 | if [ "$(printf '%s\n' "${checksum_lines}" | sed '/^$/d' | wc -l | tr -d ' ')" != "1" ]; then | |
| 89 | 89 | echo "Expected exactly one checksum for ${archive} in checksums.txt" >&2 | |
| @@ -83,7 +83,7 @@ runs: | |||||
| 83 | curl -fsSL -o "${dest}" "${url}" | 83 | curl -fsSL -o "${dest}" "${url}" | ||
| 84 | 84 | ||||
| 85 | # Verify checksum before extraction | 85 | # Verify checksum before extraction | ||
| 86 | - | archive_escaped="$(printf '%s\n' "${archive}" | sed 's/[.[\\*^$()+?{|]/\\\\&/g')" | 86 | + | archive_escaped="$(printf '%s\n' "${archive}" | sed 's/[.[\*^$()+?{|]/\\&/g')" |
| 87 | checksum_lines="$(grep -E "^[0-9a-fA-F]{64}[[:space:]]+\\*?${archive_escaped}$" "${checksums}" || true)" | 87 | checksum_lines="$(grep -E "^[0-9a-fA-F]{64}[[:space:]]+\\*?${archive_escaped}$" "${checksums}" || true)" | ||
| 88 | if [ "$(printf '%s\n' "${checksum_lines}" | sed '/^$/d' | wc -l | tr -d ' ')" != "1" ]; then | 88 | if [ "$(printf '%s\n' "${checksum_lines}" | sed '/^$/d' | wc -l | tr -d ' ')" != "1" ]; then | ||
| 89 | echo "Expected exactly one checksum for ${archive} in checksums.txt" >&2 | 89 | echo "Expected exactly one checksum for ${archive} in checksums.txt" >&2 | ||
M .github/workflows/ci.yml +2 -2
| @@ -37,7 +37,7 @@ jobs: | |||
| 37 | 37 | ||
| 38 | 38 | - uses: actions/setup-go@v5 | |
| 39 | 39 | with: | |
| 40 | - | go-version: stable | |
| 40 | + | go-version: "1.27.x" | |
| 41 | 41 | ||
| 42 | 42 | - name: Cache Go modules | |
| 43 | 43 | uses: actions/cache@v5 | |
| @@ -62,7 +62,7 @@ jobs: | |||
| 62 | 62 | ||
| 63 | 63 | - uses: actions/setup-go@v5 | |
| 64 | 64 | with: | |
| 65 | - | go-version: stable | |
| 65 | + | go-version: "1.27.x" | |
| 66 | 66 | ||
| 67 | 67 | - name: Cache Go modules | |
| 68 | 68 | uses: actions/cache@v5 | |
| @@ -37,7 +37,7 @@ jobs: | |||||
| 37 | 37 | ||||
| 38 | - uses: actions/setup-go@v5 | 38 | - uses: actions/setup-go@v5 | ||
| 39 | with: | 39 | with: | ||
| 40 | - | go-version: stable | 40 | + | go-version: "1.27.x" |
| 41 | 41 | ||||
| 42 | - name: Cache Go modules | 42 | - name: Cache Go modules | ||
| 43 | uses: actions/cache@v5 | 43 | uses: actions/cache@v5 | ||
| @@ -62,7 +62,7 @@ jobs: | |||||
| 62 | 62 | ||||
| 63 | - uses: actions/setup-go@v5 | 63 | - uses: actions/setup-go@v5 | ||
| 64 | with: | 64 | with: | ||
| 65 | - | go-version: stable | 65 | + | go-version: "1.27.x" |
| 66 | 66 | ||||
| 67 | - name: Cache Go modules | 67 | - name: Cache Go modules | ||
| 68 | uses: actions/cache@v5 | 68 | uses: actions/cache@v5 | ||
M .github/workflows/release.yml +1 -1
| @@ -20,7 +20,7 @@ jobs: | |||
| 20 | 20 | ||
| 21 | 21 | - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| 22 | 22 | with: | |
| 23 | - | go-version: stable | |
| 23 | + | go-version: "1.27.x" | |
| 24 | 24 | ||
| 25 | 25 | - name: Run GoReleaser | |
| 26 | 26 | uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6 | |
| @@ -20,7 +20,7 @@ jobs: | |||||
| 20 | 20 | ||||
| 21 | - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | 21 | - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | ||
| 22 | with: | 22 | with: | ||
| 23 | - | go-version: stable | 23 | + | go-version: "1.27.x" |
| 24 | 24 | ||||
| 25 | - name: Run GoReleaser | 25 | - name: Run GoReleaser | ||
| 26 | uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6 | 26 | uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6 | ||
M Dockerfile +29 -33
| @@ -1,34 +1,39 @@ | |||
| 1 | - | # Gordon v2 - Production Dockerfile | |
| 2 | - | # Multi-stage build for optimized container image | |
| 1 | + | # Gordon v2 production image. | |
| 2 | + | # Build with BuildKit or Podman to select TARGETOS and TARGETARCH. | |
| 3 | 3 | ||
| 4 | - | # Build stage | |
| 5 | - | FROM golang:1.26.1-alpine3.22 AS builder | |
| 4 | + | ARG BUILDPLATFORM | |
| 5 | + | ARG VERSION=dev | |
| 6 | + | ARG COMMIT=unknown | |
| 7 | + | ARG BUILD_DATE=unknown | |
| 8 | + | ||
| 9 | + | FROM --platform=$BUILDPLATFORM golang:1.27-alpine3.22 AS builder | |
| 10 | + | ||
| 11 | + | ARG TARGETOS=linux | |
| 12 | + | ARG TARGETARCH=amd64 | |
| 13 | + | ARG VERSION | |
| 14 | + | ARG COMMIT | |
| 15 | + | ARG BUILD_DATE | |
| 6 | 16 | ||
| 7 | - | # Install build dependencies | |
| 8 | 17 | RUN apk add --no-cache git ca-certificates tzdata | |
| 9 | 18 | ||
| 10 | - | # Set working directory | |
| 11 | 19 | WORKDIR /app | |
| 12 | 20 | ||
| 13 | - | # Copy go mod files | |
| 14 | 21 | COPY go.mod go.sum ./ | |
| 15 | - | ||
| 16 | - | # Download dependencies | |
| 17 | 22 | RUN go mod download | |
| 18 | 23 | ||
| 19 | - | # Copy source code | |
| 20 | 24 | COPY . . | |
| 21 | 25 | ||
| 22 | - | # Build the binary | |
| 23 | - | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ | |
| 24 | - | -ldflags='-w -s -extldflags "-static"' \ | |
| 25 | - | -a -installsuffix cgo \ | |
| 26 | - | -o gordon . | |
| 26 | + | RUN CGO_ENABLED=0 GOOS="${TARGETOS}" GOARCH="${TARGETARCH}" go build \ | |
| 27 | + | -trimpath \ | |
| 28 | + | -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_DATE}" \ | |
| 29 | + | -o /gordon ./main.go | |
| 27 | 30 | ||
| 28 | - | # Runtime stage | |
| 29 | 31 | FROM alpine:3.22 | |
| 30 | 32 | ||
| 31 | - | # Install runtime dependencies | |
| 33 | + | ARG VERSION | |
| 34 | + | ARG COMMIT | |
| 35 | + | ARG BUILD_DATE | |
| 36 | + | ||
| 32 | 37 | RUN apk add --no-cache \ | |
| 33 | 38 | ca-certificates \ | |
| 34 | 39 | docker-cli \ | |
| @@ -37,35 +42,26 @@ RUN apk add --no-cache \ | |||
| 37 | 42 | tzdata \ | |
| 38 | 43 | && rm -rf /var/cache/apk/* | |
| 39 | 44 | ||
| 40 | - | # Create non-root user | |
| 41 | 45 | RUN adduser -D -s /bin/sh gordon | |
| 42 | 46 | ||
| 43 | - | # Set working directory | |
| 44 | 47 | WORKDIR /app | |
| 45 | 48 | ||
| 46 | - | # Copy binary from builder | |
| 47 | - | COPY --from=builder /app/gordon . | |
| 49 | + | COPY --from=builder /gordon ./gordon | |
| 48 | 50 | ||
| 49 | - | # Create data directory | |
| 50 | 51 | RUN mkdir -p /data && chown gordon:gordon /data | |
| 51 | - | ||
| 52 | - | # Copy default configuration (optional) | |
| 53 | 52 | COPY --chown=gordon:gordon gordon.toml.example /app/gordon.toml.example | |
| 54 | 53 | ||
| 55 | - | # Switch to non-root user | |
| 56 | 54 | USER gordon | |
| 57 | 55 | ||
| 58 | - | # Expose ports | |
| 59 | 56 | EXPOSE 8088 5000 | |
| 60 | 57 | ||
| 61 | 58 | # Admin health is served on the registry/admin listener and is gated by auth in | |
| 62 | 59 | # normal deployments, so this image does not declare a Docker healthcheck. | |
| 63 | - | ||
| 64 | - | # Default command | |
| 65 | 60 | CMD ["./gordon", "serve"] | |
| 66 | 61 | ||
| 67 | - | # Metadata | |
| 68 | - | LABEL maintainer="bnemam" | |
| 69 | - | LABEL version="2.0" | |
| 70 | - | LABEL description="Event-driven container deployment platform" | |
| 71 | - | LABEL org.opencontainers.image.source="https://github.com/bnema/gordon"⏎̸ | |
| 62 | + | LABEL org.opencontainers.image.title="Gordon" | |
| 63 | + | LABEL org.opencontainers.image.description="Event-driven container deployment platform" | |
| 64 | + | LABEL org.opencontainers.image.source="https://github.com/bnema/gordon" | |
| 65 | + | LABEL org.opencontainers.image.version="${VERSION}" | |
| 66 | + | LABEL org.opencontainers.image.revision="${COMMIT}" | |
| 67 | + | LABEL org.opencontainers.image.created="${BUILD_DATE}" | |
| @@ -1,34 +1,39 @@ | |||||
| 1 | - | # Gordon v2 - Production Dockerfile | 1 | + | # Gordon v2 production image. |
| 2 | - | # Multi-stage build for optimized container image | 2 | + | # Build with BuildKit or Podman to select TARGETOS and TARGETARCH. |
| 3 | 3 | ||||
| 4 | - | # Build stage | 4 | + | ARG BUILDPLATFORM |
| 5 | - | FROM golang:1.26.1-alpine3.22 AS builder | 5 | + | ARG VERSION=dev |
| 6 | + | ARG COMMIT=unknown | |||
| 7 | + | ARG BUILD_DATE=unknown | |||
| 8 | + | ||||
| 9 | + | FROM --platform=$BUILDPLATFORM golang:1.27-alpine3.22 AS builder | |||
| 10 | + | ||||
| 11 | + | ARG TARGETOS=linux | |||
| 12 | + | ARG TARGETARCH=amd64 | |||
| 13 | + | ARG VERSION | |||
| 14 | + | ARG COMMIT | |||
| 15 | + | ARG BUILD_DATE | |||
| 6 | 16 | ||||
| 7 | - | # Install build dependencies | |||
| 8 | RUN apk add --no-cache git ca-certificates tzdata | 17 | RUN apk add --no-cache git ca-certificates tzdata | ||
| 9 | 18 | ||||
| 10 | - | # Set working directory | |||
| 11 | WORKDIR /app | 19 | WORKDIR /app | ||
| 12 | 20 | ||||
| 13 | - | # Copy go mod files | |||
| 14 | COPY go.mod go.sum ./ | 21 | COPY go.mod go.sum ./ | ||
| 15 | - | ||||
| 16 | - | # Download dependencies | |||
| 17 | RUN go mod download | 22 | RUN go mod download | ||
| 18 | 23 | ||||
| 19 | - | # Copy source code | |||
| 20 | COPY . . | 24 | COPY . . | ||
| 21 | 25 | ||||
| 22 | - | # Build the binary | 26 | + | RUN CGO_ENABLED=0 GOOS="${TARGETOS}" GOARCH="${TARGETARCH}" go build \ |
| 23 | - | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ | 27 | + | -trimpath \ |
| 24 | - | -ldflags='-w -s -extldflags "-static"' \ | 28 | + | -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_DATE}" \ |
| 25 | - | -a -installsuffix cgo \ | 29 | + | -o /gordon ./main.go |
| 26 | - | -o gordon . | |||
| 27 | 30 | ||||
| 28 | - | # Runtime stage | |||
| 29 | FROM alpine:3.22 | 31 | FROM alpine:3.22 | ||
| 30 | 32 | ||||
| 31 | - | # Install runtime dependencies | 33 | + | ARG VERSION |
| 34 | + | ARG COMMIT | |||
| 35 | + | ARG BUILD_DATE | |||
| 36 | + | ||||
| 32 | RUN apk add --no-cache \ | 37 | RUN apk add --no-cache \ | ||
| 33 | ca-certificates \ | 38 | ca-certificates \ | ||
| 34 | docker-cli \ | 39 | docker-cli \ | ||
| @@ -37,35 +42,26 @@ RUN apk add --no-cache \ | |||||
| 37 | tzdata \ | 42 | tzdata \ | ||
| 38 | && rm -rf /var/cache/apk/* | 43 | && rm -rf /var/cache/apk/* | ||
| 39 | 44 | ||||
| 40 | - | # Create non-root user | |||
| 41 | RUN adduser -D -s /bin/sh gordon | 45 | RUN adduser -D -s /bin/sh gordon | ||
| 42 | 46 | ||||
| 43 | - | # Set working directory | |||
| 44 | WORKDIR /app | 47 | WORKDIR /app | ||
| 45 | 48 | ||||
| 46 | - | # Copy binary from builder | 49 | + | COPY --from=builder /gordon ./gordon |
| 47 | - | COPY --from=builder /app/gordon . | |||
| 48 | 50 | ||||
| 49 | - | # Create data directory | |||
| 50 | RUN mkdir -p /data && chown gordon:gordon /data | 51 | RUN mkdir -p /data && chown gordon:gordon /data | ||
| 51 | - | ||||
| 52 | - | # Copy default configuration (optional) | |||
| 53 | COPY --chown=gordon:gordon gordon.toml.example /app/gordon.toml.example | 52 | COPY --chown=gordon:gordon gordon.toml.example /app/gordon.toml.example | ||
| 54 | 53 | ||||
| 55 | - | # Switch to non-root user | |||
| 56 | USER gordon | 54 | USER gordon | ||
| 57 | 55 | ||||
| 58 | - | # Expose ports | |||
| 59 | EXPOSE 8088 5000 | 56 | EXPOSE 8088 5000 | ||
| 60 | 57 | ||||
| 61 | # Admin health is served on the registry/admin listener and is gated by auth in | 58 | # Admin health is served on the registry/admin listener and is gated by auth in | ||
| 62 | # normal deployments, so this image does not declare a Docker healthcheck. | 59 | # normal deployments, so this image does not declare a Docker healthcheck. | ||
| 63 | - | ||||
| 64 | - | # Default command | |||
| 65 | CMD ["./gordon", "serve"] | 60 | CMD ["./gordon", "serve"] | ||
| 66 | 61 | ||||
| 67 | - | # Metadata | 62 | + | LABEL org.opencontainers.image.title="Gordon" |
| 68 | - | LABEL maintainer="bnemam" | 63 | + | LABEL org.opencontainers.image.description="Event-driven container deployment platform" |
| 69 | - | LABEL version="2.0" | 64 | + | LABEL org.opencontainers.image.source="https://github.com/bnema/gordon" |
| 70 | - | LABEL description="Event-driven container deployment platform" | 65 | + | LABEL org.opencontainers.image.version="${VERSION}" |
| 71 | - | LABEL org.opencontainers.image.source="https://github.com/bnema/gordon"⏎̸ | 66 | + | LABEL org.opencontainers.image.revision="${COMMIT}" |
| 67 | + | LABEL org.opencontainers.image.created="${BUILD_DATE}" | |||
M Makefile +6 -4
| @@ -95,15 +95,17 @@ build-local: ## Build binary for current platform | |||
| 95 | 95 | ||
| 96 | 96 | ##@ Release | |
| 97 | 97 | ||
| 98 | - | build-push: build ## Build and push Docker images | |
| 98 | + | build-push: ## Build and push Docker images | |
| 99 | 99 | @echo "Cleaning up dangling images..." | |
| 100 | 100 | @$(ENGINE) image prune -f | |
| 101 | 101 | ||
| 102 | 102 | @echo "Building and pushing Docker images..." | |
| 103 | 103 | @for arch in $(ARCHS); do \ | |
| 104 | - | cp $(DIST_DIR)/gordon-linux-$$arch gordon; \ | |
| 105 | - | $(ENGINE) build -t $(REPO):$(TAG)-$$arch .; \ | |
| 106 | - | rm gordon; \ | |
| 104 | + | $(ENGINE) build --platform linux/$$arch \ | |
| 105 | + | --build-arg VERSION="$(VERSION)" \ | |
| 106 | + | --build-arg COMMIT="$(COMMIT)" \ | |
| 107 | + | --build-arg BUILD_DATE="$(BUILD_DATE)" \ | |
| 108 | + | -t $(REPO):$(TAG)-$$arch .; \ | |
| 107 | 109 | $(ENGINE) push $(REPO):$(TAG)-$$arch; \ | |
| 108 | 110 | done | |
| 109 | 111 | ||
| @@ -95,15 +95,17 @@ build-local: ## Build binary for current platform | |||||
| 95 | 95 | ||||
| 96 | ##@ Release | 96 | ##@ Release | ||
| 97 | 97 | ||||
| 98 | - | build-push: build ## Build and push Docker images | 98 | + | build-push: ## Build and push Docker images |
| 99 | @echo "Cleaning up dangling images..." | 99 | @echo "Cleaning up dangling images..." | ||
| 100 | @$(ENGINE) image prune -f | 100 | @$(ENGINE) image prune -f | ||
| 101 | 101 | ||||
| 102 | @echo "Building and pushing Docker images..." | 102 | @echo "Building and pushing Docker images..." | ||
| 103 | @for arch in $(ARCHS); do \ | 103 | @for arch in $(ARCHS); do \ | ||
| 104 | - | cp $(DIST_DIR)/gordon-linux-$$arch gordon; \ | 104 | + | $(ENGINE) build --platform linux/$$arch \ |
| 105 | - | $(ENGINE) build -t $(REPO):$(TAG)-$$arch .; \ | 105 | + | --build-arg VERSION="$(VERSION)" \ |
| 106 | - | rm gordon; \ | 106 | + | --build-arg COMMIT="$(COMMIT)" \ |
| 107 | + | --build-arg BUILD_DATE="$(BUILD_DATE)" \ | |||
| 108 | + | -t $(REPO):$(TAG)-$$arch .; \ | |||
| 107 | $(ENGINE) push $(REPO):$(TAG)-$$arch; \ | 109 | $(ENGINE) push $(REPO):$(TAG)-$$arch; \ | ||
| 108 | done | 110 | done | ||
| 109 | 111 | ||||
M docs/cli/index.md +0 -1
| @@ -246,7 +246,6 @@ gordon routes list | |||
| 246 | 246 | Gordon reads configuration from environment variables: | |
| 247 | 247 | ||
| 248 | 248 | ```bash | |
| 249 | - | GORDON_SERVER_PORT=8088 gordon serve | |
| 250 | 249 | GORDON_LOGGING_LEVEL=debug gordon serve | |
| 251 | 250 | ``` | |
| 252 | 251 | ||
| @@ -246,7 +246,6 @@ gordon routes list | |||||
| 246 | Gordon reads configuration from environment variables: | 246 | Gordon reads configuration from environment variables: | ||
| 247 | 247 | ||||
| 248 | ```bash | 248 | ```bash | ||
| 249 | - | GORDON_SERVER_PORT=8088 gordon serve | |||
| 250 | GORDON_LOGGING_LEVEL=debug gordon serve | 249 | GORDON_LOGGING_LEVEL=debug gordon serve | ||
| 251 | ``` | 250 | ``` | ||
| 252 | 251 | ||||
M docs/cli/serve.md +0 -1
| @@ -57,7 +57,6 @@ gordon serve --config /path/to/gordon.toml | |||
| 57 | 57 | gordon serve -c ./my-config.toml | |
| 58 | 58 | ||
| 59 | 59 | # With environment override | |
| 60 | - | GORDON_SERVER_PORT=8088 gordon serve | |
| 61 | 60 | GORDON_LOGGING_LEVEL=debug gordon serve | |
| 62 | 61 | ``` | |
| 63 | 62 | ||
| @@ -57,7 +57,6 @@ gordon serve --config /path/to/gordon.toml | |||||
| 57 | gordon serve -c ./my-config.toml | 57 | gordon serve -c ./my-config.toml | ||
| 58 | 58 | ||||
| 59 | # With environment override | 59 | # With environment override | ||
| 60 | - | GORDON_SERVER_PORT=8088 gordon serve | |||
| 61 | GORDON_LOGGING_LEVEL=debug gordon serve | 60 | GORDON_LOGGING_LEVEL=debug gordon serve | ||
| 62 | ``` | 61 | ``` | ||
| 63 | 62 | ||||
M docs/config/index.md +0 -1
| @@ -278,7 +278,6 @@ gordon reload | |||
| 278 | 278 | Configuration values can be overridden with environment variables: | |
| 279 | 279 | ||
| 280 | 280 | ```bash | |
| 281 | - | GORDON_SERVER_PORT=8088 gordon serve | |
| 282 | 281 | GORDON_LOGGING_LEVEL=debug gordon serve | |
| 283 | 282 | ``` | |
| 284 | 283 | ||
| @@ -278,7 +278,6 @@ gordon reload | |||||
| 278 | Configuration values can be overridden with environment variables: | 278 | Configuration values can be overridden with environment variables: | ||
| 279 | 279 | ||||
| 280 | ```bash | 280 | ```bash | ||
| 281 | - | GORDON_SERVER_PORT=8088 gordon serve | |||
| 282 | GORDON_LOGGING_LEVEL=debug gordon serve | 281 | GORDON_LOGGING_LEVEL=debug gordon serve | ||
| 283 | ``` | 282 | ``` | ||
| 284 | 283 | ||||
M docs/config/reference.md +0 -1
| @@ -360,7 +360,6 @@ GORDON_<SECTION>_<KEY>=value | |||
| 360 | 360 | ||
| 361 | 361 | Examples: | |
| 362 | 362 | ```bash | |
| 363 | - | GORDON_SERVER_PORT=8088 | |
| 364 | 363 | GORDON_SERVER_GORDON_DOMAIN=gordon.example.com | |
| 365 | 364 | GORDON_AUTH_ENABLED=true | |
| 366 | 365 | GORDON_LOGGING_LEVEL=debug | |
| @@ -360,7 +360,6 @@ GORDON_<SECTION>_<KEY>=value | |||||
| 360 | 360 | ||||
| 361 | Examples: | 361 | Examples: | ||
| 362 | ```bash | 362 | ```bash | ||
| 363 | - | GORDON_SERVER_PORT=8088 | |||
| 364 | GORDON_SERVER_GORDON_DOMAIN=gordon.example.com | 363 | GORDON_SERVER_GORDON_DOMAIN=gordon.example.com | ||
| 365 | GORDON_AUTH_ENABLED=true | 364 | GORDON_AUTH_ENABLED=true | ||
| 366 | GORDON_LOGGING_LEVEL=debug | 365 | GORDON_LOGGING_LEVEL=debug | ||
M docs/installation.md +1 -1
| @@ -8,7 +8,7 @@ Detailed installation guide for production environments. | |||
| 8 | 8 | - **Architecture**: x86_64 (amd64) or ARM64 | |
| 9 | 9 | - **Memory**: 512MB minimum, 1GB recommended | |
| 10 | 10 | - **Disk**: 10GB minimum for registry storage | |
| 11 | - | - **Runtime**: Docker or Podman | |
| 11 | + | - **Runtime**: Docker or Podman with a Docker-compatible API version 1.40 or later | |
| 12 | 12 | ||
| 13 | 13 | ## Download Gordon | |
| 14 | 14 | ||
| @@ -8,7 +8,7 @@ Detailed installation guide for production environments. | |||||
| 8 | - **Architecture**: x86_64 (amd64) or ARM64 | 8 | - **Architecture**: x86_64 (amd64) or ARM64 | ||
| 9 | - **Memory**: 512MB minimum, 1GB recommended | 9 | - **Memory**: 512MB minimum, 1GB recommended | ||
| 10 | - **Disk**: 10GB minimum for registry storage | 10 | - **Disk**: 10GB minimum for registry storage | ||
| 11 | - | - **Runtime**: Docker or Podman | 11 | + | - **Runtime**: Docker or Podman with a Docker-compatible API version 1.40 or later |
| 12 | 12 | ||||
| 13 | ## Download Gordon | 13 | ## Download Gordon | ||
| 14 | 14 | ||||
M docs/upgrading.md +10 -21
| @@ -61,34 +61,23 @@ Keep `server.registry_port` for Docker/Podman push and pull traffic; it is separ | |||
| 61 | 61 | ||
| 62 | 62 | ## v2.30.0 to v2.31.0 | |
| 63 | 63 | ||
| 64 | - | ### Breaking: Default Port Changes | |
| 64 | + | ### Breaking: Public listener migration | |
| 65 | 65 | ||
| 66 | - | Gordon now defaults to unprivileged ports that work without root or firewall rules: | |
| 66 | + | `server.port` and `server.tls_port` no longer create public listeners. Gordon refuses to start when either legacy key is present without an `[entrypoints]` entry, preventing routes from silently becoming unavailable. | |
| 67 | 67 | ||
| 68 | - | | Setting | Old default | New default | | |
| 69 | - | |---------|-------------|-------------| | |
| 70 | - | | `server.port` | `80` | `8088` | | |
| 71 | - | | `server.registry_port` | `5000` | `5000` (unchanged) | | |
| 72 | - | | `server.tls_port` | *(n/a)* | `8443` | | |
| 73 | - | ||
| 74 | - | **If you relied on the old defaults** (no explicit `port` in your config), add the port explicitly: | |
| 68 | + | Replace the legacy listener settings with a route-capable entrypoint: | |
| 75 | 69 | ||
| 76 | 70 | ```toml | |
| 77 | 71 | [server] | |
| 78 | - | port = 80 | |
| 79 | - | ``` | |
| 80 | - | ||
| 81 | - | Or set up firewall port forwarding to the new defaults: | |
| 72 | + | registry_port = 5000 | |
| 73 | + | gordon_domain = "gordon.example.com" | |
| 82 | 74 | ||
| 83 | - | ```bash | |
| 84 | - | sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8088 | |
| 85 | - | sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=8443 | |
| 86 | - | sudo firewall-cmd --reload | |
| 75 | + | [entrypoints.edge] | |
| 76 | + | address = ":443" | |
| 77 | + | protocol = "smart_tcp" | |
| 87 | 78 | ``` | |
| 88 | 79 | ||
| 89 | - | ### New: Internal Certificate Authority | |
| 90 | - | ||
| 91 | - | Gordon now includes an internal CA for automatic on-demand TLS. Set `tls_port = 0` to disable. See [Server Configuration](./config/server.md#internal-ca-and-tls) for details. | |
| 80 | + | Keep `server.registry_port` for Docker and Podman registry traffic. For public TLS, configure `[tls.acme]` or static certificates; do not use `server.tls_port`. | |
| 92 | 81 | ||
| 93 | 82 | ### Required for Cloudflare/Proxy Setups: `proxy_allowed_ips` | |
| 94 | 83 | ||
| @@ -105,7 +94,7 @@ proxy_allowed_ips = [ | |||
| 105 | 94 | ] | |
| 106 | 95 | ``` | |
| 107 | 96 | ||
| 108 | - | Without this, all proxied HTTP traffic returns `403 Forbidden`. Set `tls_port = 0` to disable the internal CA and skip this requirement. | |
| 97 | + | Without this, all proxied HTTP traffic returns `403 Forbidden`. Remove TLS-capable entrypoints to disable the internal CA and skip this requirement. | |
| 109 | 98 | ||
| 110 | 99 | ### Breaking: `server.gordon_domain` Replaces `server.registry_domain` | |
| 111 | 100 | ||
| @@ -61,34 +61,23 @@ Keep `server.registry_port` for Docker/Podman push and pull traffic; it is separ | |||||
| 61 | 61 | ||||
| 62 | ## v2.30.0 to v2.31.0 | 62 | ## v2.30.0 to v2.31.0 | ||
| 63 | 63 | ||||
| 64 | - | ### Breaking: Default Port Changes | 64 | + | ### Breaking: Public listener migration |
| 65 | 65 | ||||
| 66 | - | Gordon now defaults to unprivileged ports that work without root or firewall rules: | 66 | + | `server.port` and `server.tls_port` no longer create public listeners. Gordon refuses to start when either legacy key is present without an `[entrypoints]` entry, preventing routes from silently becoming unavailable. |
| 67 | 67 | ||||
| 68 | - | | Setting | Old default | New default | | 68 | + | Replace the legacy listener settings with a route-capable entrypoint: |
| 69 | - | |---------|-------------|-------------| | |||
| 70 | - | | `server.port` | `80` | `8088` | | |||
| 71 | - | | `server.registry_port` | `5000` | `5000` (unchanged) | | |||
| 72 | - | | `server.tls_port` | *(n/a)* | `8443` | | |||
| 73 | - | ||||
| 74 | - | **If you relied on the old defaults** (no explicit `port` in your config), add the port explicitly: | |||
| 75 | 69 | ||||
| 76 | ```toml | 70 | ```toml | ||
| 77 | [server] | 71 | [server] | ||
| 78 | - | port = 80 | 72 | + | registry_port = 5000 |
| 79 | - | ``` | 73 | + | gordon_domain = "gordon.example.com" |
| 80 | - | ||||
| 81 | - | Or set up firewall port forwarding to the new defaults: | |||
| 82 | 74 | ||||
| 83 | - | ```bash | 75 | + | [entrypoints.edge] |
| 84 | - | sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8088 | 76 | + | address = ":443" |
| 85 | - | sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=8443 | 77 | + | protocol = "smart_tcp" |
| 86 | - | sudo firewall-cmd --reload | |||
| 87 | ``` | 78 | ``` | ||
| 88 | 79 | ||||
| 89 | - | ### New: Internal Certificate Authority | 80 | + | Keep `server.registry_port` for Docker and Podman registry traffic. For public TLS, configure `[tls.acme]` or static certificates; do not use `server.tls_port`. |
| 90 | - | ||||
| 91 | - | Gordon now includes an internal CA for automatic on-demand TLS. Set `tls_port = 0` to disable. See [Server Configuration](./config/server.md#internal-ca-and-tls) for details. | |||
| 92 | 81 | ||||
| 93 | ### Required for Cloudflare/Proxy Setups: `proxy_allowed_ips` | 82 | ### Required for Cloudflare/Proxy Setups: `proxy_allowed_ips` | ||
| 94 | 83 | ||||
| @@ -105,7 +94,7 @@ proxy_allowed_ips = [ | |||||
| 105 | ] | 94 | ] | ||
| 106 | ``` | 95 | ``` | ||
| 107 | 96 | ||||
| 108 | - | Without this, all proxied HTTP traffic returns `403 Forbidden`. Set `tls_port = 0` to disable the internal CA and skip this requirement. | 97 | + | Without this, all proxied HTTP traffic returns `403 Forbidden`. Remove TLS-capable entrypoints to disable the internal CA and skip this requirement. |
| 109 | 98 | ||||
| 110 | ### Breaking: `server.gordon_domain` Replaces `server.registry_domain` | 99 | ### Breaking: `server.gordon_domain` Replaces `server.registry_domain` | ||
| 111 | 100 | ||||
M go.mod +3 -8
| @@ -13,8 +13,6 @@ require ( | |||
| 13 | 13 | github.com/charmbracelet/lipgloss v1.1.0 | |
| 14 | 14 | github.com/containerd/errdefs v1.0.0 | |
| 15 | 15 | github.com/coreos/go-systemd/v22 v22.7.0 | |
| 16 | - | github.com/docker/docker v28.5.2+incompatible | |
| 17 | - | github.com/docker/go-connections v0.8.1 | |
| 18 | 16 | github.com/docker/go-units v0.5.0 | |
| 19 | 17 | github.com/fsnotify/fsnotify v1.10.1 // direct | |
| 20 | 18 | github.com/go-acme/lego/v4 v4.35.2 | |
| @@ -23,6 +21,8 @@ require ( | |||
| 23 | 21 | github.com/google/uuid v1.6.0 | |
| 24 | 22 | github.com/mattn/go-isatty v0.0.24 | |
| 25 | 23 | github.com/mattn/go-runewidth v0.0.28 | |
| 24 | + | github.com/moby/moby/api v1.55.0 | |
| 25 | + | github.com/moby/moby/client v0.5.1 | |
| 26 | 26 | github.com/muesli/termenv v0.16.0 | |
| 27 | 27 | github.com/pelletier/go-toml/v2 v2.4.3 | |
| 28 | 28 | github.com/rivo/uniseg v0.4.7 | |
| @@ -78,10 +78,10 @@ require ( | |||
| 78 | 78 | github.com/clipperhouse/displaywidth v0.11.0 // indirect | |
| 79 | 79 | github.com/clipperhouse/uax29/v2 v2.7.0 // indirect | |
| 80 | 80 | github.com/containerd/errdefs/pkg v0.3.0 // indirect | |
| 81 | - | github.com/containerd/log v0.1.0 // indirect | |
| 82 | 81 | github.com/distribution/reference v0.6.0 // indirect | |
| 83 | 82 | github.com/docker/cli v29.6.2+incompatible // indirect | |
| 84 | 83 | github.com/docker/docker-credential-helpers v0.9.5 // indirect | |
| 84 | + | github.com/docker/go-connections v0.8.1 // indirect | |
| 85 | 85 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | |
| 86 | 86 | github.com/felixge/httpsnoop v1.1.0 // indirect | |
| 87 | 87 | github.com/go-jose/go-jose/v4 v4.1.4 // indirect | |
| @@ -96,14 +96,10 @@ require ( | |||
| 96 | 96 | github.com/mattn/go-localereader v0.0.1 // indirect | |
| 97 | 97 | github.com/miekg/dns v1.1.72 // indirect | |
| 98 | 98 | github.com/moby/docker-image-spec v1.3.1 // indirect | |
| 99 | - | github.com/moby/sys/atomicwriter v0.1.0 // indirect | |
| 100 | - | github.com/moby/term v0.5.2 // indirect | |
| 101 | - | github.com/morikuni/aec v1.1.0 // indirect | |
| 102 | 99 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect | |
| 103 | 100 | github.com/muesli/cancelreader v0.2.2 // indirect | |
| 104 | 101 | github.com/opencontainers/go-digest v1.0.0 // indirect | |
| 105 | 102 | github.com/opencontainers/image-spec v1.1.1 // indirect | |
| 106 | - | github.com/pkg/errors v0.9.1 // indirect | |
| 107 | 103 | github.com/sagikazarmark/locafero v0.12.0 // indirect | |
| 108 | 104 | github.com/sirupsen/logrus v1.9.4 // indirect | |
| 109 | 105 | github.com/spf13/cast v1.10.0 // indirect | |
| @@ -124,6 +120,5 @@ require ( | |||
| 124 | 120 | google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect | |
| 125 | 121 | google.golang.org/grpc v1.83.0 // indirect | |
| 126 | 122 | google.golang.org/protobuf v1.36.11 // indirect | |
| 127 | - | gotest.tools/v3 v3.5.2 // indirect | |
| 128 | 123 | howett.net/plist v1.0.1 // indirect | |
| 129 | 124 | ) | |
| @@ -13,8 +13,6 @@ require ( | |||||
| 13 | github.com/charmbracelet/lipgloss v1.1.0 | 13 | github.com/charmbracelet/lipgloss v1.1.0 | ||
| 14 | github.com/containerd/errdefs v1.0.0 | 14 | github.com/containerd/errdefs v1.0.0 | ||
| 15 | github.com/coreos/go-systemd/v22 v22.7.0 | 15 | github.com/coreos/go-systemd/v22 v22.7.0 | ||
| 16 | - | github.com/docker/docker v28.5.2+incompatible | |||
| 17 | - | github.com/docker/go-connections v0.8.1 | |||
| 18 | github.com/docker/go-units v0.5.0 | 16 | github.com/docker/go-units v0.5.0 | ||
| 19 | github.com/fsnotify/fsnotify v1.10.1 // direct | 17 | github.com/fsnotify/fsnotify v1.10.1 // direct | ||
| 20 | github.com/go-acme/lego/v4 v4.35.2 | 18 | github.com/go-acme/lego/v4 v4.35.2 | ||
| @@ -23,6 +21,8 @@ require ( | |||||
| 23 | github.com/google/uuid v1.6.0 | 21 | github.com/google/uuid v1.6.0 | ||
| 24 | github.com/mattn/go-isatty v0.0.24 | 22 | github.com/mattn/go-isatty v0.0.24 | ||
| 25 | github.com/mattn/go-runewidth v0.0.28 | 23 | github.com/mattn/go-runewidth v0.0.28 | ||
| 24 | + | github.com/moby/moby/api v1.55.0 | |||
| 25 | + | github.com/moby/moby/client v0.5.1 | |||
| 26 | github.com/muesli/termenv v0.16.0 | 26 | github.com/muesli/termenv v0.16.0 | ||
| 27 | github.com/pelletier/go-toml/v2 v2.4.3 | 27 | github.com/pelletier/go-toml/v2 v2.4.3 | ||
| 28 | github.com/rivo/uniseg v0.4.7 | 28 | github.com/rivo/uniseg v0.4.7 | ||
| @@ -78,10 +78,10 @@ require ( | |||||
| 78 | github.com/clipperhouse/displaywidth v0.11.0 // indirect | 78 | github.com/clipperhouse/displaywidth v0.11.0 // indirect | ||
| 79 | github.com/clipperhouse/uax29/v2 v2.7.0 // indirect | 79 | github.com/clipperhouse/uax29/v2 v2.7.0 // indirect | ||
| 80 | github.com/containerd/errdefs/pkg v0.3.0 // indirect | 80 | github.com/containerd/errdefs/pkg v0.3.0 // indirect | ||
| 81 | - | github.com/containerd/log v0.1.0 // indirect | |||
| 82 | github.com/distribution/reference v0.6.0 // indirect | 81 | github.com/distribution/reference v0.6.0 // indirect | ||
| 83 | github.com/docker/cli v29.6.2+incompatible // indirect | 82 | github.com/docker/cli v29.6.2+incompatible // indirect | ||
| 84 | github.com/docker/docker-credential-helpers v0.9.5 // indirect | 83 | github.com/docker/docker-credential-helpers v0.9.5 // indirect | ||
| 84 | + | github.com/docker/go-connections v0.8.1 // indirect | |||
| 85 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | 85 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | ||
| 86 | github.com/felixge/httpsnoop v1.1.0 // indirect | 86 | github.com/felixge/httpsnoop v1.1.0 // indirect | ||
| 87 | github.com/go-jose/go-jose/v4 v4.1.4 // indirect | 87 | github.com/go-jose/go-jose/v4 v4.1.4 // indirect | ||
| @@ -96,14 +96,10 @@ require ( | |||||
| 96 | github.com/mattn/go-localereader v0.0.1 // indirect | 96 | github.com/mattn/go-localereader v0.0.1 // indirect | ||
| 97 | github.com/miekg/dns v1.1.72 // indirect | 97 | github.com/miekg/dns v1.1.72 // indirect | ||
| 98 | github.com/moby/docker-image-spec v1.3.1 // indirect | 98 | github.com/moby/docker-image-spec v1.3.1 // indirect | ||
| 99 | - | github.com/moby/sys/atomicwriter v0.1.0 // indirect | |||
| 100 | - | github.com/moby/term v0.5.2 // indirect | |||
| 101 | - | github.com/morikuni/aec v1.1.0 // indirect | |||
| 102 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect | 99 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect | ||
| 103 | github.com/muesli/cancelreader v0.2.2 // indirect | 100 | github.com/muesli/cancelreader v0.2.2 // indirect | ||
| 104 | github.com/opencontainers/go-digest v1.0.0 // indirect | 101 | github.com/opencontainers/go-digest v1.0.0 // indirect | ||
| 105 | github.com/opencontainers/image-spec v1.1.1 // indirect | 102 | github.com/opencontainers/image-spec v1.1.1 // indirect | ||
| 106 | - | github.com/pkg/errors v0.9.1 // indirect | |||
| 107 | github.com/sagikazarmark/locafero v0.12.0 // indirect | 103 | github.com/sagikazarmark/locafero v0.12.0 // indirect | ||
| 108 | github.com/sirupsen/logrus v1.9.4 // indirect | 104 | github.com/sirupsen/logrus v1.9.4 // indirect | ||
| 109 | github.com/spf13/cast v1.10.0 // indirect | 105 | github.com/spf13/cast v1.10.0 // indirect | ||
| @@ -124,6 +120,5 @@ require ( | |||||
| 124 | google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect | 120 | google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect | ||
| 125 | google.golang.org/grpc v1.83.0 // indirect | 121 | google.golang.org/grpc v1.83.0 // indirect | ||
| 126 | google.golang.org/protobuf v1.36.11 // indirect | 122 | google.golang.org/protobuf v1.36.11 // indirect | ||
| 127 | - | gotest.tools/v3 v3.5.2 // indirect | |||
| 128 | howett.net/plist v1.0.1 // indirect | 123 | howett.net/plist v1.0.1 // indirect | ||
| 129 | ) | 124 | ) | ||
M go.sum +6 -16 generated file: collapsed
M internal/adapters/in/cli/ca.go +2 -2
| @@ -100,8 +100,8 @@ func resolveCADataDir() (string, error) { | |||
| 100 | 100 | if err != nil { | |
| 101 | 101 | return "", err | |
| 102 | 102 | } | |
| 103 | - | if local.GetTLSPort() == 0 { | |
| 104 | - | return "", fmt.Errorf("internal TLS is disabled (server.tls_port=0); CA commands are not available — set server.tls_port to enable") | |
| 103 | + | if !local.HasInternalTLS() { | |
| 104 | + | return "", fmt.Errorf("internal TLS is disabled (no TLS-capable entrypoint configured); CA commands are not available — configure a smart_tcp or tls_mux entrypoint") | |
| 105 | 105 | } | |
| 106 | 106 | return local.GetDataDir(), nil | |
| 107 | 107 | } | |
| @@ -100,8 +100,8 @@ func resolveCADataDir() (string, error) { | |||||
| 100 | if err != nil { | 100 | if err != nil { | ||
| 101 | return "", err | 101 | return "", err | ||
| 102 | } | 102 | } | ||
| 103 | - | if local.GetTLSPort() == 0 { | 103 | + | if !local.HasInternalTLS() { |
| 104 | - | return "", fmt.Errorf("internal TLS is disabled (server.tls_port=0); CA commands are not available — set server.tls_port to enable") | 104 | + | return "", fmt.Errorf("internal TLS is disabled (no TLS-capable entrypoint configured); CA commands are not available — configure a smart_tcp or tls_mux entrypoint") |
| 105 | } | 105 | } | ||
| 106 | return local.GetDataDir(), nil | 106 | return local.GetDataDir(), nil | ||
| 107 | } | 107 | } | ||
M internal/adapters/in/cli/local.go +25 -11
| @@ -21,10 +21,10 @@ import ( | |||
| 21 | 21 | ||
| 22 | 22 | // LocalServices provides direct access to local services for CLI operations. | |
| 23 | 23 | type LocalServices struct { | |
| 24 | - | configSvc in.ConfigService | |
| 25 | - | secretSvc in.SecretService | |
| 26 | - | dataDir string | |
| 27 | - | tlsPort int | |
| 24 | + | configSvc in.ConfigService | |
| 25 | + | secretSvc in.SecretService | |
| 26 | + | dataDir string | |
| 27 | + | tlsEnabled bool | |
| 28 | 28 | } | |
| 29 | 29 | ||
| 30 | 30 | // GetConfigService returns the config service. | |
| @@ -42,9 +42,9 @@ func (l *LocalServices) GetDataDir() string { | |||
| 42 | 42 | return l.dataDir | |
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | - | // GetTLSPort returns the configured TLS port (0 means internal TLS is disabled). | |
| 46 | - | func (l *LocalServices) GetTLSPort() int { | |
| 47 | - | return l.tlsPort | |
| 45 | + | // HasInternalTLS reports whether a TLS-capable entrypoint enables the internal CA. | |
| 46 | + | func (l *LocalServices) HasInternalTLS() bool { | |
| 47 | + | return l.tlsEnabled | |
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | 50 | // GetLocalServices creates local services for CLI operations. | |
| @@ -95,13 +95,27 @@ func GetLocalServices(configPath string) (*LocalServices, error) { | |||
| 95 | 95 | secretSvc := secretsSvc.NewService(domainSecretStore, log, nil) | |
| 96 | 96 | ||
| 97 | 97 | return &LocalServices{ | |
| 98 | - | configSvc: configSvc, | |
| 99 | - | secretSvc: secretSvc, | |
| 100 | - | dataDir: dataDir, | |
| 101 | - | tlsPort: v.GetInt("server.tls_port"), | |
| 98 | + | configSvc: configSvc, | |
| 99 | + | secretSvc: secretSvc, | |
| 100 | + | dataDir: dataDir, | |
| 101 | + | tlsEnabled: hasLocalTLSCapableEntrypoint(v), | |
| 102 | 102 | }, nil | |
| 103 | 103 | } | |
| 104 | 104 | ||
| 105 | + | func hasLocalTLSCapableEntrypoint(v *viper.Viper) bool { | |
| 106 | + | for _, rawEntryPoint := range v.GetStringMap("entrypoints") { | |
| 107 | + | entryPoint, ok := rawEntryPoint.(map[string]any) | |
| 108 | + | if !ok { | |
| 109 | + | continue | |
| 110 | + | } | |
| 111 | + | switch strings.TrimSpace(fmt.Sprint(entryPoint["protocol"])) { | |
| 112 | + | case "smart_tcp", "tls_mux": | |
| 113 | + | return true | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | return false | |
| 117 | + | } | |
| 118 | + | ||
| 105 | 119 | func createLocalDomainSecretStore(v *viper.Viper, envDir string, log zerowrap.Logger) (out.DomainSecretStore, error) { | |
| 106 | 120 | backend := resolveLocalSecretsBackend(v) | |
| 107 | 121 | switch backend { | |
| @@ -21,10 +21,10 @@ import ( | |||||
| 21 | 21 | ||||
| 22 | // LocalServices provides direct access to local services for CLI operations. | 22 | // LocalServices provides direct access to local services for CLI operations. | ||
| 23 | type LocalServices struct { | 23 | type LocalServices struct { | ||
| 24 | - | configSvc in.ConfigService | 24 | + | configSvc in.ConfigService |
| 25 | - | secretSvc in.SecretService | 25 | + | secretSvc in.SecretService |
| 26 | - | dataDir string | 26 | + | dataDir string |
| 27 | - | tlsPort int | 27 | + | tlsEnabled bool |
| 28 | } | 28 | } | ||
| 29 | 29 | ||||
| 30 | // GetConfigService returns the config service. | 30 | // GetConfigService returns the config service. | ||
| @@ -42,9 +42,9 @@ func (l *LocalServices) GetDataDir() string { | |||||
| 42 | return l.dataDir | 42 | return l.dataDir | ||
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | - | // GetTLSPort returns the configured TLS port (0 means internal TLS is disabled). | 45 | + | // HasInternalTLS reports whether a TLS-capable entrypoint enables the internal CA. |
| 46 | - | func (l *LocalServices) GetTLSPort() int { | 46 | + | func (l *LocalServices) HasInternalTLS() bool { |
| 47 | - | return l.tlsPort | 47 | + | return l.tlsEnabled |
| 48 | } | 48 | } | ||
| 49 | 49 | ||||
| 50 | // GetLocalServices creates local services for CLI operations. | 50 | // GetLocalServices creates local services for CLI operations. | ||
| @@ -95,13 +95,27 @@ func GetLocalServices(configPath string) (*LocalServices, error) { | |||||
| 95 | secretSvc := secretsSvc.NewService(domainSecretStore, log, nil) | 95 | secretSvc := secretsSvc.NewService(domainSecretStore, log, nil) | ||
| 96 | 96 | ||||
| 97 | return &LocalServices{ | 97 | return &LocalServices{ | ||
| 98 | - | configSvc: configSvc, | 98 | + | configSvc: configSvc, |
| 99 | - | secretSvc: secretSvc, | 99 | + | secretSvc: secretSvc, |
| 100 | - | dataDir: dataDir, | 100 | + | dataDir: dataDir, |
| 101 | - | tlsPort: v.GetInt("server.tls_port"), | 101 | + | tlsEnabled: hasLocalTLSCapableEntrypoint(v), |
| 102 | }, nil | 102 | }, nil | ||
| 103 | } | 103 | } | ||
| 104 | 104 | ||||
| 105 | + | func hasLocalTLSCapableEntrypoint(v *viper.Viper) bool { | |||
| 106 | + | for _, rawEntryPoint := range v.GetStringMap("entrypoints") { | |||
| 107 | + | entryPoint, ok := rawEntryPoint.(map[string]any) | |||
| 108 | + | if !ok { | |||
| 109 | + | continue | |||
| 110 | + | } | |||
| 111 | + | switch strings.TrimSpace(fmt.Sprint(entryPoint["protocol"])) { | |||
| 112 | + | case "smart_tcp", "tls_mux": | |||
| 113 | + | return true | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | return false | |||
| 117 | + | } | |||
| 118 | + | ||||
| 105 | func createLocalDomainSecretStore(v *viper.Viper, envDir string, log zerowrap.Logger) (out.DomainSecretStore, error) { | 119 | func createLocalDomainSecretStore(v *viper.Viper, envDir string, log zerowrap.Logger) (out.DomainSecretStore, error) { | ||
| 106 | backend := resolveLocalSecretsBackend(v) | 120 | backend := resolveLocalSecretsBackend(v) | ||
| 107 | switch backend { | 121 | switch backend { | ||
M internal/adapters/in/cli/local_test.go +19 -0
| @@ -38,6 +38,25 @@ func TestResolveLocalSecretsBackend(t *testing.T) { | |||
| 38 | 38 | }) | |
| 39 | 39 | } | |
| 40 | 40 | ||
| 41 | + | func TestHasLocalTLSCapableEntrypoint(t *testing.T) { | |
| 42 | + | for _, tt := range []struct { | |
| 43 | + | name string | |
| 44 | + | config map[string]any | |
| 45 | + | want bool | |
| 46 | + | }{ | |
| 47 | + | {name: "smart tcp", config: map[string]any{"edge": map[string]any{"protocol": "smart_tcp"}}, want: true}, | |
| 48 | + | {name: "tls mux", config: map[string]any{"edge": map[string]any{"protocol": "tls_mux"}}, want: true}, | |
| 49 | + | {name: "plain tcp", config: map[string]any{"edge": map[string]any{"protocol": "tcp"}}, want: false}, | |
| 50 | + | {name: "empty", config: map[string]any{}, want: false}, | |
| 51 | + | } { | |
| 52 | + | t.Run(tt.name, func(t *testing.T) { | |
| 53 | + | v := viper.New() | |
| 54 | + | v.Set("entrypoints", tt.config) | |
| 55 | + | require.Equal(t, tt.want, hasLocalTLSCapableEntrypoint(v)) | |
| 56 | + | }) | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | ||
| 41 | 60 | func TestCreateLocalDomainSecretStore_UsesPassStoreForPass(t *testing.T) { | |
| 42 | 61 | // pass(1) must be available on the system for this test | |
| 43 | 62 | if _, err := domainsecrets.NewPassStore(zerowrap.New(zerowrap.Config{Level: "error", Format: "console"})); err != nil { | |
| @@ -38,6 +38,25 @@ func TestResolveLocalSecretsBackend(t *testing.T) { | |||||
| 38 | }) | 38 | }) | ||
| 39 | } | 39 | } | ||
| 40 | 40 | ||||
| 41 | + | func TestHasLocalTLSCapableEntrypoint(t *testing.T) { | |||
| 42 | + | for _, tt := range []struct { | |||
| 43 | + | name string | |||
| 44 | + | config map[string]any | |||
| 45 | + | want bool | |||
| 46 | + | }{ | |||
| 47 | + | {name: "smart tcp", config: map[string]any{"edge": map[string]any{"protocol": "smart_tcp"}}, want: true}, | |||
| 48 | + | {name: "tls mux", config: map[string]any{"edge": map[string]any{"protocol": "tls_mux"}}, want: true}, | |||
| 49 | + | {name: "plain tcp", config: map[string]any{"edge": map[string]any{"protocol": "tcp"}}, want: false}, | |||
| 50 | + | {name: "empty", config: map[string]any{}, want: false}, | |||
| 51 | + | } { | |||
| 52 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 53 | + | v := viper.New() | |||
| 54 | + | v.Set("entrypoints", tt.config) | |||
| 55 | + | require.Equal(t, tt.want, hasLocalTLSCapableEntrypoint(v)) | |||
| 56 | + | }) | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | ||||
| 41 | func TestCreateLocalDomainSecretStore_UsesPassStoreForPass(t *testing.T) { | 60 | func TestCreateLocalDomainSecretStore_UsesPassStoreForPass(t *testing.T) { | ||
| 42 | // pass(1) must be available on the system for this test | 61 | // pass(1) must be available on the system for this test | ||
| 43 | if _, err := domainsecrets.NewPassStore(zerowrap.New(zerowrap.Config{Level: "error", Format: "console"})); err != nil { | 62 | if _, err := domainsecrets.NewPassStore(zerowrap.New(zerowrap.Config{Level: "error", Format: "console"})); err != nil { | ||
M internal/adapters/in/cli/push_image_ops.go +3 -3
| @@ -11,7 +11,7 @@ import ( | |||
| 11 | 11 | "strings" | |
| 12 | 12 | ||
| 13 | 13 | cerrdefs "github.com/containerd/errdefs" | |
| 14 | - | "github.com/docker/docker/client" | |
| 14 | + | "github.com/moby/moby/client" | |
| 15 | 15 | ||
| 16 | 16 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | |
| 17 | 17 | "github.com/bnema/gordon/pkg/registrypush" | |
| @@ -41,7 +41,7 @@ type dockerImageOps struct { | |||
| 41 | 41 | ||
| 42 | 42 | // newDockerImageOps creates a dockerImageOps from the Docker environment. | |
| 43 | 43 | func newDockerImageOps(insecureTLS bool, progress io.Writer) (*dockerImageOps, error) { | |
| 44 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |
| 44 | + | cli, err := client.New(client.FromEnv) | |
| 45 | 45 | if err != nil { | |
| 46 | 46 | return nil, fmt.Errorf("failed to create Docker client: %w", err) | |
| 47 | 47 | } | |
| @@ -77,7 +77,7 @@ func newImageOpsForResolvedRemote(resolved *remote.ResolvedRemote) (pushImageOps | |||
| 77 | 77 | } | |
| 78 | 78 | ||
| 79 | 79 | func (d *dockerImageOps) Tag(ctx context.Context, sourceRef, targetRef string) error { | |
| 80 | - | if err := d.cli.ImageTag(ctx, sourceRef, targetRef); err != nil { | |
| 80 | + | if _, err := d.cli.ImageTag(ctx, client.ImageTagOptions{Source: sourceRef, Target: targetRef}); err != nil { | |
| 81 | 81 | return fmt.Errorf("failed to tag %s as %s: %w", sourceRef, targetRef, err) | |
| 82 | 82 | } | |
| 83 | 83 | return nil | |
| @@ -11,7 +11,7 @@ import ( | |||||
| 11 | "strings" | 11 | "strings" | ||
| 12 | 12 | ||||
| 13 | cerrdefs "github.com/containerd/errdefs" | 13 | cerrdefs "github.com/containerd/errdefs" | ||
| 14 | - | "github.com/docker/docker/client" | 14 | + | "github.com/moby/moby/client" |
| 15 | 15 | ||||
| 16 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | 16 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | ||
| 17 | "github.com/bnema/gordon/pkg/registrypush" | 17 | "github.com/bnema/gordon/pkg/registrypush" | ||
| @@ -41,7 +41,7 @@ type dockerImageOps struct { | |||||
| 41 | 41 | ||||
| 42 | // newDockerImageOps creates a dockerImageOps from the Docker environment. | 42 | // newDockerImageOps creates a dockerImageOps from the Docker environment. | ||
| 43 | func newDockerImageOps(insecureTLS bool, progress io.Writer) (*dockerImageOps, error) { | 43 | func newDockerImageOps(insecureTLS bool, progress io.Writer) (*dockerImageOps, error) { | ||
| 44 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | 44 | + | cli, err := client.New(client.FromEnv) |
| 45 | if err != nil { | 45 | if err != nil { | ||
| 46 | return nil, fmt.Errorf("failed to create Docker client: %w", err) | 46 | return nil, fmt.Errorf("failed to create Docker client: %w", err) | ||
| 47 | } | 47 | } | ||
| @@ -77,7 +77,7 @@ func newImageOpsForResolvedRemote(resolved *remote.ResolvedRemote) (pushImageOps | |||||
| 77 | } | 77 | } | ||
| 78 | 78 | ||||
| 79 | func (d *dockerImageOps) Tag(ctx context.Context, sourceRef, targetRef string) error { | 79 | func (d *dockerImageOps) Tag(ctx context.Context, sourceRef, targetRef string) error { | ||
| 80 | - | if err := d.cli.ImageTag(ctx, sourceRef, targetRef); err != nil { | 80 | + | if _, err := d.cli.ImageTag(ctx, client.ImageTagOptions{Source: sourceRef, Target: targetRef}); err != nil { |
| 81 | return fmt.Errorf("failed to tag %s as %s: %w", sourceRef, targetRef, err) | 81 | return fmt.Errorf("failed to tag %s as %s: %w", sourceRef, targetRef, err) | ||
| 82 | } | 82 | } | ||
| 83 | return nil | 83 | return nil | ||
M internal/adapters/out/docker/network_test.go +5 -5
| @@ -7,7 +7,7 @@ import ( | |||
| 7 | 7 | "strings" | |
| 8 | 8 | "testing" | |
| 9 | 9 | ||
| 10 | - | "github.com/docker/docker/client" | |
| 10 | + | "github.com/moby/moby/client" | |
| 11 | 11 | "github.com/stretchr/testify/assert" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| @@ -28,7 +28,7 @@ func TestRuntime_GetContainerNetwork(t *testing.T) { | |||
| 28 | 28 | defer server.Close() | |
| 29 | 29 | ||
| 30 | 30 | host := strings.TrimPrefix(server.URL, "http://") | |
| 31 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 31 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 32 | 32 | if err != nil { | |
| 33 | 33 | t.Fatalf("failed to create docker client: %v", err) | |
| 34 | 34 | } | |
| @@ -57,7 +57,7 @@ func TestRuntime_GetContainerNetwork_Fallback(t *testing.T) { | |||
| 57 | 57 | defer server.Close() | |
| 58 | 58 | ||
| 59 | 59 | host := strings.TrimPrefix(server.URL, "http://") | |
| 60 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 60 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 61 | 61 | if err != nil { | |
| 62 | 62 | t.Fatalf("failed to create docker client: %v", err) | |
| 63 | 63 | } | |
| @@ -83,7 +83,7 @@ func TestRuntime_GetContainerNetwork_EmptyNetworks(t *testing.T) { | |||
| 83 | 83 | defer server.Close() | |
| 84 | 84 | ||
| 85 | 85 | host := strings.TrimPrefix(server.URL, "http://") | |
| 86 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 86 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 87 | 87 | if err != nil { | |
| 88 | 88 | t.Fatalf("failed to create docker client: %v", err) | |
| 89 | 89 | } | |
| @@ -105,7 +105,7 @@ func TestRuntime_GetContainerNetwork_NilNetworkSettings(t *testing.T) { | |||
| 105 | 105 | defer server.Close() | |
| 106 | 106 | ||
| 107 | 107 | host := strings.TrimPrefix(server.URL, "http://") | |
| 108 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 108 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 109 | 109 | if err != nil { | |
| 110 | 110 | t.Fatalf("failed to create docker client: %v", err) | |
| 111 | 111 | } | |
| @@ -7,7 +7,7 @@ import ( | |||||
| 7 | "strings" | 7 | "strings" | ||
| 8 | "testing" | 8 | "testing" | ||
| 9 | 9 | ||||
| 10 | - | "github.com/docker/docker/client" | 10 | + | "github.com/moby/moby/client" |
| 11 | "github.com/stretchr/testify/assert" | 11 | "github.com/stretchr/testify/assert" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
| @@ -28,7 +28,7 @@ func TestRuntime_GetContainerNetwork(t *testing.T) { | |||||
| 28 | defer server.Close() | 28 | defer server.Close() | ||
| 29 | 29 | ||||
| 30 | host := strings.TrimPrefix(server.URL, "http://") | 30 | host := strings.TrimPrefix(server.URL, "http://") | ||
| 31 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | 31 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) |
| 32 | if err != nil { | 32 | if err != nil { | ||
| 33 | t.Fatalf("failed to create docker client: %v", err) | 33 | t.Fatalf("failed to create docker client: %v", err) | ||
| 34 | } | 34 | } | ||
| @@ -57,7 +57,7 @@ func TestRuntime_GetContainerNetwork_Fallback(t *testing.T) { | |||||
| 57 | defer server.Close() | 57 | defer server.Close() | ||
| 58 | 58 | ||||
| 59 | host := strings.TrimPrefix(server.URL, "http://") | 59 | host := strings.TrimPrefix(server.URL, "http://") | ||
| 60 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | 60 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) |
| 61 | if err != nil { | 61 | if err != nil { | ||
| 62 | t.Fatalf("failed to create docker client: %v", err) | 62 | t.Fatalf("failed to create docker client: %v", err) | ||
| 63 | } | 63 | } | ||
| @@ -83,7 +83,7 @@ func TestRuntime_GetContainerNetwork_EmptyNetworks(t *testing.T) { | |||||
| 83 | defer server.Close() | 83 | defer server.Close() | ||
| 84 | 84 | ||||
| 85 | host := strings.TrimPrefix(server.URL, "http://") | 85 | host := strings.TrimPrefix(server.URL, "http://") | ||
| 86 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | 86 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) |
| 87 | if err != nil { | 87 | if err != nil { | ||
| 88 | t.Fatalf("failed to create docker client: %v", err) | 88 | t.Fatalf("failed to create docker client: %v", err) | ||
| 89 | } | 89 | } | ||
| @@ -105,7 +105,7 @@ func TestRuntime_GetContainerNetwork_NilNetworkSettings(t *testing.T) { | |||||
| 105 | defer server.Close() | 105 | defer server.Close() | ||
| 106 | 106 | ||||
| 107 | host := strings.TrimPrefix(server.URL, "http://") | 107 | host := strings.TrimPrefix(server.URL, "http://") | ||
| 108 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | 108 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) |
| 109 | if err != nil { | 109 | if err != nil { | ||
| 110 | t.Fatalf("failed to create docker client: %v", err) | 110 | t.Fatalf("failed to create docker client: %v", err) | ||
| 111 | } | 111 | } | ||
M internal/adapters/out/docker/runtime.go +154 -116
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "io" | |
| 14 | 14 | "maps" | |
| 15 | 15 | "math" | |
| 16 | + | "net/netip" | |
| 16 | 17 | "os" | |
| 17 | 18 | "path/filepath" | |
| 18 | 19 | "sort" | |
| @@ -23,18 +24,13 @@ import ( | |||
| 23 | 24 | ||
| 24 | 25 | "github.com/bnema/zerowrap" | |
| 25 | 26 | cerrdefs "github.com/containerd/errdefs" | |
| 26 | - | "github.com/docker/docker/api/types/container" | |
| 27 | - | "github.com/docker/docker/api/types/filters" | |
| 28 | - | "github.com/docker/docker/api/types/image" | |
| 29 | - | "github.com/docker/docker/api/types/mount" | |
| 30 | - | "github.com/docker/docker/api/types/network" | |
| 31 | - | "github.com/docker/docker/api/types/registry" | |
| 32 | - | "github.com/docker/docker/api/types/strslice" | |
| 33 | - | "github.com/docker/docker/api/types/volume" | |
| 34 | - | "github.com/docker/docker/client" | |
| 35 | - | "github.com/docker/docker/pkg/stdcopy" | |
| 36 | - | "github.com/docker/go-connections/nat" | |
| 37 | 27 | "github.com/docker/go-units" | |
| 28 | + | "github.com/moby/moby/api/pkg/stdcopy" | |
| 29 | + | "github.com/moby/moby/api/types/container" | |
| 30 | + | "github.com/moby/moby/api/types/mount" | |
| 31 | + | "github.com/moby/moby/api/types/network" | |
| 32 | + | "github.com/moby/moby/api/types/registry" | |
| 33 | + | "github.com/moby/moby/client" | |
| 38 | 34 | ||
| 39 | 35 | "github.com/bnema/gordon/internal/boundaries/out" | |
| 40 | 36 | "github.com/bnema/gordon/internal/domain" | |
| @@ -100,7 +96,7 @@ func (f *removeOnCloseFile) Close() error { | |||
| 100 | 96 | ||
| 101 | 97 | // NewRuntime creates a new Docker runtime instance. | |
| 102 | 98 | func NewRuntime() (*Runtime, error) { | |
| 103 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |
| 99 | + | cli, err := client.New(client.FromEnv) | |
| 104 | 100 | if err != nil { | |
| 105 | 101 | return nil, fmt.Errorf("failed to create Docker client: %w", err) | |
| 106 | 102 | } | |
| @@ -113,10 +109,7 @@ func NewRuntime() (*Runtime, error) { | |||
| 113 | 109 | // NewRuntimeWithSocket creates a Docker runtime targeting a specific socket path. | |
| 114 | 110 | func NewRuntimeWithSocket(socketPath string) (*Runtime, error) { | |
| 115 | 111 | host := "unix://" + socketPath | |
| 116 | - | cli, err := client.NewClientWithOpts( | |
| 117 | - | client.WithHost(host), | |
| 118 | - | client.WithAPIVersionNegotiation(), | |
| 119 | - | ) | |
| 112 | + | cli, err := client.New(client.WithHost(host)) | |
| 120 | 113 | if err != nil { | |
| 121 | 114 | return nil, fmt.Errorf("failed to create Docker client for %s: %w", socketPath, err) | |
| 122 | 115 | } | |
| @@ -170,13 +163,13 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC | |||
| 170 | 163 | if config.PidsLimit > 0 { | |
| 171 | 164 | resources.PidsLimit = &config.PidsLimit | |
| 172 | 165 | } | |
| 173 | - | capDrop := strslice.StrSlice{"ALL"} | |
| 174 | - | capAdd := strslice.StrSlice{"CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID", "NET_BIND_SERVICE"} | |
| 166 | + | capDrop := []string{"ALL"} | |
| 167 | + | capAdd := []string{"CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID", "NET_BIND_SERVICE"} | |
| 175 | 168 | if config.CapDrop != nil { | |
| 176 | - | capDrop = strslice.StrSlice(config.CapDrop) | |
| 169 | + | capDrop = config.CapDrop | |
| 177 | 170 | } | |
| 178 | 171 | if config.CapAdd != nil { | |
| 179 | - | capAdd = strslice.StrSlice(config.CapAdd) | |
| 172 | + | capAdd = config.CapAdd | |
| 180 | 173 | } | |
| 181 | 174 | hostConfig := &container.HostConfig{ | |
| 182 | 175 | PortBindings: portBindings, | |
| @@ -208,8 +201,13 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC | |||
| 208 | 201 | } | |
| 209 | 202 | } | |
| 210 | 203 | ||
| 211 | - | // Create the container | |
| 212 | - | resp, err := r.client.ContainerCreate(ctx, containerConfig, hostConfig, networkConfig, nil, config.Name) | |
| 204 | + | // Create the container. | |
| 205 | + | resp, err := r.client.ContainerCreate(ctx, client.ContainerCreateOptions{ | |
| 206 | + | Config: containerConfig, | |
| 207 | + | HostConfig: hostConfig, | |
| 208 | + | NetworkingConfig: networkConfig, | |
| 209 | + | Name: config.Name, | |
| 210 | + | }) | |
| 213 | 211 | if err != nil { | |
| 214 | 212 | return nil, log.WrapErr(err, "failed to create container") | |
| 215 | 213 | } | |
| @@ -220,38 +218,54 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC | |||
| 220 | 218 | return r.InspectContainer(ctx, resp.ID) | |
| 221 | 219 | } | |
| 222 | 220 | ||
| 223 | - | func buildPortBindings(config *domain.ContainerConfig) (nat.PortSet, nat.PortMap, error) { | |
| 224 | - | exposedPorts := make(nat.PortSet) | |
| 225 | - | portBindings := make(nat.PortMap) | |
| 226 | - | addLegacyPortBindings(config.Ports, exposedPorts, portBindings) | |
| 221 | + | func buildPortBindings(config *domain.ContainerConfig) (network.PortSet, network.PortMap, error) { | |
| 222 | + | exposedPorts := make(network.PortSet) | |
| 223 | + | portBindings := make(network.PortMap) | |
| 224 | + | if err := addLegacyPortBindings(config.Ports, exposedPorts, portBindings); err != nil { | |
| 225 | + | return nil, nil, err | |
| 226 | + | } | |
| 227 | 227 | if err := addExplicitPortBindings(config.PortPublishes, exposedPorts, portBindings); err != nil { | |
| 228 | 228 | return nil, nil, err | |
| 229 | 229 | } | |
| 230 | 230 | return exposedPorts, portBindings, nil | |
| 231 | 231 | } | |
| 232 | 232 | ||
| 233 | - | func addLegacyPortBindings(ports []int, exposedPorts nat.PortSet, portBindings nat.PortMap) { | |
| 233 | + | func addLegacyPortBindings(ports []int, exposedPorts network.PortSet, portBindings network.PortMap) error { | |
| 234 | 234 | for _, port := range ports { | |
| 235 | - | containerPort := nat.Port(fmt.Sprintf("%d/tcp", port)) | |
| 235 | + | containerPort, err := network.ParsePort(fmt.Sprintf("%d/tcp", port)) | |
| 236 | + | if err != nil { | |
| 237 | + | return fmt.Errorf("invalid container port %d: %w", port, err) | |
| 238 | + | } | |
| 236 | 239 | exposedPorts[containerPort] = struct{}{} | |
| 237 | 240 | ||
| 238 | 241 | // Bind to random available port on localhost only. | |
| 239 | 242 | // SECURITY: Using 127.0.0.1 prevents direct access from the network, | |
| 240 | 243 | // forcing all traffic through Gordon's reverse proxy where auth, | |
| 241 | 244 | // rate limiting, and security headers are applied. | |
| 242 | - | portBindings[containerPort] = []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: "0"}} | |
| 245 | + | portBindings[containerPort] = []network.PortBinding{{HostIP: netip.MustParseAddr("127.0.0.1"), HostPort: "0"}} | |
| 243 | 246 | } | |
| 247 | + | return nil | |
| 244 | 248 | } | |
| 245 | 249 | ||
| 246 | - | func addExplicitPortBindings(publishes []domain.ContainerPortPublish, exposedPorts nat.PortSet, portBindings nat.PortMap) error { | |
| 250 | + | func addExplicitPortBindings(publishes []domain.ContainerPortPublish, exposedPorts network.PortSet, portBindings network.PortMap) error { | |
| 247 | 251 | for _, publish := range publishes { | |
| 248 | 252 | if publish.Protocol != domain.NetworkProtocolTCP && publish.Protocol != domain.NetworkProtocolUDP { | |
| 249 | 253 | return fmt.Errorf("unsupported container port publish protocol %q", publish.Protocol) | |
| 250 | 254 | } | |
| 251 | - | containerPort := nat.Port(fmt.Sprintf("%d/%s", publish.ContainerPort, publish.Protocol)) | |
| 255 | + | containerPort, err := network.ParsePort(fmt.Sprintf("%d/%s", publish.ContainerPort, publish.Protocol)) | |
| 256 | + | if err != nil { | |
| 257 | + | return fmt.Errorf("invalid container port %d: %w", publish.ContainerPort, err) | |
| 258 | + | } | |
| 259 | + | var hostIP netip.Addr | |
| 260 | + | if publish.HostIP != "" { | |
| 261 | + | hostIP, err = netip.ParseAddr(publish.HostIP) | |
| 262 | + | if err != nil { | |
| 263 | + | return fmt.Errorf("invalid host IP %q: %w", publish.HostIP, err) | |
| 264 | + | } | |
| 265 | + | } | |
| 252 | 266 | exposedPorts[containerPort] = struct{}{} | |
| 253 | - | portBindings[containerPort] = append(portBindings[containerPort], nat.PortBinding{ | |
| 254 | - | HostIP: publish.HostIP, | |
| 267 | + | portBindings[containerPort] = append(portBindings[containerPort], network.PortBinding{ | |
| 268 | + | HostIP: hostIP, | |
| 255 | 269 | HostPort: strconv.Itoa(publish.HostPort), | |
| 256 | 270 | }) | |
| 257 | 271 | } | |
| @@ -283,7 +297,7 @@ func (r *Runtime) StartContainer(ctx context.Context, containerID string) error | |||
| 283 | 297 | }) | |
| 284 | 298 | log := zerowrap.FromCtx(ctx) | |
| 285 | 299 | ||
| 286 | - | err := r.client.ContainerStart(ctx, containerID, container.StartOptions{}) | |
| 300 | + | _, err := r.client.ContainerStart(ctx, containerID, client.ContainerStartOptions{}) | |
| 287 | 301 | if err != nil { | |
| 288 | 302 | return log.WrapErr(err, "failed to start container") | |
| 289 | 303 | } | |
| @@ -302,7 +316,8 @@ func (r *Runtime) WaitForContainer(ctx context.Context, containerID string) erro | |||
| 302 | 316 | }) | |
| 303 | 317 | log := zerowrap.FromCtx(ctx) | |
| 304 | 318 | ||
| 305 | - | statusCh, errCh := r.client.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) | |
| 319 | + | waitResult := r.client.ContainerWait(ctx, containerID, client.ContainerWaitOptions{Condition: container.WaitConditionNotRunning}) | |
| 320 | + | statusCh, errCh := waitResult.Result, waitResult.Error | |
| 306 | 321 | select { | |
| 307 | 322 | case err := <-errCh: | |
| 308 | 323 | if err != nil { | |
| @@ -326,7 +341,7 @@ func (r *Runtime) StopContainer(ctx context.Context, containerID string) error { | |||
| 326 | 341 | log := zerowrap.FromCtx(ctx) | |
| 327 | 342 | ||
| 328 | 343 | timeout := 20 // 20 seconds before SIGKILL | |
| 329 | - | err := r.client.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}) | |
| 344 | + | _, err := r.client.ContainerStop(ctx, containerID, client.ContainerStopOptions{Timeout: &timeout}) | |
| 330 | 345 | if err != nil { | |
| 331 | 346 | return log.WrapErr(err, "failed to stop container") | |
| 332 | 347 | } | |
| @@ -346,7 +361,7 @@ func (r *Runtime) RestartContainer(ctx context.Context, containerID string) erro | |||
| 346 | 361 | log := zerowrap.FromCtx(ctx) | |
| 347 | 362 | ||
| 348 | 363 | timeout := 20 // 20 seconds before SIGKILL | |
| 349 | - | err := r.client.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeout}) | |
| 364 | + | _, err := r.client.ContainerRestart(ctx, containerID, client.ContainerRestartOptions{Timeout: &timeout}) | |
| 350 | 365 | if err != nil { | |
| 351 | 366 | return log.WrapErr(err, "failed to restart container") | |
| 352 | 367 | } | |
| @@ -366,7 +381,7 @@ func (r *Runtime) RemoveContainer(ctx context.Context, containerID string, force | |||
| 366 | 381 | }) | |
| 367 | 382 | log := zerowrap.FromCtx(ctx) | |
| 368 | 383 | ||
| 369 | - | err := r.client.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: force}) | |
| 384 | + | _, err := r.client.ContainerRemove(ctx, containerID, client.ContainerRemoveOptions{Force: force}) | |
| 370 | 385 | if err != nil { | |
| 371 | 386 | return log.WrapErr(err, "failed to remove container") | |
| 372 | 387 | } | |
| @@ -386,7 +401,7 @@ func (r *Runtime) RenameContainer(ctx context.Context, containerID, newName stri | |||
| 386 | 401 | }) | |
| 387 | 402 | log := zerowrap.FromCtx(ctx) | |
| 388 | 403 | ||
| 389 | - | err := r.client.ContainerRename(ctx, containerID, newName) | |
| 404 | + | _, err := r.client.ContainerRename(ctx, containerID, client.ContainerRenameOptions{NewName: newName}) | |
| 390 | 405 | if err != nil { | |
| 391 | 406 | return log.WrapErr(err, "failed to rename container") | |
| 392 | 407 | } | |
| @@ -405,13 +420,13 @@ func (r *Runtime) ListContainers(ctx context.Context, all bool) ([]*domain.Conta | |||
| 405 | 420 | }) | |
| 406 | 421 | log := zerowrap.FromCtx(ctx) | |
| 407 | 422 | ||
| 408 | - | containers, err := r.client.ContainerList(ctx, container.ListOptions{All: all}) | |
| 423 | + | containerList, err := r.client.ContainerList(ctx, client.ContainerListOptions{All: all}) | |
| 409 | 424 | if err != nil { | |
| 410 | 425 | return nil, log.WrapErr(err, "failed to list containers") | |
| 411 | 426 | } | |
| 412 | 427 | ||
| 413 | 428 | var result []*domain.Container | |
| 414 | - | for _, c := range containers { | |
| 429 | + | for _, c := range containerList.Items { | |
| 415 | 430 | // Extract ports | |
| 416 | 431 | var ports []int | |
| 417 | 432 | for _, port := range c.Ports { | |
| @@ -442,7 +457,7 @@ func (r *Runtime) ListContainers(ctx context.Context, all bool) ([]*domain.Conta | |||
| 442 | 457 | Image: c.Image, | |
| 443 | 458 | ImageID: c.ImageID, | |
| 444 | 459 | Name: name, | |
| 445 | - | Status: c.State, // Use State (e.g., "running") not Status (e.g., "Up 2 days") | |
| 460 | + | Status: string(c.State), // Use State (e.g., "running") not Status (e.g., "Up 2 days") | |
| 446 | 461 | Ports: ports, | |
| 447 | 462 | Labels: c.Labels, | |
| 448 | 463 | VolumeMounts: volumeMounts, | |
| @@ -463,10 +478,11 @@ func (r *Runtime) InspectContainer(ctx context.Context, containerID string) (*do | |||
| 463 | 478 | }) | |
| 464 | 479 | log := zerowrap.FromCtx(ctx) | |
| 465 | 480 | ||
| 466 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | |
| 481 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) | |
| 467 | 482 | if err != nil { | |
| 468 | 483 | return nil, log.WrapErr(err, "failed to inspect container") | |
| 469 | 484 | } | |
| 485 | + | resp := inspectResult.Container | |
| 470 | 486 | ||
| 471 | 487 | // Extract published ports | |
| 472 | 488 | var ports []int | |
| @@ -502,7 +518,7 @@ func (r *Runtime) InspectContainer(ctx context.Context, containerID string) (*do | |||
| 502 | 518 | Image: resp.Config.Image, | |
| 503 | 519 | ImageID: resp.Image, | |
| 504 | 520 | Name: name, | |
| 505 | - | Status: resp.State.Status, | |
| 521 | + | Status: string(resp.State.Status), | |
| 506 | 522 | ExitCode: resp.State.ExitCode, | |
| 507 | 523 | Ports: ports, | |
| 508 | 524 | Labels: resp.Config.Labels, | |
| @@ -521,7 +537,7 @@ func (r *Runtime) GetContainerLogs(ctx context.Context, containerID string, foll | |||
| 521 | 537 | }) | |
| 522 | 538 | log := zerowrap.FromCtx(ctx) | |
| 523 | 539 | ||
| 524 | - | logs, err := r.client.ContainerLogs(ctx, containerID, container.LogsOptions{ | |
| 540 | + | logs, err := r.client.ContainerLogs(ctx, containerID, client.ContainerLogsOptions{ | |
| 525 | 541 | ShowStdout: true, | |
| 526 | 542 | ShowStderr: true, | |
| 527 | 543 | Follow: follow, | |
| @@ -547,7 +563,7 @@ func (r *Runtime) PullImage(ctx context.Context, imageRef string) error { | |||
| 547 | 563 | ||
| 548 | 564 | log.Info().Msg("pulling image") | |
| 549 | 565 | ||
| 550 | - | reader, err := r.client.ImagePull(ctx, imageRef, image.PullOptions{}) | |
| 566 | + | reader, err := r.client.ImagePull(ctx, imageRef, client.ImagePullOptions{}) | |
| 551 | 567 | if err != nil { | |
| 552 | 568 | return log.WrapErr(fmt.Errorf("%w: %w", domain.ErrImagePullFailed, err), "failed to pull image") | |
| 553 | 569 | } | |
| @@ -605,7 +621,7 @@ func (r *Runtime) PullImageWithAuth(ctx context.Context, imageRef, username, pas | |||
| 605 | 621 | Msg("auth config for pull") | |
| 606 | 622 | ||
| 607 | 623 | // Pull with authentication | |
| 608 | - | reader, err := r.client.ImagePull(ctx, imageRef, image.PullOptions{ | |
| 624 | + | reader, err := r.client.ImagePull(ctx, imageRef, client.ImagePullOptions{ | |
| 609 | 625 | RegistryAuth: authStr, | |
| 610 | 626 | }) | |
| 611 | 627 | if err != nil { | |
| @@ -634,7 +650,7 @@ func (r *Runtime) TagImage(ctx context.Context, sourceRef, targetRef string) err | |||
| 634 | 650 | }) | |
| 635 | 651 | log := zerowrap.FromCtx(ctx) | |
| 636 | 652 | ||
| 637 | - | if err := r.client.ImageTag(ctx, sourceRef, targetRef); err != nil { | |
| 653 | + | if _, err := r.client.ImageTag(ctx, client.ImageTagOptions{Source: sourceRef, Target: targetRef}); err != nil { | |
| 638 | 654 | return log.WrapErr(err, "failed to tag image") | |
| 639 | 655 | } | |
| 640 | 656 | ||
| @@ -653,7 +669,7 @@ func (r *Runtime) UntagImage(ctx context.Context, imageRef string) error { | |||
| 653 | 669 | log := zerowrap.FromCtx(ctx) | |
| 654 | 670 | ||
| 655 | 671 | // ImageRemove with PruneChildren=false only removes the tag, not the image layers | |
| 656 | - | _, err := r.client.ImageRemove(ctx, imageRef, image.RemoveOptions{ | |
| 672 | + | _, err := r.client.ImageRemove(ctx, imageRef, client.ImageRemoveOptions{ | |
| 657 | 673 | Force: false, | |
| 658 | 674 | PruneChildren: false, | |
| 659 | 675 | }) | |
| @@ -676,7 +692,7 @@ func (r *Runtime) RemoveImage(ctx context.Context, imageRef string, force bool) | |||
| 676 | 692 | }) | |
| 677 | 693 | log := zerowrap.FromCtx(ctx) | |
| 678 | 694 | ||
| 679 | - | _, err := r.client.ImageRemove(ctx, imageRef, image.RemoveOptions{Force: force}) | |
| 695 | + | _, err := r.client.ImageRemove(ctx, imageRef, client.ImageRemoveOptions{Force: force}) | |
| 680 | 696 | if err != nil { | |
| 681 | 697 | return log.WrapErr(err, "failed to remove image") | |
| 682 | 698 | } | |
| @@ -694,13 +710,13 @@ func (r *Runtime) ListImages(ctx context.Context) ([]string, error) { | |||
| 694 | 710 | }) | |
| 695 | 711 | log := zerowrap.FromCtx(ctx) | |
| 696 | 712 | ||
| 697 | - | images, err := r.client.ImageList(ctx, image.ListOptions{}) | |
| 713 | + | imageList, err := r.client.ImageList(ctx, client.ImageListOptions{}) | |
| 698 | 714 | if err != nil { | |
| 699 | 715 | return nil, log.WrapErr(err, "failed to list images") | |
| 700 | 716 | } | |
| 701 | 717 | ||
| 702 | 718 | var result []string | |
| 703 | - | for _, img := range images { | |
| 719 | + | for _, img := range imageList.Items { | |
| 704 | 720 | for _, tag := range img.RepoTags { | |
| 705 | 721 | if tag != "<none>:<none>" { | |
| 706 | 722 | result = append(result, tag) | |
| @@ -720,13 +736,13 @@ func (r *Runtime) ListImagesDetailed(ctx context.Context) ([]runtimepkg.ImageDet | |||
| 720 | 736 | }) | |
| 721 | 737 | log := zerowrap.FromCtx(ctx) | |
| 722 | 738 | ||
| 723 | - | images, err := r.client.ImageList(ctx, image.ListOptions{All: true}) | |
| 739 | + | imageList, err := r.client.ImageList(ctx, client.ImageListOptions{All: true}) | |
| 724 | 740 | if err != nil { | |
| 725 | 741 | return nil, log.WrapErr(err, "failed to list detailed images") | |
| 726 | 742 | } | |
| 727 | 743 | ||
| 728 | - | result := make([]runtimepkg.ImageDetail, 0, len(images)) | |
| 729 | - | for _, img := range images { | |
| 744 | + | result := make([]runtimepkg.ImageDetail, 0, len(imageList.Items)) | |
| 745 | + | for _, img := range imageList.Items { | |
| 730 | 746 | result = append(result, runtimepkg.ImageDetail{ | |
| 731 | 747 | ID: img.ID, | |
| 732 | 748 | RepoTags: img.RepoTags, | |
| @@ -748,19 +764,18 @@ func (r *Runtime) PruneImages(ctx context.Context, danglingOnly bool) (runtimepk | |||
| 748 | 764 | }) | |
| 749 | 765 | log := zerowrap.FromCtx(ctx) | |
| 750 | 766 | ||
| 751 | - | pruneFilters := filters.NewArgs() | |
| 752 | - | pruneFilters.Add("label", domain.LabelManaged+"=true") | |
| 767 | + | pruneFilters := make(client.Filters).Add("label", domain.LabelManaged+"=true") | |
| 753 | 768 | if danglingOnly { | |
| 754 | 769 | pruneFilters.Add("dangling", "true") | |
| 755 | 770 | } | |
| 756 | 771 | ||
| 757 | - | pruneResult, err := r.client.ImagesPrune(ctx, pruneFilters) | |
| 772 | + | pruneResult, err := r.client.ImagePrune(ctx, client.ImagePruneOptions{Filters: pruneFilters}) | |
| 758 | 773 | if err != nil { | |
| 759 | 774 | return runtimepkg.PruneReport{}, log.WrapErr(err, "failed to prune images") | |
| 760 | 775 | } | |
| 761 | 776 | ||
| 762 | - | deletedIDs := make([]string, 0, len(pruneResult.ImagesDeleted)) | |
| 763 | - | for _, deleted := range pruneResult.ImagesDeleted { | |
| 777 | + | deletedIDs := make([]string, 0, len(pruneResult.Report.ImagesDeleted)) | |
| 778 | + | for _, deleted := range pruneResult.Report.ImagesDeleted { | |
| 764 | 779 | if deleted.Deleted != "" { | |
| 765 | 780 | deletedIDs = append(deletedIDs, deleted.Deleted) | |
| 766 | 781 | } | |
| @@ -769,7 +784,7 @@ func (r *Runtime) PruneImages(ctx context.Context, danglingOnly bool) (runtimepk | |||
| 769 | 784 | } | |
| 770 | 785 | } | |
| 771 | 786 | ||
| 772 | - | spaceReclaimed := pruneResult.SpaceReclaimed | |
| 787 | + | spaceReclaimed := pruneResult.Report.SpaceReclaimed | |
| 773 | 788 | if spaceReclaimed > math.MaxInt64 { | |
| 774 | 789 | log.Warn(). | |
| 775 | 790 | Uint64("space_reclaimed_bytes", spaceReclaimed). | |
| @@ -796,7 +811,7 @@ func (r *Runtime) Ping(ctx context.Context) error { | |||
| 796 | 811 | }) | |
| 797 | 812 | log := zerowrap.FromCtx(ctx) | |
| 798 | 813 | ||
| 799 | - | _, err := r.client.Ping(ctx) | |
| 814 | + | _, err := r.client.Ping(ctx, client.PingOptions{}) | |
| 800 | 815 | if err != nil { | |
| 801 | 816 | return log.WrapErr(err, "Docker ping failed") | |
| 802 | 817 | } | |
| @@ -812,7 +827,7 @@ func (r *Runtime) Version(ctx context.Context) (string, error) { | |||
| 812 | 827 | }) | |
| 813 | 828 | log := zerowrap.FromCtx(ctx) | |
| 814 | 829 | ||
| 815 | - | version, err := r.client.ServerVersion(ctx) | |
| 830 | + | version, err := r.client.ServerVersion(ctx, client.ServerVersionOptions{}) | |
| 816 | 831 | if err != nil { | |
| 817 | 832 | return "", log.WrapErr(err, "failed to get Docker version") | |
| 818 | 833 | } | |
| @@ -838,10 +853,11 @@ func (r *Runtime) GetContainerHealthStatus(ctx context.Context, containerID stri | |||
| 838 | 853 | }) | |
| 839 | 854 | log := zerowrap.FromCtx(ctx) | |
| 840 | 855 | ||
| 841 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | |
| 856 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) | |
| 842 | 857 | if err != nil { | |
| 843 | 858 | return "", false, log.WrapErr(err, "failed to inspect container") | |
| 844 | 859 | } | |
| 860 | + | resp := inspectResult.Container | |
| 845 | 861 | ||
| 846 | 862 | // Detect configured healthchecks from container config rather than relying on | |
| 847 | 863 | // State.Health presence. Some runtimes can expose an empty State.Health struct | |
| @@ -854,7 +870,7 @@ func (r *Runtime) GetContainerHealthStatus(ctx context.Context, containerID stri | |||
| 854 | 870 | return "", true, nil | |
| 855 | 871 | } | |
| 856 | 872 | ||
| 857 | - | return resp.State.Health.Status, true, nil | |
| 873 | + | return string(resp.State.Health.Status), true, nil | |
| 858 | 874 | } | |
| 859 | 875 | ||
| 860 | 876 | func hasConfiguredHealthcheck(cfg *container.Config) bool { | |
| @@ -876,16 +892,20 @@ func (r *Runtime) GetContainerPort(ctx context.Context, containerID string, inte | |||
| 876 | 892 | }) | |
| 877 | 893 | log := zerowrap.FromCtx(ctx) | |
| 878 | 894 | ||
| 879 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | |
| 895 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) | |
| 880 | 896 | if err != nil { | |
| 881 | 897 | return 0, log.WrapErr(err, "failed to inspect container") | |
| 882 | 898 | } | |
| 899 | + | resp := inspectResult.Container | |
| 883 | 900 | ||
| 884 | 901 | if resp.NetworkSettings == nil || resp.NetworkSettings.Ports == nil { | |
| 885 | 902 | return 0, fmt.Errorf("no port mappings found for container %s", containerID) | |
| 886 | 903 | } | |
| 887 | 904 | ||
| 888 | - | containerPort := nat.Port(fmt.Sprintf("%d/tcp", internalPort)) | |
| 905 | + | containerPort, err := network.ParsePort(fmt.Sprintf("%d/tcp", internalPort)) | |
| 906 | + | if err != nil { | |
| 907 | + | return 0, fmt.Errorf("invalid container port %d: %w", internalPort, err) | |
| 908 | + | } | |
| 889 | 909 | bindings, exists := resp.NetworkSettings.Ports[containerPort] | |
| 890 | 910 | if !exists || len(bindings) == 0 { | |
| 891 | 911 | return 0, fmt.Errorf("port %d not mapped for container %s", internalPort, containerID) | |
| @@ -975,16 +995,17 @@ func (r *Runtime) GetContainerExposedPorts(ctx context.Context, containerID stri | |||
| 975 | 995 | }) | |
| 976 | 996 | log := zerowrap.FromCtx(ctx) | |
| 977 | 997 | ||
| 978 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | |
| 998 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) | |
| 979 | 999 | if err != nil { | |
| 980 | 1000 | return nil, log.WrapErr(err, "failed to inspect container") | |
| 981 | 1001 | } | |
| 1002 | + | resp := inspectResult.Container | |
| 982 | 1003 | ||
| 983 | 1004 | var ports []int | |
| 984 | 1005 | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |
| 985 | 1006 | for portSpec := range resp.NetworkSettings.Ports { | |
| 986 | - | // Parse port from format like "80/tcp" | |
| 987 | - | portStr := strings.Split(string(portSpec), "/")[0] | |
| 1007 | + | // Parse port from format like "80/tcp". | |
| 1008 | + | portStr := portSpec.Port() | |
| 988 | 1009 | if port, err := strconv.Atoi(portStr); err == nil { | |
| 989 | 1010 | ports = append(ports, port) | |
| 990 | 1011 | } | |
| @@ -1004,18 +1025,19 @@ func (r *Runtime) GetContainerNetworkInfo(ctx context.Context, containerID strin | |||
| 1004 | 1025 | }) | |
| 1005 | 1026 | log := zerowrap.FromCtx(ctx) | |
| 1006 | 1027 | ||
| 1007 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | |
| 1028 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) | |
| 1008 | 1029 | if err != nil { | |
| 1009 | 1030 | return "", 0, log.WrapErr(err, "failed to inspect container") | |
| 1010 | 1031 | } | |
| 1032 | + | resp := inspectResult.Container | |
| 1011 | 1033 | ||
| 1012 | 1034 | // Get container's internal IP address | |
| 1013 | 1035 | var containerIP string | |
| 1014 | 1036 | if resp.NetworkSettings != nil && resp.NetworkSettings.Networks != nil { | |
| 1015 | 1037 | // Use the first available network (usually bridge or custom network) | |
| 1016 | 1038 | for _, net := range resp.NetworkSettings.Networks { | |
| 1017 | - | if net.IPAddress != "" { | |
| 1018 | - | containerIP = net.IPAddress | |
| 1039 | + | if net.IPAddress.IsValid() { | |
| 1040 | + | containerIP = net.IPAddress.String() | |
| 1019 | 1041 | break | |
| 1020 | 1042 | } | |
| 1021 | 1043 | } | |
| @@ -1029,9 +1051,8 @@ func (r *Runtime) GetContainerNetworkInfo(ctx context.Context, containerID strin | |||
| 1029 | 1051 | var exposedPorts []int | |
| 1030 | 1052 | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |
| 1031 | 1053 | for portSpec := range resp.NetworkSettings.Ports { | |
| 1032 | - | // Parse port from format like "80/tcp" | |
| 1033 | - | portStr := strings.Split(string(portSpec), "/")[0] | |
| 1034 | - | if port, err := strconv.Atoi(portStr); err == nil { | |
| 1054 | + | // Parse port from format like "80/tcp". | |
| 1055 | + | if port, err := strconv.Atoi(portSpec.Port()); err == nil { | |
| 1035 | 1056 | exposedPorts = append(exposedPorts, port) | |
| 1036 | 1057 | } | |
| 1037 | 1058 | } | |
| @@ -1064,12 +1085,12 @@ func (r *Runtime) GetContainerNetwork(ctx context.Context, containerID string) ( | |||
| 1064 | 1085 | }) | |
| 1065 | 1086 | log := zerowrap.FromCtx(ctx) | |
| 1066 | 1087 | ||
| 1067 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | |
| 1088 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) | |
| 1068 | 1089 | if err != nil { | |
| 1069 | 1090 | return "", log.WrapErr(err, "failed to inspect container") | |
| 1070 | 1091 | } | |
| 1071 | 1092 | ||
| 1072 | - | networks := resp.NetworkSettings | |
| 1093 | + | networks := inspectResult.Container.NetworkSettings | |
| 1073 | 1094 | if networks == nil || networks.Networks == nil || len(networks.Networks) == 0 { | |
| 1074 | 1095 | return "bridge", nil | |
| 1075 | 1096 | } | |
| @@ -1145,7 +1166,7 @@ func (r *Runtime) VolumeExists(ctx context.Context, volumeName string) (bool, er | |||
| 1145 | 1166 | }) | |
| 1146 | 1167 | log := zerowrap.FromCtx(ctx) | |
| 1147 | 1168 | ||
| 1148 | - | _, err := r.client.VolumeInspect(ctx, volumeName) | |
| 1169 | + | _, err := r.client.VolumeInspect(ctx, volumeName, client.VolumeInspectOptions{}) | |
| 1149 | 1170 | if err != nil { | |
| 1150 | 1171 | if cerrdefs.IsNotFound(err) { | |
| 1151 | 1172 | return false, nil | |
| @@ -1165,7 +1186,7 @@ func (r *Runtime) CreateVolume(ctx context.Context, volumeName string) error { | |||
| 1165 | 1186 | }) | |
| 1166 | 1187 | log := zerowrap.FromCtx(ctx) | |
| 1167 | 1188 | ||
| 1168 | - | _, err := r.client.VolumeCreate(ctx, volume.CreateOptions{ | |
| 1189 | + | _, err := r.client.VolumeCreate(ctx, client.VolumeCreateOptions{ | |
| 1169 | 1190 | Name: volumeName, | |
| 1170 | 1191 | Labels: map[string]string{ | |
| 1171 | 1192 | domain.LabelManaged: "true", | |
| @@ -1191,7 +1212,7 @@ func (r *Runtime) RemoveVolume(ctx context.Context, volumeName string, force boo | |||
| 1191 | 1212 | }) | |
| 1192 | 1213 | log := zerowrap.FromCtx(ctx) | |
| 1193 | 1214 | ||
| 1194 | - | err := r.client.VolumeRemove(ctx, volumeName, force) | |
| 1215 | + | _, err := r.client.VolumeRemove(ctx, volumeName, client.VolumeRemoveOptions{Force: force}) | |
| 1195 | 1216 | if err != nil { | |
| 1196 | 1217 | if cerrdefs.IsNotFound(err) { | |
| 1197 | 1218 | log.Debug().Msg("volume not found, already removed") | |
| @@ -1206,20 +1227,27 @@ func (r *Runtime) RemoveVolume(ctx context.Context, volumeName string, force boo | |||
| 1206 | 1227 | ||
| 1207 | 1228 | // ListVolumes returns all named volumes with their usage status. | |
| 1208 | 1229 | func (r *Runtime) ListVolumes(ctx context.Context) ([]*domain.VolumeInfo, error) { | |
| 1209 | - | volumeList, err := r.client.VolumeList(ctx, volume.ListOptions{}) | |
| 1230 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 1231 | + | zerowrap.FieldLayer: "adapter", | |
| 1232 | + | zerowrap.FieldAdapter: "docker", | |
| 1233 | + | zerowrap.FieldAction: "ListVolumes", | |
| 1234 | + | }) | |
| 1235 | + | log := zerowrap.FromCtx(ctx) | |
| 1236 | + | ||
| 1237 | + | volumeList, err := r.client.VolumeList(ctx, client.VolumeListOptions{}) | |
| 1210 | 1238 | if err != nil { | |
| 1211 | - | return nil, fmt.Errorf("failed to list volumes: %w", err) | |
| 1239 | + | return nil, log.WrapErr(err, "failed to list volumes") | |
| 1212 | 1240 | } | |
| 1213 | 1241 | ||
| 1214 | - | // Get all containers to determine volume usage | |
| 1215 | - | containers, err := r.client.ContainerList(ctx, container.ListOptions{All: true}) | |
| 1242 | + | // Get all containers to determine volume usage. | |
| 1243 | + | containerList, err := r.client.ContainerList(ctx, client.ContainerListOptions{All: true}) | |
| 1216 | 1244 | if err != nil { | |
| 1217 | - | return nil, fmt.Errorf("failed to list containers for volume usage: %w", err) | |
| 1245 | + | return nil, log.WrapErr(err, "failed to list containers for volume usage") | |
| 1218 | 1246 | } | |
| 1219 | 1247 | ||
| 1220 | - | // Build a map of volume name -> container names | |
| 1248 | + | // Build a map of volume name -> container names. | |
| 1221 | 1249 | volumeUsers := make(map[string][]string) | |
| 1222 | - | for _, c := range containers { | |
| 1250 | + | for _, c := range containerList.Items { | |
| 1223 | 1251 | for _, m := range c.Mounts { | |
| 1224 | 1252 | if m.Type == "volume" { | |
| 1225 | 1253 | name := "" | |
| @@ -1232,11 +1260,7 @@ func (r *Runtime) ListVolumes(ctx context.Context) ([]*domain.VolumeInfo, error) | |||
| 1232 | 1260 | } | |
| 1233 | 1261 | ||
| 1234 | 1262 | var result []*domain.VolumeInfo | |
| 1235 | - | for _, v := range volumeList.Volumes { | |
| 1236 | - | if v == nil { | |
| 1237 | - | continue | |
| 1238 | - | } | |
| 1239 | - | ||
| 1263 | + | for _, v := range volumeList.Items { | |
| 1240 | 1264 | var size int64 | |
| 1241 | 1265 | if v.UsageData != nil { | |
| 1242 | 1266 | size = v.UsageData.Size | |
| @@ -1281,7 +1305,7 @@ func (r *Runtime) ExportVolumeArchive(ctx context.Context, req domain.VolumeArch | |||
| 1281 | 1305 | if err != nil { | |
| 1282 | 1306 | return nil, err | |
| 1283 | 1307 | } | |
| 1284 | - | if err := r.client.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil { | |
| 1308 | + | if _, err := r.client.ContainerStart(ctx, created.ID, client.ContainerStartOptions{}); err != nil { | |
| 1285 | 1309 | _ = cleanup() | |
| 1286 | 1310 | return nil, log.WrapErr(err, "failed to start volume archive helper container") | |
| 1287 | 1311 | } | |
| @@ -1314,14 +1338,18 @@ func (r *Runtime) ensureVolumeArchiveHelperImage(ctx context.Context, imageRef s | |||
| 1314 | 1338 | return nil | |
| 1315 | 1339 | } | |
| 1316 | 1340 | ||
| 1317 | - | func (r *Runtime) createVolumeArchiveHelper(ctx context.Context, req domain.VolumeArchiveRequest, log zerowrap.Logger) (container.CreateResponse, func() error, error) { | |
| 1341 | + | func (r *Runtime) createVolumeArchiveHelper(ctx context.Context, req domain.VolumeArchiveRequest, log zerowrap.Logger) (client.ContainerCreateResult, func() error, error) { | |
| 1318 | 1342 | cmd, err := volumeArchiveCommand(req.Compression) | |
| 1319 | 1343 | if err != nil { | |
| 1320 | - | return container.CreateResponse{}, nil, err | |
| 1344 | + | return client.ContainerCreateResult{}, nil, err | |
| 1321 | 1345 | } | |
| 1322 | - | created, err := r.client.ContainerCreate(ctx, volumeArchiveContainerConfig(req.HelperImage, cmd), volumeArchiveHostConfig(req.VolumeName), nil, nil, fmt.Sprintf("gordon-volume-backup-%d", time.Now().UTC().UnixNano())) | |
| 1346 | + | created, err := r.client.ContainerCreate(ctx, client.ContainerCreateOptions{ | |
| 1347 | + | Config: volumeArchiveContainerConfig(req.HelperImage, cmd), | |
| 1348 | + | HostConfig: volumeArchiveHostConfig(req.VolumeName), | |
| 1349 | + | Name: fmt.Sprintf("gordon-volume-backup-%d", time.Now().UTC().UnixNano()), | |
| 1350 | + | }) | |
| 1323 | 1351 | if err != nil { | |
| 1324 | - | return container.CreateResponse{}, nil, log.WrapErr(err, "failed to create volume archive helper container") | |
| 1352 | + | return client.ContainerCreateResult{}, nil, log.WrapErr(err, "failed to create volume archive helper container") | |
| 1325 | 1353 | } | |
| 1326 | 1354 | var cleanupOnce sync.Once | |
| 1327 | 1355 | cleanup := func() error { | |
| @@ -1363,11 +1391,12 @@ func volumeArchiveHostConfig(volumeName string) *container.HostConfig { | |||
| 1363 | 1391 | } | |
| 1364 | 1392 | ||
| 1365 | 1393 | func (r *Runtime) attachVolumeArchiveStream(ctx context.Context, containerID string, cleanup func() error, log zerowrap.Logger) (io.ReadCloser, error) { | |
| 1366 | - | logs, err := r.client.ContainerLogs(ctx, containerID, container.LogsOptions{ShowStdout: true, ShowStderr: true, Follow: true}) | |
| 1394 | + | logs, err := r.client.ContainerLogs(ctx, containerID, client.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true}) | |
| 1367 | 1395 | if err != nil { | |
| 1368 | 1396 | return nil, log.WrapErr(err, "failed to attach volume archive stream") | |
| 1369 | 1397 | } | |
| 1370 | - | statusCh, errCh := r.client.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) | |
| 1398 | + | waitResult := r.client.ContainerWait(ctx, containerID, client.ContainerWaitOptions{Condition: container.WaitConditionNotRunning}) | |
| 1399 | + | statusCh, errCh := waitResult.Result, waitResult.Error | |
| 1371 | 1400 | pr, pw := io.Pipe() | |
| 1372 | 1401 | go streamVolumeArchiveLogs(logs, pw, statusCh, errCh, cleanup, log) | |
| 1373 | 1402 | return &pipeReadCloser{pr: pr, pw: pw, original: logs, cleanup: cleanup}, nil | |
| @@ -1538,7 +1567,7 @@ func (r *Runtime) CreateNetwork(ctx context.Context, name string, config domain. | |||
| 1538 | 1567 | maps.Copy(labels, config.Labels) | |
| 1539 | 1568 | labels[domain.LabelManaged] = "true" | |
| 1540 | 1569 | ||
| 1541 | - | createOptions := network.CreateOptions{ | |
| 1570 | + | createOptions := client.NetworkCreateOptions{ | |
| 1542 | 1571 | Driver: driver, | |
| 1543 | 1572 | Internal: config.Internal, | |
| 1544 | 1573 | Labels: labels, | |
| @@ -1563,7 +1592,7 @@ func (r *Runtime) RemoveNetwork(ctx context.Context, name string) error { | |||
| 1563 | 1592 | }) | |
| 1564 | 1593 | log := zerowrap.FromCtx(ctx) | |
| 1565 | 1594 | ||
| 1566 | - | err := r.client.NetworkRemove(ctx, name) | |
| 1595 | + | _, err := r.client.NetworkRemove(ctx, name, client.NetworkRemoveOptions{}) | |
| 1567 | 1596 | if err != nil { | |
| 1568 | 1597 | if cerrdefs.IsNotFound(err) { | |
| 1569 | 1598 | log.Debug().Msg("network not found, already removed") | |
| @@ -1585,15 +1614,19 @@ func (r *Runtime) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, erro | |||
| 1585 | 1614 | }) | |
| 1586 | 1615 | log := zerowrap.FromCtx(ctx) | |
| 1587 | 1616 | ||
| 1588 | - | networks, err := r.client.NetworkList(ctx, network.ListOptions{}) | |
| 1617 | + | networkList, err := r.client.NetworkList(ctx, client.NetworkListOptions{}) | |
| 1589 | 1618 | if err != nil { | |
| 1590 | 1619 | return nil, log.WrapErr(err, "failed to list networks") | |
| 1591 | 1620 | } | |
| 1592 | 1621 | ||
| 1593 | 1622 | var result []*domain.NetworkInfo | |
| 1594 | - | for _, net := range networks { | |
| 1623 | + | for _, net := range networkList.Items { | |
| 1624 | + | networkInspect, err := r.client.NetworkInspect(ctx, net.ID, client.NetworkInspectOptions{}) | |
| 1625 | + | if err != nil { | |
| 1626 | + | return nil, log.WrapErr(err, "failed to inspect network") | |
| 1627 | + | } | |
| 1595 | 1628 | var containers []string | |
| 1596 | - | for containerID := range net.Containers { | |
| 1629 | + | for containerID := range networkInspect.Network.Containers { | |
| 1597 | 1630 | containers = append(containers, containerID) | |
| 1598 | 1631 | } | |
| 1599 | 1632 | ||
| @@ -1619,7 +1652,7 @@ func (r *Runtime) NetworkExists(ctx context.Context, name string) (bool, error) | |||
| 1619 | 1652 | }) | |
| 1620 | 1653 | log := zerowrap.FromCtx(ctx) | |
| 1621 | 1654 | ||
| 1622 | - | _, err := r.client.NetworkInspect(ctx, name, network.InspectOptions{}) | |
| 1655 | + | _, err := r.client.NetworkInspect(ctx, name, client.NetworkInspectOptions{}) | |
| 1623 | 1656 | if err != nil { | |
| 1624 | 1657 | if cerrdefs.IsNotFound(err) { | |
| 1625 | 1658 | return false, nil | |
| @@ -1640,7 +1673,10 @@ func (r *Runtime) ConnectContainerToNetwork(ctx context.Context, containerName, | |||
| 1640 | 1673 | }) | |
| 1641 | 1674 | log := zerowrap.FromCtx(ctx) | |
| 1642 | 1675 | ||
| 1643 | - | err := r.client.NetworkConnect(ctx, networkName, containerName, &network.EndpointSettings{}) | |
| 1676 | + | _, err := r.client.NetworkConnect(ctx, networkName, client.NetworkConnectOptions{ | |
| 1677 | + | Container: containerName, | |
| 1678 | + | EndpointConfig: &network.EndpointSettings{}, | |
| 1679 | + | }) | |
| 1644 | 1680 | if err != nil { | |
| 1645 | 1681 | return log.WrapErr(err, "failed to connect container to network") | |
| 1646 | 1682 | } | |
| @@ -1660,7 +1696,9 @@ func (r *Runtime) DisconnectContainerFromNetwork(ctx context.Context, containerN | |||
| 1660 | 1696 | }) | |
| 1661 | 1697 | log := zerowrap.FromCtx(ctx) | |
| 1662 | 1698 | ||
| 1663 | - | err := r.client.NetworkDisconnect(ctx, networkName, containerName, false) | |
| 1699 | + | _, err := r.client.NetworkDisconnect(ctx, networkName, client.NetworkDisconnectOptions{ | |
| 1700 | + | Container: containerName, | |
| 1701 | + | }) | |
| 1664 | 1702 | if err != nil { | |
| 1665 | 1703 | return log.WrapErr(err, "failed to disconnect container from network") | |
| 1666 | 1704 | } | |
| @@ -1685,7 +1723,7 @@ func (r *Runtime) ExecInContainer(ctx context.Context, containerID string, cmd [ | |||
| 1685 | 1723 | }) | |
| 1686 | 1724 | log := zerowrap.FromCtx(ctx) | |
| 1687 | 1725 | ||
| 1688 | - | execResp, err := r.client.ContainerExecCreate(ctx, containerID, container.ExecOptions{ | |
| 1726 | + | execResp, err := r.client.ExecCreate(ctx, containerID, client.ExecCreateOptions{ | |
| 1689 | 1727 | Cmd: cmd, | |
| 1690 | 1728 | AttachStdout: true, | |
| 1691 | 1729 | AttachStderr: true, | |
| @@ -1701,7 +1739,7 @@ func (r *Runtime) ExecInContainer(ctx context.Context, containerID string, cmd [ | |||
| 1701 | 1739 | defer cancel() | |
| 1702 | 1740 | } | |
| 1703 | 1741 | ||
| 1704 | - | attachResp, err := r.client.ContainerExecAttach(attachCtx, execResp.ID, container.ExecAttachOptions{}) | |
| 1742 | + | attachResp, err := r.client.ExecAttach(attachCtx, execResp.ID, client.ExecAttachOptions{}) | |
| 1705 | 1743 | if err != nil { | |
| 1706 | 1744 | return nil, log.WrapErr(err, "failed to attach to exec") | |
| 1707 | 1745 | } | |
| @@ -1715,7 +1753,7 @@ func (r *Runtime) ExecInContainer(ctx context.Context, containerID string, cmd [ | |||
| 1715 | 1753 | return nil, log.WrapErr(err, "failed to read exec output") | |
| 1716 | 1754 | } | |
| 1717 | 1755 | ||
| 1718 | - | inspectResp, err := r.client.ContainerExecInspect(ctx, execResp.ID) | |
| 1756 | + | inspectResp, err := r.client.ExecInspect(ctx, execResp.ID, client.ExecInspectOptions{}) | |
| 1719 | 1757 | if err != nil { | |
| 1720 | 1758 | return nil, log.WrapErr(err, "failed to inspect exec result") | |
| 1721 | 1759 | } | |
| @@ -1787,15 +1825,15 @@ func (r *Runtime) CopyFromContainer(ctx context.Context, containerID, srcPath st | |||
| 1787 | 1825 | }) | |
| 1788 | 1826 | log := zerowrap.FromCtx(ctx) | |
| 1789 | 1827 | ||
| 1790 | - | tarReader, _, err := r.client.CopyFromContainer(ctx, containerID, srcPath) | |
| 1828 | + | copyResult, err := r.client.CopyFromContainer(ctx, containerID, client.CopyFromContainerOptions{SourcePath: srcPath}) | |
| 1791 | 1829 | if err != nil { | |
| 1792 | 1830 | return nil, log.WrapErr(err, "failed to copy from container") | |
| 1793 | 1831 | } | |
| 1794 | 1832 | ||
| 1795 | 1833 | // The response is a tar archive - we need to extract the requested file stream. | |
| 1796 | - | reader, err := extractFileFromTar(tarReader, srcPath) | |
| 1834 | + | reader, err := extractFileFromTar(copyResult.Content, srcPath) | |
| 1797 | 1835 | if err != nil { | |
| 1798 | - | _ = tarReader.Close() | |
| 1836 | + | _ = copyResult.Content.Close() | |
| 1799 | 1837 | return nil, log.WrapErr(err, "failed to extract file from tar") | |
| 1800 | 1838 | } | |
| 1801 | 1839 | ||
| @@ -1928,7 +1966,7 @@ func (r *Runtime) ExtractEnvFileFromImage(ctx context.Context, imageRef, envFile | |||
| 1928 | 1966 | Cmd: []string{"true"}, // Dummy command | |
| 1929 | 1967 | } | |
| 1930 | 1968 | ||
| 1931 | - | resp, err := r.client.ContainerCreate(ctx, containerConfig, nil, nil, nil, "") | |
| 1969 | + | resp, err := r.client.ContainerCreate(ctx, client.ContainerCreateOptions{Config: containerConfig}) | |
| 1932 | 1970 | if err != nil { | |
| 1933 | 1971 | return nil, log.WrapErr(err, "failed to create temporary container") | |
| 1934 | 1972 | } | |
| @@ -1938,7 +1976,7 @@ func (r *Runtime) ExtractEnvFileFromImage(ctx context.Context, imageRef, envFile | |||
| 1938 | 1976 | ||
| 1939 | 1977 | // Ensure cleanup | |
| 1940 | 1978 | defer func() { | |
| 1941 | - | if err := r.client.ContainerRemove(ctx, tempContainerID, container.RemoveOptions{Force: true}); err != nil { | |
| 1979 | + | if _, err := r.client.ContainerRemove(ctx, tempContainerID, client.ContainerRemoveOptions{Force: true}); err != nil { | |
| 1942 | 1980 | log.Warn().Err(err).Str("temp_container_id", tempContainerID).Msg("failed to remove temporary container") | |
| 1943 | 1981 | } | |
| 1944 | 1982 | }() | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "io" | 13 | "io" | ||
| 14 | "maps" | 14 | "maps" | ||
| 15 | "math" | 15 | "math" | ||
| 16 | + | "net/netip" | |||
| 16 | "os" | 17 | "os" | ||
| 17 | "path/filepath" | 18 | "path/filepath" | ||
| 18 | "sort" | 19 | "sort" | ||
| @@ -23,18 +24,13 @@ import ( | |||||
| 23 | 24 | ||||
| 24 | "github.com/bnema/zerowrap" | 25 | "github.com/bnema/zerowrap" | ||
| 25 | cerrdefs "github.com/containerd/errdefs" | 26 | cerrdefs "github.com/containerd/errdefs" | ||
| 26 | - | "github.com/docker/docker/api/types/container" | |||
| 27 | - | "github.com/docker/docker/api/types/filters" | |||
| 28 | - | "github.com/docker/docker/api/types/image" | |||
| 29 | - | "github.com/docker/docker/api/types/mount" | |||
| 30 | - | "github.com/docker/docker/api/types/network" | |||
| 31 | - | "github.com/docker/docker/api/types/registry" | |||
| 32 | - | "github.com/docker/docker/api/types/strslice" | |||
| 33 | - | "github.com/docker/docker/api/types/volume" | |||
| 34 | - | "github.com/docker/docker/client" | |||
| 35 | - | "github.com/docker/docker/pkg/stdcopy" | |||
| 36 | - | "github.com/docker/go-connections/nat" | |||
| 37 | "github.com/docker/go-units" | 27 | "github.com/docker/go-units" | ||
| 28 | + | "github.com/moby/moby/api/pkg/stdcopy" | |||
| 29 | + | "github.com/moby/moby/api/types/container" | |||
| 30 | + | "github.com/moby/moby/api/types/mount" | |||
| 31 | + | "github.com/moby/moby/api/types/network" | |||
| 32 | + | "github.com/moby/moby/api/types/registry" | |||
| 33 | + | "github.com/moby/moby/client" | |||
| 38 | 34 | ||||
| 39 | "github.com/bnema/gordon/internal/boundaries/out" | 35 | "github.com/bnema/gordon/internal/boundaries/out" | ||
| 40 | "github.com/bnema/gordon/internal/domain" | 36 | "github.com/bnema/gordon/internal/domain" | ||
| @@ -100,7 +96,7 @@ func (f *removeOnCloseFile) Close() error { | |||||
| 100 | 96 | ||||
| 101 | // NewRuntime creates a new Docker runtime instance. | 97 | // NewRuntime creates a new Docker runtime instance. | ||
| 102 | func NewRuntime() (*Runtime, error) { | 98 | func NewRuntime() (*Runtime, error) { | ||
| 103 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | 99 | + | cli, err := client.New(client.FromEnv) |
| 104 | if err != nil { | 100 | if err != nil { | ||
| 105 | return nil, fmt.Errorf("failed to create Docker client: %w", err) | 101 | return nil, fmt.Errorf("failed to create Docker client: %w", err) | ||
| 106 | } | 102 | } | ||
| @@ -113,10 +109,7 @@ func NewRuntime() (*Runtime, error) { | |||||
| 113 | // NewRuntimeWithSocket creates a Docker runtime targeting a specific socket path. | 109 | // NewRuntimeWithSocket creates a Docker runtime targeting a specific socket path. | ||
| 114 | func NewRuntimeWithSocket(socketPath string) (*Runtime, error) { | 110 | func NewRuntimeWithSocket(socketPath string) (*Runtime, error) { | ||
| 115 | host := "unix://" + socketPath | 111 | host := "unix://" + socketPath | ||
| 116 | - | cli, err := client.NewClientWithOpts( | 112 | + | cli, err := client.New(client.WithHost(host)) |
| 117 | - | client.WithHost(host), | |||
| 118 | - | client.WithAPIVersionNegotiation(), | |||
| 119 | - | ) | |||
| 120 | if err != nil { | 113 | if err != nil { | ||
| 121 | return nil, fmt.Errorf("failed to create Docker client for %s: %w", socketPath, err) | 114 | return nil, fmt.Errorf("failed to create Docker client for %s: %w", socketPath, err) | ||
| 122 | } | 115 | } | ||
| @@ -170,13 +163,13 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC | |||||
| 170 | if config.PidsLimit > 0 { | 163 | if config.PidsLimit > 0 { | ||
| 171 | resources.PidsLimit = &config.PidsLimit | 164 | resources.PidsLimit = &config.PidsLimit | ||
| 172 | } | 165 | } | ||
| 173 | - | capDrop := strslice.StrSlice{"ALL"} | 166 | + | capDrop := []string{"ALL"} |
| 174 | - | capAdd := strslice.StrSlice{"CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID", "NET_BIND_SERVICE"} | 167 | + | capAdd := []string{"CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID", "NET_BIND_SERVICE"} |
| 175 | if config.CapDrop != nil { | 168 | if config.CapDrop != nil { | ||
| 176 | - | capDrop = strslice.StrSlice(config.CapDrop) | 169 | + | capDrop = config.CapDrop |
| 177 | } | 170 | } | ||
| 178 | if config.CapAdd != nil { | 171 | if config.CapAdd != nil { | ||
| 179 | - | capAdd = strslice.StrSlice(config.CapAdd) | 172 | + | capAdd = config.CapAdd |
| 180 | } | 173 | } | ||
| 181 | hostConfig := &container.HostConfig{ | 174 | hostConfig := &container.HostConfig{ | ||
| 182 | PortBindings: portBindings, | 175 | PortBindings: portBindings, | ||
| @@ -208,8 +201,13 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC | |||||
| 208 | } | 201 | } | ||
| 209 | } | 202 | } | ||
| 210 | 203 | ||||
| 211 | - | // Create the container | 204 | + | // Create the container. |
| 212 | - | resp, err := r.client.ContainerCreate(ctx, containerConfig, hostConfig, networkConfig, nil, config.Name) | 205 | + | resp, err := r.client.ContainerCreate(ctx, client.ContainerCreateOptions{ |
| 206 | + | Config: containerConfig, | |||
| 207 | + | HostConfig: hostConfig, | |||
| 208 | + | NetworkingConfig: networkConfig, | |||
| 209 | + | Name: config.Name, | |||
| 210 | + | }) | |||
| 213 | if err != nil { | 211 | if err != nil { | ||
| 214 | return nil, log.WrapErr(err, "failed to create container") | 212 | return nil, log.WrapErr(err, "failed to create container") | ||
| 215 | } | 213 | } | ||
| @@ -220,38 +218,54 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC | |||||
| 220 | return r.InspectContainer(ctx, resp.ID) | 218 | return r.InspectContainer(ctx, resp.ID) | ||
| 221 | } | 219 | } | ||
| 222 | 220 | ||||
| 223 | - | func buildPortBindings(config *domain.ContainerConfig) (nat.PortSet, nat.PortMap, error) { | 221 | + | func buildPortBindings(config *domain.ContainerConfig) (network.PortSet, network.PortMap, error) { |
| 224 | - | exposedPorts := make(nat.PortSet) | 222 | + | exposedPorts := make(network.PortSet) |
| 225 | - | portBindings := make(nat.PortMap) | 223 | + | portBindings := make(network.PortMap) |
| 226 | - | addLegacyPortBindings(config.Ports, exposedPorts, portBindings) | 224 | + | if err := addLegacyPortBindings(config.Ports, exposedPorts, portBindings); err != nil { |
| 225 | + | return nil, nil, err | |||
| 226 | + | } | |||
| 227 | if err := addExplicitPortBindings(config.PortPublishes, exposedPorts, portBindings); err != nil { | 227 | if err := addExplicitPortBindings(config.PortPublishes, exposedPorts, portBindings); err != nil { | ||
| 228 | return nil, nil, err | 228 | return nil, nil, err | ||
| 229 | } | 229 | } | ||
| 230 | return exposedPorts, portBindings, nil | 230 | return exposedPorts, portBindings, nil | ||
| 231 | } | 231 | } | ||
| 232 | 232 | ||||
| 233 | - | func addLegacyPortBindings(ports []int, exposedPorts nat.PortSet, portBindings nat.PortMap) { | 233 | + | func addLegacyPortBindings(ports []int, exposedPorts network.PortSet, portBindings network.PortMap) error { |
| 234 | for _, port := range ports { | 234 | for _, port := range ports { | ||
| 235 | - | containerPort := nat.Port(fmt.Sprintf("%d/tcp", port)) | 235 | + | containerPort, err := network.ParsePort(fmt.Sprintf("%d/tcp", port)) |
| 236 | + | if err != nil { | |||
| 237 | + | return fmt.Errorf("invalid container port %d: %w", port, err) | |||
| 238 | + | } | |||
| 236 | exposedPorts[containerPort] = struct{}{} | 239 | exposedPorts[containerPort] = struct{}{} | ||
| 237 | 240 | ||||
| 238 | // Bind to random available port on localhost only. | 241 | // Bind to random available port on localhost only. | ||
| 239 | // SECURITY: Using 127.0.0.1 prevents direct access from the network, | 242 | // SECURITY: Using 127.0.0.1 prevents direct access from the network, | ||
| 240 | // forcing all traffic through Gordon's reverse proxy where auth, | 243 | // forcing all traffic through Gordon's reverse proxy where auth, | ||
| 241 | // rate limiting, and security headers are applied. | 244 | // rate limiting, and security headers are applied. | ||
| 242 | - | portBindings[containerPort] = []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: "0"}} | 245 | + | portBindings[containerPort] = []network.PortBinding{{HostIP: netip.MustParseAddr("127.0.0.1"), HostPort: "0"}} |
| 243 | } | 246 | } | ||
| 247 | + | return nil | |||
| 244 | } | 248 | } | ||
| 245 | 249 | ||||
| 246 | - | func addExplicitPortBindings(publishes []domain.ContainerPortPublish, exposedPorts nat.PortSet, portBindings nat.PortMap) error { | 250 | + | func addExplicitPortBindings(publishes []domain.ContainerPortPublish, exposedPorts network.PortSet, portBindings network.PortMap) error { |
| 247 | for _, publish := range publishes { | 251 | for _, publish := range publishes { | ||
| 248 | if publish.Protocol != domain.NetworkProtocolTCP && publish.Protocol != domain.NetworkProtocolUDP { | 252 | if publish.Protocol != domain.NetworkProtocolTCP && publish.Protocol != domain.NetworkProtocolUDP { | ||
| 249 | return fmt.Errorf("unsupported container port publish protocol %q", publish.Protocol) | 253 | return fmt.Errorf("unsupported container port publish protocol %q", publish.Protocol) | ||
| 250 | } | 254 | } | ||
| 251 | - | containerPort := nat.Port(fmt.Sprintf("%d/%s", publish.ContainerPort, publish.Protocol)) | 255 | + | containerPort, err := network.ParsePort(fmt.Sprintf("%d/%s", publish.ContainerPort, publish.Protocol)) |
| 256 | + | if err != nil { | |||
| 257 | + | return fmt.Errorf("invalid container port %d: %w", publish.ContainerPort, err) | |||
| 258 | + | } | |||
| 259 | + | var hostIP netip.Addr | |||
| 260 | + | if publish.HostIP != "" { | |||
| 261 | + | hostIP, err = netip.ParseAddr(publish.HostIP) | |||
| 262 | + | if err != nil { | |||
| 263 | + | return fmt.Errorf("invalid host IP %q: %w", publish.HostIP, err) | |||
| 264 | + | } | |||
| 265 | + | } | |||
| 252 | exposedPorts[containerPort] = struct{}{} | 266 | exposedPorts[containerPort] = struct{}{} | ||
| 253 | - | portBindings[containerPort] = append(portBindings[containerPort], nat.PortBinding{ | 267 | + | portBindings[containerPort] = append(portBindings[containerPort], network.PortBinding{ |
| 254 | - | HostIP: publish.HostIP, | 268 | + | HostIP: hostIP, |
| 255 | HostPort: strconv.Itoa(publish.HostPort), | 269 | HostPort: strconv.Itoa(publish.HostPort), | ||
| 256 | }) | 270 | }) | ||
| 257 | } | 271 | } | ||
| @@ -283,7 +297,7 @@ func (r *Runtime) StartContainer(ctx context.Context, containerID string) error | |||||
| 283 | }) | 297 | }) | ||
| 284 | log := zerowrap.FromCtx(ctx) | 298 | log := zerowrap.FromCtx(ctx) | ||
| 285 | 299 | ||||
| 286 | - | err := r.client.ContainerStart(ctx, containerID, container.StartOptions{}) | 300 | + | _, err := r.client.ContainerStart(ctx, containerID, client.ContainerStartOptions{}) |
| 287 | if err != nil { | 301 | if err != nil { | ||
| 288 | return log.WrapErr(err, "failed to start container") | 302 | return log.WrapErr(err, "failed to start container") | ||
| 289 | } | 303 | } | ||
| @@ -302,7 +316,8 @@ func (r *Runtime) WaitForContainer(ctx context.Context, containerID string) erro | |||||
| 302 | }) | 316 | }) | ||
| 303 | log := zerowrap.FromCtx(ctx) | 317 | log := zerowrap.FromCtx(ctx) | ||
| 304 | 318 | ||||
| 305 | - | statusCh, errCh := r.client.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) | 319 | + | waitResult := r.client.ContainerWait(ctx, containerID, client.ContainerWaitOptions{Condition: container.WaitConditionNotRunning}) |
| 320 | + | statusCh, errCh := waitResult.Result, waitResult.Error | |||
| 306 | select { | 321 | select { | ||
| 307 | case err := <-errCh: | 322 | case err := <-errCh: | ||
| 308 | if err != nil { | 323 | if err != nil { | ||
| @@ -326,7 +341,7 @@ func (r *Runtime) StopContainer(ctx context.Context, containerID string) error { | |||||
| 326 | log := zerowrap.FromCtx(ctx) | 341 | log := zerowrap.FromCtx(ctx) | ||
| 327 | 342 | ||||
| 328 | timeout := 20 // 20 seconds before SIGKILL | 343 | timeout := 20 // 20 seconds before SIGKILL | ||
| 329 | - | err := r.client.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}) | 344 | + | _, err := r.client.ContainerStop(ctx, containerID, client.ContainerStopOptions{Timeout: &timeout}) |
| 330 | if err != nil { | 345 | if err != nil { | ||
| 331 | return log.WrapErr(err, "failed to stop container") | 346 | return log.WrapErr(err, "failed to stop container") | ||
| 332 | } | 347 | } | ||
| @@ -346,7 +361,7 @@ func (r *Runtime) RestartContainer(ctx context.Context, containerID string) erro | |||||
| 346 | log := zerowrap.FromCtx(ctx) | 361 | log := zerowrap.FromCtx(ctx) | ||
| 347 | 362 | ||||
| 348 | timeout := 20 // 20 seconds before SIGKILL | 363 | timeout := 20 // 20 seconds before SIGKILL | ||
| 349 | - | err := r.client.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeout}) | 364 | + | _, err := r.client.ContainerRestart(ctx, containerID, client.ContainerRestartOptions{Timeout: &timeout}) |
| 350 | if err != nil { | 365 | if err != nil { | ||
| 351 | return log.WrapErr(err, "failed to restart container") | 366 | return log.WrapErr(err, "failed to restart container") | ||
| 352 | } | 367 | } | ||
| @@ -366,7 +381,7 @@ func (r *Runtime) RemoveContainer(ctx context.Context, containerID string, force | |||||
| 366 | }) | 381 | }) | ||
| 367 | log := zerowrap.FromCtx(ctx) | 382 | log := zerowrap.FromCtx(ctx) | ||
| 368 | 383 | ||||
| 369 | - | err := r.client.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: force}) | 384 | + | _, err := r.client.ContainerRemove(ctx, containerID, client.ContainerRemoveOptions{Force: force}) |
| 370 | if err != nil { | 385 | if err != nil { | ||
| 371 | return log.WrapErr(err, "failed to remove container") | 386 | return log.WrapErr(err, "failed to remove container") | ||
| 372 | } | 387 | } | ||
| @@ -386,7 +401,7 @@ func (r *Runtime) RenameContainer(ctx context.Context, containerID, newName stri | |||||
| 386 | }) | 401 | }) | ||
| 387 | log := zerowrap.FromCtx(ctx) | 402 | log := zerowrap.FromCtx(ctx) | ||
| 388 | 403 | ||||
| 389 | - | err := r.client.ContainerRename(ctx, containerID, newName) | 404 | + | _, err := r.client.ContainerRename(ctx, containerID, client.ContainerRenameOptions{NewName: newName}) |
| 390 | if err != nil { | 405 | if err != nil { | ||
| 391 | return log.WrapErr(err, "failed to rename container") | 406 | return log.WrapErr(err, "failed to rename container") | ||
| 392 | } | 407 | } | ||
| @@ -405,13 +420,13 @@ func (r *Runtime) ListContainers(ctx context.Context, all bool) ([]*domain.Conta | |||||
| 405 | }) | 420 | }) | ||
| 406 | log := zerowrap.FromCtx(ctx) | 421 | log := zerowrap.FromCtx(ctx) | ||
| 407 | 422 | ||||
| 408 | - | containers, err := r.client.ContainerList(ctx, container.ListOptions{All: all}) | 423 | + | containerList, err := r.client.ContainerList(ctx, client.ContainerListOptions{All: all}) |
| 409 | if err != nil { | 424 | if err != nil { | ||
| 410 | return nil, log.WrapErr(err, "failed to list containers") | 425 | return nil, log.WrapErr(err, "failed to list containers") | ||
| 411 | } | 426 | } | ||
| 412 | 427 | ||||
| 413 | var result []*domain.Container | 428 | var result []*domain.Container | ||
| 414 | - | for _, c := range containers { | 429 | + | for _, c := range containerList.Items { |
| 415 | // Extract ports | 430 | // Extract ports | ||
| 416 | var ports []int | 431 | var ports []int | ||
| 417 | for _, port := range c.Ports { | 432 | for _, port := range c.Ports { | ||
| @@ -442,7 +457,7 @@ func (r *Runtime) ListContainers(ctx context.Context, all bool) ([]*domain.Conta | |||||
| 442 | Image: c.Image, | 457 | Image: c.Image, | ||
| 443 | ImageID: c.ImageID, | 458 | ImageID: c.ImageID, | ||
| 444 | Name: name, | 459 | Name: name, | ||
| 445 | - | Status: c.State, // Use State (e.g., "running") not Status (e.g., "Up 2 days") | 460 | + | Status: string(c.State), // Use State (e.g., "running") not Status (e.g., "Up 2 days") |
| 446 | Ports: ports, | 461 | Ports: ports, | ||
| 447 | Labels: c.Labels, | 462 | Labels: c.Labels, | ||
| 448 | VolumeMounts: volumeMounts, | 463 | VolumeMounts: volumeMounts, | ||
| @@ -463,10 +478,11 @@ func (r *Runtime) InspectContainer(ctx context.Context, containerID string) (*do | |||||
| 463 | }) | 478 | }) | ||
| 464 | log := zerowrap.FromCtx(ctx) | 479 | log := zerowrap.FromCtx(ctx) | ||
| 465 | 480 | ||||
| 466 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | 481 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) |
| 467 | if err != nil { | 482 | if err != nil { | ||
| 468 | return nil, log.WrapErr(err, "failed to inspect container") | 483 | return nil, log.WrapErr(err, "failed to inspect container") | ||
| 469 | } | 484 | } | ||
| 485 | + | resp := inspectResult.Container | |||
| 470 | 486 | ||||
| 471 | // Extract published ports | 487 | // Extract published ports | ||
| 472 | var ports []int | 488 | var ports []int | ||
| @@ -502,7 +518,7 @@ func (r *Runtime) InspectContainer(ctx context.Context, containerID string) (*do | |||||
| 502 | Image: resp.Config.Image, | 518 | Image: resp.Config.Image, | ||
| 503 | ImageID: resp.Image, | 519 | ImageID: resp.Image, | ||
| 504 | Name: name, | 520 | Name: name, | ||
| 505 | - | Status: resp.State.Status, | 521 | + | Status: string(resp.State.Status), |
| 506 | ExitCode: resp.State.ExitCode, | 522 | ExitCode: resp.State.ExitCode, | ||
| 507 | Ports: ports, | 523 | Ports: ports, | ||
| 508 | Labels: resp.Config.Labels, | 524 | Labels: resp.Config.Labels, | ||
| @@ -521,7 +537,7 @@ func (r *Runtime) GetContainerLogs(ctx context.Context, containerID string, foll | |||||
| 521 | }) | 537 | }) | ||
| 522 | log := zerowrap.FromCtx(ctx) | 538 | log := zerowrap.FromCtx(ctx) | ||
| 523 | 539 | ||||
| 524 | - | logs, err := r.client.ContainerLogs(ctx, containerID, container.LogsOptions{ | 540 | + | logs, err := r.client.ContainerLogs(ctx, containerID, client.ContainerLogsOptions{ |
| 525 | ShowStdout: true, | 541 | ShowStdout: true, | ||
| 526 | ShowStderr: true, | 542 | ShowStderr: true, | ||
| 527 | Follow: follow, | 543 | Follow: follow, | ||
| @@ -547,7 +563,7 @@ func (r *Runtime) PullImage(ctx context.Context, imageRef string) error { | |||||
| 547 | 563 | ||||
| 548 | log.Info().Msg("pulling image") | 564 | log.Info().Msg("pulling image") | ||
| 549 | 565 | ||||
| 550 | - | reader, err := r.client.ImagePull(ctx, imageRef, image.PullOptions{}) | 566 | + | reader, err := r.client.ImagePull(ctx, imageRef, client.ImagePullOptions{}) |
| 551 | if err != nil { | 567 | if err != nil { | ||
| 552 | return log.WrapErr(fmt.Errorf("%w: %w", domain.ErrImagePullFailed, err), "failed to pull image") | 568 | return log.WrapErr(fmt.Errorf("%w: %w", domain.ErrImagePullFailed, err), "failed to pull image") | ||
| 553 | } | 569 | } | ||
| @@ -605,7 +621,7 @@ func (r *Runtime) PullImageWithAuth(ctx context.Context, imageRef, username, pas | |||||
| 605 | Msg("auth config for pull") | 621 | Msg("auth config for pull") | ||
| 606 | 622 | ||||
| 607 | // Pull with authentication | 623 | // Pull with authentication | ||
| 608 | - | reader, err := r.client.ImagePull(ctx, imageRef, image.PullOptions{ | 624 | + | reader, err := r.client.ImagePull(ctx, imageRef, client.ImagePullOptions{ |
| 609 | RegistryAuth: authStr, | 625 | RegistryAuth: authStr, | ||
| 610 | }) | 626 | }) | ||
| 611 | if err != nil { | 627 | if err != nil { | ||
| @@ -634,7 +650,7 @@ func (r *Runtime) TagImage(ctx context.Context, sourceRef, targetRef string) err | |||||
| 634 | }) | 650 | }) | ||
| 635 | log := zerowrap.FromCtx(ctx) | 651 | log := zerowrap.FromCtx(ctx) | ||
| 636 | 652 | ||||
| 637 | - | if err := r.client.ImageTag(ctx, sourceRef, targetRef); err != nil { | 653 | + | if _, err := r.client.ImageTag(ctx, client.ImageTagOptions{Source: sourceRef, Target: targetRef}); err != nil { |
| 638 | return log.WrapErr(err, "failed to tag image") | 654 | return log.WrapErr(err, "failed to tag image") | ||
| 639 | } | 655 | } | ||
| 640 | 656 | ||||
| @@ -653,7 +669,7 @@ func (r *Runtime) UntagImage(ctx context.Context, imageRef string) error { | |||||
| 653 | log := zerowrap.FromCtx(ctx) | 669 | log := zerowrap.FromCtx(ctx) | ||
| 654 | 670 | ||||
| 655 | // ImageRemove with PruneChildren=false only removes the tag, not the image layers | 671 | // ImageRemove with PruneChildren=false only removes the tag, not the image layers | ||
| 656 | - | _, err := r.client.ImageRemove(ctx, imageRef, image.RemoveOptions{ | 672 | + | _, err := r.client.ImageRemove(ctx, imageRef, client.ImageRemoveOptions{ |
| 657 | Force: false, | 673 | Force: false, | ||
| 658 | PruneChildren: false, | 674 | PruneChildren: false, | ||
| 659 | }) | 675 | }) | ||
| @@ -676,7 +692,7 @@ func (r *Runtime) RemoveImage(ctx context.Context, imageRef string, force bool) | |||||
| 676 | }) | 692 | }) | ||
| 677 | log := zerowrap.FromCtx(ctx) | 693 | log := zerowrap.FromCtx(ctx) | ||
| 678 | 694 | ||||
| 679 | - | _, err := r.client.ImageRemove(ctx, imageRef, image.RemoveOptions{Force: force}) | 695 | + | _, err := r.client.ImageRemove(ctx, imageRef, client.ImageRemoveOptions{Force: force}) |
| 680 | if err != nil { | 696 | if err != nil { | ||
| 681 | return log.WrapErr(err, "failed to remove image") | 697 | return log.WrapErr(err, "failed to remove image") | ||
| 682 | } | 698 | } | ||
| @@ -694,13 +710,13 @@ func (r *Runtime) ListImages(ctx context.Context) ([]string, error) { | |||||
| 694 | }) | 710 | }) | ||
| 695 | log := zerowrap.FromCtx(ctx) | 711 | log := zerowrap.FromCtx(ctx) | ||
| 696 | 712 | ||||
| 697 | - | images, err := r.client.ImageList(ctx, image.ListOptions{}) | 713 | + | imageList, err := r.client.ImageList(ctx, client.ImageListOptions{}) |
| 698 | if err != nil { | 714 | if err != nil { | ||
| 699 | return nil, log.WrapErr(err, "failed to list images") | 715 | return nil, log.WrapErr(err, "failed to list images") | ||
| 700 | } | 716 | } | ||
| 701 | 717 | ||||
| 702 | var result []string | 718 | var result []string | ||
| 703 | - | for _, img := range images { | 719 | + | for _, img := range imageList.Items { |
| 704 | for _, tag := range img.RepoTags { | 720 | for _, tag := range img.RepoTags { | ||
| 705 | if tag != "<none>:<none>" { | 721 | if tag != "<none>:<none>" { | ||
| 706 | result = append(result, tag) | 722 | result = append(result, tag) | ||
| @@ -720,13 +736,13 @@ func (r *Runtime) ListImagesDetailed(ctx context.Context) ([]runtimepkg.ImageDet | |||||
| 720 | }) | 736 | }) | ||
| 721 | log := zerowrap.FromCtx(ctx) | 737 | log := zerowrap.FromCtx(ctx) | ||
| 722 | 738 | ||||
| 723 | - | images, err := r.client.ImageList(ctx, image.ListOptions{All: true}) | 739 | + | imageList, err := r.client.ImageList(ctx, client.ImageListOptions{All: true}) |
| 724 | if err != nil { | 740 | if err != nil { | ||
| 725 | return nil, log.WrapErr(err, "failed to list detailed images") | 741 | return nil, log.WrapErr(err, "failed to list detailed images") | ||
| 726 | } | 742 | } | ||
| 727 | 743 | ||||
| 728 | - | result := make([]runtimepkg.ImageDetail, 0, len(images)) | 744 | + | result := make([]runtimepkg.ImageDetail, 0, len(imageList.Items)) |
| 729 | - | for _, img := range images { | 745 | + | for _, img := range imageList.Items { |
| 730 | result = append(result, runtimepkg.ImageDetail{ | 746 | result = append(result, runtimepkg.ImageDetail{ | ||
| 731 | ID: img.ID, | 747 | ID: img.ID, | ||
| 732 | RepoTags: img.RepoTags, | 748 | RepoTags: img.RepoTags, | ||
| @@ -748,19 +764,18 @@ func (r *Runtime) PruneImages(ctx context.Context, danglingOnly bool) (runtimepk | |||||
| 748 | }) | 764 | }) | ||
| 749 | log := zerowrap.FromCtx(ctx) | 765 | log := zerowrap.FromCtx(ctx) | ||
| 750 | 766 | ||||
| 751 | - | pruneFilters := filters.NewArgs() | 767 | + | pruneFilters := make(client.Filters).Add("label", domain.LabelManaged+"=true") |
| 752 | - | pruneFilters.Add("label", domain.LabelManaged+"=true") | |||
| 753 | if danglingOnly { | 768 | if danglingOnly { | ||
| 754 | pruneFilters.Add("dangling", "true") | 769 | pruneFilters.Add("dangling", "true") | ||
| 755 | } | 770 | } | ||
| 756 | 771 | ||||
| 757 | - | pruneResult, err := r.client.ImagesPrune(ctx, pruneFilters) | 772 | + | pruneResult, err := r.client.ImagePrune(ctx, client.ImagePruneOptions{Filters: pruneFilters}) |
| 758 | if err != nil { | 773 | if err != nil { | ||
| 759 | return runtimepkg.PruneReport{}, log.WrapErr(err, "failed to prune images") | 774 | return runtimepkg.PruneReport{}, log.WrapErr(err, "failed to prune images") | ||
| 760 | } | 775 | } | ||
| 761 | 776 | ||||
| 762 | - | deletedIDs := make([]string, 0, len(pruneResult.ImagesDeleted)) | 777 | + | deletedIDs := make([]string, 0, len(pruneResult.Report.ImagesDeleted)) |
| 763 | - | for _, deleted := range pruneResult.ImagesDeleted { | 778 | + | for _, deleted := range pruneResult.Report.ImagesDeleted { |
| 764 | if deleted.Deleted != "" { | 779 | if deleted.Deleted != "" { | ||
| 765 | deletedIDs = append(deletedIDs, deleted.Deleted) | 780 | deletedIDs = append(deletedIDs, deleted.Deleted) | ||
| 766 | } | 781 | } | ||
| @@ -769,7 +784,7 @@ func (r *Runtime) PruneImages(ctx context.Context, danglingOnly bool) (runtimepk | |||||
| 769 | } | 784 | } | ||
| 770 | } | 785 | } | ||
| 771 | 786 | ||||
| 772 | - | spaceReclaimed := pruneResult.SpaceReclaimed | 787 | + | spaceReclaimed := pruneResult.Report.SpaceReclaimed |
| 773 | if spaceReclaimed > math.MaxInt64 { | 788 | if spaceReclaimed > math.MaxInt64 { | ||
| 774 | log.Warn(). | 789 | log.Warn(). | ||
| 775 | Uint64("space_reclaimed_bytes", spaceReclaimed). | 790 | Uint64("space_reclaimed_bytes", spaceReclaimed). | ||
| @@ -796,7 +811,7 @@ func (r *Runtime) Ping(ctx context.Context) error { | |||||
| 796 | }) | 811 | }) | ||
| 797 | log := zerowrap.FromCtx(ctx) | 812 | log := zerowrap.FromCtx(ctx) | ||
| 798 | 813 | ||||
| 799 | - | _, err := r.client.Ping(ctx) | 814 | + | _, err := r.client.Ping(ctx, client.PingOptions{}) |
| 800 | if err != nil { | 815 | if err != nil { | ||
| 801 | return log.WrapErr(err, "Docker ping failed") | 816 | return log.WrapErr(err, "Docker ping failed") | ||
| 802 | } | 817 | } | ||
| @@ -812,7 +827,7 @@ func (r *Runtime) Version(ctx context.Context) (string, error) { | |||||
| 812 | }) | 827 | }) | ||
| 813 | log := zerowrap.FromCtx(ctx) | 828 | log := zerowrap.FromCtx(ctx) | ||
| 814 | 829 | ||||
| 815 | - | version, err := r.client.ServerVersion(ctx) | 830 | + | version, err := r.client.ServerVersion(ctx, client.ServerVersionOptions{}) |
| 816 | if err != nil { | 831 | if err != nil { | ||
| 817 | return "", log.WrapErr(err, "failed to get Docker version") | 832 | return "", log.WrapErr(err, "failed to get Docker version") | ||
| 818 | } | 833 | } | ||
| @@ -838,10 +853,11 @@ func (r *Runtime) GetContainerHealthStatus(ctx context.Context, containerID stri | |||||
| 838 | }) | 853 | }) | ||
| 839 | log := zerowrap.FromCtx(ctx) | 854 | log := zerowrap.FromCtx(ctx) | ||
| 840 | 855 | ||||
| 841 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | 856 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) |
| 842 | if err != nil { | 857 | if err != nil { | ||
| 843 | return "", false, log.WrapErr(err, "failed to inspect container") | 858 | return "", false, log.WrapErr(err, "failed to inspect container") | ||
| 844 | } | 859 | } | ||
| 860 | + | resp := inspectResult.Container | |||
| 845 | 861 | ||||
| 846 | // Detect configured healthchecks from container config rather than relying on | 862 | // Detect configured healthchecks from container config rather than relying on | ||
| 847 | // State.Health presence. Some runtimes can expose an empty State.Health struct | 863 | // State.Health presence. Some runtimes can expose an empty State.Health struct | ||
| @@ -854,7 +870,7 @@ func (r *Runtime) GetContainerHealthStatus(ctx context.Context, containerID stri | |||||
| 854 | return "", true, nil | 870 | return "", true, nil | ||
| 855 | } | 871 | } | ||
| 856 | 872 | ||||
| 857 | - | return resp.State.Health.Status, true, nil | 873 | + | return string(resp.State.Health.Status), true, nil |
| 858 | } | 874 | } | ||
| 859 | 875 | ||||
| 860 | func hasConfiguredHealthcheck(cfg *container.Config) bool { | 876 | func hasConfiguredHealthcheck(cfg *container.Config) bool { | ||
| @@ -876,16 +892,20 @@ func (r *Runtime) GetContainerPort(ctx context.Context, containerID string, inte | |||||
| 876 | }) | 892 | }) | ||
| 877 | log := zerowrap.FromCtx(ctx) | 893 | log := zerowrap.FromCtx(ctx) | ||
| 878 | 894 | ||||
| 879 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | 895 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) |
| 880 | if err != nil { | 896 | if err != nil { | ||
| 881 | return 0, log.WrapErr(err, "failed to inspect container") | 897 | return 0, log.WrapErr(err, "failed to inspect container") | ||
| 882 | } | 898 | } | ||
| 899 | + | resp := inspectResult.Container | |||
| 883 | 900 | ||||
| 884 | if resp.NetworkSettings == nil || resp.NetworkSettings.Ports == nil { | 901 | if resp.NetworkSettings == nil || resp.NetworkSettings.Ports == nil { | ||
| 885 | return 0, fmt.Errorf("no port mappings found for container %s", containerID) | 902 | return 0, fmt.Errorf("no port mappings found for container %s", containerID) | ||
| 886 | } | 903 | } | ||
| 887 | 904 | ||||
| 888 | - | containerPort := nat.Port(fmt.Sprintf("%d/tcp", internalPort)) | 905 | + | containerPort, err := network.ParsePort(fmt.Sprintf("%d/tcp", internalPort)) |
| 906 | + | if err != nil { | |||
| 907 | + | return 0, fmt.Errorf("invalid container port %d: %w", internalPort, err) | |||
| 908 | + | } | |||
| 889 | bindings, exists := resp.NetworkSettings.Ports[containerPort] | 909 | bindings, exists := resp.NetworkSettings.Ports[containerPort] | ||
| 890 | if !exists || len(bindings) == 0 { | 910 | if !exists || len(bindings) == 0 { | ||
| 891 | return 0, fmt.Errorf("port %d not mapped for container %s", internalPort, containerID) | 911 | return 0, fmt.Errorf("port %d not mapped for container %s", internalPort, containerID) | ||
| @@ -975,16 +995,17 @@ func (r *Runtime) GetContainerExposedPorts(ctx context.Context, containerID stri | |||||
| 975 | }) | 995 | }) | ||
| 976 | log := zerowrap.FromCtx(ctx) | 996 | log := zerowrap.FromCtx(ctx) | ||
| 977 | 997 | ||||
| 978 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | 998 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) |
| 979 | if err != nil { | 999 | if err != nil { | ||
| 980 | return nil, log.WrapErr(err, "failed to inspect container") | 1000 | return nil, log.WrapErr(err, "failed to inspect container") | ||
| 981 | } | 1001 | } | ||
| 1002 | + | resp := inspectResult.Container | |||
| 982 | 1003 | ||||
| 983 | var ports []int | 1004 | var ports []int | ||
| 984 | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | 1005 | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | ||
| 985 | for portSpec := range resp.NetworkSettings.Ports { | 1006 | for portSpec := range resp.NetworkSettings.Ports { | ||
| 986 | - | // Parse port from format like "80/tcp" | 1007 | + | // Parse port from format like "80/tcp". |
| 987 | - | portStr := strings.Split(string(portSpec), "/")[0] | 1008 | + | portStr := portSpec.Port() |
| 988 | if port, err := strconv.Atoi(portStr); err == nil { | 1009 | if port, err := strconv.Atoi(portStr); err == nil { | ||
| 989 | ports = append(ports, port) | 1010 | ports = append(ports, port) | ||
| 990 | } | 1011 | } | ||
| @@ -1004,18 +1025,19 @@ func (r *Runtime) GetContainerNetworkInfo(ctx context.Context, containerID strin | |||||
| 1004 | }) | 1025 | }) | ||
| 1005 | log := zerowrap.FromCtx(ctx) | 1026 | log := zerowrap.FromCtx(ctx) | ||
| 1006 | 1027 | ||||
| 1007 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | 1028 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) |
| 1008 | if err != nil { | 1029 | if err != nil { | ||
| 1009 | return "", 0, log.WrapErr(err, "failed to inspect container") | 1030 | return "", 0, log.WrapErr(err, "failed to inspect container") | ||
| 1010 | } | 1031 | } | ||
| 1032 | + | resp := inspectResult.Container | |||
| 1011 | 1033 | ||||
| 1012 | // Get container's internal IP address | 1034 | // Get container's internal IP address | ||
| 1013 | var containerIP string | 1035 | var containerIP string | ||
| 1014 | if resp.NetworkSettings != nil && resp.NetworkSettings.Networks != nil { | 1036 | if resp.NetworkSettings != nil && resp.NetworkSettings.Networks != nil { | ||
| 1015 | // Use the first available network (usually bridge or custom network) | 1037 | // Use the first available network (usually bridge or custom network) | ||
| 1016 | for _, net := range resp.NetworkSettings.Networks { | 1038 | for _, net := range resp.NetworkSettings.Networks { | ||
| 1017 | - | if net.IPAddress != "" { | 1039 | + | if net.IPAddress.IsValid() { |
| 1018 | - | containerIP = net.IPAddress | 1040 | + | containerIP = net.IPAddress.String() |
| 1019 | break | 1041 | break | ||
| 1020 | } | 1042 | } | ||
| 1021 | } | 1043 | } | ||
| @@ -1029,9 +1051,8 @@ func (r *Runtime) GetContainerNetworkInfo(ctx context.Context, containerID strin | |||||
| 1029 | var exposedPorts []int | 1051 | var exposedPorts []int | ||
| 1030 | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | 1052 | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | ||
| 1031 | for portSpec := range resp.NetworkSettings.Ports { | 1053 | for portSpec := range resp.NetworkSettings.Ports { | ||
| 1032 | - | // Parse port from format like "80/tcp" | 1054 | + | // Parse port from format like "80/tcp". |
| 1033 | - | portStr := strings.Split(string(portSpec), "/")[0] | 1055 | + | if port, err := strconv.Atoi(portSpec.Port()); err == nil { |
| 1034 | - | if port, err := strconv.Atoi(portStr); err == nil { | |||
| 1035 | exposedPorts = append(exposedPorts, port) | 1056 | exposedPorts = append(exposedPorts, port) | ||
| 1036 | } | 1057 | } | ||
| 1037 | } | 1058 | } | ||
| @@ -1064,12 +1085,12 @@ func (r *Runtime) GetContainerNetwork(ctx context.Context, containerID string) ( | |||||
| 1064 | }) | 1085 | }) | ||
| 1065 | log := zerowrap.FromCtx(ctx) | 1086 | log := zerowrap.FromCtx(ctx) | ||
| 1066 | 1087 | ||||
| 1067 | - | resp, err := r.client.ContainerInspect(ctx, containerID) | 1088 | + | inspectResult, err := r.client.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) |
| 1068 | if err != nil { | 1089 | if err != nil { | ||
| 1069 | return "", log.WrapErr(err, "failed to inspect container") | 1090 | return "", log.WrapErr(err, "failed to inspect container") | ||
| 1070 | } | 1091 | } | ||
| 1071 | 1092 | ||||
| 1072 | - | networks := resp.NetworkSettings | 1093 | + | networks := inspectResult.Container.NetworkSettings |
| 1073 | if networks == nil || networks.Networks == nil || len(networks.Networks) == 0 { | 1094 | if networks == nil || networks.Networks == nil || len(networks.Networks) == 0 { | ||
| 1074 | return "bridge", nil | 1095 | return "bridge", nil | ||
| 1075 | } | 1096 | } | ||
| @@ -1145,7 +1166,7 @@ func (r *Runtime) VolumeExists(ctx context.Context, volumeName string) (bool, er | |||||
| 1145 | }) | 1166 | }) | ||
| 1146 | log := zerowrap.FromCtx(ctx) | 1167 | log := zerowrap.FromCtx(ctx) | ||
| 1147 | 1168 | ||||
| 1148 | - | _, err := r.client.VolumeInspect(ctx, volumeName) | 1169 | + | _, err := r.client.VolumeInspect(ctx, volumeName, client.VolumeInspectOptions{}) |
| 1149 | if err != nil { | 1170 | if err != nil { | ||
| 1150 | if cerrdefs.IsNotFound(err) { | 1171 | if cerrdefs.IsNotFound(err) { | ||
| 1151 | return false, nil | 1172 | return false, nil | ||
| @@ -1165,7 +1186,7 @@ func (r *Runtime) CreateVolume(ctx context.Context, volumeName string) error { | |||||
| 1165 | }) | 1186 | }) | ||
| 1166 | log := zerowrap.FromCtx(ctx) | 1187 | log := zerowrap.FromCtx(ctx) | ||
| 1167 | 1188 | ||||
| 1168 | - | _, err := r.client.VolumeCreate(ctx, volume.CreateOptions{ | 1189 | + | _, err := r.client.VolumeCreate(ctx, client.VolumeCreateOptions{ |
| 1169 | Name: volumeName, | 1190 | Name: volumeName, | ||
| 1170 | Labels: map[string]string{ | 1191 | Labels: map[string]string{ | ||
| 1171 | domain.LabelManaged: "true", | 1192 | domain.LabelManaged: "true", | ||
| @@ -1191,7 +1212,7 @@ func (r *Runtime) RemoveVolume(ctx context.Context, volumeName string, force boo | |||||
| 1191 | }) | 1212 | }) | ||
| 1192 | log := zerowrap.FromCtx(ctx) | 1213 | log := zerowrap.FromCtx(ctx) | ||
| 1193 | 1214 | ||||
| 1194 | - | err := r.client.VolumeRemove(ctx, volumeName, force) | 1215 | + | _, err := r.client.VolumeRemove(ctx, volumeName, client.VolumeRemoveOptions{Force: force}) |
| 1195 | if err != nil { | 1216 | if err != nil { | ||
| 1196 | if cerrdefs.IsNotFound(err) { | 1217 | if cerrdefs.IsNotFound(err) { | ||
| 1197 | log.Debug().Msg("volume not found, already removed") | 1218 | log.Debug().Msg("volume not found, already removed") | ||
| @@ -1206,20 +1227,27 @@ func (r *Runtime) RemoveVolume(ctx context.Context, volumeName string, force boo | |||||
| 1206 | 1227 | ||||
| 1207 | // ListVolumes returns all named volumes with their usage status. | 1228 | // ListVolumes returns all named volumes with their usage status. | ||
| 1208 | func (r *Runtime) ListVolumes(ctx context.Context) ([]*domain.VolumeInfo, error) { | 1229 | func (r *Runtime) ListVolumes(ctx context.Context) ([]*domain.VolumeInfo, error) { | ||
| 1209 | - | volumeList, err := r.client.VolumeList(ctx, volume.ListOptions{}) | 1230 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ |
| 1231 | + | zerowrap.FieldLayer: "adapter", | |||
| 1232 | + | zerowrap.FieldAdapter: "docker", | |||
| 1233 | + | zerowrap.FieldAction: "ListVolumes", | |||
| 1234 | + | }) | |||
| 1235 | + | log := zerowrap.FromCtx(ctx) | |||
| 1236 | + | ||||
| 1237 | + | volumeList, err := r.client.VolumeList(ctx, client.VolumeListOptions{}) | |||
| 1210 | if err != nil { | 1238 | if err != nil { | ||
| 1211 | - | return nil, fmt.Errorf("failed to list volumes: %w", err) | 1239 | + | return nil, log.WrapErr(err, "failed to list volumes") |
| 1212 | } | 1240 | } | ||
| 1213 | 1241 | ||||
| 1214 | - | // Get all containers to determine volume usage | 1242 | + | // Get all containers to determine volume usage. |
| 1215 | - | containers, err := r.client.ContainerList(ctx, container.ListOptions{All: true}) | 1243 | + | containerList, err := r.client.ContainerList(ctx, client.ContainerListOptions{All: true}) |
| 1216 | if err != nil { | 1244 | if err != nil { | ||
| 1217 | - | return nil, fmt.Errorf("failed to list containers for volume usage: %w", err) | 1245 | + | return nil, log.WrapErr(err, "failed to list containers for volume usage") |
| 1218 | } | 1246 | } | ||
| 1219 | 1247 | ||||
| 1220 | - | // Build a map of volume name -> container names | 1248 | + | // Build a map of volume name -> container names. |
| 1221 | volumeUsers := make(map[string][]string) | 1249 | volumeUsers := make(map[string][]string) | ||
| 1222 | - | for _, c := range containers { | 1250 | + | for _, c := range containerList.Items { |
| 1223 | for _, m := range c.Mounts { | 1251 | for _, m := range c.Mounts { | ||
| 1224 | if m.Type == "volume" { | 1252 | if m.Type == "volume" { | ||
| 1225 | name := "" | 1253 | name := "" | ||
| @@ -1232,11 +1260,7 @@ func (r *Runtime) ListVolumes(ctx context.Context) ([]*domain.VolumeInfo, error) | |||||
| 1232 | } | 1260 | } | ||
| 1233 | 1261 | ||||
| 1234 | var result []*domain.VolumeInfo | 1262 | var result []*domain.VolumeInfo | ||
| 1235 | - | for _, v := range volumeList.Volumes { | 1263 | + | for _, v := range volumeList.Items { |
| 1236 | - | if v == nil { | |||
| 1237 | - | continue | |||
| 1238 | - | } | |||
| 1239 | - | ||||
| 1240 | var size int64 | 1264 | var size int64 | ||
| 1241 | if v.UsageData != nil { | 1265 | if v.UsageData != nil { | ||
| 1242 | size = v.UsageData.Size | 1266 | size = v.UsageData.Size | ||
| @@ -1281,7 +1305,7 @@ func (r *Runtime) ExportVolumeArchive(ctx context.Context, req domain.VolumeArch | |||||
| 1281 | if err != nil { | 1305 | if err != nil { | ||
| 1282 | return nil, err | 1306 | return nil, err | ||
| 1283 | } | 1307 | } | ||
| 1284 | - | if err := r.client.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil { | 1308 | + | if _, err := r.client.ContainerStart(ctx, created.ID, client.ContainerStartOptions{}); err != nil { |
| 1285 | _ = cleanup() | 1309 | _ = cleanup() | ||
| 1286 | return nil, log.WrapErr(err, "failed to start volume archive helper container") | 1310 | return nil, log.WrapErr(err, "failed to start volume archive helper container") | ||
| 1287 | } | 1311 | } | ||
| @@ -1314,14 +1338,18 @@ func (r *Runtime) ensureVolumeArchiveHelperImage(ctx context.Context, imageRef s | |||||
| 1314 | return nil | 1338 | return nil | ||
| 1315 | } | 1339 | } | ||
| 1316 | 1340 | ||||
| 1317 | - | func (r *Runtime) createVolumeArchiveHelper(ctx context.Context, req domain.VolumeArchiveRequest, log zerowrap.Logger) (container.CreateResponse, func() error, error) { | 1341 | + | func (r *Runtime) createVolumeArchiveHelper(ctx context.Context, req domain.VolumeArchiveRequest, log zerowrap.Logger) (client.ContainerCreateResult, func() error, error) { |
| 1318 | cmd, err := volumeArchiveCommand(req.Compression) | 1342 | cmd, err := volumeArchiveCommand(req.Compression) | ||
| 1319 | if err != nil { | 1343 | if err != nil { | ||
| 1320 | - | return container.CreateResponse{}, nil, err | 1344 | + | return client.ContainerCreateResult{}, nil, err |
| 1321 | } | 1345 | } | ||
| 1322 | - | created, err := r.client.ContainerCreate(ctx, volumeArchiveContainerConfig(req.HelperImage, cmd), volumeArchiveHostConfig(req.VolumeName), nil, nil, fmt.Sprintf("gordon-volume-backup-%d", time.Now().UTC().UnixNano())) | 1346 | + | created, err := r.client.ContainerCreate(ctx, client.ContainerCreateOptions{ |
| 1347 | + | Config: volumeArchiveContainerConfig(req.HelperImage, cmd), | |||
| 1348 | + | HostConfig: volumeArchiveHostConfig(req.VolumeName), | |||
| 1349 | + | Name: fmt.Sprintf("gordon-volume-backup-%d", time.Now().UTC().UnixNano()), | |||
| 1350 | + | }) | |||
| 1323 | if err != nil { | 1351 | if err != nil { | ||
| 1324 | - | return container.CreateResponse{}, nil, log.WrapErr(err, "failed to create volume archive helper container") | 1352 | + | return client.ContainerCreateResult{}, nil, log.WrapErr(err, "failed to create volume archive helper container") |
| 1325 | } | 1353 | } | ||
| 1326 | var cleanupOnce sync.Once | 1354 | var cleanupOnce sync.Once | ||
| 1327 | cleanup := func() error { | 1355 | cleanup := func() error { | ||
| @@ -1363,11 +1391,12 @@ func volumeArchiveHostConfig(volumeName string) *container.HostConfig { | |||||
| 1363 | } | 1391 | } | ||
| 1364 | 1392 | ||||
| 1365 | func (r *Runtime) attachVolumeArchiveStream(ctx context.Context, containerID string, cleanup func() error, log zerowrap.Logger) (io.ReadCloser, error) { | 1393 | func (r *Runtime) attachVolumeArchiveStream(ctx context.Context, containerID string, cleanup func() error, log zerowrap.Logger) (io.ReadCloser, error) { | ||
| 1366 | - | logs, err := r.client.ContainerLogs(ctx, containerID, container.LogsOptions{ShowStdout: true, ShowStderr: true, Follow: true}) | 1394 | + | logs, err := r.client.ContainerLogs(ctx, containerID, client.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true}) |
| 1367 | if err != nil { | 1395 | if err != nil { | ||
| 1368 | return nil, log.WrapErr(err, "failed to attach volume archive stream") | 1396 | return nil, log.WrapErr(err, "failed to attach volume archive stream") | ||
| 1369 | } | 1397 | } | ||
| 1370 | - | statusCh, errCh := r.client.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) | 1398 | + | waitResult := r.client.ContainerWait(ctx, containerID, client.ContainerWaitOptions{Condition: container.WaitConditionNotRunning}) |
| 1399 | + | statusCh, errCh := waitResult.Result, waitResult.Error | |||
| 1371 | pr, pw := io.Pipe() | 1400 | pr, pw := io.Pipe() | ||
| 1372 | go streamVolumeArchiveLogs(logs, pw, statusCh, errCh, cleanup, log) | 1401 | go streamVolumeArchiveLogs(logs, pw, statusCh, errCh, cleanup, log) | ||
| 1373 | return &pipeReadCloser{pr: pr, pw: pw, original: logs, cleanup: cleanup}, nil | 1402 | return &pipeReadCloser{pr: pr, pw: pw, original: logs, cleanup: cleanup}, nil | ||
| @@ -1538,7 +1567,7 @@ func (r *Runtime) CreateNetwork(ctx context.Context, name string, config domain. | |||||
| 1538 | maps.Copy(labels, config.Labels) | 1567 | maps.Copy(labels, config.Labels) | ||
| 1539 | labels[domain.LabelManaged] = "true" | 1568 | labels[domain.LabelManaged] = "true" | ||
| 1540 | 1569 | ||||
| 1541 | - | createOptions := network.CreateOptions{ | 1570 | + | createOptions := client.NetworkCreateOptions{ |
| 1542 | Driver: driver, | 1571 | Driver: driver, | ||
| 1543 | Internal: config.Internal, | 1572 | Internal: config.Internal, | ||
| 1544 | Labels: labels, | 1573 | Labels: labels, | ||
| @@ -1563,7 +1592,7 @@ func (r *Runtime) RemoveNetwork(ctx context.Context, name string) error { | |||||
| 1563 | }) | 1592 | }) | ||
| 1564 | log := zerowrap.FromCtx(ctx) | 1593 | log := zerowrap.FromCtx(ctx) | ||
| 1565 | 1594 | ||||
| 1566 | - | err := r.client.NetworkRemove(ctx, name) | 1595 | + | _, err := r.client.NetworkRemove(ctx, name, client.NetworkRemoveOptions{}) |
| 1567 | if err != nil { | 1596 | if err != nil { | ||
| 1568 | if cerrdefs.IsNotFound(err) { | 1597 | if cerrdefs.IsNotFound(err) { | ||
| 1569 | log.Debug().Msg("network not found, already removed") | 1598 | log.Debug().Msg("network not found, already removed") | ||
| @@ -1585,15 +1614,19 @@ func (r *Runtime) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, erro | |||||
| 1585 | }) | 1614 | }) | ||
| 1586 | log := zerowrap.FromCtx(ctx) | 1615 | log := zerowrap.FromCtx(ctx) | ||
| 1587 | 1616 | ||||
| 1588 | - | networks, err := r.client.NetworkList(ctx, network.ListOptions{}) | 1617 | + | networkList, err := r.client.NetworkList(ctx, client.NetworkListOptions{}) |
| 1589 | if err != nil { | 1618 | if err != nil { | ||
| 1590 | return nil, log.WrapErr(err, "failed to list networks") | 1619 | return nil, log.WrapErr(err, "failed to list networks") | ||
| 1591 | } | 1620 | } | ||
| 1592 | 1621 | ||||
| 1593 | var result []*domain.NetworkInfo | 1622 | var result []*domain.NetworkInfo | ||
| 1594 | - | for _, net := range networks { | 1623 | + | for _, net := range networkList.Items { |
| 1624 | + | networkInspect, err := r.client.NetworkInspect(ctx, net.ID, client.NetworkInspectOptions{}) | |||
| 1625 | + | if err != nil { | |||
| 1626 | + | return nil, log.WrapErr(err, "failed to inspect network") | |||
| 1627 | + | } | |||
| 1595 | var containers []string | 1628 | var containers []string | ||
| 1596 | - | for containerID := range net.Containers { | 1629 | + | for containerID := range networkInspect.Network.Containers { |
| 1597 | containers = append(containers, containerID) | 1630 | containers = append(containers, containerID) | ||
| 1598 | } | 1631 | } | ||
| 1599 | 1632 | ||||
| @@ -1619,7 +1652,7 @@ func (r *Runtime) NetworkExists(ctx context.Context, name string) (bool, error) | |||||
| 1619 | }) | 1652 | }) | ||
| 1620 | log := zerowrap.FromCtx(ctx) | 1653 | log := zerowrap.FromCtx(ctx) | ||
| 1621 | 1654 | ||||
| 1622 | - | _, err := r.client.NetworkInspect(ctx, name, network.InspectOptions{}) | 1655 | + | _, err := r.client.NetworkInspect(ctx, name, client.NetworkInspectOptions{}) |
| 1623 | if err != nil { | 1656 | if err != nil { | ||
| 1624 | if cerrdefs.IsNotFound(err) { | 1657 | if cerrdefs.IsNotFound(err) { | ||
| 1625 | return false, nil | 1658 | return false, nil | ||
| @@ -1640,7 +1673,10 @@ func (r *Runtime) ConnectContainerToNetwork(ctx context.Context, containerName, | |||||
| 1640 | }) | 1673 | }) | ||
| 1641 | log := zerowrap.FromCtx(ctx) | 1674 | log := zerowrap.FromCtx(ctx) | ||
| 1642 | 1675 | ||||
| 1643 | - | err := r.client.NetworkConnect(ctx, networkName, containerName, &network.EndpointSettings{}) | 1676 | + | _, err := r.client.NetworkConnect(ctx, networkName, client.NetworkConnectOptions{ |
| 1677 | + | Container: containerName, | |||
| 1678 | + | EndpointConfig: &network.EndpointSettings{}, | |||
| 1679 | + | }) | |||
| 1644 | if err != nil { | 1680 | if err != nil { | ||
| 1645 | return log.WrapErr(err, "failed to connect container to network") | 1681 | return log.WrapErr(err, "failed to connect container to network") | ||
| 1646 | } | 1682 | } | ||
| @@ -1660,7 +1696,9 @@ func (r *Runtime) DisconnectContainerFromNetwork(ctx context.Context, containerN | |||||
| 1660 | }) | 1696 | }) | ||
| 1661 | log := zerowrap.FromCtx(ctx) | 1697 | log := zerowrap.FromCtx(ctx) | ||
| 1662 | 1698 | ||||
| 1663 | - | err := r.client.NetworkDisconnect(ctx, networkName, containerName, false) | 1699 | + | _, err := r.client.NetworkDisconnect(ctx, networkName, client.NetworkDisconnectOptions{ |
| 1700 | + | Container: containerName, | |||
| 1701 | + | }) | |||
| 1664 | if err != nil { | 1702 | if err != nil { | ||
| 1665 | return log.WrapErr(err, "failed to disconnect container from network") | 1703 | return log.WrapErr(err, "failed to disconnect container from network") | ||
| 1666 | } | 1704 | } | ||
| @@ -1685,7 +1723,7 @@ func (r *Runtime) ExecInContainer(ctx context.Context, containerID string, cmd [ | |||||
| 1685 | }) | 1723 | }) | ||
| 1686 | log := zerowrap.FromCtx(ctx) | 1724 | log := zerowrap.FromCtx(ctx) | ||
| 1687 | 1725 | ||||
| 1688 | - | execResp, err := r.client.ContainerExecCreate(ctx, containerID, container.ExecOptions{ | 1726 | + | execResp, err := r.client.ExecCreate(ctx, containerID, client.ExecCreateOptions{ |
| 1689 | Cmd: cmd, | 1727 | Cmd: cmd, | ||
| 1690 | AttachStdout: true, | 1728 | AttachStdout: true, | ||
| 1691 | AttachStderr: true, | 1729 | AttachStderr: true, | ||
| @@ -1701,7 +1739,7 @@ func (r *Runtime) ExecInContainer(ctx context.Context, containerID string, cmd [ | |||||
| 1701 | defer cancel() | 1739 | defer cancel() | ||
| 1702 | } | 1740 | } | ||
| 1703 | 1741 | ||||
| 1704 | - | attachResp, err := r.client.ContainerExecAttach(attachCtx, execResp.ID, container.ExecAttachOptions{}) | 1742 | + | attachResp, err := r.client.ExecAttach(attachCtx, execResp.ID, client.ExecAttachOptions{}) |
| 1705 | if err != nil { | 1743 | if err != nil { | ||
| 1706 | return nil, log.WrapErr(err, "failed to attach to exec") | 1744 | return nil, log.WrapErr(err, "failed to attach to exec") | ||
| 1707 | } | 1745 | } | ||
| @@ -1715,7 +1753,7 @@ func (r *Runtime) ExecInContainer(ctx context.Context, containerID string, cmd [ | |||||
| 1715 | return nil, log.WrapErr(err, "failed to read exec output") | 1753 | return nil, log.WrapErr(err, "failed to read exec output") | ||
| 1716 | } | 1754 | } | ||
| 1717 | 1755 | ||||
| 1718 | - | inspectResp, err := r.client.ContainerExecInspect(ctx, execResp.ID) | 1756 | + | inspectResp, err := r.client.ExecInspect(ctx, execResp.ID, client.ExecInspectOptions{}) |
| 1719 | if err != nil { | 1757 | if err != nil { | ||
| 1720 | return nil, log.WrapErr(err, "failed to inspect exec result") | 1758 | return nil, log.WrapErr(err, "failed to inspect exec result") | ||
| 1721 | } | 1759 | } | ||
| @@ -1787,15 +1825,15 @@ func (r *Runtime) CopyFromContainer(ctx context.Context, containerID, srcPath st | |||||
| 1787 | }) | 1825 | }) | ||
| 1788 | log := zerowrap.FromCtx(ctx) | 1826 | log := zerowrap.FromCtx(ctx) | ||
| 1789 | 1827 | ||||
| 1790 | - | tarReader, _, err := r.client.CopyFromContainer(ctx, containerID, srcPath) | 1828 | + | copyResult, err := r.client.CopyFromContainer(ctx, containerID, client.CopyFromContainerOptions{SourcePath: srcPath}) |
| 1791 | if err != nil { | 1829 | if err != nil { | ||
| 1792 | return nil, log.WrapErr(err, "failed to copy from container") | 1830 | return nil, log.WrapErr(err, "failed to copy from container") | ||
| 1793 | } | 1831 | } | ||
| 1794 | 1832 | ||||
| 1795 | // The response is a tar archive - we need to extract the requested file stream. | 1833 | // The response is a tar archive - we need to extract the requested file stream. | ||
| 1796 | - | reader, err := extractFileFromTar(tarReader, srcPath) | 1834 | + | reader, err := extractFileFromTar(copyResult.Content, srcPath) |
| 1797 | if err != nil { | 1835 | if err != nil { | ||
| 1798 | - | _ = tarReader.Close() | 1836 | + | _ = copyResult.Content.Close() |
| 1799 | return nil, log.WrapErr(err, "failed to extract file from tar") | 1837 | return nil, log.WrapErr(err, "failed to extract file from tar") | ||
| 1800 | } | 1838 | } | ||
| 1801 | 1839 | ||||
| @@ -1928,7 +1966,7 @@ func (r *Runtime) ExtractEnvFileFromImage(ctx context.Context, imageRef, envFile | |||||
| 1928 | Cmd: []string{"true"}, // Dummy command | 1966 | Cmd: []string{"true"}, // Dummy command | ||
| 1929 | } | 1967 | } | ||
| 1930 | 1968 | ||||
| 1931 | - | resp, err := r.client.ContainerCreate(ctx, containerConfig, nil, nil, nil, "") | 1969 | + | resp, err := r.client.ContainerCreate(ctx, client.ContainerCreateOptions{Config: containerConfig}) |
| 1932 | if err != nil { | 1970 | if err != nil { | ||
| 1933 | return nil, log.WrapErr(err, "failed to create temporary container") | 1971 | return nil, log.WrapErr(err, "failed to create temporary container") | ||
| 1934 | } | 1972 | } | ||
| @@ -1938,7 +1976,7 @@ func (r *Runtime) ExtractEnvFileFromImage(ctx context.Context, imageRef, envFile | |||||
| 1938 | 1976 | ||||
| 1939 | // Ensure cleanup | 1977 | // Ensure cleanup | ||
| 1940 | defer func() { | 1978 | defer func() { | ||
| 1941 | - | if err := r.client.ContainerRemove(ctx, tempContainerID, container.RemoveOptions{Force: true}); err != nil { | 1979 | + | if _, err := r.client.ContainerRemove(ctx, tempContainerID, client.ContainerRemoveOptions{Force: true}); err != nil { |
| 1942 | log.Warn().Err(err).Str("temp_container_id", tempContainerID).Msg("failed to remove temporary container") | 1980 | log.Warn().Err(err).Str("temp_container_id", tempContainerID).Msg("failed to remove temporary container") | ||
| 1943 | } | 1981 | } | ||
| 1944 | }() | 1982 | }() | ||
M internal/adapters/out/docker/runtime_create_test.go +2 -2
| @@ -8,7 +8,7 @@ import ( | |||
| 8 | 8 | "strings" | |
| 9 | 9 | "testing" | |
| 10 | 10 | ||
| 11 | - | "github.com/docker/docker/client" | |
| 11 | + | "github.com/moby/moby/client" | |
| 12 | 12 | "github.com/stretchr/testify/assert" | |
| 13 | 13 | "github.com/stretchr/testify/require" | |
| 14 | 14 | ||
| @@ -112,7 +112,7 @@ func createTestContainer(t *testing.T, config *domain.ContainerConfig) map[strin | |||
| 112 | 112 | defer server.Close() | |
| 113 | 113 | ||
| 114 | 114 | host := strings.TrimPrefix(server.URL, "http://") | |
| 115 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 115 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 116 | 116 | require.NoError(t, err) | |
| 117 | 117 | ||
| 118 | 118 | runtime := NewRuntimeWithClient(cli) | |
| @@ -8,7 +8,7 @@ import ( | |||||
| 8 | "strings" | 8 | "strings" | ||
| 9 | "testing" | 9 | "testing" | ||
| 10 | 10 | ||||
| 11 | - | "github.com/docker/docker/client" | 11 | + | "github.com/moby/moby/client" |
| 12 | "github.com/stretchr/testify/assert" | 12 | "github.com/stretchr/testify/assert" | ||
| 13 | "github.com/stretchr/testify/require" | 13 | "github.com/stretchr/testify/require" | ||
| 14 | 14 | ||||
| @@ -112,7 +112,7 @@ func createTestContainer(t *testing.T, config *domain.ContainerConfig) map[strin | |||||
| 112 | defer server.Close() | 112 | defer server.Close() | ||
| 113 | 113 | ||||
| 114 | host := strings.TrimPrefix(server.URL, "http://") | 114 | host := strings.TrimPrefix(server.URL, "http://") | ||
| 115 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | 115 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) |
| 116 | require.NoError(t, err) | 116 | require.NoError(t, err) | ||
| 117 | 117 | ||||
| 118 | runtime := NewRuntimeWithClient(cli) | 118 | runtime := NewRuntimeWithClient(cli) | ||
M internal/adapters/out/docker/runtime_image_cleanup_test.go +9 -9
| @@ -2,6 +2,7 @@ package docker | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "encoding/json" | |
| 5 | 6 | "math" | |
| 6 | 7 | "net/http" | |
| 7 | 8 | "net/http/httptest" | |
| @@ -9,8 +10,7 @@ import ( | |||
| 9 | 10 | "testing" | |
| 10 | 11 | "time" | |
| 11 | 12 | ||
| 12 | - | "github.com/docker/docker/api/types/filters" | |
| 13 | - | "github.com/docker/docker/client" | |
| 13 | + | "github.com/moby/moby/client" | |
| 14 | 14 | "github.com/stretchr/testify/assert" | |
| 15 | 15 | "github.com/stretchr/testify/require" | |
| 16 | 16 | ) | |
| @@ -20,9 +20,9 @@ func TestRuntime_PruneImages_DanglingOnlyFilter(t *testing.T) { | |||
| 20 | 20 | assert.Equal(t, http.MethodPost, r.Method) | |
| 21 | 21 | assert.Equal(t, "/v1.41/images/prune", r.URL.Path) | |
| 22 | 22 | ||
| 23 | - | parsedFilters, err := filters.FromJSON(r.URL.Query().Get("filters")) | |
| 24 | - | require.NoError(t, err) | |
| 25 | - | assert.Equal(t, []string{"true"}, parsedFilters.Get("dangling")) | |
| 23 | + | parsedFilters := client.Filters{} | |
| 24 | + | require.NoError(t, json.Unmarshal([]byte(r.URL.Query().Get("filters")), &parsedFilters)) | |
| 25 | + | assert.True(t, parsedFilters["dangling"]["true"]) | |
| 26 | 26 | ||
| 27 | 27 | w.Header().Set("Content-Type", "application/json") | |
| 28 | 28 | _, _ = w.Write([]byte(`{"ImagesDeleted":[{"Deleted":"sha256:abc"},{"Untagged":"example:old"}],"SpaceReclaimed":1234}`)) | |
| @@ -42,9 +42,9 @@ func TestRuntime_PruneImages_FullUnusedHasNoDanglingFilter(t *testing.T) { | |||
| 42 | 42 | assert.Equal(t, http.MethodPost, r.Method) | |
| 43 | 43 | assert.Equal(t, "/v1.41/images/prune", r.URL.Path) | |
| 44 | 44 | ||
| 45 | - | parsedFilters, err := filters.FromJSON(r.URL.Query().Get("filters")) | |
| 46 | - | require.NoError(t, err) | |
| 47 | - | assert.Empty(t, parsedFilters.Get("dangling")) | |
| 45 | + | parsedFilters := client.Filters{} | |
| 46 | + | require.NoError(t, json.Unmarshal([]byte(r.URL.Query().Get("filters")), &parsedFilters)) | |
| 47 | + | assert.Empty(t, parsedFilters["dangling"]) | |
| 48 | 48 | ||
| 49 | 49 | w.Header().Set("Content-Type", "application/json") | |
| 50 | 50 | _, _ = w.Write([]byte(`{"ImagesDeleted":[{"Deleted":"sha256:def"}],"SpaceReclaimed":5678}`)) | |
| @@ -183,7 +183,7 @@ func newRuntimeForHTTPServer(t *testing.T, server *httptest.Server) *Runtime { | |||
| 183 | 183 | t.Helper() | |
| 184 | 184 | ||
| 185 | 185 | host := strings.TrimPrefix(server.URL, "http://") | |
| 186 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 186 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 187 | 187 | require.NoError(t, err) | |
| 188 | 188 | ||
| 189 | 189 | return NewRuntimeWithClient(cli) | |
| @@ -2,6 +2,7 @@ package docker | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "encoding/json" | |||
| 5 | "math" | 6 | "math" | ||
| 6 | "net/http" | 7 | "net/http" | ||
| 7 | "net/http/httptest" | 8 | "net/http/httptest" | ||
| @@ -9,8 +10,7 @@ import ( | |||||
| 9 | "testing" | 10 | "testing" | ||
| 10 | "time" | 11 | "time" | ||
| 11 | 12 | ||||
| 12 | - | "github.com/docker/docker/api/types/filters" | 13 | + | "github.com/moby/moby/client" |
| 13 | - | "github.com/docker/docker/client" | |||
| 14 | "github.com/stretchr/testify/assert" | 14 | "github.com/stretchr/testify/assert" | ||
| 15 | "github.com/stretchr/testify/require" | 15 | "github.com/stretchr/testify/require" | ||
| 16 | ) | 16 | ) | ||
| @@ -20,9 +20,9 @@ func TestRuntime_PruneImages_DanglingOnlyFilter(t *testing.T) { | |||||
| 20 | assert.Equal(t, http.MethodPost, r.Method) | 20 | assert.Equal(t, http.MethodPost, r.Method) | ||
| 21 | assert.Equal(t, "/v1.41/images/prune", r.URL.Path) | 21 | assert.Equal(t, "/v1.41/images/prune", r.URL.Path) | ||
| 22 | 22 | ||||
| 23 | - | parsedFilters, err := filters.FromJSON(r.URL.Query().Get("filters")) | 23 | + | parsedFilters := client.Filters{} |
| 24 | - | require.NoError(t, err) | 24 | + | require.NoError(t, json.Unmarshal([]byte(r.URL.Query().Get("filters")), &parsedFilters)) |
| 25 | - | assert.Equal(t, []string{"true"}, parsedFilters.Get("dangling")) | 25 | + | assert.True(t, parsedFilters["dangling"]["true"]) |
| 26 | 26 | ||||
| 27 | w.Header().Set("Content-Type", "application/json") | 27 | w.Header().Set("Content-Type", "application/json") | ||
| 28 | _, _ = w.Write([]byte(`{"ImagesDeleted":[{"Deleted":"sha256:abc"},{"Untagged":"example:old"}],"SpaceReclaimed":1234}`)) | 28 | _, _ = w.Write([]byte(`{"ImagesDeleted":[{"Deleted":"sha256:abc"},{"Untagged":"example:old"}],"SpaceReclaimed":1234}`)) | ||
| @@ -42,9 +42,9 @@ func TestRuntime_PruneImages_FullUnusedHasNoDanglingFilter(t *testing.T) { | |||||
| 42 | assert.Equal(t, http.MethodPost, r.Method) | 42 | assert.Equal(t, http.MethodPost, r.Method) | ||
| 43 | assert.Equal(t, "/v1.41/images/prune", r.URL.Path) | 43 | assert.Equal(t, "/v1.41/images/prune", r.URL.Path) | ||
| 44 | 44 | ||||
| 45 | - | parsedFilters, err := filters.FromJSON(r.URL.Query().Get("filters")) | 45 | + | parsedFilters := client.Filters{} |
| 46 | - | require.NoError(t, err) | 46 | + | require.NoError(t, json.Unmarshal([]byte(r.URL.Query().Get("filters")), &parsedFilters)) |
| 47 | - | assert.Empty(t, parsedFilters.Get("dangling")) | 47 | + | assert.Empty(t, parsedFilters["dangling"]) |
| 48 | 48 | ||||
| 49 | w.Header().Set("Content-Type", "application/json") | 49 | w.Header().Set("Content-Type", "application/json") | ||
| 50 | _, _ = w.Write([]byte(`{"ImagesDeleted":[{"Deleted":"sha256:def"}],"SpaceReclaimed":5678}`)) | 50 | _, _ = w.Write([]byte(`{"ImagesDeleted":[{"Deleted":"sha256:def"}],"SpaceReclaimed":5678}`)) | ||
| @@ -183,7 +183,7 @@ func newRuntimeForHTTPServer(t *testing.T, server *httptest.Server) *Runtime { | |||||
| 183 | t.Helper() | 183 | t.Helper() | ||
| 184 | 184 | ||||
| 185 | host := strings.TrimPrefix(server.URL, "http://") | 185 | host := strings.TrimPrefix(server.URL, "http://") | ||
| 186 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | 186 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) |
| 187 | require.NoError(t, err) | 187 | require.NoError(t, err) | ||
| 188 | 188 | ||||
| 189 | return NewRuntimeWithClient(cli) | 189 | return NewRuntimeWithClient(cli) | ||
M internal/adapters/out/docker/runtime_test.go +3 -3
| @@ -10,8 +10,8 @@ import ( | |||
| 10 | 10 | "testing" | |
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/zerowrap" | |
| 13 | - | "github.com/docker/docker/api/types/container" | |
| 14 | - | "github.com/docker/docker/client" | |
| 13 | + | "github.com/moby/moby/api/types/container" | |
| 14 | + | "github.com/moby/moby/client" | |
| 15 | 15 | "github.com/stretchr/testify/assert" | |
| 16 | 16 | "github.com/stretchr/testify/require" | |
| 17 | 17 | ) | |
| @@ -31,7 +31,7 @@ func TestRuntime_InspectImageEnv_RedactsValuesInDebugLog(t *testing.T) { | |||
| 31 | 31 | defer server.Close() | |
| 32 | 32 | ||
| 33 | 33 | host := strings.TrimPrefix(server.URL, "http://") | |
| 34 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 34 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 35 | 35 | require.NoError(t, err) | |
| 36 | 36 | runtime := NewRuntimeWithClient(cli) | |
| 37 | 37 | ||
| @@ -10,8 +10,8 @@ import ( | |||||
| 10 | "testing" | 10 | "testing" | ||
| 11 | 11 | ||||
| 12 | "github.com/bnema/zerowrap" | 12 | "github.com/bnema/zerowrap" | ||
| 13 | - | "github.com/docker/docker/api/types/container" | 13 | + | "github.com/moby/moby/api/types/container" |
| 14 | - | "github.com/docker/docker/client" | 14 | + | "github.com/moby/moby/client" |
| 15 | "github.com/stretchr/testify/assert" | 15 | "github.com/stretchr/testify/assert" | ||
| 16 | "github.com/stretchr/testify/require" | 16 | "github.com/stretchr/testify/require" | ||
| 17 | ) | 17 | ) | ||
| @@ -31,7 +31,7 @@ func TestRuntime_InspectImageEnv_RedactsValuesInDebugLog(t *testing.T) { | |||||
| 31 | defer server.Close() | 31 | defer server.Close() | ||
| 32 | 32 | ||||
| 33 | host := strings.TrimPrefix(server.URL, "http://") | 33 | host := strings.TrimPrefix(server.URL, "http://") | ||
| 34 | - | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | 34 | + | cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client())) |
| 35 | require.NoError(t, err) | 35 | require.NoError(t, err) | ||
| 36 | runtime := NewRuntimeWithClient(cli) | 36 | runtime := NewRuntimeWithClient(cli) | ||
| 37 | 37 | ||||
M internal/adapters/out/filesystem/blob.go +38 -3
| @@ -223,6 +223,23 @@ func (s *BlobStorage) ListBlobs() ([]string, error) { | |||
| 223 | 223 | return digests, nil | |
| 224 | 224 | } | |
| 225 | 225 | ||
| 226 | + | // GetBlobModTime returns the last modification time for a blob. | |
| 227 | + | func (s *BlobStorage) GetBlobModTime(digest string) (time.Time, error) { | |
| 228 | + | blobPath, err := s.getBlobPath(digest) | |
| 229 | + | if err != nil { | |
| 230 | + | return time.Time{}, fmt.Errorf("invalid blob path: %w", err) | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | info, err := os.Stat(blobPath) | |
| 234 | + | if err != nil { | |
| 235 | + | if os.IsNotExist(err) { | |
| 236 | + | return time.Time{}, fmt.Errorf("blob not found: %s", digest) | |
| 237 | + | } | |
| 238 | + | return time.Time{}, fmt.Errorf("failed to stat blob: %w", err) | |
| 239 | + | } | |
| 240 | + | return info.ModTime(), nil | |
| 241 | + | } | |
| 242 | + | ||
| 226 | 243 | // StartBlobUpload starts a new blob upload and returns the upload UUID. | |
| 227 | 244 | func (s *BlobStorage) StartBlobUpload(name string) (string, error) { | |
| 228 | 245 | uploadID := uuid.New().String() | |
| @@ -394,7 +411,14 @@ func (s *BlobStorage) FinishBlobUpload(uuid, digest string) error { | |||
| 394 | 411 | return fmt.Errorf("failed to create blob directory: %w", err) | |
| 395 | 412 | } | |
| 396 | 413 | ||
| 397 | - | // Move upload file to blob location | |
| 414 | + | // Update the timestamp before rename so it survives a restart even when the | |
| 415 | + | // upload has been open longer than the pending-blob grace period. | |
| 416 | + | now := time.Now() | |
| 417 | + | if err := os.Chtimes(uploadPath, now, now); err != nil { | |
| 418 | + | return fmt.Errorf("failed to mark blob as finalized: %w", err) | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | // Move upload file to blob location. | |
| 398 | 422 | if err := os.Rename(uploadPath, blobPath); err != nil { | |
| 399 | 423 | return fmt.Errorf("failed to move upload to blob location: %w", err) | |
| 400 | 424 | } | |
| @@ -474,11 +498,22 @@ func (s *BlobStorage) CleanupStaleUploads(maxAge time.Duration) (int, int64, err | |||
| 474 | 498 | ||
| 475 | 499 | if info.ModTime().Before(cutoff) { | |
| 476 | 500 | path := filepath.Join(uploadsDir, entry.Name()) | |
| 477 | - | size := info.Size() | |
| 478 | 501 | ||
| 479 | 502 | mu := s.getUploadLock(entry.Name()) | |
| 480 | 503 | mu.Lock() | |
| 481 | - | ||
| 504 | + | currentInfo, err := os.Stat(path) | |
| 505 | + | if err != nil { | |
| 506 | + | mu.Unlock() | |
| 507 | + | if !os.IsNotExist(err) { | |
| 508 | + | s.log.Warn().Err(err).Str("file", entry.Name()).Msg("failed to stat stale upload") | |
| 509 | + | } | |
| 510 | + | continue | |
| 511 | + | } | |
| 512 | + | if !currentInfo.ModTime().Before(cutoff) { | |
| 513 | + | mu.Unlock() | |
| 514 | + | continue | |
| 515 | + | } | |
| 516 | + | size := currentInfo.Size() | |
| 482 | 517 | if err := os.Remove(path); err != nil { | |
| 483 | 518 | mu.Unlock() | |
| 484 | 519 | s.log.Warn().Err(err).Str("file", entry.Name()).Msg("failed to remove stale upload") | |
| @@ -223,6 +223,23 @@ func (s *BlobStorage) ListBlobs() ([]string, error) { | |||||
| 223 | return digests, nil | 223 | return digests, nil | ||
| 224 | } | 224 | } | ||
| 225 | 225 | ||||
| 226 | + | // GetBlobModTime returns the last modification time for a blob. | |||
| 227 | + | func (s *BlobStorage) GetBlobModTime(digest string) (time.Time, error) { | |||
| 228 | + | blobPath, err := s.getBlobPath(digest) | |||
| 229 | + | if err != nil { | |||
| 230 | + | return time.Time{}, fmt.Errorf("invalid blob path: %w", err) | |||
| 231 | + | } | |||
| 232 | + | ||||
| 233 | + | info, err := os.Stat(blobPath) | |||
| 234 | + | if err != nil { | |||
| 235 | + | if os.IsNotExist(err) { | |||
| 236 | + | return time.Time{}, fmt.Errorf("blob not found: %s", digest) | |||
| 237 | + | } | |||
| 238 | + | return time.Time{}, fmt.Errorf("failed to stat blob: %w", err) | |||
| 239 | + | } | |||
| 240 | + | return info.ModTime(), nil | |||
| 241 | + | } | |||
| 242 | + | ||||
| 226 | // StartBlobUpload starts a new blob upload and returns the upload UUID. | 243 | // StartBlobUpload starts a new blob upload and returns the upload UUID. | ||
| 227 | func (s *BlobStorage) StartBlobUpload(name string) (string, error) { | 244 | func (s *BlobStorage) StartBlobUpload(name string) (string, error) { | ||
| 228 | uploadID := uuid.New().String() | 245 | uploadID := uuid.New().String() | ||
| @@ -394,7 +411,14 @@ func (s *BlobStorage) FinishBlobUpload(uuid, digest string) error { | |||||
| 394 | return fmt.Errorf("failed to create blob directory: %w", err) | 411 | return fmt.Errorf("failed to create blob directory: %w", err) | ||
| 395 | } | 412 | } | ||
| 396 | 413 | ||||
| 397 | - | // Move upload file to blob location | 414 | + | // Update the timestamp before rename so it survives a restart even when the |
| 415 | + | // upload has been open longer than the pending-blob grace period. | |||
| 416 | + | now := time.Now() | |||
| 417 | + | if err := os.Chtimes(uploadPath, now, now); err != nil { | |||
| 418 | + | return fmt.Errorf("failed to mark blob as finalized: %w", err) | |||
| 419 | + | } | |||
| 420 | + | ||||
| 421 | + | // Move upload file to blob location. | |||
| 398 | if err := os.Rename(uploadPath, blobPath); err != nil { | 422 | if err := os.Rename(uploadPath, blobPath); err != nil { | ||
| 399 | return fmt.Errorf("failed to move upload to blob location: %w", err) | 423 | return fmt.Errorf("failed to move upload to blob location: %w", err) | ||
| 400 | } | 424 | } | ||
| @@ -474,11 +498,22 @@ func (s *BlobStorage) CleanupStaleUploads(maxAge time.Duration) (int, int64, err | |||||
| 474 | 498 | ||||
| 475 | if info.ModTime().Before(cutoff) { | 499 | if info.ModTime().Before(cutoff) { | ||
| 476 | path := filepath.Join(uploadsDir, entry.Name()) | 500 | path := filepath.Join(uploadsDir, entry.Name()) | ||
| 477 | - | size := info.Size() | |||
| 478 | 501 | ||||
| 479 | mu := s.getUploadLock(entry.Name()) | 502 | mu := s.getUploadLock(entry.Name()) | ||
| 480 | mu.Lock() | 503 | mu.Lock() | ||
| 481 | - | 504 | + | currentInfo, err := os.Stat(path) | |
| 505 | + | if err != nil { | |||
| 506 | + | mu.Unlock() | |||
| 507 | + | if !os.IsNotExist(err) { | |||
| 508 | + | s.log.Warn().Err(err).Str("file", entry.Name()).Msg("failed to stat stale upload") | |||
| 509 | + | } | |||
| 510 | + | continue | |||
| 511 | + | } | |||
| 512 | + | if !currentInfo.ModTime().Before(cutoff) { | |||
| 513 | + | mu.Unlock() | |||
| 514 | + | continue | |||
| 515 | + | } | |||
| 516 | + | size := currentInfo.Size() | |||
| 482 | if err := os.Remove(path); err != nil { | 517 | if err := os.Remove(path); err != nil { | ||
| 483 | mu.Unlock() | 518 | mu.Unlock() | ||
| 484 | s.log.Warn().Err(err).Str("file", entry.Name()).Msg("failed to remove stale upload") | 519 | s.log.Warn().Err(err).Str("file", entry.Name()).Msg("failed to remove stale upload") | ||
M internal/adapters/out/filesystem/blob_test.go +22 -0
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "path/filepath" | |
| 11 | 11 | "sync" | |
| 12 | 12 | "testing" | |
| 13 | + | "time" | |
| 13 | 14 | ||
| 14 | 15 | "github.com/bnema/zerowrap" | |
| 15 | 16 | "github.com/stretchr/testify/assert" | |
| @@ -680,6 +681,27 @@ func TestBlobStorage_AppendAfterFinalize(t *testing.T) { | |||
| 680 | 681 | assert.Contains(t, err.Error(), "upload not found") | |
| 681 | 682 | } | |
| 682 | 683 | ||
| 684 | + | func TestBlobStorage_FinishBlobUploadMarksBlobAsRecentlyFinalized(t *testing.T) { | |
| 685 | + | tmpDir := t.TempDir() | |
| 686 | + | storage, err := NewBlobStorage(tmpDir, testLogger()) | |
| 687 | + | require.NoError(t, err) | |
| 688 | + | ||
| 689 | + | uuid, err := storage.StartBlobUpload("myapp") | |
| 690 | + | require.NoError(t, err) | |
| 691 | + | blobData := []byte("finalized content") | |
| 692 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData), -1, 0) | |
| 693 | + | require.NoError(t, err) | |
| 694 | + | ||
| 695 | + | uploadPath := filepath.Join(tmpDir, "uploads", uuid) | |
| 696 | + | old := time.Now().Add(-2 * 24 * time.Hour) | |
| 697 | + | require.NoError(t, os.Chtimes(uploadPath, old, old)) | |
| 698 | + | ||
| 699 | + | require.NoError(t, storage.FinishBlobUpload(uuid, digestForData(blobData))) | |
| 700 | + | modTime, err := storage.GetBlobModTime(digestForData(blobData)) | |
| 701 | + | require.NoError(t, err) | |
| 702 | + | assert.WithinDuration(t, time.Now(), modTime, time.Minute) | |
| 703 | + | } | |
| 704 | + | ||
| 683 | 705 | func TestBlobStorage_FailedFinalizeRetryable(t *testing.T) { | |
| 684 | 706 | tmpDir := t.TempDir() | |
| 685 | 707 | log := testLogger() | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "path/filepath" | 10 | "path/filepath" | ||
| 11 | "sync" | 11 | "sync" | ||
| 12 | "testing" | 12 | "testing" | ||
| 13 | + | "time" | |||
| 13 | 14 | ||||
| 14 | "github.com/bnema/zerowrap" | 15 | "github.com/bnema/zerowrap" | ||
| 15 | "github.com/stretchr/testify/assert" | 16 | "github.com/stretchr/testify/assert" | ||
| @@ -680,6 +681,27 @@ func TestBlobStorage_AppendAfterFinalize(t *testing.T) { | |||||
| 680 | assert.Contains(t, err.Error(), "upload not found") | 681 | assert.Contains(t, err.Error(), "upload not found") | ||
| 681 | } | 682 | } | ||
| 682 | 683 | ||||
| 684 | + | func TestBlobStorage_FinishBlobUploadMarksBlobAsRecentlyFinalized(t *testing.T) { | |||
| 685 | + | tmpDir := t.TempDir() | |||
| 686 | + | storage, err := NewBlobStorage(tmpDir, testLogger()) | |||
| 687 | + | require.NoError(t, err) | |||
| 688 | + | ||||
| 689 | + | uuid, err := storage.StartBlobUpload("myapp") | |||
| 690 | + | require.NoError(t, err) | |||
| 691 | + | blobData := []byte("finalized content") | |||
| 692 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData), -1, 0) | |||
| 693 | + | require.NoError(t, err) | |||
| 694 | + | ||||
| 695 | + | uploadPath := filepath.Join(tmpDir, "uploads", uuid) | |||
| 696 | + | old := time.Now().Add(-2 * 24 * time.Hour) | |||
| 697 | + | require.NoError(t, os.Chtimes(uploadPath, old, old)) | |||
| 698 | + | ||||
| 699 | + | require.NoError(t, storage.FinishBlobUpload(uuid, digestForData(blobData))) | |||
| 700 | + | modTime, err := storage.GetBlobModTime(digestForData(blobData)) | |||
| 701 | + | require.NoError(t, err) | |||
| 702 | + | assert.WithinDuration(t, time.Now(), modTime, time.Minute) | |||
| 703 | + | } | |||
| 704 | + | ||||
| 683 | func TestBlobStorage_FailedFinalizeRetryable(t *testing.T) { | 705 | func TestBlobStorage_FailedFinalizeRetryable(t *testing.T) { | ||
| 684 | tmpDir := t.TempDir() | 706 | tmpDir := t.TempDir() | ||
| 685 | log := testLogger() | 707 | log := testLogger() | ||
M internal/adapters/out/logwriter/logwriter.go +1 -1
| @@ -11,7 +11,7 @@ import ( | |||
| 11 | 11 | "sync/atomic" | |
| 12 | 12 | ||
| 13 | 13 | "github.com/bnema/zerowrap" | |
| 14 | - | "github.com/docker/docker/pkg/stdcopy" | |
| 14 | + | "github.com/moby/moby/api/pkg/stdcopy" | |
| 15 | 15 | "gopkg.in/natefinch/lumberjack.v2" | |
| 16 | 16 | ) | |
| 17 | 17 | ||
| @@ -11,7 +11,7 @@ import ( | |||||
| 11 | "sync/atomic" | 11 | "sync/atomic" | ||
| 12 | 12 | ||||
| 13 | "github.com/bnema/zerowrap" | 13 | "github.com/bnema/zerowrap" | ||
| 14 | - | "github.com/docker/docker/pkg/stdcopy" | 14 | + | "github.com/moby/moby/api/pkg/stdcopy" |
| 15 | "gopkg.in/natefinch/lumberjack.v2" | 15 | "gopkg.in/natefinch/lumberjack.v2" | ||
| 16 | ) | 16 | ) | ||
| 17 | 17 | ||||
M internal/app/run.go +24 -0
| @@ -411,10 +411,31 @@ func initConfig(configPath string) (*viper.Viper, Config, error) { | |||
| 411 | 411 | if err := v.Unmarshal(&cfg); err != nil { | |
| 412 | 412 | return nil, Config{}, fmt.Errorf("failed to unmarshal config: %w", err) | |
| 413 | 413 | } | |
| 414 | + | if err := validateEntrypointMigration(v, cfg); err != nil { | |
| 415 | + | return nil, Config{}, err | |
| 416 | + | } | |
| 414 | 417 | ||
| 415 | 418 | return v, cfg, nil | |
| 416 | 419 | } | |
| 417 | 420 | ||
| 421 | + | func validateEntrypointMigration(v *viper.Viper, cfg Config) error { | |
| 422 | + | if len(cfg.EntryPoints) > 0 { | |
| 423 | + | return nil | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | legacyKeys := make([]string, 0, 2) | |
| 427 | + | for _, key := range []string{"server.port", "server.tls_port"} { | |
| 428 | + | if v.IsSet(key) { | |
| 429 | + | legacyKeys = append(legacyKeys, key) | |
| 430 | + | } | |
| 431 | + | } | |
| 432 | + | if len(legacyKeys) == 0 { | |
| 433 | + | return nil | |
| 434 | + | } | |
| 435 | + | ||
| 436 | + | return fmt.Errorf("legacy %s configuration requires at least one [entrypoints] entry; see docs/upgrading.md", strings.Join(legacyKeys, " or ")) | |
| 437 | + | } | |
| 438 | + | ||
| 418 | 439 | // initLogger initializes the zerowrap logger. | |
| 419 | 440 | func initLogger(cfg Config) (zerowrap.Logger, func(), error) { | |
| 420 | 441 | logConfig := zerowrap.Config{ | |
| @@ -1835,6 +1856,9 @@ func (c *reloadCoordinator) applyLoadedConfig(ctx context.Context, now time.Time | |||
| 1835 | 1856 | c.log.Error().Err(err).Msg("failed to unmarshal config on reload") | |
| 1836 | 1857 | return fmt.Errorf("failed to unmarshal config on reload: %w", err) | |
| 1837 | 1858 | } | |
| 1859 | + | if err := validateEntrypointMigration(c.v, reloadCfg); err != nil { | |
| 1860 | + | return err | |
| 1861 | + | } | |
| 1838 | 1862 | ||
| 1839 | 1863 | reloadedProxy, err := buildProxyConfig(reloadCfg, c.log) | |
| 1840 | 1864 | if err != nil { | |
| @@ -411,10 +411,31 @@ func initConfig(configPath string) (*viper.Viper, Config, error) { | |||||
| 411 | if err := v.Unmarshal(&cfg); err != nil { | 411 | if err := v.Unmarshal(&cfg); err != nil { | ||
| 412 | return nil, Config{}, fmt.Errorf("failed to unmarshal config: %w", err) | 412 | return nil, Config{}, fmt.Errorf("failed to unmarshal config: %w", err) | ||
| 413 | } | 413 | } | ||
| 414 | + | if err := validateEntrypointMigration(v, cfg); err != nil { | |||
| 415 | + | return nil, Config{}, err | |||
| 416 | + | } | |||
| 414 | 417 | ||||
| 415 | return v, cfg, nil | 418 | return v, cfg, nil | ||
| 416 | } | 419 | } | ||
| 417 | 420 | ||||
| 421 | + | func validateEntrypointMigration(v *viper.Viper, cfg Config) error { | |||
| 422 | + | if len(cfg.EntryPoints) > 0 { | |||
| 423 | + | return nil | |||
| 424 | + | } | |||
| 425 | + | ||||
| 426 | + | legacyKeys := make([]string, 0, 2) | |||
| 427 | + | for _, key := range []string{"server.port", "server.tls_port"} { | |||
| 428 | + | if v.IsSet(key) { | |||
| 429 | + | legacyKeys = append(legacyKeys, key) | |||
| 430 | + | } | |||
| 431 | + | } | |||
| 432 | + | if len(legacyKeys) == 0 { | |||
| 433 | + | return nil | |||
| 434 | + | } | |||
| 435 | + | ||||
| 436 | + | return fmt.Errorf("legacy %s configuration requires at least one [entrypoints] entry; see docs/upgrading.md", strings.Join(legacyKeys, " or ")) | |||
| 437 | + | } | |||
| 438 | + | ||||
| 418 | // initLogger initializes the zerowrap logger. | 439 | // initLogger initializes the zerowrap logger. | ||
| 419 | func initLogger(cfg Config) (zerowrap.Logger, func(), error) { | 440 | func initLogger(cfg Config) (zerowrap.Logger, func(), error) { | ||
| 420 | logConfig := zerowrap.Config{ | 441 | logConfig := zerowrap.Config{ | ||
| @@ -1835,6 +1856,9 @@ func (c *reloadCoordinator) applyLoadedConfig(ctx context.Context, now time.Time | |||||
| 1835 | c.log.Error().Err(err).Msg("failed to unmarshal config on reload") | 1856 | c.log.Error().Err(err).Msg("failed to unmarshal config on reload") | ||
| 1836 | return fmt.Errorf("failed to unmarshal config on reload: %w", err) | 1857 | return fmt.Errorf("failed to unmarshal config on reload: %w", err) | ||
| 1837 | } | 1858 | } | ||
| 1859 | + | if err := validateEntrypointMigration(c.v, reloadCfg); err != nil { | |||
| 1860 | + | return err | |||
| 1861 | + | } | |||
| 1838 | 1862 | ||||
| 1839 | reloadedProxy, err := buildProxyConfig(reloadCfg, c.log) | 1863 | reloadedProxy, err := buildProxyConfig(reloadCfg, c.log) | ||
| 1840 | if err != nil { | 1864 | if err != nil { | ||
A internal/app/run_entrypoint_migration_test.go +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | package app | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "os" | |
| 5 | + | "path/filepath" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/stretchr/testify/require" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestInitConfigRejectsLegacyPortsWithoutEntrypoints(t *testing.T) { | |
| 12 | + | configPath := filepath.Join(t.TempDir(), "gordon.toml") | |
| 13 | + | require.NoError(t, os.WriteFile(configPath, []byte("[server]\nport = 8088\ntls_port = 8443\n"), 0o600)) | |
| 14 | + | ||
| 15 | + | _, _, err := initConfig(configPath) | |
| 16 | + | ||
| 17 | + | require.ErrorContains(t, err, "legacy server.port or server.tls_port configuration requires at least one [entrypoints] entry") | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func TestInitConfigAcceptsLegacyPortsWithEntrypoint(t *testing.T) { | |
| 21 | + | configPath := filepath.Join(t.TempDir(), "gordon.toml") | |
| 22 | + | require.NoError(t, os.WriteFile(configPath, []byte("[server]\nport = 8088\n\n[entrypoints.edge]\naddress = \":443\"\nprotocol = \"smart_tcp\"\n"), 0o600)) | |
| 23 | + | ||
| 24 | + | _, cfg, err := initConfig(configPath) | |
| 25 | + | ||
| 26 | + | require.NoError(t, err) | |
| 27 | + | require.Contains(t, cfg.EntryPoints, "edge") | |
| 28 | + | } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | package app | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "os" | |||
| 5 | + | "path/filepath" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "github.com/stretchr/testify/require" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestInitConfigRejectsLegacyPortsWithoutEntrypoints(t *testing.T) { | |||
| 12 | + | configPath := filepath.Join(t.TempDir(), "gordon.toml") | |||
| 13 | + | require.NoError(t, os.WriteFile(configPath, []byte("[server]\nport = 8088\ntls_port = 8443\n"), 0o600)) | |||
| 14 | + | ||||
| 15 | + | _, _, err := initConfig(configPath) | |||
| 16 | + | ||||
| 17 | + | require.ErrorContains(t, err, "legacy server.port or server.tls_port configuration requires at least one [entrypoints] entry") | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func TestInitConfigAcceptsLegacyPortsWithEntrypoint(t *testing.T) { | |||
| 21 | + | configPath := filepath.Join(t.TempDir(), "gordon.toml") | |||
| 22 | + | require.NoError(t, os.WriteFile(configPath, []byte("[server]\nport = 8088\n\n[entrypoints.edge]\naddress = \":443\"\nprotocol = \"smart_tcp\"\n"), 0o600)) | |||
| 23 | + | ||||
| 24 | + | _, cfg, err := initConfig(configPath) | |||
| 25 | + | ||||
| 26 | + | require.NoError(t, err) | |||
| 27 | + | require.Contains(t, cfg.EntryPoints, "edge") | |||
| 28 | + | } | |||
M internal/boundaries/out/mocks/mock_blob_storage.go +57 -0
| @@ -646,6 +646,63 @@ func (_c *MockBlobStorage_ListBlobs_Call) RunAndReturn(run func() ([]string, err | |||
| 646 | 646 | return _c | |
| 647 | 647 | } | |
| 648 | 648 | ||
| 649 | + | // GetBlobModTime provides a mock function for the type MockBlobStorage. | |
| 650 | + | func (_mock *MockBlobStorage) GetBlobModTime(digest string) (time.Time, error) { | |
| 651 | + | ret := _mock.Called(digest) | |
| 652 | + | ||
| 653 | + | if len(ret) == 0 { | |
| 654 | + | panic("no return value specified for GetBlobModTime") | |
| 655 | + | } | |
| 656 | + | ||
| 657 | + | var r0 time.Time | |
| 658 | + | var r1 error | |
| 659 | + | if returnFunc, ok := ret.Get(0).(func(string) (time.Time, error)); ok { | |
| 660 | + | return returnFunc(digest) | |
| 661 | + | } | |
| 662 | + | if returnFunc, ok := ret.Get(0).(func(string) time.Time); ok { | |
| 663 | + | r0 = returnFunc(digest) | |
| 664 | + | } else if ret.Get(0) != nil { | |
| 665 | + | r0 = ret.Get(0).(time.Time) | |
| 666 | + | } | |
| 667 | + | if returnFunc, ok := ret.Get(1).(func(string) error); ok { | |
| 668 | + | r1 = returnFunc(digest) | |
| 669 | + | } else { | |
| 670 | + | r1 = ret.Error(1) | |
| 671 | + | } | |
| 672 | + | return r0, r1 | |
| 673 | + | } | |
| 674 | + | ||
| 675 | + | // MockBlobStorage_GetBlobModTime_Call is a typed expectation for GetBlobModTime. | |
| 676 | + | type MockBlobStorage_GetBlobModTime_Call struct { | |
| 677 | + | *mock.Call | |
| 678 | + | } | |
| 679 | + | ||
| 680 | + | // GetBlobModTime is a helper method to define mock.On call. | |
| 681 | + | func (_e *MockBlobStorage_Expecter) GetBlobModTime(digest any) *MockBlobStorage_GetBlobModTime_Call { | |
| 682 | + | return &MockBlobStorage_GetBlobModTime_Call{Call: _e.mock.On("GetBlobModTime", digest)} | |
| 683 | + | } | |
| 684 | + | ||
| 685 | + | func (_c *MockBlobStorage_GetBlobModTime_Call) Run(run func(digest string)) *MockBlobStorage_GetBlobModTime_Call { | |
| 686 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 687 | + | var arg0 string | |
| 688 | + | if args[0] != nil { | |
| 689 | + | arg0 = args[0].(string) | |
| 690 | + | } | |
| 691 | + | run(arg0) | |
| 692 | + | }) | |
| 693 | + | return _c | |
| 694 | + | } | |
| 695 | + | ||
| 696 | + | func (_c *MockBlobStorage_GetBlobModTime_Call) Return(modTime time.Time, err error) *MockBlobStorage_GetBlobModTime_Call { | |
| 697 | + | _c.Call.Return(modTime, err) | |
| 698 | + | return _c | |
| 699 | + | } | |
| 700 | + | ||
| 701 | + | func (_c *MockBlobStorage_GetBlobModTime_Call) RunAndReturn(run func(digest string) (time.Time, error)) *MockBlobStorage_GetBlobModTime_Call { | |
| 702 | + | _c.Call.Return(run) | |
| 703 | + | return _c | |
| 704 | + | } | |
| 705 | + | ||
| 649 | 706 | // PutBlob provides a mock function for the type MockBlobStorage | |
| 650 | 707 | func (_mock *MockBlobStorage) PutBlob(digest string, data io.Reader, size int64) error { | |
| 651 | 708 | ret := _mock.Called(digest, data, size) | |
| @@ -646,6 +646,63 @@ func (_c *MockBlobStorage_ListBlobs_Call) RunAndReturn(run func() ([]string, err | |||||
| 646 | return _c | 646 | return _c | ||
| 647 | } | 647 | } | ||
| 648 | 648 | ||||
| 649 | + | // GetBlobModTime provides a mock function for the type MockBlobStorage. | |||
| 650 | + | func (_mock *MockBlobStorage) GetBlobModTime(digest string) (time.Time, error) { | |||
| 651 | + | ret := _mock.Called(digest) | |||
| 652 | + | ||||
| 653 | + | if len(ret) == 0 { | |||
| 654 | + | panic("no return value specified for GetBlobModTime") | |||
| 655 | + | } | |||
| 656 | + | ||||
| 657 | + | var r0 time.Time | |||
| 658 | + | var r1 error | |||
| 659 | + | if returnFunc, ok := ret.Get(0).(func(string) (time.Time, error)); ok { | |||
| 660 | + | return returnFunc(digest) | |||
| 661 | + | } | |||
| 662 | + | if returnFunc, ok := ret.Get(0).(func(string) time.Time); ok { | |||
| 663 | + | r0 = returnFunc(digest) | |||
| 664 | + | } else if ret.Get(0) != nil { | |||
| 665 | + | r0 = ret.Get(0).(time.Time) | |||
| 666 | + | } | |||
| 667 | + | if returnFunc, ok := ret.Get(1).(func(string) error); ok { | |||
| 668 | + | r1 = returnFunc(digest) | |||
| 669 | + | } else { | |||
| 670 | + | r1 = ret.Error(1) | |||
| 671 | + | } | |||
| 672 | + | return r0, r1 | |||
| 673 | + | } | |||
| 674 | + | ||||
| 675 | + | // MockBlobStorage_GetBlobModTime_Call is a typed expectation for GetBlobModTime. | |||
| 676 | + | type MockBlobStorage_GetBlobModTime_Call struct { | |||
| 677 | + | *mock.Call | |||
| 678 | + | } | |||
| 679 | + | ||||
| 680 | + | // GetBlobModTime is a helper method to define mock.On call. | |||
| 681 | + | func (_e *MockBlobStorage_Expecter) GetBlobModTime(digest any) *MockBlobStorage_GetBlobModTime_Call { | |||
| 682 | + | return &MockBlobStorage_GetBlobModTime_Call{Call: _e.mock.On("GetBlobModTime", digest)} | |||
| 683 | + | } | |||
| 684 | + | ||||
| 685 | + | func (_c *MockBlobStorage_GetBlobModTime_Call) Run(run func(digest string)) *MockBlobStorage_GetBlobModTime_Call { | |||
| 686 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 687 | + | var arg0 string | |||
| 688 | + | if args[0] != nil { | |||
| 689 | + | arg0 = args[0].(string) | |||
| 690 | + | } | |||
| 691 | + | run(arg0) | |||
| 692 | + | }) | |||
| 693 | + | return _c | |||
| 694 | + | } | |||
| 695 | + | ||||
| 696 | + | func (_c *MockBlobStorage_GetBlobModTime_Call) Return(modTime time.Time, err error) *MockBlobStorage_GetBlobModTime_Call { | |||
| 697 | + | _c.Call.Return(modTime, err) | |||
| 698 | + | return _c | |||
| 699 | + | } | |||
| 700 | + | ||||
| 701 | + | func (_c *MockBlobStorage_GetBlobModTime_Call) RunAndReturn(run func(digest string) (time.Time, error)) *MockBlobStorage_GetBlobModTime_Call { | |||
| 702 | + | _c.Call.Return(run) | |||
| 703 | + | return _c | |||
| 704 | + | } | |||
| 705 | + | ||||
| 649 | // PutBlob provides a mock function for the type MockBlobStorage | 706 | // PutBlob provides a mock function for the type MockBlobStorage | ||
| 650 | func (_mock *MockBlobStorage) PutBlob(digest string, data io.Reader, size int64) error { | 707 | func (_mock *MockBlobStorage) PutBlob(digest string, data io.Reader, size int64) error { | ||
| 651 | ret := _mock.Called(digest, data, size) | 708 | ret := _mock.Called(digest, data, size) | ||
M internal/boundaries/out/storage.go +3 -0
| @@ -26,6 +26,9 @@ type BlobStorage interface { | |||
| 26 | 26 | // ListBlobs returns all blob digests. | |
| 27 | 27 | ListBlobs() ([]string, error) | |
| 28 | 28 | ||
| 29 | + | // GetBlobModTime returns the last modification time for a blob. | |
| 30 | + | GetBlobModTime(digest string) (time.Time, error) | |
| 31 | + | ||
| 29 | 32 | // StartBlobUpload starts a new blob upload and returns the upload UUID. | |
| 30 | 33 | StartBlobUpload(name string) (string, error) | |
| 31 | 34 | ||
| @@ -26,6 +26,9 @@ type BlobStorage interface { | |||||
| 26 | // ListBlobs returns all blob digests. | 26 | // ListBlobs returns all blob digests. | ||
| 27 | ListBlobs() ([]string, error) | 27 | ListBlobs() ([]string, error) | ||
| 28 | 28 | ||||
| 29 | + | // GetBlobModTime returns the last modification time for a blob. | |||
| 30 | + | GetBlobModTime(digest string) (time.Time, error) | |||
| 31 | + | ||||
| 29 | // StartBlobUpload starts a new blob upload and returns the upload UUID. | 32 | // StartBlobUpload starts a new blob upload and returns the upload UUID. | ||
| 30 | StartBlobUpload(name string) (string, error) | 33 | StartBlobUpload(name string) (string, error) | ||
| 31 | 34 | ||||
M internal/usecase/images/service.go +35 -15
| @@ -248,26 +248,17 @@ func (s *Service) PruneRegistry(ctx context.Context, keepLast int) (domain.Image | |||
| 248 | 248 | } | |
| 249 | 249 | } | |
| 250 | 250 | ||
| 251 | - | for digest := range s.registryState.PendingDigests(time.Now().UTC()) { | |
| 251 | + | now := time.Now().UTC() | |
| 252 | + | for digest := range s.registryState.PendingDigests(now) { | |
| 252 | 253 | referencedDigests[digest] = struct{}{} | |
| 253 | 254 | } | |
| 254 | 255 | ||
| 255 | - | blobs, err := s.blobStorage.ListBlobs() | |
| 256 | + | blobsRemoved, spaceReclaimed, err := s.pruneUnreferencedBlobs(log, now, referencedDigests) | |
| 256 | 257 | if err != nil { | |
| 257 | - | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to list blobs") | |
| 258 | - | } | |
| 259 | - | ||
| 260 | - | for _, digest := range blobs { | |
| 261 | - | if _, referenced := referencedDigests[digest]; referenced { | |
| 262 | - | continue | |
| 263 | - | } | |
| 264 | - | size, err := s.blobStorage.DeleteBlob(digest) | |
| 265 | - | if err != nil { | |
| 266 | - | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to delete blob") | |
| 267 | - | } | |
| 268 | - | report.Registry.BlobsRemoved++ | |
| 269 | - | report.Registry.SpaceReclaimed += size | |
| 258 | + | return domain.ImagePruneReport{}, err | |
| 270 | 259 | } | |
| 260 | + | report.Registry.BlobsRemoved = blobsRemoved | |
| 261 | + | report.Registry.SpaceReclaimed = spaceReclaimed | |
| 271 | 262 | ||
| 272 | 263 | // Clean up stale uploads (abandoned push operations) | |
| 273 | 264 | uploadsRemoved, uploadBytes, err := s.blobStorage.CleanupStaleUploads(24 * time.Hour) | |
| @@ -281,6 +272,35 @@ func (s *Service) PruneRegistry(ctx context.Context, keepLast int) (domain.Image | |||
| 281 | 272 | return report, nil | |
| 282 | 273 | } | |
| 283 | 274 | ||
| 275 | + | func (s *Service) pruneUnreferencedBlobs(log zerowrap.Logger, now time.Time, referencedDigests map[string]struct{}) (int, int64, error) { | |
| 276 | + | blobs, err := s.blobStorage.ListBlobs() | |
| 277 | + | if err != nil { | |
| 278 | + | return 0, 0, log.WrapErr(err, "failed to list blobs") | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | var removed int | |
| 282 | + | var spaceReclaimed int64 | |
| 283 | + | for _, digest := range blobs { | |
| 284 | + | if _, referenced := referencedDigests[digest]; referenced { | |
| 285 | + | continue | |
| 286 | + | } | |
| 287 | + | modTime, err := s.blobStorage.GetBlobModTime(digest) | |
| 288 | + | if err != nil { | |
| 289 | + | return 0, 0, log.WrapErr(err, "failed to get blob modification time") | |
| 290 | + | } | |
| 291 | + | if now.Sub(modTime) <= registrystate.PendingBlobTTL { | |
| 292 | + | continue | |
| 293 | + | } | |
| 294 | + | size, err := s.blobStorage.DeleteBlob(digest) | |
| 295 | + | if err != nil { | |
| 296 | + | return 0, 0, log.WrapErr(err, "failed to delete blob") | |
| 297 | + | } | |
| 298 | + | removed++ | |
| 299 | + | spaceReclaimed += size | |
| 300 | + | } | |
| 301 | + | return removed, spaceReclaimed, nil | |
| 302 | + | } | |
| 303 | + | ||
| 284 | 304 | func (s *Service) loadRepositoryTagInfos(repository string) ([]registryTag, error) { | |
| 285 | 305 | tags, err := s.manifestStorage.ListTags(repository) | |
| 286 | 306 | if err != nil { | |
| @@ -248,26 +248,17 @@ func (s *Service) PruneRegistry(ctx context.Context, keepLast int) (domain.Image | |||||
| 248 | } | 248 | } | ||
| 249 | } | 249 | } | ||
| 250 | 250 | ||||
| 251 | - | for digest := range s.registryState.PendingDigests(time.Now().UTC()) { | 251 | + | now := time.Now().UTC() |
| 252 | + | for digest := range s.registryState.PendingDigests(now) { | |||
| 252 | referencedDigests[digest] = struct{}{} | 253 | referencedDigests[digest] = struct{}{} | ||
| 253 | } | 254 | } | ||
| 254 | 255 | ||||
| 255 | - | blobs, err := s.blobStorage.ListBlobs() | 256 | + | blobsRemoved, spaceReclaimed, err := s.pruneUnreferencedBlobs(log, now, referencedDigests) |
| 256 | if err != nil { | 257 | if err != nil { | ||
| 257 | - | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to list blobs") | 258 | + | return domain.ImagePruneReport{}, err |
| 258 | - | } | |||
| 259 | - | ||||
| 260 | - | for _, digest := range blobs { | |||
| 261 | - | if _, referenced := referencedDigests[digest]; referenced { | |||
| 262 | - | continue | |||
| 263 | - | } | |||
| 264 | - | size, err := s.blobStorage.DeleteBlob(digest) | |||
| 265 | - | if err != nil { | |||
| 266 | - | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to delete blob") | |||
| 267 | - | } | |||
| 268 | - | report.Registry.BlobsRemoved++ | |||
| 269 | - | report.Registry.SpaceReclaimed += size | |||
| 270 | } | 259 | } | ||
| 260 | + | report.Registry.BlobsRemoved = blobsRemoved | |||
| 261 | + | report.Registry.SpaceReclaimed = spaceReclaimed | |||
| 271 | 262 | ||||
| 272 | // Clean up stale uploads (abandoned push operations) | 263 | // Clean up stale uploads (abandoned push operations) | ||
| 273 | uploadsRemoved, uploadBytes, err := s.blobStorage.CleanupStaleUploads(24 * time.Hour) | 264 | uploadsRemoved, uploadBytes, err := s.blobStorage.CleanupStaleUploads(24 * time.Hour) | ||
| @@ -281,6 +272,35 @@ func (s *Service) PruneRegistry(ctx context.Context, keepLast int) (domain.Image | |||||
| 281 | return report, nil | 272 | return report, nil | ||
| 282 | } | 273 | } | ||
| 283 | 274 | ||||
| 275 | + | func (s *Service) pruneUnreferencedBlobs(log zerowrap.Logger, now time.Time, referencedDigests map[string]struct{}) (int, int64, error) { | |||
| 276 | + | blobs, err := s.blobStorage.ListBlobs() | |||
| 277 | + | if err != nil { | |||
| 278 | + | return 0, 0, log.WrapErr(err, "failed to list blobs") | |||
| 279 | + | } | |||
| 280 | + | ||||
| 281 | + | var removed int | |||
| 282 | + | var spaceReclaimed int64 | |||
| 283 | + | for _, digest := range blobs { | |||
| 284 | + | if _, referenced := referencedDigests[digest]; referenced { | |||
| 285 | + | continue | |||
| 286 | + | } | |||
| 287 | + | modTime, err := s.blobStorage.GetBlobModTime(digest) | |||
| 288 | + | if err != nil { | |||
| 289 | + | return 0, 0, log.WrapErr(err, "failed to get blob modification time") | |||
| 290 | + | } | |||
| 291 | + | if now.Sub(modTime) <= registrystate.PendingBlobTTL { | |||
| 292 | + | continue | |||
| 293 | + | } | |||
| 294 | + | size, err := s.blobStorage.DeleteBlob(digest) | |||
| 295 | + | if err != nil { | |||
| 296 | + | return 0, 0, log.WrapErr(err, "failed to delete blob") | |||
| 297 | + | } | |||
| 298 | + | removed++ | |||
| 299 | + | spaceReclaimed += size | |||
| 300 | + | } | |||
| 301 | + | return removed, spaceReclaimed, nil | |||
| 302 | + | } | |||
| 303 | + | ||||
| 284 | func (s *Service) loadRepositoryTagInfos(repository string) ([]registryTag, error) { | 304 | func (s *Service) loadRepositoryTagInfos(repository string) ([]registryTag, error) { | ||
| 285 | tags, err := s.manifestStorage.ListTags(repository) | 305 | tags, err := s.manifestStorage.ListTags(repository) | ||
| 286 | if err != nil { | 306 | if err != nil { | ||
M internal/usecase/images/service_test.go +20 -0
| @@ -333,6 +333,21 @@ func TestService_PruneRegistry_GarbageCollectsUnreferencedBlobs(t *testing.T) { | |||
| 333 | 333 | assert.Equal(t, []string{"sha256:orphan"}, blobStorage.deletedBlobs) | |
| 334 | 334 | } | |
| 335 | 335 | ||
| 336 | + | func TestService_PruneRegistry_PreservesRecentlyFinalizedBlobAfterRestart(t *testing.T) { | |
| 337 | + | manifestStorage := newFakeManifestStorage() | |
| 338 | + | blobStorage := &fakeBlobStorage{ | |
| 339 | + | blobs: []string{"sha256:recent"}, | |
| 340 | + | blobModTimes: map[string]time.Time{"sha256:recent": time.Now().UTC()}, | |
| 341 | + | } | |
| 342 | + | svc := NewService(&fakeRuntime{}, manifestStorage, blobStorage, zerowrap.Default(), registrystate.New()) | |
| 343 | + | ||
| 344 | + | report, err := svc.PruneRegistry(context.Background(), 1) | |
| 345 | + | ||
| 346 | + | require.NoError(t, err) | |
| 347 | + | assert.Zero(t, report.Registry.BlobsRemoved) | |
| 348 | + | assert.Empty(t, blobStorage.deletedBlobs) | |
| 349 | + | } | |
| 350 | + | ||
| 336 | 351 | func TestService_PruneRegistry_PreservesPendingBlob(t *testing.T) { | |
| 337 | 352 | manifestStorage := newFakeManifestStorage() | |
| 338 | 353 | manifestStorage.repositories = []string{"gordon/api"} | |
| @@ -687,6 +702,7 @@ type fakeBlobStorage struct { | |||
| 687 | 702 | ||
| 688 | 703 | blobs []string | |
| 689 | 704 | blobSizes map[string]int64 | |
| 705 | + | blobModTimes map[string]time.Time | |
| 690 | 706 | deletedBlobs []string | |
| 691 | 707 | } | |
| 692 | 708 | ||
| @@ -813,6 +829,10 @@ func (f *fakeBlobStorage) ListBlobs() ([]string, error) { | |||
| 813 | 829 | return append([]string(nil), f.blobs...), nil | |
| 814 | 830 | } | |
| 815 | 831 | ||
| 832 | + | func (f *fakeBlobStorage) GetBlobModTime(digest string) (time.Time, error) { | |
| 833 | + | return f.blobModTimes[digest], nil | |
| 834 | + | } | |
| 835 | + | ||
| 816 | 836 | func (f *fakeBlobStorage) DeleteBlob(digest string) (int64, error) { | |
| 817 | 837 | f.deletedBlobs = append(f.deletedBlobs, digest) | |
| 818 | 838 | var size int64 | |
| @@ -333,6 +333,21 @@ func TestService_PruneRegistry_GarbageCollectsUnreferencedBlobs(t *testing.T) { | |||||
| 333 | assert.Equal(t, []string{"sha256:orphan"}, blobStorage.deletedBlobs) | 333 | assert.Equal(t, []string{"sha256:orphan"}, blobStorage.deletedBlobs) | ||
| 334 | } | 334 | } | ||
| 335 | 335 | ||||
| 336 | + | func TestService_PruneRegistry_PreservesRecentlyFinalizedBlobAfterRestart(t *testing.T) { | |||
| 337 | + | manifestStorage := newFakeManifestStorage() | |||
| 338 | + | blobStorage := &fakeBlobStorage{ | |||
| 339 | + | blobs: []string{"sha256:recent"}, | |||
| 340 | + | blobModTimes: map[string]time.Time{"sha256:recent": time.Now().UTC()}, | |||
| 341 | + | } | |||
| 342 | + | svc := NewService(&fakeRuntime{}, manifestStorage, blobStorage, zerowrap.Default(), registrystate.New()) | |||
| 343 | + | ||||
| 344 | + | report, err := svc.PruneRegistry(context.Background(), 1) | |||
| 345 | + | ||||
| 346 | + | require.NoError(t, err) | |||
| 347 | + | assert.Zero(t, report.Registry.BlobsRemoved) | |||
| 348 | + | assert.Empty(t, blobStorage.deletedBlobs) | |||
| 349 | + | } | |||
| 350 | + | ||||
| 336 | func TestService_PruneRegistry_PreservesPendingBlob(t *testing.T) { | 351 | func TestService_PruneRegistry_PreservesPendingBlob(t *testing.T) { | ||
| 337 | manifestStorage := newFakeManifestStorage() | 352 | manifestStorage := newFakeManifestStorage() | ||
| 338 | manifestStorage.repositories = []string{"gordon/api"} | 353 | manifestStorage.repositories = []string{"gordon/api"} | ||
| @@ -687,6 +702,7 @@ type fakeBlobStorage struct { | |||||
| 687 | 702 | ||||
| 688 | blobs []string | 703 | blobs []string | ||
| 689 | blobSizes map[string]int64 | 704 | blobSizes map[string]int64 | ||
| 705 | + | blobModTimes map[string]time.Time | |||
| 690 | deletedBlobs []string | 706 | deletedBlobs []string | ||
| 691 | } | 707 | } | ||
| 692 | 708 | ||||
| @@ -813,6 +829,10 @@ func (f *fakeBlobStorage) ListBlobs() ([]string, error) { | |||||
| 813 | return append([]string(nil), f.blobs...), nil | 829 | return append([]string(nil), f.blobs...), nil | ||
| 814 | } | 830 | } | ||
| 815 | 831 | ||||
| 832 | + | func (f *fakeBlobStorage) GetBlobModTime(digest string) (time.Time, error) { | |||
| 833 | + | return f.blobModTimes[digest], nil | |||
| 834 | + | } | |||
| 835 | + | ||||
| 816 | func (f *fakeBlobStorage) DeleteBlob(digest string) (int64, error) { | 836 | func (f *fakeBlobStorage) DeleteBlob(digest string) (int64, error) { | ||
| 817 | f.deletedBlobs = append(f.deletedBlobs, digest) | 837 | f.deletedBlobs = append(f.deletedBlobs, digest) | ||
| 818 | var size int64 | 838 | var size int64 | ||
M internal/usecase/registry/service.go +7 -0
| @@ -495,6 +495,12 @@ func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, data i | |||
| 495 | 495 | ||
| 496 | 496 | // FinishUpload completes a blob upload. | |
| 497 | 497 | func (s *Service) FinishUpload(ctx context.Context, uuid, digest string) error { | |
| 498 | + | // Keep the transition from upload to blob storage atomic with respect to | |
| 499 | + | // registry garbage collection. PruneRegistry holds the exclusive lock, so | |
| 500 | + | // it cannot observe a finalized blob before it is marked pending. | |
| 501 | + | s.mutationMu.RLock() | |
| 502 | + | defer s.mutationMu.RUnlock() | |
| 503 | + | ||
| 498 | 504 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 499 | 505 | zerowrap.FieldLayer: "usecase", | |
| 500 | 506 | zerowrap.FieldUseCase: "FinishUpload", | |
| @@ -506,6 +512,7 @@ func (s *Service) FinishUpload(ctx context.Context, uuid, digest string) error { | |||
| 506 | 512 | if err := s.blobStorage.FinishBlobUpload(uuid, digest); err != nil { | |
| 507 | 513 | return log.WrapErr(err, "failed to finish blob upload") | |
| 508 | 514 | } | |
| 515 | + | s.registryState.AddPending(digest, time.Now().UTC()) | |
| 509 | 516 | ||
| 510 | 517 | log.Info().Msg("blob upload finished") | |
| 511 | 518 | return nil | |
| @@ -495,6 +495,12 @@ func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, data i | |||||
| 495 | 495 | ||||
| 496 | // FinishUpload completes a blob upload. | 496 | // FinishUpload completes a blob upload. | ||
| 497 | func (s *Service) FinishUpload(ctx context.Context, uuid, digest string) error { | 497 | func (s *Service) FinishUpload(ctx context.Context, uuid, digest string) error { | ||
| 498 | + | // Keep the transition from upload to blob storage atomic with respect to | |||
| 499 | + | // registry garbage collection. PruneRegistry holds the exclusive lock, so | |||
| 500 | + | // it cannot observe a finalized blob before it is marked pending. | |||
| 501 | + | s.mutationMu.RLock() | |||
| 502 | + | defer s.mutationMu.RUnlock() | |||
| 503 | + | ||||
| 498 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | 504 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | ||
| 499 | zerowrap.FieldLayer: "usecase", | 505 | zerowrap.FieldLayer: "usecase", | ||
| 500 | zerowrap.FieldUseCase: "FinishUpload", | 506 | zerowrap.FieldUseCase: "FinishUpload", | ||
| @@ -506,6 +512,7 @@ func (s *Service) FinishUpload(ctx context.Context, uuid, digest string) error { | |||||
| 506 | if err := s.blobStorage.FinishBlobUpload(uuid, digest); err != nil { | 512 | if err := s.blobStorage.FinishBlobUpload(uuid, digest); err != nil { | ||
| 507 | return log.WrapErr(err, "failed to finish blob upload") | 513 | return log.WrapErr(err, "failed to finish blob upload") | ||
| 508 | } | 514 | } | ||
| 515 | + | s.registryState.AddPending(digest, time.Now().UTC()) | |||
| 509 | 516 | ||||
| 510 | log.Info().Msg("blob upload finished") | 517 | log.Info().Msg("blob upload finished") | ||
| 511 | return nil | 518 | return nil | ||
M internal/usecase/registry/service_test.go +62 -0
| @@ -456,6 +456,68 @@ func TestService_FinishUpload_Success(t *testing.T) { | |||
| 456 | 456 | assert.NoError(t, err) | |
| 457 | 457 | } | |
| 458 | 458 | ||
| 459 | + | func TestService_FinishUploadMarksBlobPending(t *testing.T) { | |
| 460 | + | blobStorage := mocks.NewMockBlobStorage(t) | |
| 461 | + | manifestStorage := mocks.NewMockManifestStorage(t) | |
| 462 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 463 | + | state := registrystate.New() | |
| 464 | + | svc := NewService(blobStorage, manifestStorage, eventBus, state) | |
| 465 | + | const digest = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" | |
| 466 | + | ||
| 467 | + | blobStorage.EXPECT().FinishBlobUpload("1234567890-myapp", digest).Return(nil) | |
| 468 | + | ||
| 469 | + | require.NoError(t, svc.FinishUpload(testContext(), "1234567890-myapp", digest)) | |
| 470 | + | assert.Contains(t, state.PendingDigests(time.Now().UTC()), digest) | |
| 471 | + | } | |
| 472 | + | ||
| 473 | + | func TestService_FinishUploadBlocksRegistryPruning(t *testing.T) { | |
| 474 | + | blobStorage := mocks.NewMockBlobStorage(t) | |
| 475 | + | manifestStorage := mocks.NewMockManifestStorage(t) | |
| 476 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 477 | + | state := registrystate.New() | |
| 478 | + | svc := NewService(blobStorage, manifestStorage, eventBus, state) | |
| 479 | + | started := make(chan struct{}) | |
| 480 | + | release := make(chan struct{}) | |
| 481 | + | ||
| 482 | + | blobStorage.EXPECT().FinishBlobUpload("1234567890-myapp", mock.Anything).RunAndReturn(func(string, string) error { | |
| 483 | + | close(started) | |
| 484 | + | <-release | |
| 485 | + | return nil | |
| 486 | + | }) | |
| 487 | + | ||
| 488 | + | finished := make(chan error, 1) | |
| 489 | + | go func() { | |
| 490 | + | finished <- svc.FinishUpload(testContext(), "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4") | |
| 491 | + | }() | |
| 492 | + | <-started | |
| 493 | + | ||
| 494 | + | pruneLockAcquired := make(chan struct{}) | |
| 495 | + | go func() { | |
| 496 | + | state.MutationMu.Lock() | |
| 497 | + | close(pruneLockAcquired) | |
| 498 | + | state.MutationMu.Unlock() | |
| 499 | + | }() | |
| 500 | + | assert.Never(t, func() bool { | |
| 501 | + | select { | |
| 502 | + | case <-pruneLockAcquired: | |
| 503 | + | return true | |
| 504 | + | default: | |
| 505 | + | return false | |
| 506 | + | } | |
| 507 | + | }, 100*time.Millisecond, 10*time.Millisecond) | |
| 508 | + | ||
| 509 | + | close(release) | |
| 510 | + | require.NoError(t, <-finished) | |
| 511 | + | require.Eventually(t, func() bool { | |
| 512 | + | select { | |
| 513 | + | case <-pruneLockAcquired: | |
| 514 | + | return true | |
| 515 | + | default: | |
| 516 | + | return false | |
| 517 | + | } | |
| 518 | + | }, time.Second, 10*time.Millisecond) | |
| 519 | + | } | |
| 520 | + | ||
| 459 | 521 | func TestService_FinishUpload_Error(t *testing.T) { | |
| 460 | 522 | blobStorage := mocks.NewMockBlobStorage(t) | |
| 461 | 523 | manifestStorage := mocks.NewMockManifestStorage(t) | |
| @@ -456,6 +456,68 @@ func TestService_FinishUpload_Success(t *testing.T) { | |||||
| 456 | assert.NoError(t, err) | 456 | assert.NoError(t, err) | ||
| 457 | } | 457 | } | ||
| 458 | 458 | ||||
| 459 | + | func TestService_FinishUploadMarksBlobPending(t *testing.T) { | |||
| 460 | + | blobStorage := mocks.NewMockBlobStorage(t) | |||
| 461 | + | manifestStorage := mocks.NewMockManifestStorage(t) | |||
| 462 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 463 | + | state := registrystate.New() | |||
| 464 | + | svc := NewService(blobStorage, manifestStorage, eventBus, state) | |||
| 465 | + | const digest = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" | |||
| 466 | + | ||||
| 467 | + | blobStorage.EXPECT().FinishBlobUpload("1234567890-myapp", digest).Return(nil) | |||
| 468 | + | ||||
| 469 | + | require.NoError(t, svc.FinishUpload(testContext(), "1234567890-myapp", digest)) | |||
| 470 | + | assert.Contains(t, state.PendingDigests(time.Now().UTC()), digest) | |||
| 471 | + | } | |||
| 472 | + | ||||
| 473 | + | func TestService_FinishUploadBlocksRegistryPruning(t *testing.T) { | |||
| 474 | + | blobStorage := mocks.NewMockBlobStorage(t) | |||
| 475 | + | manifestStorage := mocks.NewMockManifestStorage(t) | |||
| 476 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 477 | + | state := registrystate.New() | |||
| 478 | + | svc := NewService(blobStorage, manifestStorage, eventBus, state) | |||
| 479 | + | started := make(chan struct{}) | |||
| 480 | + | release := make(chan struct{}) | |||
| 481 | + | ||||
| 482 | + | blobStorage.EXPECT().FinishBlobUpload("1234567890-myapp", mock.Anything).RunAndReturn(func(string, string) error { | |||
| 483 | + | close(started) | |||
| 484 | + | <-release | |||
| 485 | + | return nil | |||
| 486 | + | }) | |||
| 487 | + | ||||
| 488 | + | finished := make(chan error, 1) | |||
| 489 | + | go func() { | |||
| 490 | + | finished <- svc.FinishUpload(testContext(), "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4") | |||
| 491 | + | }() | |||
| 492 | + | <-started | |||
| 493 | + | ||||
| 494 | + | pruneLockAcquired := make(chan struct{}) | |||
| 495 | + | go func() { | |||
| 496 | + | state.MutationMu.Lock() | |||
| 497 | + | close(pruneLockAcquired) | |||
| 498 | + | state.MutationMu.Unlock() | |||
| 499 | + | }() | |||
| 500 | + | assert.Never(t, func() bool { | |||
| 501 | + | select { | |||
| 502 | + | case <-pruneLockAcquired: | |||
| 503 | + | return true | |||
| 504 | + | default: | |||
| 505 | + | return false | |||
| 506 | + | } | |||
| 507 | + | }, 100*time.Millisecond, 10*time.Millisecond) | |||
| 508 | + | ||||
| 509 | + | close(release) | |||
| 510 | + | require.NoError(t, <-finished) | |||
| 511 | + | require.Eventually(t, func() bool { | |||
| 512 | + | select { | |||
| 513 | + | case <-pruneLockAcquired: | |||
| 514 | + | return true | |||
| 515 | + | default: | |||
| 516 | + | return false | |||
| 517 | + | } | |||
| 518 | + | }, time.Second, 10*time.Millisecond) | |||
| 519 | + | } | |||
| 520 | + | ||||
| 459 | func TestService_FinishUpload_Error(t *testing.T) { | 521 | func TestService_FinishUpload_Error(t *testing.T) { | ||
| 460 | blobStorage := mocks.NewMockBlobStorage(t) | 522 | blobStorage := mocks.NewMockBlobStorage(t) | ||
| 461 | manifestStorage := mocks.NewMockManifestStorage(t) | 523 | manifestStorage := mocks.NewMockManifestStorage(t) | ||
M pkg/registrypush/push.go +2 -2
| @@ -15,11 +15,11 @@ import ( | |||
| 15 | 15 | "strings" | |
| 16 | 16 | "time" | |
| 17 | 17 | ||
| 18 | - | "github.com/docker/docker/client" | |
| 19 | 18 | "github.com/google/go-containerregistry/pkg/authn" | |
| 20 | 19 | "github.com/google/go-containerregistry/pkg/name" | |
| 21 | 20 | v1 "github.com/google/go-containerregistry/pkg/v1" | |
| 22 | 21 | "github.com/google/go-containerregistry/pkg/v1/tarball" | |
| 22 | + | "github.com/moby/moby/client" | |
| 23 | 23 | ) | |
| 24 | 24 | ||
| 25 | 25 | // DefaultChunkSize is 50MB - safely under Cloudflare's 100MB per-request limit | |
| @@ -207,7 +207,7 @@ func (p *Pusher) UploadBlob(ctx context.Context, baseURL, repo, digest string, s | |||
| 207 | 207 | ||
| 208 | 208 | // defaultImageSourceWithCleanup reads an image from the local Docker/Podman daemon. | |
| 209 | 209 | func defaultImageSourceWithCleanup(ctx context.Context, ref string) (v1.Image, func(), error) { | |
| 210 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |
| 210 | + | cli, err := client.New(client.FromEnv) | |
| 211 | 211 | if err != nil { | |
| 212 | 212 | return nil, nil, fmt.Errorf("failed to create Docker client: %w", err) | |
| 213 | 213 | } | |
| @@ -15,11 +15,11 @@ import ( | |||||
| 15 | "strings" | 15 | "strings" | ||
| 16 | "time" | 16 | "time" | ||
| 17 | 17 | ||||
| 18 | - | "github.com/docker/docker/client" | |||
| 19 | "github.com/google/go-containerregistry/pkg/authn" | 18 | "github.com/google/go-containerregistry/pkg/authn" | ||
| 20 | "github.com/google/go-containerregistry/pkg/name" | 19 | "github.com/google/go-containerregistry/pkg/name" | ||
| 21 | v1 "github.com/google/go-containerregistry/pkg/v1" | 20 | v1 "github.com/google/go-containerregistry/pkg/v1" | ||
| 22 | "github.com/google/go-containerregistry/pkg/v1/tarball" | 21 | "github.com/google/go-containerregistry/pkg/v1/tarball" | ||
| 22 | + | "github.com/moby/moby/client" | |||
| 23 | ) | 23 | ) | ||
| 24 | 24 | ||||
| 25 | // DefaultChunkSize is 50MB - safely under Cloudflare's 100MB per-request limit | 25 | // DefaultChunkSize is 50MB - safely under Cloudflare's 100MB per-request limit | ||
| @@ -207,7 +207,7 @@ func (p *Pusher) UploadBlob(ctx context.Context, baseURL, repo, digest string, s | |||||
| 207 | 207 | ||||
| 208 | // defaultImageSourceWithCleanup reads an image from the local Docker/Podman daemon. | 208 | // defaultImageSourceWithCleanup reads an image from the local Docker/Podman daemon. | ||
| 209 | func defaultImageSourceWithCleanup(ctx context.Context, ref string) (v1.Image, func(), error) { | 209 | func defaultImageSourceWithCleanup(ctx context.Context, ref string) (v1.Image, func(), error) { | ||
| 210 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | 210 | + | cli, err := client.New(client.FromEnv) |
| 211 | if err != nil { | 211 | if err != nil { | ||
| 212 | return nil, nil, fmt.Errorf("failed to create Docker client: %w", err) | 212 | return nil, nil, fmt.Errorf("failed to create Docker client: %w", err) | ||
| 213 | } | 213 | } | ||