refactor: remove native compose deploy feature (#5022)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
49d7954190
commit
81c3e30152
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user