diff --git a/apps/backend/go.mod b/apps/backend/go.mod index 6c02033769..0cc43cd7c6 100644 --- a/apps/backend/go.mod +++ b/apps/backend/go.mod @@ -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 diff --git a/apps/backend/go.sum b/apps/backend/go.sum index c450ca9345..f364fe03f6 100644 --- a/apps/backend/go.sum +++ b/apps/backend/go.sum @@ -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= diff --git a/apps/backend/internal/agent/docker/activity_test.go b/apps/backend/internal/agent/docker/activity_test.go index ec9fb80107..9de66f48ea 100644 --- a/apps/backend/internal/agent/docker/activity_test.go +++ b/apps/backend/internal/agent/docker/activity_test.go @@ -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 } diff --git a/apps/backend/internal/agent/docker/client.go b/apps/backend/internal/agent/docker/client.go index 47361cc9f9..a1021e3df1 100644 --- a/apps/backend/internal/agent/docker/client.go +++ b/apps/backend/internal/agent/docker/client.go @@ -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, diff --git a/apps/backend/internal/agent/docker/client_test.go b/apps/backend/internal/agent/docker/client_test.go index d2d86476b9..349ccf4f15 100644 --- a/apps/backend/internal/agent/docker/client_test.go +++ b/apps/backend/internal/agent/docker/client_test.go @@ -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) + } + } +} diff --git a/apps/backend/internal/agent/docker/storage.go b/apps/backend/internal/agent/docker/storage.go index 2e656e8079..e5fdff4f12 100644 --- a/apps/backend/internal/agent/docker/storage.go +++ b/apps/backend/internal/agent/docker/storage.go @@ -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 } diff --git a/apps/backend/internal/agent/docker/storage_test.go b/apps/backend/internal/agent/docker/storage_test.go index 9568db0a4c..702b087818 100644 --- a/apps/backend/internal/agent/docker/storage_test.go +++ b/apps/backend/internal/agent/docker/storage_test.go @@ -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 } diff --git a/apps/desktop/src-tauri/.cargo/audit.toml b/apps/desktop/src-tauri/.cargo/audit.toml index 459f717f23..74dc235712 100644 --- a/apps/desktop/src-tauri/.cargo/audit.toml +++ b/apps/desktop/src-tauri/.cargo/audit.toml @@ -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", ]