fix(backend): move Docker client to the maintained moby client module (#1935)
github.com/docker/docker is frozen at v28.5.2 — moby publishes v29+ under
github.com/moby/moby/v2, with the API client split out into the standalone
github.com/moby/moby/client and github.com/moby/moby/api modules. That leaves
five Dependabot alerts on the old module path with no reachable fix version:
CVE-2026-42306 high docker cp bind mount redirection <= 28.5.2
CVE-2026-41567 high PUT /containers/{id}/archive <= 28.5.2
CVE-2026-41568 medium docker cp symlink swap <= 28.5.2
CVE-2026-34040 high AuthZ plugin bypass < 29.3.1
CVE-2026-33997 medium plugin privilege off-by-one < 29.3.1
All five are daemon-side defects, and we only ever imported the client. Moving
to the split client module drops the daemon source out of the module graph
entirely, so these alerts resolve and future daemon advisories will not flag us
at all. It also trims six transitive dependencies.
The new client takes an options struct per call and returns a result struct, so
this is a mechanical rewrite of the wrapper. Behavioural notes:
- Ports moved from docker/go-connections/nat to moby/api/types/network, where
Port is opaque and PortBinding.HostIP is a netip.Addr. buildDockerPortBindings
now returns an error for out-of-range ports and unparseable host IPs instead
of handing the daemon a malformed request. An empty HostIP still maps to the
zero address, preserving "publish on all interfaces".
- DiskUsage needs Verbose to get per-image and per-container records; the
aggregate LayersSize is now Images.TotalSize.
- BuildCachePrune no longer takes KeepStorage — the client derives the legacy
keep-storage parameter from ReservedSpace for daemons on API <= 1.47.
- ImagesPrune is now ImagePrune.
- Container state and health are typed string enums, and InspectResponse.Config
and .State are pointers, so inspect mapping guards for nil.
Also corrects the RUSTSEC-2024-0429 note in the cargo-audit ignore list. It
claimed the advisory resolves once Tauri moves to webkit2gtk-4.1, but Tauri v2
is already on those bindings and still resolves glib to 0.18.5; the real
blocker is that gtk3-rs is archived. Backport submitted upstream.
Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
+5
-10
@@ -6,9 +6,7 @@ require (
|
||||
github.com/UserExistsError/conpty v0.1.4
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0
|
||||
github.com/coder/acp-go-sdk v0.13.5
|
||||
github.com/creack/pty v1.1.21
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-connections v0.6.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
@@ -18,6 +16,8 @@ require (
|
||||
github.com/kevinburke/ssh_config v1.6.0
|
||||
github.com/mark3labs/mcp-go v0.43.2
|
||||
github.com/mattn/go-sqlite3 v1.14.33
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.0
|
||||
github.com/nats-io/nats.go v1.31.0
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/spf13/viper v1.16.0
|
||||
@@ -43,7 +43,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.21 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
@@ -52,9 +52,9 @@ require (
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
@@ -87,18 +87,14 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.1.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.6 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
@@ -121,7 +117,6 @@ require (
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
|
||||
replace github.com/coder/acp-go-sdk => github.com/kdlbs/acp-go-sdk v0.13.6-0.20260625174839-e37105a28386
|
||||
|
||||
+12
-28
@@ -38,14 +38,12 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Microsoft/go-winio v0.4.21 h1:+6mVbXh4wPzUrl1COX9A+ZCvEpYsOBZ6/+kwDnvLyro=
|
||||
github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/UserExistsError/conpty v0.1.4 h1:+3FhJhiqhyEJa+K5qaK3/w6w+sN3Nh9O9VbJyBS02to=
|
||||
github.com/UserExistsError/conpty v0.1.4/go.mod h1:PDglKIkX3O/2xVk0MV9a6bCWxRmPVfxqZoTG/5sSd9I=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
@@ -78,19 +76,15 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -272,19 +266,15 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
|
||||
github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
|
||||
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
|
||||
github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E=
|
||||
github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8=
|
||||
github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY=
|
||||
@@ -299,7 +289,6 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||
@@ -310,9 +299,6 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
|
||||
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
@@ -501,7 +487,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -524,7 +509,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -554,8 +538,6 @@ golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
|
||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -727,6 +709,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/kandev/kandev/internal/agent/runtime/activity"
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
"github.com/moby/moby/client"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestBuildImageHoldsActivityUntilResponseBodyCloses(t *testing.T) {
|
||||
}
|
||||
coordinator := activity.NewCoordinator(activity.Options{})
|
||||
client := &Client{
|
||||
builder: &fakeImageBuilder{response: build.ImageBuildResponse{
|
||||
builder: &fakeImageBuilder{response: client.ImageBuildResult{
|
||||
Body: io.NopCloser(bytes.NewBufferString("build output")),
|
||||
}},
|
||||
logger: log,
|
||||
@@ -44,10 +44,10 @@ func TestBuildImageHoldsActivityUntilResponseBodyCloses(t *testing.T) {
|
||||
}
|
||||
|
||||
type fakeImageBuilder struct {
|
||||
response build.ImageBuildResponse
|
||||
response client.ImageBuildResult
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeImageBuilder) ImageBuild(context.Context, io.Reader, build.ImageBuildOptions) (build.ImageBuildResponse, error) {
|
||||
func (f *fakeImageBuilder) ImageBuild(context.Context, io.Reader, client.ImageBuildOptions) (client.ImageBuildResult, error) {
|
||||
return f.response, f.err
|
||||
}
|
||||
|
||||
@@ -8,20 +8,20 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/kandev/kandev/internal/agent/runtime/activity"
|
||||
"github.com/kandev/kandev/internal/common/config"
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/mount"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/client"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -72,11 +72,11 @@ type ContainerInfo struct {
|
||||
|
||||
// Client wraps the Docker client.
|
||||
type containerRemover interface {
|
||||
ContainerRemove(context.Context, string, container.RemoveOptions) error
|
||||
ContainerRemove(context.Context, string, client.ContainerRemoveOptions) (client.ContainerRemoveResult, error)
|
||||
}
|
||||
|
||||
type imageBuilder interface {
|
||||
ImageBuild(context.Context, io.Reader, build.ImageBuildOptions) (build.ImageBuildResponse, error)
|
||||
ImageBuild(context.Context, io.Reader, client.ImageBuildOptions) (client.ImageBuildResult, error)
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
@@ -104,7 +104,7 @@ func NewClient(cfg config.DockerConfig, log *logger.Logger) (*Client, error) {
|
||||
opts = append(opts, client.WithVersion(cfg.APIVersion))
|
||||
}
|
||||
|
||||
cli, err := client.NewClientWithOpts(opts...)
|
||||
cli, err := client.New(opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create docker client: %w", err)
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func (c *Client) Close() error {
|
||||
func (c *Client) PullImage(ctx context.Context, imageName string) error {
|
||||
c.logger.Info("Pulling image", zap.String("image", imageName))
|
||||
|
||||
reader, err := c.cli.ImagePull(ctx, imageName, image.PullOptions{})
|
||||
reader, err := c.cli.ImagePull(ctx, imageName, client.ImagePullOptions{})
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to pull image", zap.String("image", imageName), zap.Error(err))
|
||||
return fmt.Errorf("failed to pull image %s: %w", imageName, err)
|
||||
@@ -190,7 +190,7 @@ func (c *Client) BuildImage(ctx context.Context, dockerfile string, tag string,
|
||||
return nil, fmt.Errorf("failed to create build context: %w", err)
|
||||
}
|
||||
|
||||
resp, err := builder.ImageBuild(ctx, buildContext, build.ImageBuildOptions{
|
||||
resp, err := builder.ImageBuild(ctx, buildContext, client.ImageBuildOptions{
|
||||
Tags: []string{tag},
|
||||
BuildArgs: buildArgs,
|
||||
Dockerfile: "Dockerfile",
|
||||
@@ -271,7 +271,10 @@ func (c *Client) CreateContainer(ctx context.Context, cfg ContainerConfig) (stri
|
||||
}
|
||||
|
||||
// Container configuration
|
||||
exposedPorts, portBindings := buildDockerPortBindings(cfg.PortBindings)
|
||||
exposedPorts, portBindings, err := buildDockerPortBindings(cfg.PortBindings)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create container %s: %w", cfg.Name, err)
|
||||
}
|
||||
containerCfg := &container.Config{
|
||||
Image: cfg.Image,
|
||||
Entrypoint: cfg.Entrypoint,
|
||||
@@ -294,7 +297,11 @@ func (c *Client) CreateContainer(ctx context.Context, cfg ContainerConfig) (stri
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := c.cli.ContainerCreate(ctx, containerCfg, hostCfg, nil, nil, cfg.Name)
|
||||
resp, err := c.cli.ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
Config: containerCfg,
|
||||
HostConfig: hostCfg,
|
||||
Name: cfg.Name,
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to create container",
|
||||
zap.String("name", cfg.Name),
|
||||
@@ -307,28 +314,60 @@ func (c *Client) CreateContainer(ctx context.Context, cfg ContainerConfig) (stri
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
func buildDockerPortBindings(bindings []PortBindingConfig) (nat.PortSet, nat.PortMap) {
|
||||
func buildDockerPortBindings(bindings []PortBindingConfig) (network.PortSet, network.PortMap, error) {
|
||||
if len(bindings) == 0 {
|
||||
return nil, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
exposedPorts := nat.PortSet{}
|
||||
portBindings := nat.PortMap{}
|
||||
exposedPorts := network.PortSet{}
|
||||
portBindings := network.PortMap{}
|
||||
for _, binding := range bindings {
|
||||
containerPort := nat.Port(fmt.Sprintf("%d/tcp", binding.ContainerPort))
|
||||
containerPort, err := tcpPort(binding.ContainerPort)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid container port binding: %w", err)
|
||||
}
|
||||
hostIP, err := parseDockerHostIP(binding.HostIP)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid host IP for container port %d: %w", binding.ContainerPort, err)
|
||||
}
|
||||
exposedPorts[containerPort] = struct{}{}
|
||||
portBindings[containerPort] = append(portBindings[containerPort], nat.PortBinding{
|
||||
HostIP: binding.HostIP,
|
||||
portBindings[containerPort] = append(portBindings[containerPort], network.PortBinding{
|
||||
HostIP: hostIP,
|
||||
HostPort: binding.HostPort,
|
||||
})
|
||||
}
|
||||
return exposedPorts, portBindings
|
||||
return exposedPorts, portBindings, nil
|
||||
}
|
||||
|
||||
// tcpPort converts a container port number into the API's opaque TCP port key.
|
||||
func tcpPort(port int) (network.Port, error) {
|
||||
if port <= 0 || port > math.MaxUint16 {
|
||||
return network.Port{}, fmt.Errorf("port %d out of range", port)
|
||||
}
|
||||
parsed, ok := network.PortFrom(uint16(port), network.TCP)
|
||||
if !ok {
|
||||
return network.Port{}, fmt.Errorf("port %d is not a valid TCP port", port)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
// parseDockerHostIP converts a configured host IP into the API's address type.
|
||||
// An empty value keeps Docker's "publish on all interfaces" behavior.
|
||||
func parseDockerHostIP(hostIP string) (netip.Addr, error) {
|
||||
if hostIP == "" {
|
||||
return netip.Addr{}, nil
|
||||
}
|
||||
addr, err := netip.ParseAddr(hostIP)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("parse host IP %q: %w", hostIP, err)
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// StartContainer starts a container.
|
||||
func (c *Client) StartContainer(ctx context.Context, containerID string) error {
|
||||
c.logger.Info("Starting container", zap.String("container_id", containerID))
|
||||
|
||||
err := c.cli.ContainerStart(ctx, containerID, container.StartOptions{})
|
||||
_, err := c.cli.ContainerStart(ctx, containerID, client.ContainerStartOptions{})
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to start container", zap.String("container_id", containerID), zap.Error(err))
|
||||
return fmt.Errorf("failed to start container %s: %w", containerID, err)
|
||||
@@ -346,7 +385,7 @@ func (c *Client) StopContainer(ctx context.Context, containerID string, timeout
|
||||
)
|
||||
|
||||
timeoutSeconds := int(timeout.Seconds())
|
||||
err := c.cli.ContainerStop(ctx, containerID, container.StopOptions{
|
||||
_, err := c.cli.ContainerStop(ctx, containerID, client.ContainerStopOptions{
|
||||
Timeout: &timeoutSeconds,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -365,7 +404,7 @@ func (c *Client) RemoveContainer(ctx context.Context, containerID string, force
|
||||
zap.Bool("force", force),
|
||||
)
|
||||
|
||||
err := c.containerRemover().ContainerRemove(ctx, containerID, container.RemoveOptions{
|
||||
_, err := c.containerRemover().ContainerRemove(ctx, containerID, client.ContainerRemoveOptions{
|
||||
Force: force,
|
||||
RemoveVolumes: true,
|
||||
})
|
||||
@@ -392,7 +431,7 @@ func (c *Client) KillContainer(ctx context.Context, containerID string, signal s
|
||||
zap.String("signal", signal),
|
||||
)
|
||||
|
||||
err := c.cli.ContainerKill(ctx, containerID, signal)
|
||||
_, err := c.cli.ContainerKill(ctx, containerID, client.ContainerKillOptions{Signal: signal})
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to kill container", zap.String("container_id", containerID), zap.Error(err))
|
||||
return fmt.Errorf("failed to kill container %s: %w", containerID, err)
|
||||
@@ -402,49 +441,59 @@ func (c *Client) KillContainer(ctx context.Context, containerID string, signal s
|
||||
return nil
|
||||
}
|
||||
|
||||
// inspectContainer inspects a container and unwraps the typed response.
|
||||
func (c *Client) inspectContainer(ctx context.Context, containerID string) (container.InspectResponse, error) {
|
||||
result, err := c.cli.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
return container.InspectResponse{}, err
|
||||
}
|
||||
return result.Container, nil
|
||||
}
|
||||
|
||||
// GetContainerInfo returns information about a container.
|
||||
func (c *Client) GetContainerInfo(ctx context.Context, containerID string) (*ContainerInfo, error) {
|
||||
c.logger.Debug("Getting container info", zap.String("container_id", containerID))
|
||||
|
||||
inspect, err := c.cli.ContainerInspect(ctx, containerID)
|
||||
inspect, err := c.inspectContainer(ctx, containerID)
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to inspect container", zap.String("container_id", containerID), zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err)
|
||||
}
|
||||
|
||||
info := &ContainerInfo{
|
||||
ID: inspect.ID,
|
||||
Name: inspect.Name,
|
||||
Image: inspect.Config.Image,
|
||||
State: inspect.State.Status,
|
||||
Status: inspect.State.Status,
|
||||
ExitCode: inspect.State.ExitCode,
|
||||
Labels: inspect.Config.Labels,
|
||||
ID: inspect.ID,
|
||||
Name: inspect.Name,
|
||||
}
|
||||
|
||||
// Parse timestamps
|
||||
if inspect.State.StartedAt != "" {
|
||||
startedAt, err := time.Parse(time.RFC3339Nano, inspect.State.StartedAt)
|
||||
if err == nil {
|
||||
info.StartedAt = startedAt
|
||||
}
|
||||
}
|
||||
|
||||
if inspect.State.FinishedAt != "" {
|
||||
finishedAt, err := time.Parse(time.RFC3339Nano, inspect.State.FinishedAt)
|
||||
if err == nil {
|
||||
info.FinishedAt = finishedAt
|
||||
}
|
||||
}
|
||||
|
||||
// Get health status if available
|
||||
if inspect.State.Health != nil {
|
||||
info.Health = inspect.State.Health.Status
|
||||
if inspect.Config != nil {
|
||||
info.Image = inspect.Config.Image
|
||||
info.Labels = inspect.Config.Labels
|
||||
}
|
||||
applyContainerState(info, inspect.State)
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// applyContainerState copies the inspected runtime state onto info. The API
|
||||
// models State as a pointer, so a missing state leaves the zero values.
|
||||
func applyContainerState(info *ContainerInfo, state *container.State) {
|
||||
if state == nil {
|
||||
return
|
||||
}
|
||||
info.State = string(state.Status)
|
||||
info.Status = string(state.Status)
|
||||
info.ExitCode = state.ExitCode
|
||||
|
||||
if startedAt, err := time.Parse(time.RFC3339Nano, state.StartedAt); err == nil {
|
||||
info.StartedAt = startedAt
|
||||
}
|
||||
if finishedAt, err := time.Parse(time.RFC3339Nano, state.FinishedAt); err == nil {
|
||||
info.FinishedAt = finishedAt
|
||||
}
|
||||
if state.Health != nil {
|
||||
info.Health = string(state.Health.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// IsContainerRunning returns true if the container exists and is in "running" state
|
||||
func (c *Client) IsContainerRunning(ctx context.Context, containerID string) (bool, error) {
|
||||
info, err := c.GetContainerInfo(ctx, containerID)
|
||||
@@ -458,7 +507,7 @@ func (c *Client) IsContainerRunning(ctx context.Context, containerID string) (bo
|
||||
func (c *Client) GetContainerIP(ctx context.Context, containerID string) (string, error) {
|
||||
c.logger.Debug("Getting container IP", zap.String("container_id", containerID))
|
||||
|
||||
inspect, err := c.cli.ContainerInspect(ctx, containerID)
|
||||
inspect, err := c.inspectContainer(ctx, containerID)
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to inspect container for IP", zap.String("container_id", containerID), zap.Error(err))
|
||||
return "", err
|
||||
@@ -467,13 +516,15 @@ func (c *Client) GetContainerIP(ctx context.Context, containerID string) (string
|
||||
if inspect.NetworkSettings != nil {
|
||||
// Check available networks for an IP address.
|
||||
for netName, netSettings := range inspect.NetworkSettings.Networks {
|
||||
if netSettings.IPAddress != "" {
|
||||
c.logger.Debug("Found container IP",
|
||||
zap.String("container_id", containerID),
|
||||
zap.String("network", netName),
|
||||
zap.String("ip", netSettings.IPAddress))
|
||||
return netSettings.IPAddress, nil
|
||||
if netSettings == nil || !netSettings.IPAddress.IsValid() {
|
||||
continue
|
||||
}
|
||||
ip := netSettings.IPAddress.String()
|
||||
c.logger.Debug("Found container IP",
|
||||
zap.String("container_id", containerID),
|
||||
zap.String("network", netName),
|
||||
zap.String("ip", ip))
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +537,7 @@ func (c *Client) GetContainerHostPort(ctx context.Context, containerID string, c
|
||||
zap.String("container_id", containerID),
|
||||
zap.Int("container_port", containerPort))
|
||||
|
||||
inspect, err := c.cli.ContainerInspect(ctx, containerID)
|
||||
inspect, err := c.inspectContainer(ctx, containerID)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("failed to inspect container %s: %w", containerID, err)
|
||||
}
|
||||
@@ -494,11 +545,15 @@ func (c *Client) GetContainerHostPort(ctx context.Context, containerID string, c
|
||||
return "", 0, fmt.Errorf("container %s has no published ports", containerID)
|
||||
}
|
||||
|
||||
bindings := inspect.NetworkSettings.Ports[nat.Port(fmt.Sprintf("%d/tcp", containerPort))]
|
||||
port, err := tcpPort(containerPort)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("container %s: %w", containerID, err)
|
||||
}
|
||||
bindings := inspect.NetworkSettings.Ports[port]
|
||||
if len(bindings) == 0 {
|
||||
return "", 0, fmt.Errorf("container %s port %d/tcp is not published", containerID, containerPort)
|
||||
}
|
||||
hostPort, err := nat.ParsePort(bindings[0].HostPort)
|
||||
hostPort, err := parseHostPort(bindings[0].HostPort)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("invalid host port for container %s port %d/tcp: %w", containerID, containerPort, err)
|
||||
}
|
||||
@@ -506,20 +561,32 @@ func (c *Client) GetContainerHostPort(ctx context.Context, containerID string, c
|
||||
return hostIP, hostPort, nil
|
||||
}
|
||||
|
||||
func normalizeDockerHostIP(hostIP string) string {
|
||||
switch hostIP {
|
||||
case "", "0.0.0.0", "::":
|
||||
return "127.0.0.1"
|
||||
default:
|
||||
return hostIP
|
||||
// parseHostPort validates a host-side port number reported by the daemon.
|
||||
func parseHostPort(value string) (int, error) {
|
||||
port, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parse port %q: %w", value, err)
|
||||
}
|
||||
if port < 0 || port > math.MaxUint16 {
|
||||
return 0, fmt.Errorf("port %d out of range", port)
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
|
||||
// normalizeDockerHostIP maps an unset or wildcard publish address onto loopback
|
||||
// so callers get an address they can actually dial.
|
||||
func normalizeDockerHostIP(hostIP netip.Addr) string {
|
||||
if !hostIP.IsValid() || hostIP.IsUnspecified() {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
return hostIP.String()
|
||||
}
|
||||
|
||||
// GetContainerLabels returns the labels of a container
|
||||
func (c *Client) GetContainerLabels(ctx context.Context, containerID string) (map[string]string, error) {
|
||||
c.logger.Debug("Getting container labels", zap.String("container_id", containerID))
|
||||
|
||||
inspect, err := c.cli.ContainerInspect(ctx, containerID)
|
||||
inspect, err := c.inspectContainer(ctx, containerID)
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to inspect container for labels", zap.String("container_id", containerID), zap.Error(err))
|
||||
return nil, err
|
||||
@@ -540,7 +607,7 @@ func (c *Client) GetContainerLogs(ctx context.Context, containerID string, follo
|
||||
zap.String("tail", tail),
|
||||
)
|
||||
|
||||
opts := container.LogsOptions{
|
||||
opts := client.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: follow,
|
||||
@@ -561,15 +628,17 @@ func (c *Client) GetContainerLogs(ctx context.Context, containerID string, follo
|
||||
func (c *Client) WaitContainer(ctx context.Context, containerID string) (int64, error) {
|
||||
c.logger.Info("Waiting for container", zap.String("container_id", containerID))
|
||||
|
||||
statusCh, errCh := c.cli.ContainerWait(ctx, containerID, container.WaitConditionNotRunning)
|
||||
wait := c.cli.ContainerWait(ctx, containerID, client.ContainerWaitOptions{
|
||||
Condition: container.WaitConditionNotRunning,
|
||||
})
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
case err := <-wait.Error:
|
||||
if err != nil {
|
||||
c.logger.Error("Error waiting for container", zap.String("container_id", containerID), zap.Error(err))
|
||||
return -1, fmt.Errorf("error waiting for container %s: %w", containerID, err)
|
||||
}
|
||||
case status := <-statusCh:
|
||||
case status := <-wait.Result:
|
||||
c.logger.Info("Container exited",
|
||||
zap.String("container_id", containerID),
|
||||
zap.Int64("exit_code", status.StatusCode),
|
||||
@@ -588,22 +657,22 @@ func (c *Client) ListContainers(ctx context.Context, labels map[string]string) (
|
||||
c.logger.Debug("Listing containers", zap.Any("labels", labels))
|
||||
|
||||
// Build filters from labels
|
||||
filterArgs := filters.NewArgs()
|
||||
listFilters := make(client.Filters)
|
||||
for key, value := range labels {
|
||||
filterArgs.Add("label", fmt.Sprintf("%s=%s", key, value))
|
||||
listFilters.Add("label", fmt.Sprintf("%s=%s", key, value))
|
||||
}
|
||||
|
||||
containers, err := c.cli.ContainerList(ctx, container.ListOptions{
|
||||
result, err := c.cli.ContainerList(ctx, client.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filterArgs,
|
||||
Filters: listFilters,
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to list containers", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to list containers: %w", err)
|
||||
}
|
||||
|
||||
infos := make([]ContainerInfo, 0, len(containers))
|
||||
for _, ctr := range containers {
|
||||
infos := make([]ContainerInfo, 0, len(result.Items))
|
||||
for _, ctr := range result.Items {
|
||||
name := ""
|
||||
if len(ctr.Names) > 0 {
|
||||
name = ctr.Names[0]
|
||||
@@ -617,7 +686,7 @@ func (c *Client) ListContainers(ctx context.Context, labels map[string]string) (
|
||||
ID: ctr.ID,
|
||||
Name: name,
|
||||
Image: ctr.Image,
|
||||
State: ctr.State,
|
||||
State: string(ctr.State),
|
||||
Status: ctr.Status,
|
||||
Labels: ctr.Labels,
|
||||
}
|
||||
@@ -632,7 +701,7 @@ func (c *Client) ListContainers(ctx context.Context, labels map[string]string) (
|
||||
func (c *Client) Ping(ctx context.Context) error {
|
||||
c.logger.Debug("Pinging Docker daemon")
|
||||
|
||||
_, err := c.cli.Ping(ctx)
|
||||
_, err := c.cli.Ping(ctx, client.PingOptions{})
|
||||
if err != nil {
|
||||
c.logger.Debug("Docker ping failed", zap.Error(err))
|
||||
return fmt.Errorf("docker ping failed: %w", err)
|
||||
@@ -671,7 +740,10 @@ func (c *Client) CreateContainerInteractive(ctx context.Context, cfg ContainerCo
|
||||
// Container configuration with stdin attached. Mirror CreateContainer's
|
||||
// handling of ContainerConfig fields — including Entrypoint — so the same
|
||||
// config struct produces consistent container behavior on both paths.
|
||||
exposedPorts, portBindings := buildDockerPortBindings(cfg.PortBindings)
|
||||
exposedPorts, portBindings, err := buildDockerPortBindings(cfg.PortBindings)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create interactive container %s: %w", cfg.Name, err)
|
||||
}
|
||||
containerCfg := &container.Config{
|
||||
Image: cfg.Image,
|
||||
Entrypoint: cfg.Entrypoint,
|
||||
@@ -700,7 +772,11 @@ func (c *Client) CreateContainerInteractive(ctx context.Context, cfg ContainerCo
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := c.cli.ContainerCreate(ctx, containerCfg, hostCfg, nil, nil, cfg.Name)
|
||||
resp, err := c.cli.ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
Config: containerCfg,
|
||||
HostConfig: hostCfg,
|
||||
Name: cfg.Name,
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to create interactive container",
|
||||
zap.String("name", cfg.Name),
|
||||
@@ -717,7 +793,7 @@ func (c *Client) CreateContainerInteractive(ctx context.Context, cfg ContainerCo
|
||||
func (c *Client) AttachContainer(ctx context.Context, containerID string) (*AttachResult, error) {
|
||||
c.logger.Info("Attaching to container", zap.String("container_id", containerID))
|
||||
|
||||
opts := container.AttachOptions{
|
||||
opts := client.ContainerAttachOptions{
|
||||
Stream: true,
|
||||
Stdin: true,
|
||||
Stdout: true,
|
||||
|
||||
@@ -2,34 +2,39 @@ package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
)
|
||||
|
||||
func TestNormalizeDockerHostIP(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
name string
|
||||
in netip.Addr
|
||||
want string
|
||||
}{
|
||||
{in: "", want: "127.0.0.1"},
|
||||
{in: "0.0.0.0", want: "127.0.0.1"},
|
||||
{in: "::", want: "127.0.0.1"},
|
||||
{in: "127.0.0.1", want: "127.0.0.1"},
|
||||
{in: "10.0.0.5", want: "10.0.0.5"},
|
||||
{in: "::1", want: "::1"},
|
||||
{name: "unset", in: netip.Addr{}, want: "127.0.0.1"},
|
||||
{name: "ipv4 wildcard", in: netip.MustParseAddr("0.0.0.0"), want: "127.0.0.1"},
|
||||
{name: "ipv6 wildcard", in: netip.MustParseAddr("::"), want: "127.0.0.1"},
|
||||
{name: "ipv4 loopback", in: netip.MustParseAddr("127.0.0.1"), want: "127.0.0.1"},
|
||||
{name: "ipv4 host", in: netip.MustParseAddr("10.0.0.5"), want: "10.0.0.5"},
|
||||
{name: "ipv6 loopback", in: netip.MustParseAddr("::1"), want: "::1"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.in, func(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := normalizeDockerHostIP(tc.in); got != tc.want {
|
||||
t.Errorf("normalizeDockerHostIP(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
t.Errorf("normalizeDockerHostIP(%v) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDockerPortBindings_EmptyReturnsNil(t *testing.T) {
|
||||
exposed, bindings := buildDockerPortBindings(nil)
|
||||
exposed, bindings, err := buildDockerPortBindings(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildDockerPortBindings: %v", err)
|
||||
}
|
||||
if exposed != nil {
|
||||
t.Errorf("expected nil exposed ports, got %v", exposed)
|
||||
}
|
||||
@@ -43,13 +48,16 @@ func TestBuildDockerPortBindings_AssignsContainerAndHost(t *testing.T) {
|
||||
{ContainerPort: 8080, HostIP: "127.0.0.1", HostPort: "0"},
|
||||
{ContainerPort: 9000, HostIP: "0.0.0.0", HostPort: "9001"},
|
||||
}
|
||||
exposed, bindings := buildDockerPortBindings(in)
|
||||
exposed, bindings, err := buildDockerPortBindings(in)
|
||||
if err != nil {
|
||||
t.Fatalf("buildDockerPortBindings: %v", err)
|
||||
}
|
||||
|
||||
if got := len(exposed); got != 2 {
|
||||
t.Fatalf("exposed ports = %d, want 2", got)
|
||||
}
|
||||
for _, b := range in {
|
||||
key := nat.Port(fmt.Sprintf("%d/tcp", b.ContainerPort))
|
||||
key := network.MustParsePort(fmt.Sprintf("%d/tcp", b.ContainerPort))
|
||||
if _, ok := exposed[key]; !ok {
|
||||
t.Errorf("exposed missing %s", key)
|
||||
}
|
||||
@@ -57,7 +65,7 @@ func TestBuildDockerPortBindings_AssignsContainerAndHost(t *testing.T) {
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("bindings[%s] = %d entries, want 1", key, len(got))
|
||||
}
|
||||
if got[0].HostIP != b.HostIP || got[0].HostPort != b.HostPort {
|
||||
if got[0].HostIP.String() != b.HostIP || got[0].HostPort != b.HostPort {
|
||||
t.Errorf("bindings[%s] = %+v, want host_ip=%q host_port=%q", key, got[0], b.HostIP, b.HostPort)
|
||||
}
|
||||
}
|
||||
@@ -68,9 +76,57 @@ func TestBuildDockerPortBindings_DeduplicatesContainerPort(t *testing.T) {
|
||||
{ContainerPort: 7000, HostIP: "127.0.0.1", HostPort: "0"},
|
||||
{ContainerPort: 7000, HostIP: "10.0.0.5", HostPort: "7000"},
|
||||
}
|
||||
_, bindings := buildDockerPortBindings(in)
|
||||
got := bindings[nat.Port("7000/tcp")]
|
||||
_, bindings, err := buildDockerPortBindings(in)
|
||||
if err != nil {
|
||||
t.Fatalf("buildDockerPortBindings: %v", err)
|
||||
}
|
||||
got := bindings[network.MustParsePort("7000/tcp")]
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("want both bindings on port 7000, got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDockerPortBindings_RejectsInvalidInput(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in PortBindingConfig
|
||||
}{
|
||||
{name: "container port zero", in: PortBindingConfig{ContainerPort: 0, HostPort: "1"}},
|
||||
{name: "container port too large", in: PortBindingConfig{ContainerPort: 70000, HostPort: "1"}},
|
||||
{name: "host ip not an address", in: PortBindingConfig{ContainerPort: 8080, HostIP: "localhost"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if _, _, err := buildDockerPortBindings([]PortBindingConfig{tc.in}); err == nil {
|
||||
t.Fatal("expected an error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDockerPortBindings_EmptyHostIPPublishesOnAllInterfaces(t *testing.T) {
|
||||
_, bindings, err := buildDockerPortBindings([]PortBindingConfig{
|
||||
{ContainerPort: 8080, HostIP: "", HostPort: "8080"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("buildDockerPortBindings: %v", err)
|
||||
}
|
||||
got := bindings[network.MustParsePort("8080/tcp")]
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("bindings = %d entries, want 1", len(got))
|
||||
}
|
||||
if got[0].HostIP.IsValid() {
|
||||
t.Errorf("host IP = %v, want the zero address (all interfaces)", got[0].HostIP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHostPort(t *testing.T) {
|
||||
if got, err := parseHostPort("9001"); err != nil || got != 9001 {
|
||||
t.Fatalf("parseHostPort(9001) = %d, %v", got, err)
|
||||
}
|
||||
for _, in := range []string{"", "abc", "70000", "-1"} {
|
||||
if _, err := parseHostPort(in); err == nil {
|
||||
t.Errorf("parseHostPort(%q) = nil error, want an error", in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,13 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
dockertypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
type storageAPI interface {
|
||||
DiskUsage(context.Context, dockertypes.DiskUsageOptions) (dockertypes.DiskUsage, error)
|
||||
BuildCachePrune(context.Context, build.CachePruneOptions) (*build.CachePruneReport, error)
|
||||
ImagesPrune(context.Context, filters.Args) (image.PruneReport, error)
|
||||
DiskUsage(context.Context, client.DiskUsageOptions) (client.DiskUsageResult, error)
|
||||
BuildCachePrune(context.Context, client.BuildCachePruneOptions) (client.BuildCachePruneResult, error)
|
||||
ImagePrune(context.Context, client.ImagePruneOptions) (client.ImagePruneResult, error)
|
||||
}
|
||||
|
||||
type ImageUsage struct {
|
||||
@@ -50,39 +47,34 @@ type PruneResult struct {
|
||||
}
|
||||
|
||||
func (c *Client) DiskUsage(ctx context.Context) (DiskUsage, error) {
|
||||
usage, err := c.storageClient().DiskUsage(ctx, dockertypes.DiskUsageOptions{
|
||||
Types: []dockertypes.DiskUsageObject{
|
||||
dockertypes.ContainerObject,
|
||||
dockertypes.ImageObject,
|
||||
dockertypes.BuildCacheObject,
|
||||
},
|
||||
// Verbose is required for the daemon to return per-image and per-container
|
||||
// records alongside the aggregate totals.
|
||||
usage, err := c.storageClient().DiskUsage(ctx, client.DiskUsageOptions{
|
||||
Containers: true,
|
||||
Images: true,
|
||||
BuildCache: true,
|
||||
Verbose: true,
|
||||
})
|
||||
if err != nil {
|
||||
return DiskUsage{}, fmt.Errorf("read Docker disk usage: %w", err)
|
||||
}
|
||||
result := DiskUsage{
|
||||
ImageLayerBytes: usage.LayersSize,
|
||||
Images: make([]ImageUsage, 0, len(usage.Images)),
|
||||
Containers: make([]ContainerUsage, 0, len(usage.Containers)),
|
||||
ImageLayerBytes: usage.Images.TotalSize,
|
||||
Images: make([]ImageUsage, 0, len(usage.Images.Items)),
|
||||
Containers: make([]ContainerUsage, 0, len(usage.Containers.Items)),
|
||||
}
|
||||
for _, cache := range usage.BuildCache {
|
||||
if cache != nil && cache.Size > 0 {
|
||||
for _, cache := range usage.BuildCache.Items {
|
||||
if cache.Size > 0 {
|
||||
result.BuildCacheBytes += cache.Size
|
||||
}
|
||||
}
|
||||
for _, item := range usage.Images {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
for _, item := range usage.Images.Items {
|
||||
result.Images = append(result.Images, ImageUsage{
|
||||
ID: item.ID, SizeBytes: item.Size, Containers: item.Containers,
|
||||
CreatedAt: time.Unix(item.Created, 0).UTC(),
|
||||
})
|
||||
}
|
||||
for _, item := range usage.Containers {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
for _, item := range usage.Containers.Items {
|
||||
result.Containers = append(result.Containers, ContainerUsage{
|
||||
ID: item.ID, WritableBytes: item.SizeRw, Labels: item.Labels,
|
||||
})
|
||||
@@ -91,32 +83,34 @@ func (c *Client) DiskUsage(ctx context.Context) (DiskUsage, error) {
|
||||
}
|
||||
|
||||
func (c *Client) PruneBuildCache(ctx context.Context, options BuildCachePruneOptions) (PruneResult, error) {
|
||||
pruneFilters := filters.NewArgs(filters.Arg("until", unixFilter(options.UnusedBefore)))
|
||||
report, err := c.storageClient().BuildCachePrune(ctx, build.CachePruneOptions{
|
||||
All: true, ReservedSpace: options.KeepBytes, KeepStorage: options.KeepBytes, Filters: pruneFilters,
|
||||
// The client translates ReservedSpace into the legacy "keep-storage"
|
||||
// parameter when talking to daemons on API <= 1.47.
|
||||
result, err := c.storageClient().BuildCachePrune(ctx, client.BuildCachePruneOptions{
|
||||
All: true,
|
||||
ReservedSpace: options.KeepBytes,
|
||||
Filters: make(client.Filters).Add("until", unixFilter(options.UnusedBefore)),
|
||||
})
|
||||
if err != nil {
|
||||
return PruneResult{}, fmt.Errorf("prune Docker build cache: %w", err)
|
||||
}
|
||||
if report == nil {
|
||||
return PruneResult{}, nil
|
||||
}
|
||||
return PruneResult{
|
||||
Deleted: len(report.CachesDeleted), BytesReclaimed: reclaimedBytes(report.SpaceReclaimed),
|
||||
Deleted: len(result.Report.CachesDeleted),
|
||||
BytesReclaimed: reclaimedBytes(result.Report.SpaceReclaimed),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) PruneUnusedImages(ctx context.Context, unusedBefore time.Time) (PruneResult, error) {
|
||||
pruneFilters := filters.NewArgs(
|
||||
filters.Arg("dangling", "false"),
|
||||
filters.Arg("until", unixFilter(unusedBefore)),
|
||||
)
|
||||
report, err := c.storageClient().ImagesPrune(ctx, pruneFilters)
|
||||
result, err := c.storageClient().ImagePrune(ctx, client.ImagePruneOptions{
|
||||
Filters: make(client.Filters).
|
||||
Add("dangling", "false").
|
||||
Add("until", unixFilter(unusedBefore)),
|
||||
})
|
||||
if err != nil {
|
||||
return PruneResult{}, fmt.Errorf("prune unused Docker images: %w", err)
|
||||
}
|
||||
return PruneResult{
|
||||
Deleted: len(report.ImagesDeleted), BytesReclaimed: reclaimedBytes(report.SpaceReclaimed),
|
||||
Deleted: len(result.Report.ImagesDeleted),
|
||||
BytesReclaimed: reclaimedBytes(result.Report.SpaceReclaimed),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dockertypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
"github.com/moby/moby/api/types/build"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/client"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -63,21 +62,27 @@ func TestStorageResultJSONUsesSnakeCase(t *testing.T) {
|
||||
func TestDiskUsageMapsTypedSDKResponse(t *testing.T) {
|
||||
created := time.Unix(100, 0)
|
||||
lastUsed := time.Unix(200, 0)
|
||||
sdk := &fakeStorageAPI{usage: dockertypes.DiskUsage{
|
||||
LayersSize: 500,
|
||||
Images: []*image.Summary{{ID: "image-1", Size: 300, Containers: 0, Created: created.Unix()}},
|
||||
Containers: []*container.Summary{
|
||||
{ID: "managed", SizeRw: 75, Labels: map[string]string{"kandev.managed": "true"}},
|
||||
{ID: "unrelated", SizeRw: 125, Labels: map[string]string{"owner": "user"}},
|
||||
sdk := &fakeStorageAPI{usage: client.DiskUsageResult{
|
||||
Images: client.ImagesDiskUsage{
|
||||
TotalSize: 500,
|
||||
Items: []image.Summary{{ID: "image-1", Size: 300, Containers: 0, Created: created.Unix()}},
|
||||
},
|
||||
BuildCache: []*build.CacheRecord{
|
||||
{ID: "cache-1", Size: 100},
|
||||
{ID: "cache-2", Size: 200, LastUsedAt: &lastUsed},
|
||||
Containers: client.ContainersDiskUsage{
|
||||
Items: []container.Summary{
|
||||
{ID: "managed", SizeRw: 75, Labels: map[string]string{"kandev.managed": "true"}},
|
||||
{ID: "unrelated", SizeRw: 125, Labels: map[string]string{"owner": "user"}},
|
||||
},
|
||||
},
|
||||
BuildCache: client.BuildCacheDiskUsage{
|
||||
Items: []build.CacheRecord{
|
||||
{ID: "cache-1", Size: 100},
|
||||
{ID: "cache-2", Size: 200, LastUsedAt: &lastUsed},
|
||||
},
|
||||
},
|
||||
}}
|
||||
client := &Client{storage: sdk}
|
||||
dockerClient := &Client{storage: sdk}
|
||||
|
||||
got, err := client.DiskUsage(context.Background())
|
||||
got, err := dockerClient.DiskUsage(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("DiskUsage: %v", err)
|
||||
}
|
||||
@@ -87,20 +92,23 @@ func TestDiskUsageMapsTypedSDKResponse(t *testing.T) {
|
||||
got.Containers[0].Labels["kandev.managed"] != "true" {
|
||||
t.Fatalf("disk usage = %#v", got)
|
||||
}
|
||||
wantTypes := []dockertypes.DiskUsageObject{
|
||||
dockertypes.ContainerObject, dockertypes.ImageObject, dockertypes.BuildCacheObject,
|
||||
// Verbose is what makes the daemon return the per-object Items above.
|
||||
wantOptions := client.DiskUsageOptions{
|
||||
Containers: true, Images: true, BuildCache: true, Verbose: true,
|
||||
}
|
||||
if !reflect.DeepEqual(sdk.usageOptions.Types, wantTypes) {
|
||||
t.Fatalf("disk usage types = %#v, want %#v", sdk.usageOptions.Types, wantTypes)
|
||||
if !reflect.DeepEqual(sdk.usageOptions, wantOptions) {
|
||||
t.Fatalf("disk usage options = %#v, want %#v", sdk.usageOptions, wantOptions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPruneBuildCacheUsesAgeAndReservedSpaceFilters(t *testing.T) {
|
||||
sdk := &fakeStorageAPI{buildReport: &build.CachePruneReport{CachesDeleted: []string{"one"}, SpaceReclaimed: 42}}
|
||||
client := &Client{storage: sdk}
|
||||
sdk := &fakeStorageAPI{buildResult: client.BuildCachePruneResult{
|
||||
Report: build.CachePruneReport{CachesDeleted: []string{"one"}, SpaceReclaimed: 42},
|
||||
}}
|
||||
dockerClient := &Client{storage: sdk}
|
||||
cutoff := time.Unix(1234, 0).UTC()
|
||||
|
||||
got, err := client.PruneBuildCache(context.Background(), BuildCachePruneOptions{
|
||||
got, err := dockerClient.PruneBuildCache(context.Background(), BuildCachePruneOptions{
|
||||
KeepBytes: 1024, UnusedBefore: cutoff,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -110,27 +118,29 @@ func TestPruneBuildCacheUsesAgeAndReservedSpaceFilters(t *testing.T) {
|
||||
t.Fatalf("prune result = %#v", got)
|
||||
}
|
||||
if !sdk.buildOptions.All || sdk.buildOptions.ReservedSpace != 1024 ||
|
||||
sdk.buildOptions.KeepStorage != 1024 ||
|
||||
!sdk.buildOptions.Filters.ExactMatch("until", "1234") {
|
||||
!sdk.buildOptions.Filters["until"]["1234"] {
|
||||
t.Fatalf("build prune options = %#v", sdk.buildOptions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPruneUnusedImagesUsesAllUnusedAndAgeFilters(t *testing.T) {
|
||||
sdk := &fakeStorageAPI{imageReport: image.PruneReport{
|
||||
ImagesDeleted: []image.DeleteResponse{{Deleted: "image-1"}}, SpaceReclaimed: 84,
|
||||
sdk := &fakeStorageAPI{imageResult: client.ImagePruneResult{
|
||||
Report: image.PruneReport{
|
||||
ImagesDeleted: []image.DeleteResponse{{Deleted: "image-1"}}, SpaceReclaimed: 84,
|
||||
},
|
||||
}}
|
||||
client := &Client{storage: sdk}
|
||||
dockerClient := &Client{storage: sdk}
|
||||
|
||||
got, err := client.PruneUnusedImages(context.Background(), time.Unix(5678, 0).UTC())
|
||||
got, err := dockerClient.PruneUnusedImages(context.Background(), time.Unix(5678, 0).UTC())
|
||||
if err != nil {
|
||||
t.Fatalf("PruneUnusedImages: %v", err)
|
||||
}
|
||||
if got.Deleted != 1 || got.BytesReclaimed != 84 {
|
||||
t.Fatalf("prune result = %#v", got)
|
||||
}
|
||||
if !sdk.imageFilters.ExactMatch("dangling", "false") || !sdk.imageFilters.ExactMatch("until", "5678") {
|
||||
t.Fatalf("image prune filters = %#v", sdk.imageFilters)
|
||||
if !sdk.imageOptions.Filters["dangling"]["false"] ||
|
||||
!sdk.imageOptions.Filters["until"]["5678"] {
|
||||
t.Fatalf("image prune filters = %#v", sdk.imageOptions.Filters)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,9 +150,9 @@ func TestRemoveContainerRemovesAttachedVolumesWithoutGlobalPrune(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("new logger: %v", err)
|
||||
}
|
||||
client := &Client{remover: remover, logger: log}
|
||||
dockerClient := &Client{remover: remover, logger: log}
|
||||
|
||||
if err := client.RemoveContainer(context.Background(), "container-1", false); err != nil {
|
||||
if err := dockerClient.RemoveContainer(context.Background(), "container-1", false); err != nil {
|
||||
t.Fatalf("RemoveContainer: %v", err)
|
||||
}
|
||||
if remover.id != "container-1" || remover.options.Force || !remover.options.RemoveVolumes {
|
||||
@@ -151,39 +161,41 @@ func TestRemoveContainerRemovesAttachedVolumesWithoutGlobalPrune(t *testing.T) {
|
||||
}
|
||||
|
||||
type fakeStorageAPI struct {
|
||||
usage dockertypes.DiskUsage
|
||||
usage client.DiskUsageResult
|
||||
usageErr error
|
||||
usageOptions dockertypes.DiskUsageOptions
|
||||
buildOptions build.CachePruneOptions
|
||||
buildReport *build.CachePruneReport
|
||||
usageOptions client.DiskUsageOptions
|
||||
buildOptions client.BuildCachePruneOptions
|
||||
buildResult client.BuildCachePruneResult
|
||||
buildErr error
|
||||
imageFilters filters.Args
|
||||
imageReport image.PruneReport
|
||||
imageOptions client.ImagePruneOptions
|
||||
imageResult client.ImagePruneResult
|
||||
imageErr error
|
||||
}
|
||||
|
||||
func (f *fakeStorageAPI) DiskUsage(_ context.Context, options dockertypes.DiskUsageOptions) (dockertypes.DiskUsage, error) {
|
||||
func (f *fakeStorageAPI) DiskUsage(_ context.Context, options client.DiskUsageOptions) (client.DiskUsageResult, error) {
|
||||
f.usageOptions = options
|
||||
return f.usage, f.usageErr
|
||||
}
|
||||
|
||||
func (f *fakeStorageAPI) BuildCachePrune(_ context.Context, options build.CachePruneOptions) (*build.CachePruneReport, error) {
|
||||
func (f *fakeStorageAPI) BuildCachePrune(_ context.Context, options client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
|
||||
f.buildOptions = options
|
||||
return f.buildReport, f.buildErr
|
||||
return f.buildResult, f.buildErr
|
||||
}
|
||||
|
||||
func (f *fakeStorageAPI) ImagesPrune(_ context.Context, pruneFilters filters.Args) (image.PruneReport, error) {
|
||||
f.imageFilters = pruneFilters
|
||||
return f.imageReport, f.imageErr
|
||||
func (f *fakeStorageAPI) ImagePrune(_ context.Context, options client.ImagePruneOptions) (client.ImagePruneResult, error) {
|
||||
f.imageOptions = options
|
||||
return f.imageResult, f.imageErr
|
||||
}
|
||||
|
||||
type fakeContainerRemover struct {
|
||||
id string
|
||||
options container.RemoveOptions
|
||||
options client.ContainerRemoveOptions
|
||||
}
|
||||
|
||||
func (f *fakeContainerRemover) ContainerRemove(_ context.Context, id string, options container.RemoveOptions) error {
|
||||
func (f *fakeContainerRemover) ContainerRemove(
|
||||
_ context.Context, id string, options client.ContainerRemoveOptions,
|
||||
) (client.ContainerRemoveResult, error) {
|
||||
f.id = id
|
||||
f.options = options
|
||||
return nil
|
||||
return client.ContainerRemoveResult{}, nil
|
||||
}
|
||||
|
||||
@@ -19,8 +19,18 @@ ignore = [
|
||||
# Fix: upgrade rand to >= 0.8.6 (semver-breaking; requires coordinating dependents).
|
||||
"RUSTSEC-2026-0097",
|
||||
|
||||
# glib 0.18.5 — unsound VariantStrIter::impl_get, Linux-only via webkit2gtk chain.
|
||||
# No known exploit in the wild; will resolve when Tauri migrates to webkit2gtk-4.1
|
||||
# which pulls glib >= 0.20.0.
|
||||
# glib 0.18.5 — unsound VariantStrIter::impl_get, Linux-only via the gtk 0.18
|
||||
# chain that Tauri v2 uses for its Linux backend.
|
||||
#
|
||||
# The advisory's fix version (0.20.0) is not reachable: gtk-rs/gtk3-rs, which
|
||||
# publishes gtk/gdk/webkit2gtk, was archived in March 2024 and will never
|
||||
# release a version depending on glib >= 0.20. Tauri closed its own tracking
|
||||
# issue as not_planned and targets the GTK4 migration at Tauri v3
|
||||
# (tauri-apps/tauri#12564).
|
||||
#
|
||||
# A glib 0.18.6 patch release would fix this for us automatically, since
|
||||
# gtk 0.18.2 depends on glib with a caret requirement. Backport submitted
|
||||
# upstream: gtk-rs/gtk-rs-core#2009, release request #2010. Drop this entry
|
||||
# once 0.18.6 ships.
|
||||
"RUSTSEC-2024-0429",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user