Signed-off-by: Derek McGowan <derek@mcg.dev>
| 27 | 27 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,34 @@ |
| 0 |
+module github.com/moby/moby/client |
|
| 1 |
+ |
|
| 2 |
+go 1.23.0 |
|
| 3 |
+ |
|
| 4 |
+require ( |
|
| 5 |
+ github.com/Microsoft/go-winio v0.6.2 |
|
| 6 |
+ github.com/containerd/errdefs v1.0.0 |
|
| 7 |
+ github.com/containerd/errdefs/pkg v0.3.0 |
|
| 8 |
+ github.com/distribution/reference v0.6.0 |
|
| 9 |
+ github.com/docker/go-connections v0.5.0 |
|
| 10 |
+ github.com/moby/moby/api v0.0.0 |
|
| 11 |
+ github.com/opencontainers/go-digest v1.0.0 |
|
| 12 |
+ github.com/opencontainers/image-spec v1.1.1 |
|
| 13 |
+ github.com/pkg/errors v0.9.1 |
|
| 14 |
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 |
|
| 15 |
+ go.opentelemetry.io/otel/trace v1.35.0 |
|
| 16 |
+ gotest.tools/v3 v3.5.2 |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+require ( |
|
| 20 |
+ github.com/docker/go-units v0.5.0 // indirect |
|
| 21 |
+ github.com/felixge/httpsnoop v1.0.4 // indirect |
|
| 22 |
+ github.com/go-logr/logr v1.4.2 // indirect |
|
| 23 |
+ github.com/go-logr/stdr v1.2.2 // indirect |
|
| 24 |
+ github.com/gogo/protobuf v1.3.2 // indirect |
|
| 25 |
+ github.com/google/go-cmp v0.7.0 // indirect |
|
| 26 |
+ github.com/moby/docker-image-spec v1.3.1 // indirect |
|
| 27 |
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect |
|
| 28 |
+ go.opentelemetry.io/otel v1.35.0 // indirect |
|
| 29 |
+ go.opentelemetry.io/otel/metric v1.35.0 // indirect |
|
| 30 |
+ golang.org/x/sys v0.33.0 // indirect |
|
| 31 |
+) |
|
| 32 |
+ |
|
| 33 |
+replace github.com/moby/moby/api => ../api |
| 0 | 34 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,88 @@ |
| 0 |
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= |
|
| 1 |
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= |
|
| 2 |
+github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= |
|
| 3 |
+github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= |
|
| 4 |
+github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= |
|
| 5 |
+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= |
|
| 6 |
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
|
| 7 |
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|
| 8 |
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= |
|
| 9 |
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= |
|
| 10 |
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= |
|
| 11 |
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= |
|
| 12 |
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= |
|
| 13 |
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= |
|
| 14 |
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= |
|
| 15 |
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= |
|
| 16 |
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= |
|
| 17 |
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= |
|
| 18 |
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= |
|
| 19 |
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= |
|
| 20 |
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= |
|
| 21 |
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= |
|
| 22 |
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= |
|
| 23 |
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= |
|
| 24 |
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= |
|
| 25 |
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= |
|
| 26 |
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= |
|
| 27 |
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= |
|
| 28 |
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= |
|
| 29 |
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= |
|
| 30 |
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= |
|
| 31 |
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= |
|
| 32 |
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= |
|
| 33 |
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= |
|
| 34 |
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= |
|
| 35 |
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= |
|
| 36 |
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
|
| 37 |
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
|
| 38 |
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
|
| 39 |
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= |
|
| 40 |
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= |
|
| 41 |
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= |
|
| 42 |
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= |
|
| 43 |
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= |
|
| 44 |
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= |
|
| 45 |
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= |
|
| 46 |
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= |
|
| 47 |
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= |
|
| 48 |
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= |
|
| 49 |
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= |
|
| 50 |
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= |
|
| 51 |
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= |
|
| 52 |
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= |
|
| 53 |
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= |
|
| 54 |
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= |
|
| 55 |
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= |
|
| 56 |
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= |
|
| 57 |
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
|
| 58 |
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
|
| 59 |
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
|
| 60 |
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= |
|
| 61 |
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= |
|
| 62 |
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
|
| 63 |
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
|
| 64 |
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
|
| 65 |
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= |
|
| 66 |
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|
| 67 |
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|
| 68 |
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|
| 69 |
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|
| 70 |
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|
| 71 |
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|
| 72 |
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= |
|
| 73 |
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= |
|
| 74 |
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
|
| 75 |
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
|
| 76 |
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
|
| 77 |
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
|
| 78 |
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= |
|
| 79 |
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= |
|
| 80 |
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|
| 81 |
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|
| 82 |
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|
| 83 |
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|
| 84 |
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
|
| 85 |
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
|
| 86 |
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= |
|
| 87 |
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= |
| ... | ... |
@@ -10,7 +10,6 @@ import ( |
| 10 | 10 |
"testing" |
| 11 | 11 |
"time" |
| 12 | 12 |
|
| 13 |
- "github.com/docker/docker/client" |
|
| 14 | 13 |
"github.com/docker/docker/integration-cli/cli" |
| 15 | 14 |
"github.com/docker/docker/pkg/stdcopy" |
| 16 | 15 |
"github.com/docker/docker/testutil" |
| ... | ... |
@@ -18,6 +17,7 @@ import ( |
| 18 | 18 |
"github.com/docker/go-connections/sockets" |
| 19 | 19 |
"github.com/moby/moby/api/types" |
| 20 | 20 |
"github.com/moby/moby/api/types/container" |
| 21 |
+ "github.com/moby/moby/client" |
|
| 21 | 22 |
"github.com/pkg/errors" |
| 22 | 23 |
"golang.org/x/net/websocket" |
| 23 | 24 |
"gotest.tools/v3/assert" |
| ... | ... |
@@ -18,7 +18,6 @@ import ( |
| 18 | 18 |
"time" |
| 19 | 19 |
|
| 20 | 20 |
cerrdefs "github.com/containerd/errdefs" |
| 21 |
- "github.com/docker/docker/client" |
|
| 22 | 21 |
dconfig "github.com/docker/docker/daemon/config" |
| 23 | 22 |
"github.com/docker/docker/daemon/volume" |
| 24 | 23 |
"github.com/docker/docker/integration-cli/cli" |
| ... | ... |
@@ -30,6 +29,7 @@ import ( |
| 30 | 30 |
"github.com/moby/moby/api/types/container" |
| 31 | 31 |
"github.com/moby/moby/api/types/mount" |
| 32 | 32 |
"github.com/moby/moby/api/types/network" |
| 33 |
+ "github.com/moby/moby/client" |
|
| 33 | 34 |
"gotest.tools/v3/assert" |
| 34 | 35 |
is "gotest.tools/v3/assert/cmp" |
| 35 | 36 |
"gotest.tools/v3/poll" |
| ... | ... |
@@ -12,12 +12,12 @@ import ( |
| 12 | 12 |
"testing" |
| 13 | 13 |
"time" |
| 14 | 14 |
|
| 15 |
- "github.com/docker/docker/client" |
|
| 16 | 15 |
"github.com/docker/docker/integration-cli/checker" |
| 17 | 16 |
"github.com/docker/docker/integration-cli/cli" |
| 18 | 17 |
"github.com/docker/docker/testutil" |
| 19 | 18 |
"github.com/docker/docker/testutil/request" |
| 20 | 19 |
"github.com/moby/moby/api/types/container" |
| 20 |
+ "github.com/moby/moby/client" |
|
| 21 | 21 |
"gotest.tools/v3/assert" |
| 22 | 22 |
is "gotest.tools/v3/assert/cmp" |
| 23 | 23 |
"gotest.tools/v3/poll" |
| ... | ... |
@@ -6,12 +6,12 @@ import ( |
| 6 | 6 |
"strings" |
| 7 | 7 |
"testing" |
| 8 | 8 |
|
| 9 |
- "github.com/docker/docker/client" |
|
| 10 | 9 |
"github.com/docker/docker/integration-cli/cli" |
| 11 | 10 |
"github.com/docker/docker/integration-cli/cli/build" |
| 12 | 11 |
"github.com/docker/docker/testutil" |
| 13 | 12 |
"github.com/docker/docker/testutil/request" |
| 14 | 13 |
"github.com/moby/moby/api/types/image" |
| 14 |
+ "github.com/moby/moby/client" |
|
| 15 | 15 |
"gotest.tools/v3/assert" |
| 16 | 16 |
) |
| 17 | 17 |
|
| ... | ... |
@@ -5,10 +5,10 @@ import ( |
| 5 | 5 |
"strings" |
| 6 | 6 |
"testing" |
| 7 | 7 |
|
| 8 |
- "github.com/docker/docker/client" |
|
| 9 | 8 |
"github.com/docker/docker/integration-cli/cli" |
| 10 | 9 |
"github.com/docker/docker/testutil" |
| 11 | 10 |
"github.com/moby/moby/api/types/container" |
| 11 |
+ "github.com/moby/moby/client" |
|
| 12 | 12 |
"gotest.tools/v3/assert" |
| 13 | 13 |
is "gotest.tools/v3/assert/cmp" |
| 14 | 14 |
) |
| ... | ... |
@@ -11,12 +11,12 @@ import ( |
| 11 | 11 |
"testing" |
| 12 | 12 |
"time" |
| 13 | 13 |
|
| 14 |
- "github.com/docker/docker/client" |
|
| 15 | 14 |
"github.com/docker/docker/integration-cli/cli" |
| 16 | 15 |
"github.com/docker/docker/pkg/stdcopy" |
| 17 | 16 |
"github.com/docker/docker/testutil" |
| 18 | 17 |
"github.com/docker/docker/testutil/request" |
| 19 | 18 |
"github.com/moby/moby/api/types/container" |
| 19 |
+ "github.com/moby/moby/client" |
|
| 20 | 20 |
"gotest.tools/v3/assert" |
| 21 | 21 |
) |
| 22 | 22 |
|
| ... | ... |
@@ -11,12 +11,12 @@ import ( |
| 11 | 11 |
"testing" |
| 12 | 12 |
"time" |
| 13 | 13 |
|
| 14 |
- "github.com/docker/docker/client" |
|
| 15 | 14 |
"github.com/docker/docker/integration-cli/cli" |
| 16 | 15 |
"github.com/docker/docker/testutil" |
| 17 | 16 |
"github.com/docker/docker/testutil/request" |
| 18 | 17 |
"github.com/moby/moby/api/types/container" |
| 19 | 18 |
"github.com/moby/moby/api/types/system" |
| 19 |
+ "github.com/moby/moby/client" |
|
| 20 | 20 |
"gotest.tools/v3/assert" |
| 21 | 21 |
"gotest.tools/v3/skip" |
| 22 | 22 |
) |
| ... | ... |
@@ -13,13 +13,13 @@ import ( |
| 13 | 13 |
"testing" |
| 14 | 14 |
"time" |
| 15 | 15 |
|
| 16 |
- "github.com/docker/docker/client" |
|
| 17 | 16 |
eventstestutils "github.com/docker/docker/daemon/events/testutils" |
| 18 | 17 |
"github.com/docker/docker/integration-cli/cli" |
| 19 | 18 |
"github.com/docker/docker/integration-cli/cli/build" |
| 20 | 19 |
"github.com/docker/docker/testutil" |
| 21 | 20 |
"github.com/moby/moby/api/types/container" |
| 22 | 21 |
eventtypes "github.com/moby/moby/api/types/events" |
| 22 |
+ "github.com/moby/moby/client" |
|
| 23 | 23 |
"gotest.tools/v3/assert" |
| 24 | 24 |
is "gotest.tools/v3/assert/cmp" |
| 25 | 25 |
"gotest.tools/v3/icmd" |
| ... | ... |
@@ -15,10 +15,10 @@ import ( |
| 15 | 15 |
"testing" |
| 16 | 16 |
"time" |
| 17 | 17 |
|
| 18 |
- "github.com/docker/docker/client" |
|
| 19 | 18 |
"github.com/docker/docker/integration-cli/cli" |
| 20 | 19 |
"github.com/docker/docker/integration-cli/cli/build" |
| 21 | 20 |
"github.com/docker/docker/testutil" |
| 21 |
+ "github.com/moby/moby/client" |
|
| 22 | 22 |
"gotest.tools/v3/assert" |
| 23 | 23 |
is "gotest.tools/v3/assert/cmp" |
| 24 | 24 |
"gotest.tools/v3/icmd" |
| ... | ... |
@@ -5,9 +5,9 @@ package main |
| 5 | 5 |
import ( |
| 6 | 6 |
"testing" |
| 7 | 7 |
|
| 8 |
- "github.com/docker/docker/client" |
|
| 9 | 8 |
"github.com/docker/docker/daemon/config" |
| 10 | 9 |
"github.com/docker/docker/testutil" |
| 10 |
+ "github.com/moby/moby/client" |
|
| 11 | 11 |
"gotest.tools/v3/assert" |
| 12 | 12 |
is "gotest.tools/v3/assert/cmp" |
| 13 | 13 |
) |
| ... | ... |
@@ -5,9 +5,9 @@ import ( |
| 5 | 5 |
"strings" |
| 6 | 6 |
"testing" |
| 7 | 7 |
|
| 8 |
- "github.com/docker/docker/client" |
|
| 9 | 8 |
"github.com/docker/docker/integration-cli/cli" |
| 10 | 9 |
"github.com/docker/docker/testutil" |
| 10 |
+ "github.com/moby/moby/client" |
|
| 11 | 11 |
"gotest.tools/v3/assert" |
| 12 | 12 |
is "gotest.tools/v3/assert/cmp" |
| 13 | 13 |
) |
| ... | ... |
@@ -23,7 +23,6 @@ import ( |
| 23 | 23 |
"testing" |
| 24 | 24 |
"time" |
| 25 | 25 |
|
| 26 |
- "github.com/docker/docker/client" |
|
| 27 | 26 |
"github.com/docker/docker/daemon/libnetwork/resolvconf" |
| 28 | 27 |
"github.com/docker/docker/integration-cli/cli" |
| 29 | 28 |
"github.com/docker/docker/integration-cli/cli/build" |
| ... | ... |
@@ -34,6 +33,7 @@ import ( |
| 34 | 34 |
testdaemon "github.com/docker/docker/testutil/daemon" |
| 35 | 35 |
"github.com/docker/docker/testutil/fakecontext" |
| 36 | 36 |
"github.com/docker/go-connections/nat" |
| 37 |
+ "github.com/moby/moby/client" |
|
| 37 | 38 |
"github.com/moby/sys/mountinfo" |
| 38 | 39 |
"gotest.tools/v3/assert" |
| 39 | 40 |
is "gotest.tools/v3/assert/cmp" |
| ... | ... |
@@ -17,11 +17,11 @@ import ( |
| 17 | 17 |
"time" |
| 18 | 18 |
|
| 19 | 19 |
"github.com/creack/pty" |
| 20 |
- "github.com/docker/docker/client" |
|
| 21 | 20 |
"github.com/docker/docker/integration-cli/cli" |
| 22 | 21 |
"github.com/docker/docker/integration-cli/cli/build" |
| 23 | 22 |
"github.com/docker/docker/pkg/sysinfo" |
| 24 | 23 |
"github.com/docker/docker/testutil" |
| 24 |
+ "github.com/moby/moby/client" |
|
| 25 | 25 |
"github.com/moby/sys/mount" |
| 26 | 26 |
"gotest.tools/v3/assert" |
| 27 | 27 |
is "gotest.tools/v3/assert/cmp" |
| ... | ... |
@@ -12,11 +12,11 @@ import ( |
| 12 | 12 |
"time" |
| 13 | 13 |
|
| 14 | 14 |
"github.com/creack/pty" |
| 15 |
- "github.com/docker/docker/client" |
|
| 16 | 15 |
"github.com/docker/docker/integration-cli/cli" |
| 17 | 16 |
"github.com/docker/docker/testutil" |
| 18 | 17 |
"github.com/docker/docker/testutil/request" |
| 19 | 18 |
"github.com/moby/moby/api/types/container" |
| 19 |
+ "github.com/moby/moby/client" |
|
| 20 | 20 |
"gotest.tools/v3/assert" |
| 21 | 21 |
is "gotest.tools/v3/assert/cmp" |
| 22 | 22 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -9,13 +9,13 @@ import ( |
| 9 | 9 |
"strings" |
| 10 | 10 |
"testing" |
| 11 | 11 |
|
| 12 |
- "github.com/docker/docker/client" |
|
| 13 | 12 |
"github.com/docker/docker/integration-cli/cli" |
| 14 | 13 |
"github.com/docker/docker/integration-cli/cli/build" |
| 15 | 14 |
"github.com/docker/docker/testutil" |
| 16 | 15 |
"github.com/moby/moby/api/types/container" |
| 17 | 16 |
"github.com/moby/moby/api/types/mount" |
| 18 | 17 |
"github.com/moby/moby/api/types/network" |
| 18 |
+ "github.com/moby/moby/client" |
|
| 19 | 19 |
"gotest.tools/v3/assert" |
| 20 | 20 |
is "gotest.tools/v3/assert/cmp" |
| 21 | 21 |
"gotest.tools/v3/icmd" |
| ... | ... |
@@ -14,7 +14,6 @@ import ( |
| 14 | 14 |
"testing" |
| 15 | 15 |
"time" |
| 16 | 16 |
|
| 17 |
- "github.com/docker/docker/client" |
|
| 18 | 17 |
"github.com/docker/docker/integration-cli/cli" |
| 19 | 18 |
"github.com/docker/docker/integration-cli/daemon" |
| 20 | 19 |
"github.com/docker/docker/internal/testutils/specialimage" |
| ... | ... |
@@ -22,6 +21,7 @@ import ( |
| 22 | 22 |
"github.com/moby/go-archive" |
| 23 | 23 |
"github.com/moby/moby/api/types" |
| 24 | 24 |
"github.com/moby/moby/api/types/container" |
| 25 |
+ "github.com/moby/moby/client" |
|
| 25 | 26 |
"gotest.tools/v3/assert" |
| 26 | 27 |
is "gotest.tools/v3/assert/cmp" |
| 27 | 28 |
"gotest.tools/v3/icmd" |
| ... | ... |
@@ -11,12 +11,12 @@ import ( |
| 11 | 11 |
"time" |
| 12 | 12 |
|
| 13 | 13 |
"github.com/containerd/containerd/v2/plugins" |
| 14 |
- "github.com/docker/docker/client" |
|
| 15 | 14 |
"github.com/docker/docker/integration-cli/cli" |
| 16 | 15 |
"github.com/docker/docker/integration-cli/requirement" |
| 17 | 16 |
"github.com/docker/docker/testutil/registry" |
| 18 | 17 |
"github.com/moby/moby/api/types/network" |
| 19 | 18 |
"github.com/moby/moby/api/types/swarm" |
| 19 |
+ "github.com/moby/moby/client" |
|
| 20 | 20 |
) |
| 21 | 21 |
|
| 22 | 22 |
func DaemonIsWindows() bool {
|
| ... | ... |
@@ -6,7 +6,6 @@ import ( |
| 6 | 6 |
"strings" |
| 7 | 7 |
"testing" |
| 8 | 8 |
|
| 9 |
- dclient "github.com/docker/docker/client" |
|
| 10 | 9 |
"github.com/docker/docker/integration/internal/container" |
| 11 | 10 |
"github.com/docker/docker/pkg/stdcopy" |
| 12 | 11 |
"github.com/docker/docker/testutil" |
| ... | ... |
@@ -14,6 +13,7 @@ import ( |
| 14 | 14 |
"github.com/docker/docker/testutil/fakecontext" |
| 15 | 15 |
"github.com/moby/moby/api/types/build" |
| 16 | 16 |
containertypes "github.com/moby/moby/api/types/container" |
| 17 |
+ dclient "github.com/moby/moby/client" |
|
| 17 | 18 |
"gotest.tools/v3/assert" |
| 18 | 19 |
is "gotest.tools/v3/assert/cmp" |
| 19 | 20 |
"gotest.tools/v3/poll" |
| ... | ... |
@@ -9,13 +9,13 @@ import ( |
| 9 | 9 |
"time" |
| 10 | 10 |
|
| 11 | 11 |
cerrdefs "github.com/containerd/errdefs" |
| 12 |
- "github.com/docker/docker/client" |
|
| 13 | 12 |
"github.com/docker/docker/integration/internal/swarm" |
| 14 | 13 |
"github.com/docker/docker/pkg/stdcopy" |
| 15 | 14 |
"github.com/docker/docker/testutil" |
| 16 | 15 |
"github.com/moby/moby/api/types/container" |
| 17 | 16 |
"github.com/moby/moby/api/types/filters" |
| 18 | 17 |
swarmtypes "github.com/moby/moby/api/types/swarm" |
| 18 |
+ "github.com/moby/moby/client" |
|
| 19 | 19 |
"gotest.tools/v3/assert" |
| 20 | 20 |
is "gotest.tools/v3/assert/cmp" |
| 21 | 21 |
"gotest.tools/v3/poll" |
| ... | ... |
@@ -7,12 +7,12 @@ import ( |
| 7 | 7 |
"sort" |
| 8 | 8 |
"testing" |
| 9 | 9 |
|
| 10 |
- "github.com/docker/docker/client" |
|
| 11 | 10 |
"github.com/docker/docker/integration/internal/container" |
| 12 | 11 |
"github.com/docker/docker/testutil/request" |
| 13 | 12 |
"github.com/moby/moby/api/types/checkpoint" |
| 14 | 13 |
containertypes "github.com/moby/moby/api/types/container" |
| 15 | 14 |
mounttypes "github.com/moby/moby/api/types/mount" |
| 15 |
+ "github.com/moby/moby/client" |
|
| 16 | 16 |
"gotest.tools/v3/assert" |
| 17 | 17 |
is "gotest.tools/v3/assert/cmp" |
| 18 | 18 |
"gotest.tools/v3/poll" |
| ... | ... |
@@ -11,7 +11,6 @@ import ( |
| 11 | 11 |
|
| 12 | 12 |
containerd "github.com/containerd/containerd/v2/client" |
| 13 | 13 |
cerrdefs "github.com/containerd/errdefs" |
| 14 |
- "github.com/docker/docker/client" |
|
| 15 | 14 |
testContainer "github.com/docker/docker/integration/internal/container" |
| 16 | 15 |
net "github.com/docker/docker/integration/internal/network" |
| 17 | 16 |
"github.com/docker/docker/oci" |
| ... | ... |
@@ -20,6 +19,7 @@ import ( |
| 20 | 20 |
"github.com/moby/moby/api/types/container" |
| 21 | 21 |
"github.com/moby/moby/api/types/network" |
| 22 | 22 |
"github.com/moby/moby/api/types/versions" |
| 23 |
+ "github.com/moby/moby/client" |
|
| 23 | 24 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 24 | 25 |
"gotest.tools/v3/assert" |
| 25 | 26 |
is "gotest.tools/v3/assert/cmp" |
| ... | ... |
@@ -6,9 +6,9 @@ import ( |
| 6 | 6 |
"testing" |
| 7 | 7 |
"time" |
| 8 | 8 |
|
| 9 |
- "github.com/docker/docker/client" |
|
| 10 | 9 |
"github.com/docker/docker/integration/internal/container" |
| 11 | 10 |
containertypes "github.com/moby/moby/api/types/container" |
| 11 |
+ "github.com/moby/moby/client" |
|
| 12 | 12 |
"gotest.tools/v3/assert" |
| 13 | 13 |
"gotest.tools/v3/poll" |
| 14 | 14 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -7,10 +7,10 @@ import ( |
| 7 | 7 |
"testing" |
| 8 | 8 |
|
| 9 | 9 |
"github.com/containerd/platforms" |
| 10 |
- "github.com/docker/docker/client" |
|
| 11 | 10 |
"github.com/docker/docker/integration/internal/container" |
| 12 | 11 |
"github.com/docker/docker/testutil/request" |
| 13 | 12 |
containertypes "github.com/moby/moby/api/types/container" |
| 13 |
+ "github.com/moby/moby/client" |
|
| 14 | 14 |
"gotest.tools/v3/assert" |
| 15 | 15 |
is "gotest.tools/v3/assert/cmp" |
| 16 | 16 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -7,13 +7,13 @@ import ( |
| 7 | 7 |
"strings" |
| 8 | 8 |
"testing" |
| 9 | 9 |
|
| 10 |
- "github.com/docker/docker/client" |
|
| 11 | 10 |
"github.com/docker/docker/integration/internal/container" |
| 12 | 11 |
"github.com/docker/docker/testutil" |
| 13 | 12 |
"github.com/docker/docker/testutil/daemon" |
| 14 | 13 |
"github.com/docker/docker/testutil/request" |
| 15 | 14 |
containertypes "github.com/moby/moby/api/types/container" |
| 16 | 15 |
"github.com/moby/moby/api/types/versions" |
| 16 |
+ "github.com/moby/moby/client" |
|
| 17 | 17 |
"gotest.tools/v3/assert" |
| 18 | 18 |
is "gotest.tools/v3/assert/cmp" |
| 19 | 19 |
"gotest.tools/v3/fs" |
| ... | ... |
@@ -4,12 +4,12 @@ import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
"testing" |
| 6 | 6 |
|
| 7 |
- "github.com/docker/docker/client" |
|
| 8 | 7 |
"github.com/docker/docker/integration/internal/container" |
| 9 | 8 |
"github.com/docker/docker/testutil" |
| 10 | 9 |
"github.com/docker/docker/testutil/request" |
| 11 | 10 |
containertypes "github.com/moby/moby/api/types/container" |
| 12 | 11 |
"github.com/moby/moby/api/types/filters" |
| 12 |
+ "github.com/moby/moby/client" |
|
| 13 | 13 |
"gotest.tools/v3/assert" |
| 14 | 14 |
is "gotest.tools/v3/assert/cmp" |
| 15 | 15 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -8,7 +8,6 @@ import ( |
| 8 | 8 |
"testing" |
| 9 | 9 |
|
| 10 | 10 |
cerrdefs "github.com/containerd/errdefs" |
| 11 |
- "github.com/docker/docker/client" |
|
| 12 | 11 |
"github.com/docker/docker/daemon/volume" |
| 13 | 12 |
"github.com/docker/docker/integration/internal/container" |
| 14 | 13 |
"github.com/docker/docker/pkg/parsers/kernel" |
| ... | ... |
@@ -18,6 +17,7 @@ import ( |
| 18 | 18 |
mounttypes "github.com/moby/moby/api/types/mount" |
| 19 | 19 |
"github.com/moby/moby/api/types/network" |
| 20 | 20 |
"github.com/moby/moby/api/types/versions" |
| 21 |
+ "github.com/moby/moby/client" |
|
| 21 | 22 |
"github.com/moby/sys/mount" |
| 22 | 23 |
"github.com/moby/sys/mountinfo" |
| 23 | 24 |
"gotest.tools/v3/assert" |
| ... | ... |
@@ -7,13 +7,13 @@ import ( |
| 7 | 7 |
"testing" |
| 8 | 8 |
"time" |
| 9 | 9 |
|
| 10 |
- "github.com/docker/docker/client" |
|
| 11 | 10 |
testContainer "github.com/docker/docker/integration/internal/container" |
| 12 | 11 |
"github.com/docker/docker/testutil" |
| 13 | 12 |
"github.com/docker/docker/testutil/daemon" |
| 14 | 13 |
"github.com/moby/moby/api/types/container" |
| 15 | 14 |
"github.com/moby/moby/api/types/events" |
| 16 | 15 |
"github.com/moby/moby/api/types/filters" |
| 16 |
+ "github.com/moby/moby/client" |
|
| 17 | 17 |
"gotest.tools/v3/assert" |
| 18 | 18 |
is "gotest.tools/v3/assert/cmp" |
| 19 | 19 |
"gotest.tools/v3/poll" |
| ... | ... |
@@ -4,11 +4,11 @@ import ( |
| 4 | 4 |
"context" |
| 5 | 5 |
"testing" |
| 6 | 6 |
|
| 7 |
- "github.com/docker/docker/client" |
|
| 8 | 7 |
"github.com/docker/docker/integration/internal/container" |
| 9 | 8 |
"github.com/docker/docker/integration/internal/requirement" |
| 10 | 9 |
"github.com/docker/docker/testutil" |
| 11 | 10 |
"github.com/docker/docker/testutil/daemon" |
| 11 |
+ "github.com/moby/moby/client" |
|
| 12 | 12 |
"gotest.tools/v3/assert" |
| 13 | 13 |
"gotest.tools/v3/skip" |
| 14 | 14 |
) |
| ... | ... |
@@ -9,7 +9,6 @@ import ( |
| 9 | 9 |
"strings" |
| 10 | 10 |
"testing" |
| 11 | 11 |
|
| 12 |
- "github.com/docker/docker/client" |
|
| 13 | 12 |
"github.com/docker/docker/integration/internal/container" |
| 14 | 13 |
net "github.com/docker/docker/integration/internal/network" |
| 15 | 14 |
"github.com/docker/docker/pkg/stdcopy" |
| ... | ... |
@@ -17,6 +16,7 @@ import ( |
| 17 | 17 |
"github.com/docker/docker/testutil/daemon" |
| 18 | 18 |
containertypes "github.com/moby/moby/api/types/container" |
| 19 | 19 |
"github.com/moby/moby/api/types/versions" |
| 20 |
+ "github.com/moby/moby/client" |
|
| 20 | 21 |
"golang.org/x/sys/unix" |
| 21 | 22 |
"gotest.tools/v3/assert" |
| 22 | 23 |
is "gotest.tools/v3/assert/cmp" |
| ... | ... |
@@ -9,10 +9,10 @@ import ( |
| 9 | 9 |
"time" |
| 10 | 10 |
|
| 11 | 11 |
cerrdefs "github.com/containerd/errdefs" |
| 12 |
- "github.com/docker/docker/client" |
|
| 13 | 12 |
"github.com/docker/docker/integration/internal/container" |
| 14 | 13 |
"github.com/docker/docker/pkg/stdcopy" |
| 15 | 14 |
containertypes "github.com/moby/moby/api/types/container" |
| 15 |
+ "github.com/moby/moby/client" |
|
| 16 | 16 |
"gotest.tools/v3/assert" |
| 17 | 17 |
is "gotest.tools/v3/assert/cmp" |
| 18 | 18 |
"gotest.tools/v3/poll" |
| ... | ... |
@@ -7,11 +7,11 @@ import ( |
| 7 | 7 |
"testing" |
| 8 | 8 |
"time" |
| 9 | 9 |
|
| 10 |
- "github.com/docker/docker/client" |
|
| 11 | 10 |
"github.com/docker/docker/integration/internal/container" |
| 12 | 11 |
"github.com/docker/docker/testutil" |
| 13 | 12 |
"github.com/docker/docker/testutil/request" |
| 14 | 13 |
containertypes "github.com/moby/moby/api/types/container" |
| 14 |
+ "github.com/moby/moby/client" |
|
| 15 | 15 |
"gotest.tools/v3/assert" |
| 16 | 16 |
is "gotest.tools/v3/assert/cmp" |
| 17 | 17 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -5,10 +5,10 @@ import ( |
| 5 | 5 |
"encoding/json" |
| 6 | 6 |
"testing" |
| 7 | 7 |
|
| 8 |
- "github.com/docker/docker/client" |
|
| 9 | 8 |
iimage "github.com/docker/docker/integration/internal/image" |
| 10 | 9 |
"github.com/docker/docker/internal/testutils/specialimage" |
| 11 | 10 |
"github.com/moby/moby/api/types/image" |
| 11 |
+ "github.com/moby/moby/client" |
|
| 12 | 12 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 13 | 13 |
"gotest.tools/v3/assert" |
| 14 | 14 |
is "gotest.tools/v3/assert/cmp" |
| ... | ... |
@@ -7,7 +7,6 @@ import ( |
| 7 | 7 |
"testing" |
| 8 | 8 |
"time" |
| 9 | 9 |
|
| 10 |
- "github.com/docker/docker/client" |
|
| 11 | 10 |
"github.com/docker/docker/integration/internal/container" |
| 12 | 11 |
iimage "github.com/docker/docker/integration/internal/image" |
| 13 | 12 |
"github.com/docker/docker/internal/testutils/specialimage" |
| ... | ... |
@@ -19,6 +18,7 @@ import ( |
| 19 | 19 |
"github.com/moby/moby/api/types/filters" |
| 20 | 20 |
"github.com/moby/moby/api/types/image" |
| 21 | 21 |
"github.com/moby/moby/api/types/versions" |
| 22 |
+ "github.com/moby/moby/client" |
|
| 22 | 23 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 23 | 24 |
"gotest.tools/v3/assert" |
| 24 | 25 |
is "gotest.tools/v3/assert/cmp" |
| ... | ... |
@@ -4,7 +4,6 @@ import ( |
| 4 | 4 |
"strings" |
| 5 | 5 |
"testing" |
| 6 | 6 |
|
| 7 |
- "github.com/docker/docker/client" |
|
| 8 | 7 |
"github.com/docker/docker/integration/internal/container" |
| 9 | 8 |
iimage "github.com/docker/docker/integration/internal/image" |
| 10 | 9 |
"github.com/docker/docker/internal/testutils/specialimage" |
| ... | ... |
@@ -13,6 +12,7 @@ import ( |
| 13 | 13 |
containertypes "github.com/moby/moby/api/types/container" |
| 14 | 14 |
"github.com/moby/moby/api/types/filters" |
| 15 | 15 |
"github.com/moby/moby/api/types/image" |
| 16 |
+ "github.com/moby/moby/client" |
|
| 16 | 17 |
"gotest.tools/v3/assert" |
| 17 | 18 |
is "gotest.tools/v3/assert/cmp" |
| 18 | 19 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -15,7 +15,6 @@ import ( |
| 15 | 15 |
|
| 16 | 16 |
cerrdefs "github.com/containerd/errdefs" |
| 17 | 17 |
"github.com/cpuguy83/tar2go" |
| 18 |
- "github.com/docker/docker/client" |
|
| 19 | 18 |
"github.com/docker/docker/integration/internal/build" |
| 20 | 19 |
"github.com/docker/docker/integration/internal/container" |
| 21 | 20 |
iimage "github.com/docker/docker/integration/internal/image" |
| ... | ... |
@@ -25,6 +24,7 @@ import ( |
| 25 | 25 |
"github.com/moby/go-archive/compression" |
| 26 | 26 |
containertypes "github.com/moby/moby/api/types/container" |
| 27 | 27 |
"github.com/moby/moby/api/types/versions" |
| 28 |
+ "github.com/moby/moby/client" |
|
| 28 | 29 |
"github.com/opencontainers/go-digest" |
| 29 | 30 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 30 | 31 |
"gotest.tools/v3/assert" |
| ... | ... |
@@ -6,11 +6,11 @@ import ( |
| 6 | 6 |
"io" |
| 7 | 7 |
"testing" |
| 8 | 8 |
|
| 9 |
- "github.com/docker/docker/client" |
|
| 10 | 9 |
"github.com/docker/docker/pkg/jsonmessage" |
| 11 | 10 |
"github.com/docker/docker/testutil/fakecontext" |
| 12 | 11 |
"github.com/moby/moby/api/types/build" |
| 13 | 12 |
"github.com/moby/moby/api/types/image" |
| 13 |
+ "github.com/moby/moby/client" |
|
| 14 | 14 |
"gotest.tools/v3/assert" |
| 15 | 15 |
) |
| 16 | 16 |
|
| ... | ... |
@@ -8,11 +8,11 @@ import ( |
| 8 | 8 |
"sync" |
| 9 | 9 |
"testing" |
| 10 | 10 |
|
| 11 |
- "github.com/docker/docker/client" |
|
| 12 | 11 |
"github.com/docker/docker/pkg/stdcopy" |
| 13 | 12 |
"github.com/moby/moby/api/types" |
| 14 | 13 |
"github.com/moby/moby/api/types/container" |
| 15 | 14 |
"github.com/moby/moby/api/types/network" |
| 15 |
+ "github.com/moby/moby/client" |
|
| 16 | 16 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 17 | 17 |
"gotest.tools/v3/assert" |
| 18 | 18 |
) |
| ... | ... |
@@ -9,10 +9,10 @@ import ( |
| 9 | 9 |
"strings" |
| 10 | 10 |
"testing" |
| 11 | 11 |
|
| 12 |
- "github.com/docker/docker/client" |
|
| 13 | 12 |
"github.com/docker/docker/internal/testutils/specialimage" |
| 14 | 13 |
"github.com/docker/docker/pkg/jsonmessage" |
| 15 | 14 |
"github.com/moby/go-archive" |
| 15 |
+ "github.com/moby/moby/client" |
|
| 16 | 16 |
"gotest.tools/v3/assert" |
| 17 | 17 |
) |
| 18 | 18 |
|
| ... | ... |
@@ -6,13 +6,13 @@ import ( |
| 6 | 6 |
"testing" |
| 7 | 7 |
"time" |
| 8 | 8 |
|
| 9 |
- "github.com/docker/docker/client" |
|
| 10 | 9 |
"github.com/docker/docker/testutil/daemon" |
| 11 | 10 |
"github.com/docker/docker/testutil/environment" |
| 12 | 11 |
"github.com/moby/moby/api/types" |
| 13 | 12 |
"github.com/moby/moby/api/types/container" |
| 14 | 13 |
"github.com/moby/moby/api/types/filters" |
| 15 | 14 |
swarmtypes "github.com/moby/moby/api/types/swarm" |
| 15 |
+ "github.com/moby/moby/client" |
|
| 16 | 16 |
"gotest.tools/v3/assert" |
| 17 | 17 |
"gotest.tools/v3/poll" |
| 18 | 18 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -30,7 +30,6 @@ import ( |
| 30 | 30 |
"text/template" |
| 31 | 31 |
"time" |
| 32 | 32 |
|
| 33 |
- "github.com/docker/docker/client" |
|
| 34 | 33 |
"github.com/docker/docker/daemon/libnetwork/drivers/bridge" |
| 35 | 34 |
"github.com/docker/docker/integration/internal/container" |
| 36 | 35 |
"github.com/docker/docker/integration/internal/network" |
| ... | ... |
@@ -41,6 +40,7 @@ import ( |
| 41 | 41 |
containertypes "github.com/moby/moby/api/types/container" |
| 42 | 42 |
networktypes "github.com/moby/moby/api/types/network" |
| 43 | 43 |
swarmtypes "github.com/moby/moby/api/types/swarm" |
| 44 |
+ "github.com/moby/moby/client" |
|
| 44 | 45 |
"github.com/vishvananda/netlink" |
| 45 | 46 |
"gotest.tools/v3/assert" |
| 46 | 47 |
"gotest.tools/v3/golden" |
| ... | ... |
@@ -4,9 +4,9 @@ import ( |
| 4 | 4 |
"context" |
| 5 | 5 |
"testing" |
| 6 | 6 |
|
| 7 |
- dclient "github.com/docker/docker/client" |
|
| 8 | 7 |
"github.com/docker/docker/integration/internal/network" |
| 9 | 8 |
networktypes "github.com/moby/moby/api/types/network" |
| 9 |
+ dclient "github.com/moby/moby/client" |
|
| 10 | 10 |
"gotest.tools/v3/assert" |
| 11 | 11 |
is "gotest.tools/v3/assert/cmp" |
| 12 | 12 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -9,7 +9,6 @@ import ( |
| 9 | 9 |
"testing" |
| 10 | 10 |
"time" |
| 11 | 11 |
|
| 12 |
- dclient "github.com/docker/docker/client" |
|
| 13 | 12 |
"github.com/docker/docker/daemon/libnetwork/netlabel" |
| 14 | 13 |
"github.com/docker/docker/integration/internal/container" |
| 15 | 14 |
net "github.com/docker/docker/integration/internal/network" |
| ... | ... |
@@ -18,6 +17,7 @@ import ( |
| 18 | 18 |
"github.com/docker/docker/testutil/daemon" |
| 19 | 19 |
containertypes "github.com/moby/moby/api/types/container" |
| 20 | 20 |
"github.com/moby/moby/api/types/network" |
| 21 |
+ dclient "github.com/moby/moby/client" |
|
| 21 | 22 |
"gotest.tools/v3/assert" |
| 22 | 23 |
is "gotest.tools/v3/assert/cmp" |
| 23 | 24 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -8,7 +8,6 @@ import ( |
| 8 | 8 |
"testing" |
| 9 | 9 |
"time" |
| 10 | 10 |
|
| 11 |
- "github.com/docker/docker/client" |
|
| 12 | 11 |
"github.com/docker/docker/daemon/libnetwork/netlabel" |
| 13 | 12 |
"github.com/docker/docker/integration/internal/container" |
| 14 | 13 |
net "github.com/docker/docker/integration/internal/network" |
| ... | ... |
@@ -17,6 +16,7 @@ import ( |
| 17 | 17 |
"github.com/docker/docker/testutil/daemon" |
| 18 | 18 |
containertypes "github.com/moby/moby/api/types/container" |
| 19 | 19 |
"github.com/moby/moby/api/types/network" |
| 20 |
+ "github.com/moby/moby/client" |
|
| 20 | 21 |
"gotest.tools/v3/assert" |
| 21 | 22 |
is "gotest.tools/v3/assert/cmp" |
| 22 | 23 |
"gotest.tools/v3/icmd" |
| ... | ... |
@@ -11,7 +11,6 @@ import ( |
| 11 | 11 |
"testing" |
| 12 | 12 |
"time" |
| 13 | 13 |
|
| 14 |
- "github.com/docker/docker/client" |
|
| 15 | 14 |
"github.com/docker/docker/daemon/libnetwork/netlabel" |
| 16 | 15 |
"github.com/docker/docker/integration/internal/container" |
| 17 | 16 |
"github.com/docker/docker/integration/internal/network" |
| ... | ... |
@@ -21,6 +20,7 @@ import ( |
| 21 | 21 |
containertypes "github.com/moby/moby/api/types/container" |
| 22 | 22 |
networktypes "github.com/moby/moby/api/types/network" |
| 23 | 23 |
"github.com/moby/moby/api/types/versions" |
| 24 |
+ "github.com/moby/moby/client" |
|
| 24 | 25 |
"gotest.tools/v3/assert" |
| 25 | 26 |
is "gotest.tools/v3/assert/cmp" |
| 26 | 27 |
"gotest.tools/v3/icmd" |
| ... | ... |
@@ -6,7 +6,6 @@ import ( |
| 6 | 6 |
"testing" |
| 7 | 7 |
"time" |
| 8 | 8 |
|
| 9 |
- "github.com/docker/docker/client" |
|
| 10 | 9 |
"github.com/docker/docker/daemon/libnetwork/netlabel" |
| 11 | 10 |
"github.com/docker/docker/integration/internal/container" |
| 12 | 11 |
"github.com/docker/docker/integration/internal/network" |
| ... | ... |
@@ -16,6 +15,7 @@ import ( |
| 16 | 16 |
containertypes "github.com/moby/moby/api/types/container" |
| 17 | 17 |
networktypes "github.com/moby/moby/api/types/network" |
| 18 | 18 |
swarmtypes "github.com/moby/moby/api/types/swarm" |
| 19 |
+ "github.com/moby/moby/client" |
|
| 19 | 20 |
"gotest.tools/v3/assert" |
| 20 | 21 |
is "gotest.tools/v3/assert/cmp" |
| 21 | 22 |
"gotest.tools/v3/icmd" |
| ... | ... |
@@ -16,7 +16,6 @@ import ( |
| 16 | 16 |
"testing" |
| 17 | 17 |
"time" |
| 18 | 18 |
|
| 19 |
- "github.com/docker/docker/client" |
|
| 20 | 19 |
"github.com/docker/docker/daemon/libnetwork/drivers/bridge" |
| 21 | 20 |
"github.com/docker/docker/daemon/libnetwork/iptables" |
| 22 | 21 |
"github.com/docker/docker/daemon/libnetwork/netlabel" |
| ... | ... |
@@ -30,6 +29,7 @@ import ( |
| 30 | 30 |
"github.com/google/go-cmp/cmp/cmpopts" |
| 31 | 31 |
containertypes "github.com/moby/moby/api/types/container" |
| 32 | 32 |
networktypes "github.com/moby/moby/api/types/network" |
| 33 |
+ "github.com/moby/moby/client" |
|
| 33 | 34 |
"gotest.tools/v3/assert" |
| 34 | 35 |
is "gotest.tools/v3/assert/cmp" |
| 35 | 36 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -4,9 +4,9 @@ import ( |
| 4 | 4 |
"os" |
| 5 | 5 |
"testing" |
| 6 | 6 |
|
| 7 |
- "github.com/docker/docker/client" |
|
| 8 | 7 |
"github.com/docker/docker/internal/testutils/networking" |
| 9 | 8 |
"github.com/docker/docker/testutil/request" |
| 9 |
+ "github.com/moby/moby/client" |
|
| 10 | 10 |
"gotest.tools/v3/assert" |
| 11 | 11 |
is "gotest.tools/v3/assert/cmp" |
| 12 | 12 |
) |
| ... | ... |
@@ -3,13 +3,13 @@ package networking |
| 3 | 3 |
import ( |
| 4 | 4 |
"testing" |
| 5 | 5 |
|
| 6 |
- "github.com/docker/docker/client" |
|
| 7 | 6 |
"github.com/docker/docker/daemon/libnetwork/drivers/bridge" |
| 8 | 7 |
"github.com/docker/docker/integration/internal/container" |
| 9 | 8 |
"github.com/docker/docker/integration/internal/network" |
| 10 | 9 |
"github.com/docker/docker/testutil" |
| 11 | 10 |
"github.com/docker/docker/testutil/daemon" |
| 12 | 11 |
containertypes "github.com/moby/moby/api/types/container" |
| 12 |
+ "github.com/moby/moby/client" |
|
| 13 | 13 |
"gotest.tools/v3/assert" |
| 14 | 14 |
is "gotest.tools/v3/assert/cmp" |
| 15 | 15 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -15,7 +15,6 @@ import ( |
| 15 | 15 |
"testing" |
| 16 | 16 |
"time" |
| 17 | 17 |
|
| 18 |
- "github.com/docker/docker/client" |
|
| 19 | 18 |
"github.com/docker/docker/daemon/libnetwork/drivers/bridge" |
| 20 | 19 |
"github.com/docker/docker/integration/internal/container" |
| 21 | 20 |
"github.com/docker/docker/integration/internal/network" |
| ... | ... |
@@ -26,6 +25,7 @@ import ( |
| 26 | 26 |
"github.com/docker/go-connections/nat" |
| 27 | 27 |
containertypes "github.com/moby/moby/api/types/container" |
| 28 | 28 |
networktypes "github.com/moby/moby/api/types/network" |
| 29 |
+ "github.com/moby/moby/client" |
|
| 29 | 30 |
"gotest.tools/v3/assert" |
| 30 | 31 |
is "gotest.tools/v3/assert/cmp" |
| 31 | 32 |
"gotest.tools/v3/golden" |
| ... | ... |
@@ -17,7 +17,6 @@ import ( |
| 17 | 17 |
"testing" |
| 18 | 18 |
"time" |
| 19 | 19 |
|
| 20 |
- "github.com/docker/docker/client" |
|
| 21 | 20 |
"github.com/docker/docker/integration/internal/container" |
| 22 | 21 |
"github.com/docker/docker/pkg/authorization" |
| 23 | 22 |
"github.com/docker/docker/testutil/environment" |
| ... | ... |
@@ -26,6 +25,7 @@ import ( |
| 26 | 26 |
containertypes "github.com/moby/moby/api/types/container" |
| 27 | 27 |
eventtypes "github.com/moby/moby/api/types/events" |
| 28 | 28 |
"github.com/moby/moby/api/types/image" |
| 29 |
+ "github.com/moby/moby/client" |
|
| 29 | 30 |
"gotest.tools/v3/assert" |
| 30 | 31 |
"gotest.tools/v3/skip" |
| 31 | 32 |
) |
| ... | ... |
@@ -8,12 +8,12 @@ import ( |
| 8 | 8 |
"io" |
| 9 | 9 |
"testing" |
| 10 | 10 |
|
| 11 |
- "github.com/docker/docker/client" |
|
| 12 | 11 |
"github.com/docker/docker/integration/internal/container" |
| 13 | 12 |
"github.com/docker/docker/integration/internal/requirement" |
| 14 | 13 |
"github.com/moby/moby/api/types" |
| 15 | 14 |
"github.com/moby/moby/api/types/filters" |
| 16 | 15 |
"github.com/moby/moby/api/types/volume" |
| 16 |
+ "github.com/moby/moby/client" |
|
| 17 | 17 |
"gotest.tools/v3/assert" |
| 18 | 18 |
"gotest.tools/v3/skip" |
| 19 | 19 |
) |
| ... | ... |
@@ -9,13 +9,13 @@ import ( |
| 9 | 9 |
"time" |
| 10 | 10 |
|
| 11 | 11 |
cerrdefs "github.com/containerd/errdefs" |
| 12 |
- "github.com/docker/docker/client" |
|
| 13 | 12 |
"github.com/docker/docker/integration/internal/swarm" |
| 14 | 13 |
"github.com/docker/docker/pkg/stdcopy" |
| 15 | 14 |
"github.com/docker/docker/testutil" |
| 16 | 15 |
"github.com/moby/moby/api/types/container" |
| 17 | 16 |
"github.com/moby/moby/api/types/filters" |
| 18 | 17 |
swarmtypes "github.com/moby/moby/api/types/swarm" |
| 18 |
+ "github.com/moby/moby/client" |
|
| 19 | 19 |
"gotest.tools/v3/assert" |
| 20 | 20 |
is "gotest.tools/v3/assert/cmp" |
| 21 | 21 |
"gotest.tools/v3/poll" |
| ... | ... |
@@ -8,7 +8,6 @@ import ( |
| 8 | 8 |
"time" |
| 9 | 9 |
|
| 10 | 10 |
cerrdefs "github.com/containerd/errdefs" |
| 11 |
- "github.com/docker/docker/client" |
|
| 12 | 11 |
"github.com/docker/docker/integration/internal/network" |
| 13 | 12 |
"github.com/docker/docker/integration/internal/swarm" |
| 14 | 13 |
"github.com/docker/docker/testutil" |
| ... | ... |
@@ -16,6 +15,7 @@ import ( |
| 16 | 16 |
"github.com/moby/moby/api/types/container" |
| 17 | 17 |
"github.com/moby/moby/api/types/filters" |
| 18 | 18 |
swarmtypes "github.com/moby/moby/api/types/swarm" |
| 19 |
+ "github.com/moby/moby/client" |
|
| 19 | 20 |
"gotest.tools/v3/assert" |
| 20 | 21 |
is "gotest.tools/v3/assert/cmp" |
| 21 | 22 |
"gotest.tools/v3/poll" |
| ... | ... |
@@ -7,7 +7,6 @@ import ( |
| 7 | 7 |
"testing" |
| 8 | 8 |
"time" |
| 9 | 9 |
|
| 10 |
- "github.com/docker/docker/client" |
|
| 11 | 10 |
"github.com/docker/docker/daemon/libnetwork/scope" |
| 12 | 11 |
"github.com/docker/docker/integration/internal/container" |
| 13 | 12 |
net "github.com/docker/docker/integration/internal/network" |
| ... | ... |
@@ -17,6 +16,7 @@ import ( |
| 17 | 17 |
containertypes "github.com/moby/moby/api/types/container" |
| 18 | 18 |
"github.com/moby/moby/api/types/network" |
| 19 | 19 |
swarmtypes "github.com/moby/moby/api/types/swarm" |
| 20 |
+ "github.com/moby/moby/client" |
|
| 20 | 21 |
"gotest.tools/v3/assert" |
| 21 | 22 |
is "gotest.tools/v3/assert/cmp" |
| 22 | 23 |
"gotest.tools/v3/golden" |
| ... | ... |
@@ -4,7 +4,6 @@ import ( |
| 4 | 4 |
"context" |
| 5 | 5 |
"testing" |
| 6 | 6 |
|
| 7 |
- "github.com/docker/docker/client" |
|
| 8 | 7 |
"github.com/docker/docker/integration/internal/network" |
| 9 | 8 |
"github.com/docker/docker/integration/internal/swarm" |
| 10 | 9 |
"github.com/docker/docker/testutil" |
| ... | ... |
@@ -12,6 +11,7 @@ import ( |
| 12 | 12 |
"github.com/moby/moby/api/types/filters" |
| 13 | 13 |
networktypes "github.com/moby/moby/api/types/network" |
| 14 | 14 |
swarmtypes "github.com/moby/moby/api/types/swarm" |
| 15 |
+ "github.com/moby/moby/client" |
|
| 15 | 16 |
"gotest.tools/v3/assert" |
| 16 | 17 |
is "gotest.tools/v3/assert/cmp" |
| 17 | 18 |
"gotest.tools/v3/poll" |
| ... | ... |
@@ -9,7 +9,6 @@ import ( |
| 9 | 9 |
"strings" |
| 10 | 10 |
"testing" |
| 11 | 11 |
|
| 12 |
- "github.com/docker/docker/client" |
|
| 13 | 12 |
"github.com/docker/docker/daemon/volume/safepath" |
| 14 | 13 |
"github.com/docker/docker/integration/internal/container" |
| 15 | 14 |
"github.com/docker/docker/testutil/fakecontext" |
| ... | ... |
@@ -20,6 +19,7 @@ import ( |
| 20 | 20 |
"github.com/moby/moby/api/types/network" |
| 21 | 21 |
"github.com/moby/moby/api/types/versions" |
| 22 | 22 |
"github.com/moby/moby/api/types/volume" |
| 23 |
+ "github.com/moby/moby/client" |
|
| 23 | 24 |
"gotest.tools/v3/assert" |
| 24 | 25 |
is "gotest.tools/v3/assert/cmp" |
| 25 | 26 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -9,7 +9,6 @@ import ( |
| 9 | 9 |
"time" |
| 10 | 10 |
|
| 11 | 11 |
cerrdefs "github.com/containerd/errdefs" |
| 12 |
- clientpkg "github.com/docker/docker/client" |
|
| 13 | 12 |
"github.com/docker/docker/integration/internal/build" |
| 14 | 13 |
"github.com/docker/docker/integration/internal/container" |
| 15 | 14 |
"github.com/docker/docker/testutil" |
| ... | ... |
@@ -20,6 +19,7 @@ import ( |
| 20 | 20 |
containertypes "github.com/moby/moby/api/types/container" |
| 21 | 21 |
"github.com/moby/moby/api/types/filters" |
| 22 | 22 |
"github.com/moby/moby/api/types/volume" |
| 23 |
+ clientpkg "github.com/moby/moby/client" |
|
| 23 | 24 |
"gotest.tools/v3/assert" |
| 24 | 25 |
is "gotest.tools/v3/assert/cmp" |
| 25 | 26 |
"gotest.tools/v3/skip" |
| ... | ... |
@@ -21,7 +21,6 @@ import ( |
| 21 | 21 |
"testing" |
| 22 | 22 |
"time" |
| 23 | 23 |
|
| 24 |
- "github.com/docker/docker/client" |
|
| 25 | 24 |
"github.com/docker/docker/daemon/container" |
| 26 | 25 |
"github.com/docker/docker/pkg/ioutils" |
| 27 | 26 |
"github.com/docker/docker/pkg/stringid" |
| ... | ... |
@@ -31,6 +30,7 @@ import ( |
| 31 | 31 |
"github.com/docker/go-connections/tlsconfig" |
| 32 | 32 |
"github.com/moby/moby/api/types/events" |
| 33 | 33 |
"github.com/moby/moby/api/types/system" |
| 34 |
+ "github.com/moby/moby/client" |
|
| 34 | 35 |
"github.com/pkg/errors" |
| 35 | 36 |
"gotest.tools/v3/assert" |
| 36 | 37 |
"gotest.tools/v3/poll" |
| ... | ... |
@@ -6,7 +6,6 @@ import ( |
| 6 | 6 |
"testing" |
| 7 | 7 |
|
| 8 | 8 |
cerrdefs "github.com/containerd/errdefs" |
| 9 |
- "github.com/docker/docker/client" |
|
| 10 | 9 |
"github.com/docker/docker/internal/lazyregexp" |
| 11 | 10 |
"github.com/moby/moby/api/types" |
| 12 | 11 |
"github.com/moby/moby/api/types/container" |
| ... | ... |
@@ -14,6 +13,7 @@ import ( |
| 14 | 14 |
"github.com/moby/moby/api/types/image" |
| 15 | 15 |
"github.com/moby/moby/api/types/network" |
| 16 | 16 |
"github.com/moby/moby/api/types/volume" |
| 17 |
+ "github.com/moby/moby/client" |
|
| 17 | 18 |
"go.opentelemetry.io/otel" |
| 18 | 19 |
"gotest.tools/v3/assert" |
| 19 | 20 |
) |
| ... | ... |
@@ -8,12 +8,12 @@ import ( |
| 8 | 8 |
"strings" |
| 9 | 9 |
"testing" |
| 10 | 10 |
|
| 11 |
- "github.com/docker/docker/client" |
|
| 12 | 11 |
"github.com/docker/docker/testutil/fixtures/load" |
| 13 | 12 |
"github.com/moby/moby/api/types" |
| 14 | 13 |
"github.com/moby/moby/api/types/filters" |
| 15 | 14 |
"github.com/moby/moby/api/types/image" |
| 16 | 15 |
"github.com/moby/moby/api/types/system" |
| 16 |
+ "github.com/moby/moby/client" |
|
| 17 | 17 |
"github.com/pkg/errors" |
| 18 | 18 |
"gotest.tools/v3/assert" |
| 19 | 19 |
) |
| ... | ... |
@@ -11,7 +11,6 @@ import ( |
| 11 | 11 |
"strings" |
| 12 | 12 |
"testing" |
| 13 | 13 |
|
| 14 |
- "github.com/docker/docker/client" |
|
| 15 | 14 |
"github.com/docker/docker/testutil" |
| 16 | 15 |
"github.com/docker/docker/testutil/environment" |
| 17 | 16 |
"github.com/docker/docker/testutil/fakecontext" |
| ... | ... |
@@ -20,6 +19,7 @@ import ( |
| 20 | 20 |
"github.com/moby/moby/api/types/build" |
| 21 | 21 |
containertypes "github.com/moby/moby/api/types/container" |
| 22 | 22 |
"github.com/moby/moby/api/types/image" |
| 23 |
+ "github.com/moby/moby/client" |
|
| 23 | 24 |
"gotest.tools/v3/assert" |
| 24 | 25 |
) |
| 25 | 26 |
|
| ... | ... |
@@ -10,9 +10,9 @@ import ( |
| 10 | 10 |
"strings" |
| 11 | 11 |
"sync" |
| 12 | 12 |
|
| 13 |
- "github.com/docker/docker/client" |
|
| 14 | 13 |
"github.com/docker/docker/pkg/jsonmessage" |
| 15 | 14 |
"github.com/moby/moby/api/types/image" |
| 15 |
+ "github.com/moby/moby/client" |
|
| 16 | 16 |
"github.com/moby/term" |
| 17 | 17 |
"github.com/pkg/errors" |
| 18 | 18 |
"go.opentelemetry.io/otel" |
| ... | ... |
@@ -13,11 +13,11 @@ import ( |
| 13 | 13 |
"testing" |
| 14 | 14 |
"time" |
| 15 | 15 |
|
| 16 |
- "github.com/docker/docker/client" |
|
| 17 | 16 |
"github.com/docker/docker/pkg/ioutils" |
| 18 | 17 |
"github.com/docker/docker/testutil/environment" |
| 19 | 18 |
"github.com/docker/go-connections/sockets" |
| 20 | 19 |
"github.com/docker/go-connections/tlsconfig" |
| 20 |
+ "github.com/moby/moby/client" |
|
| 21 | 21 |
"github.com/pkg/errors" |
| 22 | 22 |
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" |
| 23 | 23 |
"gotest.tools/v3/assert" |
| ... | ... |
@@ -30,7 +30,7 @@ require ( |
| 30 | 30 |
github.com/containerd/containerd/v2 v2.1.3 |
| 31 | 31 |
github.com/containerd/continuity v0.4.5 |
| 32 | 32 |
github.com/containerd/errdefs v1.0.0 |
| 33 |
- github.com/containerd/errdefs/pkg v0.3.0 |
|
| 33 |
+ github.com/containerd/errdefs/pkg v0.3.0 // indirect |
|
| 34 | 34 |
github.com/containerd/fifo v1.1.0 |
| 35 | 35 |
github.com/containerd/log v0.1.0 |
| 36 | 36 |
github.com/containerd/platforms v1.0.0-rc.1 |
| ... | ... |
@@ -191,6 +191,7 @@ require ( |
| 191 | 191 |
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect |
| 192 | 192 |
github.com/mitchellh/reflectwalk v1.0.2 // indirect |
| 193 | 193 |
github.com/moby/moby/api v0.0.0 |
| 194 |
+ github.com/moby/moby/client v0.0.0 |
|
| 194 | 195 |
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect |
| 195 | 196 |
github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect |
| 196 | 197 |
github.com/package-url/packageurl-go v0.1.1 // indirect |
| ... | ... |
@@ -264,3 +265,5 @@ exclude ( |
| 264 | 264 |
) |
| 265 | 265 |
|
| 266 | 266 |
replace github.com/moby/moby/api => ./api |
| 267 |
+ |
|
| 268 |
+replace github.com/moby/moby/client => ./client |
| 267 | 269 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,38 @@ |
| 0 |
+# Go client for the Docker Engine API |
|
| 1 |
+ |
|
| 2 |
+The `docker` command uses this package to communicate with the daemon. It can |
|
| 3 |
+also be used by your own Go applications to do anything the command-line |
|
| 4 |
+interface does – running containers, pulling images, managing swarms, etc. |
|
| 5 |
+ |
|
| 6 |
+For example, to list all containers (the equivalent of `docker ps --all`): |
|
| 7 |
+ |
|
| 8 |
+```go |
|
| 9 |
+package main |
|
| 10 |
+ |
|
| 11 |
+import ( |
|
| 12 |
+ "context" |
|
| 13 |
+ "fmt" |
|
| 14 |
+ |
|
| 15 |
+ "github.com/docker/docker/api/types/container" |
|
| 16 |
+ "github.com/docker/docker/client" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+func main() {
|
|
| 20 |
+ apiClient, err := client.NewClientWithOpts(client.FromEnv) |
|
| 21 |
+ if err != nil {
|
|
| 22 |
+ panic(err) |
|
| 23 |
+ } |
|
| 24 |
+ defer apiClient.Close() |
|
| 25 |
+ |
|
| 26 |
+ containers, err := apiClient.ContainerList(context.Background(), container.ListOptions{All: true})
|
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ panic(err) |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ for _, ctr := range containers {
|
|
| 32 |
+ fmt.Printf("%s %s (status: %s)\n", ctr.ID, ctr.Image, ctr.Status)
|
|
| 33 |
+ } |
|
| 34 |
+} |
|
| 35 |
+``` |
|
| 36 |
+ |
|
| 37 |
+[Full documentation is available on pkg.go.dev.](https://pkg.go.dev/github.com/docker/docker/client) |
| 0 | 38 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,14 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/moby/moby/api/types/registry" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// staticAuth creates a privilegeFn from the given registryAuth. |
|
| 9 |
+func staticAuth(registryAuth string) registry.RequestAuthConfig {
|
|
| 10 |
+ return func(ctx context.Context) (string, error) {
|
|
| 11 |
+ return registryAuth, nil |
|
| 12 |
+ } |
|
| 13 |
+} |
| 0 | 14 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,16 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+// BuildCancel requests the daemon to cancel the ongoing build request. |
|
| 8 |
+func (cli *Client) BuildCancel(ctx context.Context, id string) error {
|
|
| 9 |
+ query := url.Values{}
|
|
| 10 |
+ query.Set("id", id)
|
|
| 11 |
+ |
|
| 12 |
+ resp, err := cli.post(ctx, "/build/cancel", query, nil, nil) |
|
| 13 |
+ ensureReaderClosed(resp) |
|
| 14 |
+ return err |
|
| 15 |
+} |
| 0 | 16 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,56 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ "strconv" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/build" |
|
| 9 |
+ "github.com/moby/moby/api/types/filters" |
|
| 10 |
+ "github.com/pkg/errors" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// BuildCachePrune requests the daemon to delete unused cache data |
|
| 14 |
+func (cli *Client) BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) {
|
|
| 15 |
+ if err := cli.NewVersionError(ctx, "1.31", "build prune"); err != nil {
|
|
| 16 |
+ return nil, err |
|
| 17 |
+ } |
|
| 18 |
+ |
|
| 19 |
+ query := url.Values{}
|
|
| 20 |
+ if opts.All {
|
|
| 21 |
+ query.Set("all", "1")
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ if opts.KeepStorage != 0 {
|
|
| 25 |
+ query.Set("keep-storage", strconv.Itoa(int(opts.KeepStorage)))
|
|
| 26 |
+ } |
|
| 27 |
+ if opts.ReservedSpace != 0 {
|
|
| 28 |
+ query.Set("reserved-space", strconv.Itoa(int(opts.ReservedSpace)))
|
|
| 29 |
+ } |
|
| 30 |
+ if opts.MaxUsedSpace != 0 {
|
|
| 31 |
+ query.Set("max-used-space", strconv.Itoa(int(opts.MaxUsedSpace)))
|
|
| 32 |
+ } |
|
| 33 |
+ if opts.MinFreeSpace != 0 {
|
|
| 34 |
+ query.Set("min-free-space", strconv.Itoa(int(opts.MinFreeSpace)))
|
|
| 35 |
+ } |
|
| 36 |
+ f, err := filters.ToJSON(opts.Filters) |
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ return nil, errors.Wrap(err, "prune could not marshal filters option") |
|
| 39 |
+ } |
|
| 40 |
+ query.Set("filters", f)
|
|
| 41 |
+ |
|
| 42 |
+ resp, err := cli.post(ctx, "/build/prune", query, nil, nil) |
|
| 43 |
+ defer ensureReaderClosed(resp) |
|
| 44 |
+ |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ return nil, err |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ report := build.CachePruneReport{}
|
|
| 50 |
+ if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
|
|
| 51 |
+ return nil, errors.Wrap(err, "error retrieving disk usage") |
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ return &report, nil |
|
| 55 |
+} |
| 0 | 56 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,18 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/moby/moby/api/types/checkpoint" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// CheckpointAPIClient defines API client methods for the checkpoints. |
|
| 9 |
+// |
|
| 10 |
+// Experimental: checkpoint and restore is still an experimental feature, |
|
| 11 |
+// and only available if the daemon is running with experimental features |
|
| 12 |
+// enabled. |
|
| 13 |
+type CheckpointAPIClient interface {
|
|
| 14 |
+ CheckpointCreate(ctx context.Context, container string, options checkpoint.CreateOptions) error |
|
| 15 |
+ CheckpointDelete(ctx context.Context, container string, options checkpoint.DeleteOptions) error |
|
| 16 |
+ CheckpointList(ctx context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) |
|
| 17 |
+} |
| 0 | 18 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,19 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/moby/moby/api/types/checkpoint" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// CheckpointCreate creates a checkpoint from the given container with the given name |
|
| 9 |
+func (cli *Client) CheckpointCreate(ctx context.Context, containerID string, options checkpoint.CreateOptions) error {
|
|
| 10 |
+ containerID, err := trimID("container", containerID)
|
|
| 11 |
+ if err != nil {
|
|
| 12 |
+ return err |
|
| 13 |
+ } |
|
| 14 |
+ |
|
| 15 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/checkpoints", nil, options, nil) |
|
| 16 |
+ ensureReaderClosed(resp) |
|
| 17 |
+ return err |
|
| 18 |
+} |
| 0 | 19 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,25 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/checkpoint" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// CheckpointDelete deletes the checkpoint with the given name from the given container |
|
| 10 |
+func (cli *Client) CheckpointDelete(ctx context.Context, containerID string, options checkpoint.DeleteOptions) error {
|
|
| 11 |
+ containerID, err := trimID("container", containerID)
|
|
| 12 |
+ if err != nil {
|
|
| 13 |
+ return err |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ if options.CheckpointDir != "" {
|
|
| 18 |
+ query.Set("dir", options.CheckpointDir)
|
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ resp, err := cli.delete(ctx, "/containers/"+containerID+"/checkpoints/"+options.CheckpointID, query, nil) |
|
| 22 |
+ ensureReaderClosed(resp) |
|
| 23 |
+ return err |
|
| 24 |
+} |
| 0 | 25 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,28 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/checkpoint" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// CheckpointList returns the checkpoints of the given container in the docker host |
|
| 11 |
+func (cli *Client) CheckpointList(ctx context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
|
| 12 |
+ var checkpoints []checkpoint.Summary |
|
| 13 |
+ |
|
| 14 |
+ query := url.Values{}
|
|
| 15 |
+ if options.CheckpointDir != "" {
|
|
| 16 |
+ query.Set("dir", options.CheckpointDir)
|
|
| 17 |
+ } |
|
| 18 |
+ |
|
| 19 |
+ resp, err := cli.get(ctx, "/containers/"+container+"/checkpoints", query, nil) |
|
| 20 |
+ defer ensureReaderClosed(resp) |
|
| 21 |
+ if err != nil {
|
|
| 22 |
+ return checkpoints, err |
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ err = json.NewDecoder(resp.Body).Decode(&checkpoints) |
|
| 26 |
+ return checkpoints, err |
|
| 27 |
+} |
| 0 | 28 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,485 @@ |
| 0 |
+/* |
|
| 1 |
+Package client is a Go client for the Docker Engine API. |
|
| 2 |
+ |
|
| 3 |
+For more information about the Engine API, see the documentation: |
|
| 4 |
+https://docs.docker.com/reference/api/engine/ |
|
| 5 |
+ |
|
| 6 |
+# Usage |
|
| 7 |
+ |
|
| 8 |
+You use the library by constructing a client object using [NewClientWithOpts] |
|
| 9 |
+and calling methods on it. The client can be configured from environment |
|
| 10 |
+variables by passing the [FromEnv] option, or configured manually by passing any |
|
| 11 |
+of the other available [Opts]. |
|
| 12 |
+ |
|
| 13 |
+For example, to list running containers (the equivalent of "docker ps"): |
|
| 14 |
+ |
|
| 15 |
+ package main |
|
| 16 |
+ |
|
| 17 |
+ import ( |
|
| 18 |
+ "context" |
|
| 19 |
+ "fmt" |
|
| 20 |
+ |
|
| 21 |
+ "github.com/moby/moby/api/types/container" |
|
| 22 |
+ "github.com/moby/moby/client" |
|
| 23 |
+ ) |
|
| 24 |
+ |
|
| 25 |
+ func main() {
|
|
| 26 |
+ cli, err := client.NewClientWithOpts(client.FromEnv) |
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ panic(err) |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ containers, err := cli.ContainerList(context.Background(), container.ListOptions{})
|
|
| 32 |
+ if err != nil {
|
|
| 33 |
+ panic(err) |
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ for _, ctr := range containers {
|
|
| 37 |
+ fmt.Printf("%s %s\n", ctr.ID, ctr.Image)
|
|
| 38 |
+ } |
|
| 39 |
+ } |
|
| 40 |
+*/ |
|
| 41 |
+package client |
|
| 42 |
+ |
|
| 43 |
+import ( |
|
| 44 |
+ "context" |
|
| 45 |
+ "crypto/tls" |
|
| 46 |
+ "net" |
|
| 47 |
+ "net/http" |
|
| 48 |
+ "net/url" |
|
| 49 |
+ "path" |
|
| 50 |
+ "strings" |
|
| 51 |
+ "sync" |
|
| 52 |
+ "sync/atomic" |
|
| 53 |
+ "time" |
|
| 54 |
+ |
|
| 55 |
+ "github.com/docker/go-connections/sockets" |
|
| 56 |
+ "github.com/moby/moby/api/types" |
|
| 57 |
+ "github.com/moby/moby/api/types/versions" |
|
| 58 |
+ "github.com/pkg/errors" |
|
| 59 |
+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" |
|
| 60 |
+) |
|
| 61 |
+ |
|
| 62 |
+// DummyHost is a hostname used for local communication. |
|
| 63 |
+// |
|
| 64 |
+// It acts as a valid formatted hostname for local connections (such as "unix://" |
|
| 65 |
+// or "npipe://") which do not require a hostname. It should never be resolved, |
|
| 66 |
+// but uses the special-purpose ".localhost" TLD (as defined in [RFC 2606, Section 2] |
|
| 67 |
+// and [RFC 6761, Section 6.3]). |
|
| 68 |
+// |
|
| 69 |
+// [RFC 7230, Section 5.4] defines that an empty header must be used for such |
|
| 70 |
+// cases: |
|
| 71 |
+// |
|
| 72 |
+// If the authority component is missing or undefined for the target URI, |
|
| 73 |
+// then a client MUST send a Host header field with an empty field-value. |
|
| 74 |
+// |
|
| 75 |
+// However, [Go stdlib] enforces the semantics of HTTP(S) over TCP, does not |
|
| 76 |
+// allow an empty header to be used, and requires req.URL.Scheme to be either |
|
| 77 |
+// "http" or "https". |
|
| 78 |
+// |
|
| 79 |
+// For further details, refer to: |
|
| 80 |
+// |
|
| 81 |
+// - https://github.com/docker/engine-api/issues/189 |
|
| 82 |
+// - https://github.com/golang/go/issues/13624 |
|
| 83 |
+// - https://github.com/golang/go/issues/61076 |
|
| 84 |
+// - https://github.com/moby/moby/issues/45935 |
|
| 85 |
+// |
|
| 86 |
+// [RFC 2606, Section 2]: https://www.rfc-editor.org/rfc/rfc2606.html#section-2 |
|
| 87 |
+// [RFC 6761, Section 6.3]: https://www.rfc-editor.org/rfc/rfc6761#section-6.3 |
|
| 88 |
+// [RFC 7230, Section 5.4]: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 |
|
| 89 |
+// [Go stdlib]: https://github.com/golang/go/blob/6244b1946bc2101b01955468f1be502dbadd6807/src/net/http/transport.go#L558-L569 |
|
| 90 |
+const DummyHost = "api.moby.localhost" |
|
| 91 |
+ |
|
| 92 |
+// DefaultAPIVersion is the highest REST API version supported by the client. |
|
| 93 |
+// If API-version negotiation is enabled (see [WithAPIVersionNegotiation], |
|
| 94 |
+// [Client.NegotiateAPIVersion]), the client may downgrade its API version. |
|
| 95 |
+// Similarly, the [WithVersion] and [WithVersionFromEnv] allow overriding |
|
| 96 |
+// the version. |
|
| 97 |
+// |
|
| 98 |
+// This version may be lower than the [api.DefaultVersion], which is the default |
|
| 99 |
+// (and highest supported) version of the api library module used. |
|
| 100 |
+const DefaultAPIVersion = "1.52" |
|
| 101 |
+ |
|
| 102 |
+// fallbackAPIVersion is the version to fallback to if API-version negotiation |
|
| 103 |
+// fails. This version is the highest version of the API before API-version |
|
| 104 |
+// negotiation was introduced. If negotiation fails (or no API version was |
|
| 105 |
+// included in the API response), we assume the API server uses the most |
|
| 106 |
+// recent version before negotiation was introduced. |
|
| 107 |
+const fallbackAPIVersion = "1.24" |
|
| 108 |
+ |
|
| 109 |
+// Ensure that Client always implements APIClient. |
|
| 110 |
+var _ APIClient = &Client{}
|
|
| 111 |
+ |
|
| 112 |
+// Client is the API client that performs all operations |
|
| 113 |
+// against a docker server. |
|
| 114 |
+type Client struct {
|
|
| 115 |
+ // scheme sets the scheme for the client |
|
| 116 |
+ scheme string |
|
| 117 |
+ // host holds the server address to connect to |
|
| 118 |
+ host string |
|
| 119 |
+ // proto holds the client protocol i.e. unix. |
|
| 120 |
+ proto string |
|
| 121 |
+ // addr holds the client address. |
|
| 122 |
+ addr string |
|
| 123 |
+ // basePath holds the path to prepend to the requests. |
|
| 124 |
+ basePath string |
|
| 125 |
+ // client used to send and receive http requests. |
|
| 126 |
+ client *http.Client |
|
| 127 |
+ // version of the server to talk to. |
|
| 128 |
+ version string |
|
| 129 |
+ // userAgent is the User-Agent header to use for HTTP requests. It takes |
|
| 130 |
+ // precedence over User-Agent headers set in customHTTPHeaders, and other |
|
| 131 |
+ // header variables. When set to an empty string, the User-Agent header |
|
| 132 |
+ // is removed, and no header is sent. |
|
| 133 |
+ userAgent *string |
|
| 134 |
+ // custom HTTP headers configured by users. |
|
| 135 |
+ customHTTPHeaders map[string]string |
|
| 136 |
+ // manualOverride is set to true when the version was set by users. |
|
| 137 |
+ manualOverride bool |
|
| 138 |
+ |
|
| 139 |
+ // negotiateVersion indicates if the client should automatically negotiate |
|
| 140 |
+ // the API version to use when making requests. API version negotiation is |
|
| 141 |
+ // performed on the first request, after which negotiated is set to "true" |
|
| 142 |
+ // so that subsequent requests do not re-negotiate. |
|
| 143 |
+ negotiateVersion bool |
|
| 144 |
+ |
|
| 145 |
+ // negotiated indicates that API version negotiation took place |
|
| 146 |
+ negotiated atomic.Bool |
|
| 147 |
+ |
|
| 148 |
+ // negotiateLock is used to single-flight the version negotiation process |
|
| 149 |
+ negotiateLock sync.Mutex |
|
| 150 |
+ |
|
| 151 |
+ traceOpts []otelhttp.Option |
|
| 152 |
+ |
|
| 153 |
+ // When the client transport is an *http.Transport (default) we need to do some extra things (like closing idle connections). |
|
| 154 |
+ // Store the original transport as the http.Client transport will be wrapped with tracing libs. |
|
| 155 |
+ baseTransport *http.Transport |
|
| 156 |
+} |
|
| 157 |
+ |
|
| 158 |
+// ErrRedirect is the error returned by checkRedirect when the request is non-GET. |
|
| 159 |
+var ErrRedirect = errors.New("unexpected redirect in response")
|
|
| 160 |
+ |
|
| 161 |
+// CheckRedirect specifies the policy for dealing with redirect responses. It |
|
| 162 |
+// can be set on [http.Client.CheckRedirect] to prevent HTTP redirects for |
|
| 163 |
+// non-GET requests. It returns an [ErrRedirect] for non-GET request, otherwise |
|
| 164 |
+// returns a [http.ErrUseLastResponse], which is special-cased by http.Client |
|
| 165 |
+// to use the last response. |
|
| 166 |
+// |
|
| 167 |
+// Go 1.8 changed behavior for HTTP redirects (specifically 301, 307, and 308) |
|
| 168 |
+// in the client. The client (and by extension API client) can be made to send |
|
| 169 |
+// a request like "POST /containers//start" where what would normally be in the |
|
| 170 |
+// name section of the URL is empty. This triggers an HTTP 301 from the daemon. |
|
| 171 |
+// |
|
| 172 |
+// In go 1.8 this 301 is converted to a GET request, and ends up getting |
|
| 173 |
+// a 404 from the daemon. This behavior change manifests in the client in that |
|
| 174 |
+// before, the 301 was not followed and the client did not generate an error, |
|
| 175 |
+// but now results in a message like "Error response from daemon: page not found". |
|
| 176 |
+func CheckRedirect(_ *http.Request, via []*http.Request) error {
|
|
| 177 |
+ if via[0].Method == http.MethodGet {
|
|
| 178 |
+ return http.ErrUseLastResponse |
|
| 179 |
+ } |
|
| 180 |
+ return ErrRedirect |
|
| 181 |
+} |
|
| 182 |
+ |
|
| 183 |
+// NewClientWithOpts initializes a new API client with a default HTTPClient, and |
|
| 184 |
+// default API host and version. It also initializes the custom HTTP headers to |
|
| 185 |
+// add to each request. |
|
| 186 |
+// |
|
| 187 |
+// It takes an optional list of [Opt] functional arguments, which are applied in |
|
| 188 |
+// the order they're provided, which allows modifying the defaults when creating |
|
| 189 |
+// the client. For example, the following initializes a client that configures |
|
| 190 |
+// itself with values from environment variables ([FromEnv]), and has automatic |
|
| 191 |
+// API version negotiation enabled ([WithAPIVersionNegotiation]). |
|
| 192 |
+// |
|
| 193 |
+// cli, err := client.NewClientWithOpts( |
|
| 194 |
+// client.FromEnv, |
|
| 195 |
+// client.WithAPIVersionNegotiation(), |
|
| 196 |
+// ) |
|
| 197 |
+func NewClientWithOpts(ops ...Opt) (*Client, error) {
|
|
| 198 |
+ hostURL, err := ParseHostURL(DefaultDockerHost) |
|
| 199 |
+ if err != nil {
|
|
| 200 |
+ return nil, err |
|
| 201 |
+ } |
|
| 202 |
+ |
|
| 203 |
+ client, err := defaultHTTPClient(hostURL) |
|
| 204 |
+ if err != nil {
|
|
| 205 |
+ return nil, err |
|
| 206 |
+ } |
|
| 207 |
+ c := &Client{
|
|
| 208 |
+ host: DefaultDockerHost, |
|
| 209 |
+ version: DefaultAPIVersion, |
|
| 210 |
+ client: client, |
|
| 211 |
+ proto: hostURL.Scheme, |
|
| 212 |
+ addr: hostURL.Host, |
|
| 213 |
+ |
|
| 214 |
+ traceOpts: []otelhttp.Option{
|
|
| 215 |
+ otelhttp.WithSpanNameFormatter(func(_ string, req *http.Request) string {
|
|
| 216 |
+ return req.Method + " " + req.URL.Path |
|
| 217 |
+ }), |
|
| 218 |
+ }, |
|
| 219 |
+ } |
|
| 220 |
+ |
|
| 221 |
+ for _, op := range ops {
|
|
| 222 |
+ if err := op(c); err != nil {
|
|
| 223 |
+ return nil, err |
|
| 224 |
+ } |
|
| 225 |
+ } |
|
| 226 |
+ |
|
| 227 |
+ if tr, ok := c.client.Transport.(*http.Transport); ok {
|
|
| 228 |
+ // Store the base transport before we wrap it in tracing libs below |
|
| 229 |
+ // This is used, as an example, to close idle connections when the client is closed |
|
| 230 |
+ c.baseTransport = tr |
|
| 231 |
+ } |
|
| 232 |
+ |
|
| 233 |
+ if c.scheme == "" {
|
|
| 234 |
+ // TODO(stevvooe): This isn't really the right way to write clients in Go. |
|
| 235 |
+ // `NewClient` should probably only take an `*http.Client` and work from there. |
|
| 236 |
+ // Unfortunately, the model of having a host-ish/url-thingy as the connection |
|
| 237 |
+ // string has us confusing protocol and transport layers. We continue doing |
|
| 238 |
+ // this to avoid breaking existing clients but this should be addressed. |
|
| 239 |
+ if c.tlsConfig() != nil {
|
|
| 240 |
+ c.scheme = "https" |
|
| 241 |
+ } else {
|
|
| 242 |
+ c.scheme = "http" |
|
| 243 |
+ } |
|
| 244 |
+ } |
|
| 245 |
+ |
|
| 246 |
+ c.client.Transport = otelhttp.NewTransport(c.client.Transport, c.traceOpts...) |
|
| 247 |
+ |
|
| 248 |
+ return c, nil |
|
| 249 |
+} |
|
| 250 |
+ |
|
| 251 |
+func (cli *Client) tlsConfig() *tls.Config {
|
|
| 252 |
+ if cli.baseTransport == nil {
|
|
| 253 |
+ return nil |
|
| 254 |
+ } |
|
| 255 |
+ return cli.baseTransport.TLSClientConfig |
|
| 256 |
+} |
|
| 257 |
+ |
|
| 258 |
+func defaultHTTPClient(hostURL *url.URL) (*http.Client, error) {
|
|
| 259 |
+ transport := &http.Transport{}
|
|
| 260 |
+ // Necessary to prevent long-lived processes using the |
|
| 261 |
+ // client from leaking connections due to idle connections |
|
| 262 |
+ // not being released. |
|
| 263 |
+ // TODO: see if we can also address this from the server side, |
|
| 264 |
+ // or in go-connections. |
|
| 265 |
+ // see: https://github.com/moby/moby/issues/45539 |
|
| 266 |
+ transport.MaxIdleConns = 6 |
|
| 267 |
+ transport.IdleConnTimeout = 30 * time.Second |
|
| 268 |
+ err := sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host) |
|
| 269 |
+ if err != nil {
|
|
| 270 |
+ return nil, err |
|
| 271 |
+ } |
|
| 272 |
+ return &http.Client{
|
|
| 273 |
+ Transport: transport, |
|
| 274 |
+ CheckRedirect: CheckRedirect, |
|
| 275 |
+ }, nil |
|
| 276 |
+} |
|
| 277 |
+ |
|
| 278 |
+// Close the transport used by the client |
|
| 279 |
+func (cli *Client) Close() error {
|
|
| 280 |
+ if cli.baseTransport != nil {
|
|
| 281 |
+ cli.baseTransport.CloseIdleConnections() |
|
| 282 |
+ return nil |
|
| 283 |
+ } |
|
| 284 |
+ return nil |
|
| 285 |
+} |
|
| 286 |
+ |
|
| 287 |
+// checkVersion manually triggers API version negotiation (if configured). |
|
| 288 |
+// This allows for version-dependent code to use the same version as will |
|
| 289 |
+// be negotiated when making the actual requests, and for which cases |
|
| 290 |
+// we cannot do the negotiation lazily. |
|
| 291 |
+func (cli *Client) checkVersion(ctx context.Context) error {
|
|
| 292 |
+ if !cli.manualOverride && cli.negotiateVersion && !cli.negotiated.Load() {
|
|
| 293 |
+ // Ensure exclusive write access to version and negotiated fields |
|
| 294 |
+ cli.negotiateLock.Lock() |
|
| 295 |
+ defer cli.negotiateLock.Unlock() |
|
| 296 |
+ |
|
| 297 |
+ // May have been set during last execution of critical zone |
|
| 298 |
+ if cli.negotiated.Load() {
|
|
| 299 |
+ return nil |
|
| 300 |
+ } |
|
| 301 |
+ |
|
| 302 |
+ ping, err := cli.Ping(ctx) |
|
| 303 |
+ if err != nil {
|
|
| 304 |
+ return err |
|
| 305 |
+ } |
|
| 306 |
+ cli.negotiateAPIVersionPing(ping) |
|
| 307 |
+ } |
|
| 308 |
+ return nil |
|
| 309 |
+} |
|
| 310 |
+ |
|
| 311 |
+// getAPIPath returns the versioned request path to call the API. |
|
| 312 |
+// It appends the query parameters to the path if they are not empty. |
|
| 313 |
+func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string {
|
|
| 314 |
+ var apiPath string |
|
| 315 |
+ _ = cli.checkVersion(ctx) |
|
| 316 |
+ if cli.version != "" {
|
|
| 317 |
+ apiPath = path.Join(cli.basePath, "/v"+strings.TrimPrefix(cli.version, "v"), p) |
|
| 318 |
+ } else {
|
|
| 319 |
+ apiPath = path.Join(cli.basePath, p) |
|
| 320 |
+ } |
|
| 321 |
+ return (&url.URL{Path: apiPath, RawQuery: query.Encode()}).String()
|
|
| 322 |
+} |
|
| 323 |
+ |
|
| 324 |
+// ClientVersion returns the API version used by this client. |
|
| 325 |
+func (cli *Client) ClientVersion() string {
|
|
| 326 |
+ return cli.version |
|
| 327 |
+} |
|
| 328 |
+ |
|
| 329 |
+// NegotiateAPIVersion queries the API and updates the version to match the API |
|
| 330 |
+// version. NegotiateAPIVersion downgrades the client's API version to match the |
|
| 331 |
+// APIVersion if the ping version is lower than the default version. If the API |
|
| 332 |
+// version reported by the server is higher than the maximum version supported |
|
| 333 |
+// by the client, it uses the client's maximum version. |
|
| 334 |
+// |
|
| 335 |
+// If a manual override is in place, either through the "DOCKER_API_VERSION" |
|
| 336 |
+// ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized |
|
| 337 |
+// with a fixed version ([WithVersion]), no negotiation is performed. |
|
| 338 |
+// |
|
| 339 |
+// If the API server's ping response does not contain an API version, or if the |
|
| 340 |
+// client did not get a successful ping response, it assumes it is connected with |
|
| 341 |
+// an old daemon that does not support API version negotiation, in which case it |
|
| 342 |
+// downgrades to the latest version of the API before version negotiation was |
|
| 343 |
+// added (1.24). |
|
| 344 |
+func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
|
|
| 345 |
+ if !cli.manualOverride {
|
|
| 346 |
+ // Avoid concurrent modification of version-related fields |
|
| 347 |
+ cli.negotiateLock.Lock() |
|
| 348 |
+ defer cli.negotiateLock.Unlock() |
|
| 349 |
+ |
|
| 350 |
+ ping, err := cli.Ping(ctx) |
|
| 351 |
+ if err != nil {
|
|
| 352 |
+ // FIXME(thaJeztah): Ping returns an error when failing to connect to the API; we should not swallow the error here, and instead returning it. |
|
| 353 |
+ return |
|
| 354 |
+ } |
|
| 355 |
+ cli.negotiateAPIVersionPing(ping) |
|
| 356 |
+ } |
|
| 357 |
+} |
|
| 358 |
+ |
|
| 359 |
+// NegotiateAPIVersionPing downgrades the client's API version to match the |
|
| 360 |
+// APIVersion in the ping response. If the API version in pingResponse is higher |
|
| 361 |
+// than the maximum version supported by the client, it uses the client's maximum |
|
| 362 |
+// version. |
|
| 363 |
+// |
|
| 364 |
+// If a manual override is in place, either through the "DOCKER_API_VERSION" |
|
| 365 |
+// ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized |
|
| 366 |
+// with a fixed version ([WithVersion]), no negotiation is performed. |
|
| 367 |
+// |
|
| 368 |
+// If the API server's ping response does not contain an API version, we assume |
|
| 369 |
+// we are connected with an old daemon without API version negotiation support, |
|
| 370 |
+// and downgrade to the latest version of the API before version negotiation was |
|
| 371 |
+// added (1.24). |
|
| 372 |
+func (cli *Client) NegotiateAPIVersionPing(pingResponse types.Ping) {
|
|
| 373 |
+ if !cli.manualOverride {
|
|
| 374 |
+ // Avoid concurrent modification of version-related fields |
|
| 375 |
+ cli.negotiateLock.Lock() |
|
| 376 |
+ defer cli.negotiateLock.Unlock() |
|
| 377 |
+ |
|
| 378 |
+ cli.negotiateAPIVersionPing(pingResponse) |
|
| 379 |
+ } |
|
| 380 |
+} |
|
| 381 |
+ |
|
| 382 |
+// negotiateAPIVersionPing queries the API and updates the version to match the |
|
| 383 |
+// API version from the ping response. |
|
| 384 |
+func (cli *Client) negotiateAPIVersionPing(pingResponse types.Ping) {
|
|
| 385 |
+ // default to the latest version before versioning headers existed |
|
| 386 |
+ if pingResponse.APIVersion == "" {
|
|
| 387 |
+ pingResponse.APIVersion = fallbackAPIVersion |
|
| 388 |
+ } |
|
| 389 |
+ |
|
| 390 |
+ // if the client is not initialized with a version, start with the latest supported version |
|
| 391 |
+ if cli.version == "" {
|
|
| 392 |
+ cli.version = DefaultAPIVersion |
|
| 393 |
+ } |
|
| 394 |
+ |
|
| 395 |
+ // if server version is lower than the client version, downgrade |
|
| 396 |
+ if versions.LessThan(pingResponse.APIVersion, cli.version) {
|
|
| 397 |
+ cli.version = pingResponse.APIVersion |
|
| 398 |
+ } |
|
| 399 |
+ |
|
| 400 |
+ // Store the results, so that automatic API version negotiation (if enabled) |
|
| 401 |
+ // won't be performed on the next request. |
|
| 402 |
+ if cli.negotiateVersion {
|
|
| 403 |
+ cli.negotiated.Store(true) |
|
| 404 |
+ } |
|
| 405 |
+} |
|
| 406 |
+ |
|
| 407 |
+// DaemonHost returns the host address used by the client |
|
| 408 |
+func (cli *Client) DaemonHost() string {
|
|
| 409 |
+ return cli.host |
|
| 410 |
+} |
|
| 411 |
+ |
|
| 412 |
+// HTTPClient returns a copy of the HTTP client bound to the server |
|
| 413 |
+func (cli *Client) HTTPClient() *http.Client {
|
|
| 414 |
+ c := *cli.client |
|
| 415 |
+ return &c |
|
| 416 |
+} |
|
| 417 |
+ |
|
| 418 |
+// ParseHostURL parses a url string, validates the string is a host url, and |
|
| 419 |
+// returns the parsed URL |
|
| 420 |
+func ParseHostURL(host string) (*url.URL, error) {
|
|
| 421 |
+ proto, addr, ok := strings.Cut(host, "://") |
|
| 422 |
+ if !ok || addr == "" {
|
|
| 423 |
+ return nil, errors.Errorf("unable to parse docker host `%s`", host)
|
|
| 424 |
+ } |
|
| 425 |
+ |
|
| 426 |
+ var basePath string |
|
| 427 |
+ if proto == "tcp" {
|
|
| 428 |
+ parsed, err := url.Parse("tcp://" + addr)
|
|
| 429 |
+ if err != nil {
|
|
| 430 |
+ return nil, err |
|
| 431 |
+ } |
|
| 432 |
+ addr = parsed.Host |
|
| 433 |
+ basePath = parsed.Path |
|
| 434 |
+ } |
|
| 435 |
+ return &url.URL{
|
|
| 436 |
+ Scheme: proto, |
|
| 437 |
+ Host: addr, |
|
| 438 |
+ Path: basePath, |
|
| 439 |
+ }, nil |
|
| 440 |
+} |
|
| 441 |
+ |
|
| 442 |
+func (cli *Client) dialerFromTransport() func(context.Context, string, string) (net.Conn, error) {
|
|
| 443 |
+ if cli.baseTransport == nil || cli.baseTransport.DialContext == nil {
|
|
| 444 |
+ return nil |
|
| 445 |
+ } |
|
| 446 |
+ |
|
| 447 |
+ if cli.baseTransport.TLSClientConfig != nil {
|
|
| 448 |
+ // When using a tls config we don't use the configured dialer but instead a fallback dialer... |
|
| 449 |
+ // Note: It seems like this should use the normal dialer and wrap the returned net.Conn in a tls.Conn |
|
| 450 |
+ // I honestly don't know why it doesn't do that, but it doesn't and such a change is entirely unrelated to the change in this commit. |
|
| 451 |
+ return nil |
|
| 452 |
+ } |
|
| 453 |
+ return cli.baseTransport.DialContext |
|
| 454 |
+} |
|
| 455 |
+ |
|
| 456 |
+// Dialer returns a dialer for a raw stream connection, with an HTTP/1.1 header, |
|
| 457 |
+// that can be used for proxying the daemon connection. It is used by |
|
| 458 |
+// ["docker dial-stdio"]. |
|
| 459 |
+// |
|
| 460 |
+// ["docker dial-stdio"]: https://github.com/docker/cli/pull/1014 |
|
| 461 |
+func (cli *Client) Dialer() func(context.Context) (net.Conn, error) {
|
|
| 462 |
+ return cli.dialer() |
|
| 463 |
+} |
|
| 464 |
+ |
|
| 465 |
+func (cli *Client) dialer() func(context.Context) (net.Conn, error) {
|
|
| 466 |
+ return func(ctx context.Context) (net.Conn, error) {
|
|
| 467 |
+ if dialFn := cli.dialerFromTransport(); dialFn != nil {
|
|
| 468 |
+ return dialFn(ctx, cli.proto, cli.addr) |
|
| 469 |
+ } |
|
| 470 |
+ switch cli.proto {
|
|
| 471 |
+ case "unix": |
|
| 472 |
+ return net.Dial(cli.proto, cli.addr) |
|
| 473 |
+ case "npipe": |
|
| 474 |
+ ctx, cancel := context.WithTimeout(ctx, 32*time.Second) |
|
| 475 |
+ defer cancel() |
|
| 476 |
+ return dialPipeContext(ctx, cli.addr) |
|
| 477 |
+ default: |
|
| 478 |
+ if tlsConfig := cli.tlsConfig(); tlsConfig != nil {
|
|
| 479 |
+ return tls.Dial(cli.proto, cli.addr, tlsConfig) |
|
| 480 |
+ } |
|
| 481 |
+ return net.Dial(cli.proto, cli.addr) |
|
| 482 |
+ } |
|
| 483 |
+ } |
|
| 484 |
+} |
| 0 | 485 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,27 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import "net/http" |
|
| 3 |
+ |
|
| 4 |
+// NewClient initializes a new API client for the given host and API version. |
|
| 5 |
+// It uses the given http client as transport. |
|
| 6 |
+// It also initializes the custom http headers to add to each request. |
|
| 7 |
+// |
|
| 8 |
+// It won't send any version information if the version number is empty. It is |
|
| 9 |
+// highly recommended that you set a version or your client may break if the |
|
| 10 |
+// server is upgraded. |
|
| 11 |
+// |
|
| 12 |
+// Deprecated: use [NewClientWithOpts] passing the [WithHost], [WithVersion], |
|
| 13 |
+// [WithHTTPClient] and [WithHTTPHeaders] options. We recommend enabling API |
|
| 14 |
+// version negotiation by passing the [WithAPIVersionNegotiation] option instead |
|
| 15 |
+// of WithVersion. |
|
| 16 |
+func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string) (*Client, error) {
|
|
| 17 |
+ return NewClientWithOpts(WithHost(host), WithVersion(version), WithHTTPClient(client), WithHTTPHeaders(httpHeaders)) |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+// NewEnvClient initializes a new API client based on environment variables. |
|
| 21 |
+// See FromEnv for a list of support environment variables. |
|
| 22 |
+// |
|
| 23 |
+// Deprecated: use [NewClientWithOpts] passing the [FromEnv] option. |
|
| 24 |
+func NewEnvClient() (*Client, error) {
|
|
| 25 |
+ return NewClientWithOpts(FromEnv) |
|
| 26 |
+} |
| 0 | 27 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,237 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net" |
|
| 6 |
+ "net/http" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types" |
|
| 9 |
+ "github.com/moby/moby/api/types/build" |
|
| 10 |
+ "github.com/moby/moby/api/types/container" |
|
| 11 |
+ "github.com/moby/moby/api/types/events" |
|
| 12 |
+ "github.com/moby/moby/api/types/filters" |
|
| 13 |
+ "github.com/moby/moby/api/types/image" |
|
| 14 |
+ "github.com/moby/moby/api/types/network" |
|
| 15 |
+ "github.com/moby/moby/api/types/registry" |
|
| 16 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 17 |
+ "github.com/moby/moby/api/types/system" |
|
| 18 |
+ "github.com/moby/moby/api/types/volume" |
|
| 19 |
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
|
| 20 |
+) |
|
| 21 |
+ |
|
| 22 |
+// CommonAPIClient is the common methods between stable and experimental versions of APIClient. |
|
| 23 |
+// |
|
| 24 |
+// Deprecated: use [APIClient] instead. This type will be an alias for [APIClient] in the next release, and removed after. |
|
| 25 |
+type CommonAPIClient = stableAPIClient |
|
| 26 |
+ |
|
| 27 |
+// APIClient is an interface that clients that talk with a docker server must implement. |
|
| 28 |
+type APIClient interface {
|
|
| 29 |
+ stableAPIClient |
|
| 30 |
+ CheckpointAPIClient // CheckpointAPIClient is still experimental. |
|
| 31 |
+} |
|
| 32 |
+ |
|
| 33 |
+type stableAPIClient interface {
|
|
| 34 |
+ ConfigAPIClient |
|
| 35 |
+ ContainerAPIClient |
|
| 36 |
+ DistributionAPIClient |
|
| 37 |
+ ImageAPIClient |
|
| 38 |
+ NetworkAPIClient |
|
| 39 |
+ PluginAPIClient |
|
| 40 |
+ SystemAPIClient |
|
| 41 |
+ VolumeAPIClient |
|
| 42 |
+ ClientVersion() string |
|
| 43 |
+ DaemonHost() string |
|
| 44 |
+ HTTPClient() *http.Client |
|
| 45 |
+ ServerVersion(ctx context.Context) (types.Version, error) |
|
| 46 |
+ NegotiateAPIVersion(ctx context.Context) |
|
| 47 |
+ NegotiateAPIVersionPing(types.Ping) |
|
| 48 |
+ HijackDialer |
|
| 49 |
+ Dialer() func(context.Context) (net.Conn, error) |
|
| 50 |
+ Close() error |
|
| 51 |
+ SwarmManagementAPIClient |
|
| 52 |
+} |
|
| 53 |
+ |
|
| 54 |
+// SwarmManagementAPIClient defines all methods for managing Swarm-specific |
|
| 55 |
+// objects. |
|
| 56 |
+type SwarmManagementAPIClient interface {
|
|
| 57 |
+ SwarmAPIClient |
|
| 58 |
+ NodeAPIClient |
|
| 59 |
+ ServiceAPIClient |
|
| 60 |
+ SecretAPIClient |
|
| 61 |
+ ConfigAPIClient |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// HijackDialer defines methods for a hijack dialer. |
|
| 65 |
+type HijackDialer interface {
|
|
| 66 |
+ DialHijack(ctx context.Context, url, proto string, meta map[string][]string) (net.Conn, error) |
|
| 67 |
+} |
|
| 68 |
+ |
|
| 69 |
+// ContainerAPIClient defines API client methods for the containers |
|
| 70 |
+type ContainerAPIClient interface {
|
|
| 71 |
+ ContainerAttach(ctx context.Context, container string, options container.AttachOptions) (types.HijackedResponse, error) |
|
| 72 |
+ ContainerCommit(ctx context.Context, container string, options container.CommitOptions) (container.CommitResponse, error) |
|
| 73 |
+ ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) |
|
| 74 |
+ ContainerDiff(ctx context.Context, container string) ([]container.FilesystemChange, error) |
|
| 75 |
+ ContainerExecAttach(ctx context.Context, execID string, options container.ExecAttachOptions) (types.HijackedResponse, error) |
|
| 76 |
+ ContainerExecCreate(ctx context.Context, container string, options container.ExecOptions) (container.ExecCreateResponse, error) |
|
| 77 |
+ ContainerExecInspect(ctx context.Context, execID string) (container.ExecInspect, error) |
|
| 78 |
+ ContainerExecResize(ctx context.Context, execID string, options container.ResizeOptions) error |
|
| 79 |
+ ContainerExecStart(ctx context.Context, execID string, options container.ExecStartOptions) error |
|
| 80 |
+ ContainerExport(ctx context.Context, container string) (io.ReadCloser, error) |
|
| 81 |
+ ContainerInspect(ctx context.Context, container string) (container.InspectResponse, error) |
|
| 82 |
+ ContainerInspectWithRaw(ctx context.Context, container string, getSize bool) (container.InspectResponse, []byte, error) |
|
| 83 |
+ ContainerKill(ctx context.Context, container, signal string) error |
|
| 84 |
+ ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) |
|
| 85 |
+ ContainerLogs(ctx context.Context, container string, options container.LogsOptions) (io.ReadCloser, error) |
|
| 86 |
+ ContainerPause(ctx context.Context, container string) error |
|
| 87 |
+ ContainerRemove(ctx context.Context, container string, options container.RemoveOptions) error |
|
| 88 |
+ ContainerRename(ctx context.Context, container, newContainerName string) error |
|
| 89 |
+ ContainerResize(ctx context.Context, container string, options container.ResizeOptions) error |
|
| 90 |
+ ContainerRestart(ctx context.Context, container string, options container.StopOptions) error |
|
| 91 |
+ ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error) |
|
| 92 |
+ ContainerStats(ctx context.Context, container string, stream bool) (container.StatsResponseReader, error) |
|
| 93 |
+ ContainerStatsOneShot(ctx context.Context, container string) (container.StatsResponseReader, error) |
|
| 94 |
+ ContainerStart(ctx context.Context, container string, options container.StartOptions) error |
|
| 95 |
+ ContainerStop(ctx context.Context, container string, options container.StopOptions) error |
|
| 96 |
+ ContainerTop(ctx context.Context, container string, arguments []string) (container.TopResponse, error) |
|
| 97 |
+ ContainerUnpause(ctx context.Context, container string) error |
|
| 98 |
+ ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.UpdateResponse, error) |
|
| 99 |
+ ContainerWait(ctx context.Context, container string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) |
|
| 100 |
+ CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, container.PathStat, error) |
|
| 101 |
+ CopyToContainer(ctx context.Context, container, path string, content io.Reader, options container.CopyToContainerOptions) error |
|
| 102 |
+ ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) |
|
| 103 |
+} |
|
| 104 |
+ |
|
| 105 |
+// DistributionAPIClient defines API client methods for the registry |
|
| 106 |
+type DistributionAPIClient interface {
|
|
| 107 |
+ DistributionInspect(ctx context.Context, image, encodedRegistryAuth string) (registry.DistributionInspect, error) |
|
| 108 |
+} |
|
| 109 |
+ |
|
| 110 |
+// ImageAPIClient defines API client methods for the images |
|
| 111 |
+type ImageAPIClient interface {
|
|
| 112 |
+ ImageBuild(ctx context.Context, context io.Reader, options build.ImageBuildOptions) (build.ImageBuildResponse, error) |
|
| 113 |
+ BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) |
|
| 114 |
+ BuildCancel(ctx context.Context, id string) error |
|
| 115 |
+ ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) |
|
| 116 |
+ ImageImport(ctx context.Context, source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) |
|
| 117 |
+ |
|
| 118 |
+ ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error) |
|
| 119 |
+ ImagePull(ctx context.Context, ref string, options image.PullOptions) (io.ReadCloser, error) |
|
| 120 |
+ ImagePush(ctx context.Context, ref string, options image.PushOptions) (io.ReadCloser, error) |
|
| 121 |
+ ImageRemove(ctx context.Context, image string, options image.RemoveOptions) ([]image.DeleteResponse, error) |
|
| 122 |
+ ImageSearch(ctx context.Context, term string, options registry.SearchOptions) ([]registry.SearchResult, error) |
|
| 123 |
+ ImageTag(ctx context.Context, image, ref string) error |
|
| 124 |
+ ImagesPrune(ctx context.Context, pruneFilter filters.Args) (image.PruneReport, error) |
|
| 125 |
+ |
|
| 126 |
+ ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (image.InspectResponse, error) |
|
| 127 |
+ ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) ([]image.HistoryResponseItem, error) |
|
| 128 |
+ ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (image.LoadResponse, error) |
|
| 129 |
+ ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (io.ReadCloser, error) |
|
| 130 |
+ |
|
| 131 |
+ ImageAPIClientDeprecated |
|
| 132 |
+} |
|
| 133 |
+ |
|
| 134 |
+// ImageAPIClientDeprecated defines deprecated methods of the ImageAPIClient. |
|
| 135 |
+type ImageAPIClientDeprecated interface {
|
|
| 136 |
+ // ImageInspectWithRaw returns the image information and its raw representation. |
|
| 137 |
+ // |
|
| 138 |
+ // Deprecated: Use [Client.ImageInspect] instead. Raw response can be obtained using the [ImageInspectWithRawResponse] option. |
|
| 139 |
+ ImageInspectWithRaw(ctx context.Context, image string) (image.InspectResponse, []byte, error) |
|
| 140 |
+} |
|
| 141 |
+ |
|
| 142 |
+// NetworkAPIClient defines API client methods for the networks |
|
| 143 |
+type NetworkAPIClient interface {
|
|
| 144 |
+ NetworkConnect(ctx context.Context, network, container string, config *network.EndpointSettings) error |
|
| 145 |
+ NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) |
|
| 146 |
+ NetworkDisconnect(ctx context.Context, network, container string, force bool) error |
|
| 147 |
+ NetworkInspect(ctx context.Context, network string, options network.InspectOptions) (network.Inspect, error) |
|
| 148 |
+ NetworkInspectWithRaw(ctx context.Context, network string, options network.InspectOptions) (network.Inspect, []byte, error) |
|
| 149 |
+ NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) |
|
| 150 |
+ NetworkRemove(ctx context.Context, network string) error |
|
| 151 |
+ NetworksPrune(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error) |
|
| 152 |
+} |
|
| 153 |
+ |
|
| 154 |
+// NodeAPIClient defines API client methods for the nodes |
|
| 155 |
+type NodeAPIClient interface {
|
|
| 156 |
+ NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) |
|
| 157 |
+ NodeList(ctx context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) |
|
| 158 |
+ NodeRemove(ctx context.Context, nodeID string, options swarm.NodeRemoveOptions) error |
|
| 159 |
+ NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error |
|
| 160 |
+} |
|
| 161 |
+ |
|
| 162 |
+// PluginAPIClient defines API client methods for the plugins |
|
| 163 |
+type PluginAPIClient interface {
|
|
| 164 |
+ PluginList(ctx context.Context, filter filters.Args) (types.PluginsListResponse, error) |
|
| 165 |
+ PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error |
|
| 166 |
+ PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error |
|
| 167 |
+ PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error |
|
| 168 |
+ PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) |
|
| 169 |
+ PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) |
|
| 170 |
+ PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) |
|
| 171 |
+ PluginSet(ctx context.Context, name string, args []string) error |
|
| 172 |
+ PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) |
|
| 173 |
+ PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error |
|
| 174 |
+} |
|
| 175 |
+ |
|
| 176 |
+// ServiceAPIClient defines API client methods for the services |
|
| 177 |
+type ServiceAPIClient interface {
|
|
| 178 |
+ ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options swarm.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) |
|
| 179 |
+ ServiceInspectWithRaw(ctx context.Context, serviceID string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) |
|
| 180 |
+ ServiceList(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) |
|
| 181 |
+ ServiceRemove(ctx context.Context, serviceID string) error |
|
| 182 |
+ ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) |
|
| 183 |
+ ServiceLogs(ctx context.Context, serviceID string, options container.LogsOptions) (io.ReadCloser, error) |
|
| 184 |
+ TaskLogs(ctx context.Context, taskID string, options container.LogsOptions) (io.ReadCloser, error) |
|
| 185 |
+ TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) |
|
| 186 |
+ TaskList(ctx context.Context, options swarm.TaskListOptions) ([]swarm.Task, error) |
|
| 187 |
+} |
|
| 188 |
+ |
|
| 189 |
+// SwarmAPIClient defines API client methods for the swarm |
|
| 190 |
+type SwarmAPIClient interface {
|
|
| 191 |
+ SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) |
|
| 192 |
+ SwarmJoin(ctx context.Context, req swarm.JoinRequest) error |
|
| 193 |
+ SwarmGetUnlockKey(ctx context.Context) (swarm.UnlockKeyResponse, error) |
|
| 194 |
+ SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error |
|
| 195 |
+ SwarmLeave(ctx context.Context, force bool) error |
|
| 196 |
+ SwarmInspect(ctx context.Context) (swarm.Swarm, error) |
|
| 197 |
+ SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error |
|
| 198 |
+} |
|
| 199 |
+ |
|
| 200 |
+// SystemAPIClient defines API client methods for the system |
|
| 201 |
+type SystemAPIClient interface {
|
|
| 202 |
+ Events(ctx context.Context, options events.ListOptions) (<-chan events.Message, <-chan error) |
|
| 203 |
+ Info(ctx context.Context) (system.Info, error) |
|
| 204 |
+ RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) |
|
| 205 |
+ DiskUsage(ctx context.Context, options types.DiskUsageOptions) (types.DiskUsage, error) |
|
| 206 |
+ Ping(ctx context.Context) (types.Ping, error) |
|
| 207 |
+} |
|
| 208 |
+ |
|
| 209 |
+// VolumeAPIClient defines API client methods for the volumes |
|
| 210 |
+type VolumeAPIClient interface {
|
|
| 211 |
+ VolumeCreate(ctx context.Context, options volume.CreateOptions) (volume.Volume, error) |
|
| 212 |
+ VolumeInspect(ctx context.Context, volumeID string) (volume.Volume, error) |
|
| 213 |
+ VolumeInspectWithRaw(ctx context.Context, volumeID string) (volume.Volume, []byte, error) |
|
| 214 |
+ VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) |
|
| 215 |
+ VolumeRemove(ctx context.Context, volumeID string, force bool) error |
|
| 216 |
+ VolumesPrune(ctx context.Context, pruneFilter filters.Args) (volume.PruneReport, error) |
|
| 217 |
+ VolumeUpdate(ctx context.Context, volumeID string, version swarm.Version, options volume.UpdateOptions) error |
|
| 218 |
+} |
|
| 219 |
+ |
|
| 220 |
+// SecretAPIClient defines API client methods for secrets |
|
| 221 |
+type SecretAPIClient interface {
|
|
| 222 |
+ SecretList(ctx context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) |
|
| 223 |
+ SecretCreate(ctx context.Context, secret swarm.SecretSpec) (swarm.SecretCreateResponse, error) |
|
| 224 |
+ SecretRemove(ctx context.Context, id string) error |
|
| 225 |
+ SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) |
|
| 226 |
+ SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error |
|
| 227 |
+} |
|
| 228 |
+ |
|
| 229 |
+// ConfigAPIClient defines API client methods for configs |
|
| 230 |
+type ConfigAPIClient interface {
|
|
| 231 |
+ ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) |
|
| 232 |
+ ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) |
|
| 233 |
+ ConfigRemove(ctx context.Context, id string) error |
|
| 234 |
+ ConfigInspectWithRaw(ctx context.Context, name string) (swarm.Config, []byte, error) |
|
| 235 |
+ ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error |
|
| 236 |
+} |
| 0 | 237 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,18 @@ |
| 0 |
+//go:build !windows |
|
| 1 |
+ |
|
| 2 |
+package client |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "context" |
|
| 6 |
+ "net" |
|
| 7 |
+ "syscall" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// DefaultDockerHost defines OS-specific default host if the DOCKER_HOST |
|
| 11 |
+// (EnvOverrideHost) environment variable is unset or empty. |
|
| 12 |
+const DefaultDockerHost = "unix:///var/run/docker.sock" |
|
| 13 |
+ |
|
| 14 |
+// dialPipeContext connects to a Windows named pipe. It is not supported on non-Windows. |
|
| 15 |
+func dialPipeContext(_ context.Context, _ string) (net.Conn, error) {
|
|
| 16 |
+ return nil, syscall.EAFNOSUPPORT |
|
| 17 |
+} |
| 0 | 18 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,17 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/Microsoft/go-winio" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// DefaultDockerHost defines OS-specific default host if the DOCKER_HOST |
|
| 10 |
+// (EnvOverrideHost) environment variable is unset or empty. |
|
| 11 |
+const DefaultDockerHost = "npipe:////./pipe/docker_engine" |
|
| 12 |
+ |
|
| 13 |
+// dialPipeContext connects to a Windows named pipe. It is not supported on non-Windows. |
|
| 14 |
+func dialPipeContext(ctx context.Context, addr string) (net.Conn, error) {
|
|
| 15 |
+ return winio.DialPipeContext(ctx, addr) |
|
| 16 |
+} |
| 0 | 17 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,24 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// ConfigCreate creates a new config. |
|
| 10 |
+func (cli *Client) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
|
| 11 |
+ var response swarm.ConfigCreateResponse |
|
| 12 |
+ if err := cli.NewVersionError(ctx, "1.30", "config create"); err != nil {
|
|
| 13 |
+ return response, err |
|
| 14 |
+ } |
|
| 15 |
+ resp, err := cli.post(ctx, "/configs/create", nil, config, nil) |
|
| 16 |
+ defer ensureReaderClosed(resp) |
|
| 17 |
+ if err != nil {
|
|
| 18 |
+ return response, err |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 22 |
+ return response, err |
|
| 23 |
+} |
| 0 | 24 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,37 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "io" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// ConfigInspectWithRaw returns the config information with raw data |
|
| 12 |
+func (cli *Client) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
|
| 13 |
+ id, err := trimID("contig", id)
|
|
| 14 |
+ if err != nil {
|
|
| 15 |
+ return swarm.Config{}, nil, err
|
|
| 16 |
+ } |
|
| 17 |
+ if err := cli.NewVersionError(ctx, "1.30", "config inspect"); err != nil {
|
|
| 18 |
+ return swarm.Config{}, nil, err
|
|
| 19 |
+ } |
|
| 20 |
+ resp, err := cli.get(ctx, "/configs/"+id, nil, nil) |
|
| 21 |
+ defer ensureReaderClosed(resp) |
|
| 22 |
+ if err != nil {
|
|
| 23 |
+ return swarm.Config{}, nil, err
|
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ body, err := io.ReadAll(resp.Body) |
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ return swarm.Config{}, nil, err
|
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ var config swarm.Config |
|
| 32 |
+ rdr := bytes.NewReader(body) |
|
| 33 |
+ err = json.NewDecoder(rdr).Decode(&config) |
|
| 34 |
+ |
|
| 35 |
+ return config, body, err |
|
| 36 |
+} |
| 0 | 37 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,37 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/filters" |
|
| 8 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// ConfigList returns the list of configs. |
|
| 12 |
+func (cli *Client) ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
|
| 13 |
+ if err := cli.NewVersionError(ctx, "1.30", "config list"); err != nil {
|
|
| 14 |
+ return nil, err |
|
| 15 |
+ } |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ |
|
| 18 |
+ if options.Filters.Len() > 0 {
|
|
| 19 |
+ filterJSON, err := filters.ToJSON(options.Filters) |
|
| 20 |
+ if err != nil {
|
|
| 21 |
+ return nil, err |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ query.Set("filters", filterJSON)
|
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ resp, err := cli.get(ctx, "/configs", query, nil) |
|
| 28 |
+ defer ensureReaderClosed(resp) |
|
| 29 |
+ if err != nil {
|
|
| 30 |
+ return nil, err |
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ var configs []swarm.Config |
|
| 34 |
+ err = json.NewDecoder(resp.Body).Decode(&configs) |
|
| 35 |
+ return configs, err |
|
| 36 |
+} |
| 0 | 37 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,17 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import "context" |
|
| 3 |
+ |
|
| 4 |
+// ConfigRemove removes a config. |
|
| 5 |
+func (cli *Client) ConfigRemove(ctx context.Context, id string) error {
|
|
| 6 |
+ id, err := trimID("config", id)
|
|
| 7 |
+ if err != nil {
|
|
| 8 |
+ return err |
|
| 9 |
+ } |
|
| 10 |
+ if err := cli.NewVersionError(ctx, "1.30", "config remove"); err != nil {
|
|
| 11 |
+ return err |
|
| 12 |
+ } |
|
| 13 |
+ resp, err := cli.delete(ctx, "/configs/"+id, nil, nil) |
|
| 14 |
+ defer ensureReaderClosed(resp) |
|
| 15 |
+ return err |
|
| 16 |
+} |
| 0 | 17 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,24 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// ConfigUpdate attempts to update a config |
|
| 10 |
+func (cli *Client) ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error {
|
|
| 11 |
+ id, err := trimID("config", id)
|
|
| 12 |
+ if err != nil {
|
|
| 13 |
+ return err |
|
| 14 |
+ } |
|
| 15 |
+ if err := cli.NewVersionError(ctx, "1.30", "config update"); err != nil {
|
|
| 16 |
+ return err |
|
| 17 |
+ } |
|
| 18 |
+ query := url.Values{}
|
|
| 19 |
+ query.Set("version", version.String())
|
|
| 20 |
+ resp, err := cli.post(ctx, "/configs/"+id+"/update", query, config, nil) |
|
| 21 |
+ ensureReaderClosed(resp) |
|
| 22 |
+ return err |
|
| 23 |
+} |
| 0 | 24 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,65 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/http" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types" |
|
| 8 |
+ "github.com/moby/moby/api/types/container" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// ContainerAttach attaches a connection to a container in the server. |
|
| 12 |
+// It returns a types.HijackedConnection with the hijacked connection |
|
| 13 |
+// and the a reader to get output. It's up to the called to close |
|
| 14 |
+// the hijacked connection by calling types.HijackedResponse.Close. |
|
| 15 |
+// |
|
| 16 |
+// The stream format on the response will be in one of two formats: |
|
| 17 |
+// |
|
| 18 |
+// If the container is using a TTY, there is only a single stream (stdout), and |
|
| 19 |
+// data is copied directly from the container output stream, no extra |
|
| 20 |
+// multiplexing or headers. |
|
| 21 |
+// |
|
| 22 |
+// If the container is *not* using a TTY, streams for stdout and stderr are |
|
| 23 |
+// multiplexed. |
|
| 24 |
+// The format of the multiplexed stream is as follows: |
|
| 25 |
+// |
|
| 26 |
+// [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}[]byte{OUTPUT}
|
|
| 27 |
+// |
|
| 28 |
+// STREAM_TYPE can be 1 for stdout and 2 for stderr |
|
| 29 |
+// |
|
| 30 |
+// SIZE1, SIZE2, SIZE3, and SIZE4 are four bytes of uint32 encoded as big endian. |
|
| 31 |
+// This is the size of OUTPUT. |
|
| 32 |
+// |
|
| 33 |
+// You can use github.com/docker/docker/pkg/stdcopy.StdCopy to demultiplex this |
|
| 34 |
+// stream. |
|
| 35 |
+func (cli *Client) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
|
| 36 |
+ containerID, err := trimID("container", containerID)
|
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ return types.HijackedResponse{}, err
|
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ query := url.Values{}
|
|
| 42 |
+ if options.Stream {
|
|
| 43 |
+ query.Set("stream", "1")
|
|
| 44 |
+ } |
|
| 45 |
+ if options.Stdin {
|
|
| 46 |
+ query.Set("stdin", "1")
|
|
| 47 |
+ } |
|
| 48 |
+ if options.Stdout {
|
|
| 49 |
+ query.Set("stdout", "1")
|
|
| 50 |
+ } |
|
| 51 |
+ if options.Stderr {
|
|
| 52 |
+ query.Set("stderr", "1")
|
|
| 53 |
+ } |
|
| 54 |
+ if options.DetachKeys != "" {
|
|
| 55 |
+ query.Set("detachKeys", options.DetachKeys)
|
|
| 56 |
+ } |
|
| 57 |
+ if options.Logs {
|
|
| 58 |
+ query.Set("logs", "1")
|
|
| 59 |
+ } |
|
| 60 |
+ |
|
| 61 |
+ return cli.postHijacked(ctx, "/containers/"+containerID+"/attach", query, nil, http.Header{
|
|
| 62 |
+ "Content-Type": {"text/plain"},
|
|
| 63 |
+ }) |
|
| 64 |
+} |
| 0 | 65 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,60 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "errors" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/distribution/reference" |
|
| 9 |
+ "github.com/moby/moby/api/types/container" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// ContainerCommit applies changes to a container and creates a new tagged image. |
|
| 13 |
+func (cli *Client) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (container.CommitResponse, error) {
|
|
| 14 |
+ containerID, err := trimID("container", containerID)
|
|
| 15 |
+ if err != nil {
|
|
| 16 |
+ return container.CommitResponse{}, err
|
|
| 17 |
+ } |
|
| 18 |
+ |
|
| 19 |
+ var repository, tag string |
|
| 20 |
+ if options.Reference != "" {
|
|
| 21 |
+ ref, err := reference.ParseNormalizedNamed(options.Reference) |
|
| 22 |
+ if err != nil {
|
|
| 23 |
+ return container.CommitResponse{}, err
|
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ if _, isCanonical := ref.(reference.Canonical); isCanonical {
|
|
| 27 |
+ return container.CommitResponse{}, errors.New("refusing to create a tag with a digest reference")
|
|
| 28 |
+ } |
|
| 29 |
+ ref = reference.TagNameOnly(ref) |
|
| 30 |
+ |
|
| 31 |
+ if tagged, ok := ref.(reference.Tagged); ok {
|
|
| 32 |
+ tag = tagged.Tag() |
|
| 33 |
+ } |
|
| 34 |
+ repository = ref.Name() |
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ query := url.Values{}
|
|
| 38 |
+ query.Set("container", containerID)
|
|
| 39 |
+ query.Set("repo", repository)
|
|
| 40 |
+ query.Set("tag", tag)
|
|
| 41 |
+ query.Set("comment", options.Comment)
|
|
| 42 |
+ query.Set("author", options.Author)
|
|
| 43 |
+ for _, change := range options.Changes {
|
|
| 44 |
+ query.Add("changes", change)
|
|
| 45 |
+ } |
|
| 46 |
+ if !options.Pause {
|
|
| 47 |
+ query.Set("pause", "0")
|
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ var response container.CommitResponse |
|
| 51 |
+ resp, err := cli.post(ctx, "/commit", query, options.Config, nil) |
|
| 52 |
+ defer ensureReaderClosed(resp) |
|
| 53 |
+ if err != nil {
|
|
| 54 |
+ return response, err |
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 58 |
+ return response, err |
|
| 59 |
+} |
| 0 | 60 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,104 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/base64" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "fmt" |
|
| 7 |
+ "io" |
|
| 8 |
+ "net/http" |
|
| 9 |
+ "net/url" |
|
| 10 |
+ "path/filepath" |
|
| 11 |
+ "strings" |
|
| 12 |
+ |
|
| 13 |
+ "github.com/moby/moby/api/types/container" |
|
| 14 |
+) |
|
| 15 |
+ |
|
| 16 |
+// ContainerStatPath returns stat information about a path inside the container filesystem. |
|
| 17 |
+func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) {
|
|
| 18 |
+ containerID, err := trimID("container", containerID)
|
|
| 19 |
+ if err != nil {
|
|
| 20 |
+ return container.PathStat{}, err
|
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ query := url.Values{}
|
|
| 24 |
+ query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
|
|
| 25 |
+ |
|
| 26 |
+ resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil) |
|
| 27 |
+ defer ensureReaderClosed(resp) |
|
| 28 |
+ if err != nil {
|
|
| 29 |
+ return container.PathStat{}, err
|
|
| 30 |
+ } |
|
| 31 |
+ return getContainerPathStatFromHeader(resp.Header) |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+// CopyToContainer copies content into the container filesystem. |
|
| 35 |
+// Note that `content` must be a Reader for a TAR archive |
|
| 36 |
+func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options container.CopyToContainerOptions) error {
|
|
| 37 |
+ containerID, err := trimID("container", containerID)
|
|
| 38 |
+ if err != nil {
|
|
| 39 |
+ return err |
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ query := url.Values{}
|
|
| 43 |
+ query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
|
|
| 44 |
+ // Do not allow for an existing directory to be overwritten by a non-directory and vice versa. |
|
| 45 |
+ if !options.AllowOverwriteDirWithFile {
|
|
| 46 |
+ query.Set("noOverwriteDirNonDir", "true")
|
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ if options.CopyUIDGID {
|
|
| 50 |
+ query.Set("copyUIDGID", "true")
|
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, content, nil) |
|
| 54 |
+ defer ensureReaderClosed(response) |
|
| 55 |
+ if err != nil {
|
|
| 56 |
+ return err |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ return nil |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+// CopyFromContainer gets the content from the container and returns it as a Reader |
|
| 63 |
+// for a TAR archive to manipulate it in the host. It's up to the caller to close the reader. |
|
| 64 |
+func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
|
| 65 |
+ containerID, err := trimID("container", containerID)
|
|
| 66 |
+ if err != nil {
|
|
| 67 |
+ return nil, container.PathStat{}, err
|
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 70 |
+ query := make(url.Values, 1) |
|
| 71 |
+ query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
|
|
| 72 |
+ |
|
| 73 |
+ resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil) |
|
| 74 |
+ if err != nil {
|
|
| 75 |
+ return nil, container.PathStat{}, err
|
|
| 76 |
+ } |
|
| 77 |
+ |
|
| 78 |
+ // In order to get the copy behavior right, we need to know information |
|
| 79 |
+ // about both the source and the destination. The response headers include |
|
| 80 |
+ // stat info about the source that we can use in deciding exactly how to |
|
| 81 |
+ // copy it locally. Along with the stat info about the local destination, |
|
| 82 |
+ // we have everything we need to handle the multiple possibilities there |
|
| 83 |
+ // can be when copying a file/dir from one location to another file/dir. |
|
| 84 |
+ stat, err := getContainerPathStatFromHeader(resp.Header) |
|
| 85 |
+ if err != nil {
|
|
| 86 |
+ return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
|
|
| 87 |
+ } |
|
| 88 |
+ return resp.Body, stat, err |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) {
|
|
| 92 |
+ var stat container.PathStat |
|
| 93 |
+ |
|
| 94 |
+ encodedStat := header.Get("X-Docker-Container-Path-Stat")
|
|
| 95 |
+ statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat)) |
|
| 96 |
+ |
|
| 97 |
+ err := json.NewDecoder(statDecoder).Decode(&stat) |
|
| 98 |
+ if err != nil {
|
|
| 99 |
+ err = fmt.Errorf("unable to decode container path stat header: %s", err)
|
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ return stat, err |
|
| 103 |
+} |
| 0 | 104 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,168 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "errors" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ "path" |
|
| 8 |
+ "sort" |
|
| 9 |
+ "strings" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/moby/moby/api/types/container" |
|
| 12 |
+ "github.com/moby/moby/api/types/network" |
|
| 13 |
+ "github.com/moby/moby/api/types/versions" |
|
| 14 |
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+// ContainerCreate creates a new container based on the given configuration. |
|
| 18 |
+// It can be associated with a name, but it's not mandatory. |
|
| 19 |
+func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
|
|
| 20 |
+ var response container.CreateResponse |
|
| 21 |
+ |
|
| 22 |
+ // Make sure we negotiated (if the client is configured to do so), |
|
| 23 |
+ // as code below contains API-version specific handling of options. |
|
| 24 |
+ // |
|
| 25 |
+ // Normally, version-negotiation (if enabled) would not happen until |
|
| 26 |
+ // the API request is made. |
|
| 27 |
+ if err := cli.checkVersion(ctx); err != nil {
|
|
| 28 |
+ return response, err |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ if err := cli.NewVersionError(ctx, "1.25", "stop timeout"); config != nil && config.StopTimeout != nil && err != nil {
|
|
| 32 |
+ return response, err |
|
| 33 |
+ } |
|
| 34 |
+ if err := cli.NewVersionError(ctx, "1.41", "specify container image platform"); platform != nil && err != nil {
|
|
| 35 |
+ return response, err |
|
| 36 |
+ } |
|
| 37 |
+ if err := cli.NewVersionError(ctx, "1.44", "specify health-check start interval"); config != nil && config.Healthcheck != nil && config.Healthcheck.StartInterval != 0 && err != nil {
|
|
| 38 |
+ return response, err |
|
| 39 |
+ } |
|
| 40 |
+ if err := cli.NewVersionError(ctx, "1.44", "specify mac-address per network"); hasEndpointSpecificMacAddress(networkingConfig) && err != nil {
|
|
| 41 |
+ return response, err |
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ if hostConfig != nil {
|
|
| 45 |
+ if versions.LessThan(cli.ClientVersion(), "1.25") {
|
|
| 46 |
+ // When using API 1.24 and under, the client is responsible for removing the container |
|
| 47 |
+ hostConfig.AutoRemove = false |
|
| 48 |
+ } |
|
| 49 |
+ if versions.GreaterThanOrEqualTo(cli.ClientVersion(), "1.42") || versions.LessThan(cli.ClientVersion(), "1.40") {
|
|
| 50 |
+ // KernelMemory was added in API 1.40, and deprecated in API 1.42 |
|
| 51 |
+ hostConfig.KernelMemory = 0 |
|
| 52 |
+ } |
|
| 53 |
+ if platform != nil && platform.OS == "linux" && versions.LessThan(cli.ClientVersion(), "1.42") {
|
|
| 54 |
+ // When using API under 1.42, the Linux daemon doesn't respect the ConsoleSize |
|
| 55 |
+ hostConfig.ConsoleSize = [2]uint{0, 0}
|
|
| 56 |
+ } |
|
| 57 |
+ if versions.LessThan(cli.ClientVersion(), "1.44") {
|
|
| 58 |
+ for _, m := range hostConfig.Mounts {
|
|
| 59 |
+ if m.BindOptions != nil {
|
|
| 60 |
+ // ReadOnlyNonRecursive can be safely ignored when API < 1.44 |
|
| 61 |
+ if m.BindOptions.ReadOnlyForceRecursive {
|
|
| 62 |
+ return response, errors.New("bind-recursive=readonly requires API v1.44 or later")
|
|
| 63 |
+ } |
|
| 64 |
+ if m.BindOptions.NonRecursive && versions.LessThan(cli.ClientVersion(), "1.40") {
|
|
| 65 |
+ return response, errors.New("bind-recursive=disabled requires API v1.40 or later")
|
|
| 66 |
+ } |
|
| 67 |
+ } |
|
| 68 |
+ } |
|
| 69 |
+ } |
|
| 70 |
+ |
|
| 71 |
+ hostConfig.CapAdd = normalizeCapabilities(hostConfig.CapAdd) |
|
| 72 |
+ hostConfig.CapDrop = normalizeCapabilities(hostConfig.CapDrop) |
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ // Since API 1.44, the container-wide MacAddress is deprecated and will trigger a WARNING if it's specified. |
|
| 76 |
+ if versions.GreaterThanOrEqualTo(cli.ClientVersion(), "1.44") {
|
|
| 77 |
+ config.MacAddress = "" //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44. |
|
| 78 |
+ } |
|
| 79 |
+ |
|
| 80 |
+ query := url.Values{}
|
|
| 81 |
+ if p := formatPlatform(platform); p != "" {
|
|
| 82 |
+ query.Set("platform", p)
|
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ if containerName != "" {
|
|
| 86 |
+ query.Set("name", containerName)
|
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ body := container.CreateRequest{
|
|
| 90 |
+ Config: config, |
|
| 91 |
+ HostConfig: hostConfig, |
|
| 92 |
+ NetworkingConfig: networkingConfig, |
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ resp, err := cli.post(ctx, "/containers/create", query, body, nil) |
|
| 96 |
+ defer ensureReaderClosed(resp) |
|
| 97 |
+ if err != nil {
|
|
| 98 |
+ return response, err |
|
| 99 |
+ } |
|
| 100 |
+ |
|
| 101 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 102 |
+ return response, err |
|
| 103 |
+} |
|
| 104 |
+ |
|
| 105 |
+// formatPlatform returns a formatted string representing platform (e.g. linux/arm/v7). |
|
| 106 |
+// |
|
| 107 |
+// Similar to containerd's platforms.Format(), but does allow components to be |
|
| 108 |
+// omitted (e.g. pass "architecture" only, without "os": |
|
| 109 |
+// https://github.com/containerd/containerd/blob/v1.5.2/platforms/platforms.go#L243-L263 |
|
| 110 |
+func formatPlatform(platform *ocispec.Platform) string {
|
|
| 111 |
+ if platform == nil {
|
|
| 112 |
+ return "" |
|
| 113 |
+ } |
|
| 114 |
+ return path.Join(platform.OS, platform.Architecture, platform.Variant) |
|
| 115 |
+} |
|
| 116 |
+ |
|
| 117 |
+// hasEndpointSpecificMacAddress checks whether one of the endpoint in networkingConfig has a MacAddress defined. |
|
| 118 |
+func hasEndpointSpecificMacAddress(networkingConfig *network.NetworkingConfig) bool {
|
|
| 119 |
+ if networkingConfig == nil {
|
|
| 120 |
+ return false |
|
| 121 |
+ } |
|
| 122 |
+ for _, endpoint := range networkingConfig.EndpointsConfig {
|
|
| 123 |
+ if endpoint.MacAddress != "" {
|
|
| 124 |
+ return true |
|
| 125 |
+ } |
|
| 126 |
+ } |
|
| 127 |
+ return false |
|
| 128 |
+} |
|
| 129 |
+ |
|
| 130 |
+// allCapabilities is a magic value for "all capabilities" |
|
| 131 |
+const allCapabilities = "ALL" |
|
| 132 |
+ |
|
| 133 |
+// normalizeCapabilities normalizes capabilities to their canonical form, |
|
| 134 |
+// removes duplicates, and sorts the results. |
|
| 135 |
+// |
|
| 136 |
+// It is similar to [github.com/docker/docker/oci/caps.NormalizeLegacyCapabilities], |
|
| 137 |
+// but performs no validation based on supported capabilities. |
|
| 138 |
+func normalizeCapabilities(caps []string) []string {
|
|
| 139 |
+ var normalized []string |
|
| 140 |
+ |
|
| 141 |
+ unique := make(map[string]struct{})
|
|
| 142 |
+ for _, c := range caps {
|
|
| 143 |
+ c = normalizeCap(c) |
|
| 144 |
+ if _, ok := unique[c]; ok {
|
|
| 145 |
+ continue |
|
| 146 |
+ } |
|
| 147 |
+ unique[c] = struct{}{}
|
|
| 148 |
+ normalized = append(normalized, c) |
|
| 149 |
+ } |
|
| 150 |
+ |
|
| 151 |
+ sort.Strings(normalized) |
|
| 152 |
+ return normalized |
|
| 153 |
+} |
|
| 154 |
+ |
|
| 155 |
+// normalizeCap normalizes a capability to its canonical format by upper-casing |
|
| 156 |
+// and adding a "CAP_" prefix (if not yet present). It also accepts the "ALL" |
|
| 157 |
+// magic-value. |
|
| 158 |
+func normalizeCap(capability string) string {
|
|
| 159 |
+ capability = strings.ToUpper(capability) |
|
| 160 |
+ if capability == allCapabilities {
|
|
| 161 |
+ return capability |
|
| 162 |
+ } |
|
| 163 |
+ if !strings.HasPrefix(capability, "CAP_") {
|
|
| 164 |
+ capability = "CAP_" + capability |
|
| 165 |
+ } |
|
| 166 |
+ return capability |
|
| 167 |
+} |
| 0 | 168 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,30 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/container" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// ContainerDiff shows differences in a container filesystem since it was started. |
|
| 11 |
+func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
|
|
| 12 |
+ containerID, err := trimID("container", containerID)
|
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return nil, err |
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ resp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil)
|
|
| 18 |
+ defer ensureReaderClosed(resp) |
|
| 19 |
+ if err != nil {
|
|
| 20 |
+ return nil, err |
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ var changes []container.FilesystemChange |
|
| 24 |
+ err = json.NewDecoder(resp.Body).Decode(&changes) |
|
| 25 |
+ if err != nil {
|
|
| 26 |
+ return nil, err |
|
| 27 |
+ } |
|
| 28 |
+ return changes, err |
|
| 29 |
+} |
| 0 | 30 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,94 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/http" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types" |
|
| 8 |
+ "github.com/moby/moby/api/types/container" |
|
| 9 |
+ "github.com/moby/moby/api/types/versions" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// ContainerExecCreate creates a new exec configuration to run an exec process. |
|
| 13 |
+func (cli *Client) ContainerExecCreate(ctx context.Context, containerID string, options container.ExecOptions) (container.ExecCreateResponse, error) {
|
|
| 14 |
+ containerID, err := trimID("container", containerID)
|
|
| 15 |
+ if err != nil {
|
|
| 16 |
+ return container.ExecCreateResponse{}, err
|
|
| 17 |
+ } |
|
| 18 |
+ |
|
| 19 |
+ // Make sure we negotiated (if the client is configured to do so), |
|
| 20 |
+ // as code below contains API-version specific handling of options. |
|
| 21 |
+ // |
|
| 22 |
+ // Normally, version-negotiation (if enabled) would not happen until |
|
| 23 |
+ // the API request is made. |
|
| 24 |
+ if err := cli.checkVersion(ctx); err != nil {
|
|
| 25 |
+ return container.ExecCreateResponse{}, err
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ if err := cli.NewVersionError(ctx, "1.25", "env"); len(options.Env) != 0 && err != nil {
|
|
| 29 |
+ return container.ExecCreateResponse{}, err
|
|
| 30 |
+ } |
|
| 31 |
+ if versions.LessThan(cli.ClientVersion(), "1.42") {
|
|
| 32 |
+ options.ConsoleSize = nil |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/exec", nil, options, nil) |
|
| 36 |
+ defer ensureReaderClosed(resp) |
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ return container.ExecCreateResponse{}, err
|
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ var response container.ExecCreateResponse |
|
| 42 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 43 |
+ return response, err |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+// ContainerExecStart starts an exec process already created in the docker host. |
|
| 47 |
+func (cli *Client) ContainerExecStart(ctx context.Context, execID string, config container.ExecStartOptions) error {
|
|
| 48 |
+ if versions.LessThan(cli.ClientVersion(), "1.42") {
|
|
| 49 |
+ config.ConsoleSize = nil |
|
| 50 |
+ } |
|
| 51 |
+ resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, config, nil) |
|
| 52 |
+ ensureReaderClosed(resp) |
|
| 53 |
+ return err |
|
| 54 |
+} |
|
| 55 |
+ |
|
| 56 |
+// ContainerExecAttach attaches a connection to an exec process in the server. |
|
| 57 |
+// |
|
| 58 |
+// It returns a [types.HijackedResponse] with the hijacked connection |
|
| 59 |
+// and the a reader to get output. It's up to the called to close |
|
| 60 |
+// the hijacked connection by calling [types.HijackedResponse.Close]. |
|
| 61 |
+// |
|
| 62 |
+// The stream format on the response uses one of two formats: |
|
| 63 |
+// |
|
| 64 |
+// - If the container is using a TTY, there is only a single stream (stdout), and |
|
| 65 |
+// data is copied directly from the container output stream, no extra |
|
| 66 |
+// multiplexing or headers. |
|
| 67 |
+// - If the container is *not* using a TTY, streams for stdout and stderr are |
|
| 68 |
+// multiplexed. |
|
| 69 |
+// |
|
| 70 |
+// You can use [github.com/docker/docker/pkg/stdcopy.StdCopy] to demultiplex this |
|
| 71 |
+// stream. Refer to [Client.ContainerAttach] for details about the multiplexed |
|
| 72 |
+// stream. |
|
| 73 |
+func (cli *Client) ContainerExecAttach(ctx context.Context, execID string, config container.ExecAttachOptions) (types.HijackedResponse, error) {
|
|
| 74 |
+ if versions.LessThan(cli.ClientVersion(), "1.42") {
|
|
| 75 |
+ config.ConsoleSize = nil |
|
| 76 |
+ } |
|
| 77 |
+ return cli.postHijacked(ctx, "/exec/"+execID+"/start", nil, config, http.Header{
|
|
| 78 |
+ "Content-Type": {"application/json"},
|
|
| 79 |
+ }) |
|
| 80 |
+} |
|
| 81 |
+ |
|
| 82 |
+// ContainerExecInspect returns information about a specific exec process on the docker host. |
|
| 83 |
+func (cli *Client) ContainerExecInspect(ctx context.Context, execID string) (container.ExecInspect, error) {
|
|
| 84 |
+ var response container.ExecInspect |
|
| 85 |
+ resp, err := cli.get(ctx, "/exec/"+execID+"/json", nil, nil) |
|
| 86 |
+ if err != nil {
|
|
| 87 |
+ return response, err |
|
| 88 |
+ } |
|
| 89 |
+ |
|
| 90 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 91 |
+ ensureReaderClosed(resp) |
|
| 92 |
+ return response, err |
|
| 93 |
+} |
| 0 | 94 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,24 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/url" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// ContainerExport retrieves the raw contents of a container |
|
| 9 |
+// and returns them as an io.ReadCloser. It's up to the caller |
|
| 10 |
+// to close the stream. |
|
| 11 |
+func (cli *Client) ContainerExport(ctx context.Context, containerID string) (io.ReadCloser, error) {
|
|
| 12 |
+ containerID, err := trimID("container", containerID)
|
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return nil, err |
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ resp, err := cli.get(ctx, "/containers/"+containerID+"/export", url.Values{}, nil)
|
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return nil, err |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ return resp.Body, nil |
|
| 23 |
+} |
| 0 | 24 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,57 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "io" |
|
| 7 |
+ "net/url" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/moby/moby/api/types/container" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// ContainerInspect returns the container information. |
|
| 13 |
+func (cli *Client) ContainerInspect(ctx context.Context, containerID string) (container.InspectResponse, error) {
|
|
| 14 |
+ containerID, err := trimID("container", containerID)
|
|
| 15 |
+ if err != nil {
|
|
| 16 |
+ return container.InspectResponse{}, err
|
|
| 17 |
+ } |
|
| 18 |
+ |
|
| 19 |
+ resp, err := cli.get(ctx, "/containers/"+containerID+"/json", nil, nil) |
|
| 20 |
+ defer ensureReaderClosed(resp) |
|
| 21 |
+ if err != nil {
|
|
| 22 |
+ return container.InspectResponse{}, err
|
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ var response container.InspectResponse |
|
| 26 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 27 |
+ return response, err |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+// ContainerInspectWithRaw returns the container information and its raw representation. |
|
| 31 |
+func (cli *Client) ContainerInspectWithRaw(ctx context.Context, containerID string, getSize bool) (container.InspectResponse, []byte, error) {
|
|
| 32 |
+ containerID, err := trimID("container", containerID)
|
|
| 33 |
+ if err != nil {
|
|
| 34 |
+ return container.InspectResponse{}, nil, err
|
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ query := url.Values{}
|
|
| 38 |
+ if getSize {
|
|
| 39 |
+ query.Set("size", "1")
|
|
| 40 |
+ } |
|
| 41 |
+ resp, err := cli.get(ctx, "/containers/"+containerID+"/json", query, nil) |
|
| 42 |
+ defer ensureReaderClosed(resp) |
|
| 43 |
+ if err != nil {
|
|
| 44 |
+ return container.InspectResponse{}, nil, err
|
|
| 45 |
+ } |
|
| 46 |
+ |
|
| 47 |
+ body, err := io.ReadAll(resp.Body) |
|
| 48 |
+ if err != nil {
|
|
| 49 |
+ return container.InspectResponse{}, nil, err
|
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ var response container.InspectResponse |
|
| 53 |
+ rdr := bytes.NewReader(body) |
|
| 54 |
+ err = json.NewDecoder(rdr).Decode(&response) |
|
| 55 |
+ return response, body, err |
|
| 56 |
+} |
| 0 | 57 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,23 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+// ContainerKill terminates the container process but does not remove the container from the docker host. |
|
| 8 |
+func (cli *Client) ContainerKill(ctx context.Context, containerID, signal string) error {
|
|
| 9 |
+ containerID, err := trimID("container", containerID)
|
|
| 10 |
+ if err != nil {
|
|
| 11 |
+ return err |
|
| 12 |
+ } |
|
| 13 |
+ |
|
| 14 |
+ query := url.Values{}
|
|
| 15 |
+ if signal != "" {
|
|
| 16 |
+ query.Set("signal", signal)
|
|
| 17 |
+ } |
|
| 18 |
+ |
|
| 19 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/kill", query, nil, nil) |
|
| 20 |
+ ensureReaderClosed(resp) |
|
| 21 |
+ return err |
|
| 22 |
+} |
| 0 | 23 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,56 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ "strconv" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/container" |
|
| 9 |
+ "github.com/moby/moby/api/types/filters" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// ContainerList returns the list of containers in the docker host. |
|
| 13 |
+func (cli *Client) ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) {
|
|
| 14 |
+ query := url.Values{}
|
|
| 15 |
+ |
|
| 16 |
+ if options.All {
|
|
| 17 |
+ query.Set("all", "1")
|
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ if options.Limit > 0 {
|
|
| 21 |
+ query.Set("limit", strconv.Itoa(options.Limit))
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ if options.Since != "" {
|
|
| 25 |
+ query.Set("since", options.Since)
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ if options.Before != "" {
|
|
| 29 |
+ query.Set("before", options.Before)
|
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ if options.Size {
|
|
| 33 |
+ query.Set("size", "1")
|
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ if options.Filters.Len() > 0 {
|
|
| 37 |
+ //nolint:staticcheck // ignore SA1019 for old code |
|
| 38 |
+ filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return nil, err |
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ query.Set("filters", filterJSON)
|
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ resp, err := cli.get(ctx, "/containers/json", query, nil) |
|
| 47 |
+ defer ensureReaderClosed(resp) |
|
| 48 |
+ if err != nil {
|
|
| 49 |
+ return nil, err |
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ var containers []container.Summary |
|
| 53 |
+ err = json.NewDecoder(resp.Body).Decode(&containers) |
|
| 54 |
+ return containers, err |
|
| 55 |
+} |
| 0 | 56 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,85 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ "time" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/container" |
|
| 9 |
+ timetypes "github.com/moby/moby/api/types/time" |
|
| 10 |
+ "github.com/pkg/errors" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// ContainerLogs returns the logs generated by a container in an io.ReadCloser. |
|
| 14 |
+// It's up to the caller to close the stream. |
|
| 15 |
+// |
|
| 16 |
+// The stream format on the response will be in one of two formats: |
|
| 17 |
+// |
|
| 18 |
+// If the container is using a TTY, there is only a single stream (stdout), and |
|
| 19 |
+// data is copied directly from the container output stream, no extra |
|
| 20 |
+// multiplexing or headers. |
|
| 21 |
+// |
|
| 22 |
+// If the container is *not* using a TTY, streams for stdout and stderr are |
|
| 23 |
+// multiplexed. |
|
| 24 |
+// The format of the multiplexed stream is as follows: |
|
| 25 |
+// |
|
| 26 |
+// [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}[]byte{OUTPUT}
|
|
| 27 |
+// |
|
| 28 |
+// STREAM_TYPE can be 1 for stdout and 2 for stderr |
|
| 29 |
+// |
|
| 30 |
+// SIZE1, SIZE2, SIZE3, and SIZE4 are four bytes of uint32 encoded as big endian. |
|
| 31 |
+// This is the size of OUTPUT. |
|
| 32 |
+// |
|
| 33 |
+// You can use github.com/docker/docker/pkg/stdcopy.StdCopy to demultiplex this |
|
| 34 |
+// stream. |
|
| 35 |
+func (cli *Client) ContainerLogs(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
|
|
| 36 |
+ containerID, err := trimID("container", containerID)
|
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ return nil, err |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ query := url.Values{}
|
|
| 42 |
+ if options.ShowStdout {
|
|
| 43 |
+ query.Set("stdout", "1")
|
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ if options.ShowStderr {
|
|
| 47 |
+ query.Set("stderr", "1")
|
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ if options.Since != "" {
|
|
| 51 |
+ ts, err := timetypes.GetTimestamp(options.Since, time.Now()) |
|
| 52 |
+ if err != nil {
|
|
| 53 |
+ return nil, errors.Wrap(err, `invalid value for "since"`) |
|
| 54 |
+ } |
|
| 55 |
+ query.Set("since", ts)
|
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ if options.Until != "" {
|
|
| 59 |
+ ts, err := timetypes.GetTimestamp(options.Until, time.Now()) |
|
| 60 |
+ if err != nil {
|
|
| 61 |
+ return nil, errors.Wrap(err, `invalid value for "until"`) |
|
| 62 |
+ } |
|
| 63 |
+ query.Set("until", ts)
|
|
| 64 |
+ } |
|
| 65 |
+ |
|
| 66 |
+ if options.Timestamps {
|
|
| 67 |
+ query.Set("timestamps", "1")
|
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 70 |
+ if options.Details {
|
|
| 71 |
+ query.Set("details", "1")
|
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ if options.Follow {
|
|
| 75 |
+ query.Set("follow", "1")
|
|
| 76 |
+ } |
|
| 77 |
+ query.Set("tail", options.Tail)
|
|
| 78 |
+ |
|
| 79 |
+ resp, err := cli.get(ctx, "/containers/"+containerID+"/logs", query, nil) |
|
| 80 |
+ if err != nil {
|
|
| 81 |
+ return nil, err |
|
| 82 |
+ } |
|
| 83 |
+ return resp.Body, nil |
|
| 84 |
+} |
| 0 | 85 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,15 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import "context" |
|
| 3 |
+ |
|
| 4 |
+// ContainerPause pauses the main process of a given container without terminating it. |
|
| 5 |
+func (cli *Client) ContainerPause(ctx context.Context, containerID string) error {
|
|
| 6 |
+ containerID, err := trimID("container", containerID)
|
|
| 7 |
+ if err != nil {
|
|
| 8 |
+ return err |
|
| 9 |
+ } |
|
| 10 |
+ |
|
| 11 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/pause", nil, nil, nil) |
|
| 12 |
+ ensureReaderClosed(resp) |
|
| 13 |
+ return err |
|
| 14 |
+} |
| 0 | 15 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,35 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/container" |
|
| 8 |
+ "github.com/moby/moby/api/types/filters" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// ContainersPrune requests the daemon to delete unused data |
|
| 12 |
+func (cli *Client) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
|
|
| 13 |
+ if err := cli.NewVersionError(ctx, "1.25", "container prune"); err != nil {
|
|
| 14 |
+ return container.PruneReport{}, err
|
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ query, err := getFiltersQuery(pruneFilters) |
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return container.PruneReport{}, err
|
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ resp, err := cli.post(ctx, "/containers/prune", query, nil, nil) |
|
| 23 |
+ defer ensureReaderClosed(resp) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return container.PruneReport{}, err
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ var report container.PruneReport |
|
| 29 |
+ if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
|
|
| 30 |
+ return container.PruneReport{}, fmt.Errorf("Error retrieving disk usage: %v", err)
|
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ return report, nil |
|
| 34 |
+} |
| 0 | 35 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,32 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/container" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// ContainerRemove kills and removes a container from the docker host. |
|
| 10 |
+func (cli *Client) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error {
|
|
| 11 |
+ containerID, err := trimID("container", containerID)
|
|
| 12 |
+ if err != nil {
|
|
| 13 |
+ return err |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ if options.RemoveVolumes {
|
|
| 18 |
+ query.Set("v", "1")
|
|
| 19 |
+ } |
|
| 20 |
+ if options.RemoveLinks {
|
|
| 21 |
+ query.Set("link", "1")
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ if options.Force {
|
|
| 25 |
+ query.Set("force", "1")
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ resp, err := cli.delete(ctx, "/containers/"+containerID, query, nil) |
|
| 29 |
+ defer ensureReaderClosed(resp) |
|
| 30 |
+ return err |
|
| 31 |
+} |
| 0 | 32 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,20 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+// ContainerRename changes the name of a given container. |
|
| 8 |
+func (cli *Client) ContainerRename(ctx context.Context, containerID, newContainerName string) error {
|
|
| 9 |
+ containerID, err := trimID("container", containerID)
|
|
| 10 |
+ if err != nil {
|
|
| 11 |
+ return err |
|
| 12 |
+ } |
|
| 13 |
+ |
|
| 14 |
+ query := url.Values{}
|
|
| 15 |
+ query.Set("name", newContainerName)
|
|
| 16 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/rename", query, nil, nil) |
|
| 17 |
+ ensureReaderClosed(resp) |
|
| 18 |
+ return err |
|
| 19 |
+} |
| 0 | 20 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,38 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ "strconv" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/container" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// ContainerResize changes the size of the tty for a container. |
|
| 11 |
+func (cli *Client) ContainerResize(ctx context.Context, containerID string, options container.ResizeOptions) error {
|
|
| 12 |
+ containerID, err := trimID("container", containerID)
|
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return err |
|
| 15 |
+ } |
|
| 16 |
+ return cli.resize(ctx, "/containers/"+containerID, options.Height, options.Width) |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+// ContainerExecResize changes the size of the tty for an exec process running inside a container. |
|
| 20 |
+func (cli *Client) ContainerExecResize(ctx context.Context, execID string, options container.ResizeOptions) error {
|
|
| 21 |
+ execID, err := trimID("exec", execID)
|
|
| 22 |
+ if err != nil {
|
|
| 23 |
+ return err |
|
| 24 |
+ } |
|
| 25 |
+ return cli.resize(ctx, "/exec/"+execID, options.Height, options.Width) |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func (cli *Client) resize(ctx context.Context, basePath string, height, width uint) error {
|
|
| 29 |
+ // FIXME(thaJeztah): the API / backend accepts uint32, but container.ResizeOptions uses uint. |
|
| 30 |
+ query := url.Values{}
|
|
| 31 |
+ query.Set("h", strconv.FormatUint(uint64(height), 10))
|
|
| 32 |
+ query.Set("w", strconv.FormatUint(uint64(width), 10))
|
|
| 33 |
+ |
|
| 34 |
+ resp, err := cli.post(ctx, basePath+"/resize", query, nil, nil) |
|
| 35 |
+ ensureReaderClosed(resp) |
|
| 36 |
+ return err |
|
| 37 |
+} |
| 0 | 38 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,41 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ "strconv" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/container" |
|
| 8 |
+ "github.com/moby/moby/api/types/versions" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// ContainerRestart stops and starts a container again. |
|
| 12 |
+// It makes the daemon wait for the container to be up again for |
|
| 13 |
+// a specific amount of time, given the timeout. |
|
| 14 |
+func (cli *Client) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
|
|
| 15 |
+ containerID, err := trimID("container", containerID)
|
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ return err |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ query := url.Values{}
|
|
| 21 |
+ if options.Timeout != nil {
|
|
| 22 |
+ query.Set("t", strconv.Itoa(*options.Timeout))
|
|
| 23 |
+ } |
|
| 24 |
+ if options.Signal != "" {
|
|
| 25 |
+ // Make sure we negotiated (if the client is configured to do so), |
|
| 26 |
+ // as code below contains API-version specific handling of options. |
|
| 27 |
+ // |
|
| 28 |
+ // Normally, version-negotiation (if enabled) would not happen until |
|
| 29 |
+ // the API request is made. |
|
| 30 |
+ if err := cli.checkVersion(ctx); err != nil {
|
|
| 31 |
+ return err |
|
| 32 |
+ } |
|
| 33 |
+ if versions.GreaterThanOrEqualTo(cli.version, "1.42") {
|
|
| 34 |
+ query.Set("signal", options.Signal)
|
|
| 35 |
+ } |
|
| 36 |
+ } |
|
| 37 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/restart", query, nil, nil) |
|
| 38 |
+ ensureReaderClosed(resp) |
|
| 39 |
+ return err |
|
| 40 |
+} |
| 0 | 41 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,28 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/container" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// ContainerStart sends a request to the docker daemon to start a container. |
|
| 10 |
+func (cli *Client) ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error {
|
|
| 11 |
+ containerID, err := trimID("container", containerID)
|
|
| 12 |
+ if err != nil {
|
|
| 13 |
+ return err |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ if options.CheckpointID != "" {
|
|
| 18 |
+ query.Set("checkpoint", options.CheckpointID)
|
|
| 19 |
+ } |
|
| 20 |
+ if options.CheckpointDir != "" {
|
|
| 21 |
+ query.Set("checkpoint-dir", options.CheckpointDir)
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/start", query, nil, nil) |
|
| 25 |
+ ensureReaderClosed(resp) |
|
| 26 |
+ return err |
|
| 27 |
+} |
| 0 | 28 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,56 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/container" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// ContainerStats returns near realtime stats for a given container. |
|
| 10 |
+// It's up to the caller to close the io.ReadCloser returned. |
|
| 11 |
+func (cli *Client) ContainerStats(ctx context.Context, containerID string, stream bool) (container.StatsResponseReader, error) {
|
|
| 12 |
+ containerID, err := trimID("container", containerID)
|
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return container.StatsResponseReader{}, err
|
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ query := url.Values{}
|
|
| 18 |
+ query.Set("stream", "0")
|
|
| 19 |
+ if stream {
|
|
| 20 |
+ query.Set("stream", "1")
|
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return container.StatsResponseReader{}, err
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ return container.StatsResponseReader{
|
|
| 29 |
+ Body: resp.Body, |
|
| 30 |
+ OSType: resp.Header.Get("Ostype"),
|
|
| 31 |
+ }, nil |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+// ContainerStatsOneShot gets a single stat entry from a container. |
|
| 35 |
+// It differs from `ContainerStats` in that the API should not wait to prime the stats |
|
| 36 |
+func (cli *Client) ContainerStatsOneShot(ctx context.Context, containerID string) (container.StatsResponseReader, error) {
|
|
| 37 |
+ containerID, err := trimID("container", containerID)
|
|
| 38 |
+ if err != nil {
|
|
| 39 |
+ return container.StatsResponseReader{}, err
|
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ query := url.Values{}
|
|
| 43 |
+ query.Set("stream", "0")
|
|
| 44 |
+ query.Set("one-shot", "1")
|
|
| 45 |
+ |
|
| 46 |
+ resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil) |
|
| 47 |
+ if err != nil {
|
|
| 48 |
+ return container.StatsResponseReader{}, err
|
|
| 49 |
+ } |
|
| 50 |
+ |
|
| 51 |
+ return container.StatsResponseReader{
|
|
| 52 |
+ Body: resp.Body, |
|
| 53 |
+ OSType: resp.Header.Get("Ostype"),
|
|
| 54 |
+ }, nil |
|
| 55 |
+} |
| 0 | 56 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,45 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ "strconv" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/container" |
|
| 8 |
+ "github.com/moby/moby/api/types/versions" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// ContainerStop stops a container. In case the container fails to stop |
|
| 12 |
+// gracefully within a time frame specified by the timeout argument, |
|
| 13 |
+// it is forcefully terminated (killed). |
|
| 14 |
+// |
|
| 15 |
+// If the timeout is nil, the container's StopTimeout value is used, if set, |
|
| 16 |
+// otherwise the engine default. A negative timeout value can be specified, |
|
| 17 |
+// meaning no timeout, i.e. no forceful termination is performed. |
|
| 18 |
+func (cli *Client) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
|
|
| 19 |
+ containerID, err := trimID("container", containerID)
|
|
| 20 |
+ if err != nil {
|
|
| 21 |
+ return err |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ query := url.Values{}
|
|
| 25 |
+ if options.Timeout != nil {
|
|
| 26 |
+ query.Set("t", strconv.Itoa(*options.Timeout))
|
|
| 27 |
+ } |
|
| 28 |
+ if options.Signal != "" {
|
|
| 29 |
+ // Make sure we negotiated (if the client is configured to do so), |
|
| 30 |
+ // as code below contains API-version specific handling of options. |
|
| 31 |
+ // |
|
| 32 |
+ // Normally, version-negotiation (if enabled) would not happen until |
|
| 33 |
+ // the API request is made. |
|
| 34 |
+ if err := cli.checkVersion(ctx); err != nil {
|
|
| 35 |
+ return err |
|
| 36 |
+ } |
|
| 37 |
+ if versions.GreaterThanOrEqualTo(cli.version, "1.42") {
|
|
| 38 |
+ query.Set("signal", options.Signal)
|
|
| 39 |
+ } |
|
| 40 |
+ } |
|
| 41 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/stop", query, nil, nil) |
|
| 42 |
+ ensureReaderClosed(resp) |
|
| 43 |
+ return err |
|
| 44 |
+} |
| 0 | 45 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,33 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/container" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// ContainerTop shows process information from within a container. |
|
| 12 |
+func (cli *Client) ContainerTop(ctx context.Context, containerID string, arguments []string) (container.TopResponse, error) {
|
|
| 13 |
+ containerID, err := trimID("container", containerID)
|
|
| 14 |
+ if err != nil {
|
|
| 15 |
+ return container.TopResponse{}, err
|
|
| 16 |
+ } |
|
| 17 |
+ |
|
| 18 |
+ query := url.Values{}
|
|
| 19 |
+ if len(arguments) > 0 {
|
|
| 20 |
+ query.Set("ps_args", strings.Join(arguments, " "))
|
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ resp, err := cli.get(ctx, "/containers/"+containerID+"/top", query, nil) |
|
| 24 |
+ defer ensureReaderClosed(resp) |
|
| 25 |
+ if err != nil {
|
|
| 26 |
+ return container.TopResponse{}, err
|
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ var response container.TopResponse |
|
| 30 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 31 |
+ return response, err |
|
| 32 |
+} |
| 0 | 33 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,15 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import "context" |
|
| 3 |
+ |
|
| 4 |
+// ContainerUnpause resumes the process execution within a container |
|
| 5 |
+func (cli *Client) ContainerUnpause(ctx context.Context, containerID string) error {
|
|
| 6 |
+ containerID, err := trimID("container", containerID)
|
|
| 7 |
+ if err != nil {
|
|
| 8 |
+ return err |
|
| 9 |
+ } |
|
| 10 |
+ |
|
| 11 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/unpause", nil, nil, nil) |
|
| 12 |
+ ensureReaderClosed(resp) |
|
| 13 |
+ return err |
|
| 14 |
+} |
| 0 | 15 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,26 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/container" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// ContainerUpdate updates the resources of a container. |
|
| 10 |
+func (cli *Client) ContainerUpdate(ctx context.Context, containerID string, updateConfig container.UpdateConfig) (container.UpdateResponse, error) {
|
|
| 11 |
+ containerID, err := trimID("container", containerID)
|
|
| 12 |
+ if err != nil {
|
|
| 13 |
+ return container.UpdateResponse{}, err
|
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/update", nil, updateConfig, nil) |
|
| 17 |
+ defer ensureReaderClosed(resp) |
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return container.UpdateResponse{}, err
|
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ var response container.UpdateResponse |
|
| 23 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 24 |
+ return response, err |
|
| 25 |
+} |
| 0 | 26 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,122 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "errors" |
|
| 7 |
+ "io" |
|
| 8 |
+ "net/url" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/moby/moby/api/types/container" |
|
| 11 |
+ "github.com/moby/moby/api/types/versions" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+const containerWaitErrorMsgLimit = 2 * 1024 /* Max: 2KiB */ |
|
| 15 |
+ |
|
| 16 |
+// ContainerWait waits until the specified container is in a certain state |
|
| 17 |
+// indicated by the given condition, either "not-running" (default), |
|
| 18 |
+// "next-exit", or "removed". |
|
| 19 |
+// |
|
| 20 |
+// If this client's API version is before 1.30, condition is ignored and |
|
| 21 |
+// ContainerWait will return immediately with the two channels, as the server |
|
| 22 |
+// will wait as if the condition were "not-running". |
|
| 23 |
+// |
|
| 24 |
+// If this client's API version is at least 1.30, ContainerWait blocks until |
|
| 25 |
+// the request has been acknowledged by the server (with a response header), |
|
| 26 |
+// then returns two channels on which the caller can wait for the exit status |
|
| 27 |
+// of the container or an error if there was a problem either beginning the |
|
| 28 |
+// wait request or in getting the response. This allows the caller to |
|
| 29 |
+// synchronize ContainerWait with other calls, such as specifying a |
|
| 30 |
+// "next-exit" condition before issuing a ContainerStart request. |
|
| 31 |
+func (cli *Client) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
|
|
| 32 |
+ resultC := make(chan container.WaitResponse) |
|
| 33 |
+ errC := make(chan error, 1) |
|
| 34 |
+ |
|
| 35 |
+ containerID, err := trimID("container", containerID)
|
|
| 36 |
+ if err != nil {
|
|
| 37 |
+ errC <- err |
|
| 38 |
+ return resultC, errC |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ // Make sure we negotiated (if the client is configured to do so), |
|
| 42 |
+ // as code below contains API-version specific handling of options. |
|
| 43 |
+ // |
|
| 44 |
+ // Normally, version-negotiation (if enabled) would not happen until |
|
| 45 |
+ // the API request is made. |
|
| 46 |
+ if err := cli.checkVersion(ctx); err != nil {
|
|
| 47 |
+ errC <- err |
|
| 48 |
+ return resultC, errC |
|
| 49 |
+ } |
|
| 50 |
+ if versions.LessThan(cli.ClientVersion(), "1.30") {
|
|
| 51 |
+ return cli.legacyContainerWait(ctx, containerID) |
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ query := url.Values{}
|
|
| 55 |
+ if condition != "" {
|
|
| 56 |
+ query.Set("condition", string(condition))
|
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", query, nil, nil) |
|
| 60 |
+ if err != nil {
|
|
| 61 |
+ defer ensureReaderClosed(resp) |
|
| 62 |
+ errC <- err |
|
| 63 |
+ return resultC, errC |
|
| 64 |
+ } |
|
| 65 |
+ |
|
| 66 |
+ go func() {
|
|
| 67 |
+ defer ensureReaderClosed(resp) |
|
| 68 |
+ |
|
| 69 |
+ responseText := bytes.NewBuffer(nil) |
|
| 70 |
+ stream := io.TeeReader(resp.Body, responseText) |
|
| 71 |
+ |
|
| 72 |
+ var res container.WaitResponse |
|
| 73 |
+ if err := json.NewDecoder(stream).Decode(&res); err != nil {
|
|
| 74 |
+ // NOTE(nicks): The /wait API does not work well with HTTP proxies. |
|
| 75 |
+ // At any time, the proxy could cut off the response stream. |
|
| 76 |
+ // |
|
| 77 |
+ // But because the HTTP status has already been written, the proxy's |
|
| 78 |
+ // only option is to write a plaintext error message. |
|
| 79 |
+ // |
|
| 80 |
+ // If there's a JSON parsing error, read the real error message |
|
| 81 |
+ // off the body and send it to the client. |
|
| 82 |
+ if errors.As(err, new(*json.SyntaxError)) {
|
|
| 83 |
+ _, _ = io.ReadAll(io.LimitReader(stream, containerWaitErrorMsgLimit)) |
|
| 84 |
+ errC <- errors.New(responseText.String()) |
|
| 85 |
+ } else {
|
|
| 86 |
+ errC <- err |
|
| 87 |
+ } |
|
| 88 |
+ return |
|
| 89 |
+ } |
|
| 90 |
+ |
|
| 91 |
+ resultC <- res |
|
| 92 |
+ }() |
|
| 93 |
+ |
|
| 94 |
+ return resultC, errC |
|
| 95 |
+} |
|
| 96 |
+ |
|
| 97 |
+// legacyContainerWait returns immediately and doesn't have an option to wait |
|
| 98 |
+// until the container is removed. |
|
| 99 |
+func (cli *Client) legacyContainerWait(ctx context.Context, containerID string) (<-chan container.WaitResponse, <-chan error) {
|
|
| 100 |
+ resultC := make(chan container.WaitResponse) |
|
| 101 |
+ errC := make(chan error) |
|
| 102 |
+ |
|
| 103 |
+ go func() {
|
|
| 104 |
+ resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", nil, nil, nil) |
|
| 105 |
+ if err != nil {
|
|
| 106 |
+ errC <- err |
|
| 107 |
+ return |
|
| 108 |
+ } |
|
| 109 |
+ defer ensureReaderClosed(resp) |
|
| 110 |
+ |
|
| 111 |
+ var res container.WaitResponse |
|
| 112 |
+ if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
|
| 113 |
+ errC <- err |
|
| 114 |
+ return |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ resultC <- res |
|
| 118 |
+ }() |
|
| 119 |
+ |
|
| 120 |
+ return resultC, errC |
|
| 121 |
+} |
| 0 | 122 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,33 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// DiskUsage requests the current data usage from the daemon |
|
| 12 |
+func (cli *Client) DiskUsage(ctx context.Context, options types.DiskUsageOptions) (types.DiskUsage, error) {
|
|
| 13 |
+ var query url.Values |
|
| 14 |
+ if len(options.Types) > 0 {
|
|
| 15 |
+ query = url.Values{}
|
|
| 16 |
+ for _, t := range options.Types {
|
|
| 17 |
+ query.Add("type", string(t))
|
|
| 18 |
+ } |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ resp, err := cli.get(ctx, "/system/df", query, nil) |
|
| 22 |
+ defer ensureReaderClosed(resp) |
|
| 23 |
+ if err != nil {
|
|
| 24 |
+ return types.DiskUsage{}, err
|
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ var du types.DiskUsage |
|
| 28 |
+ if err := json.NewDecoder(resp.Body).Decode(&du); err != nil {
|
|
| 29 |
+ return types.DiskUsage{}, fmt.Errorf("Error retrieving disk usage: %v", err)
|
|
| 30 |
+ } |
|
| 31 |
+ return du, nil |
|
| 32 |
+} |
| 0 | 33 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,39 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/http" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/registry" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// DistributionInspect returns the image digest with the full manifest. |
|
| 12 |
+func (cli *Client) DistributionInspect(ctx context.Context, imageRef, encodedRegistryAuth string) (registry.DistributionInspect, error) {
|
|
| 13 |
+ if imageRef == "" {
|
|
| 14 |
+ return registry.DistributionInspect{}, objectNotFoundError{object: "distribution", id: imageRef}
|
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ if err := cli.NewVersionError(ctx, "1.30", "distribution inspect"); err != nil {
|
|
| 18 |
+ return registry.DistributionInspect{}, err
|
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ var headers http.Header |
|
| 22 |
+ if encodedRegistryAuth != "" {
|
|
| 23 |
+ headers = http.Header{
|
|
| 24 |
+ registry.AuthHeader: {encodedRegistryAuth},
|
|
| 25 |
+ } |
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ // Contact the registry to retrieve digest and platform information |
|
| 29 |
+ resp, err := cli.get(ctx, "/distribution/"+imageRef+"/json", url.Values{}, headers)
|
|
| 30 |
+ defer ensureReaderClosed(resp) |
|
| 31 |
+ if err != nil {
|
|
| 32 |
+ return registry.DistributionInspect{}, err
|
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ var distributionInspect registry.DistributionInspect |
|
| 36 |
+ err = json.NewDecoder(resp.Body).Decode(&distributionInspect) |
|
| 37 |
+ return distributionInspect, err |
|
| 38 |
+} |
| 0 | 39 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,90 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+const ( |
|
| 3 |
+ // EnvOverrideHost is the name of the environment variable that can be used |
|
| 4 |
+ // to override the default host to connect to (DefaultDockerHost). |
|
| 5 |
+ // |
|
| 6 |
+ // This env-var is read by FromEnv and WithHostFromEnv and when set to a |
|
| 7 |
+ // non-empty value, takes precedence over the default host (which is platform |
|
| 8 |
+ // specific), or any host already set. |
|
| 9 |
+ EnvOverrideHost = "DOCKER_HOST" |
|
| 10 |
+ |
|
| 11 |
+ // EnvOverrideAPIVersion is the name of the environment variable that can |
|
| 12 |
+ // be used to override the API version to use. Value should be |
|
| 13 |
+ // formatted as MAJOR.MINOR, for example, "1.19". |
|
| 14 |
+ // |
|
| 15 |
+ // This env-var is read by FromEnv and WithVersionFromEnv and when set to a |
|
| 16 |
+ // non-empty value, takes precedence over API version negotiation. |
|
| 17 |
+ // |
|
| 18 |
+ // This environment variable should be used for debugging purposes only, as |
|
| 19 |
+ // it can set the client to use an incompatible (or invalid) API version. |
|
| 20 |
+ EnvOverrideAPIVersion = "DOCKER_API_VERSION" |
|
| 21 |
+ |
|
| 22 |
+ // EnvOverrideCertPath is the name of the environment variable that can be |
|
| 23 |
+ // used to specify the directory from which to load the TLS certificates |
|
| 24 |
+ // (ca.pem, cert.pem, key.pem) from. These certificates are used to configure |
|
| 25 |
+ // the Client for a TCP connection protected by TLS client authentication. |
|
| 26 |
+ // |
|
| 27 |
+ // TLS certificate verification is enabled by default if the Client is configured |
|
| 28 |
+ // to use a TLS connection. Refer to EnvTLSVerify below to learn how to |
|
| 29 |
+ // disable verification for testing purposes. |
|
| 30 |
+ // |
|
| 31 |
+ // WARNING: Access to the remote API is equivalent to root access to the |
|
| 32 |
+ // host where the daemon runs. Do not expose the API without protection, |
|
| 33 |
+ // and only if needed. Make sure you are familiar with the "daemon attack |
|
| 34 |
+ // surface" (https://docs.docker.com/go/attack-surface/). |
|
| 35 |
+ // |
|
| 36 |
+ // For local access to the API, it is recommended to connect with the daemon |
|
| 37 |
+ // using the default local socket connection (on Linux), or the named pipe |
|
| 38 |
+ // (on Windows). |
|
| 39 |
+ // |
|
| 40 |
+ // If you need to access the API of a remote daemon, consider using an SSH |
|
| 41 |
+ // (ssh://) connection, which is easier to set up, and requires no additional |
|
| 42 |
+ // configuration if the host is accessible using ssh. |
|
| 43 |
+ // |
|
| 44 |
+ // If you cannot use the alternatives above, and you must expose the API over |
|
| 45 |
+ // a TCP connection, refer to https://docs.docker.com/engine/security/protect-access/ |
|
| 46 |
+ // to learn how to configure the daemon and client to use a TCP connection |
|
| 47 |
+ // with TLS client authentication. Make sure you know the differences between |
|
| 48 |
+ // a regular TLS connection and a TLS connection protected by TLS client |
|
| 49 |
+ // authentication, and verify that the API cannot be accessed by other clients. |
|
| 50 |
+ EnvOverrideCertPath = "DOCKER_CERT_PATH" |
|
| 51 |
+ |
|
| 52 |
+ // EnvTLSVerify is the name of the environment variable that can be used to |
|
| 53 |
+ // enable or disable TLS certificate verification. When set to a non-empty |
|
| 54 |
+ // value, TLS certificate verification is enabled, and the client is configured |
|
| 55 |
+ // to use a TLS connection, using certificates from the default directories |
|
| 56 |
+ // (within `~/.docker`); refer to EnvOverrideCertPath above for additional |
|
| 57 |
+ // details. |
|
| 58 |
+ // |
|
| 59 |
+ // WARNING: Access to the remote API is equivalent to root access to the |
|
| 60 |
+ // host where the daemon runs. Do not expose the API without protection, |
|
| 61 |
+ // and only if needed. Make sure you are familiar with the "daemon attack |
|
| 62 |
+ // surface" (https://docs.docker.com/go/attack-surface/). |
|
| 63 |
+ // |
|
| 64 |
+ // Before setting up your client and daemon to use a TCP connection with TLS |
|
| 65 |
+ // client authentication, consider using one of the alternatives mentioned |
|
| 66 |
+ // in EnvOverrideCertPath above. |
|
| 67 |
+ // |
|
| 68 |
+ // Disabling TLS certificate verification (for testing purposes) |
|
| 69 |
+ // |
|
| 70 |
+ // TLS certificate verification is enabled by default if the Client is configured |
|
| 71 |
+ // to use a TLS connection, and it is highly recommended to keep verification |
|
| 72 |
+ // enabled to prevent machine-in-the-middle attacks. Refer to the documentation |
|
| 73 |
+ // at https://docs.docker.com/engine/security/protect-access/ and pages linked |
|
| 74 |
+ // from that page to learn how to configure the daemon and client to use a |
|
| 75 |
+ // TCP connection with TLS client authentication enabled. |
|
| 76 |
+ // |
|
| 77 |
+ // Set the "DOCKER_TLS_VERIFY" environment to an empty string ("") to
|
|
| 78 |
+ // disable TLS certificate verification. Disabling verification is insecure, |
|
| 79 |
+ // so should only be done for testing purposes. From the Go documentation |
|
| 80 |
+ // (https://pkg.go.dev/crypto/tls#Config): |
|
| 81 |
+ // |
|
| 82 |
+ // InsecureSkipVerify controls whether a client verifies the server's |
|
| 83 |
+ // certificate chain and host name. If InsecureSkipVerify is true, crypto/tls |
|
| 84 |
+ // accepts any certificate presented by the server and any host name in that |
|
| 85 |
+ // certificate. In this mode, TLS is susceptible to machine-in-the-middle |
|
| 86 |
+ // attacks unless custom verification is used. This should be used only for |
|
| 87 |
+ // testing or in combination with VerifyConnection or VerifyPeerCertificate. |
|
| 88 |
+ EnvTLSVerify = "DOCKER_TLS_VERIFY" |
|
| 89 |
+) |
| 0 | 90 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,129 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "errors" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "net/http" |
|
| 7 |
+ |
|
| 8 |
+ cerrdefs "github.com/containerd/errdefs" |
|
| 9 |
+ "github.com/containerd/errdefs/pkg/errhttp" |
|
| 10 |
+ "github.com/moby/moby/api/types/versions" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// errConnectionFailed implements an error returned when connection failed. |
|
| 14 |
+type errConnectionFailed struct {
|
|
| 15 |
+ error |
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+// Error returns a string representation of an errConnectionFailed |
|
| 19 |
+func (e errConnectionFailed) Error() string {
|
|
| 20 |
+ return e.error.Error() |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+func (e errConnectionFailed) Unwrap() error {
|
|
| 24 |
+ return e.error |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+// IsErrConnectionFailed returns true if the error is caused by connection failed. |
|
| 28 |
+func IsErrConnectionFailed(err error) bool {
|
|
| 29 |
+ return errors.As(err, &errConnectionFailed{})
|
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+// ErrorConnectionFailed returns an error with host in the error message when connection to docker daemon failed. |
|
| 33 |
+// |
|
| 34 |
+// Deprecated: this function was only used internally, and will be removed in the next release. |
|
| 35 |
+func ErrorConnectionFailed(host string) error {
|
|
| 36 |
+ return connectionFailed(host) |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+// connectionFailed returns an error with host in the error message when connection |
|
| 40 |
+// to docker daemon failed. |
|
| 41 |
+func connectionFailed(host string) error {
|
|
| 42 |
+ var err error |
|
| 43 |
+ if host == "" {
|
|
| 44 |
+ err = errors.New("Cannot connect to the Docker daemon. Is the docker daemon running on this host?")
|
|
| 45 |
+ } else {
|
|
| 46 |
+ err = fmt.Errorf("Cannot connect to the Docker daemon at %s. Is the docker daemon running?", host)
|
|
| 47 |
+ } |
|
| 48 |
+ return errConnectionFailed{error: err}
|
|
| 49 |
+} |
|
| 50 |
+ |
|
| 51 |
+// IsErrNotFound returns true if the error is a NotFound error, which is returned |
|
| 52 |
+// by the API when some object is not found. It is an alias for [cerrdefs.IsNotFound]. |
|
| 53 |
+// |
|
| 54 |
+// Deprecated: use [cerrdefs.IsNotFound] instead. |
|
| 55 |
+func IsErrNotFound(err error) bool {
|
|
| 56 |
+ return cerrdefs.IsNotFound(err) |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+type objectNotFoundError struct {
|
|
| 60 |
+ object string |
|
| 61 |
+ id string |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+func (e objectNotFoundError) NotFound() {}
|
|
| 65 |
+ |
|
| 66 |
+func (e objectNotFoundError) Error() string {
|
|
| 67 |
+ return fmt.Sprintf("Error: No such %s: %s", e.object, e.id)
|
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+// NewVersionError returns an error if the APIVersion required is less than the |
|
| 71 |
+// current supported version. |
|
| 72 |
+// |
|
| 73 |
+// It performs API-version negotiation if the Client is configured with this |
|
| 74 |
+// option, otherwise it assumes the latest API version is used. |
|
| 75 |
+func (cli *Client) NewVersionError(ctx context.Context, APIrequired, feature string) error {
|
|
| 76 |
+ // Make sure we negotiated (if the client is configured to do so), |
|
| 77 |
+ // as code below contains API-version specific handling of options. |
|
| 78 |
+ // |
|
| 79 |
+ // Normally, version-negotiation (if enabled) would not happen until |
|
| 80 |
+ // the API request is made. |
|
| 81 |
+ if err := cli.checkVersion(ctx); err != nil {
|
|
| 82 |
+ return err |
|
| 83 |
+ } |
|
| 84 |
+ if cli.version != "" && versions.LessThan(cli.version, APIrequired) {
|
|
| 85 |
+ return fmt.Errorf("%q requires API version %s, but the Docker daemon API version is %s", feature, APIrequired, cli.version)
|
|
| 86 |
+ } |
|
| 87 |
+ return nil |
|
| 88 |
+} |
|
| 89 |
+ |
|
| 90 |
+type httpError struct {
|
|
| 91 |
+ err error |
|
| 92 |
+ errdef error |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+func (e *httpError) Error() string {
|
|
| 96 |
+ return e.err.Error() |
|
| 97 |
+} |
|
| 98 |
+ |
|
| 99 |
+func (e *httpError) Unwrap() error {
|
|
| 100 |
+ return e.err |
|
| 101 |
+} |
|
| 102 |
+ |
|
| 103 |
+func (e *httpError) Is(target error) bool {
|
|
| 104 |
+ return errors.Is(e.errdef, target) |
|
| 105 |
+} |
|
| 106 |
+ |
|
| 107 |
+// httpErrorFromStatusCode creates an errdef error, based on the provided HTTP status-code |
|
| 108 |
+func httpErrorFromStatusCode(err error, statusCode int) error {
|
|
| 109 |
+ if err == nil {
|
|
| 110 |
+ return nil |
|
| 111 |
+ } |
|
| 112 |
+ base := errhttp.ToNative(statusCode) |
|
| 113 |
+ if base != nil {
|
|
| 114 |
+ return &httpError{err: err, errdef: base}
|
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ switch {
|
|
| 118 |
+ case statusCode >= http.StatusOK && statusCode < http.StatusBadRequest: |
|
| 119 |
+ // it's a client error |
|
| 120 |
+ return err |
|
| 121 |
+ case statusCode >= http.StatusBadRequest && statusCode < http.StatusInternalServerError: |
|
| 122 |
+ return &httpError{err: err, errdef: cerrdefs.ErrInvalidArgument}
|
|
| 123 |
+ case statusCode >= http.StatusInternalServerError && statusCode < 600: |
|
| 124 |
+ return &httpError{err: err, errdef: cerrdefs.ErrInternal}
|
|
| 125 |
+ default: |
|
| 126 |
+ return &httpError{err: err, errdef: cerrdefs.ErrUnknown}
|
|
| 127 |
+ } |
|
| 128 |
+} |
| 0 | 129 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,100 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ "time" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/events" |
|
| 9 |
+ "github.com/moby/moby/api/types/filters" |
|
| 10 |
+ timetypes "github.com/moby/moby/api/types/time" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// Events returns a stream of events in the daemon. It's up to the caller to close the stream |
|
| 14 |
+// by cancelling the context. Once the stream has been completely read an io.EOF error will |
|
| 15 |
+// be sent over the error channel. If an error is sent all processing will be stopped. It's up |
|
| 16 |
+// to the caller to reopen the stream in the event of an error by reinvoking this method. |
|
| 17 |
+func (cli *Client) Events(ctx context.Context, options events.ListOptions) (<-chan events.Message, <-chan error) {
|
|
| 18 |
+ messages := make(chan events.Message) |
|
| 19 |
+ errs := make(chan error, 1) |
|
| 20 |
+ |
|
| 21 |
+ started := make(chan struct{})
|
|
| 22 |
+ go func() {
|
|
| 23 |
+ defer close(errs) |
|
| 24 |
+ |
|
| 25 |
+ query, err := buildEventsQueryParams(cli.version, options) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ close(started) |
|
| 28 |
+ errs <- err |
|
| 29 |
+ return |
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ resp, err := cli.get(ctx, "/events", query, nil) |
|
| 33 |
+ if err != nil {
|
|
| 34 |
+ close(started) |
|
| 35 |
+ errs <- err |
|
| 36 |
+ return |
|
| 37 |
+ } |
|
| 38 |
+ defer resp.Body.Close() |
|
| 39 |
+ |
|
| 40 |
+ decoder := json.NewDecoder(resp.Body) |
|
| 41 |
+ |
|
| 42 |
+ close(started) |
|
| 43 |
+ for {
|
|
| 44 |
+ select {
|
|
| 45 |
+ case <-ctx.Done(): |
|
| 46 |
+ errs <- ctx.Err() |
|
| 47 |
+ return |
|
| 48 |
+ default: |
|
| 49 |
+ var event events.Message |
|
| 50 |
+ if err := decoder.Decode(&event); err != nil {
|
|
| 51 |
+ errs <- err |
|
| 52 |
+ return |
|
| 53 |
+ } |
|
| 54 |
+ |
|
| 55 |
+ select {
|
|
| 56 |
+ case messages <- event: |
|
| 57 |
+ case <-ctx.Done(): |
|
| 58 |
+ errs <- ctx.Err() |
|
| 59 |
+ return |
|
| 60 |
+ } |
|
| 61 |
+ } |
|
| 62 |
+ } |
|
| 63 |
+ }() |
|
| 64 |
+ <-started |
|
| 65 |
+ |
|
| 66 |
+ return messages, errs |
|
| 67 |
+} |
|
| 68 |
+ |
|
| 69 |
+func buildEventsQueryParams(cliVersion string, options events.ListOptions) (url.Values, error) {
|
|
| 70 |
+ query := url.Values{}
|
|
| 71 |
+ ref := time.Now() |
|
| 72 |
+ |
|
| 73 |
+ if options.Since != "" {
|
|
| 74 |
+ ts, err := timetypes.GetTimestamp(options.Since, ref) |
|
| 75 |
+ if err != nil {
|
|
| 76 |
+ return nil, err |
|
| 77 |
+ } |
|
| 78 |
+ query.Set("since", ts)
|
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ if options.Until != "" {
|
|
| 82 |
+ ts, err := timetypes.GetTimestamp(options.Until, ref) |
|
| 83 |
+ if err != nil {
|
|
| 84 |
+ return nil, err |
|
| 85 |
+ } |
|
| 86 |
+ query.Set("until", ts)
|
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ if options.Filters.Len() > 0 {
|
|
| 90 |
+ //nolint:staticcheck // ignore SA1019 for old code |
|
| 91 |
+ filterJSON, err := filters.ToParamWithVersion(cliVersion, options.Filters) |
|
| 92 |
+ if err != nil {
|
|
| 93 |
+ return nil, err |
|
| 94 |
+ } |
|
| 95 |
+ query.Set("filters", filterJSON)
|
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+ return query, nil |
|
| 99 |
+} |
| 0 | 100 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,139 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bufio" |
|
| 4 |
+ "context" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "net" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ "net/url" |
|
| 9 |
+ "time" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/moby/moby/api/types" |
|
| 12 |
+ "github.com/moby/moby/api/types/versions" |
|
| 13 |
+ "github.com/pkg/errors" |
|
| 14 |
+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+// postHijacked sends a POST request and hijacks the connection. |
|
| 18 |
+func (cli *Client) postHijacked(ctx context.Context, path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) {
|
|
| 19 |
+ bodyEncoded, err := encodeData(body) |
|
| 20 |
+ if err != nil {
|
|
| 21 |
+ return types.HijackedResponse{}, err
|
|
| 22 |
+ } |
|
| 23 |
+ req, err := cli.buildRequest(ctx, http.MethodPost, cli.getAPIPath(ctx, path, query), bodyEncoded, headers) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return types.HijackedResponse{}, err
|
|
| 26 |
+ } |
|
| 27 |
+ conn, mediaType, err := setupHijackConn(cli.dialer(), req, "tcp") |
|
| 28 |
+ if err != nil {
|
|
| 29 |
+ return types.HijackedResponse{}, err
|
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ if versions.LessThan(cli.ClientVersion(), "1.42") {
|
|
| 33 |
+ // Prior to 1.42, Content-Type is always set to raw-stream and not relevant |
|
| 34 |
+ mediaType = "" |
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ return types.NewHijackedResponse(conn, mediaType), nil |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+// DialHijack returns a hijacked connection with negotiated protocol proto. |
|
| 41 |
+func (cli *Client) DialHijack(ctx context.Context, url, proto string, meta map[string][]string) (net.Conn, error) {
|
|
| 42 |
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, http.NoBody) |
|
| 43 |
+ if err != nil {
|
|
| 44 |
+ return nil, err |
|
| 45 |
+ } |
|
| 46 |
+ req = cli.addHeaders(req, meta) |
|
| 47 |
+ |
|
| 48 |
+ conn, _, err := setupHijackConn(cli.Dialer(), req, proto) |
|
| 49 |
+ return conn, err |
|
| 50 |
+} |
|
| 51 |
+ |
|
| 52 |
+func setupHijackConn(dialer func(context.Context) (net.Conn, error), req *http.Request, proto string) (_ net.Conn, _ string, retErr error) {
|
|
| 53 |
+ ctx := req.Context() |
|
| 54 |
+ req.Header.Set("Connection", "Upgrade")
|
|
| 55 |
+ req.Header.Set("Upgrade", proto)
|
|
| 56 |
+ |
|
| 57 |
+ conn, err := dialer(ctx) |
|
| 58 |
+ if err != nil {
|
|
| 59 |
+ return nil, "", errors.Wrap(err, "cannot connect to the Docker daemon. Is 'docker daemon' running on this host?") |
|
| 60 |
+ } |
|
| 61 |
+ defer func() {
|
|
| 62 |
+ if retErr != nil {
|
|
| 63 |
+ conn.Close() |
|
| 64 |
+ } |
|
| 65 |
+ }() |
|
| 66 |
+ |
|
| 67 |
+ // When we set up a TCP connection for hijack, there could be long periods |
|
| 68 |
+ // of inactivity (a long running command with no output) that in certain |
|
| 69 |
+ // network setups may cause ECONNTIMEOUT, leaving the client in an unknown |
|
| 70 |
+ // state. Setting TCP KeepAlive on the socket connection will prohibit |
|
| 71 |
+ // ECONNTIMEOUT unless the socket connection truly is broken |
|
| 72 |
+ if tcpConn, ok := conn.(*net.TCPConn); ok {
|
|
| 73 |
+ _ = tcpConn.SetKeepAlive(true) |
|
| 74 |
+ _ = tcpConn.SetKeepAlivePeriod(30 * time.Second) |
|
| 75 |
+ } |
|
| 76 |
+ |
|
| 77 |
+ hc := &hijackedConn{conn, bufio.NewReader(conn)}
|
|
| 78 |
+ |
|
| 79 |
+ // Server hijacks the connection, error 'connection closed' expected |
|
| 80 |
+ resp, err := otelhttp.NewTransport(hc).RoundTrip(req) |
|
| 81 |
+ if err != nil {
|
|
| 82 |
+ return nil, "", err |
|
| 83 |
+ } |
|
| 84 |
+ if resp.StatusCode != http.StatusSwitchingProtocols {
|
|
| 85 |
+ _ = resp.Body.Close() |
|
| 86 |
+ return nil, "", fmt.Errorf("unable to upgrade to %s, received %d", proto, resp.StatusCode)
|
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ if hc.r.Buffered() > 0 {
|
|
| 90 |
+ // If there is buffered content, wrap the connection. We return an |
|
| 91 |
+ // object that implements CloseWrite if the underlying connection |
|
| 92 |
+ // implements it. |
|
| 93 |
+ if _, ok := hc.Conn.(types.CloseWriter); ok {
|
|
| 94 |
+ conn = &hijackedConnCloseWriter{hc}
|
|
| 95 |
+ } else {
|
|
| 96 |
+ conn = hc |
|
| 97 |
+ } |
|
| 98 |
+ } else {
|
|
| 99 |
+ hc.r.Reset(nil) |
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ return conn, resp.Header.Get("Content-Type"), nil
|
|
| 103 |
+} |
|
| 104 |
+ |
|
| 105 |
+// hijackedConn wraps a net.Conn and is returned by setupHijackConn in the case |
|
| 106 |
+// that a) there was already buffered data in the http layer when Hijack() was |
|
| 107 |
+// called, and b) the underlying net.Conn does *not* implement CloseWrite(). |
|
| 108 |
+// hijackedConn does not implement CloseWrite() either. |
|
| 109 |
+type hijackedConn struct {
|
|
| 110 |
+ net.Conn |
|
| 111 |
+ r *bufio.Reader |
|
| 112 |
+} |
|
| 113 |
+ |
|
| 114 |
+func (c *hijackedConn) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
| 115 |
+ if err := req.Write(c.Conn); err != nil {
|
|
| 116 |
+ return nil, err |
|
| 117 |
+ } |
|
| 118 |
+ return http.ReadResponse(c.r, req) |
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+func (c *hijackedConn) Read(b []byte) (int, error) {
|
|
| 122 |
+ return c.r.Read(b) |
|
| 123 |
+} |
|
| 124 |
+ |
|
| 125 |
+// hijackedConnCloseWriter is a hijackedConn which additionally implements |
|
| 126 |
+// CloseWrite(). It is returned by setupHijackConn in the case that a) there |
|
| 127 |
+// was already buffered data in the http layer when Hijack() was called, and b) |
|
| 128 |
+// the underlying net.Conn *does* implement CloseWrite(). |
|
| 129 |
+type hijackedConnCloseWriter struct {
|
|
| 130 |
+ *hijackedConn |
|
| 131 |
+} |
|
| 132 |
+ |
|
| 133 |
+var _ types.CloseWriter = &hijackedConnCloseWriter{}
|
|
| 134 |
+ |
|
| 135 |
+func (c *hijackedConnCloseWriter) CloseWrite() error {
|
|
| 136 |
+ conn := c.Conn.(types.CloseWriter) |
|
| 137 |
+ return conn.CloseWrite() |
|
| 138 |
+} |
| 0 | 139 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,182 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/base64" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "io" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ "net/url" |
|
| 9 |
+ "strconv" |
|
| 10 |
+ "strings" |
|
| 11 |
+ |
|
| 12 |
+ "github.com/moby/moby/api/types/build" |
|
| 13 |
+ "github.com/moby/moby/api/types/container" |
|
| 14 |
+ "github.com/moby/moby/api/types/network" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+// ImageBuild sends a request to the daemon to build images. |
|
| 18 |
+// The Body in the response implements an io.ReadCloser and it's up to the caller to |
|
| 19 |
+// close it. |
|
| 20 |
+func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options build.ImageBuildOptions) (build.ImageBuildResponse, error) {
|
|
| 21 |
+ query, err := cli.imageBuildOptionsToQuery(ctx, options) |
|
| 22 |
+ if err != nil {
|
|
| 23 |
+ return build.ImageBuildResponse{}, err
|
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ buf, err := json.Marshal(options.AuthConfigs) |
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ return build.ImageBuildResponse{}, err
|
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ headers := http.Header{}
|
|
| 32 |
+ headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf))
|
|
| 33 |
+ headers.Set("Content-Type", "application/x-tar")
|
|
| 34 |
+ |
|
| 35 |
+ resp, err := cli.postRaw(ctx, "/build", query, buildContext, headers) |
|
| 36 |
+ if err != nil {
|
|
| 37 |
+ return build.ImageBuildResponse{}, err
|
|
| 38 |
+ } |
|
| 39 |
+ |
|
| 40 |
+ return build.ImageBuildResponse{
|
|
| 41 |
+ Body: resp.Body, |
|
| 42 |
+ OSType: resp.Header.Get("Ostype"),
|
|
| 43 |
+ }, nil |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+func (cli *Client) imageBuildOptionsToQuery(ctx context.Context, options build.ImageBuildOptions) (url.Values, error) {
|
|
| 47 |
+ query := url.Values{}
|
|
| 48 |
+ if len(options.Tags) > 0 {
|
|
| 49 |
+ query["t"] = options.Tags |
|
| 50 |
+ } |
|
| 51 |
+ if len(options.SecurityOpt) > 0 {
|
|
| 52 |
+ query["securityopt"] = options.SecurityOpt |
|
| 53 |
+ } |
|
| 54 |
+ if len(options.ExtraHosts) > 0 {
|
|
| 55 |
+ query["extrahosts"] = options.ExtraHosts |
|
| 56 |
+ } |
|
| 57 |
+ if options.SuppressOutput {
|
|
| 58 |
+ query.Set("q", "1")
|
|
| 59 |
+ } |
|
| 60 |
+ if options.RemoteContext != "" {
|
|
| 61 |
+ query.Set("remote", options.RemoteContext)
|
|
| 62 |
+ } |
|
| 63 |
+ if options.NoCache {
|
|
| 64 |
+ query.Set("nocache", "1")
|
|
| 65 |
+ } |
|
| 66 |
+ if !options.Remove {
|
|
| 67 |
+ // only send value when opting out because the daemon's default is |
|
| 68 |
+ // to remove intermediate containers after a successful build, |
|
| 69 |
+ // |
|
| 70 |
+ // TODO(thaJeztah): deprecate "Remove" option, and provide a "NoRemove" or "Keep" option instead. |
|
| 71 |
+ query.Set("rm", "0")
|
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ if options.ForceRemove {
|
|
| 75 |
+ query.Set("forcerm", "1")
|
|
| 76 |
+ } |
|
| 77 |
+ |
|
| 78 |
+ if options.PullParent {
|
|
| 79 |
+ query.Set("pull", "1")
|
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ if options.Squash {
|
|
| 83 |
+ if err := cli.NewVersionError(ctx, "1.25", "squash"); err != nil {
|
|
| 84 |
+ return query, err |
|
| 85 |
+ } |
|
| 86 |
+ query.Set("squash", "1")
|
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ if !container.Isolation.IsDefault(options.Isolation) {
|
|
| 90 |
+ query.Set("isolation", string(options.Isolation))
|
|
| 91 |
+ } |
|
| 92 |
+ |
|
| 93 |
+ if options.CPUSetCPUs != "" {
|
|
| 94 |
+ query.Set("cpusetcpus", options.CPUSetCPUs)
|
|
| 95 |
+ } |
|
| 96 |
+ if options.NetworkMode != "" && options.NetworkMode != network.NetworkDefault {
|
|
| 97 |
+ query.Set("networkmode", options.NetworkMode)
|
|
| 98 |
+ } |
|
| 99 |
+ if options.CPUSetMems != "" {
|
|
| 100 |
+ query.Set("cpusetmems", options.CPUSetMems)
|
|
| 101 |
+ } |
|
| 102 |
+ if options.CPUShares != 0 {
|
|
| 103 |
+ query.Set("cpushares", strconv.FormatInt(options.CPUShares, 10))
|
|
| 104 |
+ } |
|
| 105 |
+ if options.CPUQuota != 0 {
|
|
| 106 |
+ query.Set("cpuquota", strconv.FormatInt(options.CPUQuota, 10))
|
|
| 107 |
+ } |
|
| 108 |
+ if options.CPUPeriod != 0 {
|
|
| 109 |
+ query.Set("cpuperiod", strconv.FormatInt(options.CPUPeriod, 10))
|
|
| 110 |
+ } |
|
| 111 |
+ if options.Memory != 0 {
|
|
| 112 |
+ query.Set("memory", strconv.FormatInt(options.Memory, 10))
|
|
| 113 |
+ } |
|
| 114 |
+ if options.MemorySwap != 0 {
|
|
| 115 |
+ query.Set("memswap", strconv.FormatInt(options.MemorySwap, 10))
|
|
| 116 |
+ } |
|
| 117 |
+ if options.CgroupParent != "" {
|
|
| 118 |
+ query.Set("cgroupparent", options.CgroupParent)
|
|
| 119 |
+ } |
|
| 120 |
+ if options.ShmSize != 0 {
|
|
| 121 |
+ query.Set("shmsize", strconv.FormatInt(options.ShmSize, 10))
|
|
| 122 |
+ } |
|
| 123 |
+ if options.Dockerfile != "" {
|
|
| 124 |
+ query.Set("dockerfile", options.Dockerfile)
|
|
| 125 |
+ } |
|
| 126 |
+ if options.Target != "" {
|
|
| 127 |
+ query.Set("target", options.Target)
|
|
| 128 |
+ } |
|
| 129 |
+ if len(options.Ulimits) != 0 {
|
|
| 130 |
+ ulimitsJSON, err := json.Marshal(options.Ulimits) |
|
| 131 |
+ if err != nil {
|
|
| 132 |
+ return query, err |
|
| 133 |
+ } |
|
| 134 |
+ query.Set("ulimits", string(ulimitsJSON))
|
|
| 135 |
+ } |
|
| 136 |
+ if len(options.BuildArgs) != 0 {
|
|
| 137 |
+ buildArgsJSON, err := json.Marshal(options.BuildArgs) |
|
| 138 |
+ if err != nil {
|
|
| 139 |
+ return query, err |
|
| 140 |
+ } |
|
| 141 |
+ query.Set("buildargs", string(buildArgsJSON))
|
|
| 142 |
+ } |
|
| 143 |
+ if len(options.Labels) != 0 {
|
|
| 144 |
+ labelsJSON, err := json.Marshal(options.Labels) |
|
| 145 |
+ if err != nil {
|
|
| 146 |
+ return query, err |
|
| 147 |
+ } |
|
| 148 |
+ query.Set("labels", string(labelsJSON))
|
|
| 149 |
+ } |
|
| 150 |
+ if len(options.CacheFrom) != 0 {
|
|
| 151 |
+ cacheFromJSON, err := json.Marshal(options.CacheFrom) |
|
| 152 |
+ if err != nil {
|
|
| 153 |
+ return query, err |
|
| 154 |
+ } |
|
| 155 |
+ query.Set("cachefrom", string(cacheFromJSON))
|
|
| 156 |
+ } |
|
| 157 |
+ if options.SessionID != "" {
|
|
| 158 |
+ query.Set("session", options.SessionID)
|
|
| 159 |
+ } |
|
| 160 |
+ if options.Platform != "" {
|
|
| 161 |
+ if err := cli.NewVersionError(ctx, "1.32", "platform"); err != nil {
|
|
| 162 |
+ return query, err |
|
| 163 |
+ } |
|
| 164 |
+ query.Set("platform", strings.ToLower(options.Platform))
|
|
| 165 |
+ } |
|
| 166 |
+ if options.BuildID != "" {
|
|
| 167 |
+ query.Set("buildid", options.BuildID)
|
|
| 168 |
+ } |
|
| 169 |
+ if options.Version != "" {
|
|
| 170 |
+ query.Set("version", string(options.Version))
|
|
| 171 |
+ } |
|
| 172 |
+ |
|
| 173 |
+ if options.Outputs != nil {
|
|
| 174 |
+ outputsJSON, err := json.Marshal(options.Outputs) |
|
| 175 |
+ if err != nil {
|
|
| 176 |
+ return query, err |
|
| 177 |
+ } |
|
| 178 |
+ query.Set("outputs", string(outputsJSON))
|
|
| 179 |
+ } |
|
| 180 |
+ return query, nil |
|
| 181 |
+} |
| 0 | 182 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,48 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/http" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ "strings" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/distribution/reference" |
|
| 10 |
+ "github.com/moby/moby/api/types/image" |
|
| 11 |
+ "github.com/moby/moby/api/types/registry" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+// ImageCreate creates a new image based on the parent options. |
|
| 15 |
+// It returns the JSON content in the response body. |
|
| 16 |
+func (cli *Client) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
|
| 17 |
+ ref, err := reference.ParseNormalizedNamed(parentReference) |
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return nil, err |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ query := url.Values{}
|
|
| 23 |
+ query.Set("fromImage", ref.Name())
|
|
| 24 |
+ query.Set("tag", getAPITagFromNamedRef(ref))
|
|
| 25 |
+ if options.Platform != "" {
|
|
| 26 |
+ query.Set("platform", strings.ToLower(options.Platform))
|
|
| 27 |
+ } |
|
| 28 |
+ resp, err := cli.tryImageCreate(ctx, query, staticAuth(options.RegistryAuth)) |
|
| 29 |
+ if err != nil {
|
|
| 30 |
+ return nil, err |
|
| 31 |
+ } |
|
| 32 |
+ return resp.Body, nil |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, resolveAuth registry.RequestAuthConfig) (*http.Response, error) {
|
|
| 36 |
+ hdr := http.Header{}
|
|
| 37 |
+ if resolveAuth != nil {
|
|
| 38 |
+ registryAuth, err := resolveAuth(ctx) |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return nil, err |
|
| 41 |
+ } |
|
| 42 |
+ if registryAuth != "" {
|
|
| 43 |
+ hdr.Set(registry.AuthHeader, registryAuth) |
|
| 44 |
+ } |
|
| 45 |
+ } |
|
| 46 |
+ return cli.post(ctx, "/images/create", query, nil, hdr) |
|
| 47 |
+} |
| 0 | 48 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,56 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/image" |
|
| 9 |
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// ImageHistoryWithPlatform sets the platform for the image history operation. |
|
| 13 |
+func ImageHistoryWithPlatform(platform ocispec.Platform) ImageHistoryOption {
|
|
| 14 |
+ return imageHistoryOptionFunc(func(opt *imageHistoryOpts) error {
|
|
| 15 |
+ if opt.apiOptions.Platform != nil {
|
|
| 16 |
+ return fmt.Errorf("platform already set to %s", *opt.apiOptions.Platform)
|
|
| 17 |
+ } |
|
| 18 |
+ opt.apiOptions.Platform = &platform |
|
| 19 |
+ return nil |
|
| 20 |
+ }) |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+// ImageHistory returns the changes in an image in history format. |
|
| 24 |
+func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts ...ImageHistoryOption) ([]image.HistoryResponseItem, error) {
|
|
| 25 |
+ query := url.Values{}
|
|
| 26 |
+ |
|
| 27 |
+ var opts imageHistoryOpts |
|
| 28 |
+ for _, o := range historyOpts {
|
|
| 29 |
+ if err := o.Apply(&opts); err != nil {
|
|
| 30 |
+ return nil, err |
|
| 31 |
+ } |
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ if opts.apiOptions.Platform != nil {
|
|
| 35 |
+ if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
|
|
| 36 |
+ return nil, err |
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ p, err := encodePlatform(opts.apiOptions.Platform) |
|
| 40 |
+ if err != nil {
|
|
| 41 |
+ return nil, err |
|
| 42 |
+ } |
|
| 43 |
+ query.Set("platform", p)
|
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ resp, err := cli.get(ctx, "/images/"+imageID+"/history", query, nil) |
|
| 47 |
+ defer ensureReaderClosed(resp) |
|
| 48 |
+ if err != nil {
|
|
| 49 |
+ return nil, err |
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ var history []image.HistoryResponseItem |
|
| 53 |
+ err = json.NewDecoder(resp.Body).Decode(&history) |
|
| 54 |
+ return history, err |
|
| 55 |
+} |
| 0 | 56 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,19 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "github.com/moby/moby/api/types/image" |
|
| 4 |
+) |
|
| 5 |
+ |
|
| 6 |
+// ImageHistoryOption is a type representing functional options for the image history operation. |
|
| 7 |
+type ImageHistoryOption interface {
|
|
| 8 |
+ Apply(*imageHistoryOpts) error |
|
| 9 |
+} |
|
| 10 |
+type imageHistoryOptionFunc func(opt *imageHistoryOpts) error |
|
| 11 |
+ |
|
| 12 |
+func (f imageHistoryOptionFunc) Apply(o *imageHistoryOpts) error {
|
|
| 13 |
+ return f(o) |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+type imageHistoryOpts struct {
|
|
| 17 |
+ apiOptions image.HistoryOptions |
|
| 18 |
+} |
| 0 | 19 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,48 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/distribution/reference" |
|
| 9 |
+ "github.com/moby/moby/api/types/image" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// ImageImport creates a new image based on the source options. |
|
| 13 |
+// It returns the JSON content in the response body. |
|
| 14 |
+func (cli *Client) ImageImport(ctx context.Context, source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
|
|
| 15 |
+ if ref != "" {
|
|
| 16 |
+ // Check if the given image name can be resolved |
|
| 17 |
+ if _, err := reference.ParseNormalizedNamed(ref); err != nil {
|
|
| 18 |
+ return nil, err |
|
| 19 |
+ } |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ query := url.Values{}
|
|
| 23 |
+ if source.SourceName != "" {
|
|
| 24 |
+ query.Set("fromSrc", source.SourceName)
|
|
| 25 |
+ } |
|
| 26 |
+ if ref != "" {
|
|
| 27 |
+ query.Set("repo", ref)
|
|
| 28 |
+ } |
|
| 29 |
+ if options.Tag != "" {
|
|
| 30 |
+ query.Set("tag", options.Tag)
|
|
| 31 |
+ } |
|
| 32 |
+ if options.Message != "" {
|
|
| 33 |
+ query.Set("message", options.Message)
|
|
| 34 |
+ } |
|
| 35 |
+ if options.Platform != "" {
|
|
| 36 |
+ query.Set("platform", strings.ToLower(options.Platform))
|
|
| 37 |
+ } |
|
| 38 |
+ for _, change := range options.Changes {
|
|
| 39 |
+ query.Add("changes", change)
|
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ resp, err := cli.postRaw(ctx, "/images/create", query, source.Source, nil) |
|
| 43 |
+ if err != nil {
|
|
| 44 |
+ return nil, err |
|
| 45 |
+ } |
|
| 46 |
+ return resp.Body, nil |
|
| 47 |
+} |
| 0 | 48 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,76 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "fmt" |
|
| 7 |
+ "io" |
|
| 8 |
+ "net/url" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/moby/moby/api/types/image" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// ImageInspect returns the image information. |
|
| 14 |
+func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (image.InspectResponse, error) {
|
|
| 15 |
+ if imageID == "" {
|
|
| 16 |
+ return image.InspectResponse{}, objectNotFoundError{object: "image", id: imageID}
|
|
| 17 |
+ } |
|
| 18 |
+ |
|
| 19 |
+ var opts imageInspectOpts |
|
| 20 |
+ for _, opt := range inspectOpts {
|
|
| 21 |
+ if err := opt.Apply(&opts); err != nil {
|
|
| 22 |
+ return image.InspectResponse{}, fmt.Errorf("error applying image inspect option: %w", err)
|
|
| 23 |
+ } |
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ query := url.Values{}
|
|
| 27 |
+ if opts.apiOptions.Manifests {
|
|
| 28 |
+ if err := cli.NewVersionError(ctx, "1.48", "manifests"); err != nil {
|
|
| 29 |
+ return image.InspectResponse{}, err
|
|
| 30 |
+ } |
|
| 31 |
+ query.Set("manifests", "1")
|
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ if opts.apiOptions.Platform != nil {
|
|
| 35 |
+ if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil {
|
|
| 36 |
+ return image.InspectResponse{}, err
|
|
| 37 |
+ } |
|
| 38 |
+ platform, err := encodePlatform(opts.apiOptions.Platform) |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return image.InspectResponse{}, err
|
|
| 41 |
+ } |
|
| 42 |
+ query.Set("platform", platform)
|
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ resp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil) |
|
| 46 |
+ defer ensureReaderClosed(resp) |
|
| 47 |
+ if err != nil {
|
|
| 48 |
+ return image.InspectResponse{}, err
|
|
| 49 |
+ } |
|
| 50 |
+ |
|
| 51 |
+ buf := opts.raw |
|
| 52 |
+ if buf == nil {
|
|
| 53 |
+ buf = &bytes.Buffer{}
|
|
| 54 |
+ } |
|
| 55 |
+ |
|
| 56 |
+ if _, err := io.Copy(buf, resp.Body); err != nil {
|
|
| 57 |
+ return image.InspectResponse{}, err
|
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ var response image.InspectResponse |
|
| 61 |
+ err = json.Unmarshal(buf.Bytes(), &response) |
|
| 62 |
+ return response, err |
|
| 63 |
+} |
|
| 64 |
+ |
|
| 65 |
+// ImageInspectWithRaw returns the image information and its raw representation. |
|
| 66 |
+// |
|
| 67 |
+// Deprecated: Use [Client.ImageInspect] instead. Raw response can be obtained using the [ImageInspectWithRawResponse] option. |
|
| 68 |
+func (cli *Client) ImageInspectWithRaw(ctx context.Context, imageID string) (image.InspectResponse, []byte, error) {
|
|
| 69 |
+ var buf bytes.Buffer |
|
| 70 |
+ resp, err := cli.ImageInspect(ctx, imageID, ImageInspectWithRawResponse(&buf)) |
|
| 71 |
+ if err != nil {
|
|
| 72 |
+ return image.InspectResponse{}, nil, err
|
|
| 73 |
+ } |
|
| 74 |
+ return resp, buf.Bytes(), err |
|
| 75 |
+} |
| 0 | 76 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,62 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/moby/moby/api/types/image" |
|
| 6 |
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// ImageInspectOption is a type representing functional options for the image inspect operation. |
|
| 10 |
+type ImageInspectOption interface {
|
|
| 11 |
+ Apply(*imageInspectOpts) error |
|
| 12 |
+} |
|
| 13 |
+type imageInspectOptionFunc func(opt *imageInspectOpts) error |
|
| 14 |
+ |
|
| 15 |
+func (f imageInspectOptionFunc) Apply(o *imageInspectOpts) error {
|
|
| 16 |
+ return f(o) |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+// ImageInspectWithRawResponse instructs the client to additionally store the |
|
| 20 |
+// raw inspect response in the provided buffer. |
|
| 21 |
+func ImageInspectWithRawResponse(raw *bytes.Buffer) ImageInspectOption {
|
|
| 22 |
+ return imageInspectOptionFunc(func(opts *imageInspectOpts) error {
|
|
| 23 |
+ opts.raw = raw |
|
| 24 |
+ return nil |
|
| 25 |
+ }) |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+// ImageInspectWithManifests sets manifests API option for the image inspect operation. |
|
| 29 |
+// This option is only available for API version 1.48 and up. |
|
| 30 |
+// With this option set, the image inspect operation response will have the |
|
| 31 |
+// [image.InspectResponse.Manifests] field populated if the server is multi-platform capable. |
|
| 32 |
+func ImageInspectWithManifests(manifests bool) ImageInspectOption {
|
|
| 33 |
+ return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error {
|
|
| 34 |
+ clientOpts.apiOptions.Manifests = manifests |
|
| 35 |
+ return nil |
|
| 36 |
+ }) |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+// ImageInspectWithPlatform sets platform API option for the image inspect operation. |
|
| 40 |
+// This option is only available for API version 1.49 and up. |
|
| 41 |
+// With this option set, the image inspect operation will return information for the |
|
| 42 |
+// specified platform variant of the multi-platform image. |
|
| 43 |
+func ImageInspectWithPlatform(platform *ocispec.Platform) ImageInspectOption {
|
|
| 44 |
+ return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error {
|
|
| 45 |
+ clientOpts.apiOptions.Platform = platform |
|
| 46 |
+ return nil |
|
| 47 |
+ }) |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+// ImageInspectWithAPIOpts sets the API options for the image inspect operation. |
|
| 51 |
+func ImageInspectWithAPIOpts(opts image.InspectOptions) ImageInspectOption {
|
|
| 52 |
+ return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error {
|
|
| 53 |
+ clientOpts.apiOptions = opts |
|
| 54 |
+ return nil |
|
| 55 |
+ }) |
|
| 56 |
+} |
|
| 57 |
+ |
|
| 58 |
+type imageInspectOpts struct {
|
|
| 59 |
+ raw *bytes.Buffer |
|
| 60 |
+ apiOptions image.InspectOptions |
|
| 61 |
+} |
| 0 | 62 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,67 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/filters" |
|
| 8 |
+ "github.com/moby/moby/api/types/image" |
|
| 9 |
+ "github.com/moby/moby/api/types/versions" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// ImageList returns a list of images in the docker host. |
|
| 13 |
+// |
|
| 14 |
+// Experimental: Setting the [options.Manifest] will populate |
|
| 15 |
+// [image.Summary.Manifests] with information about image manifests. |
|
| 16 |
+// This is experimental and might change in the future without any backward |
|
| 17 |
+// compatibility. |
|
| 18 |
+func (cli *Client) ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error) {
|
|
| 19 |
+ var images []image.Summary |
|
| 20 |
+ |
|
| 21 |
+ // Make sure we negotiated (if the client is configured to do so), |
|
| 22 |
+ // as code below contains API-version specific handling of options. |
|
| 23 |
+ // |
|
| 24 |
+ // Normally, version-negotiation (if enabled) would not happen until |
|
| 25 |
+ // the API request is made. |
|
| 26 |
+ if err := cli.checkVersion(ctx); err != nil {
|
|
| 27 |
+ return images, err |
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ query := url.Values{}
|
|
| 31 |
+ |
|
| 32 |
+ optionFilters := options.Filters |
|
| 33 |
+ referenceFilters := optionFilters.Get("reference")
|
|
| 34 |
+ if versions.LessThan(cli.version, "1.25") && len(referenceFilters) > 0 {
|
|
| 35 |
+ query.Set("filter", referenceFilters[0])
|
|
| 36 |
+ for _, filterValue := range referenceFilters {
|
|
| 37 |
+ optionFilters.Del("reference", filterValue)
|
|
| 38 |
+ } |
|
| 39 |
+ } |
|
| 40 |
+ if optionFilters.Len() > 0 {
|
|
| 41 |
+ //nolint:staticcheck // ignore SA1019 for old code |
|
| 42 |
+ filterJSON, err := filters.ToParamWithVersion(cli.version, optionFilters) |
|
| 43 |
+ if err != nil {
|
|
| 44 |
+ return images, err |
|
| 45 |
+ } |
|
| 46 |
+ query.Set("filters", filterJSON)
|
|
| 47 |
+ } |
|
| 48 |
+ if options.All {
|
|
| 49 |
+ query.Set("all", "1")
|
|
| 50 |
+ } |
|
| 51 |
+ if options.SharedSize && versions.GreaterThanOrEqualTo(cli.version, "1.42") {
|
|
| 52 |
+ query.Set("shared-size", "1")
|
|
| 53 |
+ } |
|
| 54 |
+ if options.Manifests && versions.GreaterThanOrEqualTo(cli.version, "1.47") {
|
|
| 55 |
+ query.Set("manifests", "1")
|
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ resp, err := cli.get(ctx, "/images/json", query, nil) |
|
| 59 |
+ defer ensureReaderClosed(resp) |
|
| 60 |
+ if err != nil {
|
|
| 61 |
+ return images, err |
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 64 |
+ err = json.NewDecoder(resp.Body).Decode(&images) |
|
| 65 |
+ return images, err |
|
| 66 |
+} |
| 0 | 67 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,54 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/http" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/image" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// ImageLoad loads an image in the docker host from the client host. |
|
| 12 |
+// It's up to the caller to close the io.ReadCloser in the |
|
| 13 |
+// ImageLoadResponse returned by this function. |
|
| 14 |
+// |
|
| 15 |
+// Platform is an optional parameter that specifies the platform to load from |
|
| 16 |
+// the provided multi-platform image. This is only has effect if the input image |
|
| 17 |
+// is a multi-platform image. |
|
| 18 |
+func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (image.LoadResponse, error) {
|
|
| 19 |
+ var opts imageLoadOpts |
|
| 20 |
+ for _, opt := range loadOpts {
|
|
| 21 |
+ if err := opt.Apply(&opts); err != nil {
|
|
| 22 |
+ return image.LoadResponse{}, err
|
|
| 23 |
+ } |
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ query := url.Values{}
|
|
| 27 |
+ query.Set("quiet", "0")
|
|
| 28 |
+ if opts.apiOptions.Quiet {
|
|
| 29 |
+ query.Set("quiet", "1")
|
|
| 30 |
+ } |
|
| 31 |
+ if len(opts.apiOptions.Platforms) > 0 {
|
|
| 32 |
+ if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
|
|
| 33 |
+ return image.LoadResponse{}, err
|
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ p, err := encodePlatforms(opts.apiOptions.Platforms...) |
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ return image.LoadResponse{}, err
|
|
| 39 |
+ } |
|
| 40 |
+ query["platform"] = p |
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ resp, err := cli.postRaw(ctx, "/images/load", query, input, http.Header{
|
|
| 44 |
+ "Content-Type": {"application/x-tar"},
|
|
| 45 |
+ }) |
|
| 46 |
+ if err != nil {
|
|
| 47 |
+ return image.LoadResponse{}, err
|
|
| 48 |
+ } |
|
| 49 |
+ return image.LoadResponse{
|
|
| 50 |
+ Body: resp.Body, |
|
| 51 |
+ JSON: resp.Header.Get("Content-Type") == "application/json",
|
|
| 52 |
+ }, nil |
|
| 53 |
+} |
| 0 | 54 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,41 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/moby/moby/api/types/image" |
|
| 6 |
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// ImageLoadOption is a type representing functional options for the image load operation. |
|
| 10 |
+type ImageLoadOption interface {
|
|
| 11 |
+ Apply(*imageLoadOpts) error |
|
| 12 |
+} |
|
| 13 |
+type imageLoadOptionFunc func(opt *imageLoadOpts) error |
|
| 14 |
+ |
|
| 15 |
+func (f imageLoadOptionFunc) Apply(o *imageLoadOpts) error {
|
|
| 16 |
+ return f(o) |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+type imageLoadOpts struct {
|
|
| 20 |
+ apiOptions image.LoadOptions |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+// ImageLoadWithQuiet sets the quiet option for the image load operation. |
|
| 24 |
+func ImageLoadWithQuiet(quiet bool) ImageLoadOption {
|
|
| 25 |
+ return imageLoadOptionFunc(func(opt *imageLoadOpts) error {
|
|
| 26 |
+ opt.apiOptions.Quiet = quiet |
|
| 27 |
+ return nil |
|
| 28 |
+ }) |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+// ImageLoadWithPlatforms sets the platforms to be loaded from the image. |
|
| 32 |
+func ImageLoadWithPlatforms(platforms ...ocispec.Platform) ImageLoadOption {
|
|
| 33 |
+ return imageLoadOptionFunc(func(opt *imageLoadOpts) error {
|
|
| 34 |
+ if opt.apiOptions.Platforms != nil {
|
|
| 35 |
+ return fmt.Errorf("platforms already set to %v", opt.apiOptions.Platforms)
|
|
| 36 |
+ } |
|
| 37 |
+ opt.apiOptions.Platforms = platforms |
|
| 38 |
+ return nil |
|
| 39 |
+ }) |
|
| 40 |
+} |
| 0 | 41 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,35 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/filters" |
|
| 8 |
+ "github.com/moby/moby/api/types/image" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// ImagesPrune requests the daemon to delete unused data |
|
| 12 |
+func (cli *Client) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (image.PruneReport, error) {
|
|
| 13 |
+ if err := cli.NewVersionError(ctx, "1.25", "image prune"); err != nil {
|
|
| 14 |
+ return image.PruneReport{}, err
|
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ query, err := getFiltersQuery(pruneFilters) |
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return image.PruneReport{}, err
|
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ resp, err := cli.post(ctx, "/images/prune", query, nil, nil) |
|
| 23 |
+ defer ensureReaderClosed(resp) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return image.PruneReport{}, err
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ var report image.PruneReport |
|
| 29 |
+ if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
|
|
| 30 |
+ return image.PruneReport{}, fmt.Errorf("Error retrieving disk usage: %v", err)
|
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ return report, nil |
|
| 34 |
+} |
| 0 | 35 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,60 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ cerrdefs "github.com/containerd/errdefs" |
|
| 9 |
+ "github.com/distribution/reference" |
|
| 10 |
+ "github.com/moby/moby/api/types/image" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// ImagePull requests the docker host to pull an image from a remote registry. |
|
| 14 |
+// It executes the privileged function if the operation is unauthorized |
|
| 15 |
+// and it tries one more time. |
|
| 16 |
+// It's up to the caller to handle the io.ReadCloser and close it properly. |
|
| 17 |
+// |
|
| 18 |
+// FIXME(vdemeester): there is currently used in a few way in docker/docker |
|
| 19 |
+// - if not in trusted content, ref is used to pass the whole reference, and tag is empty |
|
| 20 |
+// - if in trusted content, ref is used to pass the reference name, and tag for the digest |
|
| 21 |
+func (cli *Client) ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) {
|
|
| 22 |
+ ref, err := reference.ParseNormalizedNamed(refStr) |
|
| 23 |
+ if err != nil {
|
|
| 24 |
+ return nil, err |
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ query := url.Values{}
|
|
| 28 |
+ query.Set("fromImage", ref.Name())
|
|
| 29 |
+ if !options.All {
|
|
| 30 |
+ query.Set("tag", getAPITagFromNamedRef(ref))
|
|
| 31 |
+ } |
|
| 32 |
+ if options.Platform != "" {
|
|
| 33 |
+ query.Set("platform", strings.ToLower(options.Platform))
|
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ resp, err := cli.tryImageCreate(ctx, query, staticAuth(options.RegistryAuth)) |
|
| 37 |
+ if cerrdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil {
|
|
| 38 |
+ resp, err = cli.tryImageCreate(ctx, query, options.PrivilegeFunc) |
|
| 39 |
+ } |
|
| 40 |
+ if err != nil {
|
|
| 41 |
+ return nil, err |
|
| 42 |
+ } |
|
| 43 |
+ return resp.Body, nil |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+// getAPITagFromNamedRef returns a tag from the specified reference. |
|
| 47 |
+// This function is necessary as long as the docker "server" api expects |
|
| 48 |
+// digests to be sent as tags and makes a distinction between the name |
|
| 49 |
+// and tag/digest part of a reference. |
|
| 50 |
+func getAPITagFromNamedRef(ref reference.Named) string {
|
|
| 51 |
+ if digested, ok := ref.(reference.Digested); ok {
|
|
| 52 |
+ return digested.Digest().String() |
|
| 53 |
+ } |
|
| 54 |
+ ref = reference.TagNameOnly(ref) |
|
| 55 |
+ if tagged, ok := ref.(reference.Tagged); ok {
|
|
| 56 |
+ return tagged.Tag() |
|
| 57 |
+ } |
|
| 58 |
+ return "" |
|
| 59 |
+} |
| 0 | 60 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,86 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "errors" |
|
| 6 |
+ "fmt" |
|
| 7 |
+ "io" |
|
| 8 |
+ "net/http" |
|
| 9 |
+ "net/url" |
|
| 10 |
+ |
|
| 11 |
+ cerrdefs "github.com/containerd/errdefs" |
|
| 12 |
+ "github.com/distribution/reference" |
|
| 13 |
+ "github.com/moby/moby/api/types/image" |
|
| 14 |
+ "github.com/moby/moby/api/types/registry" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+// ImagePush requests the docker host to push an image to a remote registry. |
|
| 18 |
+// It executes the privileged function if the operation is unauthorized |
|
| 19 |
+// and it tries one more time. |
|
| 20 |
+// It's up to the caller to handle the io.ReadCloser and close it properly. |
|
| 21 |
+func (cli *Client) ImagePush(ctx context.Context, image string, options image.PushOptions) (io.ReadCloser, error) {
|
|
| 22 |
+ ref, err := reference.ParseNormalizedNamed(image) |
|
| 23 |
+ if err != nil {
|
|
| 24 |
+ return nil, err |
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ if _, isCanonical := ref.(reference.Canonical); isCanonical {
|
|
| 28 |
+ return nil, errors.New("cannot push a digest reference")
|
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ query := url.Values{}
|
|
| 32 |
+ if !options.All {
|
|
| 33 |
+ ref = reference.TagNameOnly(ref) |
|
| 34 |
+ if tagged, ok := ref.(reference.Tagged); ok {
|
|
| 35 |
+ query.Set("tag", tagged.Tag())
|
|
| 36 |
+ } |
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ if options.Platform != nil {
|
|
| 40 |
+ if err := cli.NewVersionError(ctx, "1.46", "platform"); err != nil {
|
|
| 41 |
+ return nil, err |
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ p := *options.Platform |
|
| 45 |
+ pJson, err := json.Marshal(p) |
|
| 46 |
+ if err != nil {
|
|
| 47 |
+ return nil, fmt.Errorf("invalid platform: %v", err)
|
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ query.Set("platform", string(pJson))
|
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ resp, err := cli.tryImagePush(ctx, ref.Name(), query, staticAuth(options.RegistryAuth)) |
|
| 54 |
+ if cerrdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil {
|
|
| 55 |
+ resp, err = cli.tryImagePush(ctx, ref.Name(), query, options.PrivilegeFunc) |
|
| 56 |
+ } |
|
| 57 |
+ if err != nil {
|
|
| 58 |
+ return nil, err |
|
| 59 |
+ } |
|
| 60 |
+ return resp.Body, nil |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 63 |
+func (cli *Client) tryImagePush(ctx context.Context, imageID string, query url.Values, resolveAuth registry.RequestAuthConfig) (*http.Response, error) {
|
|
| 64 |
+ hdr := http.Header{}
|
|
| 65 |
+ if resolveAuth != nil {
|
|
| 66 |
+ registryAuth, err := resolveAuth(ctx) |
|
| 67 |
+ if err != nil {
|
|
| 68 |
+ return nil, err |
|
| 69 |
+ } |
|
| 70 |
+ if registryAuth != "" {
|
|
| 71 |
+ hdr.Set(registry.AuthHeader, registryAuth) |
|
| 72 |
+ } |
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ // Always send a body (which may be an empty JSON document ("{}")) to prevent
|
|
| 76 |
+ // EOF errors on older daemons which had faulty fallback code for handling |
|
| 77 |
+ // authentication in the body when no auth-header was set, resulting in; |
|
| 78 |
+ // |
|
| 79 |
+ // Error response from daemon: bad parameters and missing X-Registry-Auth: invalid X-Registry-Auth header: EOF |
|
| 80 |
+ // |
|
| 81 |
+ // We use [http.NoBody], which gets marshaled to an empty JSON document. |
|
| 82 |
+ // |
|
| 83 |
+ // see: https://github.com/moby/moby/commit/ea29dffaa541289591aa44fa85d2a596ce860e16 |
|
| 84 |
+ return cli.post(ctx, "/images/"+imageID+"/push", query, http.NoBody, hdr) |
|
| 85 |
+} |
| 0 | 86 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,39 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/image" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// ImageRemove removes an image from the docker host. |
|
| 11 |
+func (cli *Client) ImageRemove(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
|
|
| 12 |
+ query := url.Values{}
|
|
| 13 |
+ |
|
| 14 |
+ if options.Force {
|
|
| 15 |
+ query.Set("force", "1")
|
|
| 16 |
+ } |
|
| 17 |
+ if !options.PruneChildren {
|
|
| 18 |
+ query.Set("noprune", "1")
|
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ if len(options.Platforms) > 0 {
|
|
| 22 |
+ p, err := encodePlatforms(options.Platforms...) |
|
| 23 |
+ if err != nil {
|
|
| 24 |
+ return nil, err |
|
| 25 |
+ } |
|
| 26 |
+ query["platforms"] = p |
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ resp, err := cli.delete(ctx, "/images/"+imageID, query, nil) |
|
| 30 |
+ defer ensureReaderClosed(resp) |
|
| 31 |
+ if err != nil {
|
|
| 32 |
+ return nil, err |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ var dels []image.DeleteResponse |
|
| 36 |
+ err = json.NewDecoder(resp.Body).Decode(&dels) |
|
| 37 |
+ return dels, err |
|
| 38 |
+} |
| 0 | 39 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,41 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/url" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// ImageSave retrieves one or more images from the docker host as an io.ReadCloser. |
|
| 9 |
+// |
|
| 10 |
+// Platforms is an optional parameter that specifies the platforms to save from the image. |
|
| 11 |
+// This is only has effect if the input image is a multi-platform image. |
|
| 12 |
+func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts ...ImageSaveOption) (io.ReadCloser, error) {
|
|
| 13 |
+ var opts imageSaveOpts |
|
| 14 |
+ for _, opt := range saveOpts {
|
|
| 15 |
+ if err := opt.Apply(&opts); err != nil {
|
|
| 16 |
+ return nil, err |
|
| 17 |
+ } |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ query := url.Values{
|
|
| 21 |
+ "names": imageIDs, |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ if len(opts.apiOptions.Platforms) > 0 {
|
|
| 25 |
+ if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
|
|
| 26 |
+ return nil, err |
|
| 27 |
+ } |
|
| 28 |
+ p, err := encodePlatforms(opts.apiOptions.Platforms...) |
|
| 29 |
+ if err != nil {
|
|
| 30 |
+ return nil, err |
|
| 31 |
+ } |
|
| 32 |
+ query["platform"] = p |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ resp, err := cli.get(ctx, "/images/get", query, nil) |
|
| 36 |
+ if err != nil {
|
|
| 37 |
+ return nil, err |
|
| 38 |
+ } |
|
| 39 |
+ return resp.Body, nil |
|
| 40 |
+} |
| 0 | 41 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,33 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/moby/moby/api/types/image" |
|
| 6 |
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+type ImageSaveOption interface {
|
|
| 10 |
+ Apply(*imageSaveOpts) error |
|
| 11 |
+} |
|
| 12 |
+ |
|
| 13 |
+type imageSaveOptionFunc func(opt *imageSaveOpts) error |
|
| 14 |
+ |
|
| 15 |
+func (f imageSaveOptionFunc) Apply(o *imageSaveOpts) error {
|
|
| 16 |
+ return f(o) |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+// ImageSaveWithPlatforms sets the platforms to be saved from the image. |
|
| 20 |
+func ImageSaveWithPlatforms(platforms ...ocispec.Platform) ImageSaveOption {
|
|
| 21 |
+ return imageSaveOptionFunc(func(opt *imageSaveOpts) error {
|
|
| 22 |
+ if opt.apiOptions.Platforms != nil {
|
|
| 23 |
+ return fmt.Errorf("platforms already set to %v", opt.apiOptions.Platforms)
|
|
| 24 |
+ } |
|
| 25 |
+ opt.apiOptions.Platforms = platforms |
|
| 26 |
+ return nil |
|
| 27 |
+ }) |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+type imageSaveOpts struct {
|
|
| 31 |
+ apiOptions image.SaveOptions |
|
| 32 |
+} |
| 0 | 33 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,54 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/http" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ "strconv" |
|
| 8 |
+ |
|
| 9 |
+ cerrdefs "github.com/containerd/errdefs" |
|
| 10 |
+ "github.com/moby/moby/api/types/filters" |
|
| 11 |
+ "github.com/moby/moby/api/types/registry" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+// ImageSearch makes the docker host search by a term in a remote registry. |
|
| 15 |
+// The list of results is not sorted in any fashion. |
|
| 16 |
+func (cli *Client) ImageSearch(ctx context.Context, term string, options registry.SearchOptions) ([]registry.SearchResult, error) {
|
|
| 17 |
+ var results []registry.SearchResult |
|
| 18 |
+ query := url.Values{}
|
|
| 19 |
+ query.Set("term", term)
|
|
| 20 |
+ if options.Limit > 0 {
|
|
| 21 |
+ query.Set("limit", strconv.Itoa(options.Limit))
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ if options.Filters.Len() > 0 {
|
|
| 25 |
+ filterJSON, err := filters.ToJSON(options.Filters) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ return results, err |
|
| 28 |
+ } |
|
| 29 |
+ query.Set("filters", filterJSON)
|
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ resp, err := cli.tryImageSearch(ctx, query, options.RegistryAuth) |
|
| 33 |
+ defer ensureReaderClosed(resp) |
|
| 34 |
+ if cerrdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil {
|
|
| 35 |
+ newAuthHeader, privilegeErr := options.PrivilegeFunc(ctx) |
|
| 36 |
+ if privilegeErr != nil {
|
|
| 37 |
+ return results, privilegeErr |
|
| 38 |
+ } |
|
| 39 |
+ resp, err = cli.tryImageSearch(ctx, query, newAuthHeader) |
|
| 40 |
+ } |
|
| 41 |
+ if err != nil {
|
|
| 42 |
+ return results, err |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ err = json.NewDecoder(resp.Body).Decode(&results) |
|
| 46 |
+ return results, err |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+func (cli *Client) tryImageSearch(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) {
|
|
| 50 |
+ return cli.get(ctx, "/images/search", query, http.Header{
|
|
| 51 |
+ registry.AuthHeader: {registryAuth},
|
|
| 52 |
+ }) |
|
| 53 |
+} |
| 0 | 54 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,37 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/distribution/reference" |
|
| 7 |
+ "github.com/pkg/errors" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// ImageTag tags an image in the docker host |
|
| 11 |
+func (cli *Client) ImageTag(ctx context.Context, source, target string) error {
|
|
| 12 |
+ if _, err := reference.ParseAnyReference(source); err != nil {
|
|
| 13 |
+ return errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", source) |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ ref, err := reference.ParseNormalizedNamed(target) |
|
| 17 |
+ if err != nil {
|
|
| 18 |
+ return errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", target) |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ if _, isCanonical := ref.(reference.Canonical); isCanonical {
|
|
| 22 |
+ return errors.New("refusing to create a tag with a digest reference")
|
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ ref = reference.TagNameOnly(ref) |
|
| 26 |
+ |
|
| 27 |
+ query := url.Values{}
|
|
| 28 |
+ query.Set("repo", ref.Name())
|
|
| 29 |
+ if tagged, ok := ref.(reference.Tagged); ok {
|
|
| 30 |
+ query.Set("tag", tagged.Tag())
|
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ resp, err := cli.post(ctx, "/images/"+source+"/tag", query, nil, nil) |
|
| 34 |
+ ensureReaderClosed(resp) |
|
| 35 |
+ return err |
|
| 36 |
+} |
| 0 | 37 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,26 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/system" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// Info returns information about the docker server. |
|
| 12 |
+func (cli *Client) Info(ctx context.Context) (system.Info, error) {
|
|
| 13 |
+ var info system.Info |
|
| 14 |
+ resp, err := cli.get(ctx, "/info", url.Values{}, nil)
|
|
| 15 |
+ defer ensureReaderClosed(resp) |
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ return info, err |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
|
| 21 |
+ return info, fmt.Errorf("Error reading remote info: %v", err)
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ return info, nil |
|
| 25 |
+} |
| 0 | 26 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,24 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/registry" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// RegistryLogin authenticates the docker server with a given docker registry. |
|
| 11 |
+// It returns unauthorizedError when the authentication fails. |
|
| 12 |
+func (cli *Client) RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) {
|
|
| 13 |
+ resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil)
|
|
| 14 |
+ defer ensureReaderClosed(resp) |
|
| 15 |
+ |
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ return registry.AuthenticateOKBody{}, err
|
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ var response registry.AuthenticateOKBody |
|
| 21 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 22 |
+ return response, err |
|
| 23 |
+} |
| 0 | 24 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,28 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/moby/moby/api/types/network" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// NetworkConnect connects a container to an existent network in the docker host. |
|
| 9 |
+func (cli *Client) NetworkConnect(ctx context.Context, networkID, containerID string, config *network.EndpointSettings) error {
|
|
| 10 |
+ networkID, err := trimID("network", networkID)
|
|
| 11 |
+ if err != nil {
|
|
| 12 |
+ return err |
|
| 13 |
+ } |
|
| 14 |
+ |
|
| 15 |
+ containerID, err = trimID("container", containerID)
|
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ return err |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ nc := network.ConnectOptions{
|
|
| 21 |
+ Container: containerID, |
|
| 22 |
+ EndpointConfig: config, |
|
| 23 |
+ } |
|
| 24 |
+ resp, err := cli.post(ctx, "/networks/"+networkID+"/connect", nil, nc, nil) |
|
| 25 |
+ ensureReaderClosed(resp) |
|
| 26 |
+ return err |
|
| 27 |
+} |
| 0 | 28 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,40 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/network" |
|
| 7 |
+ "github.com/moby/moby/api/types/versions" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// NetworkCreate creates a new network in the docker host. |
|
| 11 |
+func (cli *Client) NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) {
|
|
| 12 |
+ // Make sure we negotiated (if the client is configured to do so), |
|
| 13 |
+ // as code below contains API-version specific handling of options. |
|
| 14 |
+ // |
|
| 15 |
+ // Normally, version-negotiation (if enabled) would not happen until |
|
| 16 |
+ // the API request is made. |
|
| 17 |
+ if err := cli.checkVersion(ctx); err != nil {
|
|
| 18 |
+ return network.CreateResponse{}, err
|
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ networkCreateRequest := network.CreateRequest{
|
|
| 22 |
+ CreateOptions: options, |
|
| 23 |
+ Name: name, |
|
| 24 |
+ } |
|
| 25 |
+ if versions.LessThan(cli.version, "1.44") {
|
|
| 26 |
+ enabled := true |
|
| 27 |
+ networkCreateRequest.CheckDuplicate = &enabled //nolint:staticcheck // ignore SA1019: CheckDuplicate is deprecated since API v1.44. |
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ resp, err := cli.post(ctx, "/networks/create", nil, networkCreateRequest, nil) |
|
| 31 |
+ defer ensureReaderClosed(resp) |
|
| 32 |
+ if err != nil {
|
|
| 33 |
+ return network.CreateResponse{}, err
|
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ var response network.CreateResponse |
|
| 37 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 38 |
+ return response, err |
|
| 39 |
+} |
| 0 | 40 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,28 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/moby/moby/api/types/network" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// NetworkDisconnect disconnects a container from an existent network in the docker host. |
|
| 9 |
+func (cli *Client) NetworkDisconnect(ctx context.Context, networkID, containerID string, force bool) error {
|
|
| 10 |
+ networkID, err := trimID("network", networkID)
|
|
| 11 |
+ if err != nil {
|
|
| 12 |
+ return err |
|
| 13 |
+ } |
|
| 14 |
+ |
|
| 15 |
+ containerID, err = trimID("container", containerID)
|
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ return err |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ nd := network.DisconnectOptions{
|
|
| 21 |
+ Container: containerID, |
|
| 22 |
+ Force: force, |
|
| 23 |
+ } |
|
| 24 |
+ resp, err := cli.post(ctx, "/networks/"+networkID+"/disconnect", nil, nd, nil) |
|
| 25 |
+ ensureReaderClosed(resp) |
|
| 26 |
+ return err |
|
| 27 |
+} |
| 0 | 28 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,47 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "io" |
|
| 7 |
+ "net/url" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/moby/moby/api/types/network" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// NetworkInspect returns the information for a specific network configured in the docker host. |
|
| 13 |
+func (cli *Client) NetworkInspect(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) {
|
|
| 14 |
+ networkResource, _, err := cli.NetworkInspectWithRaw(ctx, networkID, options) |
|
| 15 |
+ return networkResource, err |
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+// NetworkInspectWithRaw returns the information for a specific network configured in the docker host and its raw representation. |
|
| 19 |
+func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, []byte, error) {
|
|
| 20 |
+ networkID, err := trimID("network", networkID)
|
|
| 21 |
+ if err != nil {
|
|
| 22 |
+ return network.Inspect{}, nil, err
|
|
| 23 |
+ } |
|
| 24 |
+ query := url.Values{}
|
|
| 25 |
+ if options.Verbose {
|
|
| 26 |
+ query.Set("verbose", "true")
|
|
| 27 |
+ } |
|
| 28 |
+ if options.Scope != "" {
|
|
| 29 |
+ query.Set("scope", options.Scope)
|
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ resp, err := cli.get(ctx, "/networks/"+networkID, query, nil) |
|
| 33 |
+ defer ensureReaderClosed(resp) |
|
| 34 |
+ if err != nil {
|
|
| 35 |
+ return network.Inspect{}, nil, err
|
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ raw, err := io.ReadAll(resp.Body) |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return network.Inspect{}, nil, err
|
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ var nw network.Inspect |
|
| 44 |
+ err = json.NewDecoder(bytes.NewReader(raw)).Decode(&nw) |
|
| 45 |
+ return nw, raw, err |
|
| 46 |
+} |
| 0 | 47 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,32 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/filters" |
|
| 8 |
+ "github.com/moby/moby/api/types/network" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// NetworkList returns the list of networks configured in the docker host. |
|
| 12 |
+func (cli *Client) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
|
| 13 |
+ query := url.Values{}
|
|
| 14 |
+ if options.Filters.Len() > 0 {
|
|
| 15 |
+ //nolint:staticcheck // ignore SA1019 for old code |
|
| 16 |
+ filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) |
|
| 17 |
+ if err != nil {
|
|
| 18 |
+ return nil, err |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ query.Set("filters", filterJSON)
|
|
| 22 |
+ } |
|
| 23 |
+ var networkResources []network.Summary |
|
| 24 |
+ resp, err := cli.get(ctx, "/networks", query, nil) |
|
| 25 |
+ defer ensureReaderClosed(resp) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ return networkResources, err |
|
| 28 |
+ } |
|
| 29 |
+ err = json.NewDecoder(resp.Body).Decode(&networkResources) |
|
| 30 |
+ return networkResources, err |
|
| 31 |
+} |
| 0 | 32 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,35 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/filters" |
|
| 8 |
+ "github.com/moby/moby/api/types/network" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// NetworksPrune requests the daemon to delete unused networks |
|
| 12 |
+func (cli *Client) NetworksPrune(ctx context.Context, pruneFilters filters.Args) (network.PruneReport, error) {
|
|
| 13 |
+ if err := cli.NewVersionError(ctx, "1.25", "network prune"); err != nil {
|
|
| 14 |
+ return network.PruneReport{}, err
|
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ query, err := getFiltersQuery(pruneFilters) |
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return network.PruneReport{}, err
|
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ resp, err := cli.post(ctx, "/networks/prune", query, nil, nil) |
|
| 23 |
+ defer ensureReaderClosed(resp) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return network.PruneReport{}, err
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ var report network.PruneReport |
|
| 29 |
+ if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
|
|
| 30 |
+ return network.PruneReport{}, fmt.Errorf("Error retrieving network prune report: %v", err)
|
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ return report, nil |
|
| 34 |
+} |
| 0 | 35 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,14 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import "context" |
|
| 3 |
+ |
|
| 4 |
+// NetworkRemove removes an existent network from the docker host. |
|
| 5 |
+func (cli *Client) NetworkRemove(ctx context.Context, networkID string) error {
|
|
| 6 |
+ networkID, err := trimID("network", networkID)
|
|
| 7 |
+ if err != nil {
|
|
| 8 |
+ return err |
|
| 9 |
+ } |
|
| 10 |
+ resp, err := cli.delete(ctx, "/networks/"+networkID, nil, nil) |
|
| 11 |
+ defer ensureReaderClosed(resp) |
|
| 12 |
+ return err |
|
| 13 |
+} |
| 0 | 14 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,33 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "io" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// NodeInspectWithRaw returns the node information. |
|
| 12 |
+func (cli *Client) NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) {
|
|
| 13 |
+ nodeID, err := trimID("node", nodeID)
|
|
| 14 |
+ if err != nil {
|
|
| 15 |
+ return swarm.Node{}, nil, err
|
|
| 16 |
+ } |
|
| 17 |
+ resp, err := cli.get(ctx, "/nodes/"+nodeID, nil, nil) |
|
| 18 |
+ defer ensureReaderClosed(resp) |
|
| 19 |
+ if err != nil {
|
|
| 20 |
+ return swarm.Node{}, nil, err
|
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ body, err := io.ReadAll(resp.Body) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return swarm.Node{}, nil, err
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ var response swarm.Node |
|
| 29 |
+ rdr := bytes.NewReader(body) |
|
| 30 |
+ err = json.NewDecoder(rdr).Decode(&response) |
|
| 31 |
+ return response, body, err |
|
| 32 |
+} |
| 0 | 33 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,34 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/filters" |
|
| 8 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// NodeList returns the list of nodes. |
|
| 12 |
+func (cli *Client) NodeList(ctx context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) {
|
|
| 13 |
+ query := url.Values{}
|
|
| 14 |
+ |
|
| 15 |
+ if options.Filters.Len() > 0 {
|
|
| 16 |
+ filterJSON, err := filters.ToJSON(options.Filters) |
|
| 17 |
+ if err != nil {
|
|
| 18 |
+ return nil, err |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ query.Set("filters", filterJSON)
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ resp, err := cli.get(ctx, "/nodes", query, nil) |
|
| 25 |
+ defer ensureReaderClosed(resp) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ return nil, err |
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ var nodes []swarm.Node |
|
| 31 |
+ err = json.NewDecoder(resp.Body).Decode(&nodes) |
|
| 32 |
+ return nodes, err |
|
| 33 |
+} |
| 0 | 34 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,25 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// NodeRemove removes a Node. |
|
| 10 |
+func (cli *Client) NodeRemove(ctx context.Context, nodeID string, options swarm.NodeRemoveOptions) error {
|
|
| 11 |
+ nodeID, err := trimID("node", nodeID)
|
|
| 12 |
+ if err != nil {
|
|
| 13 |
+ return err |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ if options.Force {
|
|
| 18 |
+ query.Set("force", "1")
|
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ resp, err := cli.delete(ctx, "/nodes/"+nodeID, query, nil) |
|
| 22 |
+ defer ensureReaderClosed(resp) |
|
| 23 |
+ return err |
|
| 24 |
+} |
| 0 | 25 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,22 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// NodeUpdate updates a Node. |
|
| 10 |
+func (cli *Client) NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error {
|
|
| 11 |
+ nodeID, err := trimID("node", nodeID)
|
|
| 12 |
+ if err != nil {
|
|
| 13 |
+ return err |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ query.Set("version", version.String())
|
|
| 18 |
+ resp, err := cli.post(ctx, "/nodes/"+nodeID+"/update", query, node, nil) |
|
| 19 |
+ ensureReaderClosed(resp) |
|
| 20 |
+ return err |
|
| 21 |
+} |
| 0 | 22 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,248 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net" |
|
| 5 |
+ "net/http" |
|
| 6 |
+ "os" |
|
| 7 |
+ "path/filepath" |
|
| 8 |
+ "strings" |
|
| 9 |
+ "time" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/docker/go-connections/sockets" |
|
| 12 |
+ "github.com/docker/go-connections/tlsconfig" |
|
| 13 |
+ "github.com/pkg/errors" |
|
| 14 |
+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" |
|
| 15 |
+ "go.opentelemetry.io/otel/trace" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+// Opt is a configuration option to initialize a [Client]. |
|
| 19 |
+type Opt func(*Client) error |
|
| 20 |
+ |
|
| 21 |
+// FromEnv configures the client with values from environment variables. It |
|
| 22 |
+// is the equivalent of using the [WithTLSClientConfigFromEnv], [WithHostFromEnv], |
|
| 23 |
+// and [WithVersionFromEnv] options. |
|
| 24 |
+// |
|
| 25 |
+// FromEnv uses the following environment variables: |
|
| 26 |
+// |
|
| 27 |
+// - DOCKER_HOST ([EnvOverrideHost]) to set the URL to the docker server. |
|
| 28 |
+// - DOCKER_API_VERSION ([EnvOverrideAPIVersion]) to set the version of the |
|
| 29 |
+// API to use, leave empty for latest. |
|
| 30 |
+// - DOCKER_CERT_PATH ([EnvOverrideCertPath]) to specify the directory from |
|
| 31 |
+// which to load the TLS certificates ("ca.pem", "cert.pem", "key.pem').
|
|
| 32 |
+// - DOCKER_TLS_VERIFY ([EnvTLSVerify]) to enable or disable TLS verification |
|
| 33 |
+// (off by default). |
|
| 34 |
+func FromEnv(c *Client) error {
|
|
| 35 |
+ ops := []Opt{
|
|
| 36 |
+ WithTLSClientConfigFromEnv(), |
|
| 37 |
+ WithHostFromEnv(), |
|
| 38 |
+ WithVersionFromEnv(), |
|
| 39 |
+ } |
|
| 40 |
+ for _, op := range ops {
|
|
| 41 |
+ if err := op(c); err != nil {
|
|
| 42 |
+ return err |
|
| 43 |
+ } |
|
| 44 |
+ } |
|
| 45 |
+ return nil |
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+// WithDialContext applies the dialer to the client transport. This can be |
|
| 49 |
+// used to set the Timeout and KeepAlive settings of the client. It returns |
|
| 50 |
+// an error if the client does not have a [http.Transport] configured. |
|
| 51 |
+func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) Opt {
|
|
| 52 |
+ return func(c *Client) error {
|
|
| 53 |
+ if transport, ok := c.client.Transport.(*http.Transport); ok {
|
|
| 54 |
+ transport.DialContext = dialContext |
|
| 55 |
+ return nil |
|
| 56 |
+ } |
|
| 57 |
+ return errors.Errorf("cannot apply dialer to transport: %T", c.client.Transport)
|
|
| 58 |
+ } |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+// WithHost overrides the client host with the specified one. |
|
| 62 |
+func WithHost(host string) Opt {
|
|
| 63 |
+ return func(c *Client) error {
|
|
| 64 |
+ hostURL, err := ParseHostURL(host) |
|
| 65 |
+ if err != nil {
|
|
| 66 |
+ return err |
|
| 67 |
+ } |
|
| 68 |
+ c.host = host |
|
| 69 |
+ c.proto = hostURL.Scheme |
|
| 70 |
+ c.addr = hostURL.Host |
|
| 71 |
+ c.basePath = hostURL.Path |
|
| 72 |
+ if transport, ok := c.client.Transport.(*http.Transport); ok {
|
|
| 73 |
+ return sockets.ConfigureTransport(transport, c.proto, c.addr) |
|
| 74 |
+ } |
|
| 75 |
+ return errors.Errorf("cannot apply host to transport: %T", c.client.Transport)
|
|
| 76 |
+ } |
|
| 77 |
+} |
|
| 78 |
+ |
|
| 79 |
+// WithHostFromEnv overrides the client host with the host specified in the |
|
| 80 |
+// DOCKER_HOST ([EnvOverrideHost]) environment variable. If DOCKER_HOST is not set, |
|
| 81 |
+// or set to an empty value, the host is not modified. |
|
| 82 |
+func WithHostFromEnv() Opt {
|
|
| 83 |
+ return func(c *Client) error {
|
|
| 84 |
+ if host := os.Getenv(EnvOverrideHost); host != "" {
|
|
| 85 |
+ return WithHost(host)(c) |
|
| 86 |
+ } |
|
| 87 |
+ return nil |
|
| 88 |
+ } |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+// WithHTTPClient overrides the client's HTTP client with the specified one. |
|
| 92 |
+func WithHTTPClient(client *http.Client) Opt {
|
|
| 93 |
+ return func(c *Client) error {
|
|
| 94 |
+ if client != nil {
|
|
| 95 |
+ c.client = client |
|
| 96 |
+ } |
|
| 97 |
+ return nil |
|
| 98 |
+ } |
|
| 99 |
+} |
|
| 100 |
+ |
|
| 101 |
+// WithTimeout configures the time limit for requests made by the HTTP client. |
|
| 102 |
+func WithTimeout(timeout time.Duration) Opt {
|
|
| 103 |
+ return func(c *Client) error {
|
|
| 104 |
+ c.client.Timeout = timeout |
|
| 105 |
+ return nil |
|
| 106 |
+ } |
|
| 107 |
+} |
|
| 108 |
+ |
|
| 109 |
+// WithUserAgent configures the User-Agent header to use for HTTP requests. |
|
| 110 |
+// It overrides any User-Agent set in headers. When set to an empty string, |
|
| 111 |
+// the User-Agent header is removed, and no header is sent. |
|
| 112 |
+func WithUserAgent(ua string) Opt {
|
|
| 113 |
+ return func(c *Client) error {
|
|
| 114 |
+ c.userAgent = &ua |
|
| 115 |
+ return nil |
|
| 116 |
+ } |
|
| 117 |
+} |
|
| 118 |
+ |
|
| 119 |
+// WithHTTPHeaders appends custom HTTP headers to the client's default headers. |
|
| 120 |
+// It does not allow for built-in headers (such as "User-Agent", if set) to |
|
| 121 |
+// be overridden. Also see [WithUserAgent]. |
|
| 122 |
+func WithHTTPHeaders(headers map[string]string) Opt {
|
|
| 123 |
+ return func(c *Client) error {
|
|
| 124 |
+ c.customHTTPHeaders = headers |
|
| 125 |
+ return nil |
|
| 126 |
+ } |
|
| 127 |
+} |
|
| 128 |
+ |
|
| 129 |
+// WithScheme overrides the client scheme with the specified one. |
|
| 130 |
+func WithScheme(scheme string) Opt {
|
|
| 131 |
+ return func(c *Client) error {
|
|
| 132 |
+ c.scheme = scheme |
|
| 133 |
+ return nil |
|
| 134 |
+ } |
|
| 135 |
+} |
|
| 136 |
+ |
|
| 137 |
+// WithTLSClientConfig applies a TLS config to the client transport. |
|
| 138 |
+func WithTLSClientConfig(cacertPath, certPath, keyPath string) Opt {
|
|
| 139 |
+ return func(c *Client) error {
|
|
| 140 |
+ transport, ok := c.client.Transport.(*http.Transport) |
|
| 141 |
+ if !ok {
|
|
| 142 |
+ return errors.Errorf("cannot apply tls config to transport: %T", c.client.Transport)
|
|
| 143 |
+ } |
|
| 144 |
+ config, err := tlsconfig.Client(tlsconfig.Options{
|
|
| 145 |
+ CAFile: cacertPath, |
|
| 146 |
+ CertFile: certPath, |
|
| 147 |
+ KeyFile: keyPath, |
|
| 148 |
+ ExclusiveRootPools: true, |
|
| 149 |
+ }) |
|
| 150 |
+ if err != nil {
|
|
| 151 |
+ return errors.Wrap(err, "failed to create tls config") |
|
| 152 |
+ } |
|
| 153 |
+ transport.TLSClientConfig = config |
|
| 154 |
+ return nil |
|
| 155 |
+ } |
|
| 156 |
+} |
|
| 157 |
+ |
|
| 158 |
+// WithTLSClientConfigFromEnv configures the client's TLS settings with the |
|
| 159 |
+// settings in the DOCKER_CERT_PATH ([EnvOverrideCertPath]) and DOCKER_TLS_VERIFY |
|
| 160 |
+// ([EnvTLSVerify]) environment variables. If DOCKER_CERT_PATH is not set or empty, |
|
| 161 |
+// TLS configuration is not modified. |
|
| 162 |
+// |
|
| 163 |
+// WithTLSClientConfigFromEnv uses the following environment variables: |
|
| 164 |
+// |
|
| 165 |
+// - DOCKER_CERT_PATH ([EnvOverrideCertPath]) to specify the directory from |
|
| 166 |
+// which to load the TLS certificates ("ca.pem", "cert.pem", "key.pem").
|
|
| 167 |
+// - DOCKER_TLS_VERIFY ([EnvTLSVerify]) to enable or disable TLS verification |
|
| 168 |
+// (off by default). |
|
| 169 |
+func WithTLSClientConfigFromEnv() Opt {
|
|
| 170 |
+ return func(c *Client) error {
|
|
| 171 |
+ dockerCertPath := os.Getenv(EnvOverrideCertPath) |
|
| 172 |
+ if dockerCertPath == "" {
|
|
| 173 |
+ return nil |
|
| 174 |
+ } |
|
| 175 |
+ tlsc, err := tlsconfig.Client(tlsconfig.Options{
|
|
| 176 |
+ CAFile: filepath.Join(dockerCertPath, "ca.pem"), |
|
| 177 |
+ CertFile: filepath.Join(dockerCertPath, "cert.pem"), |
|
| 178 |
+ KeyFile: filepath.Join(dockerCertPath, "key.pem"), |
|
| 179 |
+ InsecureSkipVerify: os.Getenv(EnvTLSVerify) == "", |
|
| 180 |
+ }) |
|
| 181 |
+ if err != nil {
|
|
| 182 |
+ return err |
|
| 183 |
+ } |
|
| 184 |
+ |
|
| 185 |
+ c.client = &http.Client{
|
|
| 186 |
+ Transport: &http.Transport{TLSClientConfig: tlsc},
|
|
| 187 |
+ CheckRedirect: CheckRedirect, |
|
| 188 |
+ } |
|
| 189 |
+ return nil |
|
| 190 |
+ } |
|
| 191 |
+} |
|
| 192 |
+ |
|
| 193 |
+// WithVersion overrides the client version with the specified one. If an empty |
|
| 194 |
+// version is provided, the value is ignored to allow version negotiation |
|
| 195 |
+// (see [WithAPIVersionNegotiation]). |
|
| 196 |
+// |
|
| 197 |
+// WithVersion does not validate if the client supports the given version, |
|
| 198 |
+// and callers should verify if the version is in the correct format and |
|
| 199 |
+// lower than the maximum supported version as defined by [DefaultAPIVersion]. |
|
| 200 |
+func WithVersion(version string) Opt {
|
|
| 201 |
+ return func(c *Client) error {
|
|
| 202 |
+ if v := strings.TrimPrefix(version, "v"); v != "" {
|
|
| 203 |
+ c.version = v |
|
| 204 |
+ c.manualOverride = true |
|
| 205 |
+ } |
|
| 206 |
+ return nil |
|
| 207 |
+ } |
|
| 208 |
+} |
|
| 209 |
+ |
|
| 210 |
+// WithVersionFromEnv overrides the client version with the version specified in |
|
| 211 |
+// the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable. |
|
| 212 |
+// If DOCKER_API_VERSION is not set, or set to an empty value, the version |
|
| 213 |
+// is not modified. |
|
| 214 |
+// |
|
| 215 |
+// WithVersion does not validate if the client supports the given version, |
|
| 216 |
+// and callers should verify if the version is in the correct format and |
|
| 217 |
+// lower than the maximum supported version as defined by [DefaultAPIVersion]. |
|
| 218 |
+func WithVersionFromEnv() Opt {
|
|
| 219 |
+ return func(c *Client) error {
|
|
| 220 |
+ return WithVersion(os.Getenv(EnvOverrideAPIVersion))(c) |
|
| 221 |
+ } |
|
| 222 |
+} |
|
| 223 |
+ |
|
| 224 |
+// WithAPIVersionNegotiation enables automatic API version negotiation for the client. |
|
| 225 |
+// With this option enabled, the client automatically negotiates the API version |
|
| 226 |
+// to use when making requests. API version negotiation is performed on the first |
|
| 227 |
+// request; subsequent requests do not re-negotiate. |
|
| 228 |
+func WithAPIVersionNegotiation() Opt {
|
|
| 229 |
+ return func(c *Client) error {
|
|
| 230 |
+ c.negotiateVersion = true |
|
| 231 |
+ return nil |
|
| 232 |
+ } |
|
| 233 |
+} |
|
| 234 |
+ |
|
| 235 |
+// WithTraceProvider sets the trace provider for the client. |
|
| 236 |
+// If this is not set then the global trace provider will be used. |
|
| 237 |
+func WithTraceProvider(provider trace.TracerProvider) Opt {
|
|
| 238 |
+ return WithTraceOptions(otelhttp.WithTracerProvider(provider)) |
|
| 239 |
+} |
|
| 240 |
+ |
|
| 241 |
+// WithTraceOptions sets tracing span options for the client. |
|
| 242 |
+func WithTraceOptions(opts ...otelhttp.Option) Opt {
|
|
| 243 |
+ return func(c *Client) error {
|
|
| 244 |
+ c.traceOpts = append(c.traceOpts, opts...) |
|
| 245 |
+ return nil |
|
| 246 |
+ } |
|
| 247 |
+} |
| 0 | 248 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,81 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/http" |
|
| 5 |
+ "path" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types" |
|
| 9 |
+ "github.com/moby/moby/api/types/build" |
|
| 10 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// Ping pings the server and returns the value of the "Docker-Experimental", |
|
| 14 |
+// "Builder-Version", "OS-Type" & "API-Version" headers. It attempts to use |
|
| 15 |
+// a HEAD request on the endpoint, but falls back to GET if HEAD is not supported |
|
| 16 |
+// by the daemon. It ignores internal server errors returned by the API, which |
|
| 17 |
+// may be returned if the daemon is in an unhealthy state, but returns errors |
|
| 18 |
+// for other non-success status codes, failing to connect to the API, or failing |
|
| 19 |
+// to parse the API response. |
|
| 20 |
+func (cli *Client) Ping(ctx context.Context) (types.Ping, error) {
|
|
| 21 |
+ var ping types.Ping |
|
| 22 |
+ |
|
| 23 |
+ // Using cli.buildRequest() + cli.doRequest() instead of cli.sendRequest() |
|
| 24 |
+ // because ping requests are used during API version negotiation, so we want |
|
| 25 |
+ // to hit the non-versioned /_ping endpoint, not /v1.xx/_ping |
|
| 26 |
+ req, err := cli.buildRequest(ctx, http.MethodHead, path.Join(cli.basePath, "/_ping"), nil, nil) |
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ return ping, err |
|
| 29 |
+ } |
|
| 30 |
+ resp, err := cli.doRequest(req) |
|
| 31 |
+ if err != nil {
|
|
| 32 |
+ if IsErrConnectionFailed(err) {
|
|
| 33 |
+ return ping, err |
|
| 34 |
+ } |
|
| 35 |
+ // We managed to connect, but got some error; continue and try GET request. |
|
| 36 |
+ } else {
|
|
| 37 |
+ defer ensureReaderClosed(resp) |
|
| 38 |
+ switch resp.StatusCode {
|
|
| 39 |
+ case http.StatusOK, http.StatusInternalServerError: |
|
| 40 |
+ // Server handled the request, so parse the response |
|
| 41 |
+ return parsePingResponse(cli, resp) |
|
| 42 |
+ } |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ // HEAD failed; fallback to GET. |
|
| 46 |
+ req.Method = http.MethodGet |
|
| 47 |
+ resp, err = cli.doRequest(req) |
|
| 48 |
+ defer ensureReaderClosed(resp) |
|
| 49 |
+ if err != nil {
|
|
| 50 |
+ return ping, err |
|
| 51 |
+ } |
|
| 52 |
+ return parsePingResponse(cli, resp) |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+func parsePingResponse(cli *Client, resp *http.Response) (types.Ping, error) {
|
|
| 56 |
+ if resp == nil {
|
|
| 57 |
+ return types.Ping{}, nil
|
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ var ping types.Ping |
|
| 61 |
+ if resp.Header == nil {
|
|
| 62 |
+ return ping, cli.checkResponseErr(resp) |
|
| 63 |
+ } |
|
| 64 |
+ ping.APIVersion = resp.Header.Get("Api-Version")
|
|
| 65 |
+ ping.OSType = resp.Header.Get("Ostype")
|
|
| 66 |
+ if resp.Header.Get("Docker-Experimental") == "true" {
|
|
| 67 |
+ ping.Experimental = true |
|
| 68 |
+ } |
|
| 69 |
+ if bv := resp.Header.Get("Builder-Version"); bv != "" {
|
|
| 70 |
+ ping.BuilderVersion = build.BuilderVersion(bv) |
|
| 71 |
+ } |
|
| 72 |
+ if si := resp.Header.Get("Swarm"); si != "" {
|
|
| 73 |
+ state, role, _ := strings.Cut(si, "/") |
|
| 74 |
+ ping.SwarmStatus = &swarm.Status{
|
|
| 75 |
+ NodeState: swarm.LocalNodeState(state), |
|
| 76 |
+ ControlAvailable: role == "manager", |
|
| 77 |
+ } |
|
| 78 |
+ } |
|
| 79 |
+ return ping, cli.checkResponseErr(resp) |
|
| 80 |
+} |
| 0 | 81 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,23 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/http" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// PluginCreate creates a plugin |
|
| 12 |
+func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error {
|
|
| 13 |
+ headers := http.Header(make(map[string][]string)) |
|
| 14 |
+ headers.Set("Content-Type", "application/x-tar")
|
|
| 15 |
+ |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ query.Set("name", createOptions.RepoName)
|
|
| 18 |
+ |
|
| 19 |
+ resp, err := cli.postRaw(ctx, "/plugins/create", query, createContext, headers) |
|
| 20 |
+ ensureReaderClosed(resp) |
|
| 21 |
+ return err |
|
| 22 |
+} |
| 0 | 23 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,23 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// PluginDisable disables a plugin |
|
| 10 |
+func (cli *Client) PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error {
|
|
| 11 |
+ name, err := trimID("plugin", name)
|
|
| 12 |
+ if err != nil {
|
|
| 13 |
+ return err |
|
| 14 |
+ } |
|
| 15 |
+ query := url.Values{}
|
|
| 16 |
+ if options.Force {
|
|
| 17 |
+ query.Set("force", "1")
|
|
| 18 |
+ } |
|
| 19 |
+ resp, err := cli.post(ctx, "/plugins/"+name+"/disable", query, nil, nil) |
|
| 20 |
+ ensureReaderClosed(resp) |
|
| 21 |
+ return err |
|
| 22 |
+} |
| 0 | 23 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,23 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ "strconv" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// PluginEnable enables a plugin |
|
| 11 |
+func (cli *Client) PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error {
|
|
| 12 |
+ name, err := trimID("plugin", name)
|
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return err |
|
| 15 |
+ } |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ query.Set("timeout", strconv.Itoa(options.Timeout))
|
|
| 18 |
+ |
|
| 19 |
+ resp, err := cli.post(ctx, "/plugins/"+name+"/enable", query, nil, nil) |
|
| 20 |
+ ensureReaderClosed(resp) |
|
| 21 |
+ return err |
|
| 22 |
+} |
| 0 | 23 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,32 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "io" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// PluginInspectWithRaw inspects an existing plugin |
|
| 12 |
+func (cli *Client) PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) {
|
|
| 13 |
+ name, err := trimID("plugin", name)
|
|
| 14 |
+ if err != nil {
|
|
| 15 |
+ return nil, nil, err |
|
| 16 |
+ } |
|
| 17 |
+ resp, err := cli.get(ctx, "/plugins/"+name+"/json", nil, nil) |
|
| 18 |
+ defer ensureReaderClosed(resp) |
|
| 19 |
+ if err != nil {
|
|
| 20 |
+ return nil, nil, err |
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ body, err := io.ReadAll(resp.Body) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return nil, nil, err |
|
| 26 |
+ } |
|
| 27 |
+ var p types.Plugin |
|
| 28 |
+ rdr := bytes.NewReader(body) |
|
| 29 |
+ err = json.NewDecoder(rdr).Decode(&p) |
|
| 30 |
+ return &p, body, err |
|
| 31 |
+} |
| 0 | 32 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,117 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "io" |
|
| 6 |
+ "net/http" |
|
| 7 |
+ "net/url" |
|
| 8 |
+ |
|
| 9 |
+ cerrdefs "github.com/containerd/errdefs" |
|
| 10 |
+ "github.com/distribution/reference" |
|
| 11 |
+ "github.com/moby/moby/api/types" |
|
| 12 |
+ "github.com/moby/moby/api/types/registry" |
|
| 13 |
+ "github.com/pkg/errors" |
|
| 14 |
+) |
|
| 15 |
+ |
|
| 16 |
+// PluginInstall installs a plugin |
|
| 17 |
+func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (_ io.ReadCloser, retErr error) {
|
|
| 18 |
+ query := url.Values{}
|
|
| 19 |
+ if _, err := reference.ParseNormalizedNamed(options.RemoteRef); err != nil {
|
|
| 20 |
+ return nil, errors.Wrap(err, "invalid remote reference") |
|
| 21 |
+ } |
|
| 22 |
+ query.Set("remote", options.RemoteRef)
|
|
| 23 |
+ |
|
| 24 |
+ privileges, err := cli.checkPluginPermissions(ctx, query, options) |
|
| 25 |
+ if err != nil {
|
|
| 26 |
+ return nil, err |
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ // set name for plugin pull, if empty should default to remote reference |
|
| 30 |
+ query.Set("name", name)
|
|
| 31 |
+ |
|
| 32 |
+ resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) |
|
| 33 |
+ if err != nil {
|
|
| 34 |
+ return nil, err |
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ name = resp.Header.Get("Docker-Plugin-Name")
|
|
| 38 |
+ |
|
| 39 |
+ pr, pw := io.Pipe() |
|
| 40 |
+ go func() { // todo: the client should probably be designed more around the actual api
|
|
| 41 |
+ _, err := io.Copy(pw, resp.Body) |
|
| 42 |
+ if err != nil {
|
|
| 43 |
+ _ = pw.CloseWithError(err) |
|
| 44 |
+ return |
|
| 45 |
+ } |
|
| 46 |
+ defer func() {
|
|
| 47 |
+ if retErr != nil {
|
|
| 48 |
+ delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) |
|
| 49 |
+ ensureReaderClosed(delResp) |
|
| 50 |
+ } |
|
| 51 |
+ }() |
|
| 52 |
+ if len(options.Args) > 0 {
|
|
| 53 |
+ if err := cli.PluginSet(ctx, name, options.Args); err != nil {
|
|
| 54 |
+ _ = pw.CloseWithError(err) |
|
| 55 |
+ return |
|
| 56 |
+ } |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ if options.Disabled {
|
|
| 60 |
+ _ = pw.Close() |
|
| 61 |
+ return |
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 64 |
+ enableErr := cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0})
|
|
| 65 |
+ _ = pw.CloseWithError(enableErr) |
|
| 66 |
+ }() |
|
| 67 |
+ return pr, nil |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) {
|
|
| 71 |
+ return cli.get(ctx, "/plugins/privileges", query, http.Header{
|
|
| 72 |
+ registry.AuthHeader: {registryAuth},
|
|
| 73 |
+ }) |
|
| 74 |
+} |
|
| 75 |
+ |
|
| 76 |
+func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileges types.PluginPrivileges, registryAuth string) (*http.Response, error) {
|
|
| 77 |
+ return cli.post(ctx, "/plugins/pull", query, privileges, http.Header{
|
|
| 78 |
+ registry.AuthHeader: {registryAuth},
|
|
| 79 |
+ }) |
|
| 80 |
+} |
|
| 81 |
+ |
|
| 82 |
+func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options types.PluginInstallOptions) (types.PluginPrivileges, error) {
|
|
| 83 |
+ resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) |
|
| 84 |
+ if cerrdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil {
|
|
| 85 |
+ // todo: do inspect before to check existing name before checking privileges |
|
| 86 |
+ newAuthHeader, privilegeErr := options.PrivilegeFunc(ctx) |
|
| 87 |
+ if privilegeErr != nil {
|
|
| 88 |
+ ensureReaderClosed(resp) |
|
| 89 |
+ return nil, privilegeErr |
|
| 90 |
+ } |
|
| 91 |
+ options.RegistryAuth = newAuthHeader |
|
| 92 |
+ resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) |
|
| 93 |
+ } |
|
| 94 |
+ if err != nil {
|
|
| 95 |
+ ensureReaderClosed(resp) |
|
| 96 |
+ return nil, err |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ var privileges types.PluginPrivileges |
|
| 100 |
+ if err := json.NewDecoder(resp.Body).Decode(&privileges); err != nil {
|
|
| 101 |
+ ensureReaderClosed(resp) |
|
| 102 |
+ return nil, err |
|
| 103 |
+ } |
|
| 104 |
+ ensureReaderClosed(resp) |
|
| 105 |
+ |
|
| 106 |
+ if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
|
|
| 107 |
+ accept, err := options.AcceptPermissionsFunc(ctx, privileges) |
|
| 108 |
+ if err != nil {
|
|
| 109 |
+ return nil, err |
|
| 110 |
+ } |
|
| 111 |
+ if !accept {
|
|
| 112 |
+ return nil, errors.Errorf("permission denied while installing plugin %s", options.RemoteRef)
|
|
| 113 |
+ } |
|
| 114 |
+ } |
|
| 115 |
+ return privileges, nil |
|
| 116 |
+} |
| 0 | 117 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,33 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types" |
|
| 8 |
+ "github.com/moby/moby/api/types/filters" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// PluginList returns the installed plugins |
|
| 12 |
+func (cli *Client) PluginList(ctx context.Context, filter filters.Args) (types.PluginsListResponse, error) {
|
|
| 13 |
+ var plugins types.PluginsListResponse |
|
| 14 |
+ query := url.Values{}
|
|
| 15 |
+ |
|
| 16 |
+ if filter.Len() > 0 {
|
|
| 17 |
+ //nolint:staticcheck // ignore SA1019 for old code |
|
| 18 |
+ filterJSON, err := filters.ToParamWithVersion(cli.version, filter) |
|
| 19 |
+ if err != nil {
|
|
| 20 |
+ return plugins, err |
|
| 21 |
+ } |
|
| 22 |
+ query.Set("filters", filterJSON)
|
|
| 23 |
+ } |
|
| 24 |
+ resp, err := cli.get(ctx, "/plugins", query, nil) |
|
| 25 |
+ defer ensureReaderClosed(resp) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ return plugins, err |
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ err = json.NewDecoder(resp.Body).Decode(&plugins) |
|
| 31 |
+ return plugins, err |
|
| 32 |
+} |
| 0 | 33 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,24 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/http" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/registry" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// PluginPush pushes a plugin to a registry |
|
| 11 |
+func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) {
|
|
| 12 |
+ name, err := trimID("plugin", name)
|
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return nil, err |
|
| 15 |
+ } |
|
| 16 |
+ resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, http.Header{
|
|
| 17 |
+ registry.AuthHeader: {registryAuth},
|
|
| 18 |
+ }) |
|
| 19 |
+ if err != nil {
|
|
| 20 |
+ return nil, err |
|
| 21 |
+ } |
|
| 22 |
+ return resp.Body, nil |
|
| 23 |
+} |
| 0 | 24 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,25 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// PluginRemove removes a plugin |
|
| 10 |
+func (cli *Client) PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error {
|
|
| 11 |
+ name, err := trimID("plugin", name)
|
|
| 12 |
+ if err != nil {
|
|
| 13 |
+ return err |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ if options.Force {
|
|
| 18 |
+ query.Set("force", "1")
|
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ resp, err := cli.delete(ctx, "/plugins/"+name, query, nil) |
|
| 22 |
+ defer ensureReaderClosed(resp) |
|
| 23 |
+ return err |
|
| 24 |
+} |
| 0 | 25 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,17 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+) |
|
| 5 |
+ |
|
| 6 |
+// PluginSet modifies settings for an existing plugin |
|
| 7 |
+func (cli *Client) PluginSet(ctx context.Context, name string, args []string) error {
|
|
| 8 |
+ name, err := trimID("plugin", name)
|
|
| 9 |
+ if err != nil {
|
|
| 10 |
+ return err |
|
| 11 |
+ } |
|
| 12 |
+ |
|
| 13 |
+ resp, err := cli.post(ctx, "/plugins/"+name+"/set", nil, args, nil) |
|
| 14 |
+ ensureReaderClosed(resp) |
|
| 15 |
+ return err |
|
| 16 |
+} |
| 0 | 17 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,47 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/http" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/distribution/reference" |
|
| 9 |
+ "github.com/moby/moby/api/types" |
|
| 10 |
+ "github.com/moby/moby/api/types/registry" |
|
| 11 |
+ "github.com/pkg/errors" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+// PluginUpgrade upgrades a plugin |
|
| 15 |
+func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
|
|
| 16 |
+ name, err := trimID("plugin", name)
|
|
| 17 |
+ if err != nil {
|
|
| 18 |
+ return nil, err |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ if err := cli.NewVersionError(ctx, "1.26", "plugin upgrade"); err != nil {
|
|
| 22 |
+ return nil, err |
|
| 23 |
+ } |
|
| 24 |
+ query := url.Values{}
|
|
| 25 |
+ if _, err := reference.ParseNormalizedNamed(options.RemoteRef); err != nil {
|
|
| 26 |
+ return nil, errors.Wrap(err, "invalid remote reference") |
|
| 27 |
+ } |
|
| 28 |
+ query.Set("remote", options.RemoteRef)
|
|
| 29 |
+ |
|
| 30 |
+ privileges, err := cli.checkPluginPermissions(ctx, query, options) |
|
| 31 |
+ if err != nil {
|
|
| 32 |
+ return nil, err |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ resp, err := cli.tryPluginUpgrade(ctx, query, privileges, name, options.RegistryAuth) |
|
| 36 |
+ if err != nil {
|
|
| 37 |
+ return nil, err |
|
| 38 |
+ } |
|
| 39 |
+ return resp.Body, nil |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privileges types.PluginPrivileges, name, registryAuth string) (*http.Response, error) {
|
|
| 43 |
+ return cli.post(ctx, "/plugins/"+name+"/upgrade", query, privileges, http.Header{
|
|
| 44 |
+ registry.AuthHeader: {registryAuth},
|
|
| 45 |
+ }) |
|
| 46 |
+} |
| 0 | 47 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,318 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "fmt" |
|
| 7 |
+ "io" |
|
| 8 |
+ "net" |
|
| 9 |
+ "net/http" |
|
| 10 |
+ "net/url" |
|
| 11 |
+ "os" |
|
| 12 |
+ "reflect" |
|
| 13 |
+ "strings" |
|
| 14 |
+ |
|
| 15 |
+ "github.com/moby/moby/api/types" |
|
| 16 |
+ "github.com/pkg/errors" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+// head sends an http request to the docker API using the method HEAD. |
|
| 20 |
+func (cli *Client) head(ctx context.Context, path string, query url.Values, headers http.Header) (*http.Response, error) {
|
|
| 21 |
+ return cli.sendRequest(ctx, http.MethodHead, path, query, nil, headers) |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+// get sends an http request to the docker API using the method GET with a specific Go context. |
|
| 25 |
+func (cli *Client) get(ctx context.Context, path string, query url.Values, headers http.Header) (*http.Response, error) {
|
|
| 26 |
+ return cli.sendRequest(ctx, http.MethodGet, path, query, nil, headers) |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+// post sends an http request to the docker API using the method POST with a specific Go context. |
|
| 30 |
+func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers http.Header) (*http.Response, error) {
|
|
| 31 |
+ body, headers, err := encodeBody(obj, headers) |
|
| 32 |
+ if err != nil {
|
|
| 33 |
+ return nil, err |
|
| 34 |
+ } |
|
| 35 |
+ return cli.sendRequest(ctx, http.MethodPost, path, query, body, headers) |
|
| 36 |
+} |
|
| 37 |
+ |
|
| 38 |
+func (cli *Client) postRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers http.Header) (*http.Response, error) {
|
|
| 39 |
+ return cli.sendRequest(ctx, http.MethodPost, path, query, body, headers) |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+func (cli *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers http.Header) (*http.Response, error) {
|
|
| 43 |
+ body, headers, err := encodeBody(obj, headers) |
|
| 44 |
+ if err != nil {
|
|
| 45 |
+ return nil, err |
|
| 46 |
+ } |
|
| 47 |
+ return cli.putRaw(ctx, path, query, body, headers) |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+// putRaw sends an http request to the docker API using the method PUT. |
|
| 51 |
+func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers http.Header) (*http.Response, error) {
|
|
| 52 |
+ // PUT requests are expected to always have a body (apparently) |
|
| 53 |
+ // so explicitly pass an empty body to sendRequest to signal that |
|
| 54 |
+ // it should set the Content-Type header if not already present. |
|
| 55 |
+ if body == nil {
|
|
| 56 |
+ body = http.NoBody |
|
| 57 |
+ } |
|
| 58 |
+ return cli.sendRequest(ctx, http.MethodPut, path, query, body, headers) |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+// delete sends an http request to the docker API using the method DELETE. |
|
| 62 |
+func (cli *Client) delete(ctx context.Context, path string, query url.Values, headers http.Header) (*http.Response, error) {
|
|
| 63 |
+ return cli.sendRequest(ctx, http.MethodDelete, path, query, nil, headers) |
|
| 64 |
+} |
|
| 65 |
+ |
|
| 66 |
+func encodeBody(obj interface{}, headers http.Header) (io.Reader, http.Header, error) {
|
|
| 67 |
+ if obj == nil {
|
|
| 68 |
+ return nil, headers, nil |
|
| 69 |
+ } |
|
| 70 |
+ // encoding/json encodes a nil pointer as the JSON document `null`, |
|
| 71 |
+ // irrespective of whether the type implements json.Marshaler or encoding.TextMarshaler. |
|
| 72 |
+ // That is almost certainly not what the caller intended as the request body. |
|
| 73 |
+ if reflect.TypeOf(obj).Kind() == reflect.Ptr && reflect.ValueOf(obj).IsNil() {
|
|
| 74 |
+ return nil, headers, nil |
|
| 75 |
+ } |
|
| 76 |
+ |
|
| 77 |
+ body, err := encodeData(obj) |
|
| 78 |
+ if err != nil {
|
|
| 79 |
+ return nil, headers, err |
|
| 80 |
+ } |
|
| 81 |
+ if headers == nil {
|
|
| 82 |
+ headers = make(map[string][]string) |
|
| 83 |
+ } |
|
| 84 |
+ headers["Content-Type"] = []string{"application/json"}
|
|
| 85 |
+ return body, headers, nil |
|
| 86 |
+} |
|
| 87 |
+ |
|
| 88 |
+func (cli *Client) buildRequest(ctx context.Context, method, path string, body io.Reader, headers http.Header) (*http.Request, error) {
|
|
| 89 |
+ req, err := http.NewRequestWithContext(ctx, method, path, body) |
|
| 90 |
+ if err != nil {
|
|
| 91 |
+ return nil, err |
|
| 92 |
+ } |
|
| 93 |
+ req = cli.addHeaders(req, headers) |
|
| 94 |
+ req.URL.Scheme = cli.scheme |
|
| 95 |
+ req.URL.Host = cli.addr |
|
| 96 |
+ |
|
| 97 |
+ if cli.proto == "unix" || cli.proto == "npipe" {
|
|
| 98 |
+ // Override host header for non-tcp connections. |
|
| 99 |
+ req.Host = DummyHost |
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ if body != nil && req.Header.Get("Content-Type") == "" {
|
|
| 103 |
+ req.Header.Set("Content-Type", "text/plain")
|
|
| 104 |
+ } |
|
| 105 |
+ return req, nil |
|
| 106 |
+} |
|
| 107 |
+ |
|
| 108 |
+func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers http.Header) (*http.Response, error) {
|
|
| 109 |
+ req, err := cli.buildRequest(ctx, method, cli.getAPIPath(ctx, path, query), body, headers) |
|
| 110 |
+ if err != nil {
|
|
| 111 |
+ return nil, err |
|
| 112 |
+ } |
|
| 113 |
+ |
|
| 114 |
+ resp, err := cli.doRequest(req) |
|
| 115 |
+ switch {
|
|
| 116 |
+ case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded): |
|
| 117 |
+ return nil, err |
|
| 118 |
+ case err == nil: |
|
| 119 |
+ return resp, cli.checkResponseErr(resp) |
|
| 120 |
+ default: |
|
| 121 |
+ return resp, err |
|
| 122 |
+ } |
|
| 123 |
+} |
|
| 124 |
+ |
|
| 125 |
+func (cli *Client) doRequest(req *http.Request) (*http.Response, error) {
|
|
| 126 |
+ resp, err := cli.client.Do(req) |
|
| 127 |
+ if err != nil {
|
|
| 128 |
+ if cli.scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") {
|
|
| 129 |
+ return nil, errConnectionFailed{fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err)}
|
|
| 130 |
+ } |
|
| 131 |
+ |
|
| 132 |
+ if cli.scheme == "https" && strings.Contains(err.Error(), "bad certificate") {
|
|
| 133 |
+ return nil, errConnectionFailed{errors.Wrap(err, "the server probably has client authentication (--tlsverify) enabled; check your TLS client certification settings")}
|
|
| 134 |
+ } |
|
| 135 |
+ |
|
| 136 |
+ // Don't decorate context sentinel errors; users may be comparing to |
|
| 137 |
+ // them directly. |
|
| 138 |
+ if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
| 139 |
+ return nil, err |
|
| 140 |
+ } |
|
| 141 |
+ |
|
| 142 |
+ var uErr *url.Error |
|
| 143 |
+ if errors.As(err, &uErr) {
|
|
| 144 |
+ var nErr *net.OpError |
|
| 145 |
+ if errors.As(uErr.Err, &nErr) {
|
|
| 146 |
+ if os.IsPermission(nErr.Err) {
|
|
| 147 |
+ return nil, errConnectionFailed{errors.Wrapf(err, "permission denied while trying to connect to the Docker daemon socket at %v", cli.host)}
|
|
| 148 |
+ } |
|
| 149 |
+ } |
|
| 150 |
+ } |
|
| 151 |
+ |
|
| 152 |
+ var nErr net.Error |
|
| 153 |
+ if errors.As(err, &nErr) {
|
|
| 154 |
+ // FIXME(thaJeztah): any net.Error should be considered a connection error (but we should include the original error)? |
|
| 155 |
+ if nErr.Timeout() {
|
|
| 156 |
+ return nil, connectionFailed(cli.host) |
|
| 157 |
+ } |
|
| 158 |
+ if strings.Contains(nErr.Error(), "connection refused") || strings.Contains(nErr.Error(), "dial unix") {
|
|
| 159 |
+ return nil, connectionFailed(cli.host) |
|
| 160 |
+ } |
|
| 161 |
+ } |
|
| 162 |
+ |
|
| 163 |
+ // Although there's not a strongly typed error for this in go-winio, |
|
| 164 |
+ // lots of people are using the default configuration for the docker |
|
| 165 |
+ // daemon on Windows where the daemon is listening on a named pipe |
|
| 166 |
+ // `//./pipe/docker_engine, and the client must be running elevated. |
|
| 167 |
+ // Give users a clue rather than the not-overly useful message |
|
| 168 |
+ // such as `error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.26/info: |
|
| 169 |
+ // open //./pipe/docker_engine: The system cannot find the file specified.`. |
|
| 170 |
+ // Note we can't string compare "The system cannot find the file specified" as |
|
| 171 |
+ // this is localised - for example in French the error would be |
|
| 172 |
+ // `open //./pipe/docker_engine: Le fichier spécifié est introuvable.` |
|
| 173 |
+ if strings.Contains(err.Error(), `open //./pipe/docker_engine`) {
|
|
| 174 |
+ // Checks if client is running with elevated privileges |
|
| 175 |
+ if f, elevatedErr := os.Open(`\\.\PHYSICALDRIVE0`); elevatedErr != nil {
|
|
| 176 |
+ err = errors.Wrap(err, "in the default daemon configuration on Windows, the docker client must be run with elevated privileges to connect") |
|
| 177 |
+ } else {
|
|
| 178 |
+ _ = f.Close() |
|
| 179 |
+ err = errors.Wrap(err, "this error may indicate that the docker daemon is not running") |
|
| 180 |
+ } |
|
| 181 |
+ } |
|
| 182 |
+ |
|
| 183 |
+ return nil, errConnectionFailed{errors.Wrap(err, "error during connect")}
|
|
| 184 |
+ } |
|
| 185 |
+ |
|
| 186 |
+ return resp, nil |
|
| 187 |
+} |
|
| 188 |
+ |
|
| 189 |
+func (cli *Client) checkResponseErr(serverResp *http.Response) (retErr error) {
|
|
| 190 |
+ if serverResp == nil {
|
|
| 191 |
+ return nil |
|
| 192 |
+ } |
|
| 193 |
+ if serverResp.StatusCode >= http.StatusOK && serverResp.StatusCode < http.StatusBadRequest {
|
|
| 194 |
+ return nil |
|
| 195 |
+ } |
|
| 196 |
+ defer func() {
|
|
| 197 |
+ retErr = httpErrorFromStatusCode(retErr, serverResp.StatusCode) |
|
| 198 |
+ }() |
|
| 199 |
+ |
|
| 200 |
+ var body []byte |
|
| 201 |
+ var err error |
|
| 202 |
+ var reqURL string |
|
| 203 |
+ if serverResp.Request != nil {
|
|
| 204 |
+ reqURL = serverResp.Request.URL.String() |
|
| 205 |
+ } |
|
| 206 |
+ statusMsg := serverResp.Status |
|
| 207 |
+ if statusMsg == "" {
|
|
| 208 |
+ statusMsg = http.StatusText(serverResp.StatusCode) |
|
| 209 |
+ } |
|
| 210 |
+ if serverResp.Body != nil {
|
|
| 211 |
+ bodyMax := 1 * 1024 * 1024 // 1 MiB |
|
| 212 |
+ bodyR := &io.LimitedReader{
|
|
| 213 |
+ R: serverResp.Body, |
|
| 214 |
+ N: int64(bodyMax), |
|
| 215 |
+ } |
|
| 216 |
+ body, err = io.ReadAll(bodyR) |
|
| 217 |
+ if err != nil {
|
|
| 218 |
+ return err |
|
| 219 |
+ } |
|
| 220 |
+ if bodyR.N == 0 {
|
|
| 221 |
+ if reqURL != "" {
|
|
| 222 |
+ return fmt.Errorf("request returned %s with a message (> %d bytes) for API route and version %s, check if the server supports the requested API version", statusMsg, bodyMax, reqURL)
|
|
| 223 |
+ } |
|
| 224 |
+ return fmt.Errorf("request returned %s with a message (> %d bytes); check if the server supports the requested API version", statusMsg, bodyMax)
|
|
| 225 |
+ } |
|
| 226 |
+ } |
|
| 227 |
+ if len(body) == 0 {
|
|
| 228 |
+ if reqURL != "" {
|
|
| 229 |
+ return fmt.Errorf("request returned %s for API route and version %s, check if the server supports the requested API version", statusMsg, reqURL)
|
|
| 230 |
+ } |
|
| 231 |
+ return fmt.Errorf("request returned %s; check if the server supports the requested API version", statusMsg)
|
|
| 232 |
+ } |
|
| 233 |
+ |
|
| 234 |
+ var daemonErr error |
|
| 235 |
+ if serverResp.Header.Get("Content-Type") == "application/json" {
|
|
| 236 |
+ var errorResponse types.ErrorResponse |
|
| 237 |
+ if err := json.Unmarshal(body, &errorResponse); err != nil {
|
|
| 238 |
+ return errors.Wrap(err, "Error reading JSON") |
|
| 239 |
+ } |
|
| 240 |
+ if errorResponse.Message == "" {
|
|
| 241 |
+ // Error-message is empty, which means that we successfully parsed the |
|
| 242 |
+ // JSON-response (no error produced), but it didn't contain an error |
|
| 243 |
+ // message. This could either be because the response was empty, or |
|
| 244 |
+ // the response was valid JSON, but not with the expected schema |
|
| 245 |
+ // ([types.ErrorResponse]). |
|
| 246 |
+ // |
|
| 247 |
+ // We cannot use "strict" JSON handling (json.NewDecoder with DisallowUnknownFields) |
|
| 248 |
+ // due to the API using an open schema (we must anticipate fields |
|
| 249 |
+ // being added to [types.ErrorResponse] in the future, and not |
|
| 250 |
+ // reject those responses. |
|
| 251 |
+ // |
|
| 252 |
+ // For these cases, we construct an error with the status-code |
|
| 253 |
+ // returned, but we could consider returning (a truncated version |
|
| 254 |
+ // of) the actual response as-is. |
|
| 255 |
+ // |
|
| 256 |
+ // TODO(thaJeztah): consider adding a log.Debug to allow clients to debug the actual response when enabling debug logging. |
|
| 257 |
+ daemonErr = fmt.Errorf(`API returned a %d (%s) but provided no error-message`, |
|
| 258 |
+ serverResp.StatusCode, |
|
| 259 |
+ http.StatusText(serverResp.StatusCode), |
|
| 260 |
+ ) |
|
| 261 |
+ } else {
|
|
| 262 |
+ daemonErr = errors.New(strings.TrimSpace(errorResponse.Message)) |
|
| 263 |
+ } |
|
| 264 |
+ } else {
|
|
| 265 |
+ // Fall back to returning the response as-is for API versions < 1.24 |
|
| 266 |
+ // that didn't support JSON error responses, and for situations |
|
| 267 |
+ // where a plain text error is returned. This branch may also catch |
|
| 268 |
+ // situations where a proxy is involved, returning a HTML response. |
|
| 269 |
+ daemonErr = errors.New(strings.TrimSpace(string(body))) |
|
| 270 |
+ } |
|
| 271 |
+ return errors.Wrap(daemonErr, "Error response from daemon") |
|
| 272 |
+} |
|
| 273 |
+ |
|
| 274 |
+func (cli *Client) addHeaders(req *http.Request, headers http.Header) *http.Request {
|
|
| 275 |
+ // Add CLI Config's HTTP Headers BEFORE we set the Docker headers |
|
| 276 |
+ // then the user can't change OUR headers |
|
| 277 |
+ for k, v := range cli.customHTTPHeaders {
|
|
| 278 |
+ req.Header.Set(k, v) |
|
| 279 |
+ } |
|
| 280 |
+ |
|
| 281 |
+ for k, v := range headers {
|
|
| 282 |
+ req.Header[http.CanonicalHeaderKey(k)] = v |
|
| 283 |
+ } |
|
| 284 |
+ |
|
| 285 |
+ if cli.userAgent != nil {
|
|
| 286 |
+ if *cli.userAgent == "" {
|
|
| 287 |
+ req.Header.Del("User-Agent")
|
|
| 288 |
+ } else {
|
|
| 289 |
+ req.Header.Set("User-Agent", *cli.userAgent)
|
|
| 290 |
+ } |
|
| 291 |
+ } |
|
| 292 |
+ return req |
|
| 293 |
+} |
|
| 294 |
+ |
|
| 295 |
+func encodeData(data interface{}) (*bytes.Buffer, error) {
|
|
| 296 |
+ params := bytes.NewBuffer(nil) |
|
| 297 |
+ if data != nil {
|
|
| 298 |
+ if err := json.NewEncoder(params).Encode(data); err != nil {
|
|
| 299 |
+ return nil, err |
|
| 300 |
+ } |
|
| 301 |
+ } |
|
| 302 |
+ return params, nil |
|
| 303 |
+} |
|
| 304 |
+ |
|
| 305 |
+func ensureReaderClosed(response *http.Response) {
|
|
| 306 |
+ if response != nil && response.Body != nil {
|
|
| 307 |
+ // Drain up to 512 bytes and close the body to let the Transport reuse the connection |
|
| 308 |
+ // see https://github.com/google/go-github/pull/317/files#r57536827 |
|
| 309 |
+ // |
|
| 310 |
+ // TODO(thaJeztah): see if this optimization is still needed, or already implemented in stdlib, |
|
| 311 |
+ // and check if context-cancellation should handle this as well. If still needed, consider |
|
| 312 |
+ // wrapping response.Body, or returning a "closer()" from [Client.sendRequest] and related |
|
| 313 |
+ // methods. |
|
| 314 |
+ _, _ = io.CopyN(io.Discard, response.Body, 512) |
|
| 315 |
+ _ = response.Body.Close() |
|
| 316 |
+ } |
|
| 317 |
+} |
| 0 | 318 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,24 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// SecretCreate creates a new secret. |
|
| 10 |
+func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (swarm.SecretCreateResponse, error) {
|
|
| 11 |
+ if err := cli.NewVersionError(ctx, "1.25", "secret create"); err != nil {
|
|
| 12 |
+ return swarm.SecretCreateResponse{}, err
|
|
| 13 |
+ } |
|
| 14 |
+ resp, err := cli.post(ctx, "/secrets/create", nil, secret, nil) |
|
| 15 |
+ defer ensureReaderClosed(resp) |
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ return swarm.SecretCreateResponse{}, err
|
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ var response swarm.SecretCreateResponse |
|
| 21 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 22 |
+ return response, err |
|
| 23 |
+} |
| 0 | 24 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,37 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "io" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// SecretInspectWithRaw returns the secret information with raw data |
|
| 12 |
+func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) {
|
|
| 13 |
+ id, err := trimID("secret", id)
|
|
| 14 |
+ if err != nil {
|
|
| 15 |
+ return swarm.Secret{}, nil, err
|
|
| 16 |
+ } |
|
| 17 |
+ if err := cli.NewVersionError(ctx, "1.25", "secret inspect"); err != nil {
|
|
| 18 |
+ return swarm.Secret{}, nil, err
|
|
| 19 |
+ } |
|
| 20 |
+ resp, err := cli.get(ctx, "/secrets/"+id, nil, nil) |
|
| 21 |
+ defer ensureReaderClosed(resp) |
|
| 22 |
+ if err != nil {
|
|
| 23 |
+ return swarm.Secret{}, nil, err
|
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ body, err := io.ReadAll(resp.Body) |
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ return swarm.Secret{}, nil, err
|
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ var secret swarm.Secret |
|
| 32 |
+ rdr := bytes.NewReader(body) |
|
| 33 |
+ err = json.NewDecoder(rdr).Decode(&secret) |
|
| 34 |
+ |
|
| 35 |
+ return secret, body, err |
|
| 36 |
+} |
| 0 | 37 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,37 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/filters" |
|
| 8 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// SecretList returns the list of secrets. |
|
| 12 |
+func (cli *Client) SecretList(ctx context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) {
|
|
| 13 |
+ if err := cli.NewVersionError(ctx, "1.25", "secret list"); err != nil {
|
|
| 14 |
+ return nil, err |
|
| 15 |
+ } |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ |
|
| 18 |
+ if options.Filters.Len() > 0 {
|
|
| 19 |
+ filterJSON, err := filters.ToJSON(options.Filters) |
|
| 20 |
+ if err != nil {
|
|
| 21 |
+ return nil, err |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ query.Set("filters", filterJSON)
|
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ resp, err := cli.get(ctx, "/secrets", query, nil) |
|
| 28 |
+ defer ensureReaderClosed(resp) |
|
| 29 |
+ if err != nil {
|
|
| 30 |
+ return nil, err |
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ var secrets []swarm.Secret |
|
| 34 |
+ err = json.NewDecoder(resp.Body).Decode(&secrets) |
|
| 35 |
+ return secrets, err |
|
| 36 |
+} |
| 0 | 37 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,17 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import "context" |
|
| 3 |
+ |
|
| 4 |
+// SecretRemove removes a secret. |
|
| 5 |
+func (cli *Client) SecretRemove(ctx context.Context, id string) error {
|
|
| 6 |
+ id, err := trimID("secret", id)
|
|
| 7 |
+ if err != nil {
|
|
| 8 |
+ return err |
|
| 9 |
+ } |
|
| 10 |
+ if err := cli.NewVersionError(ctx, "1.25", "secret remove"); err != nil {
|
|
| 11 |
+ return err |
|
| 12 |
+ } |
|
| 13 |
+ resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil) |
|
| 14 |
+ defer ensureReaderClosed(resp) |
|
| 15 |
+ return err |
|
| 16 |
+} |
| 0 | 17 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,24 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// SecretUpdate attempts to update a secret. |
|
| 10 |
+func (cli *Client) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error {
|
|
| 11 |
+ id, err := trimID("secret", id)
|
|
| 12 |
+ if err != nil {
|
|
| 13 |
+ return err |
|
| 14 |
+ } |
|
| 15 |
+ if err := cli.NewVersionError(ctx, "1.25", "secret update"); err != nil {
|
|
| 16 |
+ return err |
|
| 17 |
+ } |
|
| 18 |
+ query := url.Values{}
|
|
| 19 |
+ query.Set("version", version.String())
|
|
| 20 |
+ resp, err := cli.post(ctx, "/secrets/"+id+"/update", query, secret, nil) |
|
| 21 |
+ ensureReaderClosed(resp) |
|
| 22 |
+ return err |
|
| 23 |
+} |
| 0 | 24 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,212 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "net/http" |
|
| 7 |
+ "strings" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/distribution/reference" |
|
| 10 |
+ "github.com/moby/moby/api/types/registry" |
|
| 11 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 12 |
+ "github.com/moby/moby/api/types/versions" |
|
| 13 |
+ "github.com/opencontainers/go-digest" |
|
| 14 |
+ "github.com/pkg/errors" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+// ServiceCreate creates a new service. |
|
| 18 |
+func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options swarm.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) {
|
|
| 19 |
+ var response swarm.ServiceCreateResponse |
|
| 20 |
+ |
|
| 21 |
+ // Make sure we negotiated (if the client is configured to do so), |
|
| 22 |
+ // as code below contains API-version specific handling of options. |
|
| 23 |
+ // |
|
| 24 |
+ // Normally, version-negotiation (if enabled) would not happen until |
|
| 25 |
+ // the API request is made. |
|
| 26 |
+ if err := cli.checkVersion(ctx); err != nil {
|
|
| 27 |
+ return response, err |
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ // Make sure containerSpec is not nil when no runtime is set or the runtime is set to container |
|
| 31 |
+ if service.TaskTemplate.ContainerSpec == nil && (service.TaskTemplate.Runtime == "" || service.TaskTemplate.Runtime == swarm.RuntimeContainer) {
|
|
| 32 |
+ service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}
|
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ if err := validateServiceSpec(service); err != nil {
|
|
| 36 |
+ return response, err |
|
| 37 |
+ } |
|
| 38 |
+ if versions.LessThan(cli.version, "1.30") {
|
|
| 39 |
+ if err := validateAPIVersion(service, cli.version); err != nil {
|
|
| 40 |
+ return response, err |
|
| 41 |
+ } |
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ // ensure that the image is tagged |
|
| 45 |
+ var resolveWarning string |
|
| 46 |
+ switch {
|
|
| 47 |
+ case service.TaskTemplate.ContainerSpec != nil: |
|
| 48 |
+ if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
|
|
| 49 |
+ service.TaskTemplate.ContainerSpec.Image = taggedImg |
|
| 50 |
+ } |
|
| 51 |
+ if options.QueryRegistry {
|
|
| 52 |
+ resolveWarning = resolveContainerSpecImage(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth) |
|
| 53 |
+ } |
|
| 54 |
+ case service.TaskTemplate.PluginSpec != nil: |
|
| 55 |
+ if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
|
|
| 56 |
+ service.TaskTemplate.PluginSpec.Remote = taggedImg |
|
| 57 |
+ } |
|
| 58 |
+ if options.QueryRegistry {
|
|
| 59 |
+ resolveWarning = resolvePluginSpecRemote(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth) |
|
| 60 |
+ } |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ headers := http.Header{}
|
|
| 64 |
+ if versions.LessThan(cli.version, "1.30") {
|
|
| 65 |
+ // the custom "version" header was used by engine API before 20.10 |
|
| 66 |
+ // (API 1.30) to switch between client- and server-side lookup of |
|
| 67 |
+ // image digests. |
|
| 68 |
+ headers["version"] = []string{cli.version}
|
|
| 69 |
+ } |
|
| 70 |
+ if options.EncodedRegistryAuth != "" {
|
|
| 71 |
+ headers[registry.AuthHeader] = []string{options.EncodedRegistryAuth}
|
|
| 72 |
+ } |
|
| 73 |
+ resp, err := cli.post(ctx, "/services/create", nil, service, headers) |
|
| 74 |
+ defer ensureReaderClosed(resp) |
|
| 75 |
+ if err != nil {
|
|
| 76 |
+ return response, err |
|
| 77 |
+ } |
|
| 78 |
+ |
|
| 79 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 80 |
+ if resolveWarning != "" {
|
|
| 81 |
+ response.Warnings = append(response.Warnings, resolveWarning) |
|
| 82 |
+ } |
|
| 83 |
+ |
|
| 84 |
+ return response, err |
|
| 85 |
+} |
|
| 86 |
+ |
|
| 87 |
+func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
|
|
| 88 |
+ var warning string |
|
| 89 |
+ if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth); err != nil {
|
|
| 90 |
+ warning = digestWarning(taskSpec.ContainerSpec.Image) |
|
| 91 |
+ } else {
|
|
| 92 |
+ taskSpec.ContainerSpec.Image = img |
|
| 93 |
+ if len(imgPlatforms) > 0 {
|
|
| 94 |
+ if taskSpec.Placement == nil {
|
|
| 95 |
+ taskSpec.Placement = &swarm.Placement{}
|
|
| 96 |
+ } |
|
| 97 |
+ taskSpec.Placement.Platforms = imgPlatforms |
|
| 98 |
+ } |
|
| 99 |
+ } |
|
| 100 |
+ return warning |
|
| 101 |
+} |
|
| 102 |
+ |
|
| 103 |
+func resolvePluginSpecRemote(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
|
|
| 104 |
+ var warning string |
|
| 105 |
+ if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth); err != nil {
|
|
| 106 |
+ warning = digestWarning(taskSpec.PluginSpec.Remote) |
|
| 107 |
+ } else {
|
|
| 108 |
+ taskSpec.PluginSpec.Remote = img |
|
| 109 |
+ if len(imgPlatforms) > 0 {
|
|
| 110 |
+ if taskSpec.Placement == nil {
|
|
| 111 |
+ taskSpec.Placement = &swarm.Placement{}
|
|
| 112 |
+ } |
|
| 113 |
+ taskSpec.Placement.Platforms = imgPlatforms |
|
| 114 |
+ } |
|
| 115 |
+ } |
|
| 116 |
+ return warning |
|
| 117 |
+} |
|
| 118 |
+ |
|
| 119 |
+func imageDigestAndPlatforms(ctx context.Context, cli DistributionAPIClient, image, encodedAuth string) (string, []swarm.Platform, error) {
|
|
| 120 |
+ distributionInspect, err := cli.DistributionInspect(ctx, image, encodedAuth) |
|
| 121 |
+ var platforms []swarm.Platform |
|
| 122 |
+ if err != nil {
|
|
| 123 |
+ return "", nil, err |
|
| 124 |
+ } |
|
| 125 |
+ |
|
| 126 |
+ imageWithDigest := imageWithDigestString(image, distributionInspect.Descriptor.Digest) |
|
| 127 |
+ |
|
| 128 |
+ if len(distributionInspect.Platforms) > 0 {
|
|
| 129 |
+ platforms = make([]swarm.Platform, 0, len(distributionInspect.Platforms)) |
|
| 130 |
+ for _, p := range distributionInspect.Platforms {
|
|
| 131 |
+ // clear architecture field for arm. This is a temporary patch to address |
|
| 132 |
+ // https://github.com/docker/swarmkit/issues/2294. The issue is that while |
|
| 133 |
+ // image manifests report "arm" as the architecture, the node reports |
|
| 134 |
+ // something like "armv7l" (includes the variant), which causes arm images |
|
| 135 |
+ // to stop working with swarm mode. This patch removes the architecture |
|
| 136 |
+ // constraint for arm images to ensure tasks get scheduled. |
|
| 137 |
+ arch := p.Architecture |
|
| 138 |
+ if strings.ToLower(arch) == "arm" {
|
|
| 139 |
+ arch = "" |
|
| 140 |
+ } |
|
| 141 |
+ platforms = append(platforms, swarm.Platform{
|
|
| 142 |
+ Architecture: arch, |
|
| 143 |
+ OS: p.OS, |
|
| 144 |
+ }) |
|
| 145 |
+ } |
|
| 146 |
+ } |
|
| 147 |
+ return imageWithDigest, platforms, err |
|
| 148 |
+} |
|
| 149 |
+ |
|
| 150 |
+// imageWithDigestString takes an image string and a digest, and updates |
|
| 151 |
+// the image string if it didn't originally contain a digest. It returns |
|
| 152 |
+// image unmodified in other situations. |
|
| 153 |
+func imageWithDigestString(image string, dgst digest.Digest) string {
|
|
| 154 |
+ namedRef, err := reference.ParseNormalizedNamed(image) |
|
| 155 |
+ if err == nil {
|
|
| 156 |
+ if _, isCanonical := namedRef.(reference.Canonical); !isCanonical {
|
|
| 157 |
+ // ensure that image gets a default tag if none is provided |
|
| 158 |
+ img, err := reference.WithDigest(namedRef, dgst) |
|
| 159 |
+ if err == nil {
|
|
| 160 |
+ return reference.FamiliarString(img) |
|
| 161 |
+ } |
|
| 162 |
+ } |
|
| 163 |
+ } |
|
| 164 |
+ return image |
|
| 165 |
+} |
|
| 166 |
+ |
|
| 167 |
+// imageWithTagString takes an image string, and returns a tagged image |
|
| 168 |
+// string, adding a 'latest' tag if one was not provided. It returns an |
|
| 169 |
+// empty string if a canonical reference was provided |
|
| 170 |
+func imageWithTagString(image string) string {
|
|
| 171 |
+ namedRef, err := reference.ParseNormalizedNamed(image) |
|
| 172 |
+ if err == nil {
|
|
| 173 |
+ return reference.FamiliarString(reference.TagNameOnly(namedRef)) |
|
| 174 |
+ } |
|
| 175 |
+ return "" |
|
| 176 |
+} |
|
| 177 |
+ |
|
| 178 |
+// digestWarning constructs a formatted warning string using the |
|
| 179 |
+// image name that could not be pinned by digest. The formatting |
|
| 180 |
+// is hardcoded, but could me made smarter in the future |
|
| 181 |
+func digestWarning(image string) string {
|
|
| 182 |
+ return fmt.Sprintf("image %s could not be accessed on a registry to record\nits digest. Each node will access %s independently,\npossibly leading to different nodes running different\nversions of the image.\n", image, image)
|
|
| 183 |
+} |
|
| 184 |
+ |
|
| 185 |
+func validateServiceSpec(s swarm.ServiceSpec) error {
|
|
| 186 |
+ if s.TaskTemplate.ContainerSpec != nil && s.TaskTemplate.PluginSpec != nil {
|
|
| 187 |
+ return errors.New("must not specify both a container spec and a plugin spec in the task template")
|
|
| 188 |
+ } |
|
| 189 |
+ if s.TaskTemplate.PluginSpec != nil && s.TaskTemplate.Runtime != swarm.RuntimePlugin {
|
|
| 190 |
+ return errors.New("mismatched runtime with plugin spec")
|
|
| 191 |
+ } |
|
| 192 |
+ if s.TaskTemplate.ContainerSpec != nil && (s.TaskTemplate.Runtime != "" && s.TaskTemplate.Runtime != swarm.RuntimeContainer) {
|
|
| 193 |
+ return errors.New("mismatched runtime with container spec")
|
|
| 194 |
+ } |
|
| 195 |
+ return nil |
|
| 196 |
+} |
|
| 197 |
+ |
|
| 198 |
+func validateAPIVersion(c swarm.ServiceSpec, apiVersion string) error {
|
|
| 199 |
+ for _, m := range c.TaskTemplate.ContainerSpec.Mounts {
|
|
| 200 |
+ if m.BindOptions != nil {
|
|
| 201 |
+ if m.BindOptions.NonRecursive && versions.LessThan(apiVersion, "1.40") {
|
|
| 202 |
+ return errors.Errorf("bind-recursive=disabled requires API v1.40 or later")
|
|
| 203 |
+ } |
|
| 204 |
+ // ReadOnlyNonRecursive can be safely ignored when API < 1.44 |
|
| 205 |
+ if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(apiVersion, "1.44") {
|
|
| 206 |
+ return errors.Errorf("bind-recursive=readonly requires API v1.44 or later")
|
|
| 207 |
+ } |
|
| 208 |
+ } |
|
| 209 |
+ } |
|
| 210 |
+ return nil |
|
| 211 |
+} |
| 0 | 212 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,38 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "fmt" |
|
| 7 |
+ "io" |
|
| 8 |
+ "net/url" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// ServiceInspectWithRaw returns the service information and the raw data. |
|
| 14 |
+func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string, opts swarm.ServiceInspectOptions) (swarm.Service, []byte, error) {
|
|
| 15 |
+ serviceID, err := trimID("service", serviceID)
|
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ return swarm.Service{}, nil, err
|
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ query := url.Values{}
|
|
| 21 |
+ query.Set("insertDefaults", fmt.Sprintf("%v", opts.InsertDefaults))
|
|
| 22 |
+ resp, err := cli.get(ctx, "/services/"+serviceID, query, nil) |
|
| 23 |
+ defer ensureReaderClosed(resp) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return swarm.Service{}, nil, err
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ body, err := io.ReadAll(resp.Body) |
|
| 29 |
+ if err != nil {
|
|
| 30 |
+ return swarm.Service{}, nil, err
|
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ var response swarm.Service |
|
| 34 |
+ rdr := bytes.NewReader(body) |
|
| 35 |
+ err = json.NewDecoder(rdr).Decode(&response) |
|
| 36 |
+ return response, body, err |
|
| 37 |
+} |
| 0 | 38 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,38 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/filters" |
|
| 8 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// ServiceList returns the list of services. |
|
| 12 |
+func (cli *Client) ServiceList(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) {
|
|
| 13 |
+ query := url.Values{}
|
|
| 14 |
+ |
|
| 15 |
+ if options.Filters.Len() > 0 {
|
|
| 16 |
+ filterJSON, err := filters.ToJSON(options.Filters) |
|
| 17 |
+ if err != nil {
|
|
| 18 |
+ return nil, err |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ query.Set("filters", filterJSON)
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ if options.Status {
|
|
| 25 |
+ query.Set("status", "true")
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ resp, err := cli.get(ctx, "/services", query, nil) |
|
| 29 |
+ defer ensureReaderClosed(resp) |
|
| 30 |
+ if err != nil {
|
|
| 31 |
+ return nil, err |
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ var services []swarm.Service |
|
| 35 |
+ err = json.NewDecoder(resp.Body).Decode(&services) |
|
| 36 |
+ return services, err |
|
| 37 |
+} |
| 0 | 38 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,57 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ "time" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/container" |
|
| 9 |
+ timetypes "github.com/moby/moby/api/types/time" |
|
| 10 |
+ "github.com/pkg/errors" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// ServiceLogs returns the logs generated by a service in an io.ReadCloser. |
|
| 14 |
+// It's up to the caller to close the stream. |
|
| 15 |
+func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options container.LogsOptions) (io.ReadCloser, error) {
|
|
| 16 |
+ serviceID, err := trimID("service", serviceID)
|
|
| 17 |
+ if err != nil {
|
|
| 18 |
+ return nil, err |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ query := url.Values{}
|
|
| 22 |
+ if options.ShowStdout {
|
|
| 23 |
+ query.Set("stdout", "1")
|
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ if options.ShowStderr {
|
|
| 27 |
+ query.Set("stderr", "1")
|
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ if options.Since != "" {
|
|
| 31 |
+ ts, err := timetypes.GetTimestamp(options.Since, time.Now()) |
|
| 32 |
+ if err != nil {
|
|
| 33 |
+ return nil, errors.Wrap(err, `invalid value for "since"`) |
|
| 34 |
+ } |
|
| 35 |
+ query.Set("since", ts)
|
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ if options.Timestamps {
|
|
| 39 |
+ query.Set("timestamps", "1")
|
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ if options.Details {
|
|
| 43 |
+ query.Set("details", "1")
|
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ if options.Follow {
|
|
| 47 |
+ query.Set("follow", "1")
|
|
| 48 |
+ } |
|
| 49 |
+ query.Set("tail", options.Tail)
|
|
| 50 |
+ |
|
| 51 |
+ resp, err := cli.get(ctx, "/services/"+serviceID+"/logs", query, nil) |
|
| 52 |
+ if err != nil {
|
|
| 53 |
+ return nil, err |
|
| 54 |
+ } |
|
| 55 |
+ return resp.Body, nil |
|
| 56 |
+} |
| 0 | 57 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,15 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import "context" |
|
| 3 |
+ |
|
| 4 |
+// ServiceRemove kills and removes a service. |
|
| 5 |
+func (cli *Client) ServiceRemove(ctx context.Context, serviceID string) error {
|
|
| 6 |
+ serviceID, err := trimID("service", serviceID)
|
|
| 7 |
+ if err != nil {
|
|
| 8 |
+ return err |
|
| 9 |
+ } |
|
| 10 |
+ |
|
| 11 |
+ resp, err := cli.delete(ctx, "/services/"+serviceID, nil, nil) |
|
| 12 |
+ defer ensureReaderClosed(resp) |
|
| 13 |
+ return err |
|
| 14 |
+} |
| 0 | 15 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,89 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/http" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/registry" |
|
| 9 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 10 |
+ "github.com/moby/moby/api/types/versions" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// ServiceUpdate updates a Service. The version number is required to avoid conflicting writes. |
|
| 14 |
+// It should be the value as set *before* the update. You can find this value in the Meta field |
|
| 15 |
+// of swarm.Service, which can be found using ServiceInspectWithRaw. |
|
| 16 |
+func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
|
|
| 17 |
+ serviceID, err := trimID("service", serviceID)
|
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return swarm.ServiceUpdateResponse{}, err
|
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ // Make sure we negotiated (if the client is configured to do so), |
|
| 23 |
+ // as code below contains API-version specific handling of options. |
|
| 24 |
+ // |
|
| 25 |
+ // Normally, version-negotiation (if enabled) would not happen until |
|
| 26 |
+ // the API request is made. |
|
| 27 |
+ if err := cli.checkVersion(ctx); err != nil {
|
|
| 28 |
+ return swarm.ServiceUpdateResponse{}, err
|
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ query := url.Values{}
|
|
| 32 |
+ if options.RegistryAuthFrom != "" {
|
|
| 33 |
+ query.Set("registryAuthFrom", options.RegistryAuthFrom)
|
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ if options.Rollback != "" {
|
|
| 37 |
+ query.Set("rollback", options.Rollback)
|
|
| 38 |
+ } |
|
| 39 |
+ |
|
| 40 |
+ query.Set("version", version.String())
|
|
| 41 |
+ |
|
| 42 |
+ if err := validateServiceSpec(service); err != nil {
|
|
| 43 |
+ return swarm.ServiceUpdateResponse{}, err
|
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ // ensure that the image is tagged |
|
| 47 |
+ var resolveWarning string |
|
| 48 |
+ switch {
|
|
| 49 |
+ case service.TaskTemplate.ContainerSpec != nil: |
|
| 50 |
+ if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
|
|
| 51 |
+ service.TaskTemplate.ContainerSpec.Image = taggedImg |
|
| 52 |
+ } |
|
| 53 |
+ if options.QueryRegistry {
|
|
| 54 |
+ resolveWarning = resolveContainerSpecImage(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth) |
|
| 55 |
+ } |
|
| 56 |
+ case service.TaskTemplate.PluginSpec != nil: |
|
| 57 |
+ if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
|
|
| 58 |
+ service.TaskTemplate.PluginSpec.Remote = taggedImg |
|
| 59 |
+ } |
|
| 60 |
+ if options.QueryRegistry {
|
|
| 61 |
+ resolveWarning = resolvePluginSpecRemote(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth) |
|
| 62 |
+ } |
|
| 63 |
+ } |
|
| 64 |
+ |
|
| 65 |
+ headers := http.Header{}
|
|
| 66 |
+ if versions.LessThan(cli.version, "1.30") {
|
|
| 67 |
+ // the custom "version" header was used by engine API before 20.10 |
|
| 68 |
+ // (API 1.30) to switch between client- and server-side lookup of |
|
| 69 |
+ // image digests. |
|
| 70 |
+ headers["version"] = []string{cli.version}
|
|
| 71 |
+ } |
|
| 72 |
+ if options.EncodedRegistryAuth != "" {
|
|
| 73 |
+ headers.Set(registry.AuthHeader, options.EncodedRegistryAuth) |
|
| 74 |
+ } |
|
| 75 |
+ resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers) |
|
| 76 |
+ defer ensureReaderClosed(resp) |
|
| 77 |
+ if err != nil {
|
|
| 78 |
+ return swarm.ServiceUpdateResponse{}, err
|
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ var response swarm.ServiceUpdateResponse |
|
| 82 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 83 |
+ if resolveWarning != "" {
|
|
| 84 |
+ response.Warnings = append(response.Warnings, resolveWarning) |
|
| 85 |
+ } |
|
| 86 |
+ |
|
| 87 |
+ return response, err |
|
| 88 |
+} |
| 0 | 89 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,21 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// SwarmGetUnlockKey retrieves the swarm's unlock key. |
|
| 10 |
+func (cli *Client) SwarmGetUnlockKey(ctx context.Context) (swarm.UnlockKeyResponse, error) {
|
|
| 11 |
+ resp, err := cli.get(ctx, "/swarm/unlockkey", nil, nil) |
|
| 12 |
+ defer ensureReaderClosed(resp) |
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return swarm.UnlockKeyResponse{}, err
|
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ var response swarm.UnlockKeyResponse |
|
| 18 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 19 |
+ return response, err |
|
| 20 |
+} |
| 0 | 21 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,21 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// SwarmInit initializes the swarm. |
|
| 10 |
+func (cli *Client) SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) {
|
|
| 11 |
+ resp, err := cli.post(ctx, "/swarm/init", nil, req, nil) |
|
| 12 |
+ defer ensureReaderClosed(resp) |
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return "", err |
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ var response string |
|
| 18 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 19 |
+ return response, err |
|
| 20 |
+} |
| 0 | 21 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,21 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// SwarmInspect inspects the swarm. |
|
| 10 |
+func (cli *Client) SwarmInspect(ctx context.Context) (swarm.Swarm, error) {
|
|
| 11 |
+ resp, err := cli.get(ctx, "/swarm", nil, nil) |
|
| 12 |
+ defer ensureReaderClosed(resp) |
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return swarm.Swarm{}, err
|
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ var response swarm.Swarm |
|
| 18 |
+ err = json.NewDecoder(resp.Body).Decode(&response) |
|
| 19 |
+ return response, err |
|
| 20 |
+} |
| 0 | 21 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,14 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// SwarmJoin joins the swarm. |
|
| 9 |
+func (cli *Client) SwarmJoin(ctx context.Context, req swarm.JoinRequest) error {
|
|
| 10 |
+ resp, err := cli.post(ctx, "/swarm/join", nil, req, nil) |
|
| 11 |
+ ensureReaderClosed(resp) |
|
| 12 |
+ return err |
|
| 13 |
+} |
| 0 | 14 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,17 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+// SwarmLeave leaves the swarm. |
|
| 8 |
+func (cli *Client) SwarmLeave(ctx context.Context, force bool) error {
|
|
| 9 |
+ query := url.Values{}
|
|
| 10 |
+ if force {
|
|
| 11 |
+ query.Set("force", "1")
|
|
| 12 |
+ } |
|
| 13 |
+ resp, err := cli.post(ctx, "/swarm/leave", query, nil, nil) |
|
| 14 |
+ ensureReaderClosed(resp) |
|
| 15 |
+ return err |
|
| 16 |
+} |
| 0 | 17 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,14 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// SwarmUnlock unlocks locked swarm. |
|
| 9 |
+func (cli *Client) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error {
|
|
| 10 |
+ resp, err := cli.post(ctx, "/swarm/unlock", nil, req, nil) |
|
| 11 |
+ ensureReaderClosed(resp) |
|
| 12 |
+ return err |
|
| 13 |
+} |
| 0 | 14 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,21 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ "strconv" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// SwarmUpdate updates the swarm. |
|
| 11 |
+func (cli *Client) SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error {
|
|
| 12 |
+ query := url.Values{}
|
|
| 13 |
+ query.Set("version", version.String())
|
|
| 14 |
+ query.Set("rotateWorkerToken", strconv.FormatBool(flags.RotateWorkerToken))
|
|
| 15 |
+ query.Set("rotateManagerToken", strconv.FormatBool(flags.RotateManagerToken))
|
|
| 16 |
+ query.Set("rotateManagerUnlockKey", strconv.FormatBool(flags.RotateManagerUnlockKey))
|
|
| 17 |
+ resp, err := cli.post(ctx, "/swarm/update", query, swarm, nil) |
|
| 18 |
+ ensureReaderClosed(resp) |
|
| 19 |
+ return err |
|
| 20 |
+} |
| 0 | 21 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,34 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "io" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// TaskInspectWithRaw returns the task information and its raw representation. |
|
| 12 |
+func (cli *Client) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) {
|
|
| 13 |
+ taskID, err := trimID("task", taskID)
|
|
| 14 |
+ if err != nil {
|
|
| 15 |
+ return swarm.Task{}, nil, err
|
|
| 16 |
+ } |
|
| 17 |
+ |
|
| 18 |
+ resp, err := cli.get(ctx, "/tasks/"+taskID, nil, nil) |
|
| 19 |
+ defer ensureReaderClosed(resp) |
|
| 20 |
+ if err != nil {
|
|
| 21 |
+ return swarm.Task{}, nil, err
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ body, err := io.ReadAll(resp.Body) |
|
| 25 |
+ if err != nil {
|
|
| 26 |
+ return swarm.Task{}, nil, err
|
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ var response swarm.Task |
|
| 30 |
+ rdr := bytes.NewReader(body) |
|
| 31 |
+ err = json.NewDecoder(rdr).Decode(&response) |
|
| 32 |
+ return response, body, err |
|
| 33 |
+} |
| 0 | 34 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,34 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/filters" |
|
| 8 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// TaskList returns the list of tasks. |
|
| 12 |
+func (cli *Client) TaskList(ctx context.Context, options swarm.TaskListOptions) ([]swarm.Task, error) {
|
|
| 13 |
+ query := url.Values{}
|
|
| 14 |
+ |
|
| 15 |
+ if options.Filters.Len() > 0 {
|
|
| 16 |
+ filterJSON, err := filters.ToJSON(options.Filters) |
|
| 17 |
+ if err != nil {
|
|
| 18 |
+ return nil, err |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ query.Set("filters", filterJSON)
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ resp, err := cli.get(ctx, "/tasks", query, nil) |
|
| 25 |
+ defer ensureReaderClosed(resp) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ return nil, err |
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ var tasks []swarm.Task |
|
| 31 |
+ err = json.NewDecoder(resp.Body).Decode(&tasks) |
|
| 32 |
+ return tasks, err |
|
| 33 |
+} |
| 0 | 34 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,51 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "io" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ "time" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/container" |
|
| 9 |
+ timetypes "github.com/moby/moby/api/types/time" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// TaskLogs returns the logs generated by a task in an io.ReadCloser. |
|
| 13 |
+// It's up to the caller to close the stream. |
|
| 14 |
+func (cli *Client) TaskLogs(ctx context.Context, taskID string, options container.LogsOptions) (io.ReadCloser, error) {
|
|
| 15 |
+ query := url.Values{}
|
|
| 16 |
+ if options.ShowStdout {
|
|
| 17 |
+ query.Set("stdout", "1")
|
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ if options.ShowStderr {
|
|
| 21 |
+ query.Set("stderr", "1")
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ if options.Since != "" {
|
|
| 25 |
+ ts, err := timetypes.GetTimestamp(options.Since, time.Now()) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ return nil, err |
|
| 28 |
+ } |
|
| 29 |
+ query.Set("since", ts)
|
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ if options.Timestamps {
|
|
| 33 |
+ query.Set("timestamps", "1")
|
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ if options.Details {
|
|
| 37 |
+ query.Set("details", "1")
|
|
| 38 |
+ } |
|
| 39 |
+ |
|
| 40 |
+ if options.Follow {
|
|
| 41 |
+ query.Set("follow", "1")
|
|
| 42 |
+ } |
|
| 43 |
+ query.Set("tail", options.Tail)
|
|
| 44 |
+ |
|
| 45 |
+ resp, err := cli.get(ctx, "/tasks/"+taskID+"/logs", query, nil) |
|
| 46 |
+ if err != nil {
|
|
| 47 |
+ return nil, err |
|
| 48 |
+ } |
|
| 49 |
+ return resp.Body, nil |
|
| 50 |
+} |
| 0 | 51 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,83 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ cerrdefs "github.com/containerd/errdefs" |
|
| 9 |
+ "github.com/moby/moby/api/types/filters" |
|
| 10 |
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+type emptyIDError string |
|
| 14 |
+ |
|
| 15 |
+func (e emptyIDError) InvalidParameter() {}
|
|
| 16 |
+ |
|
| 17 |
+func (e emptyIDError) Error() string {
|
|
| 18 |
+ return "invalid " + string(e) + " name or ID: value is empty" |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+// trimID trims the given object-ID / name, returning an error if it's empty. |
|
| 22 |
+func trimID(objType, id string) (string, error) {
|
|
| 23 |
+ id = strings.TrimSpace(id) |
|
| 24 |
+ if id == "" {
|
|
| 25 |
+ return "", emptyIDError(objType) |
|
| 26 |
+ } |
|
| 27 |
+ return id, nil |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+// getFiltersQuery returns a url query with "filters" query term, based on the |
|
| 31 |
+// filters provided. |
|
| 32 |
+func getFiltersQuery(f filters.Args) (url.Values, error) {
|
|
| 33 |
+ query := url.Values{}
|
|
| 34 |
+ if f.Len() > 0 {
|
|
| 35 |
+ filterJSON, err := filters.ToJSON(f) |
|
| 36 |
+ if err != nil {
|
|
| 37 |
+ return query, err |
|
| 38 |
+ } |
|
| 39 |
+ query.Set("filters", filterJSON)
|
|
| 40 |
+ } |
|
| 41 |
+ return query, nil |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+// encodePlatforms marshals the given platform(s) to JSON format, to |
|
| 45 |
+// be used for query-parameters for filtering / selecting platforms. |
|
| 46 |
+func encodePlatforms(platform ...ocispec.Platform) ([]string, error) {
|
|
| 47 |
+ if len(platform) == 0 {
|
|
| 48 |
+ return []string{}, nil
|
|
| 49 |
+ } |
|
| 50 |
+ if len(platform) == 1 {
|
|
| 51 |
+ p, err := encodePlatform(&platform[0]) |
|
| 52 |
+ if err != nil {
|
|
| 53 |
+ return nil, err |
|
| 54 |
+ } |
|
| 55 |
+ return []string{p}, nil
|
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ seen := make(map[string]struct{}, len(platform))
|
|
| 59 |
+ out := make([]string, 0, len(platform)) |
|
| 60 |
+ for i := range platform {
|
|
| 61 |
+ p, err := encodePlatform(&platform[i]) |
|
| 62 |
+ if err != nil {
|
|
| 63 |
+ return nil, err |
|
| 64 |
+ } |
|
| 65 |
+ if _, ok := seen[p]; !ok {
|
|
| 66 |
+ out = append(out, p) |
|
| 67 |
+ seen[p] = struct{}{}
|
|
| 68 |
+ } |
|
| 69 |
+ } |
|
| 70 |
+ return out, nil |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 73 |
+// encodePlatform marshals the given platform to JSON format, to |
|
| 74 |
+// be used for query-parameters for filtering / selecting platforms. It |
|
| 75 |
+// is used as a helper for encodePlatforms, |
|
| 76 |
+func encodePlatform(platform *ocispec.Platform) (string, error) {
|
|
| 77 |
+ p, err := json.Marshal(platform) |
|
| 78 |
+ if err != nil {
|
|
| 79 |
+ return "", fmt.Errorf("%w: invalid platform: %v", cerrdefs.ErrInvalidArgument, err)
|
|
| 80 |
+ } |
|
| 81 |
+ return string(p), nil |
|
| 82 |
+} |
| 0 | 83 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,21 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// ServerVersion returns information of the docker client and server host. |
|
| 10 |
+func (cli *Client) ServerVersion(ctx context.Context) (types.Version, error) {
|
|
| 11 |
+ resp, err := cli.get(ctx, "/version", nil, nil) |
|
| 12 |
+ defer ensureReaderClosed(resp) |
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return types.Version{}, err
|
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ var server types.Version |
|
| 18 |
+ err = json.NewDecoder(resp.Body).Decode(&server) |
|
| 19 |
+ return server, err |
|
| 20 |
+} |
| 0 | 21 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,21 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/volume" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// VolumeCreate creates a volume in the docker host. |
|
| 10 |
+func (cli *Client) VolumeCreate(ctx context.Context, options volume.CreateOptions) (volume.Volume, error) {
|
|
| 11 |
+ resp, err := cli.post(ctx, "/volumes/create", nil, options, nil) |
|
| 12 |
+ defer ensureReaderClosed(resp) |
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return volume.Volume{}, err
|
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ var vol volume.Volume |
|
| 18 |
+ err = json.NewDecoder(resp.Body).Decode(&vol) |
|
| 19 |
+ return vol, err |
|
| 20 |
+} |
| 0 | 21 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,40 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "context" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "io" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/moby/moby/api/types/volume" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// VolumeInspect returns the information about a specific volume in the docker host. |
|
| 12 |
+func (cli *Client) VolumeInspect(ctx context.Context, volumeID string) (volume.Volume, error) {
|
|
| 13 |
+ vol, _, err := cli.VolumeInspectWithRaw(ctx, volumeID) |
|
| 14 |
+ return vol, err |
|
| 15 |
+} |
|
| 16 |
+ |
|
| 17 |
+// VolumeInspectWithRaw returns the information about a specific volume in the docker host and its raw representation |
|
| 18 |
+func (cli *Client) VolumeInspectWithRaw(ctx context.Context, volumeID string) (volume.Volume, []byte, error) {
|
|
| 19 |
+ volumeID, err := trimID("volume", volumeID)
|
|
| 20 |
+ if err != nil {
|
|
| 21 |
+ return volume.Volume{}, nil, err
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ resp, err := cli.get(ctx, "/volumes/"+volumeID, nil, nil) |
|
| 25 |
+ defer ensureReaderClosed(resp) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ return volume.Volume{}, nil, err
|
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ body, err := io.ReadAll(resp.Body) |
|
| 31 |
+ if err != nil {
|
|
| 32 |
+ return volume.Volume{}, nil, err
|
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ var vol volume.Volume |
|
| 36 |
+ rdr := bytes.NewReader(body) |
|
| 37 |
+ err = json.NewDecoder(rdr).Decode(&vol) |
|
| 38 |
+ return vol, body, err |
|
| 39 |
+} |
| 0 | 40 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,33 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/filters" |
|
| 8 |
+ "github.com/moby/moby/api/types/volume" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// VolumeList returns the volumes configured in the docker host. |
|
| 12 |
+func (cli *Client) VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) {
|
|
| 13 |
+ query := url.Values{}
|
|
| 14 |
+ |
|
| 15 |
+ if options.Filters.Len() > 0 {
|
|
| 16 |
+ //nolint:staticcheck // ignore SA1019 for old code |
|
| 17 |
+ filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) |
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return volume.ListResponse{}, err
|
|
| 20 |
+ } |
|
| 21 |
+ query.Set("filters", filterJSON)
|
|
| 22 |
+ } |
|
| 23 |
+ resp, err := cli.get(ctx, "/volumes", query, nil) |
|
| 24 |
+ defer ensureReaderClosed(resp) |
|
| 25 |
+ if err != nil {
|
|
| 26 |
+ return volume.ListResponse{}, err
|
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ var volumes volume.ListResponse |
|
| 30 |
+ err = json.NewDecoder(resp.Body).Decode(&volumes) |
|
| 31 |
+ return volumes, err |
|
| 32 |
+} |
| 0 | 33 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,35 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/filters" |
|
| 8 |
+ "github.com/moby/moby/api/types/volume" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// VolumesPrune requests the daemon to delete unused data |
|
| 12 |
+func (cli *Client) VolumesPrune(ctx context.Context, pruneFilters filters.Args) (volume.PruneReport, error) {
|
|
| 13 |
+ if err := cli.NewVersionError(ctx, "1.25", "volume prune"); err != nil {
|
|
| 14 |
+ return volume.PruneReport{}, err
|
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ query, err := getFiltersQuery(pruneFilters) |
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return volume.PruneReport{}, err
|
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ resp, err := cli.post(ctx, "/volumes/prune", query, nil, nil) |
|
| 23 |
+ defer ensureReaderClosed(resp) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return volume.PruneReport{}, err
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ var report volume.PruneReport |
|
| 29 |
+ if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
|
|
| 30 |
+ return volume.PruneReport{}, fmt.Errorf("Error retrieving volume prune report: %v", err)
|
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ return report, nil |
|
| 34 |
+} |
| 0 | 35 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,34 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/versions" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// VolumeRemove removes a volume from the docker host. |
|
| 10 |
+func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool) error {
|
|
| 11 |
+ volumeID, err := trimID("volume", volumeID)
|
|
| 12 |
+ if err != nil {
|
|
| 13 |
+ return err |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ if force {
|
|
| 18 |
+ // Make sure we negotiated (if the client is configured to do so), |
|
| 19 |
+ // as code below contains API-version specific handling of options. |
|
| 20 |
+ // |
|
| 21 |
+ // Normally, version-negotiation (if enabled) would not happen until |
|
| 22 |
+ // the API request is made. |
|
| 23 |
+ if err := cli.checkVersion(ctx); err != nil {
|
|
| 24 |
+ return err |
|
| 25 |
+ } |
|
| 26 |
+ if versions.GreaterThanOrEqualTo(cli.version, "1.25") {
|
|
| 27 |
+ query.Set("force", "1")
|
|
| 28 |
+ } |
|
| 29 |
+ } |
|
| 30 |
+ resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil) |
|
| 31 |
+ defer ensureReaderClosed(resp) |
|
| 32 |
+ return err |
|
| 33 |
+} |
| 0 | 34 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,28 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/moby/moby/api/types/swarm" |
|
| 7 |
+ "github.com/moby/moby/api/types/volume" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// VolumeUpdate updates a volume. This only works for Cluster Volumes, and |
|
| 11 |
+// only some fields can be updated. |
|
| 12 |
+func (cli *Client) VolumeUpdate(ctx context.Context, volumeID string, version swarm.Version, options volume.UpdateOptions) error {
|
|
| 13 |
+ volumeID, err := trimID("volume", volumeID)
|
|
| 14 |
+ if err != nil {
|
|
| 15 |
+ return err |
|
| 16 |
+ } |
|
| 17 |
+ if err := cli.NewVersionError(ctx, "1.42", "volume update"); err != nil {
|
|
| 18 |
+ return err |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ query := url.Values{}
|
|
| 22 |
+ query.Set("version", version.String())
|
|
| 23 |
+ |
|
| 24 |
+ resp, err := cli.put(ctx, "/volumes/"+volumeID, query, options, nil) |
|
| 25 |
+ ensureReaderClosed(resp) |
|
| 26 |
+ return err |
|
| 27 |
+} |
| ... | ... |
@@ -962,6 +962,9 @@ github.com/moby/moby/api/types/system |
| 962 | 962 |
github.com/moby/moby/api/types/time |
| 963 | 963 |
github.com/moby/moby/api/types/versions |
| 964 | 964 |
github.com/moby/moby/api/types/volume |
| 965 |
+# github.com/moby/moby/client v0.0.0 => ./client |
|
| 966 |
+## explicit; go 1.23.0 |
|
| 967 |
+github.com/moby/moby/client |
|
| 965 | 968 |
# github.com/moby/patternmatcher v0.6.0 |
| 966 | 969 |
## explicit; go 1.19 |
| 967 | 970 |
github.com/moby/patternmatcher |
| ... | ... |
@@ -1749,3 +1752,4 @@ tags.cncf.io/container-device-interface/pkg/parser |
| 1749 | 1749 |
## explicit; go 1.19 |
| 1750 | 1750 |
tags.cncf.io/container-device-interface/specs-go |
| 1751 | 1751 |
# github.com/moby/moby/api => ./api |
| 1752 |
+# github.com/moby/moby/client => ./client |