diff --git a/go.mod b/go.mod index 6e41571..ddc3339 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,47 @@ module github.com/middleware-labs/java-injector go 1.25.3 require ( + github.com/docker/docker v28.5.2+incompatible github.com/k0kubun/pp v3.0.1+incompatible github.com/shirou/gopsutil/v4 v4.25.9 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/containerd/errdefs v0.3.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.9.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect golang.org/x/sys v0.35.0 // indirect + golang.org/x/time v0.14.0 // indirect + gotest.tools/v3 v3.5.2 // indirect ) diff --git a/go.sum b/go.sum index 3581ad1..93c9998 100644 --- a/go.sum +++ b/go.sum @@ -1,28 +1,83 @@ +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= +github.com/containerd/errdefs v0.3.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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/google/go-cmp v0.5.6/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/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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU= github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= @@ -31,13 +86,50 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/pkg/cli/commands/instrument.go b/pkg/cli/commands/instrument.go index 6459b6a..a907255 100644 --- a/pkg/cli/commands/instrument.go +++ b/pkg/cli/commands/instrument.go @@ -326,7 +326,10 @@ func (c *InstrumentDockerCommand) Execute() error { updated := 0 skipped := 0 - dockerOps := docker.NewDockerOperations(ctx, installedPath) + dockerOps, err := docker.NewDockerOperations(ctx, installedPath) + if err != nil { + return fmt.Errorf("X error in createing docker operations, %v", err.Error()) + } for _, container := range containers { // Skip if already instrumented @@ -460,7 +463,10 @@ func (c *InstrumentContainerCommand) Execute() error { cfg.JavaAgentPath = docker.DefaultContainerAgentPath // Instrument - dockerOps := docker.NewDockerOperations(ctx, installedPath) + dockerOps, err := docker.NewDockerOperations(ctx, installedPath) + if err != nil { + return fmt.Errorf("X error in createing docker operations, %v", err.Error()) + } if err := dockerOps.InstrumentContainer(c.containerName, &cfg); err != nil { return fmt.Errorf("❌ Failed to instrument container: %v", err) } @@ -792,12 +798,6 @@ type ConfigInstrumentDockerCommand struct { configPath string } -// func NewConfigInstrumentDockerCommand(config *types.CommandConfig, configPath string) *ConfigInstrumentDockerCommand { -// return &ConfigInstrumentDockerCommand{ -// config: config, -// configPath: configPath, -// } -// } func NewConfigInstrumentDockerCommand(config *types.CommandConfig, configPath string) *ConfigInstrumentDockerCommand { // If no config path provided, try to find default if configPath == "" { @@ -875,7 +875,10 @@ func (c *ConfigInstrumentDockerCommand) Execute() error { updated := 0 skipped := 0 - dockerOps := docker.NewDockerOperations(ctx, installedPath) + dockerOps, err := docker.NewDockerOperations(ctx, installedPath) + if err != nil { + return fmt.Errorf("X error in createing docker operations, %v", err.Error()) + } for _, container := range containers { // Auto-update if already instrumented (no prompts) diff --git a/pkg/cli/commands/uninstrument.go b/pkg/cli/commands/uninstrument.go index 7cda1b3..fe6965d 100644 --- a/pkg/cli/commands/uninstrument.go +++ b/pkg/cli/commands/uninstrument.go @@ -215,7 +215,10 @@ func (c *UninstrumentDockerCommand) Execute() error { return nil } - dockerOps := docker.NewDockerOperations(ctx, c.config.DefaultAgentPath) + dockerOps, err := docker.NewDockerOperations(ctx, c.config.DefaultAgentPath) + if err != nil { + return fmt.Errorf("X error in createing docker operations, %v", err.Error()) + } // List instrumented containers instrumented, err := dockerOps.ListInstrumentedContainers() @@ -276,7 +279,10 @@ func (c *UninstrumentContainerCommand) Execute() error { return fmt.Errorf("❌ This command requires root privileges\n Run with: sudo mw-injector uninstrument-container %s", c.containerName) } - dockerOps := docker.NewDockerOperations(ctx, c.config.DefaultAgentPath) + dockerOps, err := docker.NewDockerOperations(ctx, c.config.DefaultAgentPath) + if err != nil { + return fmt.Errorf("X error in createing docker operations, %v", err.Error()) + } fmt.Printf("🔧 Uninstrumenting container: %s\n\n", c.containerName) diff --git a/pkg/discovery/discovery.go b/pkg/discovery/discovery.go index 8f35a59..b61f036 100644 --- a/pkg/discovery/discovery.go +++ b/pkg/discovery/discovery.go @@ -45,7 +45,7 @@ type JavaProcess struct { CPUPercent float64 `json:"process.cpu.percent"` Status string `json:"process.status"` - ContainerInfo *ContainerInfo `json:container_info, omitempty` + ContainerInfo *ContainerInfo `json:"container_info, omitempty"` } // 2. Add these methods to JavaProcess (can go at the end of discovery.go) diff --git a/pkg/docker/labels.go b/pkg/docker/labels.go new file mode 100644 index 0000000..0aab843 --- /dev/null +++ b/pkg/docker/labels.go @@ -0,0 +1,13 @@ +package docker + +const ( + // Label keys for instrumentation metadata + LabelInstrumented = "middleware.instrumented" + LabelInstrumentedAt = "middleware.instrumented_at" + LabelAgentPath = "middleware.agent_path" + LabelOriginalEnv = "middleware.original_env" + LabelComposeFile = "middleware.compose_file" + LabelComposeService = "middleware.compose_service" + LabelOriginalConfig = "middleware.original_config" + LabelServiceName = "middleware.service_name" +) diff --git a/pkg/docker/migration.go b/pkg/docker/migration.go new file mode 100644 index 0000000..f48bfbd --- /dev/null +++ b/pkg/docker/migration.go @@ -0,0 +1,21 @@ +package docker + +import ( + "fmt" + "os" +) + +func (do *DockerOperations) MigrateFromStateFile() error { + fmt.Println("🔄 Migrating from old state file to container labels...") + + // Check if old state file exists + if _, err := os.Stat(StateFile); os.IsNotExist(err) { + fmt.Println("No old state file found, migration not needed") + return nil + } + + // Load and process old state... + // Implementation here + + return nil +} diff --git a/pkg/docker/operations.go b/pkg/docker/operations.go index 89dbdc7..e52efb7 100644 --- a/pkg/docker/operations.go +++ b/pkg/docker/operations.go @@ -1,15 +1,23 @@ package docker import ( + "archive/tar" + "bytes" "context" "encoding/json" "fmt" + "log" "os" "os/exec" "path/filepath" "strings" "time" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" "github.com/k0kubun/pp" "github.com/middleware-labs/java-injector/pkg/config" "github.com/middleware-labs/java-injector/pkg/discovery" @@ -28,16 +36,22 @@ const ( type DockerOperations struct { ctx context.Context discoverer *discovery.DockerDiscoverer - hostAgentPath string + hostAgentPath string // TODO: Make this name better. Its not exactly a hostAgent + cli *client.Client } // NewDockerOperations creates a new Docker operations handler -func NewDockerOperations(ctx context.Context, hostAgentPath string) *DockerOperations { +func NewDockerOperations(ctx context.Context, hostAgentPath string) (*DockerOperations, error) { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return nil, fmt.Errorf("could not create a client for docker operations %v", err.Error()) + } return &DockerOperations{ ctx: ctx, discoverer: discovery.NewDockerDiscoverer(ctx), hostAgentPath: hostAgentPath, - } + cli: cli, + }, nil } // InstrumentedState represents the state of instrumented containers @@ -136,57 +150,28 @@ func (do *DockerOperations) InstrumentContainer(containerName string, cfg *confi func (do *DockerOperations) instrumentStandaloneContainer(container *discovery.DockerContainer, cfg *config.ProcessConfiguration) error { fmt.Printf("🔧 Instrumenting standalone container: %s\n", container.ContainerName) - // Step 1: Get and save original container configuration BEFORE making any changes + // Step 1: Get original container configuration containerConfig, err := do.getContainerConfig(container.ContainerID) if err != nil { return fmt.Errorf("failed to get container config: %w", err) } - // Save original configuration as JSON string for restoration - originalConfigBytes, err := json.Marshal(containerConfig) - if err != nil { - return fmt.Errorf("failed to serialize original config: %w", err) - } - - // Build original recreation command from current state (before instrumentation) - originalRecreationCommand := do.buildOriginalDockerRunCommand(containerConfig, container.ContainerName) - - // Step 2: Copy agent to container - if err := do.copyAgentToContainer(container.ContainerID); err != nil { - return fmt.Errorf("failed to copy agent: %w", err) - } - fmt.Println(" ✅ Agent copied to container") - - // Step 3: Build new environment variables with instrumentation + // Step 2: Build new environment variables with instrumentation newEnv := do.buildInstrumentationEnv(container, cfg) - // Step 4: Stop the container - fmt.Println(" 🛑 Stopping container...") - if err := do.stopContainer(container.ContainerID); err != nil { - return fmt.Errorf("failed to stop container: %w", err) - } - - // Step 5: Commit container to preserve any changes - newImageName := fmt.Sprintf("%s-mw-instrumented:latest", container.ContainerName) - if err := do.commitContainer(container.ContainerID, newImageName); err != nil { - fmt.Printf(" ⚠️ Warning: Could not commit container: %v\n", err) - // Use original image name if commit fails - newImageName = container.ImageName + ":" + container.ImageTag + // Convert map to slice format + envSlice := make([]string, 0, len(newEnv)) + for k, v := range newEnv { + envSlice = append(envSlice, fmt.Sprintf("%s=%s", k, v)) } - // Step 6: Remove old container - if err := do.removeContainer(container.ContainerID); err != nil { - return fmt.Errorf("failed to remove old container: %w", err) - } - - // Step 7: Recreate container with instrumentation using the committed image - instrumentedRunCommand := do.buildInstrumentedDockerRunCommand(containerConfig, newEnv, container.ContainerName, newImageName) - if err := do.runContainer(instrumentedRunCommand); err != nil { + // Step 3: Recreate container with volume mount + new environment + if err := do.recreateContainerWithAPI(*containerConfig, envSlice); err != nil { return fmt.Errorf("failed to recreate container: %w", err) } - // Step 8: Save state with ORIGINAL recreation command for proper restoration - if err := do.saveContainerStateWithCommand(container, cfg, originalRecreationCommand, string(originalConfigBytes)); err != nil { + // Step 4: Save state + if err := do.saveContainerState(container, cfg); err != nil { fmt.Printf(" ⚠️ Warning: Could not save state: %v\n", err) } @@ -195,30 +180,28 @@ func (do *DockerOperations) instrumentStandaloneContainer(container *discovery.D } // buildInstrumentedDockerRunCommand creates docker run command with instrumentation -func (do *DockerOperations) buildInstrumentedDockerRunCommand(config map[string]interface{}, env map[string]string, containerName, imageName string) string { +func (do *DockerOperations) buildInstrumentedDockerRunCommand(config container.InspectResponse, env map[string]string, containerName, imageName string) string { + pp.Println("BUILDING INSTRUMENTED DOCKER RUN COMMAND") var cmdParts []string cmdParts = append(cmdParts, "docker", "run", "-d") cmdParts = append(cmdParts, "--name", containerName) + configSection := config.Config + // Add environment variables (with instrumentation) for k, v := range env { cmdParts = append(cmdParts, "-e", fmt.Sprintf("%s=%s", k, v)) } // Add original volume mounts - if mounts, ok := config["Mounts"].([]interface{}); ok { - for _, m := range mounts { - if mount, ok := m.(map[string]interface{}); ok { - src, srcOk := mount["Source"].(string) - dst, dstOk := mount["Destination"].(string) - - if srcOk && dstOk { - mode := "rw" - if rw, ok := mount["RW"].(bool); ok && !rw { - mode = "ro" - } - cmdParts = append(cmdParts, "-v", fmt.Sprintf("%s:%s:%s", src, dst, mode)) + if len(config.Mounts) > 0 { + for _, m := range config.Mounts { + if m.Source != "" && m.Destination != "" { + mode := "rw" + if !m.RW { + mode = "ro" } + cmdParts = append(cmdParts, "-v", fmt.Sprintf("%s:%s:%s", m.Source, m.Destination, mode)) } } } @@ -227,18 +210,17 @@ func (do *DockerOperations) buildInstrumentedDockerRunCommand(config map[string] cmdParts = append(cmdParts, "-v", fmt.Sprintf("%s:%s:ro", do.hostAgentPath, DefaultContainerAgentPath)) // Add port mappings - if networkSettings, ok := config["NetworkSettings"].(map[string]interface{}); ok { - if ports, ok := networkSettings["Ports"].(map[string]interface{}); ok { + if networkSettings := config.NetworkSettings; networkSettings != nil { + if ports := networkSettings.Ports; ports != nil { for containerPort, bindings := range ports { - if bindingList, ok := bindings.([]interface{}); ok && len(bindingList) > 0 { - if binding, ok := bindingList[0].(map[string]interface{}); ok { - if hostPort, ok := binding["HostPort"].(string); ok && hostPort != "" { - hostIP := "0.0.0.0" - if hip, ok := binding["HostIp"].(string); ok && hip != "" { - hostIP = hip - } - cmdParts = append(cmdParts, "-p", fmt.Sprintf("%s:%s:%s", hostIP, hostPort, containerPort)) + if len(bindings) > 0 { + binding := bindings[0] + if binding.HostPort != "" { + hostIP := "0.0.0.0" + if binding.HostIP != "" { + hostIP = binding.HostIP } + cmdParts = append(cmdParts, "-p", fmt.Sprintf("%s:%s:%s", hostIP, binding.HostPort, containerPort)) } } } @@ -246,9 +228,9 @@ func (do *DockerOperations) buildInstrumentedDockerRunCommand(config map[string] } // Add networks - if networkSettings, ok := config["NetworkSettings"].(map[string]interface{}); ok { - if networks, ok := networkSettings["Networks"].(map[string]interface{}); ok { - for networkName := range networks { + if config.NetworkSettings != nil { + if networks := config.NetworkSettings.Networks; networks != nil { + for networkName := range config.NetworkSettings.Networks { if networkName != "bridge" { cmdParts = append(cmdParts, "--network", networkName) } @@ -257,43 +239,37 @@ func (do *DockerOperations) buildInstrumentedDockerRunCommand(config map[string] } // Add restart policy - if hostConfig, ok := config["HostConfig"].(map[string]interface{}); ok { - if restartPolicy, ok := hostConfig["RestartPolicy"].(map[string]interface{}); ok { - if name, ok := restartPolicy["Name"].(string); ok && name != "" && name != "no" { - if maxRetries, ok := restartPolicy["MaximumRetryCount"].(float64); ok && maxRetries > 0 { - cmdParts = append(cmdParts, "--restart", fmt.Sprintf("%s:%d", name, int(maxRetries))) - } else { - cmdParts = append(cmdParts, "--restart", name) - } + + if hostConfig := config.HostConfig; hostConfig != nil { + if name := hostConfig.RestartPolicy.Name; name != "" && name != "no" { + if maxRetries := hostConfig.RestartPolicy.MaximumRetryCount; maxRetries > 0 { + cmdParts = append(cmdParts, "--restart", fmt.Sprintf("%s:%d", name, int(maxRetries))) + } else { + cmdParts = append(cmdParts, "--restart", string(name)) } } + } - // Add working directory - if configSection, ok := config["Config"].(map[string]interface{}); ok { - if workingDir, ok := configSection["WorkingDir"].(string); ok && workingDir != "" { - cmdParts = append(cmdParts, "--workdir", workingDir) - } + if workingDir := configSection.WorkingDir; workingDir != "" { + cmdParts = append(cmdParts, "--workdir", workingDir) + } - // Add user - if user, ok := configSection["User"].(string); ok && user != "" { - cmdParts = append(cmdParts, "--user", user) - } + if user := configSection.User; user != "" { + cmdParts = append(cmdParts, "--user", user) + } - // Add original command - if cmd, ok := configSection["Cmd"].([]interface{}); ok && len(cmd) > 0 { - cmdParts = append(cmdParts, imageName) - for _, c := range cmd { - if cStr, ok := c.(string); ok { - cmdParts = append(cmdParts, cStr) - } - } - return strings.Join(cmdParts, " ") - } + // Add image + if image := configSection.Image; image != "" { + cmdParts = append(cmdParts, image) + } + + if cmd := configSection.Cmd; len(cmd) > 0 { + for _, c := range cmd { + cmdParts = append(cmdParts, c) } } - // Add image - cmdParts = append(cmdParts, imageName) + pp.Println("ORIGINAL COMMAND: ", strings.Join(cmdParts, " ")) return strings.Join(cmdParts, " ") } @@ -369,77 +345,61 @@ func (do *DockerOperations) instrumentComposeContainer(container *discovery.Dock } // buildOriginalDockerRunCommand creates the original docker run command before instrumentation -func (do *DockerOperations) buildOriginalDockerRunCommand(config map[string]interface{}, containerName string) string { +func (do *DockerOperations) buildOriginalDockerRunCommand(config *container.InspectResponse, containerName string) string { var cmdParts []string cmdParts = append(cmdParts, "docker", "run", "-d") cmdParts = append(cmdParts, "--name", containerName) - configSection, ok := config["Config"].(map[string]interface{}) - if !ok { - return "" - } + configSection := config.Config // Add original environment variables (without instrumentation) - if env, ok := configSection["Env"].([]interface{}); ok { - for _, e := range env { - if envStr, ok := e.(string); ok { - // Skip any existing MW_ or OTEL_ variables and JAVA_TOOL_OPTIONS with javaagent - if !strings.HasPrefix(envStr, "MW_") && - !strings.HasPrefix(envStr, "OTEL_") && - !(strings.HasPrefix(envStr, "JAVA_TOOL_OPTIONS=") && strings.Contains(envStr, "javaagent")) { - cmdParts = append(cmdParts, "-e", envStr) - } - } + for _, e := range configSection.Env { + if !strings.HasPrefix(e, "MW_") && + !strings.HasPrefix(e, "OTEL_") && + !(strings.HasPrefix(e, "JAVA_TOOL_OPTIONS=") && strings.Contains(e, "javaagent")) { + + cmdParts = append(cmdParts, "-e", e) } } // Add original volume mounts (excluding our agent mount) - if mounts, ok := config["Mounts"].([]interface{}); ok { - for _, m := range mounts { - if mount, ok := m.(map[string]interface{}); ok { - src, srcOk := mount["Source"].(string) - dst, dstOk := mount["Destination"].(string) + for _, mount := range config.Mounts { + src := mount.Source + dst := mount.Destination - if srcOk && dstOk { - // Skip our agent mount - if dst == DefaultContainerAgentPath { - continue - } - - mode := "rw" - if rw, ok := mount["RW"].(bool); ok && !rw { - mode = "ro" - } - cmdParts = append(cmdParts, "-v", fmt.Sprintf("%s:%s:%s", src, dst, mode)) - } + if src != "" && dst != "" { + if dst == DefaultContainerAgentPath { + continue } + mode := "rw" + if !mount.RW { + mode = "ro" + } + cmdParts = append(cmdParts, "-v", fmt.Sprintf("%s:%s:%s", src, dst, mode)) } } // Add port mappings - if networkSettings, ok := config["NetworkSettings"].(map[string]interface{}); ok { - if ports, ok := networkSettings["Ports"].(map[string]interface{}); ok { - for containerPort, bindings := range ports { - if bindingList, ok := bindings.([]interface{}); ok && len(bindingList) > 0 { - if binding, ok := bindingList[0].(map[string]interface{}); ok { - if hostPort, ok := binding["HostPort"].(string); ok && hostPort != "" { - hostIP := "0.0.0.0" - if hip, ok := binding["HostIp"].(string); ok && hip != "" { - hostIP = hip - } - cmdParts = append(cmdParts, "-p", fmt.Sprintf("%s:%s:%s", hostIP, hostPort, containerPort)) - } + if config.NetworkSettings != nil { + for containerPort, bindings := range config.NetworkSettings.NetworkSettingsBase.Ports { + if len(bindings) > 0 { + binding := bindings[0] + if binding.HostPort != "" { + hostIP := "0.0.0.0" + if binding.HostIP != "" { + hostIP = binding.HostIP } + cmdParts = append(cmdParts, "-p", fmt.Sprintf("%s:%s:%s", hostIP, binding.HostPort, containerPort)) } } } } // Add networks - if networkSettings, ok := config["NetworkSettings"].(map[string]interface{}); ok { - if networks, ok := networkSettings["Networks"].(map[string]interface{}); ok { - for networkName := range networks { - if networkName != "bridge" { // Skip default bridge network + if config.NetworkSettings != nil { + if config.NetworkSettings.Networks != nil { + for networkName := range config.NetworkSettings.Networks { + if networkName != "bridge" { cmdParts = append(cmdParts, "--network", networkName) } } @@ -447,42 +407,37 @@ func (do *DockerOperations) buildOriginalDockerRunCommand(config map[string]inte } // Add restart policy - if hostConfig, ok := config["HostConfig"].(map[string]interface{}); ok { - if restartPolicy, ok := hostConfig["RestartPolicy"].(map[string]interface{}); ok { - if name, ok := restartPolicy["Name"].(string); ok && name != "" && name != "no" { - if maxRetries, ok := restartPolicy["MaximumRetryCount"].(float64); ok && maxRetries > 0 { - cmdParts = append(cmdParts, "--restart", fmt.Sprintf("%s:%d", name, int(maxRetries))) - } else { - cmdParts = append(cmdParts, "--restart", name) - } + if hostConfig := config.HostConfig; hostConfig != nil { + if name := hostConfig.RestartPolicy.Name; name != "" && name != "no" { + if maxRetries := hostConfig.RestartPolicy.MaximumRetryCount; maxRetries > 0 { + cmdParts = append(cmdParts, "--restart", fmt.Sprintf("%s:%d", name, int(maxRetries))) + } else { + cmdParts = append(cmdParts, "--restart", string(name)) } } - // Add working directory - if workingDir, ok := configSection["WorkingDir"].(string); ok && workingDir != "" { - cmdParts = append(cmdParts, "--workdir", workingDir) - } + } - // Add user - if user, ok := configSection["User"].(string); ok && user != "" { - cmdParts = append(cmdParts, "--user", user) - } + if workingDir := configSection.WorkingDir; workingDir != "" { + cmdParts = append(cmdParts, "--workdir", workingDir) + } + + if user := configSection.User; user != "" { + cmdParts = append(cmdParts, "--user", user) } // Add original image - if image, ok := configSection["Image"].(string); ok { + if image := configSection.Image; image != "" { cmdParts = append(cmdParts, image) } // Add original command - if cmd, ok := configSection["Cmd"].([]interface{}); ok && len(cmd) > 0 { + if cmd := configSection.Cmd; len(cmd) > 0 { for _, c := range cmd { - if cStr, ok := c.(string); ok { - cmdParts = append(cmdParts, cStr) - } + cmdParts = append(cmdParts, c) } } - + pp.Println("ORIGINAL COMMAND: ", strings.Join(cmdParts, " ")) return strings.Join(cmdParts, " ") } @@ -502,35 +457,284 @@ func (do *DockerOperations) saveContainerStateWithCommand(container *discovery.D OriginalEnv: container.Environment, ComposeFile: container.ComposeFile, ComposeService: container.ComposeService, - RecreationCommand: recreationCommand, // Now properly set! - OriginalConfig: originalConfig, // Full original config for debugging + RecreationCommand: recreationCommand, + OriginalConfig: originalConfig, // Full original config for debugging } state.UpdatedAt = time.Now() return do.saveState(state) } -// UninstrumentContainer removes instrumentation from a container func (do *DockerOperations) UninstrumentContainer(containerName string) error { - // Load state to check if container was instrumented by us + // Strategy 1: Try label-based uninstrumentation first (new approach) + listOptions := container.ListOptions{ + All: true, + Filters: filters.NewArgs( + filters.Arg("name", containerName), + filters.Arg("label", LabelInstrumented+"=true"), + ), + } + + containers, err := do.cli.ContainerList(do.ctx, listOptions) + if err != nil { + return err + } + + if len(containers) > 0 { + // Found container with labels - use label-based restoration + fmt.Printf("🔧 Found container with instrumentation labels\n") + info, err := do.GetContainerInstrumentationInfo(containers[0].ID) + if err != nil { + return fmt.Errorf("failed to get instrumentation info: %w", err) + } + return do.restoreContainerFromLabels(containers[0].ID, info) + } + + // Strategy 2: Check state file (old approach) state, err := do.loadState() + if err == nil { + if containerState, exists := state.Containers[containerName]; exists { + fmt.Printf("🔧 Found container in state file\n") + if containerState.ComposeFile != "" { + return do.uninstrumentComposeContainer(&containerState) + } else { + return do.uninstrumentStandaloneContainer(&containerState) + } + } + } + + // Strategy 3: Try to detect compose information and restore from backup + listOptionsAny := container.ListOptions{ + All: true, + Filters: filters.NewArgs(filters.Arg("name", containerName)), + } + + containers, err = do.cli.ContainerList(do.ctx, listOptionsAny) if err != nil { - return fmt.Errorf("failed to load state: %w", err) + return err } - containerState, exists := state.Containers[containerName] - if !exists { - return fmt.Errorf("container %s was not instrumented by this tool", containerName) + if len(containers) == 0 { + return fmt.Errorf("container %s not found", containerName) + } + + containerInfo := containers[0] + composeFile, composeService := do.detectComposeInfo(containerInfo) + if composeFile != "" { + fmt.Printf("🔧 Detected compose container, attempting backup restoration\n") + return do.restoreComposeFile(composeFile, composeService) + } + + // Strategy 4: Manual removal of instrumentation environment variables + fmt.Printf("⚠️ Unable to automatically uninstrument container %s\n", containerName) + fmt.Printf("💡 Manual steps to uninstrument:\n") + fmt.Printf(" 1. Remove JAVA_TOOL_OPTIONS containing javaagent\n") + fmt.Printf(" 2. Remove MW_* environment variables\n") + fmt.Printf(" 3. Remove OTEL_* environment variables\n") + fmt.Printf(" 4. Remove agent volume mount: %s\n", DefaultContainerAgentPath) + fmt.Printf(" 5. Restart the container\n") + + return fmt.Errorf("unable to determine how to uninstrument container %s", containerName) +} + +func (do *DockerOperations) uninstrumentComposeContainerByName(containerName string) error { + // Look for the container without label filter + listOptions := container.ListOptions{ + All: true, + Filters: filters.NewArgs(filters.Arg("name", containerName)), + } + + containers, err := do.cli.ContainerList(do.ctx, listOptions) + if err != nil { + return err + } + + if len(containers) == 0 { + return fmt.Errorf("container %s not found", containerName) + } + + container := containers[0] + + // Try to find the compose file and restore from backup + composeFile, composeService := do.detectComposeInfo(container) + if composeFile != "" { + return do.restoreComposeFile(composeFile, composeService) + } + + return fmt.Errorf("unable to determine how to uninstrument container %s", containerName) +} + +func (do *DockerOperations) detectComposeInfo(container types.Container) (string, string) { + // Check compose labels + if projectName := container.Labels["com.docker.compose.project"]; projectName != "" { + if service := container.Labels["com.docker.compose.service"]; service != "" { + if workingDir := container.Labels["com.docker.compose.project.working_dir"]; workingDir != "" { + // Look for docker-compose.yaml in the working directory + possibleFiles := []string{ + filepath.Join(workingDir, "docker-compose.yaml"), + filepath.Join(workingDir, "docker-compose.yml"), + } + + for _, file := range possibleFiles { + if _, err := os.Stat(file); err == nil { + return file, service + } + } + } + } } - fmt.Printf("🔧 Uninstrumenting container: %s\n", containerName) + return "", "" +} + +func (do *DockerOperations) restoreComposeFile(composeFile, serviceName string) error { + backupFile := composeFile + ".backup" - // Check if it's a compose container - if containerState.ComposeFile != "" { - return do.uninstrumentComposeContainer(&containerState) + if _, err := os.Stat(backupFile); err == nil { + // Restore from backup + if err := do.copyFile(backupFile, composeFile); err != nil { + return fmt.Errorf("failed to restore compose file: %w", err) + } + + fmt.Printf(" ✅ Restored %s from backup\n", filepath.Base(composeFile)) + + // Recreate the service + return do.recreateComposeServiceFromFile(composeFile, serviceName) } - return do.uninstrumentStandaloneContainer(&containerState) + return fmt.Errorf("backup file not found: %s", backupFile) +} + +func (do *DockerOperations) recreateComposeServiceFromFile(composeFile, serviceName string) error { + workingDir := filepath.Dir(composeFile) + + // Change to compose file directory + originalDir, _ := os.Getwd() + defer os.Chdir(originalDir) + + if err := os.Chdir(workingDir); err != nil { + return fmt.Errorf("failed to change to working directory: %w", err) + } + + fmt.Printf(" Working directory: %s\n", workingDir) + + // Stop and remove the existing container first + fmt.Println(" Stopping existing container...") + stopCmd := exec.CommandContext(do.ctx, "docker-compose", "stop", serviceName) + if output, err := stopCmd.CombinedOutput(); err != nil { + fmt.Printf(" Warning: Failed to stop container: %s\n", string(output)) + } + + fmt.Println(" Removing existing container...") + rmCmd := exec.CommandContext(do.ctx, "docker-compose", "rm", "-f", serviceName) + if output, err := rmCmd.CombinedOutput(); err != nil { + fmt.Printf(" Warning: Failed to remove container: %s\n", string(output)) + } + + // Recreate with restored compose file + fmt.Println(" Creating new container from restored compose file...") + cmd := exec.CommandContext(do.ctx, "docker-compose", "up", "-d", serviceName) + + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Printf(" Docker-compose error output: %s\n", string(output)) + return fmt.Errorf("failed to recreate service: %w", err) + } + + return nil +} + +// Add this method to pkg/docker/operations.go +func (do *DockerOperations) restoreContainerFromLabels(containerID string, info *InstrumentationInfo) error { + fmt.Printf("🔧 Restoring container to original state: %s\n", containerID[:12]) + + // Get current container info + containerInfo, err := do.cli.ContainerInspect(do.ctx, containerID) + if err != nil { + return fmt.Errorf("failed to inspect container: %w", err) + } + + // Parse original configuration from labels + var originalConfig container.Config + if info.OriginalConfig != "" { + if err := json.Unmarshal([]byte(info.OriginalConfig), &originalConfig); err != nil { + return fmt.Errorf("failed to parse original config: %w", err) + } + } else { + // Fallback: use current config but restore original environment + originalConfig = *containerInfo.Config + } + + // Restore original environment variables + originalEnvSlice := make([]string, 0, len(info.OriginalEnv)) + for k, v := range info.OriginalEnv { + originalEnvSlice = append(originalEnvSlice, fmt.Sprintf("%s=%s", k, v)) + } + originalConfig.Env = originalEnvSlice + + // Remove instrumentation labels + if originalConfig.Labels == nil { + originalConfig.Labels = make(map[string]string) + } + cleanLabels := make(map[string]string) + for k, v := range originalConfig.Labels { + // Skip middleware instrumentation labels + if !strings.HasPrefix(k, "middleware.") { + cleanLabels[k] = v + } + } + originalConfig.Labels = cleanLabels + + // Create host config without agent volume mount + originalHostConfig := *containerInfo.HostConfig + + // Remove agent volume mount + var cleanBinds []string + for _, bind := range originalHostConfig.Binds { + // Skip the agent mount + if !strings.Contains(bind, DefaultContainerAgentPath) { + cleanBinds = append(cleanBinds, bind) + } + } + originalHostConfig.Binds = cleanBinds + + // Get container name + containerName := containerInfo.Name + if strings.HasPrefix(containerName, "/") { + containerName = containerName[1:] + } + + // Stop and remove current container + if err := do.stopContainer(containerID); err != nil { + return fmt.Errorf("failed to stop container: %w", err) + } + + if err := do.removeContainer(containerID); err != nil { + return fmt.Errorf("failed to remove container: %w", err) + } + + // Create restored container + resp, err := do.cli.ContainerCreate( + do.ctx, + &originalConfig, + &originalHostConfig, + &network.NetworkingConfig{ + EndpointsConfig: containerInfo.NetworkSettings.Networks, + }, + nil, // platform + containerName, + ) + if err != nil { + return fmt.Errorf("failed to create restored container: %w", err) + } + + // Start the restored container + if err := do.cli.ContainerStart(do.ctx, resp.ID, container.StartOptions{}); err != nil { + return fmt.Errorf("failed to start restored container: %w", err) + } + + fmt.Printf(" ✅ Container %s restored to original configuration\n", containerName) + return nil } // uninstrumentStandaloneContainer removes instrumentation from standalone container @@ -595,20 +799,58 @@ func (do *DockerOperations) uninstrumentComposeContainer(state *ContainerState) // copyAgentToContainer copies the agent JAR to a running container func (do *DockerOperations) copyAgentToContainer(containerID string) error { - // Create directory in container - mkdirCmd := exec.CommandContext(do.ctx, "docker", "exec", containerID, "mkdir", "-p", "/opt/middleware/agents") - if err := mkdirCmd.Run(); err != nil { - // Try without mkdir if it fails (some distroless images don't have mkdir) - fmt.Println(" ⚠️ Could not create directory, trying direct copy...") + agentFileContent, err := os.ReadFile(do.hostAgentPath) + if err != nil { + return fmt.Errorf("failed to read agent file: %w", err) + } + tarBuffer, err := do.createAgentTar(agentFileContent) + if err != nil { + return fmt.Errorf("failed to create Agent tar: %w", err) } - // Copy agent file - containerPath := containerID + ":" + DefaultContainerAgentPath - cmd := exec.CommandContext(do.ctx, "docker", "cp", do.hostAgentPath, containerPath) - return cmd.Run() + err = do.cli.CopyToContainer( + do.ctx, + containerID, + "/opt/middleware/agents/", + tarBuffer, + container.CopyToContainerOptions{ + AllowOverwriteDirWithFile: false, + }, + ) + + if err != nil { + return fmt.Errorf("failed to copy agent to container: %w", err) + } + + fmt.Println(" ✅ Agent copied to container via Docker API") + return nil +} + +func (do *DockerOperations) createAgentTar(agentContent []byte) (*bytes.Buffer, error) { + tarBuffer := &bytes.Buffer{} + tarWriter := tar.NewWriter(tarBuffer) + defer tarWriter.Close() + + // Create tar header for the agent file + header := &tar.Header{ + Name: "middleware-javaagent.jar", + Size: int64(len(agentContent)), + Mode: 0644, + ModTime: time.Now(), + Typeflag: tar.TypeReg, + } + + if err := tarWriter.WriteHeader(header); err != nil { + return nil, err + } + + if _, err := tarWriter.Write(agentContent); err != nil { + return nil, err + } + + return tarBuffer, nil } -// buildInstrumentationEnv builds environment variables for instrumentation func (do *DockerOperations) buildInstrumentationEnv(container *discovery.DockerContainer, cfg *config.ProcessConfiguration) map[string]string { env := make(map[string]string) @@ -641,41 +883,253 @@ func (do *DockerOperations) buildInstrumentationEnv(container *discovery.DockerC } // getContainerConfig gets full container configuration -func (do *DockerOperations) getContainerConfig(containerID string) (map[string]interface{}, error) { - cmd := exec.CommandContext(do.ctx, "docker", "inspect", containerID) - output, err := cmd.Output() +func (do *DockerOperations) getContainerConfig(containerID string) (*container.InspectResponse, error) { + ctx := context.Background() + containerInfo, err := do.cli.ContainerInspect(ctx, containerID) if err != nil { - return nil, err + log.Fatal(err) } - var inspectData []map[string]interface{} - if err := json.Unmarshal(output, &inspectData); err != nil { - return nil, err - } - - if len(inspectData) == 0 { - return nil, fmt.Errorf("no data returned") - } - - return inspectData[0], nil + return &containerInfo, nil } // stopContainer stops a running container func (do *DockerOperations) stopContainer(containerID string) error { - cmd := exec.CommandContext(do.ctx, "docker", "stop", containerID) - return cmd.Run() + // Docker uses a graceful shutdown process when stopping containers: + // 1. First, Docker sends SIGTERM to the main process (PID 1) in the container + // 2. The application has 'timeout' seconds to handle SIGTERM and shut down gracefully + // - This allows the app to: close database connections, save state, cleanup resources, etc. + // 3. If the container is still running after the timeout expires, Docker sends SIGKILL + // - SIGKILL cannot be caught or ignored - it immediately terminates the process + // - This prevents containers from hanging indefinitely during shutdown + // + // 30 seconds is a reasonable timeout for most Java applications to: + // - Complete current requests + // - Close connection pools + // - Flush logs and caches + // - Perform other cleanup operations + + timeout := 30 + + err := do.cli.ContainerStop( + do.ctx, + containerID, + container.StopOptions{ + Timeout: &timeout, + }, + ) + + if err != nil { + return fmt.Errorf("failed to stop container %s: %w", containerID, err) + } + + return nil } // stopContainerByName stops a container by name func (do *DockerOperations) stopContainerByName(name string) error { - cmd := exec.CommandContext(do.ctx, "docker", "stop", name) - return cmd.Run() + err := do.stopContainer(name) + if err != nil { + return fmt.Errorf("failed to stop container %s: %w", name, err) + } + + return nil } // removeContainer removes a container func (do *DockerOperations) removeContainer(containerID string) error { - cmd := exec.CommandContext(do.ctx, "docker", "rm", containerID) - return cmd.Run() + err := do.cli.ContainerRemove( + do.ctx, + containerID, + container.RemoveOptions{ + Force: true, + }, + ) + + if err != nil { + return fmt.Errorf("failed to remove container %s: %w", containerID, err) + } + + return nil +} + +func (do *DockerOperations) updateContainerEnvironment( + containerID string, + newEnv []string, +) error { + containerInfo, err := do.cli.ContainerInspect(do.ctx, containerID) + if err != nil { + return fmt.Errorf("failed to inspect container %s: %w", containerID, err) + } + + // For now, we'll still need to recreate the container because Docker doesn't + // allow updating environment variables of existing containers + // But we'll do it through the API instead of shell commands + return do.recreateContainerWithAPI(containerInfo, newEnv) +} + +func (do *DockerOperations) recreateContainerWithAPI(containerInfo container.InspectResponse, newEnv []string) error { + // Extract original environment from current env (before instrumentation was added) + originalEnv := do.extractOriginalEnvFromCurrent(containerInfo.Config.Env) + + // Derive service name from container metadata + serviceName := do.deriveServiceName(containerInfo) + // Serialize original config for potential restoration + originalConfigBytes, err := json.Marshal(containerInfo.Config) + if err != nil { + return fmt.Errorf("failed to serialize original config: %w", err) + } + + // Create instrumentation labels + instrumentationLabels := map[string]string{ + LabelInstrumented: "true", + LabelInstrumentedAt: time.Now().Format(time.RFC3339), + LabelAgentPath: do.hostAgentPath, + LabelServiceName: serviceName, + LabelOriginalEnv: do.serializeEnv(originalEnv), + LabelOriginalConfig: string(originalConfigBytes), + } + + // Merge with existing labels (preserve user labels, add instrumentation labels) + newLabels := make(map[string]string) + if containerInfo.Config.Labels != nil { + for k, v := range containerInfo.Config.Labels { + newLabels[k] = v + } + } + for k, v := range instrumentationLabels { + newLabels[k] = v + } + + // Create new container config based on existing one + config := &container.Config{ + Image: containerInfo.Config.Image, + Env: newEnv, // Updated environment with instrumentation + Cmd: containerInfo.Config.Cmd, + Entrypoint: containerInfo.Config.Entrypoint, + WorkingDir: containerInfo.Config.WorkingDir, + User: containerInfo.Config.User, + Labels: newLabels, // Include instrumentation metadata + ExposedPorts: containerInfo.Config.ExposedPorts, + } + + // Create host config based on existing one + hostConfig := &container.HostConfig{ + Binds: containerInfo.HostConfig.Binds, + PortBindings: containerInfo.HostConfig.PortBindings, + RestartPolicy: containerInfo.HostConfig.RestartPolicy, + NetworkMode: containerInfo.HostConfig.NetworkMode, + VolumeDriver: containerInfo.HostConfig.VolumeDriver, + VolumesFrom: containerInfo.HostConfig.VolumesFrom, + Resources: containerInfo.HostConfig.Resources, + } + + // Add agent volume mount + agentMount := fmt.Sprintf("%s:%s:ro", do.hostAgentPath, DefaultContainerAgentPath) + hostConfig.Binds = append(hostConfig.Binds, agentMount) + + // Network config + networkConfig := &network.NetworkingConfig{ + EndpointsConfig: containerInfo.NetworkSettings.Networks, + } + + containerName := containerInfo.Name + if strings.HasPrefix(containerName, "/") { + containerName = containerName[1:] // Remove leading slash + } + + // Stop and remove old container + if err := do.stopContainer(containerInfo.ID); err != nil { + return fmt.Errorf("failed to stop old container: %w", err) + } + + if err := do.removeContainer(containerInfo.ID); err != nil { + return fmt.Errorf("failed to remove old container: %w", err) + } + + // Create new container + resp, err := do.cli.ContainerCreate( + do.ctx, + config, + hostConfig, + networkConfig, + nil, // platform + containerName, + ) + if err != nil { + return fmt.Errorf("failed to create new container: %w", err) + } + + // Start the new container + if err := do.cli.ContainerStart(do.ctx, resp.ID, container.StartOptions{}); err != nil { + return fmt.Errorf("failed to start new container: %w", err) + } + + fmt.Printf(" ✅ Container recreated with ID: %s\n", resp.ID[:12]) + return nil +} + +// Helper method to create instrumentation labels +func (do *DockerOperations) createInstrumentationLabels(containerInfo container.InspectResponse, originalContainer *discovery.DockerContainer) map[string]string { + // Extract original environment (filter out instrumentation vars) + originalEnv := do.extractOriginalEnvFromCurrent(containerInfo.Config.Env) + + // Serialize original config for restoration + originalConfigBytes, _ := json.Marshal(containerInfo.Config) + + // Determine service name + serviceName := do.deriveServiceName(containerInfo) + + labels := map[string]string{ + LabelInstrumented: "true", + LabelInstrumentedAt: time.Now().Format(time.RFC3339), + LabelAgentPath: do.hostAgentPath, + LabelOriginalConfig: string(originalConfigBytes), + LabelServiceName: serviceName, + LabelOriginalEnv: do.serializeEnv(originalEnv), + } + + // Add compose-specific labels if available + if originalContainer != nil && originalContainer.ComposeFile != "" { + labels[LabelComposeFile] = originalContainer.ComposeFile + labels[LabelComposeService] = originalContainer.ComposeService + } + + return labels +} + +func (do *DockerOperations) deriveServiceName(containerInfo container.InspectResponse) string { + // Try to get from existing labels first + if serviceName := containerInfo.Config.Labels["com.docker.compose.service"]; serviceName != "" { + return serviceName + } + + // Fall back to container name + name := strings.TrimPrefix(containerInfo.Name, "/") + return name +} + +func (do *DockerOperations) extractOriginalEnvFromCurrent(currentEnv []string) map[string]string { + originalEnv := make(map[string]string) + for _, env := range currentEnv { + // Skip instrumentation-specific variables + if strings.HasPrefix(env, "MW_") || + strings.HasPrefix(env, "OTEL_") || + (strings.HasPrefix(env, "JAVA_TOOL_OPTIONS=") && strings.Contains(env, "javaagent")) { + continue + } + + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + originalEnv[parts[0]] = parts[1] + } + } + return originalEnv +} + +func (do *DockerOperations) serializeEnv(env map[string]string) string { + data, _ := json.Marshal(env) + return string(data) } // removeContainerByName removes a container by name @@ -1080,22 +1534,37 @@ func (do *DockerOperations) ListInstrumentedContainers() ([]ContainerState, erro // verifyContainerInstrumentation checks if instrumentation actually worked func (do *DockerOperations) verifyContainerInstrumentation(containerName string) error { - // Check if agent file exists in container - cmd := exec.CommandContext(do.ctx, "docker", "exec", containerName, "test", "-f", DefaultContainerAgentPath) - if err := cmd.Run(); err != nil { - return fmt.Errorf("agent file not found in container") + // Get container info + containerInfo, err := do.cli.ContainerInspect(do.ctx, containerName) + if err != nil { + return fmt.Errorf("failed to inspect container: %w", err) } - // Check if JAVA_TOOL_OPTIONS is set - cmd = exec.CommandContext(do.ctx, "docker", "exec", containerName, "sh", "-c", "echo $JAVA_TOOL_OPTIONS") - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to check JAVA_TOOL_OPTIONS: %w", err) + // Check if JAVA_TOOL_OPTIONS is set correctly in environment + javaOptsFound := false + for _, env := range containerInfo.Config.Env { + if strings.HasPrefix(env, "JAVA_TOOL_OPTIONS=") && strings.Contains(env, "javaagent") { + javaOptsFound = true + break + } } - if !strings.Contains(string(output), "javaagent") { + if !javaOptsFound { return fmt.Errorf("JAVA_TOOL_OPTIONS not set correctly") } + // Verify agent mount exists + agentMountFound := false + for _, mount := range containerInfo.HostConfig.Binds { + if strings.Contains(mount, DefaultContainerAgentPath) { + agentMountFound = true + break + } + } + + if !agentMountFound { + return fmt.Errorf("agent mount not found") + } + return nil } diff --git a/pkg/docker/queries.go b/pkg/docker/queries.go new file mode 100644 index 0000000..3944dd6 --- /dev/null +++ b/pkg/docker/queries.go @@ -0,0 +1,65 @@ +// pkg/docker/queries.go +package docker + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" +) + +type InstrumentationInfo struct { + ContainerID string + ContainerName string + InstrumentedAt string + AgentPath string + ServiceName string + OriginalConfig string + OriginalEnv map[string]string +} + +func (do *DockerOperations) GetInstrumentedContainers() ([]types.Container, error) { + listOptions := container.ListOptions{ + All: true, + Filters: filters.NewArgs( + filters.Arg("label", LabelInstrumented+"=true"), + ), + } + + containers, err := do.cli.ContainerList(do.ctx, listOptions) + if err != nil { + return nil, fmt.Errorf("failed to list instrumented containers: %w", err) + } + + return containers, nil +} + +func (do *DockerOperations) GetContainerInstrumentationInfo(containerID string) (*InstrumentationInfo, error) { + containerInfo, err := do.cli.ContainerInspect(do.ctx, containerID) + if err != nil { + return nil, fmt.Errorf("failed to inspect container: %w", err) + } + + if containerInfo.Config.Labels[LabelInstrumented] != "true" { + return nil, fmt.Errorf("container is not instrumented") + } + + info := &InstrumentationInfo{ + ContainerID: containerInfo.ID, + ContainerName: strings.TrimPrefix(containerInfo.Name, "/"), + InstrumentedAt: containerInfo.Config.Labels[LabelInstrumentedAt], + AgentPath: containerInfo.Config.Labels[LabelAgentPath], + ServiceName: containerInfo.Config.Labels[LabelServiceName], + OriginalConfig: containerInfo.Config.Labels[LabelOriginalConfig], + } + + // Deserialize original environment + if originalEnvStr := containerInfo.Config.Labels[LabelOriginalEnv]; originalEnvStr != "" { + json.Unmarshal([]byte(originalEnvStr), &info.OriginalEnv) + } + + return info, nil +} diff --git a/pkg/docker/validation.go b/pkg/docker/validation.go new file mode 100644 index 0000000..2ea406a --- /dev/null +++ b/pkg/docker/validation.go @@ -0,0 +1,49 @@ +// pkg/docker/validation.go +package docker + +import ( + "strings" +) + +type ValidationResult struct { + ContainerID string + Issues []string + Warnings []string + CanBeRepaired bool +} + +func (do *DockerOperations) ValidateInstrumentedContainer(containerID string) (*ValidationResult, error) { + result := &ValidationResult{ + ContainerID: containerID, + Issues: []string{}, + Warnings: []string{}, + } + + containerInfo, err := do.cli.ContainerInspect(do.ctx, containerID) + if err != nil { + result.Issues = append(result.Issues, "Container not found") + return result, nil + } + + // Validate instrumentation labels + if containerInfo.Config.Labels[LabelInstrumented] != "true" { + result.Issues = append(result.Issues, "Container not marked as instrumented") + return result, nil + } + + // Validate agent mount exists + agentMountExists := false + for _, bind := range containerInfo.HostConfig.Binds { + if strings.Contains(bind, DefaultContainerAgentPath) { + agentMountExists = true + break + } + } + + if !agentMountExists { + result.Issues = append(result.Issues, "Agent volume mount missing") + result.CanBeRepaired = true + } + + return result, nil +}