mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
Port lazydocker to lazyapple for Apple Container runtime
Rewrite of lazydocker to work with Apple's native container CLI instead of Docker. - Replace Docker SDK with Apple Container CLI wrapper - Remove docker-compose/services/projects support - 4 panels: Containers, Images, Volumes, Networks - Remove all Docker Go dependencies - Rename binary to lazyapple - Update README with install and usage instructions
This commit is contained in:
parent
2ebe3848d6
commit
065a57e445
727 changed files with 6269 additions and 89734 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,4 +1,6 @@
|
|||
lazydocker*
|
||||
lazyapple
|
||||
lazycontainer
|
||||
TODO.md
|
||||
Lazydocker.code-workspace
|
||||
.vscode
|
||||
|
|
|
|||
387
README.md
387
README.md
File diff suppressed because one or more lines are too long
48
go.mod
48
go.mod
|
|
@ -1,19 +1,14 @@
|
|||
module github.com/jesseduffield/lazydocker
|
||||
module github.com/jesseduffield/lazycontainer
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.6
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c
|
||||
github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/docker/cli v27.1.1+incompatible
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/fatih/color v1.10.0
|
||||
github.com/go-errors/errors v1.5.1
|
||||
github.com/gookit/color v1.5.0
|
||||
github.com/imdario/mergo v0.3.16
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20240418080333-8cd33929c513
|
||||
|
|
@ -33,53 +28,26 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
dario.cat/mergo v1.0.2
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.39.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.7.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/goccy/go-yaml v1.11.0
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/onsi/ginkgo v1.8.0 // indirect
|
||||
github.com/onsi/gomega v1.5.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/term v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
|
|
|
|||
165
go.sum
165
go.sum
|
|
@ -1,43 +1,19 @@
|
|||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c h1:YDsGA6tou+tAxVe0Dre29iSbQ8TrWdWfwOisKArJT5E=
|
||||
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
|
||||
github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1 h1:1fx+RA5lk1ZkzPAUP7DEgZnVHYxEcHO77vQO/V8z/2Q=
|
||||
github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1/go.mod h1:z0nyIb42Zs97wyX1V+8MbEFhHeTw1OgFQfR6q57ZuHc=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
|
||||
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/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
|
||||
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
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/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
|
||||
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
|
|
@ -46,32 +22,30 @@ github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7
|
|||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54=
|
||||
github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw=
|
||||
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
|
||||
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
|
||||
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee h1:7Zi/OQlGbMz4MT2V1+prN/gv1C64NDyVb/MbJnS0ZfA=
|
||||
|
|
@ -100,31 +74,21 @@ github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767 h1:BrhJNdEFWGui
|
|||
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767/go.mod h1:ct+byCpkFokm4J0tiuAvB8cf2ttm6GcCe89Yr25nGKg=
|
||||
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
|
||||
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
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.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
|
||||
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
|
|
@ -144,6 +108,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
|
|
@ -151,47 +116,48 @@ github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
|
|||
github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
@ -199,8 +165,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
|
@ -212,36 +178,33 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
|
||||
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
|
|
|
|||
BIN
lazycontainer
Executable file
BIN
lazycontainer
Executable file
Binary file not shown.
27
main.go
27
main.go
|
|
@ -8,12 +8,11 @@ import (
|
|||
"runtime"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazydocker/pkg/app"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/app"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/jesseduffield/yaml"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
|
@ -28,8 +27,6 @@ var (
|
|||
|
||||
configFlag = false
|
||||
debuggingFlag = false
|
||||
composeFiles []string
|
||||
projectName string
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -45,14 +42,12 @@ func main() {
|
|||
runtime.GOARCH,
|
||||
)
|
||||
|
||||
flaggy.SetName("lazydocker")
|
||||
flaggy.SetDescription("The lazier way to manage everything docker")
|
||||
flaggy.DefaultParser.AdditionalHelpPrepend = "https://github.com/jesseduffield/lazydocker"
|
||||
flaggy.SetName("lazyapple")
|
||||
flaggy.SetDescription("The lazier way to manage Apple Containers")
|
||||
flaggy.DefaultParser.AdditionalHelpPrepend = "https://github.com/g-battaglia/lazy-apple-container"
|
||||
|
||||
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
|
||||
flaggy.Bool(&debuggingFlag, "d", "debug", "a boolean")
|
||||
flaggy.StringSlice(&composeFiles, "f", "file", "Specify alternate compose files")
|
||||
flaggy.String(&projectName, "p", "project", "Specify a docker compose project name")
|
||||
flaggy.SetVersion(info)
|
||||
|
||||
flaggy.Parse()
|
||||
|
|
@ -73,7 +68,7 @@ func main() {
|
|||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir, projectName)
|
||||
appConfig, err := config.NewAppConfig("lazyapple", version, commit, date, buildSource, debuggingFlag, nil, projectDir, "")
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
|
@ -90,11 +85,6 @@ func main() {
|
|||
os.Exit(0)
|
||||
}
|
||||
|
||||
if client.IsErrConnectionFailed(err) {
|
||||
log.Println(app.Tr.ConnectionFailed)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
newErr := errors.Wrap(err, 0)
|
||||
stackTrace := newErr.ErrorStack()
|
||||
app.Log.Error(stackTrace)
|
||||
|
|
@ -111,12 +101,9 @@ func updateBuildInfo() {
|
|||
})
|
||||
if ok {
|
||||
commit = revision.Value
|
||||
// if lazydocker was built from source we'll show the version as the
|
||||
// abbreviated commit hash
|
||||
version = utils.SafeTruncate(revision.Value, 7)
|
||||
}
|
||||
|
||||
// if version hasn't been set we assume that neither has the date
|
||||
time, ok := lo.Find(buildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||
return setting.Key == "vcs.time"
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,29 +4,27 @@ import (
|
|||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
"github.com/jesseduffield/lazydocker/pkg/log"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/i18n"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/log"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
closers []io.Closer
|
||||
|
||||
Config *config.AppConfig
|
||||
Log *logrus.Entry
|
||||
OSCommand *commands.OSCommand
|
||||
DockerCommand *commands.DockerCommand
|
||||
ContainerCmd *commands.ContainerCommand
|
||||
Gui *gui.Gui
|
||||
Tr *i18n.TranslationSet
|
||||
ErrorChan chan error
|
||||
}
|
||||
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config *config.AppConfig) (*App, error) {
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
|
|
@ -41,14 +39,12 @@ func NewApp(config *config.AppConfig) (*App, error) {
|
|||
}
|
||||
app.OSCommand = commands.NewOSCommand(app.Log, config)
|
||||
|
||||
// here is the place to make use of the docker-compose.yml file in the current directory
|
||||
|
||||
app.DockerCommand, err = commands.NewDockerCommand(app.Log, app.OSCommand, app.Tr, app.Config, app.ErrorChan)
|
||||
app.ContainerCmd, err = commands.NewContainerCommand(app.Log, app.OSCommand, app.Tr, app.Config, app.ErrorChan)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
app.closers = append(app.closers, app.DockerCommand)
|
||||
app.Gui, err = gui.NewGui(app.Log, app.DockerCommand, app.OSCommand, app.Tr, config, app.ErrorChan)
|
||||
app.closers = append(app.closers, app.ContainerCmd)
|
||||
app.Gui, err = gui.NewGui(app.Log, app.ContainerCmd, app.OSCommand, app.Tr, config, app.ErrorChan)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
|
@ -68,14 +64,17 @@ type errorMapping struct {
|
|||
newError string
|
||||
}
|
||||
|
||||
// KnownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace
|
||||
func (app *App) KnownError(err error) (string, bool) {
|
||||
errorMessage := err.Error()
|
||||
|
||||
mappings := []errorMapping{
|
||||
{
|
||||
originalError: "Got permission denied while trying to connect to the Docker daemon socket",
|
||||
newError: app.Tr.CannotAccessDockerSocketError,
|
||||
originalError: "container: command not found",
|
||||
newError: "Apple Container is not installed. Please install it and try again.",
|
||||
},
|
||||
{
|
||||
originalError: "container system is not running",
|
||||
newError: "Apple Container system is not running. Run 'container system start' first.",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazydocker/pkg/app"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/app"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/i18n"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -33,7 +33,7 @@ func Generate() {
|
|||
}
|
||||
|
||||
func generateAtDir(dir string) {
|
||||
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "", "")
|
||||
mConfig, err := config.NewAppConfig("lazyapple", "", "", "", "", true, nil, "", "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
171
pkg/commands/apple_types.go
Normal file
171
pkg/commands/apple_types.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package commands
|
||||
|
||||
type AppleContainer struct {
|
||||
Configuration struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"-"`
|
||||
Image struct {
|
||||
Reference string `json:"reference"`
|
||||
Descriptor struct {
|
||||
Digest string `json:"digest"`
|
||||
Size int64 `json:"size"`
|
||||
MediaType string `json:"mediaType"`
|
||||
} `json:"descriptor"`
|
||||
} `json:"image"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Resources struct {
|
||||
CPUS int `json:"cpus"`
|
||||
MemoryInBytes int64 `json:"memoryInBytes"`
|
||||
} `json:"resources"`
|
||||
Platform struct {
|
||||
Architecture string `json:"architecture"`
|
||||
OS string `json:"os"`
|
||||
} `json:"platform"`
|
||||
InitProcess struct {
|
||||
Environment []string `json:"environment"`
|
||||
Executable string `json:"executable"`
|
||||
Arguments []string `json:"arguments"`
|
||||
WorkingDir string `json:"workingDirectory"`
|
||||
Terminal bool `json:"terminal"`
|
||||
User struct {
|
||||
ID struct {
|
||||
UID int `json:"uid"`
|
||||
GID int `json:"gid"`
|
||||
} `json:"id"`
|
||||
} `json:"user"`
|
||||
} `json:"initProcess"`
|
||||
Mounts []Mount `json:"mounts"`
|
||||
PublishedPorts []struct {
|
||||
HostIP string `json:"hostIP"`
|
||||
HostPort int `json:"hostPort"`
|
||||
ContainerPort int `json:"containerPort"`
|
||||
Protocol string `json:"protocol"`
|
||||
} `json:"publishedPorts"`
|
||||
Networks []struct {
|
||||
Network string `json:"network"`
|
||||
Options struct {
|
||||
Hostname string `json:"hostname"`
|
||||
} `json:"options"`
|
||||
} `json:"networks"`
|
||||
RuntimeHandler string `json:"runtimeHandler"`
|
||||
Rosetta bool `json:"rosetta"`
|
||||
UseInit bool `json:"useInit"`
|
||||
Virtualization bool `json:"virtualization"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
SSH bool `json:"ssh"`
|
||||
} `json:"configuration"`
|
||||
Networks []struct {
|
||||
Network string `json:"network"`
|
||||
IPv4Address string `json:"ipv4Address"`
|
||||
IPv6Address string `json:"ipv6Address"`
|
||||
IPv4Gateway string `json:"ipv4Gateway"`
|
||||
MACAddress string `json:"macAddress"`
|
||||
Hostname string `json:"hostname"`
|
||||
} `json:"networks"`
|
||||
Status string `json:"status"`
|
||||
StartedDate float64 `json:"startedDate"`
|
||||
}
|
||||
|
||||
type Mount struct {
|
||||
Type string `json:"type"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
ReadOnly bool `json:"readonly"`
|
||||
}
|
||||
|
||||
type AppleContainerStats struct {
|
||||
ID string `json:"id"`
|
||||
CPUUsageUsec int64 `json:"cpuUsageUsec"`
|
||||
MemoryUsageBytes int64 `json:"memoryUsageBytes"`
|
||||
MemoryLimitBytes int64 `json:"memoryLimitBytes"`
|
||||
NetworkRxBytes int64 `json:"networkRxBytes"`
|
||||
NetworkTxBytes int64 `json:"networkTxBytes"`
|
||||
BlockReadBytes int64 `json:"blockReadBytes"`
|
||||
BlockWriteBytes int64 `json:"blockWriteBytes"`
|
||||
NumProcesses int `json:"numProcesses"`
|
||||
}
|
||||
|
||||
type AppleImage struct {
|
||||
Reference string `json:"reference"`
|
||||
Descriptor struct {
|
||||
Digest string `json:"digest"`
|
||||
Size int64 `json:"size"`
|
||||
MediaType string `json:"mediaType"`
|
||||
} `json:"descriptor"`
|
||||
FullSize string `json:"fullSize"`
|
||||
}
|
||||
|
||||
type AppleVolume struct {
|
||||
Name string `json:"name"`
|
||||
SizeInBytes int64 `json:"sizeInBytes"`
|
||||
}
|
||||
|
||||
type AppleNetwork struct {
|
||||
ID string `json:"id"`
|
||||
Config struct {
|
||||
Labels map[string]string `json:"labels"`
|
||||
CreationDate float64 `json:"creationDate"`
|
||||
Mode string `json:"mode"`
|
||||
PluginInfo struct {
|
||||
Variant string `json:"variant"`
|
||||
Plugin string `json:"plugin"`
|
||||
} `json:"pluginInfo"`
|
||||
} `json:"config"`
|
||||
State string `json:"state"`
|
||||
Status struct {
|
||||
IPv4Subnet string `json:"ipv4Subnet"`
|
||||
IPv6Subnet string `json:"ipv6Subnet"`
|
||||
IPv4Gateway string `json:"ipv4Gateway"`
|
||||
} `json:"status"`
|
||||
}
|
||||
|
||||
type SystemDF struct {
|
||||
Containers struct {
|
||||
Active int `json:"active"`
|
||||
Reclaimable int `json:"reclaimable"`
|
||||
SizeInBytes int64 `json:"sizeInBytes"`
|
||||
Total int `json:"total"`
|
||||
} `json:"containers"`
|
||||
Images struct {
|
||||
Active int `json:"active"`
|
||||
Reclaimable int `json:"reclaimable"`
|
||||
SizeInBytes int64 `json:"sizeInBytes"`
|
||||
Total int `json:"total"`
|
||||
} `json:"images"`
|
||||
Volumes struct {
|
||||
Active int `json:"active"`
|
||||
Reclaimable int `json:"reclaimable"`
|
||||
SizeInBytes int64 `json:"sizeInBytes"`
|
||||
Total int `json:"total"`
|
||||
} `json:"volumes"`
|
||||
}
|
||||
|
||||
type SystemStatus struct {
|
||||
Running bool
|
||||
}
|
||||
|
||||
type CreateContainerOptions struct {
|
||||
Name string
|
||||
Image string
|
||||
Command []string
|
||||
Env []string
|
||||
WorkingDir string
|
||||
CPUS int
|
||||
Memory string
|
||||
Volumes []string
|
||||
Networks []string
|
||||
Ports []string
|
||||
Labels map[string]string
|
||||
Detach bool
|
||||
Interactive bool
|
||||
TTY bool
|
||||
}
|
||||
|
||||
type ExecOptions struct {
|
||||
Command []string
|
||||
Env []string
|
||||
WorkingDir string
|
||||
Interactive bool
|
||||
TTY bool
|
||||
User string
|
||||
}
|
||||
|
|
@ -6,45 +6,32 @@ import (
|
|||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/i18n"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Container : A docker Container
|
||||
type Container struct {
|
||||
Name string
|
||||
ServiceName string
|
||||
ContainerNumber string // might make this an int in the future if need be
|
||||
|
||||
// OneOff tells us if the container is just a job container or is actually bound to the service
|
||||
OneOff bool
|
||||
ProjectName string
|
||||
ID string
|
||||
Container container.Summary
|
||||
Client *client.Client
|
||||
AppleContainer AppleContainer
|
||||
Client *ContainerClient
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
StatHistory []*RecordedStats
|
||||
Details container.InspectResponse
|
||||
MonitoringStats bool
|
||||
DockerCommand LimitedDockerCommand
|
||||
Tr *i18n.TranslationSet
|
||||
|
||||
StatsMutex deadlock.Mutex
|
||||
}
|
||||
|
||||
// Remove removes the container
|
||||
func (c *Container) Remove(options container.RemoveOptions) error {
|
||||
func (c *Container) Remove(force bool) error {
|
||||
c.Log.Warn(fmt.Sprintf("removing container %s", c.Name))
|
||||
if err := c.Client.ContainerRemove(context.Background(), c.ID, options); err != nil {
|
||||
if strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") {
|
||||
if err := c.Client.RemoveContainer(c.ID, force); err != nil {
|
||||
if strings.Contains(err.Error(), "Stop the container before attempting removal") {
|
||||
return ComplexError{
|
||||
Code: MustStopContainer,
|
||||
Message: err.Error(),
|
||||
|
|
@ -53,100 +40,146 @@ func (c *Container) Remove(options container.RemoveOptions) error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the container
|
||||
func (c *Container) Start() error {
|
||||
c.Log.Warn(fmt.Sprintf("starting container %s", c.Name))
|
||||
return c.Client.ContainerStart(context.Background(), c.ID, container.StartOptions{})
|
||||
return c.Client.StartContainer(c.ID)
|
||||
}
|
||||
|
||||
// Stop stops the container
|
||||
func (c *Container) Stop() error {
|
||||
c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name))
|
||||
return c.Client.ContainerStop(context.Background(), c.ID, container.StopOptions{})
|
||||
return c.Client.StopContainer(c.ID)
|
||||
}
|
||||
|
||||
// Pause pauses the container
|
||||
func (c *Container) Pause() error {
|
||||
c.Log.Warn(fmt.Sprintf("pausing container %s", c.Name))
|
||||
return c.Client.ContainerPause(context.Background(), c.ID)
|
||||
return errors.New("pause is not supported by Apple Container")
|
||||
}
|
||||
|
||||
// Unpause unpauses the container
|
||||
func (c *Container) Unpause() error {
|
||||
c.Log.Warn(fmt.Sprintf("unpausing container %s", c.Name))
|
||||
return c.Client.ContainerUnpause(context.Background(), c.ID)
|
||||
return errors.New("unpause is not supported by Apple Container")
|
||||
}
|
||||
|
||||
// Restart restarts the container
|
||||
func (c *Container) Restart() error {
|
||||
c.Log.Warn(fmt.Sprintf("restarting container %s", c.Name))
|
||||
return c.Client.ContainerRestart(context.Background(), c.ID, container.StopOptions{})
|
||||
return c.Client.RestartContainer(c.ID)
|
||||
}
|
||||
|
||||
func (c *Container) Kill(signal string) error {
|
||||
c.Log.Warn(fmt.Sprintf("killing container %s with signal %s", c.Name, signal))
|
||||
return c.Client.KillContainer(c.ID, signal)
|
||||
}
|
||||
|
||||
// Attach attaches the container
|
||||
func (c *Container) Attach() (*exec.Cmd, error) {
|
||||
if !c.DetailsLoaded() {
|
||||
return nil, errors.New(c.Tr.WaitingForContainerInfo)
|
||||
}
|
||||
|
||||
// verify that we can in fact attach to this container
|
||||
if !c.Details.Config.OpenStdin {
|
||||
return nil, errors.New(c.Tr.UnattachableContainerError)
|
||||
}
|
||||
|
||||
if c.Container.State == "exited" {
|
||||
c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name))
|
||||
if c.AppleContainer.Status != "running" {
|
||||
return nil, errors.New(c.Tr.CannotAttachStoppedContainerError)
|
||||
}
|
||||
|
||||
c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name))
|
||||
// TODO: use SDK
|
||||
cmd := c.OSCommand.NewCmd("docker", "attach", "--sig-proxy=false", c.ID)
|
||||
return cmd, nil
|
||||
return c.Client.ExecContainer(c.ID, ExecOptions{
|
||||
Command: []string{c.getShell()},
|
||||
Interactive: true,
|
||||
TTY: true,
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Top returns process information
|
||||
func (c *Container) Top(ctx context.Context) (container.TopResponse, error) {
|
||||
detail, err := c.Inspect()
|
||||
func (c *Container) getShell() string {
|
||||
for _, env := range c.AppleContainer.Configuration.InitProcess.Environment {
|
||||
if strings.HasPrefix(env, "PATH=") {
|
||||
path := strings.TrimPrefix(env, "PATH=")
|
||||
for _, dir := range strings.Split(path, ":") {
|
||||
for _, shell := range []string{"/bin/sh", "/bin/bash", "/usr/bin/bash"} {
|
||||
if dir == "/bin" || dir == "/usr/bin" {
|
||||
return shell
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "/bin/sh"
|
||||
}
|
||||
|
||||
func (c *Container) Top(ctx context.Context) ([][]string, []string, error) {
|
||||
cmd := c.Client.ExecContainer(c.ID, ExecOptions{
|
||||
Command: []string{"ps", "aux"},
|
||||
})
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return container.TopResponse{}, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// check container status
|
||||
if !detail.State.Running {
|
||||
return container.TopResponse{}, errors.New("container is not running")
|
||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
if len(lines) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
return c.Client.ContainerTop(ctx, c.ID, []string{})
|
||||
titles := strings.Fields(lines[0])
|
||||
var processes [][]string
|
||||
for _, line := range lines[1:] {
|
||||
if strings.TrimSpace(line) != "" {
|
||||
processes = append(processes, strings.Fields(line))
|
||||
}
|
||||
}
|
||||
|
||||
return processes, titles, nil
|
||||
}
|
||||
|
||||
// PruneContainers prunes containers
|
||||
func (c *DockerCommand) PruneContainers() error {
|
||||
_, err := c.Client.ContainersPrune(context.Background(), filters.Args{})
|
||||
return err
|
||||
func (c *Container) Inspect() (*AppleContainer, error) {
|
||||
return c.Client.InspectContainer(c.ID)
|
||||
}
|
||||
|
||||
// Inspect returns details about the container
|
||||
func (c *Container) Inspect() (container.InspectResponse, error) {
|
||||
return c.Client.ContainerInspect(context.Background(), c.ID)
|
||||
}
|
||||
|
||||
// RenderTop returns details about the container
|
||||
func (c *Container) RenderTop(ctx context.Context) (string, error) {
|
||||
result, err := c.Top(ctx)
|
||||
processes, titles, err := c.Top(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return utils.RenderTable(append([][]string{result.Titles}, result.Processes...))
|
||||
return utils.RenderTable(append([][]string{titles}, processes...))
|
||||
}
|
||||
|
||||
// DetailsLoaded tells us whether we have yet loaded the details for a container.
|
||||
// Sometimes it takes some time for a container to have its details loaded
|
||||
// after it starts.
|
||||
func (c *Container) DetailsLoaded() bool {
|
||||
return c.Details.ContainerJSONBase != nil
|
||||
return c.AppleContainer.Configuration.ID != ""
|
||||
}
|
||||
|
||||
func (c *Container) GetStatus() string {
|
||||
return c.AppleContainer.Status
|
||||
}
|
||||
|
||||
func (c *Container) GetImage() string {
|
||||
return c.AppleContainer.Configuration.Image.Reference
|
||||
}
|
||||
|
||||
func (c *Container) GetIP() string {
|
||||
for _, net := range c.AppleContainer.Networks {
|
||||
if net.IPv4Address != "" {
|
||||
return strings.Split(net.IPv4Address, "/")[0]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Container) GetPorts() []string {
|
||||
var ports []string
|
||||
for _, p := range c.AppleContainer.Configuration.PublishedPorts {
|
||||
ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", p.HostIP, p.HostPort, p.ContainerPort, p.Protocol))
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
func (c *Container) GetEnv() []string {
|
||||
return c.AppleContainer.Configuration.InitProcess.Environment
|
||||
}
|
||||
|
||||
func (c *Container) GetLabels() map[string]string {
|
||||
return c.AppleContainer.Configuration.Labels
|
||||
}
|
||||
|
||||
func (c *Container) GetCPUs() int {
|
||||
return c.AppleContainer.Configuration.Resources.CPUS
|
||||
}
|
||||
|
||||
func (c *Container) GetMemory() int64 {
|
||||
return c.AppleContainer.Configuration.Resources.MemoryInBytes
|
||||
}
|
||||
|
|
|
|||
632
pkg/commands/container_client.go
Normal file
632
pkg/commands/container_client.go
Normal file
|
|
@ -0,0 +1,632 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/i18n"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ContainerClient struct {
|
||||
Log *logrus.Entry
|
||||
OSCommand *OSCommand
|
||||
Tr *i18n.TranslationSet
|
||||
Config *config.AppConfig
|
||||
ErrorChan chan error
|
||||
|
||||
ContainerMutex deadlock.Mutex
|
||||
ImageMutex deadlock.Mutex
|
||||
VolumeMutex deadlock.Mutex
|
||||
NetworkMutex deadlock.Mutex
|
||||
}
|
||||
|
||||
func NewContainerClient(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*ContainerClient, error) {
|
||||
return &ContainerClient{
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Config: config,
|
||||
ErrorChan: errorChan,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *ContainerClient) runContainerCommand(args ...string) ([]byte, error) {
|
||||
cmd := exec.Command("container", args...)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return nil, fmt.Errorf("%s: %s", err.Error(), string(exitErr.Stderr))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (c *ContainerClient) runContainerCommandWithOutput(args ...string) (string, error) {
|
||||
output, err := c.runContainerCommand(args...)
|
||||
return string(output), err
|
||||
}
|
||||
|
||||
// Containers
|
||||
|
||||
func (c *ContainerClient) ListContainers(all bool) ([]AppleContainer, error) {
|
||||
args := []string{"list", "--format", "json"}
|
||||
if all {
|
||||
args = []string{"list", "--all", "--format", "json"}
|
||||
}
|
||||
|
||||
output, err := c.runContainerCommand(args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(output) == 0 {
|
||||
return []AppleContainer{}, nil
|
||||
}
|
||||
|
||||
var containers []AppleContainer
|
||||
if err := json.Unmarshal(output, &containers); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse container list: %w", err)
|
||||
}
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (c *ContainerClient) InspectContainer(id string) (*AppleContainer, error) {
|
||||
output, err := c.runContainerCommand("inspect", id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var containers []AppleContainer
|
||||
if err := json.Unmarshal(output, &containers); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse container inspect: %w", err)
|
||||
}
|
||||
|
||||
if len(containers) == 0 {
|
||||
return nil, fmt.Errorf("container %s not found", id)
|
||||
}
|
||||
|
||||
return &containers[0], nil
|
||||
}
|
||||
|
||||
func (c *ContainerClient) CreateContainer(opts CreateContainerOptions) error {
|
||||
args := []string{"create"}
|
||||
|
||||
if opts.Name != "" {
|
||||
args = append(args, "--name", opts.Name)
|
||||
}
|
||||
|
||||
for _, env := range opts.Env {
|
||||
args = append(args, "-e", env)
|
||||
}
|
||||
|
||||
if opts.WorkingDir != "" {
|
||||
args = append(args, "-w", opts.WorkingDir)
|
||||
}
|
||||
|
||||
if opts.CPUS > 0 {
|
||||
args = append(args, "-c", fmt.Sprintf("%d", opts.CPUS))
|
||||
}
|
||||
|
||||
if opts.Memory != "" {
|
||||
args = append(args, "-m", opts.Memory)
|
||||
}
|
||||
|
||||
for _, vol := range opts.Volumes {
|
||||
args = append(args, "-v", vol)
|
||||
}
|
||||
|
||||
for _, net := range opts.Networks {
|
||||
args = append(args, "--network", net)
|
||||
}
|
||||
|
||||
for _, port := range opts.Ports {
|
||||
args = append(args, "-p", port)
|
||||
}
|
||||
|
||||
for k, v := range opts.Labels {
|
||||
args = append(args, "-l", fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
args = append(args, "-i")
|
||||
}
|
||||
|
||||
if opts.TTY {
|
||||
args = append(args, "-t")
|
||||
}
|
||||
|
||||
args = append(args, opts.Image)
|
||||
|
||||
if len(opts.Command) > 0 {
|
||||
args = append(args, opts.Command...)
|
||||
}
|
||||
|
||||
_, err := c.runContainerCommand(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) RunContainer(opts CreateContainerOptions) (string, error) {
|
||||
args := []string{"run"}
|
||||
|
||||
if opts.Name != "" {
|
||||
args = append(args, "--name", opts.Name)
|
||||
}
|
||||
|
||||
if opts.Detach {
|
||||
args = append(args, "-d")
|
||||
}
|
||||
|
||||
for _, env := range opts.Env {
|
||||
args = append(args, "-e", env)
|
||||
}
|
||||
|
||||
if opts.WorkingDir != "" {
|
||||
args = append(args, "-w", opts.WorkingDir)
|
||||
}
|
||||
|
||||
if opts.CPUS > 0 {
|
||||
args = append(args, "-c", fmt.Sprintf("%d", opts.CPUS))
|
||||
}
|
||||
|
||||
if opts.Memory != "" {
|
||||
args = append(args, "-m", opts.Memory)
|
||||
}
|
||||
|
||||
for _, vol := range opts.Volumes {
|
||||
args = append(args, "-v", vol)
|
||||
}
|
||||
|
||||
for _, net := range opts.Networks {
|
||||
args = append(args, "--network", net)
|
||||
}
|
||||
|
||||
for _, port := range opts.Ports {
|
||||
args = append(args, "-p", port)
|
||||
}
|
||||
|
||||
for k, v := range opts.Labels {
|
||||
args = append(args, "-l", fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
args = append(args, "-i")
|
||||
}
|
||||
|
||||
if opts.TTY {
|
||||
args = append(args, "-t")
|
||||
}
|
||||
|
||||
args = append(args, opts.Image)
|
||||
|
||||
if len(opts.Command) > 0 {
|
||||
args = append(args, opts.Command...)
|
||||
}
|
||||
|
||||
output, err := c.runContainerCommand(args...)
|
||||
return string(output), err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) StartContainer(id string) error {
|
||||
_, err := c.runContainerCommand("start", id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) StopContainer(id string) error {
|
||||
_, err := c.runContainerCommand("stop", id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) RestartContainer(id string) error {
|
||||
_, err := c.runContainerCommand("stop", id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.StartContainer(id)
|
||||
}
|
||||
|
||||
func (c *ContainerClient) KillContainer(id string, signal string) error {
|
||||
args := []string{"kill", id}
|
||||
if signal != "" {
|
||||
args = []string{"kill", "--signal", signal, id}
|
||||
}
|
||||
_, err := c.runContainerCommand(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) RemoveContainer(id string, force bool) error {
|
||||
args := []string{"rm", id}
|
||||
if force {
|
||||
args = []string{"rm", "--force", id}
|
||||
}
|
||||
_, err := c.runContainerCommand(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) PruneContainers() error {
|
||||
_, err := c.runContainerCommand("prune")
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) ExecContainer(id string, opts ExecOptions) *exec.Cmd {
|
||||
args := []string{"exec"}
|
||||
|
||||
for _, env := range opts.Env {
|
||||
args = append(args, "-e", env)
|
||||
}
|
||||
|
||||
if opts.WorkingDir != "" {
|
||||
args = append(args, "-w", opts.WorkingDir)
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
args = append(args, "-i")
|
||||
}
|
||||
|
||||
if opts.TTY {
|
||||
args = append(args, "-t")
|
||||
}
|
||||
|
||||
if opts.User != "" {
|
||||
args = append(args, "-u", opts.User)
|
||||
}
|
||||
|
||||
args = append(args, id)
|
||||
args = append(args, opts.Command...)
|
||||
|
||||
return exec.Command("container", args...)
|
||||
}
|
||||
|
||||
func (c *ContainerClient) LogsContainer(id string, follow bool, tail int) *exec.Cmd {
|
||||
args := []string{"logs"}
|
||||
|
||||
if follow {
|
||||
args = append(args, "--follow")
|
||||
}
|
||||
|
||||
if tail > 0 {
|
||||
args = append(args, "-n", fmt.Sprintf("%d", tail))
|
||||
}
|
||||
|
||||
args = append(args, id)
|
||||
|
||||
return exec.Command("container", args...)
|
||||
}
|
||||
|
||||
func (c *ContainerClient) StatsContainer(id string, noStream bool) ([]AppleContainerStats, error) {
|
||||
args := []string{"stats", "--format", "json"}
|
||||
|
||||
if noStream {
|
||||
args = append(args, "--no-stream")
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
args = append(args, id)
|
||||
}
|
||||
|
||||
output, err := c.runContainerCommand(args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(output) == 0 {
|
||||
return []AppleContainerStats{}, nil
|
||||
}
|
||||
|
||||
var stats []AppleContainerStats
|
||||
if err := json.Unmarshal(output, &stats); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse container stats: %w", err)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// Images
|
||||
|
||||
func (c *ContainerClient) ListImages() ([]AppleImage, error) {
|
||||
c.ImageMutex.Lock()
|
||||
defer c.ImageMutex.Unlock()
|
||||
|
||||
output, err := c.runContainerCommand("image", "list", "--format", "json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(output) == 0 {
|
||||
return []AppleImage{}, nil
|
||||
}
|
||||
|
||||
var images []AppleImage
|
||||
if err := json.Unmarshal(output, &images); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse image list: %w", err)
|
||||
}
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func (c *ContainerClient) PullImage(ref string) error {
|
||||
_, err := c.runContainerCommand("image", "pull", ref)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) RemoveImage(id string, force bool) error {
|
||||
args := []string{"image", "rm", id}
|
||||
if force {
|
||||
args = []string{"image", "rm", "--force", id}
|
||||
}
|
||||
_, err := c.runContainerCommand(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) PruneImages(all bool) error {
|
||||
args := []string{"image", "prune"}
|
||||
if all {
|
||||
args = []string{"image", "prune", "-a"}
|
||||
}
|
||||
_, err := c.runContainerCommand(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) BuildImage(contextDir string, dockerfile string, tag string) error {
|
||||
args := []string{"build"}
|
||||
|
||||
if dockerfile != "" {
|
||||
args = append(args, "-f", dockerfile)
|
||||
}
|
||||
|
||||
if tag != "" {
|
||||
args = append(args, "-t", tag)
|
||||
}
|
||||
|
||||
if contextDir != "" {
|
||||
args = append(args, contextDir)
|
||||
}
|
||||
|
||||
_, err := c.runContainerCommand(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) SaveImage(ids []string, output string) error {
|
||||
args := []string{"image", "save"}
|
||||
args = append(args, ids...)
|
||||
if output != "" {
|
||||
args = append(args, "--output", output)
|
||||
}
|
||||
_, err := c.runContainerCommand(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) LoadImage(input string) error {
|
||||
args := []string{"image", "load"}
|
||||
if input != "" {
|
||||
args = append(args, "--input", input)
|
||||
}
|
||||
_, err := c.runContainerCommand(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Volumes
|
||||
|
||||
func (c *ContainerClient) ListVolumes() ([]AppleVolume, error) {
|
||||
c.VolumeMutex.Lock()
|
||||
defer c.VolumeMutex.Unlock()
|
||||
|
||||
output, err := c.runContainerCommand("volume", "list", "--format", "json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(output) == 0 {
|
||||
return []AppleVolume{}, nil
|
||||
}
|
||||
|
||||
var volumes []AppleVolume
|
||||
if err := json.Unmarshal(output, &volumes); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse volume list: %w", err)
|
||||
}
|
||||
|
||||
return volumes, nil
|
||||
}
|
||||
|
||||
func (c *ContainerClient) CreateVolume(name string) error {
|
||||
args := []string{"volume", "create"}
|
||||
if name != "" {
|
||||
args = append(args, name)
|
||||
}
|
||||
_, err := c.runContainerCommand(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) RemoveVolume(name string) error {
|
||||
_, err := c.runContainerCommand("volume", "rm", name)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) PruneVolumes() error {
|
||||
_, err := c.runContainerCommand("volume", "prune")
|
||||
return err
|
||||
}
|
||||
|
||||
// Networks
|
||||
|
||||
func (c *ContainerClient) ListNetworks() ([]AppleNetwork, error) {
|
||||
c.NetworkMutex.Lock()
|
||||
defer c.NetworkMutex.Unlock()
|
||||
|
||||
output, err := c.runContainerCommand("network", "list", "--format", "json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(output) == 0 {
|
||||
return []AppleNetwork{}, nil
|
||||
}
|
||||
|
||||
var networks []AppleNetwork
|
||||
if err := json.Unmarshal(output, &networks); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse network list: %w", err)
|
||||
}
|
||||
|
||||
return networks, nil
|
||||
}
|
||||
|
||||
func (c *ContainerClient) CreateNetwork(name string) error {
|
||||
_, err := c.runContainerCommand("network", "create", name)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) RemoveNetwork(name string) error {
|
||||
_, err := c.runContainerCommand("network", "rm", name)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) PruneNetworks() error {
|
||||
_, err := c.runContainerCommand("network", "prune")
|
||||
return err
|
||||
}
|
||||
|
||||
// System
|
||||
|
||||
func (c *ContainerClient) SystemDF() (*SystemDF, error) {
|
||||
output, err := c.runContainerCommand("system", "df", "--format", "json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var df SystemDF
|
||||
if err := json.Unmarshal(output, &df); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse system df: %w", err)
|
||||
}
|
||||
|
||||
return &df, nil
|
||||
}
|
||||
|
||||
func (c *ContainerClient) SystemStatus() (*SystemStatus, error) {
|
||||
output, err := c.runContainerCommand("system", "status")
|
||||
if err != nil {
|
||||
return &SystemStatus{Running: false}, nil
|
||||
}
|
||||
|
||||
status := &SystemStatus{
|
||||
Running: strings.Contains(string(output), "running"),
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (c *ContainerClient) SystemStart() error {
|
||||
_, err := c.runContainerCommand("system", "start")
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) SystemStop() error {
|
||||
_, err := c.runContainerCommand("system", "stop")
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ContainerClient) SystemVersion() (string, error) {
|
||||
return c.runContainerCommandWithOutput("--version")
|
||||
}
|
||||
|
||||
// Monitor stats for a container
|
||||
func (c *ContainerClient) CreateStatsMonitor(container *Container, callback func(stats AppleContainerStats)) {
|
||||
container.MonitoringStats = true
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if !container.MonitoringStats {
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := c.StatsContainer(container.ID, true)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(stats) > 0 {
|
||||
callback(stats[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _ io.Closer = &ContainerClient{}
|
||||
|
||||
func (c *ContainerClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommandObject is what we pass to our template resolvers
|
||||
type CommandObject struct {
|
||||
Container *Container
|
||||
Image *Image
|
||||
Volume *Volume
|
||||
Network *Network
|
||||
}
|
||||
|
||||
func (c *ContainerClient) NewCommandObject(obj CommandObject) CommandObject {
|
||||
return obj
|
||||
}
|
||||
|
||||
func (c *ContainerClient) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, error) {
|
||||
return c.RefreshContainers(currentContainers), nil
|
||||
}
|
||||
|
||||
func (c *ContainerClient) RefreshContainers(currentContainers []*Container) []*Container {
|
||||
c.ContainerMutex.Lock()
|
||||
defer c.ContainerMutex.Unlock()
|
||||
|
||||
appleContainers, err := c.ListContainers(true)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
return currentContainers
|
||||
}
|
||||
|
||||
containers := make([]*Container, len(appleContainers))
|
||||
|
||||
for i, ac := range appleContainers {
|
||||
var existingContainer *Container
|
||||
for _, cc := range currentContainers {
|
||||
if cc.ID == ac.Configuration.ID {
|
||||
existingContainer = cc
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if existingContainer != nil {
|
||||
existingContainer.AppleContainer = ac
|
||||
existingContainer.Name = ac.Configuration.ID
|
||||
if ac.Configuration.ID != "" {
|
||||
existingContainer.Name = ac.Configuration.ID
|
||||
}
|
||||
containers[i] = existingContainer
|
||||
} else {
|
||||
container := &Container{
|
||||
ID: ac.Configuration.ID,
|
||||
Name: ac.Configuration.ID,
|
||||
AppleContainer: ac,
|
||||
Client: c,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
Tr: c.Tr,
|
||||
}
|
||||
containers[i] = container
|
||||
}
|
||||
}
|
||||
|
||||
return containers
|
||||
}
|
||||
|
||||
func (c *ContainerClient) GetProjectNames(containers []*Container) []string {
|
||||
return []string{}
|
||||
}
|
||||
167
pkg/commands/container_cmd.go
Normal file
167
pkg/commands/container_cmd.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"io"
|
||||
ogLog "log"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/i18n"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ContainerCommand struct {
|
||||
Log *logrus.Entry
|
||||
OSCommand *OSCommand
|
||||
Tr *i18n.TranslationSet
|
||||
Config *config.AppConfig
|
||||
Client *ContainerClient
|
||||
ErrorChan chan error
|
||||
ContainerMutex deadlock.Mutex
|
||||
ImageMutex deadlock.Mutex
|
||||
VolumeMutex deadlock.Mutex
|
||||
NetworkMutex deadlock.Mutex
|
||||
|
||||
Closers []io.Closer
|
||||
}
|
||||
|
||||
var _ io.Closer = &ContainerCommand{}
|
||||
|
||||
type LimitedContainerCommand interface {
|
||||
NewCommandObject(CommandObject) CommandObject
|
||||
}
|
||||
|
||||
func NewContainerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*ContainerCommand, error) {
|
||||
client, err := NewContainerClient(log, osCommand, tr, config, errorChan)
|
||||
if err != nil {
|
||||
ogLog.Fatal(err)
|
||||
}
|
||||
|
||||
containerCommand := &ContainerCommand{
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Config: config,
|
||||
Client: client,
|
||||
ErrorChan: errorChan,
|
||||
Closers: []io.Closer{client},
|
||||
}
|
||||
|
||||
return containerCommand, nil
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) Close() error {
|
||||
return utils.CloseMany(c.Closers)
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) NewCommandObject(obj CommandObject) CommandObject {
|
||||
return obj
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) CreateClientStatMonitor(container *Container) {
|
||||
container.MonitoringStats = true
|
||||
|
||||
var prevStats *ContainerStats
|
||||
lastTime := time.Now()
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if !container.MonitoringStats {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
timeDelta := now.Sub(lastTime)
|
||||
|
||||
stats, err := c.Client.StatsContainer(container.ID, true)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(stats) > 0 {
|
||||
containerStats := ConvertAppleStatsToContainerStats(stats[0], prevStats, timeDelta)
|
||||
|
||||
recordedStats := &RecordedStats{
|
||||
ClientStats: *containerStats,
|
||||
DerivedStats: DerivedStats{
|
||||
CPUPercentage: containerStats.CalculateContainerCPUPercentage(),
|
||||
MemoryPercentage: containerStats.CalculateContainerMemoryUsage(),
|
||||
},
|
||||
RecordedAt: now,
|
||||
}
|
||||
|
||||
container.appendStats(recordedStats, c.Config.UserConfig.Stats.MaxDuration)
|
||||
|
||||
prevStats = containerStats
|
||||
lastTime = now
|
||||
}
|
||||
}
|
||||
|
||||
container.MonitoringStats = false
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, error) {
|
||||
containers := c.RefreshContainers(currentContainers)
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) RefreshContainers(currentContainers []*Container) []*Container {
|
||||
return c.Client.RefreshContainers(currentContainers)
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) RefreshImages() ([]*Image, error) {
|
||||
return c.Client.RefreshImages()
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) RefreshVolumes() ([]*Volume, error) {
|
||||
return c.Client.RefreshVolumes()
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) RefreshNetworks() ([]*Network, error) {
|
||||
return c.Client.RefreshNetworks()
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) PruneContainers() error {
|
||||
return c.Client.PruneContainers()
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) PruneImages() error {
|
||||
return c.Client.PruneImages(false)
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) PruneVolumes() error {
|
||||
return c.Client.PruneVolumes()
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) PruneNetworks() error {
|
||||
return c.Client.PruneNetworks()
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) SystemDF() (*SystemDF, error) {
|
||||
return c.Client.SystemDF()
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) SystemStatus() (*SystemStatus, error) {
|
||||
return c.Client.SystemStatus()
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) SystemStart() error {
|
||||
return c.Client.SystemStart()
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) SystemStop() error {
|
||||
return c.Client.SystemStop()
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) BuildImage(contextDir string, dockerfile string, tag string) error {
|
||||
return c.Client.BuildImage(contextDir, dockerfile, tag)
|
||||
}
|
||||
|
||||
func (c *ContainerCommand) GetProjectNames(containers []*Container) []string {
|
||||
return []string{}
|
||||
}
|
||||
|
|
@ -5,151 +5,53 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// RecordedStats contains both the container stats we've received from docker, and our own derived stats from those container stats. When configuring a graph, you're basically specifying the path of a value in this struct
|
||||
type RecordedStats struct {
|
||||
ClientStats ContainerStats
|
||||
DerivedStats DerivedStats
|
||||
RecordedAt time.Time
|
||||
}
|
||||
|
||||
// DerivedStats contains some useful stats that we've calculated based on the raw container stats that we got back from docker
|
||||
type DerivedStats struct {
|
||||
CPUPercentage float64
|
||||
MemoryPercentage float64
|
||||
}
|
||||
|
||||
// ContainerStats autogenerated at https://mholt.github.io/json-to-go/
|
||||
type ContainerStats struct {
|
||||
Read time.Time `json:"read"`
|
||||
Preread time.Time `json:"preread"`
|
||||
PidsStats struct {
|
||||
Current int `json:"current"`
|
||||
} `json:"pids_stats"`
|
||||
BlkioStats struct {
|
||||
IoServiceBytesRecursive []struct {
|
||||
Major int `json:"major"`
|
||||
Minor int `json:"minor"`
|
||||
Op string `json:"op"`
|
||||
Value int `json:"value"`
|
||||
} `json:"io_service_bytes_recursive"`
|
||||
IoServicedRecursive []struct {
|
||||
Major int `json:"major"`
|
||||
Minor int `json:"minor"`
|
||||
Op string `json:"op"`
|
||||
Value int `json:"value"`
|
||||
} `json:"io_serviced_recursive"`
|
||||
IoQueueRecursive []interface{} `json:"io_queue_recursive"`
|
||||
IoServiceTimeRecursive []interface{} `json:"io_service_time_recursive"`
|
||||
IoWaitTimeRecursive []interface{} `json:"io_wait_time_recursive"`
|
||||
IoMergedRecursive []interface{} `json:"io_merged_recursive"`
|
||||
IoTimeRecursive []interface{} `json:"io_time_recursive"`
|
||||
SectorsRecursive []interface{} `json:"sectors_recursive"`
|
||||
} `json:"blkio_stats"`
|
||||
NumProcs int `json:"num_procs"`
|
||||
StorageStats struct{} `json:"storage_stats"`
|
||||
CPUStats struct {
|
||||
CPUUsage struct {
|
||||
TotalUsage int64 `json:"total_usage"`
|
||||
PercpuUsage []int64 `json:"percpu_usage"`
|
||||
UsageInKernelmode int64 `json:"usage_in_kernelmode"`
|
||||
UsageInUsermode int64 `json:"usage_in_usermode"`
|
||||
} `json:"cpu_usage"`
|
||||
SystemCPUUsage int64 `json:"system_cpu_usage"`
|
||||
OnlineCpus int `json:"online_cpus"`
|
||||
ThrottlingData struct {
|
||||
Periods int `json:"periods"`
|
||||
ThrottledPeriods int `json:"throttled_periods"`
|
||||
ThrottledTime int `json:"throttled_time"`
|
||||
} `json:"throttling_data"`
|
||||
} `json:"cpu_stats"`
|
||||
PrecpuStats struct {
|
||||
CPUUsage struct {
|
||||
TotalUsage int64 `json:"total_usage"`
|
||||
PercpuUsage []int64 `json:"percpu_usage"`
|
||||
UsageInKernelmode int64 `json:"usage_in_kernelmode"`
|
||||
UsageInUsermode int64 `json:"usage_in_usermode"`
|
||||
} `json:"cpu_usage"`
|
||||
SystemCPUUsage int64 `json:"system_cpu_usage"`
|
||||
OnlineCpus int `json:"online_cpus"`
|
||||
ThrottlingData struct {
|
||||
Periods int `json:"periods"`
|
||||
ThrottledPeriods int `json:"throttled_periods"`
|
||||
ThrottledTime int `json:"throttled_time"`
|
||||
} `json:"throttling_data"`
|
||||
} `json:"precpu_stats"`
|
||||
MemoryStats struct {
|
||||
Usage int `json:"usage"`
|
||||
MaxUsage int `json:"max_usage"`
|
||||
Stats struct {
|
||||
ActiveAnon int `json:"active_anon"`
|
||||
ActiveFile int `json:"active_file"`
|
||||
Cache int `json:"cache"`
|
||||
Dirty int `json:"dirty"`
|
||||
HierarchicalMemoryLimit int64 `json:"hierarchical_memory_limit"`
|
||||
HierarchicalMemswLimit int64 `json:"hierarchical_memsw_limit"`
|
||||
InactiveAnon int `json:"inactive_anon"`
|
||||
InactiveFile int `json:"inactive_file"`
|
||||
MappedFile int `json:"mapped_file"`
|
||||
Pgfault int `json:"pgfault"`
|
||||
Pgmajfault int `json:"pgmajfault"`
|
||||
Pgpgin int `json:"pgpgin"`
|
||||
Pgpgout int `json:"pgpgout"`
|
||||
Rss int `json:"rss"`
|
||||
RssHuge int `json:"rss_huge"`
|
||||
TotalActiveAnon int `json:"total_active_anon"`
|
||||
TotalActiveFile int `json:"total_active_file"`
|
||||
TotalCache int `json:"total_cache"`
|
||||
TotalDirty int `json:"total_dirty"`
|
||||
TotalInactiveAnon int `json:"total_inactive_anon"`
|
||||
TotalInactiveFile int `json:"total_inactive_file"`
|
||||
TotalMappedFile int `json:"total_mapped_file"`
|
||||
TotalPgfault int `json:"total_pgfault"`
|
||||
TotalPgmajfault int `json:"total_pgmajfault"`
|
||||
TotalPgpgin int `json:"total_pgpgin"`
|
||||
TotalPgpgout int `json:"total_pgpgout"`
|
||||
TotalRss int `json:"total_rss"`
|
||||
TotalRssHuge int `json:"total_rss_huge"`
|
||||
TotalUnevictable int `json:"total_unevictable"`
|
||||
TotalWriteback int `json:"total_writeback"`
|
||||
Unevictable int `json:"unevictable"`
|
||||
Writeback int `json:"writeback"`
|
||||
} `json:"stats"`
|
||||
Limit int64 `json:"limit"`
|
||||
} `json:"memory_stats"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Networks struct {
|
||||
Eth0 struct {
|
||||
RxBytes int `json:"rx_bytes"`
|
||||
RxPackets int `json:"rx_packets"`
|
||||
RxErrors int `json:"rx_errors"`
|
||||
RxDropped int `json:"rx_dropped"`
|
||||
TxBytes int `json:"tx_bytes"`
|
||||
TxPackets int `json:"tx_packets"`
|
||||
TxErrors int `json:"tx_errors"`
|
||||
TxDropped int `json:"tx_dropped"`
|
||||
} `json:"eth0"`
|
||||
} `json:"networks"`
|
||||
ID string `json:"id"`
|
||||
CPUUsageUsec int64 `json:"cpuUsageUsec"`
|
||||
MemoryUsageBytes int64 `json:"memoryUsageBytes"`
|
||||
MemoryLimitBytes int64 `json:"memoryLimitBytes"`
|
||||
NetworkRxBytes int64 `json:"networkRxBytes"`
|
||||
NetworkTxBytes int64 `json:"networkTxBytes"`
|
||||
BlockReadBytes int64 `json:"blockReadBytes"`
|
||||
BlockWriteBytes int64 `json:"blockWriteBytes"`
|
||||
NumProcesses int `json:"numProcesses"`
|
||||
|
||||
PrevCPUUsageUsec int64 `json:"-"`
|
||||
TimeDeltaUsec int64 `json:"-"`
|
||||
}
|
||||
|
||||
// CalculateContainerCPUPercentage calculates the cpu usage of the container as a percent of total CPU usage
|
||||
// to calculate CPU usage, we take the increase in CPU time from the container since the last poll, divide that by the total increase in CPU time since the last poll, times by the number of cores, and times by 100 to get a percentage
|
||||
// I'm not entirely sure why we need to multiply by the number of cores, but the numbers work
|
||||
func (s *ContainerStats) CalculateContainerCPUPercentage() float64 {
|
||||
cpuUsageDelta := s.CPUStats.CPUUsage.TotalUsage - s.PrecpuStats.CPUUsage.TotalUsage
|
||||
cpuTotalUsageDelta := s.CPUStats.SystemCPUUsage - s.PrecpuStats.SystemCPUUsage
|
||||
|
||||
value := float64(cpuUsageDelta*100) / float64(cpuTotalUsageDelta)
|
||||
if math.IsNaN(value) {
|
||||
if s.TimeDeltaUsec == 0 {
|
||||
return 0
|
||||
}
|
||||
cpuDelta := s.CPUUsageUsec - s.PrevCPUUsageUsec
|
||||
if cpuDelta < 0 {
|
||||
cpuDelta = s.CPUUsageUsec
|
||||
}
|
||||
value := float64(cpuDelta*1000000) / float64(s.TimeDeltaUsec)
|
||||
if math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// CalculateContainerMemoryUsage calculates the memory usage of the container as a percent of total available memory
|
||||
func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
|
||||
value := float64(s.MemoryStats.Usage*100) / float64(s.MemoryStats.Limit)
|
||||
if math.IsNaN(value) {
|
||||
if s.MemoryLimitBytes == 0 {
|
||||
return 0
|
||||
}
|
||||
value := float64(s.MemoryUsageBytes*100) / float64(s.MemoryLimitBytes)
|
||||
if math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return 0
|
||||
}
|
||||
return value
|
||||
|
|
@ -163,7 +65,6 @@ func (c *Container) appendStats(stats *RecordedStats, maxDuration time.Duration)
|
|||
c.eraseOldHistory(maxDuration)
|
||||
}
|
||||
|
||||
// eraseOldHistory removes any history before the user-specified max duration
|
||||
func (c *Container) eraseOldHistory(maxDuration time.Duration) {
|
||||
if maxDuration == 0 {
|
||||
return
|
||||
|
|
@ -186,3 +87,24 @@ func (c *Container) GetLastStats() (*RecordedStats, bool) {
|
|||
}
|
||||
return history[len(history)-1], true
|
||||
}
|
||||
|
||||
func ConvertAppleStatsToContainerStats(appleStats AppleContainerStats, prevStats *ContainerStats, timeDelta time.Duration) *ContainerStats {
|
||||
stats := &ContainerStats{
|
||||
ID: appleStats.ID,
|
||||
CPUUsageUsec: appleStats.CPUUsageUsec,
|
||||
MemoryUsageBytes: appleStats.MemoryUsageBytes,
|
||||
MemoryLimitBytes: appleStats.MemoryLimitBytes,
|
||||
NetworkRxBytes: appleStats.NetworkRxBytes,
|
||||
NetworkTxBytes: appleStats.NetworkTxBytes,
|
||||
BlockReadBytes: appleStats.BlockReadBytes,
|
||||
BlockWriteBytes: appleStats.BlockWriteBytes,
|
||||
NumProcesses: appleStats.NumProcesses,
|
||||
}
|
||||
|
||||
if prevStats != nil {
|
||||
stats.PrevCPUUsageUsec = prevStats.CPUUsageUsec
|
||||
stats.TimeDeltaUsec = int64(timeDelta.Microseconds())
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCalculateContainerCPUPercentage(t *testing.T) {
|
||||
container := &ContainerStats{}
|
||||
container.CPUStats.CPUUsage.TotalUsage = 10
|
||||
container.CPUStats.SystemCPUUsage = 10
|
||||
container.PrecpuStats.CPUUsage.TotalUsage = 5
|
||||
container.PrecpuStats.SystemCPUUsage = 2
|
||||
|
||||
assert.EqualValues(t, 62.5, container.CalculateContainerCPUPercentage())
|
||||
}
|
||||
|
|
@ -1,569 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
ogLog "log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
ddocker "github.com/docker/cli/cli/context/docker"
|
||||
ctxstore "github.com/docker/cli/cli/context/store"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands/ssh"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
dockerHostEnvKey = "DOCKER_HOST"
|
||||
)
|
||||
|
||||
// DockerCommand is our main docker interface
|
||||
type DockerCommand struct {
|
||||
Log *logrus.Entry
|
||||
OSCommand *OSCommand
|
||||
Tr *i18n.TranslationSet
|
||||
Config *config.AppConfig
|
||||
Client *client.Client
|
||||
InDockerComposeProject bool
|
||||
// LocalProjectName is the compose project name for the directory where lazydocker was launched.
|
||||
LocalProjectName string
|
||||
ErrorChan chan error
|
||||
ContainerMutex deadlock.Mutex
|
||||
ServiceMutex deadlock.Mutex
|
||||
|
||||
Closers []io.Closer
|
||||
}
|
||||
|
||||
var _ io.Closer = &DockerCommand{}
|
||||
|
||||
// LimitedDockerCommand is a stripped-down DockerCommand with just the methods the container/service/image might need
|
||||
type LimitedDockerCommand interface {
|
||||
NewCommandObject(CommandObject) CommandObject
|
||||
}
|
||||
|
||||
// CommandObject is what we pass to our template resolvers when we are running a custom command. We do not guarantee that all fields will be populated: just the ones that make sense for the current context
|
||||
type CommandObject struct {
|
||||
DockerCompose string
|
||||
Service *Service
|
||||
Container *Container
|
||||
Image *Image
|
||||
Volume *Volume
|
||||
Network *Network
|
||||
Project *Project
|
||||
}
|
||||
|
||||
// NewCommandObject takes a command object and returns a default command object with the passed command object merged in
|
||||
func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject {
|
||||
defaultObj := CommandObject{DockerCompose: c.Config.UserConfig.CommandTemplates.DockerCompose}
|
||||
_ = mergo.Merge(&defaultObj, obj)
|
||||
|
||||
// When operating on a specific project, include -p flag so that
|
||||
// docker compose targets the correct project.
|
||||
if obj.Service != nil && obj.Service.ProjectName != "" {
|
||||
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, obj.Service.ProjectName)
|
||||
} else if obj.Project != nil && obj.Project.Name != "" {
|
||||
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, obj.Project.Name)
|
||||
}
|
||||
|
||||
return defaultObj
|
||||
}
|
||||
|
||||
// newDockerClient creates a Docker client with the given host.
|
||||
// We avoid using client.FromEnv because it includes WithVersionFromEnv() which
|
||||
// sets manualOverride=true when DOCKER_API_VERSION is set, preventing API version
|
||||
// negotiation even when WithAPIVersionNegotiation() is specified.
|
||||
// Instead, we explicitly configure only what we need, and rely on proper
|
||||
// API version negotiation to support older Docker daemons.
|
||||
// See https://github.com/jesseduffield/lazydocker/issues/715
|
||||
func newDockerClient(dockerHost string) (*client.Client, error) {
|
||||
return client.NewClientWithOpts(
|
||||
client.WithTLSClientConfigFromEnv(),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
client.WithHost(dockerHost),
|
||||
)
|
||||
}
|
||||
|
||||
// NewDockerCommand it runs docker commands
|
||||
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
|
||||
dockerHost, err := determineDockerHost()
|
||||
if err != nil {
|
||||
ogLog.Printf("> could not determine host %v", err)
|
||||
}
|
||||
|
||||
// NOTE: Inject the determined docker host to the environment. This allows the
|
||||
// `SSHHandler.HandleSSHDockerHost()` to create a local unix socket tunneled
|
||||
// over SSH to the specified ssh host.
|
||||
if strings.HasPrefix(dockerHost, "ssh://") {
|
||||
os.Setenv(dockerHostEnvKey, dockerHost)
|
||||
}
|
||||
|
||||
tunnelCloser, err := ssh.NewSSHHandler(osCommand).HandleSSHDockerHost()
|
||||
if err != nil {
|
||||
ogLog.Fatal(err)
|
||||
}
|
||||
|
||||
// Retrieve the docker host from the environment which could have been set by
|
||||
// the `SSHHandler.HandleSSHDockerHost()` and override `dockerHost`.
|
||||
dockerHostFromEnv := os.Getenv(dockerHostEnvKey)
|
||||
if dockerHostFromEnv != "" {
|
||||
dockerHost = dockerHostFromEnv
|
||||
}
|
||||
|
||||
cli, err := newDockerClient(dockerHost)
|
||||
if err != nil {
|
||||
ogLog.Fatal(err)
|
||||
}
|
||||
|
||||
dockerCommand := &DockerCommand{
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Config: config,
|
||||
Client: cli,
|
||||
ErrorChan: errorChan,
|
||||
InDockerComposeProject: true,
|
||||
Closers: []io.Closer{tunnelCloser},
|
||||
}
|
||||
|
||||
dockerCommand.setDockerComposeCommand(config)
|
||||
|
||||
err = osCommand.RunCommand(
|
||||
utils.ApplyTemplate(
|
||||
config.UserConfig.CommandTemplates.CheckDockerComposeConfig,
|
||||
dockerCommand.NewCommandObject(CommandObject{}),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
dockerCommand.InDockerComposeProject = false
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
|
||||
return dockerCommand, nil
|
||||
}
|
||||
|
||||
func (c *DockerCommand) setDockerComposeCommand(config *config.AppConfig) {
|
||||
if config.UserConfig.CommandTemplates.DockerCompose != "docker compose" {
|
||||
return
|
||||
}
|
||||
|
||||
// it's possible that a user is still using docker-compose, so we'll check if 'docker comopose' is available, and if not, we'll fall back to 'docker-compose'
|
||||
err := c.OSCommand.RunCommand("docker compose version")
|
||||
if err != nil {
|
||||
config.UserConfig.CommandTemplates.DockerCompose = "docker-compose"
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DockerCommand) Close() error {
|
||||
return utils.CloseMany(c.Closers)
|
||||
}
|
||||
|
||||
func (c *DockerCommand) CreateClientStatMonitor(container *Container) {
|
||||
container.MonitoringStats = true
|
||||
stream, err := c.Client.ContainerStats(context.Background(), container.ID, true)
|
||||
if err != nil {
|
||||
// not creating error panel because if we've disconnected from docker we'll
|
||||
// have already created an error panel
|
||||
c.Log.Error(err)
|
||||
container.MonitoringStats = false
|
||||
return
|
||||
}
|
||||
|
||||
defer stream.Body.Close()
|
||||
|
||||
scanner := bufio.NewScanner(stream.Body)
|
||||
for scanner.Scan() {
|
||||
data := scanner.Bytes()
|
||||
var stats ContainerStats
|
||||
_ = json.Unmarshal(data, &stats)
|
||||
|
||||
recordedStats := &RecordedStats{
|
||||
ClientStats: stats,
|
||||
DerivedStats: DerivedStats{
|
||||
CPUPercentage: stats.CalculateContainerCPUPercentage(),
|
||||
MemoryPercentage: stats.CalculateContainerMemoryUsage(),
|
||||
},
|
||||
RecordedAt: time.Now(),
|
||||
}
|
||||
|
||||
container.appendStats(recordedStats, c.Config.UserConfig.Stats.MaxDuration)
|
||||
}
|
||||
|
||||
container.MonitoringStats = false
|
||||
}
|
||||
|
||||
func (c *DockerCommand) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, []*Service, error) {
|
||||
c.ServiceMutex.Lock()
|
||||
defer c.ServiceMutex.Unlock()
|
||||
|
||||
containers, err := c.GetContainers(currentContainers)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Derive services from container labels (covers all projects)
|
||||
services := c.GetServicesFromContainers(containers)
|
||||
|
||||
var composeServices []*Service
|
||||
if c.InDockerComposeProject {
|
||||
composeServices, err = c.GetServices()
|
||||
if err != nil {
|
||||
c.Log.Warn("Failed to get compose services: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the local project name before merging services, since
|
||||
// mergeServices needs it. We match compose service names against container
|
||||
// labels to handle cases where the project name differs from the directory
|
||||
// name (e.g. a `name:` directive in the compose file).
|
||||
if c.LocalProjectName == "" && c.InDockerComposeProject && composeServices != nil {
|
||||
for _, ctr := range containers {
|
||||
if ctr.ProjectName == "" || ctr.ServiceName == "" {
|
||||
continue
|
||||
}
|
||||
for _, svc := range composeServices {
|
||||
if ctr.ServiceName == svc.Name {
|
||||
c.LocalProjectName = ctr.ProjectName
|
||||
break
|
||||
}
|
||||
}
|
||||
if c.LocalProjectName != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Fall back to directory name
|
||||
if c.LocalProjectName == "" && c.Config.ProjectDir != "" {
|
||||
c.LocalProjectName = filepath.Base(c.Config.ProjectDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge compose services (which include stopped services) with
|
||||
// container-derived services from all projects
|
||||
if composeServices != nil {
|
||||
services = c.mergeServices(services, composeServices)
|
||||
}
|
||||
|
||||
c.assignContainersToServices(containers, services)
|
||||
|
||||
return containers, services, nil
|
||||
}
|
||||
|
||||
// GetServicesFromContainers derives services from container labels for all projects
|
||||
func (c *DockerCommand) GetServicesFromContainers(containers []*Container) []*Service {
|
||||
// Use project+service as key to avoid duplicates
|
||||
type serviceKey struct {
|
||||
project string
|
||||
service string
|
||||
}
|
||||
seen := make(map[serviceKey]bool)
|
||||
services := make([]*Service, 0, len(containers))
|
||||
|
||||
for _, ctr := range containers {
|
||||
if ctr.ServiceName == "" || ctr.OneOff {
|
||||
continue
|
||||
}
|
||||
key := serviceKey{project: ctr.ProjectName, service: ctr.ServiceName}
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
services = append(services, &Service{
|
||||
Name: ctr.ServiceName,
|
||||
ID: ctr.ProjectName + "-" + ctr.ServiceName,
|
||||
ProjectName: ctr.ProjectName,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
DockerCommand: c,
|
||||
})
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
// mergeServices merges compose services (which may lack ProjectName) with
|
||||
// container-derived services. Compose services take priority because they
|
||||
// include services without running containers.
|
||||
func (c *DockerCommand) mergeServices(containerServices []*Service, composeServices []*Service) []*Service {
|
||||
// Set project name on compose services
|
||||
for _, svc := range composeServices {
|
||||
if svc.ProjectName == "" {
|
||||
svc.ProjectName = c.LocalProjectName
|
||||
}
|
||||
}
|
||||
|
||||
// Build a set of compose service names for the local project
|
||||
composeServiceNames := make(map[string]bool)
|
||||
for _, svc := range composeServices {
|
||||
composeServiceNames[svc.Name] = true
|
||||
}
|
||||
|
||||
// Start with compose services, then add container-derived services
|
||||
// that aren't already covered by compose (i.e. from other projects)
|
||||
result := make([]*Service, 0, len(composeServices)+len(containerServices))
|
||||
result = append(result, composeServices...)
|
||||
|
||||
for _, svc := range containerServices {
|
||||
if svc.ProjectName == c.LocalProjectName && composeServiceNames[svc.Name] {
|
||||
continue // already covered by compose service
|
||||
}
|
||||
result = append(result, svc)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetProjectNames returns all unique project names from containers
|
||||
func (c *DockerCommand) GetProjectNames(containers []*Container) []string {
|
||||
seen := make(map[string]bool)
|
||||
var names []string
|
||||
for _, ctr := range containers {
|
||||
if ctr.ProjectName != "" && !seen[ctr.ProjectName] {
|
||||
seen[ctr.ProjectName] = true
|
||||
names = append(names, ctr.ProjectName)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) {
|
||||
L:
|
||||
for _, service := range services {
|
||||
for _, ctr := range containers {
|
||||
if !ctr.OneOff && ctr.ServiceName == service.Name && ctr.ProjectName == service.ProjectName {
|
||||
service.Container = ctr
|
||||
continue L
|
||||
}
|
||||
}
|
||||
service.Container = nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetContainers gets the docker containers
|
||||
func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Container, error) {
|
||||
c.ContainerMutex.Lock()
|
||||
defer c.ContainerMutex.Unlock()
|
||||
|
||||
containers, err := c.Client.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownContainers := make([]*Container, len(containers))
|
||||
|
||||
for i, ctr := range containers {
|
||||
var newContainer *Container
|
||||
|
||||
// check if we already have data stored against the container
|
||||
for _, existingContainer := range existingContainers {
|
||||
if existingContainer.ID == ctr.ID {
|
||||
newContainer = existingContainer
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// initialise the container if it's completely new
|
||||
if newContainer == nil {
|
||||
newContainer = &Container{
|
||||
ID: ctr.ID,
|
||||
Client: c.Client,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
DockerCommand: c,
|
||||
Tr: c.Tr,
|
||||
}
|
||||
}
|
||||
|
||||
newContainer.Container = ctr
|
||||
// if the container is made with a name label we will use that
|
||||
if name, ok := ctr.Labels["name"]; ok {
|
||||
newContainer.Name = name
|
||||
} else {
|
||||
if len(ctr.Names) > 0 {
|
||||
newContainer.Name = strings.TrimLeft(ctr.Names[0], "/")
|
||||
} else {
|
||||
newContainer.Name = ctr.ID
|
||||
}
|
||||
}
|
||||
newContainer.ServiceName = ctr.Labels["com.docker.compose.service"]
|
||||
newContainer.ProjectName = ctr.Labels["com.docker.compose.project"]
|
||||
newContainer.ContainerNumber = ctr.Labels["com.docker.compose.container"]
|
||||
newContainer.OneOff = ctr.Labels["com.docker.compose.oneoff"] == "True"
|
||||
|
||||
ownContainers[i] = newContainer
|
||||
}
|
||||
|
||||
c.SetContainerDetails(ownContainers)
|
||||
|
||||
return ownContainers, nil
|
||||
}
|
||||
|
||||
// GetServices gets services
|
||||
func (c *DockerCommand) GetServices() ([]*Service, error) {
|
||||
if !c.InDockerComposeProject {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose
|
||||
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --services", composeCommand))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// output looks like:
|
||||
// service1
|
||||
// service2
|
||||
|
||||
lines := utils.SplitLines(output)
|
||||
services := make([]*Service, len(lines))
|
||||
for i, str := range lines {
|
||||
services[i] = &Service{
|
||||
Name: str,
|
||||
ID: c.LocalProjectName + "-" + str,
|
||||
ProjectName: c.LocalProjectName,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
DockerCommand: c,
|
||||
}
|
||||
}
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
func (c *DockerCommand) RefreshContainerDetails(containers []*Container) error {
|
||||
c.ContainerMutex.Lock()
|
||||
defer c.ContainerMutex.Unlock()
|
||||
|
||||
c.SetContainerDetails(containers)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Attaches the details returned from docker inspect to each of the containers
|
||||
// this contains a bit more info than what you get from the go-docker client
|
||||
func (c *DockerCommand) SetContainerDetails(containers []*Container) {
|
||||
wg := sync.WaitGroup{}
|
||||
for _, ctr := range containers {
|
||||
ctr := ctr
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
details, err := c.Client.ContainerInspect(context.Background(), ctr.ID)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
} else {
|
||||
ctr.Details = details
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// ViewAllLogs attaches to a subprocess viewing all the logs from docker-compose
|
||||
func (c *DockerCommand) ViewAllLogs(project *Project) (*exec.Cmd, error) {
|
||||
cmd := c.OSCommand.ExecutableFromString(
|
||||
utils.ApplyTemplate(
|
||||
c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs,
|
||||
c.NewCommandObject(CommandObject{Project: project}),
|
||||
),
|
||||
)
|
||||
|
||||
c.OSCommand.PrepareForChildren(cmd)
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// DockerComposeConfig returns the result of 'docker-compose config'
|
||||
func (c *DockerCommand) DockerComposeConfig() string {
|
||||
return c.DockerComposeConfigForProject(nil)
|
||||
}
|
||||
|
||||
// DockerComposeConfigForProject returns the result of 'docker-compose config' for a specific project
|
||||
func (c *DockerCommand) DockerComposeConfigForProject(project *Project) string {
|
||||
output, err := c.OSCommand.RunCommandWithOutput(
|
||||
utils.ApplyTemplate(
|
||||
c.OSCommand.Config.UserConfig.CommandTemplates.DockerComposeConfig,
|
||||
c.NewCommandObject(CommandObject{Project: project}),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
output = err.Error()
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// determineDockerHost tries to the determine the docker host that we should connect to
|
||||
// in the following order of decreasing precedence:
|
||||
// - value of "DOCKER_HOST" environment variable
|
||||
// - host retrieved from the current context (specified via DOCKER_CONTEXT)
|
||||
// - "default docker host" for the host operating system, otherwise
|
||||
func determineDockerHost() (string, error) {
|
||||
// If the docker host is explicitly set via the "DOCKER_HOST" environment variable,
|
||||
// then its a no-brainer :shrug:
|
||||
if os.Getenv("DOCKER_HOST") != "" {
|
||||
return os.Getenv("DOCKER_HOST"), nil
|
||||
}
|
||||
|
||||
currentContext := os.Getenv("DOCKER_CONTEXT")
|
||||
if currentContext == "" {
|
||||
cf, err := cliconfig.Load(cliconfig.Dir())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
currentContext = cf.CurrentContext
|
||||
}
|
||||
|
||||
// On some systems (windows) `default` is stored in the docker config as the currentContext.
|
||||
if currentContext == "" || currentContext == "default" {
|
||||
// If a docker context is neither specified via the "DOCKER_CONTEXT" environment variable nor via the
|
||||
// $HOME/.docker/config file, then we fall back to connecting to the "default docker host" meant for
|
||||
// the host operating system.
|
||||
return defaultDockerHost, nil
|
||||
}
|
||||
|
||||
storeConfig := ctxstore.NewConfig(
|
||||
func() interface{} { return &ddocker.EndpointMeta{} },
|
||||
ctxstore.EndpointTypeGetter(ddocker.DockerEndpoint, func() interface{} { return &ddocker.EndpointMeta{} }),
|
||||
)
|
||||
|
||||
st := ctxstore.New(cliconfig.ContextStoreDir(), storeConfig)
|
||||
md, err := st.GetMetadata(currentContext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dockerEP, ok := md.Endpoints[ddocker.DockerEndpoint]
|
||||
if !ok {
|
||||
return "", err
|
||||
}
|
||||
dockerEPMeta, ok := dockerEP.(ddocker.EndpointMeta)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("expected docker.EndpointMeta, got %T", dockerEP)
|
||||
}
|
||||
|
||||
if dockerEPMeta.Host != "" {
|
||||
return dockerEPMeta.Host, nil
|
||||
}
|
||||
|
||||
// We might end up here, if the context was created with the `host` set to an empty value (i.e. '').
|
||||
// For example:
|
||||
// ```sh
|
||||
// docker context create foo --docker "host="
|
||||
// ```
|
||||
// In such scenario, we mimic the `docker` cli and try to connect to the "default docker host".
|
||||
return defaultDockerHost, nil
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
//go:build !windows
|
||||
|
||||
package commands
|
||||
|
||||
const (
|
||||
defaultDockerHost = "unix:///var/run/docker.sock"
|
||||
)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package commands
|
||||
|
||||
const (
|
||||
defaultDockerHost = "npipe:////./pipe/docker_engine"
|
||||
)
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestNewDockerClientVersionNegotiation verifies that newDockerClient allows
|
||||
// API version negotiation even when DOCKER_API_VERSION is set.
|
||||
//
|
||||
// This is a regression test for https://github.com/jesseduffield/lazydocker/issues/715
|
||||
// where users got "client version 1.25 is too old" errors because FromEnv()
|
||||
// includes WithVersionFromEnv() which sets manualOverride=true, preventing
|
||||
// API version negotiation.
|
||||
func TestNewDockerClientVersionNegotiation(t *testing.T) {
|
||||
// Save original env var and restore after test
|
||||
originalAPIVersion := os.Getenv("DOCKER_API_VERSION")
|
||||
defer func() {
|
||||
if originalAPIVersion == "" {
|
||||
os.Unsetenv("DOCKER_API_VERSION")
|
||||
} else {
|
||||
os.Setenv("DOCKER_API_VERSION", originalAPIVersion)
|
||||
}
|
||||
}()
|
||||
|
||||
// Set DOCKER_API_VERSION to an old version that would cause
|
||||
// "client version 1.25 is too old" errors if negotiation is disabled
|
||||
os.Setenv("DOCKER_API_VERSION", "1.25")
|
||||
|
||||
t.Run("FromEnv locks version preventing negotiation", func(t *testing.T) {
|
||||
// This demonstrates the problematic behavior we're avoiding.
|
||||
// When using FromEnv with DOCKER_API_VERSION set, the client
|
||||
// version gets locked to 1.25 and negotiation is disabled.
|
||||
cli, err := client.NewClientWithOpts(
|
||||
client.FromEnv,
|
||||
client.WithAPIVersionNegotiation(),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
defer cli.Close()
|
||||
|
||||
// Version is locked to the env var value
|
||||
assert.Equal(t, "1.25", cli.ClientVersion())
|
||||
})
|
||||
|
||||
t.Run("newDockerClient allows version negotiation", func(t *testing.T) {
|
||||
// Test the actual production function.
|
||||
// Use DefaultDockerHost for cross-platform compatibility
|
||||
// (unix socket on Linux/macOS, named pipe on Windows).
|
||||
cli, err := newDockerClient(client.DefaultDockerHost)
|
||||
assert.NoError(t, err)
|
||||
defer cli.Close()
|
||||
|
||||
// Version is NOT locked to the env var value (1.25).
|
||||
// Instead, it uses the library's default version and will negotiate
|
||||
// with the server on first request. This is the key difference that
|
||||
// fixes the "version too old" error.
|
||||
assert.NotEqual(t, "1.25", cli.ClientVersion(),
|
||||
"client version should not be locked to DOCKER_API_VERSION env var")
|
||||
})
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// This file exports dummy constructors for use by tests in other packages
|
||||
|
||||
// NewDummyOSCommand creates a new dummy OSCommand for testing
|
||||
func NewDummyOSCommand() *OSCommand {
|
||||
return NewOSCommand(NewDummyLog(), NewDummyAppConfig())
|
||||
}
|
||||
|
||||
// NewDummyAppConfig creates a new dummy AppConfig for testing
|
||||
func NewDummyAppConfig() *config.AppConfig {
|
||||
appConfig := &config.AppConfig{
|
||||
Name: "lazydocker",
|
||||
Version: "unversioned",
|
||||
Commit: "",
|
||||
BuildDate: "",
|
||||
Debug: false,
|
||||
BuildSource: "",
|
||||
}
|
||||
return appConfig
|
||||
}
|
||||
|
||||
// NewDummyLog creates a new dummy Log for testing
|
||||
func NewDummyLog() *logrus.Entry {
|
||||
log := logrus.New()
|
||||
log.Out = io.Discard
|
||||
return log.WithField("test", "test")
|
||||
}
|
||||
|
||||
// NewDummyDockerCommand creates a new dummy DockerCommand for testing
|
||||
func NewDummyDockerCommand() *DockerCommand {
|
||||
return NewDummyDockerCommandWithOSCommand(NewDummyOSCommand())
|
||||
}
|
||||
|
||||
// NewDummyDockerCommandWithOSCommand creates a new dummy DockerCommand for testing
|
||||
func NewDummyDockerCommandWithOSCommand(osCommand *OSCommand) *DockerCommand {
|
||||
newAppConfig := NewDummyAppConfig()
|
||||
return &DockerCommand{
|
||||
Log: NewDummyLog(),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(NewDummyLog(), newAppConfig.UserConfig.Gui.Language),
|
||||
Config: newAppConfig,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +1,42 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Image : A docker Image
|
||||
type Image struct {
|
||||
Name string
|
||||
Tag string
|
||||
ID string
|
||||
Image image.Summary
|
||||
Client *client.Client
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
DockerCommand LimitedDockerCommand
|
||||
Name string
|
||||
Tag string
|
||||
ID string
|
||||
AppleImage AppleImage
|
||||
Client *ContainerClient
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
ContainerCmd LimitedContainerCommand
|
||||
}
|
||||
|
||||
// Remove removes the image
|
||||
func (i *Image) Remove(options image.RemoveOptions) error {
|
||||
if _, err := i.Client.ImageRemove(context.Background(), i.ID, options); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
func (i *Image) Remove(force bool) error {
|
||||
return i.Client.RemoveImage(i.ID, force)
|
||||
}
|
||||
|
||||
func getHistoryResponseItemDisplayStrings(layer image.HistoryResponseItem) []string {
|
||||
tag := ""
|
||||
if len(layer.Tags) > 0 {
|
||||
tag = layer.Tags[0]
|
||||
func (i *Image) Pull() error {
|
||||
return i.Client.PullImage(i.Name + ":" + i.Tag)
|
||||
}
|
||||
|
||||
func getImageDisplayStrings(img *Image, replacements map[string]string) []string {
|
||||
name := img.Name
|
||||
for prefix, replacement := range replacements {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
name = strings.Replace(name, prefix, replacement, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
id := strings.TrimPrefix(layer.ID, "sha256:")
|
||||
id := strings.TrimPrefix(img.AppleImage.Descriptor.Digest, "sha256:")
|
||||
if len(id) > 10 {
|
||||
id = id[0:10]
|
||||
}
|
||||
|
|
@ -49,95 +45,56 @@ func getHistoryResponseItemDisplayStrings(layer image.HistoryResponseItem) []str
|
|||
idColor = color.FgBlue
|
||||
}
|
||||
|
||||
dockerFileCommandPrefix := "/bin/sh -c #(nop) "
|
||||
createdBy := layer.CreatedBy
|
||||
if strings.Contains(layer.CreatedBy, dockerFileCommandPrefix) {
|
||||
createdBy = strings.Trim(strings.TrimPrefix(layer.CreatedBy, dockerFileCommandPrefix), " ")
|
||||
split := strings.Split(createdBy, " ")
|
||||
createdBy = utils.ColoredString(split[0], color.FgYellow) + " " + strings.Join(split[1:], " ")
|
||||
}
|
||||
|
||||
createdBy = strings.Replace(createdBy, "\t", " ", -1)
|
||||
|
||||
size := utils.FormatBinaryBytes(int(layer.Size))
|
||||
sizeColor := color.FgWhite
|
||||
if size == "0B" {
|
||||
sizeColor = color.FgBlue
|
||||
size := img.AppleImage.FullSize
|
||||
if size == "" {
|
||||
size = utils.FormatBinaryBytes(int(img.AppleImage.Descriptor.Size))
|
||||
}
|
||||
|
||||
return []string{
|
||||
utils.ColoredString(id, idColor),
|
||||
utils.ColoredString(tag, color.FgGreen),
|
||||
utils.ColoredString(size, sizeColor),
|
||||
createdBy,
|
||||
utils.ColoredString(name, color.FgGreen),
|
||||
utils.ColoredString(img.Tag, color.FgCyan),
|
||||
utils.ColoredString(size, color.FgYellow),
|
||||
}
|
||||
}
|
||||
|
||||
// RenderHistory renders the history of the image
|
||||
func (i *Image) RenderHistory() (string, error) {
|
||||
history, err := i.Client.ImageHistory(context.Background(), i.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tableBody := lo.Map(history, func(layer image.HistoryResponseItem, _ int) []string {
|
||||
return getHistoryResponseItemDisplayStrings(layer)
|
||||
})
|
||||
|
||||
headers := [][]string{{"ID", "TAG", "SIZE", "COMMAND"}}
|
||||
table := append(headers, tableBody...)
|
||||
|
||||
return utils.RenderTable(table)
|
||||
}
|
||||
|
||||
// RefreshImages returns a slice of docker images
|
||||
func (c *DockerCommand) RefreshImages() ([]*Image, error) {
|
||||
images, err := c.Client.ImageList(context.Background(), image.ListOptions{})
|
||||
func (c *ContainerClient) RefreshImages() ([]*Image, error) {
|
||||
appleImages, err := c.ListImages()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownImages := make([]*Image, len(images))
|
||||
ownImages := make([]*Image, len(appleImages))
|
||||
|
||||
for i, img := range images {
|
||||
firstTag := ""
|
||||
tags := img.RepoTags
|
||||
if len(tags) > 0 {
|
||||
firstTag = tags[0]
|
||||
}
|
||||
|
||||
nameParts := strings.Split(firstTag, ":")
|
||||
tag := ""
|
||||
name := "none"
|
||||
for i, img := range appleImages {
|
||||
nameParts := strings.Split(img.Reference, ":")
|
||||
name := ""
|
||||
tag := "latest"
|
||||
if len(nameParts) > 1 {
|
||||
tag = nameParts[len(nameParts)-1]
|
||||
name = strings.Join(nameParts[:len(nameParts)-1], ":")
|
||||
} else if len(nameParts) == 1 {
|
||||
name = nameParts[0]
|
||||
}
|
||||
|
||||
for prefix, replacement := range c.Config.UserConfig.Replacements.ImageNamePrefixes {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
name = strings.Replace(name, prefix, replacement, 1)
|
||||
break
|
||||
}
|
||||
for prefix, replacement := range c.Config.UserConfig.Replacements.ImageNamePrefixes {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
name = strings.Replace(name, prefix, replacement, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ownImages[i] = &Image{
|
||||
ID: img.ID,
|
||||
Name: name,
|
||||
Tag: tag,
|
||||
Image: img,
|
||||
Client: c.Client,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
DockerCommand: c,
|
||||
ID: img.Descriptor.Digest,
|
||||
Name: name,
|
||||
Tag: tag,
|
||||
AppleImage: img,
|
||||
Client: c,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
ContainerCmd: c,
|
||||
}
|
||||
}
|
||||
|
||||
return ownImages, nil
|
||||
}
|
||||
|
||||
// PruneImages prunes images
|
||||
func (c *DockerCommand) PruneImages() error {
|
||||
_, err := c.Client.ImagesPrune(context.Background(), filters.Args{})
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +1,39 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Network : A docker Network
|
||||
type Network struct {
|
||||
Name string
|
||||
Network network.Inspect
|
||||
Client *client.Client
|
||||
AppleNetwork AppleNetwork
|
||||
Client *ContainerClient
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
DockerCommand LimitedDockerCommand
|
||||
ContainerCmd LimitedContainerCommand
|
||||
}
|
||||
|
||||
// RefreshNetworks gets the networks and stores them
|
||||
func (c *DockerCommand) RefreshNetworks() ([]*Network, error) {
|
||||
networks, err := c.Client.NetworkList(context.Background(), network.ListOptions{})
|
||||
func (n *Network) Remove() error {
|
||||
return n.Client.RemoveNetwork(n.Name)
|
||||
}
|
||||
|
||||
func (c *ContainerClient) RefreshNetworks() ([]*Network, error) {
|
||||
appleNetworks, err := c.ListNetworks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownNetworks := make([]*Network, len(networks))
|
||||
ownNetworks := make([]*Network, len(appleNetworks))
|
||||
|
||||
for i, nw := range networks {
|
||||
for i, net := range appleNetworks {
|
||||
ownNetworks[i] = &Network{
|
||||
Name: nw.Name,
|
||||
Network: nw,
|
||||
Client: c.Client,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
DockerCommand: c,
|
||||
Name: net.ID,
|
||||
AppleNetwork: net,
|
||||
Client: c,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
}
|
||||
}
|
||||
|
||||
return ownNetworks, nil
|
||||
}
|
||||
|
||||
// PruneNetworks prunes networks
|
||||
func (c *DockerCommand) PruneNetworks() error {
|
||||
_, err := c.Client.NetworksPrune(context.Background(), filters.Args{})
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove removes the network
|
||||
func (v *Network) Remove() error {
|
||||
return v.Client.NetworkRemove(context.Background(), v.Name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import (
|
|||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/jesseduffield/kill"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,355 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestOSCommandRunCommandWithOutput is a function.
|
||||
func TestOSCommandRunCommandWithOutput(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"echo -n '123'",
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "123", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"rmdir unexisting-folder",
|
||||
func(output string, err error) {
|
||||
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.test(NewDummyOSCommand().RunCommandWithOutput(s.command))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandRunCommand is a function.
|
||||
func TestOSCommandRunCommand(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"rmdir unexisting-folder",
|
||||
func(err error) {
|
||||
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.test(NewDummyOSCommand().RunCommand(s.command))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandEditFile is a function.
|
||||
func TestOSCommandEditFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getenv func(string) string
|
||||
test func(*exec.Cmd, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return exec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.EqualError(t, err, "No editor defined in $VISUAL or $EDITOR")
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "nano", name)
|
||||
|
||||
return exec.Command("exit", "0")
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "VISUAL" {
|
||||
return "nano"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "emacs", name)
|
||||
|
||||
return exec.Command("exit", "0")
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "EDITOR" {
|
||||
return "emacs"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "vi", name)
|
||||
|
||||
return exec.Command("exit", "0")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.command = s.command
|
||||
OSCmd.getenv = s.getenv
|
||||
|
||||
s.test(OSCmd.EditFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandQuote(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
osCommand.Platform.os = "linux"
|
||||
|
||||
actual := osCommand.Quote("hello `test`")
|
||||
|
||||
expected := "\"hello \\`test\\`\""
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandQuoteSingleQuote tests the quote function with ' quotes explicitly for Linux
|
||||
func TestOSCommandQuoteSingleQuote(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
osCommand.Platform.os = "linux"
|
||||
|
||||
actual := osCommand.Quote("hello 'test'")
|
||||
|
||||
expected := `"hello 'test'"`
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandQuoteDoubleQuote tests the quote function with " quotes explicitly for Linux
|
||||
func TestOSCommandQuoteDoubleQuote(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
osCommand.Platform.os = "linux"
|
||||
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := `"hello \"test\""`
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandQuoteWindows tests the quote function for Windows
|
||||
func TestOSCommandQuoteWindows(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
osCommand.Platform.os = "windows"
|
||||
|
||||
actual := osCommand.Quote(`hello "test" 'test2'`)
|
||||
|
||||
expected := `\"hello "'"'"test"'"'" 'test2'\"`
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandUnquote is a function.
|
||||
func TestOSCommandUnquote(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
actual := osCommand.Unquote(`hello "test"`)
|
||||
|
||||
expected := "hello test"
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandFileType is a function.
|
||||
func TestOSCommandFileType(t *testing.T) {
|
||||
type scenario struct {
|
||||
path string
|
||||
setup func()
|
||||
test func(string)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"testFile",
|
||||
func() {
|
||||
if _, err := os.Create("testFile"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "file", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"file with spaces",
|
||||
func() {
|
||||
if _, err := os.Create("file with spaces"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "file", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"testDirectory",
|
||||
func() {
|
||||
if err := os.Mkdir("testDirectory", 0o644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "directory", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"nonExistant",
|
||||
func() {},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "other", output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.setup()
|
||||
s.test(NewDummyOSCommand().FileType(s.path))
|
||||
_ = os.RemoveAll(s.path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandCreateTempFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filename string
|
||||
content string
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"filename",
|
||||
"content",
|
||||
func(path string, err error) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "content", string(content))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(NewDummyOSCommand().CreateTempFile(s.filename, s.content))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandExecutableFromStringWithShellLinux(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
osCommand.Platform.os = "linux"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
commandStr string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"success",
|
||||
"pwd",
|
||||
fmt.Sprintf("%v %v %v", osCommand.Platform.shell, osCommand.Platform.shellArg, "\"pwd\""),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := osCommand.NewCommandStringWithShell(tt.commandStr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandNewCommandStringWithShellWindows(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
osCommand.Platform.os = "windows"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
commandStr string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"success",
|
||||
"pwd",
|
||||
fmt.Sprintf("%v %v %v", osCommand.Platform.shell, osCommand.Platform.shellArg, "pwd"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := osCommand.NewCommandStringWithShell(tt.commandStr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package commands
|
||||
|
||||
type Project struct {
|
||||
Name string
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Service : A docker Service
|
||||
type Service struct {
|
||||
Name string
|
||||
ID string
|
||||
ProjectName string
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
Container *Container
|
||||
DockerCommand LimitedDockerCommand
|
||||
}
|
||||
|
||||
// Remove removes the service's containers
|
||||
func (s *Service) Remove(options container.RemoveOptions) error {
|
||||
return s.Container.Remove(options)
|
||||
}
|
||||
|
||||
// Stop stops the service's containers
|
||||
func (s *Service) Stop() error {
|
||||
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StopService)
|
||||
}
|
||||
|
||||
// Up up's the service
|
||||
func (s *Service) Up() error {
|
||||
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.UpService)
|
||||
}
|
||||
|
||||
// Restart restarts the service
|
||||
func (s *Service) Restart() error {
|
||||
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.RestartService)
|
||||
}
|
||||
|
||||
// Start starts the service
|
||||
func (s *Service) Start() error {
|
||||
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StartService)
|
||||
}
|
||||
|
||||
func (s *Service) runCommand(templateCmdStr string) error {
|
||||
command := utils.ApplyTemplate(
|
||||
templateCmdStr,
|
||||
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
|
||||
)
|
||||
return s.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// Attach attaches to the service
|
||||
func (s *Service) Attach() (*exec.Cmd, error) {
|
||||
return s.Container.Attach()
|
||||
}
|
||||
|
||||
// ViewLogs attaches to a subprocess viewing the service's logs
|
||||
func (s *Service) ViewLogs() (*exec.Cmd, error) {
|
||||
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ViewServiceLogs
|
||||
command := utils.ApplyTemplate(
|
||||
templateString,
|
||||
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
|
||||
)
|
||||
|
||||
cmd := s.OSCommand.ExecutableFromString(command)
|
||||
s.OSCommand.PrepareForChildren(cmd)
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// RenderTop renders the process list of the service
|
||||
func (s *Service) RenderTop(ctx context.Context) (string, error) {
|
||||
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ServiceTop
|
||||
command := utils.ApplyTemplate(
|
||||
templateString,
|
||||
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
|
||||
)
|
||||
|
||||
return s.OSCommand.RunCommandWithOutputContext(ctx, command)
|
||||
}
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
// we only need these two methods from our OSCommand struct, for killing commands
|
||||
type CmdKiller interface {
|
||||
Kill(cmd *exec.Cmd) error
|
||||
PrepareForChildren(cmd *exec.Cmd)
|
||||
}
|
||||
|
||||
type SSHHandler struct {
|
||||
oSCommand CmdKiller
|
||||
|
||||
dialContext func(ctx context.Context, network, addr string) (io.Closer, error)
|
||||
startCmd func(*exec.Cmd) error
|
||||
tempDir func(dir string, pattern string) (name string, err error)
|
||||
getenv func(key string) string
|
||||
setenv func(key, value string) error
|
||||
}
|
||||
|
||||
func NewSSHHandler(oSCommand CmdKiller) *SSHHandler {
|
||||
return &SSHHandler{
|
||||
oSCommand: oSCommand,
|
||||
|
||||
dialContext: func(ctx context.Context, network, addr string) (io.Closer, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, network, addr)
|
||||
},
|
||||
startCmd: func(cmd *exec.Cmd) error { return cmd.Start() },
|
||||
tempDir: os.MkdirTemp,
|
||||
getenv: os.Getenv,
|
||||
setenv: os.Setenv,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSSHDockerHost overrides the DOCKER_HOST environment variable
|
||||
// to point towards a local unix socket tunneled over SSH to the specified ssh host.
|
||||
func (self *SSHHandler) HandleSSHDockerHost() (io.Closer, error) {
|
||||
const key = "DOCKER_HOST"
|
||||
ctx := context.Background()
|
||||
u, err := url.Parse(self.getenv(key))
|
||||
if err != nil {
|
||||
// if no or an invalid docker host is specified, continue nominally
|
||||
return noopCloser{}, nil
|
||||
}
|
||||
|
||||
// if the docker host scheme is "ssh", forward the docker socket before creating the client
|
||||
if u.Scheme == "ssh" {
|
||||
tunnel, err := self.createDockerHostTunnel(ctx, u.Host)
|
||||
if err != nil {
|
||||
return noopCloser{}, fmt.Errorf("tunnel ssh docker host: %w", err)
|
||||
}
|
||||
err = self.setenv(key, tunnel.socketPath)
|
||||
if err != nil {
|
||||
return noopCloser{}, fmt.Errorf("override DOCKER_HOST to tunneled socket: %w", err)
|
||||
}
|
||||
|
||||
return tunnel, nil
|
||||
}
|
||||
return noopCloser{}, nil
|
||||
}
|
||||
|
||||
type noopCloser struct{}
|
||||
|
||||
func (noopCloser) Close() error { return nil }
|
||||
|
||||
type tunneledDockerHost struct {
|
||||
socketPath string
|
||||
cmd *exec.Cmd
|
||||
oSCommand CmdKiller
|
||||
}
|
||||
|
||||
var _ io.Closer = (*tunneledDockerHost)(nil)
|
||||
|
||||
func (t *tunneledDockerHost) Close() error {
|
||||
return t.oSCommand.Kill(t.cmd)
|
||||
}
|
||||
|
||||
func (self *SSHHandler) createDockerHostTunnel(ctx context.Context, remoteHost string) (*tunneledDockerHost, error) {
|
||||
socketDir, err := self.tempDir("/tmp", "lazydocker-sshtunnel-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create ssh tunnel tmp file: %w", err)
|
||||
}
|
||||
localSocket := path.Join(socketDir, "dockerhost.sock")
|
||||
|
||||
cmd, err := self.tunnelSSH(ctx, remoteHost, localSocket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tunnel docker host over ssh: %w", err)
|
||||
}
|
||||
|
||||
// set a reasonable timeout, then wait for the socket to dial successfully
|
||||
// before attempting to create a new docker client
|
||||
const socketTunnelTimeout = 8 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, socketTunnelTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = self.retrySocketDial(ctx, localSocket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh tunneled socket never became available: %w", err)
|
||||
}
|
||||
|
||||
// construct the new DOCKER_HOST url with the proper scheme
|
||||
newDockerHostURL := url.URL{Scheme: "unix", Path: localSocket}
|
||||
return &tunneledDockerHost{
|
||||
socketPath: newDockerHostURL.String(),
|
||||
cmd: cmd,
|
||||
oSCommand: self.oSCommand,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Attempt to dial the socket until it becomes available.
|
||||
// The retry loop will continue until the parent context is canceled.
|
||||
func (self *SSHHandler) retrySocketDial(ctx context.Context, socketPath string) error {
|
||||
t := time.NewTicker(1 * time.Second)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-t.C:
|
||||
}
|
||||
// attempt to dial the socket, exit on success
|
||||
err := self.tryDial(ctx, socketPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to dial the specified unix socket, immediately close the connection if successfully created.
|
||||
func (self *SSHHandler) tryDial(ctx context.Context, socketPath string) error {
|
||||
conn, err := self.dialContext(ctx, "unix", socketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SSHHandler) tunnelSSH(ctx context.Context, host, localSocket string) (*exec.Cmd, error) {
|
||||
cmd := exec.CommandContext(ctx, "ssh", "-L", localSocket+":/var/run/docker.sock", host, "-N")
|
||||
self.oSCommand.PrepareForChildren(cmd)
|
||||
err := self.startCmd(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSSHHandlerHandleSSHDockerHost(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
envVarValue string
|
||||
expectedDialContextCount int
|
||||
expectedStartCmdCount int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "No env var set",
|
||||
envVarValue: "",
|
||||
expectedDialContextCount: 0,
|
||||
expectedStartCmdCount: 0,
|
||||
},
|
||||
{
|
||||
testName: "Env var set with https scheme",
|
||||
envVarValue: "https://myhost.com",
|
||||
expectedStartCmdCount: 0,
|
||||
expectedDialContextCount: 0,
|
||||
},
|
||||
{
|
||||
testName: "Env var set with ssh scheme",
|
||||
envVarValue: "ssh://myhost@192.168.5.178",
|
||||
expectedStartCmdCount: 1,
|
||||
expectedDialContextCount: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
getenv := func(key string) string {
|
||||
if key != "DOCKER_HOST" {
|
||||
t.Errorf("Expected key to be DOCKER_HOST, got %s", key)
|
||||
}
|
||||
|
||||
return s.envVarValue
|
||||
}
|
||||
|
||||
tempDir := func(dir string, pattern string) (string, error) {
|
||||
assert.Equal(t, "/tmp", dir)
|
||||
assert.Equal(t, "lazydocker-sshtunnel-", pattern)
|
||||
|
||||
return "/tmp/lazydocker-ssh-tunnel-12345", nil
|
||||
}
|
||||
|
||||
setenv := func(key, value string) error {
|
||||
assert.Equal(t, "DOCKER_HOST", key)
|
||||
assert.Equal(t, "unix:///tmp/lazydocker-ssh-tunnel-12345/dockerhost.sock", value)
|
||||
return nil
|
||||
}
|
||||
|
||||
startCmdCount := 0
|
||||
startCmd := func(cmd *exec.Cmd) error {
|
||||
assert.EqualValues(t, []string{"ssh", "-L", "/tmp/lazydocker-ssh-tunnel-12345/dockerhost.sock:/var/run/docker.sock", "192.168.5.178", "-N"}, cmd.Args)
|
||||
|
||||
startCmdCount++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
dialContextCount := 0
|
||||
dialContext := func(ctx context.Context, network string, address string) (io.Closer, error) {
|
||||
assert.Equal(t, "unix", network)
|
||||
assert.Equal(t, "/tmp/lazydocker-ssh-tunnel-12345/dockerhost.sock", address)
|
||||
|
||||
dialContextCount++
|
||||
|
||||
return noopCloser{}, nil
|
||||
}
|
||||
|
||||
handler := &SSHHandler{
|
||||
oSCommand: &fakeCmdKiller{},
|
||||
|
||||
dialContext: dialContext,
|
||||
startCmd: startCmd,
|
||||
tempDir: tempDir,
|
||||
getenv: getenv,
|
||||
setenv: setenv,
|
||||
}
|
||||
|
||||
_, err := handler.HandleSSHDockerHost()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, s.expectedDialContextCount, dialContextCount)
|
||||
assert.Equal(t, s.expectedStartCmdCount, startCmdCount)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeCmdKiller struct{}
|
||||
|
||||
func (self *fakeCmdKiller) Kill(cmd *exec.Cmd) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *fakeCmdKiller) PrepareForChildren(cmd *exec.Cmd) {}
|
||||
|
|
@ -1,56 +1,39 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Volume : A docker Volume
|
||||
type Volume struct {
|
||||
Name string
|
||||
Volume *volume.Volume
|
||||
Client *client.Client
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
DockerCommand LimitedDockerCommand
|
||||
Name string
|
||||
AppleVolume AppleVolume
|
||||
Client *ContainerClient
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
ContainerCmd LimitedContainerCommand
|
||||
}
|
||||
|
||||
// RefreshVolumes gets the volumes and stores them
|
||||
func (c *DockerCommand) RefreshVolumes() ([]*Volume, error) {
|
||||
result, err := c.Client.VolumeList(context.Background(), volume.ListOptions{})
|
||||
func (v *Volume) Remove() error {
|
||||
return v.Client.RemoveVolume(v.Name)
|
||||
}
|
||||
|
||||
func (c *ContainerClient) RefreshVolumes() ([]*Volume, error) {
|
||||
appleVolumes, err := c.ListVolumes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
volumes := result.Volumes
|
||||
ownVolumes := make([]*Volume, len(appleVolumes))
|
||||
|
||||
ownVolumes := make([]*Volume, len(volumes))
|
||||
|
||||
for i, vol := range volumes {
|
||||
for i, vol := range appleVolumes {
|
||||
ownVolumes[i] = &Volume{
|
||||
Name: vol.Name,
|
||||
Volume: vol,
|
||||
Client: c.Client,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
DockerCommand: c,
|
||||
Name: vol.Name,
|
||||
AppleVolume: vol,
|
||||
Client: c,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
}
|
||||
}
|
||||
|
||||
return ownVolumes, nil
|
||||
}
|
||||
|
||||
// PruneVolumes prunes volumes
|
||||
func (c *DockerCommand) PruneVolumes() error {
|
||||
_, err := c.Client.VolumesPrune(context.Background(), filters.Args{})
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove removes the volume
|
||||
func (v *Volume) Remove(force bool) error {
|
||||
return v.Client.VolumeRemove(context.Background(), v.Name, force)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +1,26 @@
|
|||
// Package config handles all the user-configuration. The fields here are
|
||||
// all in PascalCase but in your actual config.yml they'll be in camelCase.
|
||||
// You can view the default config with `lazydocker --config`.
|
||||
// You can open your config file by going to the status panel (using left-arrow)
|
||||
// and pressing 'o'.
|
||||
// You can directly edit the file (e.g. in vim) by pressing 'e' instead.
|
||||
// To see the final config after your user-specific options have been merged
|
||||
// with the defaults, go to the 'about' tab in the status panel.
|
||||
// Because of the way we merge your user config with the defaults you may need
|
||||
// to be careful: if for example you set a `commandTemplates:` yaml key but then
|
||||
// give it no child values, it will scrap all of the defaults and the app will
|
||||
// probably crash.
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/OpenPeeDeeP/xdg"
|
||||
"github.com/jesseduffield/yaml"
|
||||
)
|
||||
|
||||
// UserConfig holds all of the user-configurable options
|
||||
type UserConfig struct {
|
||||
// Gui is for configuring visual things like colors and whether we show or
|
||||
// hide things
|
||||
Gui GuiConfig `yaml:"gui,omitempty"`
|
||||
|
||||
// ConfirmOnQuit when enabled prompts you to confirm you want to quit when you
|
||||
// hit esc or q when no confirmation panels are open
|
||||
ConfirmOnQuit bool `yaml:"confirmOnQuit,omitempty"`
|
||||
|
||||
// Logs determines how we render/filter a container's logs
|
||||
Logs LogsConfig `yaml:"logs,omitempty"`
|
||||
|
||||
// CommandTemplates determines what commands actually get called when we run
|
||||
// certain commands
|
||||
CommandTemplates CommandTemplatesConfig `yaml:"commandTemplates,omitempty"`
|
||||
|
||||
// CustomCommands determines what shows up in your custom commands menu when
|
||||
// you press 'c'. You can use go templates to access three items on the
|
||||
// struct: the DockerCompose command (defaulted to 'docker-compose'), the
|
||||
// Service if present, and the Container if present. The struct types for
|
||||
// those are found in the commands package
|
||||
Gui GuiConfig `yaml:"gui,omitempty"`
|
||||
ConfirmOnQuit bool `yaml:"confirmOnQuit,omitempty"`
|
||||
Logs LogsConfig `yaml:"logs,omitempty"`
|
||||
CustomCommands CustomCommands `yaml:"customCommands,omitempty"`
|
||||
|
||||
// BulkCommands are commands that apply to all items in a panel e.g.
|
||||
// killing all containers, stopping all services, or pruning all images
|
||||
BulkCommands CustomCommands `yaml:"bulkCommands,omitempty"`
|
||||
|
||||
// OS determines what defaults are set for opening files and links
|
||||
OS OSConfig `yaml:"oS,omitempty"`
|
||||
|
||||
// Stats determines how long lazydocker will gather container stats for, and
|
||||
// what stat info to graph
|
||||
Stats StatsConfig `yaml:"stats,omitempty"`
|
||||
|
||||
// Replacements determines how we render an item's info
|
||||
Replacements Replacements `yaml:"replacements,omitempty"`
|
||||
|
||||
// For demo purposes: any list item with one of these strings as a substring
|
||||
// will be filtered out and not displayed.
|
||||
// Not documented because it's subject to change
|
||||
Ignore []string `yaml:"ignore,omitempty"`
|
||||
BulkCommands CustomCommands `yaml:"bulkCommands,omitempty"`
|
||||
OS OSConfig `yaml:"oS,omitempty"`
|
||||
Stats StatsConfig `yaml:"stats,omitempty"`
|
||||
Replacements Replacements `yaml:"replacements,omitempty"`
|
||||
Ignore []string `yaml:"ignore,omitempty"`
|
||||
}
|
||||
|
||||
// ThemeConfig is for setting the colors of panels and some text.
|
||||
type ThemeConfig struct {
|
||||
ActiveBorderColor []string `yaml:"activeBorderColor,omitempty"`
|
||||
InactiveBorderColor []string `yaml:"inactiveBorderColor,omitempty"`
|
||||
|
|
@ -74,269 +28,56 @@ type ThemeConfig struct {
|
|||
OptionsTextColor []string `yaml:"optionsTextColor,omitempty"`
|
||||
}
|
||||
|
||||
// GuiConfig is for configuring visual things like colors and whether we show or
|
||||
// hide things
|
||||
type GuiConfig struct {
|
||||
// ScrollHeight determines how many characters you scroll at a time when
|
||||
// scrolling the main panel
|
||||
ScrollHeight int `yaml:"scrollHeight,omitempty"`
|
||||
|
||||
// Language determines which language the GUI displayed.
|
||||
Language string `yaml:"language,omitempty"`
|
||||
|
||||
// ScrollPastBottom determines whether you can scroll past the bottom of the
|
||||
// main view
|
||||
ScrollPastBottom bool `yaml:"scrollPastBottom,omitempty"`
|
||||
|
||||
// IgnoreMouseEvents is for when you do not want to use your mouse to interact
|
||||
// with anything
|
||||
IgnoreMouseEvents bool `yaml:"mouseEvents,omitempty"`
|
||||
|
||||
// Theme determines what colors and color attributes your panel borders have.
|
||||
// I always set inactiveBorderColor to black because in my terminal it's more
|
||||
// of a grey, but that doesn't work in your average terminal. I highly
|
||||
// recommended finding a combination that works for you
|
||||
Theme ThemeConfig `yaml:"theme,omitempty"`
|
||||
|
||||
// ShowAllContainers determines whether the Containers panel contains all the
|
||||
// containers returned by `docker ps -a`, or just those containers that aren't
|
||||
// directly linked to a service. It is probably desirable to enable this if
|
||||
// you have multiple containers per service, but otherwise it can cause a lot
|
||||
// of clutter
|
||||
ShowAllContainers bool `yaml:"showAllContainers,omitempty"`
|
||||
|
||||
// ReturnImmediately determines whether you get the 'press enter to return to
|
||||
// lazydocker' message after a subprocess has completed. You would set this to
|
||||
// true if you often want to see the output of subprocesses before returning
|
||||
// to lazydocker. I would default this to false but then people who want it
|
||||
// set to true won't even know the config option exists.
|
||||
ReturnImmediately bool `yaml:"returnImmediately,omitempty"`
|
||||
|
||||
// WrapMainPanel determines whether we use word wrap on the main panel
|
||||
WrapMainPanel bool `yaml:"wrapMainPanel,omitempty"`
|
||||
|
||||
// LegacySortContainers determines if containers should be sorted using legacy approach.
|
||||
// By default, containers are now sorted by status. This setting allows users to
|
||||
// use legacy behaviour instead.
|
||||
LegacySortContainers bool `yaml:"legacySortContainers,omitempty"`
|
||||
|
||||
// If 0.333, then the side panels will be 1/3 of the screen's width
|
||||
SidePanelWidth float64 `yaml:"sidePanelWidth"`
|
||||
|
||||
// Determines whether we show the bottom line (the one containing keybinding
|
||||
// info and the status of the app).
|
||||
ShowBottomLine bool `yaml:"showBottomLine"`
|
||||
|
||||
// When true, increases vertical space used by focused side panel,
|
||||
// creating an accordion effect
|
||||
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||
|
||||
// ScreenMode allow user to specify which screen mode will be used on startup
|
||||
ScreenMode string `yaml:"screenMode,omitempty"`
|
||||
|
||||
// Determines the style of the container status and container health display in the
|
||||
// containers panel. "long": full words (default), "short": one or two characters,
|
||||
// "icon": unicode emoji.
|
||||
ContainerStatusHealthStyle string `yaml:"containerStatusHealthStyle"`
|
||||
|
||||
// Window border style.
|
||||
// One of 'rounded' (default) | 'single' | 'double' | 'hidden'
|
||||
Border string `yaml:"border"`
|
||||
ScrollHeight int `yaml:"scrollHeight,omitempty"`
|
||||
Language string `yaml:"language,omitempty"`
|
||||
ScrollPastBottom bool `yaml:"scrollPastBottom,omitempty"`
|
||||
IgnoreMouseEvents bool `yaml:"mouseEvents,omitempty"`
|
||||
Theme ThemeConfig `yaml:"theme,omitempty"`
|
||||
ShowAllContainers bool `yaml:"showAllContainers,omitempty"`
|
||||
ReturnImmediately bool `yaml:"returnImmediately,omitempty"`
|
||||
WrapMainPanel bool `yaml:"wrapMainPanel,omitempty"`
|
||||
LegacySortContainers bool `yaml:"legacySortContainers,omitempty"`
|
||||
SidePanelWidth float64 `yaml:"sidePanelWidth"`
|
||||
ShowBottomLine bool `yaml:"showBottomLine"`
|
||||
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||
ScreenMode string `yaml:"screenMode,omitempty"`
|
||||
ContainerStatusHealthStyle string `yaml:"containerStatusHealthStyle"`
|
||||
Border string `yaml:"border"`
|
||||
}
|
||||
|
||||
// CommandTemplatesConfig determines what commands actually get called when we
|
||||
// run certain commands
|
||||
type CommandTemplatesConfig struct {
|
||||
// RestartService is for restarting a service. docker-compose restart {{
|
||||
// .Service.Name }} works but I prefer docker-compose up --force-recreate {{
|
||||
// .Service.Name }}
|
||||
RestartService string `yaml:"restartService,omitempty"`
|
||||
|
||||
// StartService is just like the above but for starting
|
||||
StartService string `yaml:"startService,omitempty"`
|
||||
|
||||
// UpService ups the service (creates and starts)
|
||||
UpService string `yaml:"upService,omitempty"`
|
||||
|
||||
// Runs "docker-compose up -d"
|
||||
Up string `yaml:"up,omitempty"`
|
||||
|
||||
// downs everything
|
||||
Down string `yaml:"down,omitempty"`
|
||||
// downs and removes volumes
|
||||
DownWithVolumes string `yaml:"downWithVolumes,omitempty"`
|
||||
|
||||
// DockerCompose is for your docker-compose command. You may want to combine a
|
||||
// few different docker-compose.yml files together, in which case you can set
|
||||
// this to "docker compose -f foo/docker-compose.yml -f
|
||||
// bah/docker-compose.yml". The reason that the other docker-compose command
|
||||
// templates all start with {{ .DockerCompose }} is so that they can make use
|
||||
// of whatever you've set in this value rather than you having to copy and
|
||||
// paste it to all the other commands
|
||||
DockerCompose string `yaml:"dockerCompose,omitempty"`
|
||||
|
||||
// StopService is the command for stopping a service
|
||||
StopService string `yaml:"stopService,omitempty"`
|
||||
|
||||
// ServiceLogs get the logs for a service. This is actually not currently
|
||||
// used; we just get the logs of the corresponding container. But we should
|
||||
// probably support explicitly returning the logs of the service when you've
|
||||
// selected the service, given that a service may have multiple containers.
|
||||
ServiceLogs string `yaml:"serviceLogs,omitempty"`
|
||||
|
||||
// ViewServiceLogs is for when you want to view the logs of a service as a
|
||||
// subprocess. This defaults to having no filter, unlike the in-app logs
|
||||
// commands which will usually filter down to the last hour for the sake of
|
||||
// performance.
|
||||
ViewServiceLogs string `yaml:"viewServiceLogs,omitempty"`
|
||||
|
||||
// RebuildService is the command for rebuilding a service. Defaults to
|
||||
// something along the lines of `{{ .DockerCompose }} up --build {{
|
||||
// .Service.Name }}`
|
||||
RebuildService string `yaml:"rebuildService,omitempty"`
|
||||
|
||||
// RecreateService is for force-recreating a service. I prefer this to
|
||||
// restarting a service because it will also restart any dependent services
|
||||
// and ensure they're running before trying to run the service at hand
|
||||
RecreateService string `yaml:"recreateService,omitempty"`
|
||||
|
||||
// AllLogs is for showing what you get from doing `docker compose logs`. It
|
||||
// combines all the logs together
|
||||
AllLogs string `yaml:"allLogs,omitempty"`
|
||||
|
||||
// ViewAllLogs is the command we use when you want to see all logs in a subprocess with no filtering
|
||||
ViewAllLogs string `yaml:"viewAlLogs,omitempty"`
|
||||
|
||||
// DockerComposeConfig is the command for viewing the config of your docker
|
||||
// compose. It basically prints out the yaml from your docker-compose.yml
|
||||
// file(s)
|
||||
DockerComposeConfig string `yaml:"dockerComposeConfig,omitempty"`
|
||||
|
||||
// CheckDockerComposeConfig is what we use to check whether we are in a
|
||||
// docker-compose context. If the command returns an error then we clearly
|
||||
// aren't in a docker-compose config and we then just hide the services panel
|
||||
// and only show containers
|
||||
CheckDockerComposeConfig string `yaml:"checkDockerComposeConfig,omitempty"`
|
||||
|
||||
// ServiceTop is the command for viewing the processes under a given service
|
||||
ServiceTop string `yaml:"serviceTop,omitempty"`
|
||||
}
|
||||
|
||||
// OSConfig contains config on the level of the os
|
||||
type OSConfig struct {
|
||||
// OpenCommand is the command for opening a file
|
||||
OpenCommand string `yaml:"openCommand,omitempty"`
|
||||
|
||||
// OpenCommand is the command for opening a link
|
||||
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
|
||||
}
|
||||
|
||||
// GraphConfig specifies how to make a graph of recorded container stats
|
||||
type GraphConfig struct {
|
||||
// Min sets the minimum value that you want to display. If you want to set
|
||||
// this, you should also set MinType to "static". The reason for this is that
|
||||
// if Min == 0, it's not clear if it has not been set (given that the
|
||||
// zero-value of an int is 0) or if it's intentionally been set to 0.
|
||||
Min float64 `yaml:"min,omitempty"`
|
||||
|
||||
// Max sets the maximum value that you want to display. If you want to set
|
||||
// this, you should also set MaxType to "static". The reason for this is that
|
||||
// if Max == 0, it's not clear if it has not been set (given that the
|
||||
// zero-value of an int is 0) or if it's intentionally been set to 0.
|
||||
Max float64 `yaml:"max,omitempty"`
|
||||
|
||||
// Height sets the height of the graph in ascii characters
|
||||
Height int `yaml:"height,omitempty"`
|
||||
|
||||
// Caption sets the caption of the graph. If you want to show CPU Percentage
|
||||
// you could set this to "CPU (%)"
|
||||
Caption string `yaml:"caption,omitempty"`
|
||||
|
||||
// This is the path to the stat that you want to display. It is based on the
|
||||
// RecordedStats struct in container_stats.go, so feel free to look there to
|
||||
// see all the options available. Alternatively if you go into lazydocker and
|
||||
// go to the stats tab, you'll see that same struct in JSON format, so you can
|
||||
// just PascalCase the path and you'll have a valid path. E.g.
|
||||
// ClientStats.blkio_stats -> "ClientStats.BlkioStats"
|
||||
StatPath string `yaml:"statPath,omitempty"`
|
||||
|
||||
// This determines the color of the graph. This can be any color attribute,
|
||||
// e.g. 'blue', 'green'
|
||||
Color string `yaml:"color,omitempty"`
|
||||
|
||||
// MinType and MaxType are each one of "", "static". blank means the min/max
|
||||
// of the data set will be used. "static" means the min/max specified will be
|
||||
// used
|
||||
MinType string `yaml:"minType,omitempty"`
|
||||
|
||||
// MaxType is just like MinType but for the max value
|
||||
MaxType string `yaml:"maxType,omitempty"`
|
||||
Min float64 `yaml:"min,omitempty"`
|
||||
Max float64 `yaml:"max,omitempty"`
|
||||
Height int `yaml:"height,omitempty"`
|
||||
Caption string `yaml:"caption,omitempty"`
|
||||
StatPath string `yaml:"statPath,omitempty"`
|
||||
Color string `yaml:"color,omitempty"`
|
||||
MinType string `yaml:"minType,omitempty"`
|
||||
MaxType string `yaml:"maxType,omitempty"`
|
||||
}
|
||||
|
||||
// StatsConfig contains the stuff relating to stats and graphs
|
||||
type StatsConfig struct {
|
||||
// Graphs contains the configuration for the stats graphs we want to show in
|
||||
// the app
|
||||
Graphs []GraphConfig
|
||||
|
||||
// MaxDuration tells us how long to collect stats for. Currently this defaults
|
||||
// to "5m" i.e. 5 minutes.
|
||||
Graphs []GraphConfig `yaml:"graphs"`
|
||||
MaxDuration time.Duration `yaml:"maxDuration,omitempty"`
|
||||
}
|
||||
|
||||
// CustomCommands contains the custom commands that you might want to use on any
|
||||
// given service or container
|
||||
type CustomCommands struct {
|
||||
// Containers contains the custom commands for containers
|
||||
Containers []CustomCommand `yaml:"containers,omitempty"`
|
||||
|
||||
// Services contains the custom commands for services
|
||||
Services []CustomCommand `yaml:"services,omitempty"`
|
||||
|
||||
// Images contains the custom commands for images
|
||||
Images []CustomCommand `yaml:"images,omitempty"`
|
||||
|
||||
// Volumes contains the custom commands for volumes
|
||||
Volumes []CustomCommand `yaml:"volumes,omitempty"`
|
||||
|
||||
// Networks contains the custom commands for networks
|
||||
Networks []CustomCommand `yaml:"networks,omitempty"`
|
||||
Images []CustomCommand `yaml:"images,omitempty"`
|
||||
Volumes []CustomCommand `yaml:"volumes,omitempty"`
|
||||
Networks []CustomCommand `yaml:"networks,omitempty"`
|
||||
}
|
||||
|
||||
// Replacements contains the stuff relating to rendering a container's info
|
||||
type Replacements struct {
|
||||
// ImageNamePrefixes tells us how to replace a prefix in the Docker image name
|
||||
ImageNamePrefixes map[string]string `yaml:"imageNamePrefixes,omitempty"`
|
||||
}
|
||||
|
||||
// CustomCommand is a template for a command we want to run against a service or
|
||||
// container
|
||||
type CustomCommand struct {
|
||||
// Name is the name of the command, purely for visual display
|
||||
Name string `yaml:"name"`
|
||||
|
||||
// Attach tells us whether to switch to a subprocess to interact with the
|
||||
// called program, or just read its output. If Attach is set to false, the
|
||||
// command will run in the background. I'm open to the idea of having a third
|
||||
// option where the output plays in the main panel.
|
||||
Attach bool `yaml:"attach"`
|
||||
|
||||
// Shell indicates whether to invoke the Command on a shell or not.
|
||||
// Example of a bash invoked command: `/bin/bash -c "{Command}".
|
||||
Shell bool `yaml:"shell"`
|
||||
|
||||
// Command is the command we want to run. We can use the go templates here as
|
||||
// well. One example might be `{{ .DockerCompose }} exec {{ .Service.Name }}
|
||||
// /bin/sh`
|
||||
Command string `yaml:"command"`
|
||||
|
||||
// ServiceNames is used to restrict this command to just one or more services.
|
||||
// An example might be 'rails migrate' for your rails api service(s). This
|
||||
// field has no effect on customcommands under the 'communications' part of
|
||||
// the customCommand config.
|
||||
ServiceNames []string `yaml:"serviceNames"`
|
||||
|
||||
// InternalFunction is the name of a function inside lazydocker that we want to run, as opposed to a command-line command. This is only used internally and can't be configured by the user
|
||||
Name string `yaml:"name"`
|
||||
Attach bool `yaml:"attach"`
|
||||
Shell bool `yaml:"shell"`
|
||||
Command string `yaml:"command"`
|
||||
InternalFunction func() error `yaml:"-"`
|
||||
}
|
||||
|
||||
|
|
@ -346,10 +87,11 @@ type LogsConfig struct {
|
|||
Tail string `yaml:"tail,omitempty"`
|
||||
}
|
||||
|
||||
// GetDefaultConfig returns the application default configuration NOTE (to
|
||||
// contributors, not users): do not default a boolean to true, because false is
|
||||
// the boolean zero value and this will be ignored when parsing the user's
|
||||
// config
|
||||
type OSConfig struct {
|
||||
OpenCommand string `yaml:"openCommand,omitempty"`
|
||||
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
|
||||
}
|
||||
|
||||
func GetDefaultConfig() UserConfig {
|
||||
duration, err := time.ParseDuration("3m")
|
||||
if err != nil {
|
||||
|
|
@ -368,11 +110,11 @@ func GetDefaultConfig() UserConfig {
|
|||
SelectedLineBgColor: []string{"blue"},
|
||||
OptionsTextColor: []string{"blue"},
|
||||
},
|
||||
ShowAllContainers: false,
|
||||
ShowAllContainers: true,
|
||||
ReturnImmediately: false,
|
||||
WrapMainPanel: true,
|
||||
LegacySortContainers: false,
|
||||
SidePanelWidth: 0.3333,
|
||||
SidePanelWidth: 0.25,
|
||||
ShowBottomLine: true,
|
||||
ExpandFocusedSidePanel: false,
|
||||
ScreenMode: "normal",
|
||||
|
|
@ -384,76 +126,41 @@ func GetDefaultConfig() UserConfig {
|
|||
Since: "60m",
|
||||
Tail: "",
|
||||
},
|
||||
CommandTemplates: CommandTemplatesConfig{
|
||||
DockerCompose: "docker compose",
|
||||
RestartService: "{{ .DockerCompose }} restart {{ .Service.Name }}",
|
||||
StartService: "{{ .DockerCompose }} start {{ .Service.Name }}",
|
||||
Up: "{{ .DockerCompose }} up -d",
|
||||
Down: "{{ .DockerCompose }} down",
|
||||
DownWithVolumes: "{{ .DockerCompose }} down --volumes",
|
||||
UpService: "{{ .DockerCompose }} up -d {{ .Service.Name }}",
|
||||
RebuildService: "{{ .DockerCompose }} up -d --build {{ .Service.Name }}",
|
||||
RecreateService: "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}",
|
||||
StopService: "{{ .DockerCompose }} stop {{ .Service.Name }}",
|
||||
ServiceLogs: "{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}",
|
||||
ViewServiceLogs: "{{ .DockerCompose }} logs --follow {{ .Service.Name }}",
|
||||
AllLogs: "{{ .DockerCompose }} logs --tail=300 --follow",
|
||||
ViewAllLogs: "{{ .DockerCompose }} logs",
|
||||
DockerComposeConfig: "{{ .DockerCompose }} config",
|
||||
CheckDockerComposeConfig: "{{ .DockerCompose }} config --quiet",
|
||||
ServiceTop: "{{ .DockerCompose }} top {{ .Service.Name }}",
|
||||
},
|
||||
CustomCommands: CustomCommands{
|
||||
Containers: []CustomCommand{},
|
||||
Services: []CustomCommand{},
|
||||
Images: []CustomCommand{},
|
||||
Volumes: []CustomCommand{},
|
||||
Networks: []CustomCommand{},
|
||||
},
|
||||
BulkCommands: CustomCommands{
|
||||
Services: []CustomCommand{
|
||||
Containers: []CustomCommand{
|
||||
{
|
||||
Name: "up",
|
||||
Command: "{{ .DockerCompose }} up -d",
|
||||
},
|
||||
{
|
||||
Name: "up (attached)",
|
||||
Command: "{{ .DockerCompose }} up",
|
||||
Attach: true,
|
||||
},
|
||||
{
|
||||
Name: "stop",
|
||||
Command: "{{ .DockerCompose }} stop",
|
||||
},
|
||||
{
|
||||
Name: "pull",
|
||||
Command: "{{ .DockerCompose }} pull",
|
||||
Attach: true,
|
||||
},
|
||||
{
|
||||
Name: "build",
|
||||
Command: "{{ .DockerCompose }} build --parallel --force-rm",
|
||||
Attach: true,
|
||||
},
|
||||
{
|
||||
Name: "down",
|
||||
Command: "{{ .DockerCompose }} down",
|
||||
},
|
||||
{
|
||||
Name: "down with volumes",
|
||||
Command: "{{ .DockerCompose }} down --volumes",
|
||||
},
|
||||
{
|
||||
Name: "down with images",
|
||||
Command: "{{ .DockerCompose }} down --rmi all",
|
||||
},
|
||||
{
|
||||
Name: "down with volumes and images",
|
||||
Command: "{{ .DockerCompose }} down --volumes --rmi all",
|
||||
Name: "prune stopped",
|
||||
Command: "container prune",
|
||||
},
|
||||
},
|
||||
Images: []CustomCommand{
|
||||
{
|
||||
Name: "prune dangling",
|
||||
Command: "container image prune",
|
||||
},
|
||||
{
|
||||
Name: "prune all",
|
||||
Command: "container image prune -a",
|
||||
},
|
||||
},
|
||||
Volumes: []CustomCommand{
|
||||
{
|
||||
Name: "prune",
|
||||
Command: "container volume prune",
|
||||
},
|
||||
},
|
||||
Networks: []CustomCommand{
|
||||
{
|
||||
Name: "prune",
|
||||
Command: "container network prune",
|
||||
},
|
||||
},
|
||||
Containers: []CustomCommand{},
|
||||
Images: []CustomCommand{},
|
||||
Volumes: []CustomCommand{},
|
||||
},
|
||||
OS: GetPlatformDefaultConfig(),
|
||||
Stats: StatsConfig{
|
||||
|
|
@ -477,13 +184,12 @@ func GetDefaultConfig() UserConfig {
|
|||
}
|
||||
}
|
||||
|
||||
// AppConfig contains the base configuration fields required for lazydocker.
|
||||
type AppConfig struct {
|
||||
Debug bool `long:"debug" env:"DEBUG" default:"false"`
|
||||
Version string `long:"version" env:"VERSION" default:"unversioned"`
|
||||
Commit string `long:"commit" env:"COMMIT"`
|
||||
BuildDate string `long:"build-date" env:"BUILD_DATE"`
|
||||
Name string `long:"name" env:"NAME" default:"lazydocker"`
|
||||
Name string `long:"name" env:"NAME" default:"lazyapple"`
|
||||
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
|
||||
UserConfig *UserConfig
|
||||
ConfigDir string
|
||||
|
|
@ -491,7 +197,6 @@ type AppConfig struct {
|
|||
ProjectName string
|
||||
}
|
||||
|
||||
// NewAppConfig makes a new app config
|
||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string, projectName string) (*AppConfig, error) {
|
||||
configDir, err := findOrCreateConfigDir(name)
|
||||
if err != nil {
|
||||
|
|
@ -503,11 +208,6 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Pass compose files as individual -f flags to docker compose
|
||||
if len(composeFiles) > 0 {
|
||||
userConfig.CommandTemplates.DockerCompose += " -f " + strings.Join(composeFiles, " -f ")
|
||||
}
|
||||
|
||||
appConfig := &AppConfig{
|
||||
Name: name,
|
||||
Version: version,
|
||||
|
|
@ -586,10 +286,6 @@ func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
|
|||
return base, nil
|
||||
}
|
||||
|
||||
// WriteToUserConfig allows you to set a value on the user config to be saved
|
||||
// note that if you set a zero-value, it may be ignored e.g. a false or 0 or
|
||||
// empty string this is because we are using the omitempty yaml directive so
|
||||
// that we don't write a heap of zero values to the user's config.yml
|
||||
func (c *AppConfig) WriteToUserConfig(updateConfig func(*UserConfig) error) error {
|
||||
userConfig, err := loadUserConfig(c.ConfigDir, &UserConfig{})
|
||||
if err != nil {
|
||||
|
|
@ -608,7 +304,6 @@ func (c *AppConfig) WriteToUserConfig(updateConfig func(*UserConfig) error) erro
|
|||
return yaml.NewEncoder(file).Encode(userConfig)
|
||||
}
|
||||
|
||||
// ConfigFilename returns the filename of the current config file
|
||||
func (c *AppConfig) ConfigFilename() string {
|
||||
return filepath.Join(c.ConfigDir, "config.yml")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,50 +7,7 @@ import (
|
|||
"github.com/jesseduffield/yaml"
|
||||
)
|
||||
|
||||
func TestDockerComposeCommandNoFiles(t *testing.T) {
|
||||
composeFiles := []string{}
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
actual := conf.UserConfig.CommandTemplates.DockerCompose
|
||||
expected := "docker compose"
|
||||
if actual != expected {
|
||||
t.Fatalf("Expected %s but got %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerComposeCommandSingleFile(t *testing.T) {
|
||||
composeFiles := []string{"one.yml"}
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
actual := conf.UserConfig.CommandTemplates.DockerCompose
|
||||
expected := "docker compose -f one.yml"
|
||||
if actual != expected {
|
||||
t.Fatalf("Expected %s but got %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerComposeCommandMultipleFiles(t *testing.T) {
|
||||
composeFiles := []string{"one.yml", "two.yml", "three.yml"}
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
actual := conf.UserConfig.CommandTemplates.DockerCompose
|
||||
expected := "docker compose -f one.yml -f two.yml -f three.yml"
|
||||
if actual != expected {
|
||||
t.Fatalf("Expected %s but got %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritingToConfigFile(t *testing.T) {
|
||||
// init the AppConfig
|
||||
emptyComposeFiles := []string{}
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir", "")
|
||||
if err != nil {
|
||||
|
|
@ -90,9 +47,6 @@ func TestWritingToConfigFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// insert value into an empty file
|
||||
testFn(t, conf, true)
|
||||
|
||||
// modifying an existing file that already has 'ConfirmOnQuit'
|
||||
testFn(t, conf, false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
)
|
||||
|
||||
type appStatus struct {
|
||||
|
|
|
|||
|
|
@ -2,15 +2,12 @@ package gui
|
|||
|
||||
import (
|
||||
"github.com/jesseduffield/lazycore/pkg/boxlayout"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// In this file we use the boxlayout package, along with knowledge about the app's state,
|
||||
// to arrange the windows (i.e. panels) on the screen.
|
||||
|
||||
const INFO_SECTION_PADDING = " "
|
||||
|
||||
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
|
||||
|
|
@ -67,9 +64,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
|||
func (gui *Gui) getMidSectionWeights() (int, int) {
|
||||
currentWindow := gui.currentStaticWindowName()
|
||||
|
||||
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
|
||||
sidePanelWidthRatio := gui.Config.UserConfig.Gui.SidePanelWidth
|
||||
// we could make this better by creating ratios like 2:3 rather than always 1:something
|
||||
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
|
||||
sideSectionWeight := 1
|
||||
|
||||
|
|
@ -120,7 +115,6 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
|
|||
},
|
||||
{
|
||||
Window: "information",
|
||||
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
|
||||
Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)),
|
||||
},
|
||||
}...,
|
||||
|
|
@ -143,6 +137,10 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
|||
currentWindow := gui.currentSideWindowName()
|
||||
sideWindowNames := gui.sideViewNames()
|
||||
|
||||
if len(sideWindowNames) == 0 {
|
||||
return []*boxlayout.Box{}
|
||||
}
|
||||
|
||||
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
|
||||
fullHeightBox := func(window string) *boxlayout.Box {
|
||||
if window == currentWindow {
|
||||
|
|
@ -162,7 +160,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
|||
return fullHeightBox(window)
|
||||
})
|
||||
|
||||
} else if height >= 28 {
|
||||
} else if height >= 20 {
|
||||
accordionMode := gui.Config.UserConfig.Gui.ExpandFocusedSidePanel
|
||||
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
||||
if accordionMode && defaultBox.Window == currentWindow {
|
||||
|
|
@ -175,27 +173,12 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
|||
return defaultBox
|
||||
}
|
||||
|
||||
// The project panel is compact (Size: 3) when not focused, but expands
|
||||
// when focused to show the list of projects.
|
||||
projectBox := &boxlayout.Box{
|
||||
Window: sideWindowNames[0],
|
||||
Size: 3,
|
||||
}
|
||||
if currentWindow == sideWindowNames[0] {
|
||||
projectBox = &boxlayout.Box{
|
||||
Window: sideWindowNames[0],
|
||||
Weight: 2,
|
||||
}
|
||||
}
|
||||
|
||||
return append([]*boxlayout.Box{
|
||||
projectBox,
|
||||
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
|
||||
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
|
||||
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
|
||||
})...)
|
||||
})
|
||||
} else {
|
||||
squashedHeight := 1
|
||||
if height >= 21 {
|
||||
if height >= 12 {
|
||||
squashedHeight = 3
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/tasks"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.TaskFunc {
|
||||
|
|
@ -22,7 +22,6 @@ func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.T
|
|||
gui.renderContainerLogsToMainAux(container, ctx, notifyStopped)
|
||||
},
|
||||
Duration: time.Millisecond * 200,
|
||||
// TODO: see why this isn't working (when switching from Top tab to Logs tab in the services panel, the tops tab's content isn't removed)
|
||||
Before: func(ctx context.Context) { gui.clearMainView() },
|
||||
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
||||
Autoscroll: true,
|
||||
|
|
@ -41,8 +40,6 @@ func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx
|
|||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
// if we are here because the task has been stopped, we should return
|
||||
// if we are here then the container must have exited, meaning we should wait until it's back again before
|
||||
ticker := time.NewTicker(time.Millisecond * 100)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
|
|
@ -52,11 +49,10 @@ func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx
|
|||
case <-ticker.C:
|
||||
result, err := container.Inspect()
|
||||
if err != nil {
|
||||
// if we get an error, then the container has probably been removed so we'll get out of here
|
||||
gui.Log.Error(err)
|
||||
return
|
||||
}
|
||||
if result.State.Running {
|
||||
if result.Status == "running" {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -97,7 +93,6 @@ func (gui *Gui) promptToReturn() {
|
|||
if !gui.Config.UserConfig.Gui.ReturnImmediately {
|
||||
fmt.Fprintf(os.Stdout, "\n\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen))
|
||||
|
||||
// wait for enter press
|
||||
if _, err := fmt.Scanln(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
|
@ -105,48 +100,71 @@ func (gui *Gui) promptToReturn() {
|
|||
}
|
||||
|
||||
func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context, writer io.Writer) error {
|
||||
readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, ctr.ID, container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Timestamps: gui.Config.UserConfig.Logs.Timestamps,
|
||||
Since: gui.Config.UserConfig.Logs.Since,
|
||||
Tail: gui.Config.UserConfig.Logs.Tail,
|
||||
Follow: true,
|
||||
})
|
||||
tail := 100
|
||||
if gui.Config.UserConfig.Logs.Tail != "" {
|
||||
fmt.Sscanf(gui.Config.UserConfig.Logs.Tail, "%d", &tail)
|
||||
}
|
||||
|
||||
cmd := gui.ContainerCmd.Client.LogsContainer(ctr.ID, true, tail)
|
||||
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return err
|
||||
}
|
||||
stderrPipe, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return err
|
||||
}
|
||||
defer readCloser.Close()
|
||||
|
||||
if !ctr.DetailsLoaded() {
|
||||
// loop until the details load or context is cancelled, using timer
|
||||
ticker := time.NewTicker(time.Millisecond * 100)
|
||||
defer ticker.Stop()
|
||||
outer:
|
||||
for {
|
||||
if err := cmd.Start(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdoutPipe)
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
if ctr.DetailsLoaded() {
|
||||
break outer
|
||||
}
|
||||
return
|
||||
default:
|
||||
fmt.Fprintln(writer, scanner.Text())
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if ctr.Details.Config.Tty {
|
||||
_, err = io.Copy(writer, readCloser)
|
||||
if err != nil {
|
||||
return err
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderrPipe)
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
fmt.Fprintln(writer, scanner.Text())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, err = stdcopy.StdCopy(writer, writer, readCloser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
go func() {
|
||||
cmd.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
killCmd(cmd)
|
||||
return nil
|
||||
case <-done:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func killCmd(cmd *exec.Cmd) {
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,31 +6,16 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/tasks"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container] {
|
||||
// Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context.
|
||||
isStandaloneContainer := func(container *commands.Container) bool {
|
||||
if container.OneOff || container.ServiceName == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return !lo.SomeBy(gui.Panels.Services.List.GetAllItems(), func(service *commands.Service) bool {
|
||||
return service.Name == container.ServiceName && service.ProjectName == container.ProjectName
|
||||
})
|
||||
}
|
||||
|
||||
return &panels.SideListPanel[*commands.Container]{
|
||||
ContextState: &panels.ContextState[*commands.Container]{
|
||||
GetMainTabs: func() []panels.MainTab[*commands.Container] {
|
||||
|
|
@ -63,12 +48,7 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
|
|||
}
|
||||
},
|
||||
GetItemContextCacheKey: func(container *commands.Container) string {
|
||||
// Including the container state in the cache key so that if the container
|
||||
// restarts we re-read the logs. In the past we've had some glitchiness
|
||||
// where a container restarts but the new logs don't get read.
|
||||
// Note that this might be jarring if we have a lot of logs and the container
|
||||
// restarts a lot, so let's keep an eye on it.
|
||||
return "containers-" + container.ID + "-" + container.Container.State
|
||||
return "containers-" + container.ID + "-" + container.GetStatus()
|
||||
},
|
||||
},
|
||||
ListPanel: panels.ListPanel[*commands.Container]{
|
||||
|
|
@ -77,33 +57,13 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
|
|||
},
|
||||
NoItemsMessage: gui.Tr.NoContainers,
|
||||
Gui: gui.intoInterface(),
|
||||
// sortedContainers returns containers sorted by state if c.SortContainersByState is true (follows 1- running, 2- exited, 3- created)
|
||||
// and sorted by name if c.SortContainersByState is false
|
||||
Sort: func(a *commands.Container, b *commands.Container) bool {
|
||||
return sortContainers(a, b, gui.Config.UserConfig.Gui.LegacySortContainers)
|
||||
},
|
||||
Filter: func(container *commands.Container) bool {
|
||||
// Note that this is O(N*M) time complexity where N is the number of services
|
||||
// and M is the number of containers. We expect N to be small but M may be large,
|
||||
// so we will need to keep an eye on this.
|
||||
if !gui.Config.UserConfig.Gui.ShowAllContainers && !isStandaloneContainer(container) {
|
||||
if !gui.State.ShowExitedContainers && container.GetStatus() != "running" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !gui.State.ShowExitedContainers && container.Container.State == "exited" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter by selected project. Containers with no project (truly
|
||||
// standalone, not from any compose project) are always shown.
|
||||
selectedProject := gui.getSelectedProjectName()
|
||||
if selectedProject == "" {
|
||||
selectedProject = gui.DockerCommand.LocalProjectName
|
||||
}
|
||||
if selectedProject != "" && container.ProjectName != "" && container.ProjectName != selectedProject {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
GetTableCells: func(container *commands.Container) []string {
|
||||
|
|
@ -114,6 +74,7 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
|
|||
|
||||
var containerStates = map[string]int{
|
||||
"running": 1,
|
||||
"stopped": 2,
|
||||
"exited": 2,
|
||||
"created": 3,
|
||||
}
|
||||
|
|
@ -123,13 +84,13 @@ func sortContainers(a *commands.Container, b *commands.Container, legacySort boo
|
|||
return a.Name < b.Name
|
||||
}
|
||||
|
||||
stateLeft := containerStates[a.Container.State]
|
||||
stateRight := containerStates[b.Container.State]
|
||||
stateLeft := containerStates[a.GetStatus()]
|
||||
stateRight := containerStates[b.GetStatus()]
|
||||
if stateLeft == stateRight {
|
||||
return a.Name < b.Name
|
||||
}
|
||||
|
||||
return containerStates[a.Container.State] < containerStates[b.Container.State]
|
||||
return stateLeft < stateRight
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContainerEnv(container *commands.Container) tasks.TaskFunc {
|
||||
|
|
@ -137,105 +98,113 @@ func (gui *Gui) renderContainerEnv(container *commands.Container) tasks.TaskFunc
|
|||
}
|
||||
|
||||
func (gui *Gui) containerEnv(container *commands.Container) string {
|
||||
if !container.DetailsLoaded() {
|
||||
return gui.Tr.WaitingForContainerInfo
|
||||
}
|
||||
|
||||
if len(container.Details.Config.Env) == 0 {
|
||||
env := container.GetEnv()
|
||||
if len(env) == 0 {
|
||||
return gui.Tr.NothingToDisplay
|
||||
}
|
||||
|
||||
envVarsList := lo.Map(container.Details.Config.Env, func(envVar string, _ int) []string {
|
||||
var lines []string
|
||||
for _, envVar := range env {
|
||||
splitEnv := strings.SplitN(envVar, "=", 2)
|
||||
key := splitEnv[0]
|
||||
value := ""
|
||||
if len(splitEnv) > 1 {
|
||||
value = splitEnv[1]
|
||||
}
|
||||
return []string{
|
||||
utils.ColoredString(key+":", color.FgGreen),
|
||||
lines = append(lines, fmt.Sprintf("%s=%s",
|
||||
utils.ColoredString(key, color.FgGreen),
|
||||
utils.ColoredString(value, color.FgYellow),
|
||||
}
|
||||
})
|
||||
|
||||
output, err := utils.RenderTable(envVarsList)
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return gui.Tr.CannotDisplayEnvVariables
|
||||
))
|
||||
}
|
||||
|
||||
return output
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContainerConfig(container *commands.Container) tasks.TaskFunc {
|
||||
return gui.NewSimpleRenderStringTask(func() string { return gui.containerConfigStr(container) })
|
||||
return gui.NewSimpleRenderStringTask(func() string { return gui.containerConfig(container) })
|
||||
}
|
||||
|
||||
func (gui *Gui) containerConfigStr(container *commands.Container) string {
|
||||
if !container.DetailsLoaded() {
|
||||
return gui.Tr.WaitingForContainerInfo
|
||||
func (gui *Gui) containerConfig(container *commands.Container) string {
|
||||
var lines []string
|
||||
|
||||
lines = append(lines, fmt.Sprintf("%s: %s",
|
||||
utils.ColoredString("ID", color.FgCyan),
|
||||
container.ID,
|
||||
))
|
||||
lines = append(lines, fmt.Sprintf("%s: %s",
|
||||
utils.ColoredString("Name", color.FgCyan),
|
||||
container.Name,
|
||||
))
|
||||
lines = append(lines, fmt.Sprintf("%s: %s",
|
||||
utils.ColoredString("Image", color.FgCyan),
|
||||
container.GetImage(),
|
||||
))
|
||||
lines = append(lines, fmt.Sprintf("%s: %s",
|
||||
utils.ColoredString("Status", color.FgCyan),
|
||||
container.GetStatus(),
|
||||
))
|
||||
lines = append(lines, fmt.Sprintf("%s: %s",
|
||||
utils.ColoredString("IP", color.FgCyan),
|
||||
container.GetIP(),
|
||||
))
|
||||
lines = append(lines, fmt.Sprintf("%s: %d",
|
||||
utils.ColoredString("CPUs", color.FgCyan),
|
||||
container.GetCPUs(),
|
||||
))
|
||||
lines = append(lines, fmt.Sprintf("%s: %s",
|
||||
utils.ColoredString("Memory", color.FgCyan),
|
||||
utils.FormatBinaryBytes(int(container.GetMemory())),
|
||||
))
|
||||
|
||||
ports := container.GetPorts()
|
||||
if len(ports) > 0 {
|
||||
lines = append(lines, fmt.Sprintf("%s: %s",
|
||||
utils.ColoredString("Ports", color.FgCyan),
|
||||
strings.Join(ports, ", "),
|
||||
))
|
||||
}
|
||||
|
||||
padding := 10
|
||||
output := ""
|
||||
output += utils.WithPadding("ID: ", padding) + container.ID + "\n"
|
||||
output += utils.WithPadding("Name: ", padding) + container.Name + "\n"
|
||||
output += utils.WithPadding("Image: ", padding) + container.Details.Config.Image + "\n"
|
||||
output += utils.WithPadding("Command: ", padding) + strings.Join(append([]string{container.Details.Path}, container.Details.Args...), " ") + "\n"
|
||||
output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, container.Details.Config.Labels)
|
||||
output += "\n"
|
||||
|
||||
output += utils.WithPadding("Mounts: ", padding)
|
||||
if len(container.Details.Mounts) > 0 {
|
||||
output += "\n"
|
||||
for _, mount := range container.Details.Mounts {
|
||||
if mount.Type == "volume" {
|
||||
output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Name)
|
||||
} else {
|
||||
output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Source, mount.Destination)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output += "none\n"
|
||||
}
|
||||
|
||||
output += utils.WithPadding("Ports: ", padding)
|
||||
if len(container.Details.NetworkSettings.Ports) > 0 {
|
||||
output += "\n"
|
||||
for k, v := range container.Details.NetworkSettings.Ports {
|
||||
for _, host := range v {
|
||||
output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(host.HostPort+":", color.FgYellow), k)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output += "none\n"
|
||||
}
|
||||
|
||||
data, err := utils.MarshalIntoYaml(&container.Details)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Error marshalling container details: %v", err)
|
||||
}
|
||||
|
||||
output += fmt.Sprintf("\nFull details:\n\n%s", utils.ColoredYamlString(string(data)))
|
||||
|
||||
return output
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContainerStats(container *commands.Container) tasks.TaskFunc {
|
||||
return gui.NewTickerTask(TickerTaskOpts{
|
||||
Func: func(ctx context.Context, notifyStopped chan struct{}) {
|
||||
contents, err := presentation.RenderStats(gui.Config.UserConfig, container, gui.Views.Main.Width())
|
||||
if err != nil {
|
||||
_ = gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return gui.NewSimpleRenderStringTask(func() string { return gui.containerStats(container) })
|
||||
}
|
||||
|
||||
gui.reRenderStringMain(contents)
|
||||
},
|
||||
Duration: time.Second,
|
||||
Before: func(ctx context.Context) { gui.clearMainView() },
|
||||
Wrap: false, // wrapping looks bad here so we're overriding the config value
|
||||
Autoscroll: false,
|
||||
})
|
||||
func (gui *Gui) containerStats(container *commands.Container) string {
|
||||
stats, ok := container.GetLastStats()
|
||||
if !ok {
|
||||
return gui.Tr.NoStats
|
||||
}
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, fmt.Sprintf("%s: %.1f%%",
|
||||
utils.ColoredString("CPU", color.FgCyan),
|
||||
stats.DerivedStats.CPUPercentage,
|
||||
))
|
||||
lines = append(lines, fmt.Sprintf("%s: %.1f%% (%s / %s)",
|
||||
utils.ColoredString("Memory",
|
||||
color.FgCyan),
|
||||
stats.DerivedStats.MemoryPercentage,
|
||||
utils.FormatBinaryBytes(int(stats.ClientStats.MemoryUsageBytes)),
|
||||
utils.FormatBinaryBytes(int(stats.ClientStats.MemoryLimitBytes)),
|
||||
))
|
||||
lines = append(lines, fmt.Sprintf("%s: %s / %s",
|
||||
utils.ColoredString("Network I/O", color.FgCyan),
|
||||
utils.FormatBinaryBytes(int(stats.ClientStats.NetworkRxBytes)),
|
||||
utils.FormatBinaryBytes(int(stats.ClientStats.NetworkTxBytes)),
|
||||
))
|
||||
lines = append(lines, fmt.Sprintf("%s: %s / %s",
|
||||
utils.ColoredString("Block I/O", color.FgCyan),
|
||||
utils.FormatBinaryBytes(int(stats.ClientStats.BlockReadBytes)),
|
||||
utils.FormatBinaryBytes(int(stats.ClientStats.BlockWriteBytes)),
|
||||
))
|
||||
lines = append(lines, fmt.Sprintf("%s: %d",
|
||||
utils.ColoredString("Processes", color.FgCyan),
|
||||
stats.ClientStats.NumProcesses,
|
||||
))
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContainerTop(container *commands.Container) tasks.TaskFunc {
|
||||
|
|
@ -255,176 +224,94 @@ func (gui *Gui) renderContainerTop(container *commands.Container) tasks.TaskFunc
|
|||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshContainersAndServices() error {
|
||||
if gui.Views.Containers == nil {
|
||||
// if the containersView hasn't been instantiated yet we just return
|
||||
return nil
|
||||
}
|
||||
|
||||
// keep track of current service selected so that we can reposition our cursor if it moves position in the list
|
||||
originalSelectedLineIdx := gui.Panels.Services.SelectedIdx
|
||||
selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx)
|
||||
|
||||
containers, services, err := gui.DockerCommand.RefreshContainersAndServices(
|
||||
gui.Panels.Containers.List.GetAllItems(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.Panels.Services.SetItems(services)
|
||||
func (gui *Gui) refreshContainers() error {
|
||||
containers := gui.ContainerCmd.RefreshContainers(gui.Panels.Containers.List.GetAllItems())
|
||||
gui.Panels.Containers.SetItems(containers)
|
||||
|
||||
// see if our selected service has moved
|
||||
if isServiceSelected {
|
||||
for i, service := range gui.Panels.Services.List.GetItems() {
|
||||
if service.ID == selectedService.ID {
|
||||
if i == originalSelectedLineIdx {
|
||||
break
|
||||
}
|
||||
gui.Panels.Services.SetSelectedLineIdx(i)
|
||||
gui.Panels.Services.Refocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gui.renderContainersAndServices()
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContainersAndServices() error {
|
||||
if err := gui.Panels.Services.RerenderList(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.Panels.Containers.RerenderList(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.Panels.Containers.RerenderList()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleHideStoppedContainers(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.ShowExitedContainers = !gui.State.ShowExitedContainers
|
||||
|
||||
return gui.Panels.Containers.RerenderList()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
handleMenuPress := func(configOptions container.RemoveOptions) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
if err := ctr.Remove(configOptions); err != nil {
|
||||
if commands.HasErrorCode(err, commands.MustStopContainer) {
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.MustForceToRemoveContainer, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
configOptions.Force = true
|
||||
return ctr.Remove(configOptions)
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
menuItems := []*types.MenuItem{
|
||||
{
|
||||
LabelColumns: []string{gui.Tr.Remove, "docker rm " + ctr.ID[1:10]},
|
||||
OnPress: func() error { return handleMenuPress(container.RemoveOptions{}) },
|
||||
},
|
||||
{
|
||||
LabelColumns: []string{gui.Tr.RemoveWithVolumes, "docker rm --volumes " + ctr.ID[1:10]},
|
||||
OnPress: func() error { return handleMenuPress(container.RemoveOptions{RemoveVolumes: true}) },
|
||||
},
|
||||
}
|
||||
|
||||
return gui.Menu(CreateMenuOptions{
|
||||
Title: "",
|
||||
Items: menuItems,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) PauseContainer(container *commands.Container) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.PausingStatus, func() (err error) {
|
||||
if container.Details.State.Paused {
|
||||
err = container.Unpause()
|
||||
} else {
|
||||
err = container.Pause()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return gui.refreshContainersAndServices()
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.PauseContainer(ctr)
|
||||
return gui.refreshContainers()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
container, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
||||
if err := ctr.Stop(); err != nil {
|
||||
if err := container.Stop(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
container, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
||||
if err := ctr.Restart(); err != nil {
|
||||
if err := container.Restart(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
container, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := ctr.Attach()
|
||||
c, err := container.Attach()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithMessage(c, gui.Tr.DetachFromContainerShortCut)
|
||||
return gui.runSubprocess(c)
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePruneContainers() error {
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
|
||||
err := gui.DockerCommand.PruneContainers()
|
||||
if err != nil {
|
||||
func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error {
|
||||
container, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := gui.ContainerCmd.Client.LogsContainer(container.ID, true, 0)
|
||||
return gui.runSubprocess(cmd)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainerKill(g *gocui.Gui, v *gocui.View) error {
|
||||
container, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.KillingStatus, func() error {
|
||||
if err := container.Kill(""); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
container, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.RemoveContainer, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
if err := container.Remove(true); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
|
|
@ -432,46 +319,33 @@ func (gui *Gui) handlePruneContainers() error {
|
|||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.renderLogsToStdout(ctr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
container, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.containerExecShell(ctr)
|
||||
}
|
||||
if container.GetStatus() != "running" {
|
||||
return gui.createErrorPanel(gui.Tr.ContainerNotRunning)
|
||||
}
|
||||
|
||||
func (gui *Gui) containerExecShell(container *commands.Container) error {
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
||||
Container: container,
|
||||
cmd := gui.ContainerCmd.Client.ExecContainer(container.ID, commands.ExecOptions{
|
||||
Command: []string{"/bin/sh"},
|
||||
Interactive: true,
|
||||
TTY: true,
|
||||
})
|
||||
|
||||
// TODO: use SDK
|
||||
resolvedCommand := utils.ApplyTemplate("docker exec -it {{ .Container.ID }} /bin/sh -c 'eval $(grep ^$(id -un): /etc/passwd | cut -d : -f 7-)'", commandObject)
|
||||
// attach and return the subprocess error
|
||||
cmd := gui.OSCommand.ExecutableFromString(resolvedCommand)
|
||||
return gui.runSubprocess(cmd)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
container, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
||||
Container: ctr,
|
||||
commandObject := gui.ContainerCmd.NewCommandObject(commands.CommandObject{
|
||||
Container: container,
|
||||
})
|
||||
|
||||
customCommands := gui.Config.UserConfig.CustomCommands.Containers
|
||||
|
|
@ -479,80 +353,41 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error
|
|||
return gui.createCustomCommandMenu(customCommands, commandObject)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStopContainers() error {
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
||||
for _, ctr := range gui.Panels.Containers.List.GetAllItems() {
|
||||
if err := ctr.Stop(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRemoveContainers() error {
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
for _, ctr := range gui.Panels.Containers.List.GetAllItems() {
|
||||
if err := ctr.Remove(container.RemoveOptions{Force: true}); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
baseBulkCommands := []config.CustomCommand{
|
||||
{
|
||||
Name: gui.Tr.StopAllContainers,
|
||||
InternalFunction: gui.handleStopContainers,
|
||||
},
|
||||
{
|
||||
Name: gui.Tr.RemoveAllContainers,
|
||||
InternalFunction: gui.handleRemoveContainers,
|
||||
},
|
||||
{
|
||||
Name: gui.Tr.PruneContainers,
|
||||
InternalFunction: gui.handlePruneContainers,
|
||||
},
|
||||
}
|
||||
|
||||
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Containers...)
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
|
||||
bulkCommands := gui.Config.UserConfig.BulkCommands.Containers
|
||||
commandObject := gui.ContainerCmd.NewCommandObject(commands.CommandObject{})
|
||||
|
||||
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
||||
}
|
||||
|
||||
// Open first port in browser
|
||||
func (gui *Gui) handleContainersOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
container, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.openContainerInBrowser(ctr)
|
||||
ip := container.GetIP()
|
||||
if ip == "" {
|
||||
return gui.createErrorPanel(gui.Tr.NoIp)
|
||||
}
|
||||
|
||||
ports := container.GetPorts()
|
||||
if len(ports) == 0 {
|
||||
return gui.createErrorPanel(gui.Tr.NoPorts)
|
||||
}
|
||||
|
||||
for _, port := range ports {
|
||||
parts := strings.Split(port, ":")
|
||||
if len(parts) >= 2 {
|
||||
hostPort := strings.Split(parts[0], "->")[0]
|
||||
url := fmt.Sprintf("http://%s:%s", ip, hostPort)
|
||||
return gui.OSCommand.OpenLink(url)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) openContainerInBrowser(ctr *commands.Container) error {
|
||||
// skip if no any ports
|
||||
if len(ctr.Container.Ports) == 0 {
|
||||
return nil
|
||||
}
|
||||
// skip if the first port is not published
|
||||
port := ctr.Container.Ports[0]
|
||||
if port.IP == "" {
|
||||
return nil
|
||||
}
|
||||
ip := port.IP
|
||||
if ip == "0.0.0.0" {
|
||||
ip = "localhost"
|
||||
}
|
||||
link := fmt.Sprintf("http://%s:%d/", ip, port.PublicPort)
|
||||
return gui.OSCommand.OpenLink(link)
|
||||
func (gui *Gui) PauseContainer(container *commands.Container) error {
|
||||
return gui.createErrorPanel("Pause is not supported by Apple Container")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ package gui
|
|||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package gui
|
|||
import (
|
||||
"github.com/gookit/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
)
|
||||
|
||||
var gocuiColorMap = map[string]gocui.Attribute{
|
||||
|
|
|
|||
160
pkg/gui/gui.go
160
pkg/gui/gui.go
|
|
@ -6,40 +6,32 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/events"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
throttle "github.com/boz/go-throttle"
|
||||
"github.com/jesseduffield/gocui"
|
||||
lcUtils "github.com/jesseduffield/lazycore/pkg/utils"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/i18n"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/tasks"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Gui wraps the gocui Gui object which handles rendering and events
|
||||
type Gui struct {
|
||||
g *gocui.Gui
|
||||
Log *logrus.Entry
|
||||
DockerCommand *commands.DockerCommand
|
||||
OSCommand *commands.OSCommand
|
||||
State guiState
|
||||
Config *config.AppConfig
|
||||
Tr *i18n.TranslationSet
|
||||
statusManager *statusManager
|
||||
taskManager *tasks.TaskManager
|
||||
ErrorChan chan error
|
||||
Views Views
|
||||
g *gocui.Gui
|
||||
Log *logrus.Entry
|
||||
ContainerCmd *commands.ContainerCommand
|
||||
OSCommand *commands.OSCommand
|
||||
State guiState
|
||||
Config *config.AppConfig
|
||||
Tr *i18n.TranslationSet
|
||||
statusManager *statusManager
|
||||
taskManager *tasks.TaskManager
|
||||
ErrorChan chan error
|
||||
Views Views
|
||||
|
||||
// if we've suspended the gui (e.g. because we've switched to a subprocess)
|
||||
// we typically want to pause some things that are running like background
|
||||
// file refreshes
|
||||
PauseBackgroundThreads bool
|
||||
|
||||
Mutexes
|
||||
|
|
@ -48,8 +40,6 @@ type Gui struct {
|
|||
}
|
||||
|
||||
type Panels struct {
|
||||
Projects *panels.SideListPanel[*commands.Project]
|
||||
Services *panels.SideListPanel[*commands.Service]
|
||||
Containers *panels.SideListPanel[*commands.Container]
|
||||
Images *panels.SideListPanel[*commands.Image]
|
||||
Volumes *panels.SideListPanel[*commands.Volume]
|
||||
|
|
@ -63,7 +53,6 @@ type Mutexes struct {
|
|||
}
|
||||
|
||||
type mainPanelState struct {
|
||||
// ObjectKey tells us what context we are in. For example, if we are looking at the logs of a particular service in the services panel this key might be 'services-<service id>-logs'. The key is made so that if something changes which might require us to re-run the logs command or run a different command, the key will be different, and we'll then know to do whatever is required. Object key probably isn't the best name for this but Context is already used to refer to tabs. Maybe I should just call them tabs.
|
||||
ObjectKey string
|
||||
}
|
||||
|
||||
|
|
@ -72,37 +61,25 @@ type panelStates struct {
|
|||
}
|
||||
|
||||
type guiState struct {
|
||||
// the names of views in the current focus stack (last item is the current view)
|
||||
ViewStack []string
|
||||
Platform commands.Platform
|
||||
Panels *panelStates
|
||||
SubProcessOutput string
|
||||
Stats map[string]commands.ContainerStats
|
||||
|
||||
// if true, we show containers with an 'exited' status in the containers panel
|
||||
ShowExitedContainers bool
|
||||
|
||||
ScreenMode WindowMaximisation
|
||||
|
||||
// Maintains the state of manual filtering i.e. typing in a substring
|
||||
// to filter on in the current panel.
|
||||
Filter filterState
|
||||
}
|
||||
|
||||
type filterState struct {
|
||||
// If true then we're either currently inside the filter view
|
||||
// or we've committed the filter and we're back in the list view
|
||||
active bool
|
||||
// The panel that we're filtering.
|
||||
panel panels.ISideListPanel
|
||||
// The string that we're filtering on
|
||||
panel panels.ISideListPanel
|
||||
needle string
|
||||
}
|
||||
|
||||
// screen sizing determines how much space your selected window takes up (window
|
||||
// as in panel, not your terminal's window). Sometimes you want a bit more space
|
||||
// to see the contents of a panel, and this keeps track of how much maximisation
|
||||
// you've set
|
||||
type WindowMaximisation int
|
||||
|
||||
const (
|
||||
|
|
@ -124,8 +101,7 @@ func getScreenMode(config *config.AppConfig) WindowMaximisation {
|
|||
}
|
||||
}
|
||||
|
||||
// NewGui builds a new gui handler
|
||||
func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) {
|
||||
func NewGui(log *logrus.Entry, containerCmd *commands.ContainerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) {
|
||||
initialState := guiState{
|
||||
Platform: *oSCommand.Platform,
|
||||
Panels: &panelStates{
|
||||
|
|
@ -141,7 +117,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
|
|||
|
||||
gui := &Gui{
|
||||
Log: log,
|
||||
DockerCommand: dockerCommand,
|
||||
ContainerCmd: containerCmd,
|
||||
OSCommand: oSCommand,
|
||||
State: initialState,
|
||||
Config: config,
|
||||
|
|
@ -168,7 +144,7 @@ func (gui *Gui) renderGlobalOptions() error {
|
|||
}
|
||||
|
||||
func (gui *Gui) goEvery(interval time.Duration, function func() error) {
|
||||
_ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change
|
||||
_ = function()
|
||||
go func() {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
|
@ -180,9 +156,7 @@ func (gui *Gui) goEvery(interval time.Duration, function func() error) {
|
|||
}()
|
||||
}
|
||||
|
||||
// Run setup the gui with keybindings and start the mainloop
|
||||
func (gui *Gui) Run() error {
|
||||
// closing our task manager which in turn closes the current task if there is any, so we aren't leaving processes lying around after closing lazydocker
|
||||
defer gui.taskManager.Close()
|
||||
|
||||
g, err := gocui.NewGui(gocui.NewGuiOpts{
|
||||
|
|
@ -194,15 +168,12 @@ func (gui *Gui) Run() error {
|
|||
}
|
||||
defer g.Close()
|
||||
|
||||
// forgive the double-negative, this is because of my yaml `omitempty` woes
|
||||
if !gui.Config.UserConfig.Gui.IgnoreMouseEvents {
|
||||
g.Mouse = true
|
||||
}
|
||||
|
||||
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
|
||||
gui.g = g
|
||||
|
||||
// if the deadlock package wants to report a deadlock, we first need to
|
||||
// close the gui so that we can actually read what it prints.
|
||||
deadlock.Opts.LogBuf = lcUtils.NewOnceWriter(os.Stderr, func() {
|
||||
gui.g.Close()
|
||||
})
|
||||
|
|
@ -220,7 +191,6 @@ func (gui *Gui) Run() error {
|
|||
continue
|
||||
}
|
||||
if strings.Contains(err.Error(), "No such container") {
|
||||
// this happens all the time when e.g. restarting containers so we won't worry about it
|
||||
gui.Log.Warn(err)
|
||||
continue
|
||||
}
|
||||
|
|
@ -237,7 +207,6 @@ func (gui *Gui) Run() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// TODO: see if we can avoid the circular dependency
|
||||
gui.setPanels()
|
||||
|
||||
if err = gui.keybindings(g); err != nil {
|
||||
|
|
@ -259,17 +228,14 @@ func (gui *Gui) Run() error {
|
|||
ctx, finish := context.WithCancel(context.Background())
|
||||
defer finish()
|
||||
|
||||
go gui.listenForEvents(ctx, throttledRefresh.Trigger)
|
||||
go gui.monitorContainerStats(ctx)
|
||||
|
||||
go func() {
|
||||
throttledRefresh.Trigger()
|
||||
|
||||
gui.goEvery(time.Millisecond*30, gui.reRenderMain)
|
||||
gui.goEvery(time.Millisecond*1000, gui.updateContainerDetails)
|
||||
gui.goEvery(time.Millisecond*1000, gui.renderContainers)
|
||||
gui.goEvery(time.Millisecond*1000, gui.checkForContextChange)
|
||||
// we need to regularly re-render these because their stats will be changed in the background
|
||||
gui.goEvery(time.Millisecond*1000, gui.renderContainersAndServices)
|
||||
}()
|
||||
|
||||
err = g.MainLoop()
|
||||
|
|
@ -281,8 +247,6 @@ func (gui *Gui) Run() error {
|
|||
|
||||
func (gui *Gui) setPanels() {
|
||||
gui.Panels = Panels{
|
||||
Projects: gui.getProjectPanel(),
|
||||
Services: gui.getServicesPanel(),
|
||||
Containers: gui.getContainersPanel(),
|
||||
Images: gui.getImagesPanel(),
|
||||
Volumes: gui.getVolumesPanel(),
|
||||
|
|
@ -291,18 +255,13 @@ func (gui *Gui) setPanels() {
|
|||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) updateContainerDetails() error {
|
||||
return gui.DockerCommand.RefreshContainerDetails(gui.Panels.Containers.List.GetAllItems())
|
||||
func (gui *Gui) renderContainers() error {
|
||||
return gui.Panels.Containers.RerenderList()
|
||||
}
|
||||
|
||||
func (gui *Gui) refresh() {
|
||||
go func() {
|
||||
// Refresh containers/services first, then projects (which depend on
|
||||
// container labels to discover projects).
|
||||
if err := gui.refreshContainersAndServices(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
if err := gui.refreshProject(); err != nil {
|
||||
if err := gui.refreshContainers(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}()
|
||||
|
|
@ -323,61 +282,6 @@ func (gui *Gui) refresh() {
|
|||
}()
|
||||
}
|
||||
|
||||
func (gui *Gui) listenForEvents(ctx context.Context, refresh func()) {
|
||||
errorCount := 0
|
||||
|
||||
onError := func(err error) {
|
||||
if err != nil {
|
||||
gui.ErrorChan <- errors.Errorf("Docker event stream returned error: %s\nRetry count: %d", err.Error(), errorCount)
|
||||
}
|
||||
errorCount++
|
||||
time.Sleep(time.Second * 2)
|
||||
}
|
||||
|
||||
outer:
|
||||
for {
|
||||
messageChan, errChan := gui.DockerCommand.Client.Events(context.Background(), events.ListOptions{})
|
||||
|
||||
if errorCount > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case err := <-errChan:
|
||||
onError(err)
|
||||
continue outer
|
||||
default:
|
||||
// If we're here then we lost connection to docker and we just got it back.
|
||||
// The reason we do this refresh explicitly is because successfully
|
||||
// reconnecting with docker does not mean it's going to send us a new
|
||||
// event any time soon.
|
||||
|
||||
// Assuming the confirmation prompt currently holds the given error
|
||||
_ = gui.closeConfirmationPrompt()
|
||||
refresh()
|
||||
errorCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case message := <-messageChan:
|
||||
// We could be more granular about what events should trigger which refreshes.
|
||||
// At the moment it's pretty efficient though, and it might not be worth
|
||||
// the maintenance burden of mapping specific events to specific refreshes
|
||||
refresh()
|
||||
|
||||
gui.Log.Infof("received event of type: %s", message.Type)
|
||||
case err := <-errChan:
|
||||
onError(err)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkForContextChange runs the currently focused panel's 'select' function, simulating the current item having just been selected. This will then trigger a check to see if anything's changed (e.g. a service has a new container) and if so, the appropriate code will run. For example, if you're reading logs from a service and all of a sudden its container changes, this will trigger the 'select' function, which will work out that the context is not different because of the new container, and then it will re-attempt to get the logs, this time for the correct container. This 'context' is stored in the main panel's ObjectKey. I'm using the term 'context' here more broadly than just the different tabs you can view in a panel.
|
||||
func (gui *Gui) checkForContextChange() error {
|
||||
return gui.newLineFocused(gui.g.CurrentView())
|
||||
}
|
||||
|
|
@ -404,8 +308,6 @@ func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
|
|||
return gocui.ErrQuit
|
||||
}
|
||||
|
||||
// this handler is executed when we press escape when there is only one view
|
||||
// on the stack.
|
||||
func (gui *Gui) escape() error {
|
||||
if gui.State.Filter.active {
|
||||
return gui.clearFilter()
|
||||
|
|
@ -459,9 +361,6 @@ func (gui *Gui) ShouldRefresh(key string) bool {
|
|||
}
|
||||
|
||||
func (gui *Gui) initiallyFocusedViewName() string {
|
||||
if gui.DockerCommand.InDockerComposeProject {
|
||||
return "services"
|
||||
}
|
||||
return "containers"
|
||||
}
|
||||
|
||||
|
|
@ -474,8 +373,6 @@ func (gui *Gui) Update(f func() error) {
|
|||
}
|
||||
|
||||
func (gui *Gui) monitorContainerStats(ctx context.Context) {
|
||||
// periodically loop through running containers and see if we need to create a monitor goroutine for any
|
||||
// every second we check if we need to spawn a new goroutine
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
|
|
@ -485,16 +382,13 @@ func (gui *Gui) monitorContainerStats(ctx context.Context) {
|
|||
case <-ticker.C:
|
||||
for _, container := range gui.Panels.Containers.List.GetAllItems() {
|
||||
if !container.MonitoringStats {
|
||||
go gui.DockerCommand.CreateClientStatMonitor(container)
|
||||
go gui.ContainerCmd.CreateClientStatMonitor(container)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is used by our cheatsheet code to generate keybindings. We need some views
|
||||
// and panels to exist for us to know what keybindings there are, so we invoke
|
||||
// gocui in headless mode and create them.
|
||||
func (gui *Gui) SetupFakeGui() {
|
||||
g, err := gocui.NewGui(gocui.NewGuiOpts{
|
||||
OutputMode: gocui.OutputTrue,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,14 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/tasks"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) getImagesPanel() *panels.SideListPanel[*commands.Image] {
|
||||
|
|
@ -42,23 +35,19 @@ func (gui *Gui) getImagesPanel() *panels.SideListPanel[*commands.Image] {
|
|||
},
|
||||
NoItemsMessage: gui.Tr.NoImages,
|
||||
Gui: gui.intoInterface(),
|
||||
Sort: func(a *commands.Image, b *commands.Image) bool {
|
||||
Sort: func(a, b *commands.Image) bool {
|
||||
if a.Name == noneLabel && b.Name != noneLabel {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.Name != noneLabel && b.Name == noneLabel {
|
||||
return true
|
||||
}
|
||||
|
||||
if a.Name != b.Name {
|
||||
return a.Name < b.Name
|
||||
}
|
||||
|
||||
if a.Tag != b.Tag {
|
||||
return a.Tag < b.Tag
|
||||
}
|
||||
|
||||
return a.ID < b.ID
|
||||
},
|
||||
GetTableCells: presentation.GetImageDisplayStrings,
|
||||
|
|
@ -69,7 +58,7 @@ func (gui *Gui) renderImageConfigTask(image *commands.Image) tasks.TaskFunc {
|
|||
return gui.NewRenderStringTask(RenderStringTaskOpts{
|
||||
GetStrContent: func() string { return gui.imageConfigStr(image) },
|
||||
Autoscroll: false,
|
||||
Wrap: false, // don't care what your config is this page is ugly without wrapping
|
||||
Wrap: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -77,18 +66,11 @@ func (gui *Gui) imageConfigStr(image *commands.Image) string {
|
|||
padding := 10
|
||||
output := ""
|
||||
output += utils.WithPadding("Name: ", padding) + image.Name + "\n"
|
||||
output += utils.WithPadding("ID: ", padding) + image.Image.ID + "\n"
|
||||
output += utils.WithPadding("Tags: ", padding) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n"
|
||||
output += utils.WithPadding("Size: ", padding) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n"
|
||||
output += utils.WithPadding("Created: ", padding) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n"
|
||||
|
||||
history, err := image.RenderHistory()
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
output += "\n\n" + history
|
||||
|
||||
output += utils.WithPadding("Tag: ", padding) + image.Tag + "\n"
|
||||
output += utils.WithPadding("ID: ", padding) + image.ID + "\n"
|
||||
output += utils.WithPadding("Reference: ", padding) + image.AppleImage.Reference + "\n"
|
||||
output += utils.WithPadding("Digest: ", padding) + image.AppleImage.Descriptor.Digest + "\n"
|
||||
output += utils.WithPadding("Size: ", padding) + image.AppleImage.FullSize + "\n"
|
||||
return output
|
||||
}
|
||||
|
||||
|
|
@ -96,82 +78,51 @@ func (gui *Gui) reloadImages() error {
|
|||
if err := gui.refreshStateImages(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.Panels.Images.RerenderList()
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshStateImages() error {
|
||||
images, err := gui.DockerCommand.RefreshImages()
|
||||
images, err := gui.ContainerCmd.RefreshImages()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.Panels.Images.SetItems(images)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) FilterString(view *gocui.View) string {
|
||||
if gui.State.Filter.panel != nil && gui.State.Filter.panel.GetView() != view {
|
||||
return ""
|
||||
}
|
||||
|
||||
return gui.State.Filter.needle
|
||||
}
|
||||
|
||||
func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
type removeImageOption struct {
|
||||
description string
|
||||
command string
|
||||
configOptions image.RemoveOptions
|
||||
}
|
||||
|
||||
img, err := gui.Panels.Images.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
shortSha := img.ID[7:17]
|
||||
|
||||
// TODO: have a way of toggling in a menu instead of showing each permutation as a separate menu item
|
||||
options := []*removeImageOption{
|
||||
{
|
||||
description: gui.Tr.Remove,
|
||||
command: "docker image rm " + shortSha,
|
||||
configOptions: image.RemoveOptions{PruneChildren: true, Force: false},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.RemoveWithoutPrune,
|
||||
command: "docker image rm --no-prune " + shortSha,
|
||||
configOptions: image.RemoveOptions{PruneChildren: false, Force: false},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.RemoveWithForce,
|
||||
command: "docker image rm --force " + shortSha,
|
||||
configOptions: image.RemoveOptions{PruneChildren: true, Force: true},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.RemoveWithoutPruneWithForce,
|
||||
command: "docker image rm --no-prune --force " + shortSha,
|
||||
configOptions: image.RemoveOptions{PruneChildren: false, Force: true},
|
||||
},
|
||||
shortSha := img.ID
|
||||
if len(img.ID) > 12 {
|
||||
shortSha = img.ID[:12]
|
||||
}
|
||||
|
||||
menuItems := lo.Map(options, func(option *removeImageOption, _ int) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: []string{
|
||||
option.description,
|
||||
color.New(color.FgRed).Sprint(option.command),
|
||||
},
|
||||
options := []struct {
|
||||
description string
|
||||
command string
|
||||
force bool
|
||||
}{
|
||||
{description: gui.Tr.Remove, command: "container image rm " + shortSha, force: false},
|
||||
{description: gui.Tr.RemoveWithForce, command: "container image rm --force " + shortSha, force: true},
|
||||
}
|
||||
|
||||
menuItems := make([]*types.MenuItem, len(options))
|
||||
for i, opt := range options {
|
||||
opt := opt
|
||||
menuItems[i] = &types.MenuItem{
|
||||
LabelColumns: []string{opt.description, color.New(color.FgRed).Sprint(opt.command)},
|
||||
OnPress: func() error {
|
||||
if err := img.Remove(option.configOptions); err != nil {
|
||||
if err := img.Remove(opt.force); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return gui.Menu(CreateMenuOptions{
|
||||
Title: "",
|
||||
|
|
@ -179,10 +130,11 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePruneImages() error {
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handlePruneImages(all bool) error {
|
||||
title := gui.Tr.ConfirmPruneImages
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, title, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
|
||||
err := gui.DockerCommand.PruneImages()
|
||||
err := gui.ContainerCmd.Client.PruneImages(all)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
|
@ -197,25 +149,13 @@ func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
||||
Image: img,
|
||||
})
|
||||
|
||||
commandObject := gui.ContainerCmd.NewCommandObject(commands.CommandObject{Image: img})
|
||||
customCommands := gui.Config.UserConfig.CustomCommands.Images
|
||||
|
||||
return gui.createCustomCommandMenu(customCommands, commandObject)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleImagesBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
baseBulkCommands := []config.CustomCommand{
|
||||
{
|
||||
Name: gui.Tr.PruneImages,
|
||||
InternalFunction: gui.handlePruneImages,
|
||||
},
|
||||
}
|
||||
|
||||
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Images...)
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
|
||||
|
||||
bulkCommands := gui.Config.UserConfig.BulkCommands.Images
|
||||
commandObject := gui.ContainerCmd.NewCommandObject(commands.CommandObject{})
|
||||
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,18 +6,14 @@ import (
|
|||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
// Binding - a keybinding mapping a key and modifier to a handler. The keypress
|
||||
// is only handled if the given view has focus, or handled globally if the view
|
||||
// is ""
|
||||
type Binding struct {
|
||||
ViewName string
|
||||
Handler func(*gocui.Gui, *gocui.View) error
|
||||
Key interface{} // FIXME: find out how to get `gocui.Key | rune`
|
||||
Key interface{}
|
||||
Modifier gocui.Modifier
|
||||
Description string
|
||||
}
|
||||
|
||||
// GetKey is a function.
|
||||
func (b *Binding) GetKey() string {
|
||||
key := 0
|
||||
|
||||
|
|
@ -28,7 +24,6 @@ func (b *Binding) GetKey() string {
|
|||
key = int(b.Key.(gocui.Key))
|
||||
}
|
||||
|
||||
// special keys
|
||||
switch key {
|
||||
case 27:
|
||||
return "esc"
|
||||
|
|
@ -53,7 +48,6 @@ func (b *Binding) GetKey() string {
|
|||
return fmt.Sprintf("%c", key)
|
||||
}
|
||||
|
||||
// GetInitialKeybindings is a function.
|
||||
func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
bindings := []*Binding{
|
||||
{
|
||||
|
|
@ -128,27 +122,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCustomCommand,
|
||||
},
|
||||
{
|
||||
ViewName: "project",
|
||||
Key: 'e',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleEditConfig,
|
||||
Description: gui.Tr.EditConfig,
|
||||
},
|
||||
{
|
||||
ViewName: "project",
|
||||
Key: 'o',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleOpenConfig,
|
||||
Description: gui.Tr.OpenConfig,
|
||||
},
|
||||
{
|
||||
ViewName: "project",
|
||||
Key: 'm',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleViewAllLogs,
|
||||
Description: gui.Tr.ViewLogs,
|
||||
},
|
||||
{
|
||||
ViewName: "menu",
|
||||
Key: gocui.KeyEsc,
|
||||
|
|
@ -199,13 +172,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
Handler: gui.handleHideStoppedContainers,
|
||||
Description: gui.Tr.HideStopped,
|
||||
},
|
||||
{
|
||||
ViewName: "containers",
|
||||
Key: 'p',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleContainerPause,
|
||||
Description: gui.Tr.Pause,
|
||||
},
|
||||
{
|
||||
ViewName: "containers",
|
||||
Key: 's',
|
||||
|
|
@ -263,109 +229,11 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
Description: gui.Tr.OpenInBrowser,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'u',
|
||||
ViewName: "containers",
|
||||
Key: 'k',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServiceUp,
|
||||
Description: gui.Tr.UpService,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'd',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServiceRemoveMenu,
|
||||
Description: gui.Tr.RemoveService,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 's',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServiceStop,
|
||||
Description: gui.Tr.Stop,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'p',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServicePause,
|
||||
Description: gui.Tr.Pause,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'r',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServiceRestart,
|
||||
Description: gui.Tr.Restart,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'S',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServiceStart,
|
||||
Description: gui.Tr.Start,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'a',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServiceAttach,
|
||||
Description: gui.Tr.Attach,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'm',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServiceRenderLogsToMain,
|
||||
Description: gui.Tr.ViewLogs,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'U',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleProjectUp,
|
||||
Description: gui.Tr.UpProject,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'D',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleProjectDown,
|
||||
Description: gui.Tr.DownProject,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'R',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServiceRestartMenu,
|
||||
Description: gui.Tr.ViewRestartOptions,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'c',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServicesCustomCommand,
|
||||
Description: gui.Tr.RunCustomCommand,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'b',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServicesBulkCommand,
|
||||
Description: gui.Tr.ViewBulkCommands,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'E',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServicesExecShell,
|
||||
Description: gui.Tr.ExecShell,
|
||||
},
|
||||
{
|
||||
ViewName: "services",
|
||||
Key: 'w',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleServicesOpenInBrowserCommand,
|
||||
Description: gui.Tr.OpenInBrowser,
|
||||
Handler: gui.handleContainerKill,
|
||||
Description: gui.Tr.Kill,
|
||||
},
|
||||
{
|
||||
ViewName: "images",
|
||||
|
|
@ -535,12 +403,10 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
}
|
||||
|
||||
bindings = append(bindings, []*Binding{
|
||||
{Handler: gui.handleGoTo(gui.Panels.Projects.View), Key: '1', Description: gui.Tr.FocusProjects},
|
||||
{Handler: gui.handleGoTo(gui.Panels.Services.View), Key: '2', Description: gui.Tr.FocusServices},
|
||||
{Handler: gui.handleGoTo(gui.Panels.Containers.View), Key: '3', Description: gui.Tr.FocusContainers},
|
||||
{Handler: gui.handleGoTo(gui.Panels.Images.View), Key: '4', Description: gui.Tr.FocusImages},
|
||||
{Handler: gui.handleGoTo(gui.Panels.Volumes.View), Key: '5', Description: gui.Tr.FocusVolumes},
|
||||
{Handler: gui.handleGoTo(gui.Panels.Networks.View), Key: '6', Description: gui.Tr.FocusNetworks},
|
||||
{Handler: gui.handleGoTo(gui.Panels.Containers.View), Key: '1', Description: gui.Tr.FocusContainers},
|
||||
{Handler: gui.handleGoTo(gui.Panels.Images.View), Key: '2', Description: gui.Tr.FocusImages},
|
||||
{Handler: gui.handleGoTo(gui.Panels.Volumes.View), Key: '3', Description: gui.Tr.FocusVolumes},
|
||||
{Handler: gui.handleGoTo(gui.Panels.Networks.View), Key: '4', Description: gui.Tr.FocusNetworks},
|
||||
}...)
|
||||
|
||||
for _, panel := range gui.allListPanels() {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
)
|
||||
|
||||
type CreateMenuOptions struct {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/tasks"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
|
|
@ -37,9 +35,6 @@ func (gui *Gui) getNetworksPanel() *panels.SideListPanel[*commands.Network] {
|
|||
},
|
||||
NoItemsMessage: gui.Tr.NoNetworks,
|
||||
Gui: gui.intoInterface(),
|
||||
// we're sorting these networks based on whether they have labels defined,
|
||||
// because those are the ones you typically care about.
|
||||
// Within that, we also sort them alphabetically
|
||||
Sort: func(a *commands.Network, b *commands.Network) bool {
|
||||
return a.Name < b.Name
|
||||
},
|
||||
|
|
@ -54,28 +49,14 @@ func (gui *Gui) renderNetworkConfig(network *commands.Network) tasks.TaskFunc {
|
|||
func (gui *Gui) networkConfigStr(network *commands.Network) string {
|
||||
padding := 15
|
||||
output := ""
|
||||
output += utils.WithPadding("ID: ", padding) + network.Network.ID + "\n"
|
||||
output += utils.WithPadding("ID: ", padding) + network.AppleNetwork.ID + "\n"
|
||||
output += utils.WithPadding("Name: ", padding) + network.Name + "\n"
|
||||
output += utils.WithPadding("Driver: ", padding) + network.Network.Driver + "\n"
|
||||
output += utils.WithPadding("Scope: ", padding) + network.Network.Scope + "\n"
|
||||
output += utils.WithPadding("EnabledIPV6: ", padding) + strconv.FormatBool(network.Network.EnableIPv6) + "\n"
|
||||
output += utils.WithPadding("Internal: ", padding) + strconv.FormatBool(network.Network.Internal) + "\n"
|
||||
output += utils.WithPadding("Attachable: ", padding) + strconv.FormatBool(network.Network.Attachable) + "\n"
|
||||
output += utils.WithPadding("Ingress: ", padding) + strconv.FormatBool(network.Network.Ingress) + "\n"
|
||||
|
||||
output += utils.WithPadding("Containers: ", padding)
|
||||
if len(network.Network.Containers) > 0 {
|
||||
output += "\n"
|
||||
for _, v := range network.Network.Containers {
|
||||
output += utils.FormatMapItem(padding, v.Name, v.EndpointID)
|
||||
}
|
||||
} else {
|
||||
output += "none\n"
|
||||
}
|
||||
|
||||
output += utils.WithPadding("Mode: ", padding) + network.AppleNetwork.Config.Mode + "\n"
|
||||
output += utils.WithPadding("State: ", padding) + network.AppleNetwork.State + "\n"
|
||||
output += utils.WithPadding("IPv4 Subnet: ", padding) + network.AppleNetwork.Status.IPv4Subnet + "\n"
|
||||
output += utils.WithPadding("IPv4 Gateway: ", padding) + network.AppleNetwork.Status.IPv4Gateway + "\n"
|
||||
output += "\n"
|
||||
output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, network.Network.Labels) + "\n"
|
||||
output += utils.WithPadding("Options: ", padding) + utils.FormatMap(padding, network.Network.Options)
|
||||
output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, network.AppleNetwork.Config.Labels)
|
||||
|
||||
return output
|
||||
}
|
||||
|
|
@ -89,7 +70,7 @@ func (gui *Gui) reloadNetworks() error {
|
|||
}
|
||||
|
||||
func (gui *Gui) refreshStateNetworks() error {
|
||||
networks, err := gui.DockerCommand.RefreshNetworks()
|
||||
networks, err := gui.ContainerCmd.RefreshNetworks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -113,7 +94,7 @@ func (gui *Gui) handleNetworksRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
options := []*removeNetworkOption{
|
||||
{
|
||||
description: gui.Tr.Remove,
|
||||
command: utils.WithShortSha("docker network rm " + network.Name),
|
||||
command: utils.WithShortSha("container network rm " + network.Name),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +121,7 @@ func (gui *Gui) handleNetworksRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
func (gui *Gui) handlePruneNetworks() error {
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneNetworks, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
|
||||
err := gui.DockerCommand.PruneNetworks()
|
||||
err := gui.ContainerCmd.PruneNetworks()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
|
@ -155,7 +136,7 @@ func (gui *Gui) handleNetworksCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
||||
commandObject := gui.ContainerCmd.Client.NewCommandObject(commands.CommandObject{
|
||||
Network: network,
|
||||
})
|
||||
|
||||
|
|
@ -173,7 +154,7 @@ func (gui *Gui) handleNetworksBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
|||
}
|
||||
|
||||
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Networks...)
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
|
||||
commandObject := gui.ContainerCmd.Client.NewCommandObject(commands.CommandObject{})
|
||||
|
||||
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"github.com/samber/lo"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
|
||||
)
|
||||
|
||||
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
package gui
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
|
||||
)
|
||||
|
||||
func (gui *Gui) intoInterface() panels.IGui {
|
||||
return gui
|
||||
}
|
||||
|
||||
func (gui *Gui) FilterString(view *gocui.View) string {
|
||||
if gui.State.Filter.active && gui.State.Filter.panel != nil && gui.State.Filter.panel.GetView() == view {
|
||||
return gui.State.Filter.needle
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package panels
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/tasks"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import (
|
|||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/tasks"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import (
|
|||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/asciigraph"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/mcuadros/go-lookup"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
|
@ -33,9 +33,9 @@ func RenderStats(userConfig *config.UserConfig, container *commands.Container, v
|
|||
graphs[i] = utils.ColoredString(graph, utils.GetColorAttribute(spec.Color))
|
||||
}
|
||||
|
||||
pidsCount := fmt.Sprintf("PIDs: %d", stats.ClientStats.PidsStats.Current)
|
||||
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.RxBytes))
|
||||
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.TxBytes))
|
||||
pidsCount := fmt.Sprintf("Processes: %d", stats.ClientStats.NumProcesses)
|
||||
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(int(stats.ClientStats.NetworkRxBytes)))
|
||||
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(int(stats.ClientStats.NetworkTxBytes)))
|
||||
|
||||
originalStats, err := utils.MarshalIntoYaml(stats)
|
||||
if err != nil {
|
||||
|
|
@ -53,7 +53,6 @@ func RenderStats(userConfig *config.UserConfig, container *commands.Container, v
|
|||
return contents, nil
|
||||
}
|
||||
|
||||
// plotGraph returns the plotted graph based on the graph spec and the stat history
|
||||
func plotGraph(container *commands.Container, spec config.GraphConfig, width int) (string, error) {
|
||||
container.StatsMutex.Lock()
|
||||
defer container.StatsMutex.Unlock()
|
||||
|
|
@ -105,7 +104,6 @@ func plotGraph(container *commands.Container, spec config.GraphConfig, width int
|
|||
), nil
|
||||
}
|
||||
|
||||
// from Dave C's answer at https://stackoverflow.com/questions/20767724/converting-unknown-interface-to-float64-in-golang
|
||||
func getFloat(unk interface{}) (float64, error) {
|
||||
floatType := reflect.TypeOf(float64(0))
|
||||
stringType := reflect.TypeOf("")
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@ package presentation
|
|||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
|
|
@ -26,34 +24,36 @@ func GetContainerDisplayStrings(guiConfig *config.GuiConfig, container *commands
|
|||
}
|
||||
|
||||
func displayContainerImage(container *commands.Container) string {
|
||||
return strings.TrimPrefix(container.Container.Image, "sha256:")
|
||||
return strings.TrimPrefix(container.GetImage(), "sha256:")
|
||||
}
|
||||
|
||||
func displayPorts(c *commands.Container) string {
|
||||
portStrings := lo.Map(c.Container.Ports, func(port container.Port, _ int) string {
|
||||
if port.PublicPort == 0 {
|
||||
return fmt.Sprintf("%d/%s", port.PrivatePort, port.Type)
|
||||
ports := c.AppleContainer.Configuration.PublishedPorts
|
||||
portStrings := lo.Map(ports, func(port struct {
|
||||
HostIP string `json:"hostIP"`
|
||||
HostPort int `json:"hostPort"`
|
||||
ContainerPort int `json:"containerPort"`
|
||||
Protocol string `json:"protocol"`
|
||||
}, _ int) string {
|
||||
if port.HostPort == 0 {
|
||||
return fmt.Sprintf("%d/%s", port.ContainerPort, port.Protocol)
|
||||
}
|
||||
|
||||
// docker ps will show '0.0.0.0:80->80/tcp' but we'll show
|
||||
// '80->80/tcp' instead to save space (unless the IP is something other than
|
||||
// 0.0.0.0)
|
||||
ipString := ""
|
||||
if port.IP != "0.0.0.0" {
|
||||
ipString = port.IP + ":"
|
||||
if port.HostIP != "0.0.0.0" && port.HostIP != "" {
|
||||
ipString = port.HostIP + ":"
|
||||
}
|
||||
return fmt.Sprintf("%s%d->%d/%s", ipString, port.PublicPort, port.PrivatePort, port.Type)
|
||||
return fmt.Sprintf("%s%d->%d/%s", ipString, port.HostPort, port.ContainerPort, port.Protocol)
|
||||
})
|
||||
|
||||
// sorting because the order of the ports is not deterministic
|
||||
// and we don't want to have them constantly swapping
|
||||
sort.Strings(portStrings)
|
||||
|
||||
return strings.Join(portStrings, ", ")
|
||||
}
|
||||
|
||||
// getContainerDisplayStatus returns the colored status of the container
|
||||
func getContainerDisplayStatus(guiConfig *config.GuiConfig, c *commands.Container) string {
|
||||
status := c.GetStatus()
|
||||
|
||||
shortStatusMap := map[string]string{
|
||||
"paused": "P",
|
||||
"exited": "X",
|
||||
|
|
@ -62,6 +62,7 @@ func getContainerDisplayStatus(guiConfig *config.GuiConfig, c *commands.Containe
|
|||
"restarting": "RS",
|
||||
"running": "R",
|
||||
"dead": "D",
|
||||
"stopped": "S",
|
||||
}
|
||||
|
||||
iconStatusMap := map[string]rune{
|
||||
|
|
@ -72,33 +73,34 @@ func getContainerDisplayStatus(guiConfig *config.GuiConfig, c *commands.Containe
|
|||
"restarting": '⟳',
|
||||
"running": '▶',
|
||||
"dead": '!',
|
||||
"stopped": '■',
|
||||
}
|
||||
|
||||
var containerState string
|
||||
switch guiConfig.ContainerStatusHealthStyle {
|
||||
case "short":
|
||||
containerState = shortStatusMap[c.Container.State]
|
||||
containerState = shortStatusMap[status]
|
||||
case "icon":
|
||||
containerState = string(iconStatusMap[c.Container.State])
|
||||
containerState = string(iconStatusMap[status])
|
||||
case "long":
|
||||
fallthrough
|
||||
default:
|
||||
containerState = c.Container.State
|
||||
containerState = status
|
||||
}
|
||||
|
||||
return utils.ColoredString(containerState, getContainerColor(c))
|
||||
}
|
||||
|
||||
// GetDisplayStatus returns the exit code if the container has exited, and the health status if the container is running (and has a health check)
|
||||
func getContainerDisplaySubstatus(guiConfig *config.GuiConfig, c *commands.Container) string {
|
||||
if !c.DetailsLoaded() {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch c.Container.State {
|
||||
case "exited":
|
||||
status := c.GetStatus()
|
||||
switch status {
|
||||
case "exited", "stopped":
|
||||
return utils.ColoredString(
|
||||
fmt.Sprintf("(%s)", strconv.Itoa(c.Details.State.ExitCode)), getContainerColor(c),
|
||||
fmt.Sprintf("(%d)", 0), getContainerColor(c),
|
||||
)
|
||||
case "running":
|
||||
return getHealthStatus(guiConfig, c)
|
||||
|
|
@ -108,51 +110,9 @@ func getContainerDisplaySubstatus(guiConfig *config.GuiConfig, c *commands.Conta
|
|||
}
|
||||
|
||||
func getHealthStatus(guiConfig *config.GuiConfig, c *commands.Container) string {
|
||||
if !c.DetailsLoaded() {
|
||||
return ""
|
||||
}
|
||||
|
||||
healthStatusColorMap := map[string]color.Attribute{
|
||||
"healthy": color.FgGreen,
|
||||
"unhealthy": color.FgRed,
|
||||
"starting": color.FgYellow,
|
||||
}
|
||||
|
||||
if c.Details.State.Health == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
shortHealthStatusMap := map[string]string{
|
||||
"healthy": "H",
|
||||
"unhealthy": "U",
|
||||
"starting": "S",
|
||||
}
|
||||
|
||||
iconHealthStatusMap := map[string]rune{
|
||||
"healthy": '✔',
|
||||
"unhealthy": '?',
|
||||
"starting": '…',
|
||||
}
|
||||
|
||||
var healthStatus string
|
||||
switch guiConfig.ContainerStatusHealthStyle {
|
||||
case "short":
|
||||
healthStatus = shortHealthStatusMap[c.Details.State.Health.Status]
|
||||
case "icon":
|
||||
healthStatus = string(iconHealthStatusMap[c.Details.State.Health.Status])
|
||||
case "long":
|
||||
fallthrough
|
||||
default:
|
||||
healthStatus = c.Details.State.Health.Status
|
||||
}
|
||||
|
||||
if healthStatusColor, ok := healthStatusColorMap[c.Details.State.Health.Status]; ok {
|
||||
return utils.ColoredString(fmt.Sprintf("(%s)", healthStatus), healthStatusColor)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getDisplayCPUPerc colors the cpu percentage based on how extreme it is
|
||||
func getDisplayCPUPerc(c *commands.Container) string {
|
||||
stats, ok := c.GetLastStats()
|
||||
if !ok {
|
||||
|
|
@ -174,16 +134,11 @@ func getDisplayCPUPerc(c *commands.Container) string {
|
|||
return utils.ColoredString(formattedPercentage, clr)
|
||||
}
|
||||
|
||||
// getContainerColor Container color
|
||||
func getContainerColor(c *commands.Container) color.Attribute {
|
||||
switch c.Container.State {
|
||||
case "exited":
|
||||
// This means the colour may be briefly yellow and then switch to red upon starting
|
||||
// Not sure what a better alternative is.
|
||||
if !c.DetailsLoaded() || c.Details.State.ExitCode == 0 {
|
||||
return color.FgYellow
|
||||
}
|
||||
return color.FgRed
|
||||
status := c.GetStatus()
|
||||
switch status {
|
||||
case "exited", "stopped":
|
||||
return color.FgYellow
|
||||
case "created":
|
||||
return color.FgCyan
|
||||
case "running":
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
)
|
||||
|
||||
func GetImageDisplayStrings(image *commands.Image) []string {
|
||||
return []string{
|
||||
image.Name,
|
||||
image.Tag,
|
||||
utils.FormatDecimalBytes(int(image.Image.Size)),
|
||||
utils.FormatDecimalBytes(int(image.AppleImage.Descriptor.Size)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package presentation
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
import "github.com/jesseduffield/lazycontainer/pkg/gui/types"
|
||||
|
||||
func GetMenuItemDisplayStrings(menuItem *types.MenuItem) []string {
|
||||
return menuItem.LabelColumns
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package presentation
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
import "github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
|
||||
func GetNetworkDisplayStrings(network *commands.Network) []string {
|
||||
return []string{network.Network.Driver, network.Name}
|
||||
return []string{network.AppleNetwork.Config.Mode, network.Name}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
package presentation
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
|
||||
func GetProjectDisplayStrings(project *commands.Project) []string {
|
||||
return []string{project.Name}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
)
|
||||
|
||||
func GetServiceDisplayStrings(guiConfig *config.GuiConfig, service *commands.Service) []string {
|
||||
if service.Container == nil {
|
||||
var containerState string
|
||||
switch guiConfig.ContainerStatusHealthStyle {
|
||||
case "short":
|
||||
containerState = "n"
|
||||
case "icon":
|
||||
containerState = "."
|
||||
case "long":
|
||||
fallthrough
|
||||
default:
|
||||
containerState = "none"
|
||||
}
|
||||
|
||||
return []string{
|
||||
utils.ColoredString(containerState, color.FgBlue),
|
||||
"",
|
||||
service.Name,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
}
|
||||
}
|
||||
|
||||
container := service.Container
|
||||
return []string{
|
||||
getContainerDisplayStatus(guiConfig, container),
|
||||
getContainerDisplaySubstatus(guiConfig, container),
|
||||
service.Name,
|
||||
getDisplayCPUPerc(container),
|
||||
utils.ColoredString(displayPorts(container), color.FgYellow),
|
||||
utils.ColoredString(displayContainerImage(container), color.FgMagenta),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
package presentation
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
import (
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
)
|
||||
|
||||
func GetVolumeDisplayStrings(volume *commands.Volume) []string {
|
||||
return []string{volume.Volume.Driver, volume.Name}
|
||||
return []string{utils.FormatBinaryBytes(int(volume.AppleVolume.SizeInBytes)), volume.Name}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,230 +0,0 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/yaml"
|
||||
)
|
||||
|
||||
func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
|
||||
return &panels.SideListPanel[*commands.Project]{
|
||||
ContextState: &panels.ContextState[*commands.Project]{
|
||||
GetMainTabs: func() []panels.MainTab[*commands.Project] {
|
||||
return []panels.MainTab[*commands.Project]{
|
||||
{
|
||||
Key: "logs",
|
||||
Title: gui.Tr.LogsTitle,
|
||||
Render: gui.renderAllLogs,
|
||||
},
|
||||
{
|
||||
Key: "config",
|
||||
Title: gui.Tr.DockerComposeConfigTitle,
|
||||
Render: gui.renderDockerComposeConfig,
|
||||
},
|
||||
{
|
||||
Key: "credits",
|
||||
Title: gui.Tr.CreditsTitle,
|
||||
Render: gui.renderCredits,
|
||||
},
|
||||
}
|
||||
},
|
||||
GetItemContextCacheKey: func(project *commands.Project) string {
|
||||
return "projects-" + project.Name
|
||||
},
|
||||
},
|
||||
|
||||
ListPanel: panels.ListPanel[*commands.Project]{
|
||||
List: panels.NewFilteredList[*commands.Project](),
|
||||
View: gui.Views.Project,
|
||||
},
|
||||
NoItemsMessage: "",
|
||||
Gui: gui.intoInterface(),
|
||||
|
||||
Sort: func(a *commands.Project, b *commands.Project) bool {
|
||||
return a.Name < b.Name
|
||||
},
|
||||
GetTableCells: presentation.GetProjectDisplayStrings,
|
||||
OnSelect: func(project *commands.Project) error {
|
||||
// When a different project is selected, re-filter services and
|
||||
// containers to show only those belonging to the selected project.
|
||||
return gui.renderContainersAndServices()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshProject() error {
|
||||
projects := gui.getDiscoveredProjects()
|
||||
|
||||
// Preserve the current selection across refreshes. On the first refresh,
|
||||
// select the project specified via -p flag, or fall back to the local project.
|
||||
selectedName := gui.getSelectedProjectName()
|
||||
if selectedName == "" {
|
||||
if gui.Config.ProjectName != "" {
|
||||
selectedName = gui.Config.ProjectName
|
||||
} else {
|
||||
selectedName = gui.DockerCommand.LocalProjectName
|
||||
}
|
||||
}
|
||||
|
||||
gui.Panels.Projects.SetItems(projects)
|
||||
|
||||
if selectedName != "" {
|
||||
for i, p := range gui.Panels.Projects.List.GetItems() {
|
||||
if p.Name == selectedName {
|
||||
gui.Panels.Projects.SetSelectedLineIdx(i)
|
||||
gui.Panels.Projects.Refocus()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gui.Panels.Projects.RerenderList()
|
||||
}
|
||||
|
||||
// getDiscoveredProjects returns all docker compose projects by examining container labels.
|
||||
// The local project (from docker-compose.yml in the current directory) is included if
|
||||
// it has running containers or if InDockerComposeProject is true.
|
||||
func (gui *Gui) getDiscoveredProjects() []*commands.Project {
|
||||
containers := gui.Panels.Containers.List.GetAllItems()
|
||||
projectNames := gui.DockerCommand.GetProjectNames(containers)
|
||||
|
||||
// If we're in a docker compose project but it has no running containers,
|
||||
// still include it. We don't fall back to the directory name here to avoid
|
||||
// briefly flashing the wrong project name on startup.
|
||||
localName := gui.DockerCommand.LocalProjectName
|
||||
|
||||
if gui.DockerCommand.InDockerComposeProject && localName != "" {
|
||||
found := false
|
||||
for _, name := range projectNames {
|
||||
if name == localName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
projectNames = append([]string{localName}, projectNames...)
|
||||
}
|
||||
}
|
||||
|
||||
projects := make([]*commands.Project, len(projectNames))
|
||||
for i, name := range projectNames {
|
||||
projects[i] = &commands.Project{Name: name}
|
||||
}
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
// getSelectedProjectName returns the name of the currently selected project,
|
||||
// or empty string if none is selected.
|
||||
func (gui *Gui) getSelectedProjectName() string {
|
||||
project, err := gui.Panels.Projects.GetSelectedItem()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return project.Name
|
||||
}
|
||||
|
||||
func (gui *Gui) renderCredits(_project *commands.Project) tasks.TaskFunc {
|
||||
return gui.NewSimpleRenderStringTask(func() string { return gui.creditsStr() })
|
||||
}
|
||||
|
||||
func (gui *Gui) creditsStr() string {
|
||||
var configBuf bytes.Buffer
|
||||
_ = yaml.NewEncoder(&configBuf, yaml.IncludeOmitted).Encode(gui.Config.UserConfig)
|
||||
|
||||
return strings.Join(
|
||||
[]string{
|
||||
lazydockerTitle(),
|
||||
"Copyright (c) 2019 Jesse Duffield",
|
||||
"Keybindings: https://github.com/jesseduffield/lazydocker/blob/master/docs/keybindings",
|
||||
"Config Options: https://github.com/jesseduffield/lazydocker/blob/master/docs/Config.md",
|
||||
"Raise an Issue: https://github.com/jesseduffield/lazydocker/issues",
|
||||
utils.ColoredString("Buy Jesse a coffee: https://github.com/sponsors/jesseduffield", color.FgMagenta), // caffeine ain't free
|
||||
"Here's your lazydocker config when merged in with the defaults (you can open your config by pressing 'o'):",
|
||||
utils.ColoredYamlString(configBuf.String()),
|
||||
}, "\n\n")
|
||||
}
|
||||
|
||||
func (gui *Gui) renderAllLogs(project *commands.Project) tasks.TaskFunc {
|
||||
return gui.NewTask(TaskOpts{
|
||||
Autoscroll: true,
|
||||
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
||||
Func: func(ctx context.Context) {
|
||||
gui.clearMainView()
|
||||
|
||||
cmd := gui.OSCommand.RunCustomCommand(
|
||||
utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.AllLogs,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||
),
|
||||
)
|
||||
|
||||
cmd.Stdout = gui.Views.Main
|
||||
cmd.Stderr = gui.Views.Main
|
||||
|
||||
gui.OSCommand.PrepareForChildren(cmd)
|
||||
_ = cmd.Start()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if err := gui.OSCommand.Kill(cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
_ = cmd.Wait()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) renderDockerComposeConfig(project *commands.Project) tasks.TaskFunc {
|
||||
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
||||
return gui.NewSimpleRenderStringTask(func() string {
|
||||
return "Compose config is not available for non-local projects"
|
||||
})
|
||||
}
|
||||
return gui.NewSimpleRenderStringTask(func() string {
|
||||
return utils.ColoredYamlString(gui.DockerCommand.DockerComposeConfigForProject(project))
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.openFile(gui.Config.ConfigFilename())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEditConfig(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.editFile(gui.Config.ConfigFilename())
|
||||
}
|
||||
|
||||
func lazydockerTitle() string {
|
||||
return `
|
||||
_ _ _
|
||||
| | | | | |
|
||||
| | __ _ _____ _ __| | ___ ___| | _____ _ __
|
||||
| |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` |/ _ \ / __| |/ / _ \ '__|
|
||||
| | (_| |/ /| |_| | (_| | (_) | (__| < __/ |
|
||||
|_|\__,_/___|\__, |\__,_|\___/ \___|_|\_\___|_|
|
||||
__/ |
|
||||
|___/
|
||||
`
|
||||
}
|
||||
|
||||
// handleViewAllLogs switches to a subprocess viewing all the logs from docker-compose
|
||||
func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error {
|
||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||
c, err := gui.DockerCommand.ViewAllLogs(project)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return gui.runSubprocess(c)
|
||||
}
|
||||
|
|
@ -1,559 +0,0 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] {
|
||||
return &panels.SideListPanel[*commands.Service]{
|
||||
ContextState: &panels.ContextState[*commands.Service]{
|
||||
GetMainTabs: func() []panels.MainTab[*commands.Service] {
|
||||
return []panels.MainTab[*commands.Service]{
|
||||
{
|
||||
Key: "logs",
|
||||
Title: gui.Tr.LogsTitle,
|
||||
Render: gui.renderServiceLogs,
|
||||
},
|
||||
{
|
||||
Key: "stats",
|
||||
Title: gui.Tr.StatsTitle,
|
||||
Render: gui.renderServiceStats,
|
||||
},
|
||||
{
|
||||
Key: "container-env",
|
||||
Title: gui.Tr.ContainerEnvTitle,
|
||||
Render: gui.renderServiceContainerEnv,
|
||||
},
|
||||
{
|
||||
Key: "container-config",
|
||||
Title: gui.Tr.ContainerConfigTitle,
|
||||
Render: gui.renderServiceContainerConfig,
|
||||
},
|
||||
{
|
||||
Key: "top",
|
||||
Title: gui.Tr.TopTitle,
|
||||
Render: gui.renderServiceTop,
|
||||
},
|
||||
}
|
||||
},
|
||||
GetItemContextCacheKey: func(service *commands.Service) string {
|
||||
if service.Container == nil {
|
||||
return "services-" + service.ID
|
||||
}
|
||||
return "services-" + service.ID + "-" + service.Container.ID + "-" + service.Container.Container.State
|
||||
},
|
||||
},
|
||||
ListPanel: panels.ListPanel[*commands.Service]{
|
||||
List: panels.NewFilteredList[*commands.Service](),
|
||||
View: gui.Views.Services,
|
||||
},
|
||||
NoItemsMessage: gui.Tr.NoServices,
|
||||
Gui: gui.intoInterface(),
|
||||
// sort services first by whether they have a linked container, and second by alphabetical order
|
||||
Sort: func(a *commands.Service, b *commands.Service) bool {
|
||||
if a.Container != nil && b.Container == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if a.Container == nil && b.Container != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return a.Name < b.Name
|
||||
},
|
||||
Filter: func(service *commands.Service) bool {
|
||||
selectedProject := gui.getSelectedProjectName()
|
||||
if selectedProject == "" {
|
||||
// Before any project is selected (e.g. startup), default to
|
||||
// the local project so we don't briefly flash all services.
|
||||
selectedProject = gui.DockerCommand.LocalProjectName
|
||||
}
|
||||
if selectedProject == "" {
|
||||
return true
|
||||
}
|
||||
return service.ProjectName == selectedProject
|
||||
},
|
||||
GetTableCells: func(service *commands.Service) []string {
|
||||
return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service)
|
||||
},
|
||||
Hide: func() bool {
|
||||
// Show services panel if there are any compose projects (local or discovered)
|
||||
return !gui.DockerCommand.InDockerComposeProject && len(gui.Panels.Services.List.GetAllItems()) == 0
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) renderServiceContainerConfig(service *commands.Service) tasks.TaskFunc {
|
||||
if service.Container == nil {
|
||||
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer })
|
||||
}
|
||||
|
||||
return gui.renderContainerConfig(service.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) renderServiceContainerEnv(service *commands.Service) tasks.TaskFunc {
|
||||
if service.Container == nil {
|
||||
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer })
|
||||
}
|
||||
|
||||
return gui.renderContainerEnv(service.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) renderServiceStats(service *commands.Service) tasks.TaskFunc {
|
||||
if service.Container == nil {
|
||||
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer })
|
||||
}
|
||||
|
||||
return gui.renderContainerStats(service.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) renderServiceTop(service *commands.Service) tasks.TaskFunc {
|
||||
return gui.NewTickerTask(TickerTaskOpts{
|
||||
Func: func(ctx context.Context, notifyStopped chan struct{}) {
|
||||
contents, err := service.RenderTop(ctx)
|
||||
if err != nil {
|
||||
gui.RenderStringMain(err.Error())
|
||||
}
|
||||
|
||||
gui.reRenderStringMain(contents)
|
||||
},
|
||||
Duration: time.Second,
|
||||
Before: func(ctx context.Context) { gui.clearMainView() },
|
||||
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
||||
Autoscroll: false,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) renderServiceLogs(service *commands.Service) tasks.TaskFunc {
|
||||
if service.Container == nil {
|
||||
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainerForService })
|
||||
}
|
||||
|
||||
return gui.renderContainerLogsToMain(service.Container)
|
||||
}
|
||||
|
||||
type commandOption struct {
|
||||
description string
|
||||
command string
|
||||
onPress func() error
|
||||
}
|
||||
|
||||
func (r *commandOption) getDisplayStrings() []string {
|
||||
return []string{r.description, color.New(color.FgCyan).Sprint(r.command)}
|
||||
}
|
||||
|
||||
// isServiceFromLocalProject returns true if the given service belongs to the
|
||||
// local compose project (the one whose compose file is in the current directory).
|
||||
// Compose commands like up/stop/restart only work for local project services.
|
||||
func (gui *Gui) isServiceFromLocalProject(service *commands.Service) bool {
|
||||
return service.ProjectName == gui.DockerCommand.LocalProjectName
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
|
||||
composeCommand := gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}).DockerCompose
|
||||
|
||||
options := []*commandOption{
|
||||
{
|
||||
description: gui.Tr.Remove,
|
||||
command: fmt.Sprintf("%s rm --stop --force %s", composeCommand, service.Name),
|
||||
},
|
||||
{
|
||||
description: gui.Tr.RemoveWithVolumes,
|
||||
command: fmt.Sprintf("%s rm --stop --force -v %s", composeCommand, service.Name),
|
||||
},
|
||||
}
|
||||
|
||||
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: option.getDisplayStrings(),
|
||||
OnPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
if err := gui.OSCommand.RunCommand(option.command); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return gui.Menu(CreateMenuOptions{
|
||||
Title: "",
|
||||
Items: menuItems,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServicePause(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if service.Container == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.PauseContainer(service.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
if service.Container == nil {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
return service.Container.Stop()
|
||||
}
|
||||
if err := service.Stop(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServiceUp(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.UppingServiceStatus, func() error {
|
||||
if err := service.Up(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
if service.Container == nil {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
return service.Container.Restart()
|
||||
}
|
||||
if err := service.Restart(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServiceStart(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
if service.Container == nil {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error {
|
||||
return service.Container.Start()
|
||||
})
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error {
|
||||
if err := service.Start(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if service.Container == nil {
|
||||
return gui.createErrorPanel(gui.Tr.NoContainers)
|
||||
}
|
||||
|
||||
c, err := service.Attach()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return gui.runSubprocess(c)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := service.ViewLogs()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return gui.runSubprocess(c)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
|
||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmUpProject, func(g *gocui.Gui, v *gocui.View) error {
|
||||
cmdStr := utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.Up,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||
)
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error {
|
||||
if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error {
|
||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
downCommand := utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.Down,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||
)
|
||||
|
||||
downWithVolumesCommand := utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||
)
|
||||
|
||||
options := []*commandOption{
|
||||
{
|
||||
description: gui.Tr.Down,
|
||||
command: downCommand,
|
||||
onPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error {
|
||||
if err := gui.OSCommand.RunCommand(downCommand); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.DownWithVolumes,
|
||||
command: downWithVolumesCommand,
|
||||
onPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error {
|
||||
if err := gui.OSCommand.RunCommand(downWithVolumesCommand); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: option.getDisplayStrings(),
|
||||
OnPress: option.onPress,
|
||||
}
|
||||
})
|
||||
|
||||
return gui.Menu(CreateMenuOptions{
|
||||
Title: "",
|
||||
Items: menuItems,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
|
||||
rebuildCommand := utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.RebuildService,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
||||
)
|
||||
|
||||
recreateCommand := utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.RecreateService,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
||||
)
|
||||
|
||||
options := []*commandOption{
|
||||
{
|
||||
description: gui.Tr.Restart,
|
||||
command: utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.RestartService,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
||||
),
|
||||
onPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
||||
if err := service.Restart(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.Recreate,
|
||||
command: utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.RecreateService,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
||||
),
|
||||
onPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
||||
if err := gui.OSCommand.RunCommand(recreateCommand); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.Rebuild,
|
||||
command: utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.RebuildService,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
||||
),
|
||||
onPress: func() error {
|
||||
return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: option.getDisplayStrings(),
|
||||
OnPress: option.onPress,
|
||||
}
|
||||
})
|
||||
|
||||
return gui.Menu(CreateMenuOptions{
|
||||
Title: "",
|
||||
Items: menuItems,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServicesCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
||||
Service: service,
|
||||
Container: service.Container,
|
||||
})
|
||||
|
||||
var customCommands []config.CustomCommand
|
||||
|
||||
customServiceCommands := gui.Config.UserConfig.CustomCommands.Services
|
||||
// we only include service commands if they have no serviceNames defined or if our service happens to be one of the serviceNames defined
|
||||
L:
|
||||
for _, cmd := range customServiceCommands {
|
||||
if len(cmd.ServiceNames) == 0 {
|
||||
customCommands = append(customCommands, cmd)
|
||||
continue L
|
||||
}
|
||||
for _, serviceName := range cmd.ServiceNames {
|
||||
if serviceName == service.Name {
|
||||
// appending these to the top given they're more likely to be selected
|
||||
customCommands = append([]config.CustomCommand{cmd}, customCommands...)
|
||||
continue L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if service.Container != nil {
|
||||
customCommands = append(customCommands, gui.Config.UserConfig.CustomCommands.Containers...)
|
||||
}
|
||||
|
||||
return gui.createCustomCommandMenu(customCommands, commandObject)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServicesBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||
bulkCommands := gui.Config.UserConfig.BulkCommands.Services
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project})
|
||||
|
||||
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServicesExecShell(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
container := service.Container
|
||||
if container == nil {
|
||||
return gui.createErrorPanel(gui.Tr.NoContainers)
|
||||
}
|
||||
|
||||
return gui.containerExecShell(container)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServicesOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
container := service.Container
|
||||
if container == nil {
|
||||
return gui.createErrorPanel(gui.Tr.NoContainers)
|
||||
}
|
||||
|
||||
return gui.openContainerInBrowser(container)
|
||||
}
|
||||
|
|
@ -4,40 +4,31 @@ import (
|
|||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func sampleContainers() []*commands.Container {
|
||||
return []*commands.Container{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: container.Summary{
|
||||
State: "exited",
|
||||
},
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
AppleContainer: commands.AppleContainer{Status: "exited"},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: container.Summary{
|
||||
State: "running",
|
||||
},
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
AppleContainer: commands.AppleContainer{Status: "running"},
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: container.Summary{
|
||||
State: "running",
|
||||
},
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
AppleContainer: commands.AppleContainer{Status: "running"},
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: container.Summary{
|
||||
State: "created",
|
||||
},
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
AppleContainer: commands.AppleContainer{Status: "created"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -45,32 +36,24 @@ func sampleContainers() []*commands.Container {
|
|||
func expectedPerStatusContainers() []*commands.Container {
|
||||
return []*commands.Container{
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: container.Summary{
|
||||
State: "running",
|
||||
},
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
AppleContainer: commands.AppleContainer{Status: "running"},
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: container.Summary{
|
||||
State: "running",
|
||||
},
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
AppleContainer: commands.AppleContainer{Status: "running"},
|
||||
},
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: container.Summary{
|
||||
State: "exited",
|
||||
},
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
AppleContainer: commands.AppleContainer{Status: "exited"},
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: container.Summary{
|
||||
State: "created",
|
||||
},
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
AppleContainer: commands.AppleContainer{Status: "created"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -78,40 +61,32 @@ func expectedPerStatusContainers() []*commands.Container {
|
|||
func expectedLegacySortedContainers() []*commands.Container {
|
||||
return []*commands.Container{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: container.Summary{
|
||||
State: "exited",
|
||||
},
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
AppleContainer: commands.AppleContainer{Status: "exited"},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: container.Summary{
|
||||
State: "running",
|
||||
},
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
AppleContainer: commands.AppleContainer{Status: "running"},
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: container.Summary{
|
||||
State: "running",
|
||||
},
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
AppleContainer: commands.AppleContainer{Status: "running"},
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: container.Summary{
|
||||
State: "created",
|
||||
},
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
AppleContainer: commands.AppleContainer{Status: "created"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqualContainers(t *testing.T, left *commands.Container, right *commands.Container) {
|
||||
t.Helper()
|
||||
assert.Equal(t, left.Container.State, right.Container.State)
|
||||
assert.Equal(t, left.Container.ID, right.Container.ID)
|
||||
assert.Equal(t, left.GetStatus(), right.GetStatus())
|
||||
assert.Equal(t, left.ID, right.ID)
|
||||
assert.Equal(t, left.Name, right.Name)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) runSubprocess(cmd *exec.Cmd) error {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/tasks"
|
||||
)
|
||||
|
||||
func (gui *Gui) QueueTask(f func(ctx context.Context)) error {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spkg/bom"
|
||||
)
|
||||
|
|
@ -364,8 +364,6 @@ func (gui *Gui) currentListPanel() (panels.ISideListPanel, bool) {
|
|||
|
||||
func (gui *Gui) allSidePanels() []panels.ISideListPanel {
|
||||
return []panels.ISideListPanel{
|
||||
gui.Panels.Projects,
|
||||
gui.Panels.Services,
|
||||
gui.Panels.Containers,
|
||||
gui.Panels.Images,
|
||||
gui.Panels.Volumes,
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@ import (
|
|||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// See https://github.com/xtermjs/xterm.js/issues/4238
|
||||
// VSCode is soon to fix this in an upcoming update.
|
||||
// Once that's done, we can scrap the HIDE_UNDERSCORES variable
|
||||
var (
|
||||
underscoreEnvChecked bool
|
||||
hideUnderscores bool
|
||||
|
|
@ -26,48 +23,33 @@ func hideUnderScores() bool {
|
|||
}
|
||||
|
||||
type Views struct {
|
||||
// side panels
|
||||
Project *gocui.View
|
||||
Services *gocui.View
|
||||
Containers *gocui.View
|
||||
Images *gocui.View
|
||||
Volumes *gocui.View
|
||||
Networks *gocui.View
|
||||
|
||||
// main panel
|
||||
Main *gocui.View
|
||||
|
||||
// bottom line
|
||||
Options *gocui.View
|
||||
Information *gocui.View
|
||||
AppStatus *gocui.View
|
||||
// text that prompts you to enter text in the Filter view
|
||||
FilterPrefix *gocui.View
|
||||
// appears next to the SearchPrefix view, it's where you type in the search string
|
||||
Filter *gocui.View
|
||||
|
||||
// popups
|
||||
Confirmation *gocui.View
|
||||
Menu *gocui.View
|
||||
|
||||
// will cover everything when it appears
|
||||
Limit *gocui.View
|
||||
}
|
||||
|
||||
type viewNameMapping struct {
|
||||
viewPtr **gocui.View
|
||||
name string
|
||||
// if true, we handle the position/size of the view in arrangement.go. Otherwise
|
||||
// we handle it manually.
|
||||
autoPosition bool
|
||||
}
|
||||
|
||||
func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
|
||||
return []viewNameMapping{
|
||||
// first layer. Ordering within this layer does not matter because there are
|
||||
// no overlapping views
|
||||
{viewPtr: &gui.Views.Project, name: "project", autoPosition: true},
|
||||
{viewPtr: &gui.Views.Services, name: "services", autoPosition: true},
|
||||
{viewPtr: &gui.Views.Containers, name: "containers", autoPosition: true},
|
||||
{viewPtr: &gui.Views.Images, name: "images", autoPosition: true},
|
||||
{viewPtr: &gui.Views.Volumes, name: "volumes", autoPosition: true},
|
||||
|
|
@ -75,18 +57,15 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
|
|||
|
||||
{viewPtr: &gui.Views.Main, name: "main", autoPosition: true},
|
||||
|
||||
// bottom line
|
||||
{viewPtr: &gui.Views.Options, name: "options", autoPosition: true},
|
||||
{viewPtr: &gui.Views.AppStatus, name: "appStatus", autoPosition: true},
|
||||
{viewPtr: &gui.Views.Information, name: "information", autoPosition: true},
|
||||
{viewPtr: &gui.Views.Filter, name: "filter", autoPosition: true},
|
||||
{viewPtr: &gui.Views.FilterPrefix, name: "filterPrefix", autoPosition: true},
|
||||
|
||||
// popups.
|
||||
{viewPtr: &gui.Views.Menu, name: "menu", autoPosition: false},
|
||||
{viewPtr: &gui.Views.Confirmation, name: "confirmation", autoPosition: false},
|
||||
|
||||
// this guy will cover everything else when it appears
|
||||
{viewPtr: &gui.Views.Limit, name: "limit", autoPosition: true},
|
||||
}
|
||||
}
|
||||
|
|
@ -115,41 +94,26 @@ func (gui *Gui) createAllViews() error {
|
|||
selectedLineBgColor := GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.SelectedLineBgColor)
|
||||
|
||||
gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
|
||||
// when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default.
|
||||
gui.Views.Main.IgnoreCarriageReturns = true
|
||||
|
||||
gui.Views.Project.Title = gui.Tr.ProjectTitle
|
||||
gui.Views.Project.TitlePrefix = "[1]"
|
||||
gui.Views.Project.Highlight = true
|
||||
gui.Views.Project.SelBgColor = selectedLineBgColor
|
||||
|
||||
gui.Views.Services.Highlight = true
|
||||
gui.Views.Services.Title = gui.Tr.ServicesTitle
|
||||
gui.Views.Services.TitlePrefix = "[2]"
|
||||
gui.Views.Services.SelBgColor = selectedLineBgColor
|
||||
|
||||
gui.Views.Containers.Highlight = true
|
||||
gui.Views.Containers.SelBgColor = selectedLineBgColor
|
||||
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject {
|
||||
gui.Views.Containers.Title = gui.Tr.ContainersTitle
|
||||
} else {
|
||||
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
|
||||
}
|
||||
gui.Views.Containers.TitlePrefix = "[3]"
|
||||
gui.Views.Containers.Title = gui.Tr.ContainersTitle
|
||||
gui.Views.Containers.TitlePrefix = "[1]"
|
||||
|
||||
gui.Views.Images.Highlight = true
|
||||
gui.Views.Images.Title = gui.Tr.ImagesTitle
|
||||
gui.Views.Images.SelBgColor = selectedLineBgColor
|
||||
gui.Views.Images.TitlePrefix = "[4]"
|
||||
gui.Views.Images.TitlePrefix = "[2]"
|
||||
|
||||
gui.Views.Volumes.Highlight = true
|
||||
gui.Views.Volumes.Title = gui.Tr.VolumesTitle
|
||||
gui.Views.Volumes.TitlePrefix = "[5]"
|
||||
gui.Views.Volumes.TitlePrefix = "[3]"
|
||||
gui.Views.Volumes.SelBgColor = selectedLineBgColor
|
||||
|
||||
gui.Views.Networks.Highlight = true
|
||||
gui.Views.Networks.Title = gui.Tr.NetworksTitle
|
||||
gui.Views.Networks.TitlePrefix = "[6]"
|
||||
gui.Views.Networks.TitlePrefix = "[4]"
|
||||
gui.Views.Networks.SelBgColor = selectedLineBgColor
|
||||
|
||||
gui.Views.Options.Frame = false
|
||||
|
|
@ -212,7 +176,6 @@ func (gui *Gui) popupViewNames() []string {
|
|||
return []string{"confirmation", "menu"}
|
||||
}
|
||||
|
||||
// these views have their position and size determined by arrangement.go
|
||||
func (gui *Gui) autoPositionedViewNames() []string {
|
||||
views := lo.Filter(gui.orderedViewNameMappings(), func(viewNameMapping viewNameMapping, _ int) bool {
|
||||
return viewNameMapping.autoPosition
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/commands"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/tasks"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
|
|
@ -37,16 +35,7 @@ func (gui *Gui) getVolumesPanel() *panels.SideListPanel[*commands.Volume] {
|
|||
},
|
||||
NoItemsMessage: gui.Tr.NoVolumes,
|
||||
Gui: gui.intoInterface(),
|
||||
// we're sorting these volumes based on whether they have labels defined,
|
||||
// because those are the ones you typically care about.
|
||||
// Within that, we also sort them alphabetically
|
||||
Sort: func(a *commands.Volume, b *commands.Volume) bool {
|
||||
if len(a.Volume.Labels) == 0 && len(b.Volume.Labels) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(a.Volume.Labels) > 0 && len(b.Volume.Labels) == 0 {
|
||||
return true
|
||||
}
|
||||
return a.Name < b.Name
|
||||
},
|
||||
GetTableCells: presentation.GetVolumeDisplayStrings,
|
||||
|
|
@ -61,26 +50,7 @@ func (gui *Gui) volumeConfigStr(volume *commands.Volume) string {
|
|||
padding := 15
|
||||
output := ""
|
||||
output += utils.WithPadding("Name: ", padding) + volume.Name + "\n"
|
||||
output += utils.WithPadding("Driver: ", padding) + volume.Volume.Driver + "\n"
|
||||
output += utils.WithPadding("Scope: ", padding) + volume.Volume.Scope + "\n"
|
||||
output += utils.WithPadding("Mountpoint: ", padding) + volume.Volume.Mountpoint + "\n"
|
||||
output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, volume.Volume.Labels) + "\n"
|
||||
output += utils.WithPadding("Options: ", padding) + utils.FormatMap(padding, volume.Volume.Options) + "\n"
|
||||
|
||||
output += utils.WithPadding("Status: ", padding)
|
||||
if volume.Volume.Status != nil {
|
||||
output += "\n"
|
||||
for k, v := range volume.Volume.Status {
|
||||
output += utils.FormatMapItem(padding, k, v)
|
||||
}
|
||||
} else {
|
||||
output += "n/a"
|
||||
}
|
||||
|
||||
if volume.Volume.UsageData != nil {
|
||||
output += utils.WithPadding("RefCount: ", padding) + fmt.Sprintf("%d", volume.Volume.UsageData.RefCount) + "\n"
|
||||
output += utils.WithPadding("Size: ", padding) + utils.FormatBinaryBytes(int(volume.Volume.UsageData.Size)) + "\n"
|
||||
}
|
||||
output += utils.WithPadding("Size: ", padding) + utils.FormatBinaryBytes(int(volume.AppleVolume.SizeInBytes)) + "\n"
|
||||
|
||||
return output
|
||||
}
|
||||
|
|
@ -94,7 +64,7 @@ func (gui *Gui) reloadVolumes() error {
|
|||
}
|
||||
|
||||
func (gui *Gui) refreshStateVolumes() error {
|
||||
volumes, err := gui.DockerCommand.RefreshVolumes()
|
||||
volumes, err := gui.ContainerCmd.RefreshVolumes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -113,19 +83,12 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
type removeVolumeOption struct {
|
||||
description string
|
||||
command string
|
||||
force bool
|
||||
}
|
||||
|
||||
options := []*removeVolumeOption{
|
||||
{
|
||||
description: gui.Tr.Remove,
|
||||
command: utils.WithShortSha("docker volume rm " + volume.Name),
|
||||
force: false,
|
||||
},
|
||||
{
|
||||
description: gui.Tr.ForceRemove,
|
||||
command: utils.WithShortSha("docker volume rm --force " + volume.Name),
|
||||
force: true,
|
||||
command: utils.WithShortSha("container volume rm " + volume.Name),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +97,7 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
LabelColumns: []string{option.description, color.New(color.FgRed).Sprint(option.command)},
|
||||
OnPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
if err := volume.Remove(option.force); err != nil {
|
||||
if err := volume.Remove(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
return nil
|
||||
|
|
@ -152,7 +115,7 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
func (gui *Gui) handlePruneVolumes() error {
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
|
||||
err := gui.DockerCommand.PruneVolumes()
|
||||
err := gui.ContainerCmd.PruneVolumes()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
|
@ -167,7 +130,7 @@ func (gui *Gui) handleVolumesCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
||||
commandObject := gui.ContainerCmd.Client.NewCommandObject(commands.CommandObject{
|
||||
Volume: volume,
|
||||
})
|
||||
|
||||
|
|
@ -185,7 +148,7 @@ func (gui *Gui) handleVolumesBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
|||
}
|
||||
|
||||
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Volumes...)
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
|
||||
commandObject := gui.ContainerCmd.Client.NewCommandObject(commands.CommandObject{})
|
||||
|
||||
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ func chineseSet() TranslationSet {
|
|||
|
||||
NoViewMachingNewLineFocusedSwitchStatement: "没有匹配 newLineFocused switch 语句的视图",
|
||||
|
||||
ErrorOccurred: "发生错误!请在 https://github.com/jesseduffield/lazydocker/issues 上创建一个问题",
|
||||
ErrorOccurred: "发生错误!请在 https://github.com/g-battaglia/lazy-apple-container/issues 上创建一个问题",
|
||||
ConnectionFailed: "无法连接到 Docker 客户端。您可能需要重新启动 Docker 客户端",
|
||||
UnattachableContainerError: "容器不支持 attaching。您必须使用“-it”标志运行服务,或者在docker-compose.yml文件中使用`stdin_open: true,tty: true`",
|
||||
WaitingForContainerInfo: "在 Docker 给我们更多关于容器的信息之前,无法继续。请几分钟后重试。",
|
||||
CannotAttachStoppedContainerError: "您不能 attach 到已停止的容器,您需要先启动它(您可以用 'r' 键来执行此操作)(是的,我懒得为您自动执行此操作)(很酷的是,我可以通过错误消息与您进行一对一的通讯)",
|
||||
CannotAccessDockerSocketError: "无法访问 Docker 套接字:unix:///var/run/docker.sock\n请以 root 用户身份运行 lazydocker 或阅读https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotAccessDockerSocketError: "无法访问 Docker 套接字:unix:///var/run/docker.sock\n请以 root 用户身份运行 lazyapple 或阅读https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotKillChildError: "等待三秒钟以停止子进程。可能有一个孤儿进程在您的系统上继续运行。",
|
||||
|
||||
Donate: "捐赠",
|
||||
|
|
@ -37,8 +37,8 @@ func chineseSet() TranslationSet {
|
|||
Menu: "菜单",
|
||||
MenuTitle: "菜单",
|
||||
Scroll: "滚动",
|
||||
OpenConfig: "打开lazydocker配置",
|
||||
EditConfig: "编辑lazydocker配置",
|
||||
OpenConfig: "打开lazyapple配置",
|
||||
EditConfig: "编辑lazyapple配置",
|
||||
Cancel: "取消",
|
||||
Remove: "移除",
|
||||
HideStopped: "隐藏/显示已停止的容器",
|
||||
|
|
@ -124,7 +124,7 @@ func chineseSet() TranslationSet {
|
|||
ConfirmPruneNetworks: "您确定要删除所有未使用的网络吗?",
|
||||
StopService: "您确定要停止此服务的容器吗?",
|
||||
StopContainer: "您确定要停止此容器吗?",
|
||||
PressEnterToReturn: "按 enter 返回 lazydocker(您可以在配置文件中设置 `gui.returnImmediately: true` 来禁用此提示)",
|
||||
PressEnterToReturn: "按 enter 返回 lazyapple(您可以在配置文件中设置 `gui.returnImmediately: true` 来禁用此提示)",
|
||||
|
||||
No: "否",
|
||||
Yes: "是",
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ func dutchSet() TranslationSet {
|
|||
|
||||
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
|
||||
|
||||
ErrorOccurred: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/jesseduffield/lazydocker/issues",
|
||||
ErrorOccurred: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/g-battaglia/lazy-apple-container/issues",
|
||||
ConnectionFailed: "connectie naar de docker client mislukt. Het zou kunnen dat je de docker client moet herstarten",
|
||||
UnattachableContainerError: "Container heeft geen ondersteuning voor vastmaken. Je zou de service met het '-it' argument kunnen draaien of stop dit in je `stdin_open: true, tty: true` docker-compose.yml",
|
||||
CannotAttachStoppedContainerError: "Je kan niet een vastgemaakte container stoppen, je moet het eerst starten (dit kan je doen met de 'r' toets) (ja ik ben te leu om dat voor je te doen automatisch)",
|
||||
CannotAccessDockerSocketError: "Kan de docker socket niet bereiken: unix:///var/run/docker.sock\nDraai lazydocker als root of lees https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotAccessDockerSocketError: "Kan de docker socket niet bereiken: unix:///var/run/docker.sock\nDraai lazyapple als root of lees https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
|
||||
Donate: "Doneer",
|
||||
Confirm: "Bevestigen",
|
||||
|
|
@ -27,8 +27,8 @@ func dutchSet() TranslationSet {
|
|||
Menu: "menu",
|
||||
MenuTitle: "Menu",
|
||||
Scroll: "scroll",
|
||||
OpenConfig: "open de lazydocker configuratie",
|
||||
EditConfig: "verander de lazydocker configuratie",
|
||||
OpenConfig: "open de lazyapple configuratie",
|
||||
EditConfig: "verander de lazyapple configuratie",
|
||||
Cancel: "annuleren",
|
||||
Remove: "verwijder",
|
||||
HideStopped: "verberg gestopte containers",
|
||||
|
|
@ -92,7 +92,7 @@ func dutchSet() TranslationSet {
|
|||
ConfirmPruneNetworks: "Weet je zeker dat je alle niet gebruikte networks wil vernietigen?",
|
||||
StopService: "Weet je zeker dat je deze service zijn containers wil stoppen?",
|
||||
StopContainer: "Weet je zeker dat je deze container wil stoppen?",
|
||||
PressEnterToReturn: "Druk op enter om terug te gaan naar lazydocker (Deze popup kan uit gezet worden door in de config dit neer te zetten `gui.returnImmediately: true`)",
|
||||
PressEnterToReturn: "Druk op enter om terug te gaan naar lazyapple (Deze popup kan uit gezet worden door in de config dit neer te zetten `gui.returnImmediately: true`)",
|
||||
DetachFromContainerShortCut: "Als u wilt loskoppelen van de container, drukt u standaard op ctrl-p en vervolgens op ctrl-q",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,6 +136,14 @@ type TranslationSet struct {
|
|||
FocusImages string
|
||||
FocusVolumes string
|
||||
FocusNetworks string
|
||||
|
||||
Kill string
|
||||
KillingStatus string
|
||||
RemoveContainer string
|
||||
ContainerNotRunning string
|
||||
NoIp string
|
||||
NoPorts string
|
||||
NoStats string
|
||||
}
|
||||
|
||||
func englishSet() TranslationSet {
|
||||
|
|
@ -154,12 +162,12 @@ func englishSet() TranslationSet {
|
|||
|
||||
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
|
||||
|
||||
ErrorOccurred: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazydocker/issues",
|
||||
ErrorOccurred: "An error occurred! Please create an issue at https://github.com/g-battaglia/lazy-apple-container/issues",
|
||||
ConnectionFailed: "connection to docker client failed. You may need to restart the docker client",
|
||||
UnattachableContainerError: "Container does not support attaching. You must either run the service with the '-it' flag or use `stdin_open: true, tty: true` in the docker-compose.yml file",
|
||||
WaitingForContainerInfo: "Cannot proceed until docker gives us more information about the container. Please retry in a few moments.",
|
||||
CannotAttachStoppedContainerError: "You cannot attach to a stopped container, you need to start it first (which you can actually do with the 'r' key) (yes I'm too lazy to do this automatically for you) (pretty cool that I get to communicate one-on-one with you in the form of an error message though)",
|
||||
CannotAccessDockerSocketError: "Can't access docker socket at: unix:///var/run/docker.sock\nRun lazydocker as root or read https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotAccessDockerSocketError: "Can't access docker socket at: unix:///var/run/docker.sock\nRun lazyapple as root or read https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotKillChildError: "Waited three seconds for child process to stop. There may be an orphan process that continues to run on your system.",
|
||||
|
||||
Donate: "Donate",
|
||||
|
|
@ -175,8 +183,8 @@ func englishSet() TranslationSet {
|
|||
Menu: "menu",
|
||||
MenuTitle: "Menu",
|
||||
Scroll: "scroll",
|
||||
OpenConfig: "open lazydocker config",
|
||||
EditConfig: "edit lazydocker config",
|
||||
OpenConfig: "open lazyapple config",
|
||||
EditConfig: "edit lazyapple config",
|
||||
Cancel: "cancel",
|
||||
Remove: "remove",
|
||||
HideStopped: "hide/show stopped containers",
|
||||
|
|
@ -242,7 +250,7 @@ func englishSet() TranslationSet {
|
|||
NothingToDisplay: "Nothing to display",
|
||||
NoContainerForService: "No logs to show; service is not associated with a container",
|
||||
CannotDisplayEnvVariables: "Something went wrong while displaying environment variables",
|
||||
CannotManageNonLocalService: "This service belongs to a different compose project. Run lazydocker from that project's directory to manage it.",
|
||||
CannotManageNonLocalService: "This service belongs to a different compose project. Run lazyapple from that project's directory to manage it.",
|
||||
|
||||
NoContainers: "No containers",
|
||||
NoContainer: "No container",
|
||||
|
|
@ -263,7 +271,7 @@ func englishSet() TranslationSet {
|
|||
ConfirmPruneNetworks: "Are you sure you want to prune all unused networks?",
|
||||
StopService: "Are you sure you want to stop this service's containers?",
|
||||
StopContainer: "Are you sure you want to stop this container?",
|
||||
PressEnterToReturn: "Press enter to return to lazydocker (this prompt can be disabled in your config by setting `gui.returnImmediately: true`)",
|
||||
PressEnterToReturn: "Press enter to return to lazyapple (this prompt can be disabled in your config by setting `gui.returnImmediately: true`)",
|
||||
DetachFromContainerShortCut: "By default, to detach from the container press ctrl-p then ctrl-q",
|
||||
|
||||
No: "no",
|
||||
|
|
@ -279,5 +287,13 @@ func englishSet() TranslationSet {
|
|||
FocusImages: "focus images panel",
|
||||
FocusVolumes: "focus volumes panel",
|
||||
FocusNetworks: "focus networks panel",
|
||||
|
||||
Kill: "kill",
|
||||
KillingStatus: "killing",
|
||||
RemoveContainer: "remove container",
|
||||
ContainerNotRunning: "container is not running",
|
||||
NoIp: "n/a",
|
||||
NoPorts: "n/a",
|
||||
NoStats: "No stats available",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ func frenchSet() TranslationSet {
|
|||
|
||||
NoViewMachingNewLineFocusedSwitchStatement: "Aucune vue correspondant au switch newLineFocused",
|
||||
|
||||
ErrorOccurred: "Une erreur s'est produite ! Veuillez créer un rapport d'erreur sur https://github.com/jesseduffield/lazydocker/issues",
|
||||
ErrorOccurred: "Une erreur s'est produite ! Veuillez créer un rapport d'erreur sur https://github.com/g-battaglia/lazy-apple-container/issues",
|
||||
ConnectionFailed: "Erreur lors de la connexion au client Docker. Essayez de redémarrer votre client Docker",
|
||||
UnattachableContainerError: "Le conteneur ne peut pas être attaché. Vous devez exécuter le service avec le drapeau 'it' ou bien utiliser `stdin_open: true, tty: true` dans votre fichier docker-compose.yml",
|
||||
WaitingForContainerInfo: "Le processus ne peut pas continuer avant que Docker ne fournisse plus d'informations. Veuillez réessayer dans quelques instants.",
|
||||
|
||||
CannotAttachStoppedContainerError: "Vous ne pouvez pas vous attacher à un conteneur arrêté, vous devez le démarrer en amont (ce que vous pouvez faire avec la touche 'r') (oui, je suis trop paresseux pour le faire automatiquement pour vous) (plutôt cool que je puisse communiquer en tête-à-tête avec vous au travers d'un message d'erreur, cependant)",
|
||||
CannotAccessDockerSocketError: "Impossible d'accéder au socket Docker à : unix:///var/run/docker.sock\nLancez lazydocker en tant que root ou alors lisez https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotAccessDockerSocketError: "Impossible d'accéder au socket Docker à : unix:///var/run/docker.sock\nLancez lazyapple en tant que root ou alors lisez https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotKillChildError: "Trois secondes se sont écoulées depuis la demande d'arrêt des processus enfants. Il se peut qu'un processus orphelin continue à tourner sur votre système.",
|
||||
|
||||
Donate: "Donner",
|
||||
|
|
@ -33,8 +33,8 @@ func frenchSet() TranslationSet {
|
|||
Menu: "menu",
|
||||
MenuTitle: "Menu",
|
||||
Scroll: "faire défiler",
|
||||
OpenConfig: "ouvrir la configuration lazydocker",
|
||||
EditConfig: "modifier la configuration lazydocker",
|
||||
OpenConfig: "ouvrir la configuration lazyapple",
|
||||
EditConfig: "modifier la configuration lazyapple",
|
||||
Cancel: "annuler",
|
||||
Remove: "supprimer",
|
||||
HideStopped: "cacher/montrer les conteneurs arrêtés",
|
||||
|
|
@ -111,7 +111,7 @@ func frenchSet() TranslationSet {
|
|||
ConfirmPruneNetworks: "Êtes-vous certain de vouloir détruire tous les réseaux non utilisés ?",
|
||||
StopService: "Êtes-vous certain de vouloir arrêter le conteneur de ce service ?",
|
||||
StopContainer: "Êtes-vous certain de vouloir arrêter ce conteneur ?",
|
||||
PressEnterToReturn: "Appuyez sur Entrée pour revenir à lazydocker (ce message peut être désactivé dans vos configurations en appliquant `gui.returnImmediately: true`)",
|
||||
PressEnterToReturn: "Appuyez sur Entrée pour revenir à lazyapple (ce message peut être désactivé dans vos configurations en appliquant `gui.returnImmediately: true`)",
|
||||
DetachFromContainerShortCut: "Par défaut, pour se détacher du conteneur appuyez sur CTRL-P puis CTRL-Q",
|
||||
|
||||
No: "non",
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ func germanSet() TranslationSet {
|
|||
|
||||
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
|
||||
|
||||
ErrorOccurred: "Es ist ein Fehler aufgetreten! Bitte erstelle ein Issue hier: https://github.com/jesseduffield/lazydocker/issues",
|
||||
ErrorOccurred: "Es ist ein Fehler aufgetreten! Bitte erstelle ein Issue hier: https://github.com/g-battaglia/lazy-apple-container/issues",
|
||||
ConnectionFailed: "Verbindung zum Docker Client fehlgeschlagen. Du musst ggf. den Docker Client neustarten.",
|
||||
UnattachableContainerError: "Der Container bietet keine Unterstützung für das Anbinden. Du musst den Dienst entweder mit der '-it' Flagge benutzen oder `stdin_open: true, tty: true` in der docker-compose.yml Datei setzen.",
|
||||
CannotAttachStoppedContainerError: "Du kannst keinen angehaltenen Container anbinden. Du musst ihn erst starten (was du tun kannst, indem du 'r' drückst), (ja, ich bin zu faul um das zu automatisieren) (aber ist schon cool, dass ich so eine Konversation durch eine Fehlermeldung mit dir führen kann)",
|
||||
CannotAccessDockerSocketError: "Kann nicht auf den Socket zugreifen: unix:///var/run/docker.sock\nFühre lazydocker als root aus oder lese https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotAccessDockerSocketError: "Kann nicht auf den Socket zugreifen: unix:///var/run/docker.sock\nFühre lazyapple als root aus oder lese https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
|
||||
Donate: "Spenden",
|
||||
Confirm: "Bestätigen",
|
||||
|
|
@ -27,8 +27,8 @@ func germanSet() TranslationSet {
|
|||
Menu: "menü",
|
||||
MenuTitle: "Menü",
|
||||
Scroll: "scrollen",
|
||||
OpenConfig: "öffne lazydocker Konfiguration",
|
||||
EditConfig: "bearbeite lazydocker Konfiguration",
|
||||
OpenConfig: "öffne lazyapple Konfiguration",
|
||||
EditConfig: "bearbeite lazyapple Konfiguration",
|
||||
Cancel: "abbrechen",
|
||||
Remove: "entfernen",
|
||||
ForceRemove: "Entfernen erzwingen",
|
||||
|
|
@ -91,7 +91,7 @@ func germanSet() TranslationSet {
|
|||
ConfirmPruneNetworks: "Bist du dir sicher, dass du alle unbenutzen Netzwerk entfernen möchtest?",
|
||||
StopService: "Bist du dir sicher, dass du den Dienst dieses Containers anhalten möchtest?",
|
||||
StopContainer: "Bist du dir sicher, dass du den Container anhalten möchtest?",
|
||||
PressEnterToReturn: "Drücke Eingabe um zu lazydocker zurückzukehren. (Diese Nachfrage kann in Deiner Konfiguration deaktiviert werden, indem du folgenden Wert setzt: `gui.returnImmediately: true`)",
|
||||
PressEnterToReturn: "Drücke Eingabe um zu lazyapple zurückzukehren. (Diese Nachfrage kann in Deiner Konfiguration deaktiviert werden, indem du folgenden Wert setzt: `gui.returnImmediately: true`)",
|
||||
DetachFromContainerShortCut: "Um sich vom Container zu trennen, drücken Sie standardmäßig ctrl-p und dann ctrl-q",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package i18n
|
|||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"dario.cat/mergo"
|
||||
|
||||
"github.com/cloudfoundry/jibber_jabber"
|
||||
"github.com/go-errors/errors"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ func polishSet() TranslationSet {
|
|||
|
||||
NoViewMachingNewLineFocusedSwitchStatement: "Żaden widok nie odpowiada instrukcji przełączenia newLineFocused",
|
||||
|
||||
ErrorOccurred: "Wystąpił błąd! Proszę go zgłosić na https://github.com/jesseduffield/lazydocker/issues",
|
||||
ErrorOccurred: "Wystąpił błąd! Proszę go zgłosić na https://github.com/g-battaglia/lazy-apple-container/issues",
|
||||
ConnectionFailed: "Błąd połączenia z Dockerem. Być może należy go zrestartować.",
|
||||
UnattachableContainerError: "Kontener nie obsługuje przyczepiania (attach). Musisz albo użyć flag '-it', albo `stdin_open: true, tty: true` w pliku docker-compose.yml.",
|
||||
CannotAttachStoppedContainerError: "Nie można przyczepić się do zatrzymanego kontenera, należy go najpierw uruchomić (co można wykonać wciskając przycisk 'r')",
|
||||
|
|
@ -91,7 +91,7 @@ func polishSet() TranslationSet {
|
|||
ConfirmPruneNetworks: "Na pewno wyczyścić wszystkie nieużywane sieci?",
|
||||
StopService: "Na pewno zatrzymać kontenery tego serwisu?",
|
||||
StopContainer: "Na pewno zatrzymać ten kontener?",
|
||||
PressEnterToReturn: "Wciśnij enter aby powrócić do lazydockera (ten komunikat może być wyłączony w konfiguracji poprzez ustawienie `gui.returnImmediately: true`)",
|
||||
PressEnterToReturn: "Wciśnij enter aby powrócić do lazyapplea (ten komunikat może być wyłączony w konfiguracji poprzez ustawienie `gui.returnImmediately: true`)",
|
||||
DetachFromContainerShortCut: "Domyślnie, aby odłączyć się od kontenera, naciśnij ctrl-p, a następnie ctrl-q",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ func portugueseSet() TranslationSet {
|
|||
|
||||
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
|
||||
|
||||
ErrorOccurred: "Um erro ocorreu! Por favor, crie uma issue em https://github.com/jesseduffield/lazydocker/issues",
|
||||
ErrorOccurred: "Um erro ocorreu! Por favor, crie uma issue em https://github.com/g-battaglia/lazy-apple-container/issues",
|
||||
ConnectionFailed: "Falha na conexão com o cliente Docker. Você pode precisar reiniciar o seu cliente Docker",
|
||||
UnattachableContainerError: "O contêiner não suporta anexação. Você deve executar o serviço com a flag '-it' ou usar `stdin_open: true, tty: true` no arquivo docker-compose.yml",
|
||||
WaitingForContainerInfo: "Não é possível prosseguir até que o Docker forneça mais informações sobre o contêiner. Por favor, tente novamente em alguns momentos.",
|
||||
CannotAttachStoppedContainerError: "Você não pode anexar a um contêiner parado, você precisa iniciá-lo primeiro (o que você pode fazer com a tecla 'r') (sim, sou preguiçoso demais para fazer isso automaticamente para você) (aliás, bem legal que eu posso me comunicar diretamente com você na forma de uma mensagem de erro)",
|
||||
CannotAccessDockerSocketError: "Não é possível acessar o sôquete docker em: unix:///var/run/docker.sock\nExecute o lazydocker como root ou leia https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotAccessDockerSocketError: "Não é possível acessar o sôquete docker em: unix:///var/run/docker.sock\nExecute o lazyapple como root ou leia https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotKillChildError: "Três segundos foram esperarados para que os processos filhos parassem. Pode haver um processo órfão que continua em execução em seu sistema.",
|
||||
|
||||
Donate: "Doar",
|
||||
|
|
@ -37,8 +37,8 @@ func portugueseSet() TranslationSet {
|
|||
Menu: "menu",
|
||||
MenuTitle: "Menu",
|
||||
Scroll: "rolar",
|
||||
OpenConfig: "abrir configuração do lazydocker",
|
||||
EditConfig: "editar configuração do lazydocker",
|
||||
OpenConfig: "abrir configuração do lazyapple",
|
||||
EditConfig: "editar configuração do lazyapple",
|
||||
Cancel: "cancelar",
|
||||
Remove: "remover",
|
||||
HideStopped: "ocultar/mostrar contêineres parados",
|
||||
|
|
@ -124,7 +124,7 @@ func portugueseSet() TranslationSet {
|
|||
ConfirmPruneNetworks: "Tem certeza que deseja destruir todas as redes não utilizadas?",
|
||||
StopService: "Tem certeza que deseja parar os contêineres deste serviço?",
|
||||
StopContainer: "Tem certeza que deseja parar este contêiner?",
|
||||
PressEnterToReturn: "Pressione enter para retornar ao lazydocker (este prompt pode ser desativado em sua configuração definindo `gui.returnImmediately: true`)",
|
||||
PressEnterToReturn: "Pressione enter para retornar ao lazyapple (este prompt pode ser desativado em sua configuração definindo `gui.returnImmediately: true`)",
|
||||
DetachFromContainerShortCut: "Por padrão, para desanexar do contêiner, pressione ctrl-p e depois ctrl-q",
|
||||
|
||||
No: "não",
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ func spanishSet() TranslationSet {
|
|||
RunningCustomCommandStatus: "ejecutando comando personalizado",
|
||||
RunningBulkCommandStatus: "ejecutando comando masivo",
|
||||
|
||||
ErrorOccurred: "¡Hubo un error! Por favor crea un issue en https://github.com/jesseduffield/lazydocker/issues",
|
||||
ErrorOccurred: "¡Hubo un error! Por favor crea un issue en https://github.com/g-battaglia/lazy-apple-container/issues",
|
||||
ConnectionFailed: "Falló la conexión con el docker client. Quizá necesitas reiniciar tu docker client",
|
||||
UnattachableContainerError: "Container does not support attaching. You must either run the service with the '-it' flag or use `stdin_open: true, tty: true` in the docker-compose.yml file",
|
||||
WaitingForContainerInfo: "No podemos proceder hasta que docker nos de más información sobre el contenedor. Inténtalo otra vez en unos segundos.",
|
||||
CannotAccessDockerSocketError: "No es posible acceder al docker socket en: unix:///var/run/docker.sock\nEjecuta lazydocker como root o lee https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotAccessDockerSocketError: "No es posible acceder al docker socket en: unix:///var/run/docker.sock\nEjecuta lazyapple como root o lee https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotKillChildError: "Esperamos tres segundos a que el proceso hijo se detenga. Debe de haber un proceso huérfano que continua activo en tu sistema.",
|
||||
|
||||
Donate: "Donar",
|
||||
|
|
@ -33,8 +33,8 @@ func spanishSet() TranslationSet {
|
|||
Quit: "salir",
|
||||
Menu: "menú",
|
||||
MenuTitle: "Menú",
|
||||
OpenConfig: "abrir configuración de lazydocker",
|
||||
EditConfig: "editar configuración de lazydocker",
|
||||
OpenConfig: "abrir configuración de lazyapple",
|
||||
EditConfig: "editar configuración de lazyapple",
|
||||
Cancel: "cancelar",
|
||||
Remove: "borrar",
|
||||
HideStopped: "esconder/mostrar contenedores parados",
|
||||
|
|
@ -119,7 +119,7 @@ func spanishSet() TranslationSet {
|
|||
ConfirmPruneNetworks: "¿Realmente quieres limpiar todas las redes sin usar?",
|
||||
StopService: "¿Realmente quieres detener los contenedores de este servicio?",
|
||||
StopContainer: "¿Realmente quieres detener este contenedor?",
|
||||
PressEnterToReturn: "Presionar [enter] para volver a lazydocker (este mensaje puede ser desactivado en tu configuración poniendo `gui.returnImmediately: true`)",
|
||||
PressEnterToReturn: "Presionar [enter] para volver a lazyapple (este mensaje puede ser desactivado en tu configuración poniendo `gui.returnImmediately: true`)",
|
||||
|
||||
No: "no",
|
||||
Yes: "sí",
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ func turkishSet() TranslationSet {
|
|||
|
||||
NoViewMachingNewLineFocusedSwitchStatement: "NewLineFocused anahtar deyimi ile eşleşen görünüm yok",
|
||||
|
||||
ErrorOccurred: "Bir hata oluştu! Lütfen https://github.com/jesseduffield/lazydocker/issues adresinden bir hataya ilişkin konu oluşturun",
|
||||
ErrorOccurred: "Bir hata oluştu! Lütfen https://github.com/g-battaglia/lazy-apple-container/issues adresinden bir hataya ilişkin konu oluşturun",
|
||||
ConnectionFailed: "Docker bağlantısı başarısız oldu. Docker' ı yeniden başlatmanız gerekebilir",
|
||||
UnattachableContainerError: "Konteyner attaching modunda çalışmayı desteklemiyor. Hizmeti '-it' opsiyonu ile çalıştırmanız veya docker-compose.yml dosyasında `stdin_open: true, tty: true` kullanmanız gerekir.",
|
||||
CannotAttachStoppedContainerError: "Durdurulan konteynera bağlanamazsınız, ilk önce başlatmanız gerekir (aslında başlatmayı r tuşu ile yapabilirsiniz) (evet, senin için bunu otomatik olarak yapabilirim fakat çok tembelim) (hata mesajı ile seninle birebir iletişim kurmam çok daha güzel)",
|
||||
CannotAccessDockerSocketError: "Docker' a şu adresten erişilemiyor : unix:///var/run/docker.sock\n lazydocker' ı root(kök kullanıcı) olarak çalıştır veya şu adresteki adımları takip et : https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
CannotAccessDockerSocketError: "Docker' a şu adresten erişilemiyor : unix:///var/run/docker.sock\n lazyapple' ı root(kök kullanıcı) olarak çalıştır veya şu adresteki adımları takip et : https://docs.docker.com/install/linux/linux-postinstall/",
|
||||
|
||||
Donate: "Bağış",
|
||||
Confirm: "Onayla",
|
||||
|
|
@ -27,7 +27,7 @@ func turkishSet() TranslationSet {
|
|||
Menu: "menü",
|
||||
MenuTitle: "Menü",
|
||||
Scroll: "kaydır",
|
||||
OpenConfig: "lazydocker ayarlarını aç",
|
||||
OpenConfig: "lazyapple ayarlarını aç",
|
||||
EditConfig: "lazzydocker ayarlarını düzenle",
|
||||
Cancel: "iptal",
|
||||
Remove: "kaldır",
|
||||
|
|
@ -91,7 +91,7 @@ func turkishSet() TranslationSet {
|
|||
ConfirmPruneNetworks: "Kullanılmayan tüm ağları temizlemek istediğinizden emin misiniz?",
|
||||
StopService: "Bu servisin konteynerlerini durdurmak istediğinize emin misiniz?",
|
||||
StopContainer: "Bu konteyneri durdurmak istediğinize emin misiniz?",
|
||||
PressEnterToReturn: "lazydocker' a geri dönmek için enter tuşuna basın ( Bu uyarı, `gui.return Immediately: true` ayarıyla devre dışı bırakılabilir)",
|
||||
PressEnterToReturn: "lazyapple' a geri dönmek için enter tuşuna basın ( Bu uyarı, `gui.return Immediately: true` ayarıyla devre dışı bırakılabilir)",
|
||||
DetachFromContainerShortCut: "Varsayılan olarak, kaptan ayırmak için ctrl-p ve ardından ctrl-q tuşlarına basın",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/i18n"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazydocker/pkg/cheatsheet"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/cheatsheet"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
"github.com/jesseduffield/lazycontainer/pkg/i18n"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ name = "go"
|
|||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
import_path = "github.com/imdario/mergo"
|
||||
import_path = "dario.cat/mergo"
|
||||
|
|
@ -13,6 +13,9 @@
|
|||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Golang/Intellij
|
||||
.idea
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
|
||||
7
vendor/dario.cat/mergo/FUNDING.json
vendored
Normal file
7
vendor/dario.cat/mergo/FUNDING.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"drips": {
|
||||
"ethereum": {
|
||||
"ownedBy": "0x6160020e7102237aC41bdb156e94401692D76930"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,77 +44,59 @@ Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the
|
|||
|
||||
## Status
|
||||
|
||||
It is ready for production use. [It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, Microsoft, etc](https://github.com/imdario/mergo#mergo-in-the-wild).
|
||||
Mergo is stable and frozen, ready for production. Check a short list of the projects using at large scale it [here](https://github.com/imdario/mergo#mergo-in-the-wild).
|
||||
|
||||
### Important note
|
||||
No new features are accepted. They will be considered for a future v2 that improves the implementation and fixes bugs for corner cases.
|
||||
|
||||
### Important notes
|
||||
|
||||
#### 1.0.0
|
||||
|
||||
In [1.0.0](//github.com/imdario/mergo/releases/tag/1.0.0) Mergo moves to a vanity URL `dario.cat/mergo`. No more v1 versions will be released.
|
||||
|
||||
If the vanity URL is causing issues in your project due to a dependency pulling Mergo - it isn't a direct dependency in your project - it is recommended to use [replace](https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive) to pin the version to the last one with the old import URL:
|
||||
|
||||
```
|
||||
replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16
|
||||
```
|
||||
|
||||
#### 0.3.9
|
||||
|
||||
Please keep in mind that a problematic PR broke [0.3.9](//github.com/imdario/mergo/releases/tag/0.3.9). I reverted it in [0.3.10](//github.com/imdario/mergo/releases/tag/0.3.10), and I consider it stable but not bug-free. Also, this version adds support for go modules.
|
||||
|
||||
Keep in mind that in [0.3.2](//github.com/imdario/mergo/releases/tag/0.3.2), Mergo changed `Merge()`and `Map()` signatures to support [transformers](#transformers). I added an optional/variadic argument so that it won't break the existing code.
|
||||
|
||||
If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with ```go get -u github.com/imdario/mergo```. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0).
|
||||
If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with ```go get -u dario.cat/mergo```. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0).
|
||||
|
||||
### Donations
|
||||
|
||||
If Mergo is useful to you, consider buying me a coffee, a beer, or making a monthly donation to allow me to keep building great free software. :heart_eyes:
|
||||
|
||||
<a href='https://ko-fi.com/B0B58839' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
|
||||
<a href="https://liberapay.com/dario/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
|
||||
<a href='https://github.com/sponsors/imdario' target='_blank'><img alt="Become my sponsor" src="https://img.shields.io/github/sponsors/imdario?style=for-the-badge" /></a>
|
||||
|
||||
### Mergo in the wild
|
||||
|
||||
- [moby/moby](https://github.com/moby/moby)
|
||||
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)
|
||||
- [vmware/dispatch](https://github.com/vmware/dispatch)
|
||||
- [Shopify/themekit](https://github.com/Shopify/themekit)
|
||||
- [imdario/zas](https://github.com/imdario/zas)
|
||||
- [matcornic/hermes](https://github.com/matcornic/hermes)
|
||||
- [OpenBazaar/openbazaar-go](https://github.com/OpenBazaar/openbazaar-go)
|
||||
- [kataras/iris](https://github.com/kataras/iris)
|
||||
- [michaelsauter/crane](https://github.com/michaelsauter/crane)
|
||||
- [go-task/task](https://github.com/go-task/task)
|
||||
- [sensu/uchiwa](https://github.com/sensu/uchiwa)
|
||||
- [ory/hydra](https://github.com/ory/hydra)
|
||||
- [sisatech/vcli](https://github.com/sisatech/vcli)
|
||||
- [dairycart/dairycart](https://github.com/dairycart/dairycart)
|
||||
- [projectcalico/felix](https://github.com/projectcalico/felix)
|
||||
- [resin-os/balena](https://github.com/resin-os/balena)
|
||||
- [go-kivik/kivik](https://github.com/go-kivik/kivik)
|
||||
- [Telefonica/govice](https://github.com/Telefonica/govice)
|
||||
- [supergiant/supergiant](supergiant/supergiant)
|
||||
- [SergeyTsalkov/brooce](https://github.com/SergeyTsalkov/brooce)
|
||||
- [soniah/dnsmadeeasy](https://github.com/soniah/dnsmadeeasy)
|
||||
- [ohsu-comp-bio/funnel](https://github.com/ohsu-comp-bio/funnel)
|
||||
- [EagerIO/Stout](https://github.com/EagerIO/Stout)
|
||||
- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api)
|
||||
- [russross/canvasassignments](https://github.com/russross/canvasassignments)
|
||||
- [rdegges/cryptly-api](https://github.com/rdegges/cryptly-api)
|
||||
- [casualjim/exeggutor](https://github.com/casualjim/exeggutor)
|
||||
- [divshot/gitling](https://github.com/divshot/gitling)
|
||||
- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl)
|
||||
- [andrerocker/deploy42](https://github.com/andrerocker/deploy42)
|
||||
- [elwinar/rambler](https://github.com/elwinar/rambler)
|
||||
- [tmaiaroto/gopartman](https://github.com/tmaiaroto/gopartman)
|
||||
- [jfbus/impressionist](https://github.com/jfbus/impressionist)
|
||||
- [Jmeyering/zealot](https://github.com/Jmeyering/zealot)
|
||||
- [godep-migrator/rigger-host](https://github.com/godep-migrator/rigger-host)
|
||||
- [Dronevery/MultiwaySwitch-Go](https://github.com/Dronevery/MultiwaySwitch-Go)
|
||||
- [thoas/picfit](https://github.com/thoas/picfit)
|
||||
- [mantasmatelis/whooplist-server](https://github.com/mantasmatelis/whooplist-server)
|
||||
- [jnuthong/item_search](https://github.com/jnuthong/item_search)
|
||||
- [bukalapak/snowboard](https://github.com/bukalapak/snowboard)
|
||||
- [containerssh/containerssh](https://github.com/containerssh/containerssh)
|
||||
- [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
|
||||
- [tjpnz/structbot](https://github.com/tjpnz/structbot)
|
||||
Mergo is used by [thousands](https://deps.dev/go/dario.cat%2Fmergo/v1.0.0/dependents) [of](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.16/dependents) [projects](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.12), including:
|
||||
|
||||
* [containerd/containerd](https://github.com/containerd/containerd)
|
||||
* [datadog/datadog-agent](https://github.com/datadog/datadog-agent)
|
||||
* [docker/cli/](https://github.com/docker/cli/)
|
||||
* [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
|
||||
* [go-micro/go-micro](https://github.com/go-micro/go-micro)
|
||||
* [grafana/loki](https://github.com/grafana/loki)
|
||||
* [masterminds/sprig](github.com/Masterminds/sprig)
|
||||
* [moby/moby](https://github.com/moby/moby)
|
||||
* [slackhq/nebula](https://github.com/slackhq/nebula)
|
||||
* [volcano-sh/volcano](https://github.com/volcano-sh/volcano)
|
||||
|
||||
## Install
|
||||
|
||||
go get github.com/imdario/mergo
|
||||
go get dario.cat/mergo
|
||||
|
||||
// use in your .go code
|
||||
import (
|
||||
"github.com/imdario/mergo"
|
||||
"dario.cat/mergo"
|
||||
)
|
||||
|
||||
## Usage
|
||||
|
|
@ -135,6 +117,39 @@ if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil {
|
|||
}
|
||||
```
|
||||
|
||||
If you need to override pointers, so the source pointer's value is assigned to the destination's pointer, you must use `WithoutDereference`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"dario.cat/mergo"
|
||||
)
|
||||
|
||||
type Foo struct {
|
||||
A *string
|
||||
B int64
|
||||
}
|
||||
|
||||
func main() {
|
||||
first := "first"
|
||||
second := "second"
|
||||
src := Foo{
|
||||
A: &first,
|
||||
B: 2,
|
||||
}
|
||||
|
||||
dest := Foo{
|
||||
A: &second,
|
||||
B: 1,
|
||||
}
|
||||
|
||||
mergo.Merge(&dest, src, mergo.WithOverride, mergo.WithoutDereference)
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field.
|
||||
|
||||
```go
|
||||
|
|
@ -152,7 +167,7 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"dario.cat/mergo"
|
||||
)
|
||||
|
||||
type Foo struct {
|
||||
|
|
@ -175,10 +190,6 @@ func main() {
|
|||
}
|
||||
```
|
||||
|
||||
Note: if test are failing due missing package, please execute:
|
||||
|
||||
go get gopkg.in/yaml.v3
|
||||
|
||||
### Transformers
|
||||
|
||||
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`?
|
||||
|
|
@ -188,9 +199,9 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"reflect"
|
||||
"time"
|
||||
"dario.cat/mergo"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type timeTransformer struct {
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.3.x | :white_check_mark: |
|
||||
| < 0.3 | :x: |
|
||||
| 1.x.x | :white_check_mark: |
|
||||
| < 1.0 | :x: |
|
||||
|
||||
## Security contact information
|
||||
|
||||
|
|
@ -8,30 +8,36 @@ A helper to merge structs and maps in Golang. Useful for configuration default v
|
|||
|
||||
Mergo merges same-type structs and maps by setting default values in zero-value fields. Mergo won't merge unexported (private) fields. It will do recursively any exported one. It also won't merge structs inside maps (because they are not addressable using Go reflection).
|
||||
|
||||
Status
|
||||
# Status
|
||||
|
||||
It is ready for production use. It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, etc.
|
||||
|
||||
Important note
|
||||
# Important notes
|
||||
|
||||
1.0.0
|
||||
|
||||
In 1.0.0 Mergo moves to a vanity URL `dario.cat/mergo`.
|
||||
|
||||
0.3.9
|
||||
|
||||
Please keep in mind that a problematic PR broke 0.3.9. We reverted it in 0.3.10. We consider 0.3.10 as stable but not bug-free. . Also, this version adds suppot for go modules.
|
||||
|
||||
Keep in mind that in 0.3.2, Mergo changed Merge() and Map() signatures to support transformers. We added an optional/variadic argument so that it won't break the existing code.
|
||||
|
||||
If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with go get -u github.com/imdario/mergo. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0).
|
||||
If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with go get -u dario.cat/mergo. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0).
|
||||
|
||||
Install
|
||||
# Install
|
||||
|
||||
Do your usual installation procedure:
|
||||
|
||||
go get github.com/imdario/mergo
|
||||
go get dario.cat/mergo
|
||||
|
||||
// use in your .go code
|
||||
import (
|
||||
"github.com/imdario/mergo"
|
||||
)
|
||||
// use in your .go code
|
||||
import (
|
||||
"dario.cat/mergo"
|
||||
)
|
||||
|
||||
Usage
|
||||
# Usage
|
||||
|
||||
You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. It won't merge empty structs value as they are zero values too. Also, maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection).
|
||||
|
||||
|
|
@ -59,7 +65,7 @@ Here is a nice example:
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"dario.cat/mergo"
|
||||
)
|
||||
|
||||
type Foo struct {
|
||||
|
|
@ -81,7 +87,7 @@ Here is a nice example:
|
|||
// {two 2}
|
||||
}
|
||||
|
||||
Transformers
|
||||
# Transformers
|
||||
|
||||
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, time.Time is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero time.Time?
|
||||
|
||||
|
|
@ -89,9 +95,9 @@ Transformers allow to merge specific types differently than in the default behav
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"reflect"
|
||||
"time"
|
||||
"dario.cat/mergo"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type timeTransformer struct {
|
||||
|
|
@ -127,17 +133,16 @@ Transformers allow to merge specific types differently than in the default behav
|
|||
// { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 }
|
||||
}
|
||||
|
||||
Contact me
|
||||
# Contact me
|
||||
|
||||
If I can help you, you have an idea or you are using Mergo in your projects, don't hesitate to drop me a line (or a pull request): https://twitter.com/im_dario
|
||||
|
||||
About
|
||||
# About
|
||||
|
||||
Written by Dario Castañé: https://da.rio.hn
|
||||
|
||||
License
|
||||
# License
|
||||
|
||||
BSD 3-Clause license, as Go language.
|
||||
|
||||
*/
|
||||
package mergo
|
||||
|
|
@ -58,7 +58,7 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, conf
|
|||
}
|
||||
fieldName := field.Name
|
||||
fieldName = changeInitialCase(fieldName, unicode.ToLower)
|
||||
if v, ok := dstMap[fieldName]; !ok || (isEmptyValue(reflect.ValueOf(v), !config.ShouldNotDereference) || overwrite) {
|
||||
if _, ok := dstMap[fieldName]; !ok || (!isEmptyValue(reflect.ValueOf(src.Field(i).Interface()), !config.ShouldNotDereference) && overwrite) || config.overwriteWithEmptyValue {
|
||||
dstMap[fieldName] = src.Field(i).Interface()
|
||||
}
|
||||
}
|
||||
|
|
@ -269,7 +269,7 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co
|
|||
if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
} else if src.Elem().Kind() != reflect.Struct {
|
||||
if overwriteWithEmptySrc || (overwrite && !src.IsNil()) || dst.IsNil() {
|
||||
dst.Set(src)
|
||||
}
|
||||
1
vendor/github.com/Microsoft/go-winio/.gitattributes
generated
vendored
1
vendor/github.com/Microsoft/go-winio/.gitattributes
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
* text=auto eol=lf
|
||||
10
vendor/github.com/Microsoft/go-winio/.gitignore
generated
vendored
10
vendor/github.com/Microsoft/go-winio/.gitignore
generated
vendored
|
|
@ -1,10 +0,0 @@
|
|||
.vscode/
|
||||
|
||||
*.exe
|
||||
|
||||
# testing
|
||||
testdata
|
||||
|
||||
# go workspaces
|
||||
go.work
|
||||
go.work.sum
|
||||
147
vendor/github.com/Microsoft/go-winio/.golangci.yml
generated
vendored
147
vendor/github.com/Microsoft/go-winio/.golangci.yml
generated
vendored
|
|
@ -1,147 +0,0 @@
|
|||
linters:
|
||||
enable:
|
||||
# style
|
||||
- containedctx # struct contains a context
|
||||
- dupl # duplicate code
|
||||
- errname # erorrs are named correctly
|
||||
- nolintlint # "//nolint" directives are properly explained
|
||||
- revive # golint replacement
|
||||
- unconvert # unnecessary conversions
|
||||
- wastedassign
|
||||
|
||||
# bugs, performance, unused, etc ...
|
||||
- contextcheck # function uses a non-inherited context
|
||||
- errorlint # errors not wrapped for 1.13
|
||||
- exhaustive # check exhaustiveness of enum switch statements
|
||||
- gofmt # files are gofmt'ed
|
||||
- gosec # security
|
||||
- nilerr # returns nil even with non-nil error
|
||||
- thelper # test helpers without t.Helper()
|
||||
- unparam # unused function params
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- pkg/etw/sample
|
||||
|
||||
exclude-rules:
|
||||
# err is very often shadowed in nested scopes
|
||||
- linters:
|
||||
- govet
|
||||
text: '^shadow: declaration of "err" shadows declaration'
|
||||
|
||||
# ignore long lines for skip autogen directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^line-length-limit: "
|
||||
source: "^//(go:generate|sys) "
|
||||
|
||||
#TODO: remove after upgrading to go1.18
|
||||
# ignore comment spacing for nolint and sys directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^comment-spacings: no space between comment delimiter and comment text"
|
||||
source: "//(cspell:|nolint:|sys |todo)"
|
||||
|
||||
# not on go 1.18 yet, so no any
|
||||
- linters:
|
||||
- revive
|
||||
text: "^use-any: since GO 1.18 'interface{}' can be replaced by 'any'"
|
||||
|
||||
# allow unjustified ignores of error checks in defer statements
|
||||
- linters:
|
||||
- nolintlint
|
||||
text: "^directive `//nolint:errcheck` should provide explanation"
|
||||
source: '^\s*defer '
|
||||
|
||||
# allow unjustified ignores of error lints for io.EOF
|
||||
- linters:
|
||||
- nolintlint
|
||||
text: "^directive `//nolint:errorlint` should provide explanation"
|
||||
source: '[=|!]= io.EOF'
|
||||
|
||||
|
||||
linters-settings:
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
# struct order is often for Win32 compat
|
||||
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
|
||||
- fieldalignment
|
||||
nolintlint:
|
||||
require-explanation: true
|
||||
require-specific: true
|
||||
revive:
|
||||
# revive is more configurable than static check, so likely the preferred alternative to static-check
|
||||
# (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997)
|
||||
enable-all-rules:
|
||||
true
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
|
||||
rules:
|
||||
# rules with required arguments
|
||||
- name: argument-limit
|
||||
disabled: true
|
||||
- name: banned-characters
|
||||
disabled: true
|
||||
- name: cognitive-complexity
|
||||
disabled: true
|
||||
- name: cyclomatic
|
||||
disabled: true
|
||||
- name: file-header
|
||||
disabled: true
|
||||
- name: function-length
|
||||
disabled: true
|
||||
- name: function-result-limit
|
||||
disabled: true
|
||||
- name: max-public-structs
|
||||
disabled: true
|
||||
# geneally annoying rules
|
||||
- name: add-constant # complains about any and all strings and integers
|
||||
disabled: true
|
||||
- name: confusing-naming # we frequently use "Foo()" and "foo()" together
|
||||
disabled: true
|
||||
- name: flag-parameter # excessive, and a common idiom we use
|
||||
disabled: true
|
||||
- name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead
|
||||
disabled: true
|
||||
# general config
|
||||
- name: line-length-limit
|
||||
arguments:
|
||||
- 140
|
||||
- name: var-naming
|
||||
arguments:
|
||||
- []
|
||||
- - CID
|
||||
- CRI
|
||||
- CTRD
|
||||
- DACL
|
||||
- DLL
|
||||
- DOS
|
||||
- ETW
|
||||
- FSCTL
|
||||
- GCS
|
||||
- GMSA
|
||||
- HCS
|
||||
- HV
|
||||
- IO
|
||||
- LCOW
|
||||
- LDAP
|
||||
- LPAC
|
||||
- LTSC
|
||||
- MMIO
|
||||
- NT
|
||||
- OCI
|
||||
- PMEM
|
||||
- PWSH
|
||||
- RX
|
||||
- SACl
|
||||
- SID
|
||||
- SMB
|
||||
- TX
|
||||
- VHD
|
||||
- VHDX
|
||||
- VMID
|
||||
- VPCI
|
||||
- WCOW
|
||||
- WIM
|
||||
1
vendor/github.com/Microsoft/go-winio/CODEOWNERS
generated
vendored
1
vendor/github.com/Microsoft/go-winio/CODEOWNERS
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
* @microsoft/containerplat
|
||||
22
vendor/github.com/Microsoft/go-winio/LICENSE
generated
vendored
22
vendor/github.com/Microsoft/go-winio/LICENSE
generated
vendored
|
|
@ -1,22 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
89
vendor/github.com/Microsoft/go-winio/README.md
generated
vendored
89
vendor/github.com/Microsoft/go-winio/README.md
generated
vendored
|
|
@ -1,89 +0,0 @@
|
|||
# go-winio [](https://github.com/microsoft/go-winio/actions/workflows/ci.yml)
|
||||
|
||||
This repository contains utilities for efficiently performing Win32 IO operations in
|
||||
Go. Currently, this is focused on accessing named pipes and other file handles, and
|
||||
for using named pipes as a net transport.
|
||||
|
||||
This code relies on IO completion ports to avoid blocking IO on system threads, allowing Go
|
||||
to reuse the thread to schedule another goroutine. This limits support to Windows Vista and
|
||||
newer operating systems. This is similar to the implementation of network sockets in Go's net
|
||||
package.
|
||||
|
||||
Please see the LICENSE file for licensing information.
|
||||
|
||||
## Contributing
|
||||
|
||||
This project welcomes contributions and suggestions.
|
||||
Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that
|
||||
you have the right to, and actually do, grant us the rights to use your contribution.
|
||||
For details, visit [Microsoft CLA](https://cla.microsoft.com).
|
||||
|
||||
When you submit a pull request, a CLA-bot will automatically determine whether you need to
|
||||
provide a CLA and decorate the PR appropriately (e.g., label, comment).
|
||||
Simply follow the instructions provided by the bot.
|
||||
You will only need to do this once across all repos using our CLA.
|
||||
|
||||
Additionally, the pull request pipeline requires the following steps to be performed before
|
||||
mergining.
|
||||
|
||||
### Code Sign-Off
|
||||
|
||||
We require that contributors sign their commits using [`git commit --signoff`][git-commit-s]
|
||||
to certify they either authored the work themselves or otherwise have permission to use it in this project.
|
||||
|
||||
A range of commits can be signed off using [`git rebase --signoff`][git-rebase-s].
|
||||
|
||||
Please see [the developer certificate](https://developercertificate.org) for more info,
|
||||
as well as to make sure that you can attest to the rules listed.
|
||||
Our CI uses the DCO Github app to ensure that all commits in a given PR are signed-off.
|
||||
|
||||
### Linting
|
||||
|
||||
Code must pass a linting stage, which uses [`golangci-lint`][lint].
|
||||
The linting settings are stored in [`.golangci.yaml`](./.golangci.yaml), and can be run
|
||||
automatically with VSCode by adding the following to your workspace or folder settings:
|
||||
|
||||
```json
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintOnSave": "package",
|
||||
```
|
||||
|
||||
Additional editor [integrations options are also available][lint-ide].
|
||||
|
||||
Alternatively, `golangci-lint` can be [installed locally][lint-install] and run from the repo root:
|
||||
|
||||
```shell
|
||||
# use . or specify a path to only lint a package
|
||||
# to show all lint errors, use flags "--max-issues-per-linter=0 --max-same-issues=0"
|
||||
> golangci-lint run ./...
|
||||
```
|
||||
|
||||
### Go Generate
|
||||
|
||||
The pipeline checks that auto-generated code, via `go generate`, are up to date.
|
||||
|
||||
This can be done for the entire repo:
|
||||
|
||||
```shell
|
||||
> go generate ./...
|
||||
```
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
## Special Thanks
|
||||
|
||||
Thanks to [natefinch][natefinch] for the inspiration for this library.
|
||||
See [npipe](https://github.com/natefinch/npipe) for another named pipe implementation.
|
||||
|
||||
[lint]: https://golangci-lint.run/
|
||||
[lint-ide]: https://golangci-lint.run/usage/integrations/#editor-integration
|
||||
[lint-install]: https://golangci-lint.run/usage/install/#local-installation
|
||||
|
||||
[git-commit-s]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s
|
||||
[git-rebase-s]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---signoff
|
||||
|
||||
[natefinch]: https://github.com/natefinch
|
||||
41
vendor/github.com/Microsoft/go-winio/SECURITY.md
generated
vendored
41
vendor/github.com/Microsoft/go-winio/SECURITY.md
generated
vendored
|
|
@ -1,41 +0,0 @@
|
|||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.7 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
|
||||
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
|
||||
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
||||
287
vendor/github.com/Microsoft/go-winio/backup.go
generated
vendored
287
vendor/github.com/Microsoft/go-winio/backup.go
generated
vendored
|
|
@ -1,287 +0,0 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/Microsoft/go-winio/internal/fs"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//sys backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead
|
||||
//sys backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite
|
||||
|
||||
const (
|
||||
BackupData = uint32(iota + 1)
|
||||
BackupEaData
|
||||
BackupSecurity
|
||||
BackupAlternateData
|
||||
BackupLink
|
||||
BackupPropertyData
|
||||
BackupObjectId //revive:disable-line:var-naming ID, not Id
|
||||
BackupReparseData
|
||||
BackupSparseBlock
|
||||
BackupTxfsData
|
||||
)
|
||||
|
||||
const (
|
||||
StreamSparseAttributes = uint32(8)
|
||||
)
|
||||
|
||||
//nolint:revive // var-naming: ALL_CAPS
|
||||
const (
|
||||
WRITE_DAC = windows.WRITE_DAC
|
||||
WRITE_OWNER = windows.WRITE_OWNER
|
||||
ACCESS_SYSTEM_SECURITY = windows.ACCESS_SYSTEM_SECURITY
|
||||
)
|
||||
|
||||
// BackupHeader represents a backup stream of a file.
|
||||
type BackupHeader struct {
|
||||
//revive:disable-next-line:var-naming ID, not Id
|
||||
Id uint32 // The backup stream ID
|
||||
Attributes uint32 // Stream attributes
|
||||
Size int64 // The size of the stream in bytes
|
||||
Name string // The name of the stream (for BackupAlternateData only).
|
||||
Offset int64 // The offset of the stream in the file (for BackupSparseBlock only).
|
||||
}
|
||||
|
||||
type win32StreamID struct {
|
||||
StreamID uint32
|
||||
Attributes uint32
|
||||
Size uint64
|
||||
NameSize uint32
|
||||
}
|
||||
|
||||
// BackupStreamReader reads from a stream produced by the BackupRead Win32 API and produces a series
|
||||
// of BackupHeader values.
|
||||
type BackupStreamReader struct {
|
||||
r io.Reader
|
||||
bytesLeft int64
|
||||
}
|
||||
|
||||
// NewBackupStreamReader produces a BackupStreamReader from any io.Reader.
|
||||
func NewBackupStreamReader(r io.Reader) *BackupStreamReader {
|
||||
return &BackupStreamReader{r, 0}
|
||||
}
|
||||
|
||||
// Next returns the next backup stream and prepares for calls to Read(). It skips the remainder of the current stream if
|
||||
// it was not completely read.
|
||||
func (r *BackupStreamReader) Next() (*BackupHeader, error) {
|
||||
if r.bytesLeft > 0 { //nolint:nestif // todo: flatten this
|
||||
if s, ok := r.r.(io.Seeker); ok {
|
||||
// Make sure Seek on io.SeekCurrent sometimes succeeds
|
||||
// before trying the actual seek.
|
||||
if _, err := s.Seek(0, io.SeekCurrent); err == nil {
|
||||
if _, err = s.Seek(r.bytesLeft, io.SeekCurrent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.bytesLeft = 0
|
||||
}
|
||||
}
|
||||
if _, err := io.Copy(io.Discard, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var wsi win32StreamID
|
||||
if err := binary.Read(r.r, binary.LittleEndian, &wsi); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr := &BackupHeader{
|
||||
Id: wsi.StreamID,
|
||||
Attributes: wsi.Attributes,
|
||||
Size: int64(wsi.Size),
|
||||
}
|
||||
if wsi.NameSize != 0 {
|
||||
name := make([]uint16, int(wsi.NameSize/2))
|
||||
if err := binary.Read(r.r, binary.LittleEndian, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr.Name = windows.UTF16ToString(name)
|
||||
}
|
||||
if wsi.StreamID == BackupSparseBlock {
|
||||
if err := binary.Read(r.r, binary.LittleEndian, &hdr.Offset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr.Size -= 8
|
||||
}
|
||||
r.bytesLeft = hdr.Size
|
||||
return hdr, nil
|
||||
}
|
||||
|
||||
// Read reads from the current backup stream.
|
||||
func (r *BackupStreamReader) Read(b []byte) (int, error) {
|
||||
if r.bytesLeft == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if int64(len(b)) > r.bytesLeft {
|
||||
b = b[:r.bytesLeft]
|
||||
}
|
||||
n, err := r.r.Read(b)
|
||||
r.bytesLeft -= int64(n)
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
} else if r.bytesLeft == 0 && err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// BackupStreamWriter writes a stream compatible with the BackupWrite Win32 API.
|
||||
type BackupStreamWriter struct {
|
||||
w io.Writer
|
||||
bytesLeft int64
|
||||
}
|
||||
|
||||
// NewBackupStreamWriter produces a BackupStreamWriter on top of an io.Writer.
|
||||
func NewBackupStreamWriter(w io.Writer) *BackupStreamWriter {
|
||||
return &BackupStreamWriter{w, 0}
|
||||
}
|
||||
|
||||
// WriteHeader writes the next backup stream header and prepares for calls to Write().
|
||||
func (w *BackupStreamWriter) WriteHeader(hdr *BackupHeader) error {
|
||||
if w.bytesLeft != 0 {
|
||||
return fmt.Errorf("missing %d bytes", w.bytesLeft)
|
||||
}
|
||||
name := utf16.Encode([]rune(hdr.Name))
|
||||
wsi := win32StreamID{
|
||||
StreamID: hdr.Id,
|
||||
Attributes: hdr.Attributes,
|
||||
Size: uint64(hdr.Size),
|
||||
NameSize: uint32(len(name) * 2),
|
||||
}
|
||||
if hdr.Id == BackupSparseBlock {
|
||||
// Include space for the int64 block offset
|
||||
wsi.Size += 8
|
||||
}
|
||||
if err := binary.Write(w.w, binary.LittleEndian, &wsi); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(name) != 0 {
|
||||
if err := binary.Write(w.w, binary.LittleEndian, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if hdr.Id == BackupSparseBlock {
|
||||
if err := binary.Write(w.w, binary.LittleEndian, hdr.Offset); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.bytesLeft = hdr.Size
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write writes to the current backup stream.
|
||||
func (w *BackupStreamWriter) Write(b []byte) (int, error) {
|
||||
if w.bytesLeft < int64(len(b)) {
|
||||
return 0, fmt.Errorf("too many bytes by %d", int64(len(b))-w.bytesLeft)
|
||||
}
|
||||
n, err := w.w.Write(b)
|
||||
w.bytesLeft -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// BackupFileReader provides an io.ReadCloser interface on top of the BackupRead Win32 API.
|
||||
type BackupFileReader struct {
|
||||
f *os.File
|
||||
includeSecurity bool
|
||||
ctx uintptr
|
||||
}
|
||||
|
||||
// NewBackupFileReader returns a new BackupFileReader from a file handle. If includeSecurity is true,
|
||||
// Read will attempt to read the security descriptor of the file.
|
||||
func NewBackupFileReader(f *os.File, includeSecurity bool) *BackupFileReader {
|
||||
r := &BackupFileReader{f, includeSecurity, 0}
|
||||
return r
|
||||
}
|
||||
|
||||
// Read reads a backup stream from the file by calling the Win32 API BackupRead().
|
||||
func (r *BackupFileReader) Read(b []byte) (int, error) {
|
||||
var bytesRead uint32
|
||||
err := backupRead(windows.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "BackupRead", Path: r.f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(r.f)
|
||||
if bytesRead == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return int(bytesRead), nil
|
||||
}
|
||||
|
||||
// Close frees Win32 resources associated with the BackupFileReader. It does not close
|
||||
// the underlying file.
|
||||
func (r *BackupFileReader) Close() error {
|
||||
if r.ctx != 0 {
|
||||
_ = backupRead(windows.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx)
|
||||
runtime.KeepAlive(r.f)
|
||||
r.ctx = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackupFileWriter provides an io.WriteCloser interface on top of the BackupWrite Win32 API.
|
||||
type BackupFileWriter struct {
|
||||
f *os.File
|
||||
includeSecurity bool
|
||||
ctx uintptr
|
||||
}
|
||||
|
||||
// NewBackupFileWriter returns a new BackupFileWriter from a file handle. If includeSecurity is true,
|
||||
// Write() will attempt to restore the security descriptor from the stream.
|
||||
func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter {
|
||||
w := &BackupFileWriter{f, includeSecurity, 0}
|
||||
return w
|
||||
}
|
||||
|
||||
// Write restores a portion of the file using the provided backup stream.
|
||||
func (w *BackupFileWriter) Write(b []byte) (int, error) {
|
||||
var bytesWritten uint32
|
||||
err := backupWrite(windows.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "BackupWrite", Path: w.f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(w.f)
|
||||
if int(bytesWritten) != len(b) {
|
||||
return int(bytesWritten), errors.New("not all bytes could be written")
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// Close frees Win32 resources associated with the BackupFileWriter. It does not
|
||||
// close the underlying file.
|
||||
func (w *BackupFileWriter) Close() error {
|
||||
if w.ctx != 0 {
|
||||
_ = backupWrite(windows.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx)
|
||||
runtime.KeepAlive(w.f)
|
||||
w.ctx = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OpenForBackup opens a file or directory, potentially skipping access checks if the backup
|
||||
// or restore privileges have been acquired.
|
||||
//
|
||||
// If the file opened was a directory, it cannot be used with Readdir().
|
||||
func OpenForBackup(path string, access uint32, share uint32, createmode uint32) (*os.File, error) {
|
||||
h, err := fs.CreateFile(path,
|
||||
fs.AccessMask(access),
|
||||
fs.FileShareMode(share),
|
||||
nil,
|
||||
fs.FileCreationDisposition(createmode),
|
||||
fs.FILE_FLAG_BACKUP_SEMANTICS|fs.FILE_FLAG_OPEN_REPARSE_POINT,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
err = &os.PathError{Op: "open", Path: path, Err: err}
|
||||
return nil, err
|
||||
}
|
||||
return os.NewFile(uintptr(h), path), nil
|
||||
}
|
||||
22
vendor/github.com/Microsoft/go-winio/doc.go
generated
vendored
22
vendor/github.com/Microsoft/go-winio/doc.go
generated
vendored
|
|
@ -1,22 +0,0 @@
|
|||
// This package provides utilities for efficiently performing Win32 IO operations in Go.
|
||||
// Currently, this package is provides support for genreal IO and management of
|
||||
// - named pipes
|
||||
// - files
|
||||
// - [Hyper-V sockets]
|
||||
//
|
||||
// This code is similar to Go's [net] package, and uses IO completion ports to avoid
|
||||
// blocking IO on system threads, allowing Go to reuse the thread to schedule other goroutines.
|
||||
//
|
||||
// This limits support to Windows Vista and newer operating systems.
|
||||
//
|
||||
// Additionally, this package provides support for:
|
||||
// - creating and managing GUIDs
|
||||
// - writing to [ETW]
|
||||
// - opening and manageing VHDs
|
||||
// - parsing [Windows Image files]
|
||||
// - auto-generating Win32 API code
|
||||
//
|
||||
// [Hyper-V sockets]: https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service
|
||||
// [ETW]: https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/event-tracing-for-windows--etw-
|
||||
// [Windows Image files]: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/work-with-windows-images
|
||||
package winio
|
||||
137
vendor/github.com/Microsoft/go-winio/ea.go
generated
vendored
137
vendor/github.com/Microsoft/go-winio/ea.go
generated
vendored
|
|
@ -1,137 +0,0 @@
|
|||
package winio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type fileFullEaInformation struct {
|
||||
NextEntryOffset uint32
|
||||
Flags uint8
|
||||
NameLength uint8
|
||||
ValueLength uint16
|
||||
}
|
||||
|
||||
var (
|
||||
fileFullEaInformationSize = binary.Size(&fileFullEaInformation{})
|
||||
|
||||
errInvalidEaBuffer = errors.New("invalid extended attribute buffer")
|
||||
errEaNameTooLarge = errors.New("extended attribute name too large")
|
||||
errEaValueTooLarge = errors.New("extended attribute value too large")
|
||||
)
|
||||
|
||||
// ExtendedAttribute represents a single Windows EA.
|
||||
type ExtendedAttribute struct {
|
||||
Name string
|
||||
Value []byte
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
func parseEa(b []byte) (ea ExtendedAttribute, nb []byte, err error) {
|
||||
var info fileFullEaInformation
|
||||
err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info)
|
||||
if err != nil {
|
||||
err = errInvalidEaBuffer
|
||||
return ea, nb, err
|
||||
}
|
||||
|
||||
nameOffset := fileFullEaInformationSize
|
||||
nameLen := int(info.NameLength)
|
||||
valueOffset := nameOffset + int(info.NameLength) + 1
|
||||
valueLen := int(info.ValueLength)
|
||||
nextOffset := int(info.NextEntryOffset)
|
||||
if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) {
|
||||
err = errInvalidEaBuffer
|
||||
return ea, nb, err
|
||||
}
|
||||
|
||||
ea.Name = string(b[nameOffset : nameOffset+nameLen])
|
||||
ea.Value = b[valueOffset : valueOffset+valueLen]
|
||||
ea.Flags = info.Flags
|
||||
if info.NextEntryOffset != 0 {
|
||||
nb = b[info.NextEntryOffset:]
|
||||
}
|
||||
return ea, nb, err
|
||||
}
|
||||
|
||||
// DecodeExtendedAttributes decodes a list of EAs from a FILE_FULL_EA_INFORMATION
|
||||
// buffer retrieved from BackupRead, ZwQueryEaFile, etc.
|
||||
func DecodeExtendedAttributes(b []byte) (eas []ExtendedAttribute, err error) {
|
||||
for len(b) != 0 {
|
||||
ea, nb, err := parseEa(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eas = append(eas, ea)
|
||||
b = nb
|
||||
}
|
||||
return eas, err
|
||||
}
|
||||
|
||||
func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error {
|
||||
if int(uint8(len(ea.Name))) != len(ea.Name) {
|
||||
return errEaNameTooLarge
|
||||
}
|
||||
if int(uint16(len(ea.Value))) != len(ea.Value) {
|
||||
return errEaValueTooLarge
|
||||
}
|
||||
entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value))
|
||||
withPadding := (entrySize + 3) &^ 3
|
||||
nextOffset := uint32(0)
|
||||
if !last {
|
||||
nextOffset = withPadding
|
||||
}
|
||||
info := fileFullEaInformation{
|
||||
NextEntryOffset: nextOffset,
|
||||
Flags: ea.Flags,
|
||||
NameLength: uint8(len(ea.Name)),
|
||||
ValueLength: uint16(len(ea.Value)),
|
||||
}
|
||||
|
||||
err := binary.Write(buf, binary.LittleEndian, &info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write([]byte(ea.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = buf.WriteByte(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write(ea.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeExtendedAttributes encodes a list of EAs into a FILE_FULL_EA_INFORMATION
|
||||
// buffer for use with BackupWrite, ZwSetEaFile, etc.
|
||||
func EncodeExtendedAttributes(eas []ExtendedAttribute) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
for i := range eas {
|
||||
last := false
|
||||
if i == len(eas)-1 {
|
||||
last = true
|
||||
}
|
||||
|
||||
err := writeEa(&buf, &eas[i], last)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
320
vendor/github.com/Microsoft/go-winio/file.go
generated
vendored
320
vendor/github.com/Microsoft/go-winio/file.go
generated
vendored
|
|
@ -1,320 +0,0 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//sys cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) = CancelIoEx
|
||||
//sys createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) = CreateIoCompletionPort
|
||||
//sys getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
|
||||
//sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
|
||||
//sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
|
||||
|
||||
var (
|
||||
ErrFileClosed = errors.New("file has already been closed")
|
||||
ErrTimeout = &timeoutError{}
|
||||
)
|
||||
|
||||
type timeoutError struct{}
|
||||
|
||||
func (*timeoutError) Error() string { return "i/o timeout" }
|
||||
func (*timeoutError) Timeout() bool { return true }
|
||||
func (*timeoutError) Temporary() bool { return true }
|
||||
|
||||
type timeoutChan chan struct{}
|
||||
|
||||
var ioInitOnce sync.Once
|
||||
var ioCompletionPort windows.Handle
|
||||
|
||||
// ioResult contains the result of an asynchronous IO operation.
|
||||
type ioResult struct {
|
||||
bytes uint32
|
||||
err error
|
||||
}
|
||||
|
||||
// ioOperation represents an outstanding asynchronous Win32 IO.
|
||||
type ioOperation struct {
|
||||
o windows.Overlapped
|
||||
ch chan ioResult
|
||||
}
|
||||
|
||||
func initIO() {
|
||||
h, err := createIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ioCompletionPort = h
|
||||
go ioCompletionProcessor(h)
|
||||
}
|
||||
|
||||
// win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall.
|
||||
// It takes ownership of this handle and will close it if it is garbage collected.
|
||||
type win32File struct {
|
||||
handle windows.Handle
|
||||
wg sync.WaitGroup
|
||||
wgLock sync.RWMutex
|
||||
closing atomic.Bool
|
||||
socket bool
|
||||
readDeadline deadlineHandler
|
||||
writeDeadline deadlineHandler
|
||||
}
|
||||
|
||||
type deadlineHandler struct {
|
||||
setLock sync.Mutex
|
||||
channel timeoutChan
|
||||
channelLock sync.RWMutex
|
||||
timer *time.Timer
|
||||
timedout atomic.Bool
|
||||
}
|
||||
|
||||
// makeWin32File makes a new win32File from an existing file handle.
|
||||
func makeWin32File(h windows.Handle) (*win32File, error) {
|
||||
f := &win32File{handle: h}
|
||||
ioInitOnce.Do(initIO)
|
||||
_, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = setFileCompletionNotificationModes(h, windows.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS|windows.FILE_SKIP_SET_EVENT_ON_HANDLE)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.readDeadline.channel = make(timeoutChan)
|
||||
f.writeDeadline.channel = make(timeoutChan)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Deprecated: use NewOpenFile instead.
|
||||
func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
|
||||
return NewOpenFile(windows.Handle(h))
|
||||
}
|
||||
|
||||
func NewOpenFile(h windows.Handle) (io.ReadWriteCloser, error) {
|
||||
// If we return the result of makeWin32File directly, it can result in an
|
||||
// interface-wrapped nil, rather than a nil interface value.
|
||||
f, err := makeWin32File(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// closeHandle closes the resources associated with a Win32 handle.
|
||||
func (f *win32File) closeHandle() {
|
||||
f.wgLock.Lock()
|
||||
// Atomically set that we are closing, releasing the resources only once.
|
||||
if !f.closing.Swap(true) {
|
||||
f.wgLock.Unlock()
|
||||
// cancel all IO and wait for it to complete
|
||||
_ = cancelIoEx(f.handle, nil)
|
||||
f.wg.Wait()
|
||||
// at this point, no new IO can start
|
||||
windows.Close(f.handle)
|
||||
f.handle = 0
|
||||
} else {
|
||||
f.wgLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes a win32File.
|
||||
func (f *win32File) Close() error {
|
||||
f.closeHandle()
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsClosed checks if the file has been closed.
|
||||
func (f *win32File) IsClosed() bool {
|
||||
return f.closing.Load()
|
||||
}
|
||||
|
||||
// prepareIO prepares for a new IO operation.
|
||||
// The caller must call f.wg.Done() when the IO is finished, prior to Close() returning.
|
||||
func (f *win32File) prepareIO() (*ioOperation, error) {
|
||||
f.wgLock.RLock()
|
||||
if f.closing.Load() {
|
||||
f.wgLock.RUnlock()
|
||||
return nil, ErrFileClosed
|
||||
}
|
||||
f.wg.Add(1)
|
||||
f.wgLock.RUnlock()
|
||||
c := &ioOperation{}
|
||||
c.ch = make(chan ioResult)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ioCompletionProcessor processes completed async IOs forever.
|
||||
func ioCompletionProcessor(h windows.Handle) {
|
||||
for {
|
||||
var bytes uint32
|
||||
var key uintptr
|
||||
var op *ioOperation
|
||||
err := getQueuedCompletionStatus(h, &bytes, &key, &op, windows.INFINITE)
|
||||
if op == nil {
|
||||
panic(err)
|
||||
}
|
||||
op.ch <- ioResult{bytes, err}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: helsaawy - create an asyncIO version that takes a context
|
||||
|
||||
// asyncIO processes the return value from ReadFile or WriteFile, blocking until
|
||||
// the operation has actually completed.
|
||||
func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) {
|
||||
if err != windows.ERROR_IO_PENDING { //nolint:errorlint // err is Errno
|
||||
return int(bytes), err
|
||||
}
|
||||
|
||||
if f.closing.Load() {
|
||||
_ = cancelIoEx(f.handle, &c.o)
|
||||
}
|
||||
|
||||
var timeout timeoutChan
|
||||
if d != nil {
|
||||
d.channelLock.Lock()
|
||||
timeout = d.channel
|
||||
d.channelLock.Unlock()
|
||||
}
|
||||
|
||||
var r ioResult
|
||||
select {
|
||||
case r = <-c.ch:
|
||||
err = r.err
|
||||
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
if f.closing.Load() {
|
||||
err = ErrFileClosed
|
||||
}
|
||||
} else if err != nil && f.socket {
|
||||
// err is from Win32. Query the overlapped structure to get the winsock error.
|
||||
var bytes, flags uint32
|
||||
err = wsaGetOverlappedResult(f.handle, &c.o, &bytes, false, &flags)
|
||||
}
|
||||
case <-timeout:
|
||||
_ = cancelIoEx(f.handle, &c.o)
|
||||
r = <-c.ch
|
||||
err = r.err
|
||||
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
err = ErrTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// runtime.KeepAlive is needed, as c is passed via native
|
||||
// code to ioCompletionProcessor, c must remain alive
|
||||
// until the channel read is complete.
|
||||
// todo: (de)allocate *ioOperation via win32 heap functions, instead of needing to KeepAlive?
|
||||
runtime.KeepAlive(c)
|
||||
return int(r.bytes), err
|
||||
}
|
||||
|
||||
// Read reads from a file handle.
|
||||
func (f *win32File) Read(b []byte) (int, error) {
|
||||
c, err := f.prepareIO()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.wg.Done()
|
||||
|
||||
if f.readDeadline.timedout.Load() {
|
||||
return 0, ErrTimeout
|
||||
}
|
||||
|
||||
var bytes uint32
|
||||
err = windows.ReadFile(f.handle, b, &bytes, &c.o)
|
||||
n, err := f.asyncIO(c, &f.readDeadline, bytes, err)
|
||||
runtime.KeepAlive(b)
|
||||
|
||||
// Handle EOF conditions.
|
||||
if err == nil && n == 0 && len(b) != 0 {
|
||||
return 0, io.EOF
|
||||
} else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
|
||||
return 0, io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Write writes to a file handle.
|
||||
func (f *win32File) Write(b []byte) (int, error) {
|
||||
c, err := f.prepareIO()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.wg.Done()
|
||||
|
||||
if f.writeDeadline.timedout.Load() {
|
||||
return 0, ErrTimeout
|
||||
}
|
||||
|
||||
var bytes uint32
|
||||
err = windows.WriteFile(f.handle, b, &bytes, &c.o)
|
||||
n, err := f.asyncIO(c, &f.writeDeadline, bytes, err)
|
||||
runtime.KeepAlive(b)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (f *win32File) SetReadDeadline(deadline time.Time) error {
|
||||
return f.readDeadline.set(deadline)
|
||||
}
|
||||
|
||||
func (f *win32File) SetWriteDeadline(deadline time.Time) error {
|
||||
return f.writeDeadline.set(deadline)
|
||||
}
|
||||
|
||||
func (f *win32File) Flush() error {
|
||||
return windows.FlushFileBuffers(f.handle)
|
||||
}
|
||||
|
||||
func (f *win32File) Fd() uintptr {
|
||||
return uintptr(f.handle)
|
||||
}
|
||||
|
||||
func (d *deadlineHandler) set(deadline time.Time) error {
|
||||
d.setLock.Lock()
|
||||
defer d.setLock.Unlock()
|
||||
|
||||
if d.timer != nil {
|
||||
if !d.timer.Stop() {
|
||||
<-d.channel
|
||||
}
|
||||
d.timer = nil
|
||||
}
|
||||
d.timedout.Store(false)
|
||||
|
||||
select {
|
||||
case <-d.channel:
|
||||
d.channelLock.Lock()
|
||||
d.channel = make(chan struct{})
|
||||
d.channelLock.Unlock()
|
||||
default:
|
||||
}
|
||||
|
||||
if deadline.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
timeoutIO := func() {
|
||||
d.timedout.Store(true)
|
||||
close(d.channel)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
duration := deadline.Sub(now)
|
||||
if deadline.After(now) {
|
||||
// Deadline is in the future, set a timer to wait
|
||||
d.timer = time.AfterFunc(duration, timeoutIO)
|
||||
} else {
|
||||
// Deadline is in the past. Cancel all pending IO now.
|
||||
timeoutIO()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
106
vendor/github.com/Microsoft/go-winio/fileinfo.go
generated
vendored
106
vendor/github.com/Microsoft/go-winio/fileinfo.go
generated
vendored
|
|
@ -1,106 +0,0 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// FileBasicInfo contains file access time and file attributes information.
|
||||
type FileBasicInfo struct {
|
||||
CreationTime, LastAccessTime, LastWriteTime, ChangeTime windows.Filetime
|
||||
FileAttributes uint32
|
||||
_ uint32 // padding
|
||||
}
|
||||
|
||||
// alignedFileBasicInfo is a FileBasicInfo, but aligned to uint64 by containing
|
||||
// uint64 rather than windows.Filetime. Filetime contains two uint32s. uint64
|
||||
// alignment is necessary to pass this as FILE_BASIC_INFO.
|
||||
type alignedFileBasicInfo struct {
|
||||
CreationTime, LastAccessTime, LastWriteTime, ChangeTime uint64
|
||||
FileAttributes uint32
|
||||
_ uint32 // padding
|
||||
}
|
||||
|
||||
// GetFileBasicInfo retrieves times and attributes for a file.
|
||||
func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
|
||||
bi := &alignedFileBasicInfo{}
|
||||
if err := windows.GetFileInformationByHandleEx(
|
||||
windows.Handle(f.Fd()),
|
||||
windows.FileBasicInfo,
|
||||
(*byte)(unsafe.Pointer(bi)),
|
||||
uint32(unsafe.Sizeof(*bi)),
|
||||
); err != nil {
|
||||
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
// Reinterpret the alignedFileBasicInfo as a FileBasicInfo so it matches the
|
||||
// public API of this module. The data may be unnecessarily aligned.
|
||||
return (*FileBasicInfo)(unsafe.Pointer(bi)), nil
|
||||
}
|
||||
|
||||
// SetFileBasicInfo sets times and attributes for a file.
|
||||
func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error {
|
||||
// Create an alignedFileBasicInfo based on a FileBasicInfo. The copy is
|
||||
// suitable to pass to GetFileInformationByHandleEx.
|
||||
biAligned := *(*alignedFileBasicInfo)(unsafe.Pointer(bi))
|
||||
if err := windows.SetFileInformationByHandle(
|
||||
windows.Handle(f.Fd()),
|
||||
windows.FileBasicInfo,
|
||||
(*byte)(unsafe.Pointer(&biAligned)),
|
||||
uint32(unsafe.Sizeof(biAligned)),
|
||||
); err != nil {
|
||||
return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileStandardInfo contains extended information for the file.
|
||||
// FILE_STANDARD_INFO in WinBase.h
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info
|
||||
type FileStandardInfo struct {
|
||||
AllocationSize, EndOfFile int64
|
||||
NumberOfLinks uint32
|
||||
DeletePending, Directory bool
|
||||
}
|
||||
|
||||
// GetFileStandardInfo retrieves ended information for the file.
|
||||
func GetFileStandardInfo(f *os.File) (*FileStandardInfo, error) {
|
||||
si := &FileStandardInfo{}
|
||||
if err := windows.GetFileInformationByHandleEx(windows.Handle(f.Fd()),
|
||||
windows.FileStandardInfo,
|
||||
(*byte)(unsafe.Pointer(si)),
|
||||
uint32(unsafe.Sizeof(*si))); err != nil {
|
||||
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
return si, nil
|
||||
}
|
||||
|
||||
// FileIDInfo contains the volume serial number and file ID for a file. This pair should be
|
||||
// unique on a system.
|
||||
type FileIDInfo struct {
|
||||
VolumeSerialNumber uint64
|
||||
FileID [16]byte
|
||||
}
|
||||
|
||||
// GetFileID retrieves the unique (volume, file ID) pair for a file.
|
||||
func GetFileID(f *os.File) (*FileIDInfo, error) {
|
||||
fileID := &FileIDInfo{}
|
||||
if err := windows.GetFileInformationByHandleEx(
|
||||
windows.Handle(f.Fd()),
|
||||
windows.FileIdInfo,
|
||||
(*byte)(unsafe.Pointer(fileID)),
|
||||
uint32(unsafe.Sizeof(*fileID)),
|
||||
); err != nil {
|
||||
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
return fileID, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue