refactor: remove native compose deploy feature (#5022)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-09-06 01:56:14 +00:00
committed by GitHub
co-authored by Claude Opus 5
parent 49d7954190
commit 81c3e30152
13 changed files with 69 additions and 2232 deletions
+2 -24
View File
@@ -5,16 +5,12 @@ require (
github.com/alexflint/go-arg v1.6.1
github.com/andybalholm/brotli v1.2.3
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/compose-spec/compose-go/v2 v2.15.0
github.com/docker/go-units v0.5.0
github.com/dustin/go-humanize v1.0.1
github.com/expr-lang/expr v1.17.8
github.com/go-chi/chi/v5 v5.3.2
github.com/go-chi/jwtauth/v5 v5.4.0
github.com/go-faker/faker/v4 v4.11.0
github.com/go-git/go-git/v5 v5.19.2
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/moby/docker-image-spec v1.3.1
github.com/moby/moby/api v1.56.0
github.com/moby/moby/client v0.6.0
github.com/modelcontextprotocol/go-sdk v1.7.0
@@ -41,7 +37,6 @@ require (
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/air-verse/air v1.67.4 // indirect
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.4 // indirect
@@ -51,22 +46,18 @@ require (
github.com/bits-and-blooms/bitset v1.24.5 // indirect
github.com/buger/jsonparser v1.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.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/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
@@ -83,20 +74,15 @@ require (
github.com/go-openapi/swag/stringutils v0.27.1 // indirect
github.com/go-openapi/swag/typeutils v0.27.1 // indirect
github.com/go-openapi/swag/yamlutils v0.27.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/gohugoio/hashstructure v0.6.0 // indirect
github.com/gohugoio/hugo v0.164.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.3.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
@@ -107,7 +93,7 @@ require (
github.com/mailru/easyjson v0.9.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-shellwords v1.0.13 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/spdystream v0.5.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
@@ -116,21 +102,15 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/sirupsen/logrus v1.10.1 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/tdewolff/parse/v2 v2.8.12 // indirect
github.com/valyala/fastjson v1.6.10 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/goldmark v1.8.5 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
@@ -139,7 +119,6 @@ require (
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
@@ -149,7 +128,6 @@ require (
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
k8s.io/streaming v0.37.0 // indirect
-80
View File
@@ -6,11 +6,8 @@ github.com/JohannesKaufmann/dom v0.3.1 h1:J16l9JAHWgkFPR3VIPbQ1gvS0cWab6laK1q7PF
github.com/JohannesKaufmann/dom v0.3.1/go.mod h1:BZPkf8ZeYrBgABjwJn9iiKt8aiCtkxpHkevms+Yp2DE=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2 h1:XFJZFWESIWlUEHHjzBuv8RvrtCWnSGlimEX17ysSDb8=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2/go.mod h1:BHWO8lJzttJLqwuV8Rb1B3OG2OSzLbssZDI1FRg2eAA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/PuerkitoBio/goquery v1.13.0 h1:mqHbjD7Jmnul4DTR24LKTjo1uUmHUh072kteGV+xpFM=
github.com/PuerkitoBio/goquery v1.13.0/go.mod h1:Hip5mdBL8K2wEGKJdr27sRaNwIdDajmCwB/ExUPwW+g=
github.com/air-verse/air v1.67.4 h1:pkWBQues0094VMgUZ9MboNfS1vyCBEXeU6QxuD6/PwU=
@@ -25,8 +22,6 @@ github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
@@ -77,18 +72,10 @@ github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+Urai
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/compose-spec/compose-go/v2 v2.14.0 h1:uaJeo5B3+OVlu+Rx2qLBcAdXPEUUzm5nQrRiGJafRAQ=
github.com/compose-spec/compose-go/v2 v2.14.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg=
github.com/compose-spec/compose-go/v2 v2.15.0 h1:tdQw+eMyT+P6ZIb09JfcIVvbMmIa+PjST7cWezVLf00=
github.com/compose-spec/compose-go/v2 v2.15.0/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -98,7 +85,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjY
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/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
@@ -107,12 +93,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/evanw/esbuild v0.28.1 h1:ds+yuRyUaZGx++GR56CrCeuXh8PVhVM4xq8v7PNELFc=
github.com/evanw/esbuild v0.28.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
@@ -130,22 +112,12 @@ github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getkin/kin-openapi v0.140.0 h1:JFn675aXRFjyiZKa/BFWploGldQlI0gobp4J5k0EZ2g=
github.com/getkin/kin-openapi v0.140.0/go.mod h1:lISrB64F0CPcuDJ3LdtPTMJBY8VENjR9wJBdrcT6J3g=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/jwtauth/v5 v5.4.0 h1:Ieh0xMJsFvqylqJ02/mQHKzbbKO9DYNBh4DPKCwTwYI=
github.com/go-chi/jwtauth/v5 v5.4.0/go.mod h1:w6yjqUUXz1b8+oiJel64Sz1KJwduQM6qUA5QNzO5+bQ=
github.com/go-faker/faker/v4 v4.11.0 h1:HeIFTzafXsgrlxKE2QySGGTQocfGdQ8sGqU2CXvs120=
github.com/go-faker/faker/v4 v4.11.0/go.mod h1:VFIEwWDd16EdYDLF6NJ5gAAzEp7vz5LgKgJ2iZ17Tdg=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -185,8 +157,6 @@ github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
@@ -213,8 +183,6 @@ github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0 h1:p13Q0DBCrBRpJ
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0/go.mod h1:ob9PCHy/ocsQhTz68uxhyInaYCbbVNpOOrJkIoSeD+8=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
@@ -235,18 +203,12 @@ github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTA
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -282,20 +244,14 @@ github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4=
github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/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/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/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ=
github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs=
github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ=
github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y=
@@ -326,8 +282,6 @@ github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -338,8 +292,6 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -361,13 +313,6 @@ github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfv
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q=
github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -398,10 +343,6 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
@@ -410,8 +351,6 @@ github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
github.com/yuin/goldmark/v2 v2.0.0 h1:P6JP1Px30eqo339He8dDFE8D0BSM21eQcbozLWH+E6g=
github.com/yuin/goldmark/v2 v2.0.0/go.mod h1:G6M4/qOFtfn01/o14BU1UR2Lo5N3S9Qo7xCuz/sHjGQ=
github.com/yuin/goldmark/v2 v2.0.1 h1:Os0APQLbfXBzA7pc8qF8HBSTO8LOIvhQHX6pEA5/0CY=
github.com/yuin/goldmark/v2 v2.0.1/go.mod h1:G6M4/qOFtfn01/o14BU1UR2Lo5N3S9Qo7xCuz/sHjGQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
@@ -432,43 +371,26 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
@@ -489,8 +411,6 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-21
View File
@@ -1,21 +0,0 @@
package deploy
import (
"context"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
)
// ParseCompose parses raw compose YAML bytes into a Project.
// The project name is used as a prefix for resource names (networks, volumes, containers).
func ParseCompose(ctx context.Context, data []byte, projectName string) (*types.Project, error) {
return loader.LoadWithContext(ctx, types.ConfigDetails{
ConfigFiles: []types.ConfigFile{
{Content: data},
},
}, func(opts *loader.Options) {
opts.SetProjectName(projectName, true)
opts.SkipResolveEnvironment = true
}, loader.WithSkipValidation)
}
-292
View File
@@ -1,292 +0,0 @@
package deploy
import (
"context"
"testing"
composetypes "github.com/compose-spec/compose-go/v2/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseCompose_BasicServices(t *testing.T) {
data := []byte(`
services:
web:
image: nginx:latest
ports:
- "8080:80"
environment:
- FOO=bar
- BAZ=qux
volumes:
- ./html:/usr/share/nginx/html:ro
depends_on:
- db
restart: unless-stopped
labels:
app: web
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
restart: always
volumes:
pgdata:
networks:
default:
driver: bridge
`)
project, err := ParseCompose(context.Background(), data, "myapp")
require.NoError(t, err)
assert.Equal(t, "myapp", project.Name)
assert.Len(t, project.Services, 2)
web, err := project.GetService("web")
require.NoError(t, err)
assert.Equal(t, "nginx:latest", web.Image)
assert.Len(t, web.Ports, 1)
assert.Equal(t, uint32(80), web.Ports[0].Target)
assert.Equal(t, "8080", web.Ports[0].Published)
assert.Equal(t, "bar", *web.Environment["FOO"])
assert.Equal(t, "qux", *web.Environment["BAZ"])
assert.Contains(t, web.DependsOn, "db")
assert.Equal(t, "unless-stopped", web.Restart)
assert.Equal(t, "web", web.Labels["app"])
db, err := project.GetService("db")
require.NoError(t, err)
assert.Equal(t, "postgres:16", db.Image)
assert.Equal(t, "secret", *db.Environment["POSTGRES_PASSWORD"])
assert.Equal(t, "always", db.Restart)
assert.Contains(t, project.Volumes, "pgdata")
assert.Equal(t, "bridge", project.Networks["default"].Driver)
}
func TestParseCompose_MapDependsOn(t *testing.T) {
data := []byte(`
services:
web:
image: nginx
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres
redis:
image: redis
`)
project, err := ParseCompose(context.Background(), data, "test")
require.NoError(t, err)
web, _ := project.GetService("web")
assert.Contains(t, web.DependsOn, "db")
assert.Contains(t, web.DependsOn, "redis")
}
func TestParseCompose_ServiceNetworksList(t *testing.T) {
data := []byte(`
services:
web:
image: nginx
networks:
- frontend
- backend
networks:
frontend:
backend:
`)
project, err := ParseCompose(context.Background(), data, "test")
require.NoError(t, err)
web, _ := project.GetService("web")
assert.Contains(t, web.Networks, "frontend")
assert.Contains(t, web.Networks, "backend")
}
func TestParseCompose_ServiceNetworksMapWithAliases(t *testing.T) {
data := []byte(`
services:
web:
image: nginx
networks:
frontend:
aliases:
- web-alias
backend: {}
networks:
frontend:
backend:
`)
project, err := ParseCompose(context.Background(), data, "test")
require.NoError(t, err)
web, _ := project.GetService("web")
assert.Len(t, web.Networks, 2)
assert.Equal(t, []string{"web-alias"}, web.Networks["frontend"].Aliases)
}
func TestParseCompose_CommandStringAndSlice(t *testing.T) {
data := []byte(`
services:
string_cmd:
image: alpine
command: echo hello world
slice_cmd:
image: alpine
command:
- echo
- hello
- world
`)
project, err := ParseCompose(context.Background(), data, "test")
require.NoError(t, err)
strCmd, _ := project.GetService("string_cmd")
assert.Equal(t, []string{"echo", "hello", "world"}, []string(strCmd.Command))
sliceCmd, _ := project.GetService("slice_cmd")
assert.Equal(t, []string{"echo", "hello", "world"}, []string(sliceCmd.Command))
}
func TestParseCompose_EnvironmentMap(t *testing.T) {
data := []byte(`
services:
web:
image: nginx
environment:
FOO: bar
BAZ: "123"
`)
project, err := ParseCompose(context.Background(), data, "test")
require.NoError(t, err)
web, _ := project.GetService("web")
assert.Equal(t, "bar", *web.Environment["FOO"])
assert.Equal(t, "123", *web.Environment["BAZ"])
}
func TestParseCompose_LabelsAsList(t *testing.T) {
data := []byte(`
services:
web:
image: nginx
labels:
- "com.example.foo=bar"
- "com.example.baz=qux"
`)
project, err := ParseCompose(context.Background(), data, "test")
require.NoError(t, err)
web, _ := project.GetService("web")
assert.Equal(t, "bar", web.Labels["com.example.foo"])
assert.Equal(t, "qux", web.Labels["com.example.baz"])
}
func TestParseCompose_ExternalNetwork(t *testing.T) {
data := []byte(`
services:
web:
image: nginx
networks:
- mynet
networks:
mynet:
external: true
`)
project, err := ParseCompose(context.Background(), data, "test")
require.NoError(t, err)
assert.True(t, bool(project.Networks["mynet"].External))
}
func TestParseCompose_ExternalVolume(t *testing.T) {
data := []byte(`
services:
web:
image: nginx
volumes:
- shared:/data
volumes:
shared:
external: true
`)
project, err := ParseCompose(context.Background(), data, "test")
require.NoError(t, err)
assert.True(t, bool(project.Volumes["shared"].External))
}
func TestParseCompose_PortFormats(t *testing.T) {
data := []byte(`
services:
web:
image: nginx
ports:
- "80"
- "8080:80"
- "8080:80/udp"
- "127.0.0.1:8080:80"
`)
project, err := ParseCompose(context.Background(), data, "test")
require.NoError(t, err)
web, _ := project.GetService("web")
assert.Len(t, web.Ports, 4)
assert.Equal(t, uint32(80), web.Ports[0].Target)
assert.Equal(t, uint32(80), web.Ports[1].Target)
assert.Equal(t, "8080", web.Ports[1].Published)
assert.Equal(t, "udp", web.Ports[2].Protocol)
assert.Equal(t, "127.0.0.1", web.Ports[3].HostIP)
}
func TestParseCompose_DependencyOrder(t *testing.T) {
data := []byte(`
services:
web:
image: nginx
depends_on:
- api
api:
image: myapi
depends_on:
- db
- redis
db:
image: postgres
redis:
image: redis
`)
project, err := ParseCompose(context.Background(), data, "test")
require.NoError(t, err)
var order []string
err = project.ForEachService(nil, func(name string, svc *composetypes.ServiceConfig) error {
order = append(order, name)
return nil
})
require.NoError(t, err)
indexOf := func(name string) int {
for i, n := range order {
if n == name {
return i
}
}
return -1
}
assert.Less(t, indexOf("db"), indexOf("api"))
assert.Less(t, indexOf("redis"), indexOf("api"))
assert.Less(t, indexOf("api"), indexOf("web"))
}
-724
View File
@@ -1,724 +0,0 @@
package deploy
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"net/netip"
"sort"
"strconv"
"time"
composetypes "github.com/compose-spec/compose-go/v2/types"
units "github.com/docker/go-units"
dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
"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"
"github.com/rs/zerolog/log"
"golang.org/x/sync/errgroup"
)
// Compose resource labels. Used to tag every network/volume/container we
// create so we can filter project-owned resources on teardown.
const (
labelComposeProject = "com.docker.compose.project"
labelComposeService = "com.docker.compose.service"
labelComposeNetwork = "com.docker.compose.network"
labelComposeVolume = "com.docker.compose.volume"
)
// StatusUpdate reports progress during deployment.
type StatusUpdate struct {
Service string
Action string
}
func sendStatus(ctx context.Context, ch chan<- StatusUpdate, service, action string) {
if ch == nil {
return
}
select {
case ch <- StatusUpdate{Service: service, Action: action}:
case <-ctx.Done():
}
}
func shortID(id string) string {
if len(id) < 12 {
return id
}
return id[:12]
}
func drainPullStream(r io.Reader) error {
dec := json.NewDecoder(r)
for {
var msg struct {
Error string `json:"error"`
ErrorDetail struct {
Message string `json:"message"`
} `json:"errorDetail"`
}
if err := dec.Decode(&msg); err != nil {
if err == io.EOF {
return nil
}
return fmt.Errorf("decoding pull stream: %w", err)
}
if msg.ErrorDetail.Message != "" {
return errors.New(msg.ErrorDetail.Message)
}
if msg.Error != "" {
return errors.New(msg.Error)
}
}
}
// DockerClient is the subset of the Docker API needed for deployment.
type DockerClient interface {
ImagePull(ctx context.Context, refStr string, options client.ImagePullOptions) (client.ImagePullResponse, error)
ContainerCreate(ctx context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error)
ContainerStart(ctx context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error)
ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error)
ContainerStop(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error)
ContainerRemove(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error)
NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error)
NetworkCreate(ctx context.Context, name string, options client.NetworkCreateOptions) (client.NetworkCreateResult, error)
NetworkConnect(ctx context.Context, networkID string, options client.NetworkConnectOptions) (client.NetworkConnectResult, error)
NetworkRemove(ctx context.Context, networkID string, options client.NetworkRemoveOptions) (client.NetworkRemoveResult, error)
VolumeCreate(ctx context.Context, options client.VolumeCreateOptions) (client.VolumeCreateResult, error)
VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error)
VolumeRemove(ctx context.Context, volumeID string, options client.VolumeRemoveOptions) (client.VolumeRemoveResult, error)
}
// Deployer deploys a parsed compose project using the Docker API directly.
type Deployer struct {
cli DockerClient
}
// NewDeployer creates a deployer that uses the given Docker client.
func NewDeployer(cli DockerClient) *Deployer {
return &Deployer{cli: cli}
}
// Deploy removes existing project containers, ensures networks and volumes
// exist, then pulls images and creates+starts containers in dependency order.
// Status updates are sent to the optional status channel.
func (d *Deployer) Deploy(ctx context.Context, project *composetypes.Project, status chan<- StatusUpdate) error {
if err := d.removeProjectContainers(ctx, project.Name, status); err != nil {
return fmt.Errorf("removing existing containers: %w", err)
}
networkIDs, err := d.ensureNetworks(ctx, project)
if err != nil {
return fmt.Errorf("ensuring networks: %w", err)
}
if err := d.createVolumes(ctx, project); err != nil {
return fmt.Errorf("creating volumes: %w", err)
}
if err := d.pullImages(ctx, project, status); err != nil {
return err
}
return project.ForEachService(nil, func(name string, svc *composetypes.ServiceConfig) error {
return d.deployService(ctx, project.Name, name, *svc, networkIDs, status)
})
}
// pullImages pulls every service image concurrently (bounded). Pulls are
// independent, so fanning them out collapses wall time on multi-service
// projects where the ordered create phase is gated on the slowest image.
func (d *Deployer) pullImages(ctx context.Context, project *composetypes.Project, status chan<- StatusUpdate) error {
g, gctx := errgroup.WithContext(ctx)
g.SetLimit(5)
for svcName, svc := range project.Services {
if svc.Image == "" {
continue
}
imageRef := svc.Image
g.Go(func() error {
sendStatus(gctx, status, svcName, "pulling")
log.Info().Str("service", svcName).Str("image", imageRef).Msg("Pulling image")
reader, err := d.cli.ImagePull(gctx, imageRef, client.ImagePullOptions{})
if err != nil {
return fmt.Errorf("pulling image %q: %w", imageRef, err)
}
pullErr := drainPullStream(reader)
reader.Close()
if pullErr != nil {
return fmt.Errorf("pulling image %q: %w", imageRef, pullErr)
}
return nil
})
}
return g.Wait()
}
// Remove stops and removes all containers and project-labeled networks for
// the given project. Volumes are preserved unless removeVolumes is true —
// volumes typically hold user data, so they opt in explicitly.
func (d *Deployer) Remove(ctx context.Context, projectName string, removeVolumes bool, status chan<- StatusUpdate) error {
if err := d.removeProjectContainers(ctx, projectName, status); err != nil {
return fmt.Errorf("removing containers: %w", err)
}
if err := d.removeProjectNetworks(ctx, projectName); err != nil {
return fmt.Errorf("removing networks: %w", err)
}
if removeVolumes {
if err := d.removeProjectVolumes(ctx, projectName); err != nil {
return fmt.Errorf("removing volumes: %w", err)
}
}
return nil
}
func (d *Deployer) removeProjectNetworks(ctx context.Context, projectName string) error {
networks, err := d.cli.NetworkList(ctx, client.NetworkListOptions{
Filters: make(client.Filters).Add("label", labelComposeProject+"="+projectName),
})
if err != nil {
return fmt.Errorf("listing networks: %w", err)
}
g, gctx := errgroup.WithContext(ctx)
g.SetLimit(5)
for _, n := range networks.Items {
g.Go(func() error {
if _, err := d.cli.NetworkRemove(gctx, n.ID, client.NetworkRemoveOptions{}); err != nil {
log.Warn().Err(err).Str("network", n.Name).Msg("Failed to remove network")
return err
}
log.Info().Str("network", n.Name).Msg("Removed network")
return nil
})
}
return g.Wait()
}
func (d *Deployer) removeProjectVolumes(ctx context.Context, projectName string) error {
vols, err := d.cli.VolumeList(ctx, client.VolumeListOptions{
Filters: make(client.Filters).Add("label", labelComposeProject+"="+projectName),
})
if err != nil {
return fmt.Errorf("listing volumes: %w", err)
}
g, gctx := errgroup.WithContext(ctx)
g.SetLimit(5)
for _, v := range vols.Items {
g.Go(func() error {
if _, err := d.cli.VolumeRemove(gctx, v.Name, client.VolumeRemoveOptions{Force: false}); err != nil {
log.Warn().Err(err).Str("volume", v.Name).Msg("Failed to remove volume")
return err
}
log.Info().Str("volume", v.Name).Msg("Removed volume")
return nil
})
}
return g.Wait()
}
func (d *Deployer) removeProjectContainers(ctx context.Context, projectName string, status chan<- StatusUpdate) error {
containers, err := d.cli.ContainerList(ctx, client.ContainerListOptions{
All: true,
Filters: make(client.Filters).Add("label", labelComposeProject+"="+projectName),
})
if err != nil {
return fmt.Errorf("listing containers: %w", err)
}
g, gctx := errgroup.WithContext(ctx)
g.SetLimit(5)
for _, c := range containers.Items {
service := c.Labels[labelComposeService]
g.Go(func() error {
sendStatus(gctx, status, service, "removing")
if _, err := d.cli.ContainerRemove(gctx, c.ID, client.ContainerRemoveOptions{Force: true}); err != nil {
return fmt.Errorf("removing container %s: %w", shortID(c.ID), err)
}
log.Info().Str("service", service).Str("container", shortID(c.ID)).Msg("Removed container")
return nil
})
}
return g.Wait()
}
func (d *Deployer) ensureNetworks(ctx context.Context, project *composetypes.Project) (map[string]string, error) {
ids := make(map[string]string)
// Two listings: project-owned (for idempotent reuse) and external references.
// Project filter keeps the response small on hosts with many stacks.
ownedList, err := d.cli.NetworkList(ctx, client.NetworkListOptions{
Filters: make(client.Filters).Add("label", labelComposeProject+"="+project.Name),
})
if err != nil {
return nil, fmt.Errorf("listing networks: %w", err)
}
ownedByName := make(map[string]string, len(ownedList.Items))
for _, n := range ownedList.Items {
ownedByName[n.Name] = n.ID
}
// External networks are looked up unfiltered, on demand (they are not project-labeled).
var externalByName map[string]string
lookupExternalNet := func() error {
if externalByName != nil {
return nil
}
all, err := d.cli.NetworkList(ctx, client.NetworkListOptions{})
if err != nil {
return fmt.Errorf("listing external networks: %w", err)
}
externalByName = make(map[string]string, len(all.Items))
for _, n := range all.Items {
externalByName[n.Name] = n.ID
}
return nil
}
for name, netCfg := range project.Networks {
if bool(netCfg.External) {
extName := netCfg.Name
if extName == "" {
extName = name
}
if err := lookupExternalNet(); err != nil {
return nil, err
}
id, ok := externalByName[extName]
if !ok {
return nil, fmt.Errorf("external network %q not found", extName)
}
ids[name] = id
continue
}
fullName := netCfg.Name
if fullName == "" {
fullName = project.Name + "_" + name
}
if id, ok := ownedByName[fullName]; ok {
ids[name] = id
log.Info().Str("network", fullName).Msg("Network already exists, skipping")
continue
}
driver := netCfg.Driver
if driver == "" {
driver = "bridge"
}
labels := make(map[string]string)
maps.Copy(labels, netCfg.Labels)
labels[labelComposeProject] = project.Name
labels[labelComposeNetwork] = name
resp, err := d.cli.NetworkCreate(ctx, fullName, client.NetworkCreateOptions{
Driver: driver,
Internal: netCfg.Internal,
Attachable: netCfg.Attachable,
Labels: labels,
Options: netCfg.DriverOpts,
})
if err != nil {
return nil, fmt.Errorf("creating network %q: %w", fullName, err)
}
ids[name] = resp.ID
log.Info().Str("network", fullName).Str("id", shortID(resp.ID)).Msg("Created network")
}
return ids, nil
}
func (d *Deployer) createVolumes(ctx context.Context, project *composetypes.Project) error {
// Project-filtered listing keeps the response small on hosts with many stacks.
ownedList, err := d.cli.VolumeList(ctx, client.VolumeListOptions{
Filters: make(client.Filters).Add("label", labelComposeProject+"="+project.Name),
})
if err != nil {
return fmt.Errorf("listing volumes: %w", err)
}
ownedByName := make(map[string]struct{}, len(ownedList.Items))
for _, v := range ownedList.Items {
ownedByName[v.Name] = struct{}{}
}
// External volumes are looked up unfiltered, on demand (they are not project-labeled).
var externalByName map[string]struct{}
lookupExternal := func() error {
if externalByName != nil {
return nil
}
all, err := d.cli.VolumeList(ctx, client.VolumeListOptions{})
if err != nil {
return fmt.Errorf("listing external volumes: %w", err)
}
externalByName = make(map[string]struct{}, len(all.Items))
for _, v := range all.Items {
externalByName[v.Name] = struct{}{}
}
return nil
}
for name, volCfg := range project.Volumes {
if bool(volCfg.External) {
extName := volCfg.Name
if extName == "" {
extName = name
}
if err := lookupExternal(); err != nil {
return err
}
if _, ok := externalByName[extName]; !ok {
return fmt.Errorf("external volume %q not found", extName)
}
continue
}
fullName := volCfg.Name
if fullName == "" {
fullName = project.Name + "_" + name
}
if _, ok := ownedByName[fullName]; ok {
log.Info().Str("volume", fullName).Msg("Volume already exists, skipping")
continue
}
driver := volCfg.Driver
if driver == "" {
driver = "local"
}
labels := make(map[string]string)
maps.Copy(labels, volCfg.Labels)
labels[labelComposeProject] = project.Name
labels[labelComposeVolume] = name
if _, err := d.cli.VolumeCreate(ctx, client.VolumeCreateOptions{
Name: fullName,
Driver: driver,
DriverOpts: volCfg.DriverOpts,
Labels: labels,
}); err != nil {
return fmt.Errorf("creating volume %q: %w", fullName, err)
}
log.Info().Str("volume", fullName).Msg("Created volume")
}
return nil
}
func (d *Deployer) deployService(ctx context.Context, projectName, name string, svc composetypes.ServiceConfig, networkIDs map[string]string, status chan<- StatusUpdate) error {
if svc.Image == "" {
return fmt.Errorf("service %q has no image (build is not supported)", name)
}
// Pick the primary network (attached at container-create time) deterministically.
// sortedNetworks is alphabetical so create-time and later NetworkConnect agree.
sortedNetworks := sortedNetworkNames(svc.Networks)
primaryNetName := ""
for _, n := range sortedNetworks {
if _, ok := networkIDs[n]; ok {
primaryNetName = n
break
}
}
config, hostConfig, networkingConfig := buildContainerConfig(projectName, name, svc, networkIDs, primaryNetName)
containerName := svc.ContainerName
if containerName == "" {
containerName = projectName + "-" + name + "-1"
}
sendStatus(ctx, status, name, "creating")
resp, err := d.cli.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
Name: containerName,
})
if err != nil {
return fmt.Errorf("creating container %q: %w", containerName, err)
}
log.Info().Str("service", name).Str("container", containerName).Str("id", shortID(resp.ID)).Msg("Created container")
for _, netName := range sortedNetworks {
if netName == primaryNetName {
continue
}
netID, ok := networkIDs[netName]
if !ok {
continue
}
netCfg := svc.Networks[netName]
aliases := copyAliases(netCfg, name)
if _, err := d.cli.NetworkConnect(ctx, netID, client.NetworkConnectOptions{Container: resp.ID, EndpointConfig: &network.EndpointSettings{
Aliases: aliases,
}}); err != nil {
// Partial attachment would leave an orphan. Remove and bail.
if _, rmErr := d.cli.ContainerRemove(ctx, resp.ID, client.ContainerRemoveOptions{Force: true}); rmErr != nil {
log.Warn().Err(rmErr).Str("container", shortID(resp.ID)).Msg("Failed to clean up after NetworkConnect error")
}
return fmt.Errorf("connecting container to network %q: %w", netName, err)
}
}
sendStatus(ctx, status, name, "starting")
if _, err := d.cli.ContainerStart(ctx, resp.ID, client.ContainerStartOptions{}); err != nil {
return fmt.Errorf("starting container %q: %w", containerName, err)
}
log.Info().Str("service", name).Str("container", containerName).Msg("Started container")
return nil
}
// copyAliases returns a fresh aliases slice with the service name appended.
// Important: compose-go's ServiceNetworkConfig.Aliases is shared across
// deploys — appending directly would mutate the project model if cap allows.
func copyAliases(netCfg *composetypes.ServiceNetworkConfig, serviceName string) []string {
var src []string
if netCfg != nil {
src = netCfg.Aliases
}
out := make([]string, 0, len(src)+1)
out = append(out, src...)
out = append(out, serviceName)
return out
}
func sortedNetworkNames(nets map[string]*composetypes.ServiceNetworkConfig) []string {
names := make([]string, 0, len(nets))
for k := range nets {
names = append(names, k)
}
sort.Strings(names)
return names
}
func buildContainerConfig(projectName, name string, svc composetypes.ServiceConfig, networkIDs map[string]string, primaryNetName string) (*container.Config, *container.HostConfig, *network.NetworkingConfig) {
env := make([]string, 0, len(svc.Environment))
for k, v := range svc.Environment {
if v != nil {
env = append(env, k+"="+*v)
} else {
env = append(env, k)
}
}
labels := make(map[string]string)
maps.Copy(labels, svc.Labels)
labels[labelComposeProject] = projectName
labels[labelComposeService] = name
exposedPorts := network.PortSet{}
portBindings := network.PortMap{}
for _, p := range svc.Ports {
proto := p.Protocol
if proto == "" {
proto = "tcp"
}
port, err := network.ParsePort(strconv.FormatUint(uint64(p.Target), 10) + "/" + proto)
if err != nil {
log.Warn().Err(err).Uint32("port", p.Target).Str("protocol", proto).Msg("Skipping invalid port")
continue
}
exposedPorts[port] = struct{}{}
var hostIP netip.Addr
if p.HostIP != "" {
var err error
hostIP, err = netip.ParseAddr(p.HostIP)
if err != nil {
log.Warn().Err(err).Str("hostIP", p.HostIP).Msg("Failed to parse host IP, using zero value")
}
}
portBindings[port] = append(portBindings[port], network.PortBinding{
HostIP: hostIP,
HostPort: p.Published,
})
}
mounts := make([]mount.Mount, 0, len(svc.Volumes))
for _, v := range svc.Volumes {
m := mount.Mount{
Target: v.Target,
ReadOnly: v.ReadOnly,
}
switch v.Type {
case "bind":
m.Type = mount.TypeBind
m.Source = v.Source
case "volume":
m.Type = mount.TypeVolume
m.Source = v.Source
case "tmpfs":
m.Type = mount.TypeTmpfs
default:
m.Type = mount.TypeVolume
m.Source = v.Source
}
mounts = append(mounts, m)
}
tmpfs := make(map[string]string)
for _, t := range svc.Tmpfs {
tmpfs[t] = ""
}
restartPolicy := container.RestartPolicy{}
switch svc.Restart {
case "always":
restartPolicy.Name = container.RestartPolicyAlways
case "on-failure":
restartPolicy.Name = container.RestartPolicyOnFailure
case "unless-stopped":
restartPolicy.Name = container.RestartPolicyUnlessStopped
default:
restartPolicy.Name = container.RestartPolicyDisabled
}
networkMode := container.NetworkMode(svc.NetworkMode)
var networkingConfig *network.NetworkingConfig
if networkMode == "" && primaryNetName != "" {
if netID, ok := networkIDs[primaryNetName]; ok {
netCfg := svc.Networks[primaryNetName]
aliases := copyAliases(netCfg, name)
networkingConfig = &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
netID: {Aliases: aliases},
},
}
}
}
extraHosts := svc.ExtraHosts.AsList(":")
var healthcheck *dockerspec.HealthcheckConfig
if svc.HealthCheck != nil {
if svc.HealthCheck.Disable {
healthcheck = &dockerspec.HealthcheckConfig{
Test: []string{"NONE"},
}
} else {
healthcheck = &dockerspec.HealthcheckConfig{
Test: []string(svc.HealthCheck.Test),
}
if svc.HealthCheck.Interval != nil {
healthcheck.Interval = time.Duration(*svc.HealthCheck.Interval)
}
if svc.HealthCheck.Timeout != nil {
healthcheck.Timeout = time.Duration(*svc.HealthCheck.Timeout)
}
if svc.HealthCheck.StartPeriod != nil {
healthcheck.StartPeriod = time.Duration(*svc.HealthCheck.StartPeriod)
}
if svc.HealthCheck.StartInterval != nil {
healthcheck.StartInterval = time.Duration(*svc.HealthCheck.StartInterval)
}
if svc.HealthCheck.Retries != nil {
healthcheck.Retries = int(*svc.HealthCheck.Retries)
}
}
}
var logConfig container.LogConfig
if svc.Logging != nil && svc.Logging.Driver != "" {
logConfig.Type = svc.Logging.Driver
logConfig.Config = maps.Clone(svc.Logging.Options)
}
devices := make([]container.DeviceMapping, 0, len(svc.Devices))
for _, dev := range svc.Devices {
devices = append(devices, container.DeviceMapping{
PathOnHost: dev.Source,
PathInContainer: dev.Target,
CgroupPermissions: dev.Permissions,
})
}
ulimits := make([]*units.Ulimit, 0, len(svc.Ulimits))
for ulName, ul := range svc.Ulimits {
if ul.Single != 0 {
ulimits = append(ulimits, &units.Ulimit{
Name: ulName,
Hard: int64(ul.Single),
Soft: int64(ul.Single),
})
} else {
ulimits = append(ulimits, &units.Ulimit{
Name: ulName,
Soft: int64(ul.Soft),
Hard: int64(ul.Hard),
})
}
}
config := &container.Config{
Image: svc.Image,
Env: env,
Labels: labels,
ExposedPorts: exposedPorts,
Hostname: svc.Hostname,
WorkingDir: svc.WorkingDir,
User: svc.User,
Tty: svc.Tty,
StdinOnce: svc.StdinOpen,
StopSignal: svc.StopSignal,
Healthcheck: healthcheck,
}
if svc.StopGracePeriod != nil {
timeout := int(time.Duration(*svc.StopGracePeriod).Seconds())
config.StopTimeout = &timeout
}
if len(svc.Command) > 0 {
config.Cmd = []string(svc.Command)
}
if len(svc.Entrypoint) > 0 {
config.Entrypoint = []string(svc.Entrypoint)
}
dns := make([]netip.Addr, 0, len(svc.DNS))
for _, d := range svc.DNS {
addr, err := netip.ParseAddr(d)
if err != nil {
log.Warn().Err(err).Str("dns", d).Msg("Skipping invalid DNS address")
continue
}
dns = append(dns, addr)
}
hostConfig := &container.HostConfig{
PortBindings: portBindings,
RestartPolicy: restartPolicy,
Mounts: mounts,
Privileged: svc.Privileged,
ReadonlyRootfs: svc.ReadOnly,
ExtraHosts: extraHosts,
DNS: dns,
CapAdd: svc.CapAdd,
CapDrop: svc.CapDrop,
NetworkMode: networkMode,
PidMode: container.PidMode(svc.Pid),
IpcMode: container.IpcMode(svc.Ipc),
Tmpfs: tmpfs,
ShmSize: int64(svc.ShmSize),
Init: svc.Init,
SecurityOpt: svc.SecurityOpt,
Sysctls: svc.Sysctls,
LogConfig: logConfig,
Runtime: svc.Runtime,
NanoCPUs: int64(svc.CPUS * 1e9),
Memory: int64(svc.MemLimit),
Ulimits: ulimits,
Devices: devices,
}
return config, hostConfig, networkingConfig
}
-398
View File
@@ -1,398 +0,0 @@
package deploy
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/storer"
"github.com/rs/zerolog/log"
)
const (
composeFilename = "compose.yaml"
// DefaultStacksDir is the default on-disk location for git-backed compose projects.
DefaultStacksDir = "./data/stacks"
commitAuthorName = "Dozzle Deploy"
commitAuthorEmail = "deploy@dozzle.dev"
)
// projectNameRegexp follows the compose project-name rules: lowercase letters,
// digits, dashes, and underscores only. Anything else is rejected to prevent
// path traversal and to stay consistent with compose-go's own validation.
var projectNameRegexp = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`)
func validateProjectName(name string) error {
if name == "" {
return fmt.Errorf("project name is required")
}
if !projectNameRegexp.MatchString(name) {
return fmt.Errorf("invalid project name %q: must match [a-z0-9][a-z0-9_-]*", name)
}
return nil
}
// Version represents a single commit in a project's git history.
type Version struct {
Hash string
Message string
Time time.Time
}
// Manager manages git-backed compose projects under a data directory.
// Each project lives in its own git repository at {dataDir}/{projectName}.
//
// All operations on the same project serialize through a per-project RWMutex:
// concurrent write ops (Deploy/Rollback/Remove) for one project run one at a
// time, while ListVersions can proceed in parallel. Different projects run
// fully in parallel.
type Manager struct {
deployer *Deployer
dataDir string
locks sync.Map // project name -> *sync.RWMutex
}
// NewManager creates a manager that stores projects under dataDir.
func NewManager(cli DockerClient, dataDir string) *Manager {
return &Manager{
deployer: NewDeployer(cli),
dataDir: dataDir,
}
}
func (m *Manager) projectDir(name string) string {
return filepath.Join(m.dataDir, name)
}
func (m *Manager) projectLock(name string) *sync.RWMutex {
actual, _ := m.locks.LoadOrStore(name, &sync.RWMutex{})
return actual.(*sync.RWMutex)
}
// Deploy creates a new git-backed project if it doesn't exist, or updates the
// existing one, then redeploys. Status updates are sent to the optional channel.
func (m *Manager) Deploy(ctx context.Context, name string, composeYAML []byte, status chan<- StatusUpdate) error {
if err := validateProjectName(name); err != nil {
return err
}
mu := m.projectLock(name)
mu.Lock()
defer mu.Unlock()
dir := m.projectDir(name)
_, err := os.Stat(dir)
switch {
case errors.Is(err, os.ErrNotExist):
return m.createLocked(ctx, name, composeYAML, status)
case err != nil:
return fmt.Errorf("stat project %q: %w", name, err)
default:
return m.updateLocked(ctx, name, composeYAML, status)
}
}
func (m *Manager) createLocked(ctx context.Context, name string, composeYAML []byte, status chan<- StatusUpdate) error {
dir := m.projectDir(name)
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("creating project directory: %w", err)
}
cleanup := func(stage string, err error) error {
if rmErr := os.RemoveAll(dir); rmErr != nil {
log.Warn().Err(rmErr).Str("project", name).Msg("Failed to clean up project directory after setup error")
}
return fmt.Errorf("%s: %w", stage, err)
}
repo, err := git.PlainInit(dir, false)
if err != nil {
return cleanup("initializing git repo", err)
}
composePath := filepath.Join(dir, composeFilename)
if err := os.WriteFile(composePath, composeYAML, 0o644); err != nil {
return cleanup("writing compose file", err)
}
if err := commitCompose(repo, "Initial deployment"); err != nil {
return cleanup("committing", err)
}
project, err := ParseCompose(ctx, composeYAML, name)
if err != nil {
return fmt.Errorf("parsing compose file: %w", err)
}
if err := m.deployer.Deploy(ctx, project, status); err != nil {
return fmt.Errorf("deploying: %w", err)
}
log.Info().Str("project", name).Msg("Project created and deployed")
return nil
}
func (m *Manager) updateLocked(ctx context.Context, name string, composeYAML []byte, status chan<- StatusUpdate) error {
dir := m.projectDir(name)
repo, err := git.PlainOpen(dir)
if err != nil {
return fmt.Errorf("opening project %q: %w", name, err)
}
composePath := filepath.Join(dir, composeFilename)
existing, readErr := os.ReadFile(composePath)
if readErr != nil && !errors.Is(readErr, os.ErrNotExist) {
return fmt.Errorf("reading existing compose file: %w", readErr)
}
if !bytes.Equal(existing, composeYAML) {
if err := os.WriteFile(composePath, composeYAML, 0o644); err != nil {
return fmt.Errorf("writing compose file: %w", err)
}
if err := commitCompose(repo, "Update config"); err != nil {
return err
}
}
project, err := ParseCompose(ctx, composeYAML, name)
if err != nil {
return fmt.Errorf("parsing compose file: %w", err)
}
if err := m.deployer.Deploy(ctx, project, status); err != nil {
return fmt.Errorf("deploying: %w", err)
}
log.Info().Str("project", name).Msg("Config updated and deployed")
return nil
}
// Remove tears down a deployed project: stops and removes all containers,
// removes project-labeled networks, and deletes the on-disk project directory
// (including git history). If removeVolumes is true, project-labeled named
// volumes are also deleted — this is destructive and should be opt-in.
func (m *Manager) Remove(ctx context.Context, name string, removeVolumes bool, status chan<- StatusUpdate) error {
if err := validateProjectName(name); err != nil {
return err
}
mu := m.projectLock(name)
mu.Lock()
defer mu.Unlock()
// Drop the lock entry on successful removal so a churn of ephemeral project
// names doesn't leak a per-project RWMutex forever.
defer m.locks.Delete(name)
dir := m.projectDir(name)
if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("project %q does not exist", name)
} else if err != nil {
return fmt.Errorf("stat project %q: %w", name, err)
}
if err := m.deployer.Remove(ctx, name, removeVolumes, status); err != nil {
return fmt.Errorf("removing project: %w", err)
}
if err := os.RemoveAll(dir); err != nil {
return fmt.Errorf("removing project directory: %w", err)
}
log.Info().Str("project", name).Bool("removed_volumes", removeVolumes).Msg("Project removed")
return nil
}
// ListVersions returns the git commit history for a project, newest first.
func (m *Manager) ListVersions(name string) ([]Version, error) {
if err := validateProjectName(name); err != nil {
return nil, err
}
mu := m.projectLock(name)
mu.RLock()
defer mu.RUnlock()
dir := m.projectDir(name)
repo, err := git.PlainOpen(dir)
if err != nil {
return nil, fmt.Errorf("opening project %q: %w", name, err)
}
logIter, err := repo.Log(&git.LogOptions{})
if err != nil {
return nil, fmt.Errorf("reading log: %w", err)
}
versions := make([]Version, 0, 32)
err = logIter.ForEach(func(c *object.Commit) error {
versions = append(versions, Version{
Hash: c.Hash.String(),
Message: strings.TrimSpace(c.Message),
Time: c.Author.When,
})
return nil
})
if err != nil {
return nil, fmt.Errorf("iterating log: %w", err)
}
return versions, nil
}
// RollbackVersion restores the compose file from a previous commit, creates
// a new rollback commit, and redeploys. Supports both full and short commit hashes.
func (m *Manager) RollbackVersion(ctx context.Context, name string, commitHash string, status chan<- StatusUpdate) error {
if err := validateProjectName(name); err != nil {
return err
}
if commitHash == "" {
return fmt.Errorf("commit_hash is required")
}
mu := m.projectLock(name)
mu.Lock()
defer mu.Unlock()
dir := m.projectDir(name)
repo, err := git.PlainOpen(dir)
if err != nil {
return fmt.Errorf("opening project %q: %w", name, err)
}
commitObj, err := resolveCommit(repo, commitHash)
if err != nil {
return fmt.Errorf("resolving commit: %w", err)
}
tree, err := commitObj.Tree()
if err != nil {
return fmt.Errorf("reading tree: %w", err)
}
file, err := tree.File(composeFilename)
if err != nil {
return fmt.Errorf("reading compose file from commit: %w", err)
}
reader, err := file.Reader()
if err != nil {
return fmt.Errorf("opening file reader: %w", err)
}
composeYAML, err := readAllAndClose(reader)
if err != nil {
return fmt.Errorf("reading file contents: %w", err)
}
composePath := filepath.Join(dir, composeFilename)
existing, readErr := os.ReadFile(composePath)
if readErr != nil && !errors.Is(readErr, os.ErrNotExist) {
return fmt.Errorf("reading existing compose file: %w", readErr)
}
shortHash := shortID(commitObj.Hash.String())
if !bytes.Equal(existing, composeYAML) {
if err := os.WriteFile(composePath, composeYAML, 0o644); err != nil {
return fmt.Errorf("writing compose file: %w", err)
}
if err := commitCompose(repo, fmt.Sprintf("Rollback to %s", shortHash)); err != nil {
return err
}
}
project, err := ParseCompose(ctx, composeYAML, name)
if err != nil {
return fmt.Errorf("parsing compose file: %w", err)
}
if err := m.deployer.Deploy(ctx, project, status); err != nil {
return fmt.Errorf("deploying: %w", err)
}
log.Info().Str("project", name).Str("rollback_to", shortHash).Msg("Rolled back and deployed")
return nil
}
// commitCompose stages compose.yaml and commits with the given message. It
// assumes the caller holds the per-project write lock. No-op when nothing is
// staged (caller is expected to skip entirely in that case, but this is
// defensive in case staging detects no diff).
func commitCompose(repo *git.Repository, message string) error {
wt, err := repo.Worktree()
if err != nil {
return fmt.Errorf("getting worktree: %w", err)
}
if _, err := wt.Add(composeFilename); err != nil {
return fmt.Errorf("staging compose file: %w", err)
}
_, err = wt.Commit(message, &git.CommitOptions{
Author: &object.Signature{
Name: commitAuthorName,
Email: commitAuthorEmail,
When: time.Now(),
},
})
if err != nil {
return fmt.Errorf("committing: %w", err)
}
return nil
}
// readAllAndClose reads the reader to completion and closes it.
func readAllAndClose(r interface {
Read([]byte) (int, error)
Close() error
}) ([]byte, error) {
var buf bytes.Buffer
_, err := buf.ReadFrom(r)
closeErr := r.Close()
if err != nil {
return nil, err
}
if closeErr != nil {
return nil, closeErr
}
return buf.Bytes(), nil
}
// resolveCommit finds a commit by full or short hash prefix.
func resolveCommit(repo *git.Repository, hash string) (*object.Commit, error) {
// Full-hash fast path: skip the log walk entirely.
if len(hash) == 40 {
c, err := repo.CommitObject(plumbing.NewHash(hash))
if err == nil {
return c, nil
}
// fall through to the prefix walk in case the caller passed a 40-char
// string that doesn't resolve (unlikely but not impossible).
}
iter, err := repo.Log(&git.LogOptions{})
if err != nil {
return nil, err
}
var match *object.Commit
err = iter.ForEach(func(c *object.Commit) error {
if strings.HasPrefix(c.Hash.String(), hash) {
if match != nil {
return fmt.Errorf("ambiguous commit prefix %q", hash)
}
match = c
// Keep walking to detect ambiguity.
}
return nil
})
if err != nil && !errors.Is(err, storer.ErrStop) {
return nil, err
}
if match == nil {
return nil, fmt.Errorf("commit %q not found", hash)
}
return match, nil
}
-478
View File
@@ -1,478 +0,0 @@
package deploy
import (
"bytes"
"context"
"errors"
"io"
"iter"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/moby/moby/api/types/jsonstream"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testCompose = `
services:
web:
image: nginx:latest
`
const testComposeUpdated = `
services:
web:
image: nginx:1.27
`
const testComposeV3 = `
services:
web:
image: nginx:1.28
`
// fakeDockerClient is a no-op DockerClient for tests. Hooks let tests inject
// delays or errors into specific calls.
type fakeDockerClient struct {
mu sync.Mutex
onContainerList func(ctx context.Context) error
// networksOnList / volumesOnList are returned by NetworkList / VolumeList
// respectively. Tests set these to simulate existing resources.
networksOnList []network.Summary
volumesOnList []volume.Volume
// pullBody is returned by ImagePull so tests can exercise pull-stream
// error handling. Empty body is a successful no-op.
pullBody []byte
pullCalls atomic.Int64
startCalls atomic.Int64
networkRemoveCalls atomic.Int64
volumeRemoveCalls atomic.Int64
volumeCreateCalls atomic.Int64
}
type fakeImagePullResponse struct {
io.ReadCloser
}
func (r fakeImagePullResponse) JSONMessages(_ context.Context) iter.Seq2[jsonstream.Message, error] {
return func(func(jsonstream.Message, error) bool) {}
}
func (r fakeImagePullResponse) Wait(_ context.Context) error {
_, err := io.Copy(io.Discard, r.ReadCloser)
return err
}
func (f *fakeDockerClient) ImagePull(_ context.Context, _ string, _ client.ImagePullOptions) (client.ImagePullResponse, error) {
f.pullCalls.Add(1)
f.mu.Lock()
body := f.pullBody
f.mu.Unlock()
return fakeImagePullResponse{ReadCloser: io.NopCloser(bytes.NewReader(body))}, nil
}
func (f *fakeDockerClient) ContainerCreate(_ context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
return client.ContainerCreateResult{ID: "container-id-for-" + options.Name + "-1234567890ab"}, nil
}
func (f *fakeDockerClient) ContainerStart(_ context.Context, _ string, _ client.ContainerStartOptions) (client.ContainerStartResult, error) {
f.startCalls.Add(1)
return client.ContainerStartResult{}, nil
}
func (f *fakeDockerClient) ContainerList(ctx context.Context, _ client.ContainerListOptions) (client.ContainerListResult, error) {
f.mu.Lock()
hook := f.onContainerList
f.mu.Unlock()
if hook != nil {
if err := hook(ctx); err != nil {
return client.ContainerListResult{}, err
}
}
return client.ContainerListResult{}, nil
}
func (f *fakeDockerClient) ContainerStop(_ context.Context, _ string, _ client.ContainerStopOptions) (client.ContainerStopResult, error) {
return client.ContainerStopResult{}, nil
}
func (f *fakeDockerClient) ContainerRemove(_ context.Context, _ string, _ client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
return client.ContainerRemoveResult{}, nil
}
func (f *fakeDockerClient) NetworkList(_ context.Context, opts client.NetworkListOptions) (client.NetworkListResult, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []network.Summary
for _, n := range f.networksOnList {
if matchesLabelFilter(filterValues(opts.Filters, "label"), n.Labels) {
out = append(out, n)
}
}
return client.NetworkListResult{Items: out}, nil
}
func (f *fakeDockerClient) NetworkCreate(_ context.Context, _ string, _ client.NetworkCreateOptions) (client.NetworkCreateResult, error) {
return client.NetworkCreateResult{ID: "net-1234567890ab"}, nil
}
func (f *fakeDockerClient) NetworkConnect(_ context.Context, _ string, _ client.NetworkConnectOptions) (client.NetworkConnectResult, error) {
return client.NetworkConnectResult{}, nil
}
func (f *fakeDockerClient) NetworkRemove(_ context.Context, _ string, _ client.NetworkRemoveOptions) (client.NetworkRemoveResult, error) {
f.networkRemoveCalls.Add(1)
return client.NetworkRemoveResult{}, nil
}
func (f *fakeDockerClient) VolumeCreate(_ context.Context, _ client.VolumeCreateOptions) (client.VolumeCreateResult, error) {
f.volumeCreateCalls.Add(1)
return client.VolumeCreateResult{Volume: volume.Volume{}}, nil
}
func (f *fakeDockerClient) VolumeList(_ context.Context, opts client.VolumeListOptions) (client.VolumeListResult, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []volume.Volume
for _, v := range f.volumesOnList {
if matchesLabelFilter(filterValues(opts.Filters, "label"), v.Labels) {
out = append(out, v)
}
}
return client.VolumeListResult{Items: out}, nil
}
// matchesLabelFilter mimics Docker's label filter: every requested "k=v"
// must exist in the resource's labels.
func matchesLabelFilter(wanted []string, labels map[string]string) bool {
for _, w := range wanted {
k, v, ok := strings.Cut(w, "=")
if !ok {
if _, present := labels[w]; !present {
return false
}
continue
}
if labels[k] != v {
return false
}
}
return true
}
func filterValues(filters client.Filters, key string) []string {
values, ok := filters[key]
if !ok {
return nil
}
out := make([]string, 0, len(values))
for v := range values {
out = append(out, v)
}
return out
}
func (f *fakeDockerClient) VolumeRemove(_ context.Context, _ string, _ client.VolumeRemoveOptions) (client.VolumeRemoveResult, error) {
f.volumeRemoveCalls.Add(1)
return client.VolumeRemoveResult{}, nil
}
func TestManager_Remove_DeletesProjectAndCleansNetworks(t *testing.T) {
dir := t.TempDir()
cli := &fakeDockerClient{
networksOnList: []network.Summary{
{ID: "net-1", Name: "myapp_default", Labels: map[string]string{"com.docker.compose.project": "myapp"}},
},
volumesOnList: []volume.Volume{
{Name: "myapp_data", Labels: map[string]string{"com.docker.compose.project": "myapp"}},
},
}
mgr := NewManager(cli, dir)
ctx := context.Background()
require.NoError(t, mgr.Deploy(ctx, "myapp", []byte(testCompose), nil))
require.DirExists(t, filepath.Join(dir, "myapp"))
require.NoError(t, mgr.Remove(ctx, "myapp", false, nil))
assert.NoDirExists(t, filepath.Join(dir, "myapp"))
assert.Equal(t, int64(1), cli.networkRemoveCalls.Load(), "project-labeled network should be removed")
assert.Equal(t, int64(0), cli.volumeRemoveCalls.Load(), "volumes must be preserved by default")
_, err := mgr.ListVersions("myapp")
assert.Error(t, err, "ListVersions on a removed project should error")
}
func TestManager_Remove_WithVolumes(t *testing.T) {
dir := t.TempDir()
cli := &fakeDockerClient{
volumesOnList: []volume.Volume{
{Name: "myapp_data", Labels: map[string]string{"com.docker.compose.project": "myapp"}},
{Name: "other_project_data", Labels: map[string]string{"com.docker.compose.project": "other"}},
},
}
mgr := NewManager(cli, dir)
ctx := context.Background()
require.NoError(t, mgr.Deploy(ctx, "myapp", []byte(testCompose), nil))
require.NoError(t, mgr.Remove(ctx, "myapp", true, nil))
assert.Equal(t, int64(1), cli.volumeRemoveCalls.Load(), "only myapp's volume should be removed")
}
func TestManager_Remove_NonexistentProject(t *testing.T) {
dir := t.TempDir()
mgr := NewManager(&fakeDockerClient{}, dir)
err := mgr.Remove(context.Background(), "ghost", false, nil)
require.Error(t, err)
}
func TestManager_Deploy_CreatesNewProject(t *testing.T) {
dir := t.TempDir()
mgr := NewManager(&fakeDockerClient{}, dir)
err := mgr.Deploy(context.Background(), "myapp", []byte(testCompose), nil)
require.NoError(t, err)
versions, err := mgr.ListVersions("myapp")
require.NoError(t, err)
require.Len(t, versions, 1)
assert.Equal(t, "Initial deployment", versions[0].Message)
assert.FileExists(t, filepath.Join(dir, "myapp", composeFilename))
}
func TestManager_Deploy_UpdatesExistingProject(t *testing.T) {
dir := t.TempDir()
mgr := NewManager(&fakeDockerClient{}, dir)
ctx := context.Background()
require.NoError(t, mgr.Deploy(ctx, "myapp", []byte(testCompose), nil))
require.NoError(t, mgr.Deploy(ctx, "myapp", []byte(testComposeUpdated), nil))
versions, err := mgr.ListVersions("myapp")
require.NoError(t, err)
require.Len(t, versions, 2)
assert.Equal(t, "Update config", versions[0].Message)
assert.Equal(t, "Initial deployment", versions[1].Message)
}
func TestManager_Deploy_PropagatesUpdateError(t *testing.T) {
dir := t.TempDir()
wantErr := errors.New("docker daemon is unreachable")
cli := &fakeDockerClient{onContainerList: func(context.Context) error { return wantErr }}
mgr := NewManager(cli, dir)
// Pre-create the project so Deploy takes the update branch.
cli.mu.Lock()
cli.onContainerList = nil
cli.mu.Unlock()
require.NoError(t, mgr.Deploy(context.Background(), "myapp", []byte(testCompose), nil))
cli.mu.Lock()
cli.onContainerList = func(context.Context) error { return wantErr }
cli.mu.Unlock()
err := mgr.Deploy(context.Background(), "myapp", []byte(testComposeUpdated), nil)
require.Error(t, err)
assert.ErrorIs(t, err, wantErr, "update error must propagate verbatim, not be masked by a fallback create")
}
func TestManager_Deploy_SerializesSameProject(t *testing.T) {
dir := t.TempDir()
gate := make(chan struct{})
var active, maxActive atomic.Int32
cli := &fakeDockerClient{}
cli.onContainerList = func(ctx context.Context) error {
n := active.Add(1)
defer active.Add(-1)
for {
if m := maxActive.Load(); n > m {
if maxActive.CompareAndSwap(m, n) {
break
}
continue
}
break
}
select {
case <-gate:
case <-ctx.Done():
return ctx.Err()
}
return nil
}
mgr := NewManager(cli, dir)
var wg sync.WaitGroup
wg.Add(2)
errs := make(chan error, 2)
for range 2 {
go func() {
defer wg.Done()
errs <- mgr.Deploy(context.Background(), "shared", []byte(testCompose), nil)
}()
}
// Let the first goroutine enter the docker call, then confirm the second is blocked.
time.Sleep(100 * time.Millisecond)
assert.Equal(t, int32(1), active.Load(), "second Deploy on same project must wait for the lock")
close(gate)
wg.Wait()
close(errs)
for err := range errs {
require.NoError(t, err)
}
assert.Equal(t, int32(1), maxActive.Load(), "same-project Deploys must not overlap")
}
func TestManager_Deploy_ParallelizesDifferentProjects(t *testing.T) {
dir := t.TempDir()
gate := make(chan struct{})
reached := make(chan struct{}, 2)
cli := &fakeDockerClient{}
cli.onContainerList = func(ctx context.Context) error {
reached <- struct{}{}
select {
case <-gate:
case <-ctx.Done():
return ctx.Err()
}
return nil
}
mgr := NewManager(cli, dir)
var wg sync.WaitGroup
wg.Add(2)
errs := make(chan error, 2)
go func() {
defer wg.Done()
errs <- mgr.Deploy(context.Background(), "alpha", []byte(testCompose), nil)
}()
go func() {
defer wg.Done()
errs <- mgr.Deploy(context.Background(), "beta", []byte(testCompose), nil)
}()
// Both must reach the docker call before either is released.
for range 2 {
select {
case <-reached:
case <-time.After(2 * time.Second):
t.Fatal("different-project Deploys serialized; expected parallelism")
}
}
close(gate)
wg.Wait()
close(errs)
for err := range errs {
require.NoError(t, err)
}
}
func TestManager_RejectsUnsafeProjectNames(t *testing.T) {
dir := t.TempDir()
mgr := NewManager(&fakeDockerClient{}, dir)
ctx := context.Background()
cases := []string{"../etc", "foo/bar", "..", "", "Foo", "foo bar"}
for _, name := range cases {
err := mgr.Deploy(ctx, name, []byte(testCompose), nil)
assert.Error(t, err, "expected Deploy to reject unsafe project name %q", name)
}
}
func TestManager_PullError_Surfaces(t *testing.T) {
dir := t.TempDir()
cli := &fakeDockerClient{
// Simulate Docker pull stream emitting an error JSON object.
pullBody: []byte(`{"errorDetail":{"message":"pull access denied"},"error":"pull access denied"}`),
}
mgr := NewManager(cli, dir)
err := mgr.Deploy(context.Background(), "myapp", []byte(testCompose), nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "pull access denied")
}
func TestManager_CreateVolumes_Idempotent(t *testing.T) {
const compose = `
services:
web:
image: nginx
volumes:
- data:/data
volumes:
data:
`
dir := t.TempDir()
cli := &fakeDockerClient{}
mgr := NewManager(cli, dir)
ctx := context.Background()
// First deploy creates the volume.
require.NoError(t, mgr.Deploy(ctx, "myapp", []byte(compose), nil))
firstCreates := cli.volumeCreateCalls.Load()
// Pretend the volume now exists so the second deploy must skip it.
cli.mu.Lock()
cli.volumesOnList = []volume.Volume{{Name: "myapp_data", Labels: map[string]string{"com.docker.compose.project": "myapp"}}}
cli.mu.Unlock()
require.NoError(t, mgr.Deploy(ctx, "myapp", []byte(compose), nil))
assert.Equal(t, firstCreates, cli.volumeCreateCalls.Load(), "existing volume must not be re-created")
}
func TestManager_RollbackVersion_SerializesWithDeploy(t *testing.T) {
dir := t.TempDir()
mgr := NewManager(&fakeDockerClient{}, dir)
ctx := context.Background()
require.NoError(t, mgr.Deploy(ctx, "myapp", []byte(testCompose), nil))
require.NoError(t, mgr.Deploy(ctx, "myapp", []byte(testComposeUpdated), nil))
versions, err := mgr.ListVersions("myapp")
require.NoError(t, err)
require.Len(t, versions, 2)
firstHash := versions[1].Hash
// Kick off concurrent rollback + deploy. They must serialize — no git corruption, both succeed.
var wg sync.WaitGroup
wg.Add(2)
errs := make(chan error, 2)
go func() {
defer wg.Done()
errs <- mgr.RollbackVersion(ctx, "myapp", firstHash, nil)
}()
go func() {
defer wg.Done()
errs <- mgr.Deploy(ctx, "myapp", []byte(testComposeV3), nil)
}()
wg.Wait()
close(errs)
for err := range errs {
require.NoError(t, err)
}
versions, err = mgr.ListVersions("myapp")
require.NoError(t, err)
assert.Len(t, versions, 4, "two concurrent writes should each land one commit")
}
-10
View File
@@ -502,16 +502,6 @@ func (d *DockerClient) Host() container.Host {
return d.host
}
// RawClient returns the underlying *client.Client if the DockerCLI is one.
// Needed for operations like network/volume management that aren't part of
// the DockerCLI interface.
func (d *DockerClient) RawClient() *client.Client {
if c, ok := d.cli.(*client.Client); ok {
return c
}
return nil
}
func (d *DockerClient) ContainerAttach(ctx context.Context, id string) (*container.ExecSession, error) {
log.Debug().Str("id", id).Str("host", d.host.Name).Msg("Attaching to container")
options := client.ContainerAttachOptions{
-1
View File
@@ -45,7 +45,6 @@ type Args struct {
Generate *GenerateCmd `arg:"subcommand:generate" help:"generates a configuration file for simple auth"`
Agent *AgentCmd `arg:"subcommand:agent" help:"starts the agent"`
AgentTest *AgentTestCmd `arg:"subcommand:agent-test" help:"tests an agent"`
Deploy *DeployCmd `arg:"subcommand:deploy" help:"deploys a compose file"`
}
type Runnable interface {
-48
View File
@@ -1,48 +0,0 @@
package cli
import (
"context"
"embed"
"fmt"
"os"
"time"
"github.com/amir20/dozzle/internal/deploy"
"github.com/amir20/dozzle/internal/docker"
"github.com/rs/zerolog/log"
)
type DeployCmd struct {
File string `arg:"positional,required" help:"path to compose file"`
Project string `arg:"-p,--project" default:"dozzle" help:"project name for resource prefixes"`
}
func (dc *DeployCmd) Run(args Args, _ embed.FS) error {
data, err := os.ReadFile(dc.File)
if err != nil {
return fmt.Errorf("reading compose file: %w", err)
}
log.Info().Str("project", dc.Project).Str("file", dc.File).Msg("Deploying compose file")
localClient, err := docker.NewLocalClient("")
if err != nil {
return fmt.Errorf("creating docker client: %w", err)
}
raw := localClient.RawClient()
if raw == nil {
return fmt.Errorf("local Docker client is missing a raw handle")
}
timeout := max(args.Timeout, 10*time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
mgr := deploy.NewManager(raw, deploy.DefaultStacksDir)
if err := mgr.Deploy(ctx, dc.Project, data, nil); err != nil {
return fmt.Errorf("deploying: %w", err)
}
log.Info().Str("project", dc.Project).Msg("Deployment complete")
return nil
}
+61 -146
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.35.1
// protoc v7.36.1
// source: cloud.proto
package cloud
@@ -30,7 +30,7 @@ const (
// Applies to the whole Dozzle instance (list_hosts, notifications, etc.).
// Requires instance_id in the args.
ToolScope_TOOL_SCOPE_INSTANCE ToolScope = 1
// Targets a specific Docker host (deploy_compose, list_deploy_versions).
// Targets a specific Docker host.
// Requires host_id in the args.
ToolScope_TOOL_SCOPE_HOST ToolScope = 2
// Targets a specific container (logs, actions, inspect). Requires
@@ -922,7 +922,6 @@ type CallToolResponse struct {
// *CallToolResponse_Action
// *CallToolResponse_FetchLogs
// *CallToolResponse_InspectContainer
// *CallToolResponse_Deploy
// *CallToolResponse_Notification
Result isCallToolResponse_Result `protobuf_oneof:"result"`
unknownFields protoimpl.UnknownFields
@@ -1048,15 +1047,6 @@ func (x *CallToolResponse) GetInspectContainer() *InspectContainerResult {
return nil
}
func (x *CallToolResponse) GetDeploy() *DeployResult {
if x != nil {
if x, ok := x.Result.(*CallToolResponse_Deploy); ok {
return x.Deploy
}
}
return nil
}
func (x *CallToolResponse) GetNotification() *NotificationResult {
if x != nil {
if x, ok := x.Result.(*CallToolResponse_Notification); ok {
@@ -1094,10 +1084,6 @@ type CallToolResponse_InspectContainer struct {
InspectContainer *InspectContainerResult `protobuf:"bytes,8,opt,name=inspect_container,json=inspectContainer,proto3,oneof"`
}
type CallToolResponse_Deploy struct {
Deploy *DeployResult `protobuf:"bytes,11,opt,name=deploy,proto3,oneof"`
}
type CallToolResponse_Notification struct {
Notification *NotificationResult `protobuf:"bytes,12,opt,name=notification,proto3,oneof"`
}
@@ -1114,8 +1100,6 @@ func (*CallToolResponse_FetchLogs) isCallToolResponse_Result() {}
func (*CallToolResponse_InspectContainer) isCallToolResponse_Result() {}
func (*CallToolResponse_Deploy) isCallToolResponse_Result() {}
func (*CallToolResponse_Notification) isCallToolResponse_Result() {}
type CancelStreamRequest struct {
@@ -2040,67 +2024,6 @@ func (x *ActionResult) GetMessage() string {
return ""
}
// Deploy compose result
type DeployResult struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
Project string `protobuf:"bytes,2,opt,name=project,proto3" json:"project,omitempty"`
Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeployResult) Reset() {
*x = DeployResult{}
mi := &file_cloud_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeployResult) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeployResult) ProtoMessage() {}
func (x *DeployResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeployResult.ProtoReflect.Descriptor instead.
func (*DeployResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{22}
}
func (x *DeployResult) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
func (x *DeployResult) GetProject() string {
if x != nil {
return x.Project
}
return ""
}
func (x *DeployResult) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
// Notification/alert tool result (list, create).
type NotificationResult struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -2112,7 +2035,7 @@ type NotificationResult struct {
func (x *NotificationResult) Reset() {
*x = NotificationResult{}
mi := &file_cloud_proto_msgTypes[23]
mi := &file_cloud_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2124,7 +2047,7 @@ func (x *NotificationResult) String() string {
func (*NotificationResult) ProtoMessage() {}
func (x *NotificationResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[23]
mi := &file_cloud_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2137,7 +2060,7 @@ func (x *NotificationResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use NotificationResult.ProtoReflect.Descriptor instead.
func (*NotificationResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{23}
return file_cloud_proto_rawDescGZIP(), []int{22}
}
func (x *NotificationResult) GetSuccess() bool {
@@ -2176,7 +2099,7 @@ type SearchLogsRequest struct {
func (x *SearchLogsRequest) Reset() {
*x = SearchLogsRequest{}
mi := &file_cloud_proto_msgTypes[24]
mi := &file_cloud_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2188,7 +2111,7 @@ func (x *SearchLogsRequest) String() string {
func (*SearchLogsRequest) ProtoMessage() {}
func (x *SearchLogsRequest) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[24]
mi := &file_cloud_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2201,7 +2124,7 @@ func (x *SearchLogsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SearchLogsRequest.ProtoReflect.Descriptor instead.
func (*SearchLogsRequest) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{24}
return file_cloud_proto_rawDescGZIP(), []int{23}
}
func (x *SearchLogsRequest) GetQuery() string {
@@ -2251,7 +2174,7 @@ type SearchLogsResponse struct {
func (x *SearchLogsResponse) Reset() {
*x = SearchLogsResponse{}
mi := &file_cloud_proto_msgTypes[25]
mi := &file_cloud_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2263,7 +2186,7 @@ func (x *SearchLogsResponse) String() string {
func (*SearchLogsResponse) ProtoMessage() {}
func (x *SearchLogsResponse) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[25]
mi := &file_cloud_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2276,7 +2199,7 @@ func (x *SearchLogsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SearchLogsResponse.ProtoReflect.Descriptor instead.
func (*SearchLogsResponse) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{25}
return file_cloud_proto_rawDescGZIP(), []int{24}
}
func (x *SearchLogsResponse) GetHits() []*SearchLogHit {
@@ -2320,7 +2243,7 @@ type SearchLogHit struct {
func (x *SearchLogHit) Reset() {
*x = SearchLogHit{}
mi := &file_cloud_proto_msgTypes[26]
mi := &file_cloud_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2332,7 +2255,7 @@ func (x *SearchLogHit) String() string {
func (*SearchLogHit) ProtoMessage() {}
func (x *SearchLogHit) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[26]
mi := &file_cloud_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2345,7 +2268,7 @@ func (x *SearchLogHit) ProtoReflect() protoreflect.Message {
// Deprecated: Use SearchLogHit.ProtoReflect.Descriptor instead.
func (*SearchLogHit) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{26}
return file_cloud_proto_rawDescGZIP(), []int{25}
}
func (x *SearchLogHit) GetTimestampNs() int64 {
@@ -2435,7 +2358,7 @@ type GetAlertsRequest struct {
func (x *GetAlertsRequest) Reset() {
*x = GetAlertsRequest{}
mi := &file_cloud_proto_msgTypes[27]
mi := &file_cloud_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2447,7 +2370,7 @@ func (x *GetAlertsRequest) String() string {
func (*GetAlertsRequest) ProtoMessage() {}
func (x *GetAlertsRequest) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[27]
mi := &file_cloud_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2460,7 +2383,7 @@ func (x *GetAlertsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAlertsRequest.ProtoReflect.Descriptor instead.
func (*GetAlertsRequest) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{27}
return file_cloud_proto_rawDescGZIP(), []int{26}
}
func (x *GetAlertsRequest) GetContainerIds() []string {
@@ -2568,7 +2491,7 @@ type AlertHit struct {
func (x *AlertHit) Reset() {
*x = AlertHit{}
mi := &file_cloud_proto_msgTypes[28]
mi := &file_cloud_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2580,7 +2503,7 @@ func (x *AlertHit) String() string {
func (*AlertHit) ProtoMessage() {}
func (x *AlertHit) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[28]
mi := &file_cloud_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2593,7 +2516,7 @@ func (x *AlertHit) ProtoReflect() protoreflect.Message {
// Deprecated: Use AlertHit.ProtoReflect.Descriptor instead.
func (*AlertHit) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{28}
return file_cloud_proto_rawDescGZIP(), []int{27}
}
func (x *AlertHit) GetAlertId() string {
@@ -2748,7 +2671,7 @@ type EventHit struct {
func (x *EventHit) Reset() {
*x = EventHit{}
mi := &file_cloud_proto_msgTypes[29]
mi := &file_cloud_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2760,7 +2683,7 @@ func (x *EventHit) String() string {
func (*EventHit) ProtoMessage() {}
func (x *EventHit) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[29]
mi := &file_cloud_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2773,7 +2696,7 @@ func (x *EventHit) ProtoReflect() protoreflect.Message {
// Deprecated: Use EventHit.ProtoReflect.Descriptor instead.
func (*EventHit) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{29}
return file_cloud_proto_rawDescGZIP(), []int{28}
}
func (x *EventHit) GetTsNs() int64 {
@@ -2861,7 +2784,7 @@ type GetAlertsResponse struct {
func (x *GetAlertsResponse) Reset() {
*x = GetAlertsResponse{}
mi := &file_cloud_proto_msgTypes[30]
mi := &file_cloud_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2873,7 +2796,7 @@ func (x *GetAlertsResponse) String() string {
func (*GetAlertsResponse) ProtoMessage() {}
func (x *GetAlertsResponse) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[30]
mi := &file_cloud_proto_msgTypes[29]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2886,7 +2809,7 @@ func (x *GetAlertsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAlertsResponse.ProtoReflect.Descriptor instead.
func (*GetAlertsResponse) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{30}
return file_cloud_proto_rawDescGZIP(), []int{29}
}
func (x *GetAlertsResponse) GetHits() []*AlertHit {
@@ -2976,7 +2899,7 @@ const file_cloud_proto_rawDesc = "" +
"\tread_only\x18\x05 \x01(\bR\breadOnly\"L\n" +
"\x0fCallToolRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12%\n" +
"\x0earguments_json\x18\x02 \x01(\tR\rargumentsJson\"\xf2\x04\n" +
"\x0earguments_json\x18\x02 \x01(\tR\rargumentsJson\"\xd1\x04\n" +
"\x10CallToolResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x14\n" +
"\x05error\x18\x02 \x01(\tR\x05error\x12\x16\n" +
@@ -2991,10 +2914,9 @@ const file_cloud_proto_rawDesc = "" +
"\x06action\x18\x06 \x01(\v2\x13.cloud.ActionResultH\x00R\x06action\x127\n" +
"\n" +
"fetch_logs\x18\a \x01(\v2\x16.cloud.FetchLogsResultH\x00R\tfetchLogs\x12L\n" +
"\x11inspect_container\x18\b \x01(\v2\x1d.cloud.InspectContainerResultH\x00R\x10inspectContainer\x12-\n" +
"\x06deploy\x18\v \x01(\v2\x13.cloud.DeployResultH\x00R\x06deploy\x12?\n" +
"\x11inspect_container\x18\b \x01(\v2\x1d.cloud.InspectContainerResultH\x00R\x10inspectContainer\x12?\n" +
"\fnotification\x18\f \x01(\v2\x19.cloud.NotificationResultH\x00R\fnotificationB\b\n" +
"\x06result\"A\n" +
"\x06resultJ\x04\b\v\x10\fR\x06deploy\"A\n" +
"\x13CancelStreamRequest\x12*\n" +
"\x11stream_request_id\x18\x01 \x01(\tR\x0fstreamRequestId\"\xde\x01\n" +
"\bHostInfo\x12\x0e\n" +
@@ -3084,11 +3006,7 @@ const file_cloud_proto_rawDesc = "" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12!\n" +
"\fcontainer_id\x18\x02 \x01(\tR\vcontainerId\x12\x16\n" +
"\x06action\x18\x03 \x01(\tR\x06action\x12\x18\n" +
"\amessage\x18\x04 \x01(\tR\amessage\"\\\n" +
"\fDeployResult\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" +
"\aproject\x18\x02 \x01(\tR\aproject\x12\x18\n" +
"\amessage\x18\x03 \x01(\tR\amessage\"H\n" +
"\amessage\x18\x04 \x01(\tR\amessage\"H\n" +
"\x12NotificationResult\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\"\x9d\x01\n" +
@@ -3185,7 +3103,7 @@ func file_cloud_proto_rawDescGZIP() []byte {
}
var file_cloud_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_cloud_proto_msgTypes = make([]protoimpl.MessageInfo, 32)
var file_cloud_proto_msgTypes = make([]protoimpl.MessageInfo, 31)
var file_cloud_proto_goTypes = []any{
(ToolScope)(0), // 0: cloud.ToolScope
(*ToolRequest)(nil), // 1: cloud.ToolRequest
@@ -3210,16 +3128,15 @@ var file_cloud_proto_goTypes = []any{
(*FetchLogsResult)(nil), // 20: cloud.FetchLogsResult
(*InspectContainerResult)(nil), // 21: cloud.InspectContainerResult
(*ActionResult)(nil), // 22: cloud.ActionResult
(*DeployResult)(nil), // 23: cloud.DeployResult
(*NotificationResult)(nil), // 24: cloud.NotificationResult
(*SearchLogsRequest)(nil), // 25: cloud.SearchLogsRequest
(*SearchLogsResponse)(nil), // 26: cloud.SearchLogsResponse
(*SearchLogHit)(nil), // 27: cloud.SearchLogHit
(*GetAlertsRequest)(nil), // 28: cloud.GetAlertsRequest
(*AlertHit)(nil), // 29: cloud.AlertHit
(*EventHit)(nil), // 30: cloud.EventHit
(*GetAlertsResponse)(nil), // 31: cloud.GetAlertsResponse
nil, // 32: cloud.InspectContainerResult.LabelsEntry
(*NotificationResult)(nil), // 23: cloud.NotificationResult
(*SearchLogsRequest)(nil), // 24: cloud.SearchLogsRequest
(*SearchLogsResponse)(nil), // 25: cloud.SearchLogsResponse
(*SearchLogHit)(nil), // 26: cloud.SearchLogHit
(*GetAlertsRequest)(nil), // 27: cloud.GetAlertsRequest
(*AlertHit)(nil), // 28: cloud.AlertHit
(*EventHit)(nil), // 29: cloud.EventHit
(*GetAlertsResponse)(nil), // 30: cloud.GetAlertsResponse
nil, // 31: cloud.InspectContainerResult.LabelsEntry
}
var file_cloud_proto_depIdxs = []int32{
7, // 0: cloud.ToolRequest.list_tools:type_name -> cloud.ListToolsRequest
@@ -3239,27 +3156,26 @@ var file_cloud_proto_depIdxs = []int32{
22, // 14: cloud.CallToolResponse.action:type_name -> cloud.ActionResult
20, // 15: cloud.CallToolResponse.fetch_logs:type_name -> cloud.FetchLogsResult
21, // 16: cloud.CallToolResponse.inspect_container:type_name -> cloud.InspectContainerResult
23, // 17: cloud.CallToolResponse.deploy:type_name -> cloud.DeployResult
24, // 18: cloud.CallToolResponse.notification:type_name -> cloud.NotificationResult
13, // 19: cloud.ListHostsResult.hosts:type_name -> cloud.HostInfo
15, // 20: cloud.ListContainersResult.containers:type_name -> cloud.ContainerInfo
17, // 21: cloud.ContainerStatsResult.stats:type_name -> cloud.ContainerStatEntry
19, // 22: cloud.FetchLogsResult.entries:type_name -> cloud.LogEntry
32, // 23: cloud.InspectContainerResult.labels:type_name -> cloud.InspectContainerResult.LabelsEntry
27, // 24: cloud.SearchLogsResponse.hits:type_name -> cloud.SearchLogHit
29, // 25: cloud.GetAlertsResponse.hits:type_name -> cloud.AlertHit
30, // 26: cloud.GetAlertsResponse.events:type_name -> cloud.EventHit
2, // 27: cloud.CloudToolService.ToolStream:input_type -> cloud.ToolResponse
25, // 28: cloud.CloudToolService.SearchLogs:input_type -> cloud.SearchLogsRequest
28, // 29: cloud.CloudToolService.GetAlerts:input_type -> cloud.GetAlertsRequest
1, // 30: cloud.CloudToolService.ToolStream:output_type -> cloud.ToolRequest
26, // 31: cloud.CloudToolService.SearchLogs:output_type -> cloud.SearchLogsResponse
31, // 32: cloud.CloudToolService.GetAlerts:output_type -> cloud.GetAlertsResponse
30, // [30:33] is the sub-list for method output_type
27, // [27:30] is the sub-list for method input_type
27, // [27:27] is the sub-list for extension type_name
27, // [27:27] is the sub-list for extension extendee
0, // [0:27] is the sub-list for field type_name
23, // 17: cloud.CallToolResponse.notification:type_name -> cloud.NotificationResult
13, // 18: cloud.ListHostsResult.hosts:type_name -> cloud.HostInfo
15, // 19: cloud.ListContainersResult.containers:type_name -> cloud.ContainerInfo
17, // 20: cloud.ContainerStatsResult.stats:type_name -> cloud.ContainerStatEntry
19, // 21: cloud.FetchLogsResult.entries:type_name -> cloud.LogEntry
31, // 22: cloud.InspectContainerResult.labels:type_name -> cloud.InspectContainerResult.LabelsEntry
26, // 23: cloud.SearchLogsResponse.hits:type_name -> cloud.SearchLogHit
28, // 24: cloud.GetAlertsResponse.hits:type_name -> cloud.AlertHit
29, // 25: cloud.GetAlertsResponse.events:type_name -> cloud.EventHit
2, // 26: cloud.CloudToolService.ToolStream:input_type -> cloud.ToolResponse
24, // 27: cloud.CloudToolService.SearchLogs:input_type -> cloud.SearchLogsRequest
27, // 28: cloud.CloudToolService.GetAlerts:input_type -> cloud.GetAlertsRequest
1, // 29: cloud.CloudToolService.ToolStream:output_type -> cloud.ToolRequest
25, // 30: cloud.CloudToolService.SearchLogs:output_type -> cloud.SearchLogsResponse
30, // 31: cloud.CloudToolService.GetAlerts:output_type -> cloud.GetAlertsResponse
29, // [29:32] is the sub-list for method output_type
26, // [26:29] is the sub-list for method input_type
26, // [26:26] is the sub-list for extension type_name
26, // [26:26] is the sub-list for extension extendee
0, // [0:26] is the sub-list for field type_name
}
func init() { file_cloud_proto_init() }
@@ -3285,7 +3201,6 @@ func file_cloud_proto_init() {
(*CallToolResponse_Action)(nil),
(*CallToolResponse_FetchLogs)(nil),
(*CallToolResponse_InspectContainer)(nil),
(*CallToolResponse_Deploy)(nil),
(*CallToolResponse_Notification)(nil),
}
type x struct{}
@@ -3294,7 +3209,7 @@ func file_cloud_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_cloud_proto_rawDesc), len(file_cloud_proto_rawDesc)),
NumEnums: 1,
NumMessages: 32,
NumMessages: 31,
NumExtensions: 0,
NumServices: 1,
},
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.0
// - protoc v7.35.1
// - protoc v7.36.1
// source: cloud.proto
package cloud
+5 -9
View File
@@ -151,7 +151,7 @@ enum ToolScope {
// Applies to the whole Dozzle instance (list_hosts, notifications, etc.).
// Requires instance_id in the args.
TOOL_SCOPE_INSTANCE = 1;
// Targets a specific Docker host (deploy_compose, list_deploy_versions).
// Targets a specific Docker host.
// Requires host_id in the args.
TOOL_SCOPE_HOST = 2;
// Targets a specific container (logs, actions, inspect). Requires
@@ -177,9 +177,12 @@ message CallToolResponse {
ActionResult action = 6;
FetchLogsResult fetch_logs = 7;
InspectContainerResult inspect_container = 8;
DeployResult deploy = 11;
NotificationResult notification = 12;
}
// 11 was DeployResult deploy, removed with the native compose deploy feature.
reserved 11;
reserved "deploy";
}
message CancelStreamRequest {
@@ -288,13 +291,6 @@ message ActionResult {
string message = 4;
}
// Deploy compose result
message DeployResult {
bool success = 1;
string project = 2;
string message = 3;
}
// Notification/alert tool result (list, create).
message NotificationResult {
bool success = 1;