build(deps): bump github.com/testcontainers/testcontainers-go/modules/opensearch

Bumps [github.com/testcontainers/testcontainers-go/modules/opensearch](https://github.com/testcontainers/testcontainers-go) from 0.41.0 to 0.42.0.
- [Release notes](https://github.com/testcontainers/testcontainers-go/releases)
- [Commits](https://github.com/testcontainers/testcontainers-go/compare/v0.41.0...v0.42.0)

---
updated-dependencies:
- dependency-name: github.com/testcontainers/testcontainers-go/modules/opensearch
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2026-04-22 11:29:03 +02:00
committed by Ralf Haferkamp
parent 68b356292b
commit d88bd1aa59
445 changed files with 11220 additions and 26099 deletions
@@ -26,3 +26,8 @@ coverage.out
# Usage metrics script binary
usage-metrics/scripts/collect-metrics
# Gas Town / Claude Code agent artifacts
.beads/
.claude/
.runtime/
+2
View File
@@ -8,6 +8,7 @@ formatters:
- standard
- default
- prefix(github.com/testcontainers)
linters:
enable:
- errorlint
@@ -115,4 +116,5 @@ output:
path: stdout
run:
relative-path-mode: gitroot
version: "2"
+5 -5
View File
@@ -14,15 +14,15 @@ This is a **Go monorepo** containing:
## Environment Setup
### Go Version
- **Required**: Go 1.25.7
- **Required**: Go 1.25.9
- **Tool**: Use [gvm](https://github.com/andrewkroh/gvm) for version management
- **CRITICAL**: Always run this before ANY Go command:
```bash
# For Apple Silicon (M1/M2/M3)
eval "$(gvm 1.25.7 --arch=arm64)"
eval "$(gvm 1.25.9 --arch=arm64)"
# For Intel/AMD (x86_64)
eval "$(gvm 1.25.7 --arch=amd64)"
eval "$(gvm 1.25.9 --arch=amd64)"
```
### Project Structure
@@ -171,14 +171,14 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
### When Tests Fail
1. **Read the error message carefully** - it usually tells you exactly what's wrong
2. **Check if it's a lint issue** - run `make pre-commit` first
3. **Verify Go version** - ensure using Go 1.25.7
3. **Verify Go version** - ensure using Go 1.25.9
4. **Check Docker** - some tests require Docker daemon running
## Common Pitfalls to Avoid
### Code Issues
- ❌ Using interface types as return values
- ❌ Forgetting to run `eval "$(gvm 1.25.7 --arch=arm64)"`
- ❌ Forgetting to run `eval "$(gvm 1.25.9 --arch=arm64)"`
- ❌ Not handling errors from built-in options
- ❌ Using module-specific container names (`PostgresContainer`)
- ❌ Calling `.Customize()` method instead of direct function call
+1 -1
View File
@@ -8,7 +8,7 @@ verify_ssl = true
[packages]
mkdocs = "==1.5.3"
mkdocs-codeinclude-plugin = "==0.3.1"
mkdocs-include-markdown-plugin = "==7.2.1"
mkdocs-include-markdown-plugin = "==7.2.2"
mkdocs-material = "==9.5.18"
mkdocs-markdownextradata-plugin = "==0.2.6"
+153 -116
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "c30436cdeb31baa333fdc82d0679532754a1ea51be468b4fd83da0de242b70cf"
"sha256": "d2dc50d3b1c6818dd8a8fb4fa7a60013292b7f173db53d3e996bf43b4191ad70"
},
"pipfile-spec": 6,
"requires": {
@@ -34,109 +34,146 @@
},
"certifi": {
"hashes": [
"sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2",
"sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995"
"sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa",
"sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"
],
"markers": "python_version >= '3.7'",
"version": "==2025.7.14"
"version": "==2026.2.25"
},
"charset-normalizer": {
"hashes": [
"sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4",
"sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45",
"sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7",
"sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0",
"sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7",
"sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d",
"sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d",
"sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0",
"sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184",
"sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db",
"sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b",
"sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64",
"sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b",
"sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8",
"sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff",
"sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344",
"sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58",
"sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e",
"sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471",
"sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148",
"sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a",
"sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836",
"sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e",
"sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63",
"sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c",
"sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1",
"sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01",
"sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366",
"sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58",
"sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5",
"sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c",
"sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2",
"sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a",
"sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597",
"sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b",
"sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5",
"sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb",
"sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f",
"sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0",
"sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941",
"sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0",
"sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86",
"sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7",
"sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7",
"sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455",
"sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6",
"sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4",
"sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0",
"sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3",
"sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1",
"sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6",
"sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981",
"sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c",
"sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980",
"sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645",
"sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7",
"sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12",
"sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa",
"sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd",
"sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef",
"sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f",
"sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2",
"sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d",
"sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5",
"sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02",
"sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3",
"sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd",
"sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e",
"sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214",
"sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd",
"sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a",
"sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c",
"sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681",
"sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba",
"sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f",
"sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a",
"sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28",
"sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691",
"sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82",
"sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a",
"sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027",
"sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7",
"sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518",
"sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf",
"sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b",
"sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9",
"sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544",
"sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da",
"sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509",
"sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f",
"sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a",
"sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"
"sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e",
"sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c",
"sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5",
"sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815",
"sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f",
"sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0",
"sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484",
"sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407",
"sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6",
"sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8",
"sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264",
"sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815",
"sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2",
"sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4",
"sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579",
"sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f",
"sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa",
"sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95",
"sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab",
"sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297",
"sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a",
"sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e",
"sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84",
"sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8",
"sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0",
"sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9",
"sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f",
"sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1",
"sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843",
"sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565",
"sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7",
"sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c",
"sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b",
"sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7",
"sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687",
"sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9",
"sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14",
"sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89",
"sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f",
"sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0",
"sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9",
"sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a",
"sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389",
"sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0",
"sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30",
"sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd",
"sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e",
"sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9",
"sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc",
"sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532",
"sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d",
"sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae",
"sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2",
"sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64",
"sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f",
"sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557",
"sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e",
"sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff",
"sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398",
"sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db",
"sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a",
"sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43",
"sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597",
"sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c",
"sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e",
"sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2",
"sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54",
"sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e",
"sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4",
"sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4",
"sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7",
"sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6",
"sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5",
"sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194",
"sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69",
"sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f",
"sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316",
"sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e",
"sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73",
"sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8",
"sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923",
"sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88",
"sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f",
"sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21",
"sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4",
"sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6",
"sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc",
"sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2",
"sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866",
"sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021",
"sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2",
"sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d",
"sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8",
"sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de",
"sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237",
"sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4",
"sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778",
"sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb",
"sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc",
"sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602",
"sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4",
"sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f",
"sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5",
"sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611",
"sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8",
"sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf",
"sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d",
"sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b",
"sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db",
"sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e",
"sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077",
"sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd",
"sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef",
"sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e",
"sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8",
"sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe",
"sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058",
"sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17",
"sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833",
"sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421",
"sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550",
"sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff",
"sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2",
"sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc",
"sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982",
"sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d",
"sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed",
"sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104",
"sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"
],
"markers": "python_version >= '3.7'",
"version": "==3.4.2"
"version": "==3.4.6"
},
"click": {
"hashes": [
@@ -163,11 +200,11 @@
},
"idna": {
"hashes": [
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
"sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
],
"markers": "python_version >= '3.6'",
"version": "==3.10"
"markers": "python_version >= '3.8'",
"version": "==3.11"
},
"importlib-metadata": {
"hashes": [
@@ -316,12 +353,12 @@
},
"mkdocs-include-markdown-plugin": {
"hashes": [
"sha256:30da634c568ea5d5f9e5881d51f80ac30d8c5f891cec160344ad7a0fdaea6286",
"sha256:5d94db87b06cd303619dbaebba5f7f43a3ded7fd7709451d26f08c176376ffec"
"sha256:f052ccb741eccf498116b826c1d78a2d761c56747372594709441cee0963fbc9",
"sha256:f2ec4487cf32d3e33ca528f9366f20fb9280ded9c8d1630eb2bbda244962dcd1"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==7.2.1"
"version": "==7.2.2"
},
"mkdocs-markdownextradata-plugin": {
"hashes": [
@@ -373,19 +410,20 @@
},
"platformdirs": {
"hashes": [
"sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd",
"sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291"
"sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934",
"sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"
],
"markers": "python_version >= '3.10'",
"version": "==4.9.2"
"version": "==4.9.4"
},
"pygments": {
"hashes": [
"sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887",
"sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"
"sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f",
"sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"
],
"markers": "python_version >= '3.8'",
"version": "==2.19.2"
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==2.20.0"
},
"pymdown-extensions": {
"hashes": [
@@ -586,12 +624,12 @@
},
"requests": {
"hashes": [
"sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c",
"sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"
"sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b",
"sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==2.32.4"
"markers": "python_version >= '3.10'",
"version": "==2.33.0"
},
"six": {
"hashes": [
@@ -606,7 +644,6 @@
"sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed",
"sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==2.6.3"
},
+5 -3
View File
@@ -6,6 +6,8 @@ import (
"fmt"
"reflect"
"time"
"github.com/moby/moby/client"
)
// TerminateOptions is a type that holds the options for terminating a container.
@@ -48,15 +50,15 @@ func (o *TerminateOptions) Cleanup() error {
if len(o.volumes) == 0 {
return nil
}
client, err := NewDockerClientWithOpts(o.ctx)
apiClient, err := NewDockerClientWithOpts(o.ctx)
if err != nil {
return fmt.Errorf("docker client: %w", err)
}
defer client.Close()
defer apiClient.Close()
// Best effort to remove all volumes.
var errs []error
for _, volume := range o.volumes {
if errRemove := client.VolumeRemove(o.ctx, volume, true); errRemove != nil {
if _, errRemove := apiClient.VolumeRemove(o.ctx, volume, client.VolumeRemoveOptions{Force: true}); errRemove != nil {
errs = append(errs, fmt.Errorf("volume remove %q: %w", volume, errRemove))
}
}
+5 -1
View File
@@ -14,6 +14,9 @@ $(GOBIN)/gotestsum:
$(GOBIN)/mockery:
$(call go_install,github.com/vektra/mockery/v2@v2.53.4)
$(GOBIN)/gci:
$(call go_install,github.com/daixiang0/gci@v0.13.5)
.PHONY: install
install: $(GOBIN)/golangci-lint $(GOBIN)/gotestsum $(GOBIN)/mockery
@@ -30,9 +33,10 @@ dependencies-scan:
.PHONY: lint
lint: $(GOBIN)/golangci-lint
golangci-lint run --verbose -c $(ROOT_DIR)/.golangci.yml --fix
golangci-lint run -c $(ROOT_DIR)/.golangci.yml --fix
.PHONY: generate
generate: $(GOBIN)/gci
generate: $(GOBIN)/mockery
go generate ./...
+33 -34
View File
@@ -13,13 +13,12 @@ import (
"time"
"github.com/cpuguy83/dockercfg"
"github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/registry"
"github.com/docker/go-connections/nat"
"github.com/google/uuid"
"github.com/moby/go-archive"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client"
"github.com/moby/patternmatcher/ignorefile"
tcexec "github.com/testcontainers/testcontainers-go/exec"
@@ -34,23 +33,23 @@ import (
type DeprecatedContainer interface {
GetHostEndpoint(ctx context.Context, port string) (string, string, error)
GetIPAddress(ctx context.Context) (string, error)
LivenessCheckPorts(ctx context.Context) (nat.PortSet, error)
LivenessCheckPorts(ctx context.Context) (network.PortSet, error)
Terminate(ctx context.Context) error
}
// Container allows getting info about and controlling a single container instance
type Container interface {
GetContainerID() string // get the container id from the provider
Endpoint(context.Context, string) (string, error) // get proto://ip:port string for the lowest exposed port
PortEndpoint(ctx context.Context, port nat.Port, proto string) (string, error) // get proto://ip:port string for the given exposed port
Host(context.Context) (string, error) // get host where the container port is exposed
Inspect(context.Context) (*container.InspectResponse, error) // get container info
MappedPort(context.Context, nat.Port) (nat.Port, error) // get externally mapped port for a container port
Ports(context.Context) (nat.PortMap, error) // Deprecated: Use c.Inspect(ctx).NetworkSettings.Ports instead
SessionID() string // get session id
IsRunning() bool // IsRunning returns true if the container is running, false otherwise.
Start(context.Context) error // start the container
Stop(context.Context, *time.Duration) error // stop the container
GetContainerID() string // get the container id from the provider
Endpoint(context.Context, string) (string, error) // get proto://ip:port string for the lowest exposed port
PortEndpoint(ctx context.Context, port string, proto string) (string, error) // get proto://ip:port string for the given exposed port
Host(context.Context) (string, error) // get host where the container port is exposed
Inspect(context.Context) (*container.InspectResponse, error) // get container info
MappedPort(context.Context, string) (network.Port, error) // get externally mapped port for a container port
Ports(context.Context) (network.PortMap, error) // Deprecated: Use c.Inspect(ctx).NetworkSettings.Ports instead
SessionID() string // get session id
IsRunning() bool // IsRunning returns true if the container is running, false otherwise.
Start(context.Context) error // start the container
Stop(context.Context, *time.Duration) error // stop the container
// Terminate stops and removes the container and its image if it was built and not flagged as kept.
Terminate(ctx context.Context, opts ...TerminateOption) error
@@ -75,15 +74,15 @@ type Container interface {
// ImageBuildInfo defines what is needed to build an image
type ImageBuildInfo interface {
BuildOptions() (build.ImageBuildOptions, error) // converts the ImageBuildInfo to a build.ImageBuildOptions
GetContext() (io.Reader, error) // the path to the build context
GetDockerfile() string // the relative path to the Dockerfile, including the file itself
GetRepo() string // get repo label for image
GetTag() string // get tag label for image
BuildLogWriter() io.Writer // for output of build log, use io.Discard to disable the output
ShouldBuildImage() bool // return true if the image needs to be built
GetBuildArgs() map[string]*string // return the environment args used to build the Dockerfile
GetAuthConfigs() map[string]registry.AuthConfig // Deprecated. Testcontainers will detect registry credentials automatically. Return the auth configs to be able to pull from an authenticated docker registry
BuildOptions() (client.ImageBuildOptions, error) // converts the ImageBuildInfo to a build.ImageBuildOptions
GetContext() (io.Reader, error) // the path to the build context
GetDockerfile() string // the relative path to the Dockerfile, including the file itself
GetRepo() string // get repo label for image
GetTag() string // get tag label for image
BuildLogWriter() io.Writer // for output of build log, use io.Discard to disable the output
ShouldBuildImage() bool // return true if the image needs to be built
GetBuildArgs() map[string]*string // return the environment args used to build the Dockerfile
GetAuthConfigs() map[string]registry.AuthConfig // Deprecated. Testcontainers will detect registry credentials automatically. Return the auth configs to be able to pull from an authenticated docker registry
}
// FromDockerfile represents the parameters needed to build an image from a Dockerfile
@@ -105,7 +104,7 @@ type FromDockerfile struct {
// BuildOptionsModifier Modifier for the build options before image build. Use it for
// advanced configurations while building the image. Please consider that the modifier
// is called after the default build options are set.
BuildOptionsModifier func(*build.ImageBuildOptions)
BuildOptionsModifier func(*client.ImageBuildOptions)
}
type ContainerFile struct {
@@ -435,8 +434,8 @@ func (c *ContainerRequest) BuildLogWriter() io.Writer {
// BuildOptions returns the image build options when building a Docker image from a Dockerfile.
// It will apply some defaults and finally call the BuildOptionsModifier from the FromDockerfile struct,
// if set.
func (c *ContainerRequest) BuildOptions() (build.ImageBuildOptions, error) {
buildOptions := build.ImageBuildOptions{
func (c *ContainerRequest) BuildOptions() (client.ImageBuildOptions, error) {
buildOptions := client.ImageBuildOptions{
Remove: true,
ForceRemove: true,
}
@@ -452,7 +451,7 @@ func (c *ContainerRequest) BuildOptions() (build.ImageBuildOptions, error) {
// Make sure the auth configs from the Dockerfile are set right after the user-defined build options.
authsFromDockerfile, err := getAuthConfigsFromDockerfile(c)
if err != nil {
return build.ImageBuildOptions{}, fmt.Errorf("auth configs from Dockerfile: %w", err)
return client.ImageBuildOptions{}, fmt.Errorf("auth configs from Dockerfile: %w", err)
}
if buildOptions.AuthConfigs == nil {
@@ -468,7 +467,7 @@ func (c *ContainerRequest) BuildOptions() (build.ImageBuildOptions, error) {
for _, is := range c.ImageSubstitutors {
modifiedTag, err := is.Substitute(tag)
if err != nil {
return build.ImageBuildOptions{}, fmt.Errorf("failed to substitute image %s with %s: %w", tag, is.Description(), err)
return client.ImageBuildOptions{}, fmt.Errorf("failed to substitute image %s with %s: %w", tag, is.Description(), err)
}
if modifiedTag != tag {
@@ -487,10 +486,10 @@ func (c *ContainerRequest) BuildOptions() (build.ImageBuildOptions, error) {
if !c.ShouldKeepBuiltImage() {
dst := GenericLabels()
if err = core.MergeCustomLabels(dst, c.Labels); err != nil {
return build.ImageBuildOptions{}, err
return client.ImageBuildOptions{}, err
}
if err = core.MergeCustomLabels(dst, buildOptions.Labels); err != nil {
return build.ImageBuildOptions{}, err
return client.ImageBuildOptions{}, err
}
buildOptions.Labels = dst
}
@@ -498,7 +497,7 @@ func (c *ContainerRequest) BuildOptions() (build.ImageBuildOptions, error) {
// Do this as late as possible to ensure we don't leak the context on error/panic.
buildContext, err := c.GetContext()
if err != nil {
return build.ImageBuildOptions{}, err
return client.ImageBuildOptions{}, err
}
buildOptions.Context = buildContext
+141 -111
View File
@@ -4,9 +4,7 @@ import (
"archive/tar"
"bufio"
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
@@ -17,22 +15,20 @@ import (
"path/filepath"
"regexp"
"slices"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/containerd/errdefs"
"github.com/containerd/platforms"
"github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
"github.com/moby/term"
"github.com/moby/moby/api/pkg/authconfig"
"github.com/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/jsonmessage"
specs "github.com/opencontainers/image-spec/specs-go/v1"
tcexec "github.com/testcontainers/testcontainers-go/exec"
@@ -74,7 +70,7 @@ type DockerContainer struct {
Image string
exposedPorts []string // a reference to the container's requested exposed ports. It allows checking they are ready before any wait strategy
isRunning bool
isRunning atomic.Bool
imageWasBuilt bool
// keepBuiltImage makes Terminate not remove the image if imageWasBuilt.
keepBuiltImage bool
@@ -96,7 +92,7 @@ type DockerContainer struct {
logger log.Logger
lifecycleHooks []ContainerLifecycleHooks
healthStatus string // container health status, will default to healthStatusNone if no healthcheck is present
healthStatus container.HealthStatus // container health status, will default to healthStatusNone if no healthcheck is present
}
// SetLogger sets the logger for the container
@@ -119,7 +115,7 @@ func (c *DockerContainer) GetContainerID() string {
}
func (c *DockerContainer) IsRunning() bool {
return c.isRunning
return c.isRunning.Load()
}
// Endpoint gets proto://host:port string for the lowest numbered exposed port
@@ -131,20 +127,20 @@ func (c *DockerContainer) Endpoint(ctx context.Context, proto string) (string, e
}
// Get lowest numbered bound port.
var lowestPort nat.Port
var lowestPort network.Port
for port := range inspect.NetworkSettings.Ports {
if lowestPort == "" || port.Int() < lowestPort.Int() {
if lowestPort.IsZero() || port.Num() < lowestPort.Num() {
lowestPort = port
}
}
return c.PortEndpoint(ctx, lowestPort, proto)
return c.PortEndpoint(ctx, lowestPort.String(), proto)
}
// PortEndpoint gets proto://host:port string for the given exposed port
// It returns proto://host:port or proto://[IPv6host]:port string for the given exposed port.
// It returns just host:port or [IPv6host]:port if proto is blank.
func (c *DockerContainer) PortEndpoint(ctx context.Context, port nat.Port, proto string) (string, error) {
func (c *DockerContainer) PortEndpoint(ctx context.Context, port string, proto string) (string, error) {
host, err := c.Host(ctx)
if err != nil {
return "", err
@@ -181,40 +177,56 @@ func (c *DockerContainer) Inspect(ctx context.Context) (*container.InspectRespon
return nil, err
}
return jsonRaw, nil
return &jsonRaw.Container, nil
}
// MappedPort gets externally mapped port for a container port
func (c *DockerContainer) MappedPort(ctx context.Context, port nat.Port) (nat.Port, error) {
func (c *DockerContainer) MappedPort(ctx context.Context, port string) (network.Port, error) {
inspect, err := c.Inspect(ctx)
if err != nil {
return "", fmt.Errorf("inspect: %w", err)
return network.Port{}, fmt.Errorf("inspect: %w", err)
}
// The old nat.Port type (a plain string) accepted empty strings:
// nat.SplitProtoPort("") returns ("", ""), so Port() == "" and
// no container port matches, yielding "not found".
// See https://github.com/docker/go-connections/blob/v0.6.0/nat/nat.go#L101-L110
// Skip parsing here to preserve that behavior and avoid a
// ParsePort error on empty input.
var nwPort network.Port
if port != "" {
nwPort, err = network.ParsePort(port)
if err != nil {
return network.Port{}, err
}
}
if inspect.HostConfig.NetworkMode == "host" {
return port, nil
return nwPort, nil
}
ports := inspect.NetworkSettings.Ports
for k, p := range ports {
if k.Port() != port.Port() {
if k.Num() != nwPort.Num() {
continue
}
if port.Proto() != "" && k.Proto() != port.Proto() {
if nwPort.Proto() != "" && k.Proto() != nwPort.Proto() {
continue
}
if len(p) == 0 {
continue
}
return nat.NewPort(k.Proto(), p[0].HostPort)
pNum, _ := strconv.ParseUint(p[0].HostPort, 10, 16)
hPort, _ := network.PortFrom(uint16(pNum), k.Proto())
return hPort, nil
}
return "", errdefs.ErrNotFound.WithMessage(fmt.Sprintf("port %q not found", port))
return network.Port{}, errdefs.ErrNotFound.WithMessage(fmt.Sprintf("port %q not found", nwPort))
}
// Deprecated: use c.Inspect(ctx).NetworkSettings.Ports instead.
// Ports gets the exposed ports for the container.
func (c *DockerContainer) Ports(ctx context.Context) (nat.PortMap, error) {
func (c *DockerContainer) Ports(ctx context.Context) (network.PortMap, error) {
inspect, err := c.Inspect(ctx)
if err != nil {
return nil, err
@@ -234,7 +246,7 @@ func (c *DockerContainer) Start(ctx context.Context) error {
return fmt.Errorf("starting hook: %w", err)
}
if err := c.provider.client.ContainerStart(ctx, c.ID, container.StartOptions{}); err != nil {
if _, err := c.provider.client.ContainerStart(ctx, c.ID, client.ContainerStartOptions{}); err != nil {
return fmt.Errorf("container start: %w", err)
}
defer c.provider.Close()
@@ -244,7 +256,7 @@ func (c *DockerContainer) Start(ctx context.Context) error {
return fmt.Errorf("started hook: %w", err)
}
c.isRunning = true
c.isRunning.Store(true)
err = c.readiedHook(ctx)
if err != nil {
@@ -278,20 +290,20 @@ func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) erro
return fmt.Errorf("stopping hook: %w", err)
}
var options container.StopOptions
var options client.ContainerStopOptions
if timeout != nil {
timeoutSeconds := int(timeout.Seconds())
options.Timeout = &timeoutSeconds
}
if err := c.provider.client.ContainerStop(ctx, c.ID, options); err != nil {
if _, err := c.provider.client.ContainerStop(ctx, c.ID, options); err != nil {
return fmt.Errorf("container stop: %w", err)
}
defer c.provider.Close()
c.isRunning = false
c.isRunning.Store(false)
err = c.stoppedHook(ctx)
if err != nil {
@@ -331,17 +343,17 @@ func (c *DockerContainer) Terminate(ctx context.Context, opts ...TerminateOption
// TODO: Handle errors from ContainerRemove more correctly, e.g. should we
// run the terminated hook?
errs := []error{
c.terminatingHook(ctx),
c.provider.client.ContainerRemove(ctx, c.GetContainerID(), container.RemoveOptions{
RemoveVolumes: true,
Force: true,
}),
c.terminatedHook(ctx),
}
var errs []error
errs = append(errs, c.terminatingHook(ctx))
_, err = c.provider.client.ContainerRemove(ctx, c.GetContainerID(), client.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
})
errs = append(errs, err)
errs = append(errs, c.terminatedHook(ctx))
if c.imageWasBuilt && !c.keepBuiltImage {
_, err := c.provider.client.ImageRemove(ctx, c.Image, image.RemoveOptions{
_, err := c.provider.client.ImageRemove(ctx, c.Image, client.ImageRemoveOptions{
Force: true,
PruneChildren: true,
})
@@ -349,7 +361,7 @@ func (c *DockerContainer) Terminate(ctx context.Context, opts ...TerminateOption
}
c.sessionID = ""
c.isRunning = false
c.isRunning.Store(false)
if err = options.Cleanup(); err != nil {
errs = append(errs, err)
@@ -359,9 +371,9 @@ func (c *DockerContainer) Terminate(ctx context.Context, opts ...TerminateOption
}
// update container raw info
func (c *DockerContainer) inspectRawContainer(ctx context.Context) (*container.InspectResponse, error) {
func (c *DockerContainer) inspectRawContainer(ctx context.Context) (*client.ContainerInspectResult, error) {
defer c.provider.Close()
inspect, err := c.provider.client.ContainerInspect(ctx, c.ID)
inspect, err := c.provider.client.ContainerInspect(ctx, c.ID, client.ContainerInspectOptions{})
if err != nil {
return nil, err
}
@@ -372,12 +384,10 @@ func (c *DockerContainer) inspectRawContainer(ctx context.Context) (*container.I
// Logs will fetch both STDOUT and STDERR from the current container. Returns a
// ReadCloser and leaves it up to the caller to extract what it wants.
func (c *DockerContainer) Logs(ctx context.Context) (io.ReadCloser, error) {
options := container.LogsOptions{
rc, err := c.provider.client.ContainerLogs(ctx, c.ID, client.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
}
rc, err := c.provider.client.ContainerLogs(ctx, c.ID, options)
})
if err != nil {
return nil, err
}
@@ -469,7 +479,7 @@ func (c *DockerContainer) State(ctx context.Context) (*container.State, error) {
if err != nil {
return nil, err
}
return inspect.State, nil
return inspect.Container.State, nil
}
// Networks gets the names of the networks the container is attached to.
@@ -497,14 +507,13 @@ func (c *DockerContainer) ContainerIP(ctx context.Context) (string, error) {
return "", err
}
//nolint:staticcheck // SA1019: IPAddress is deprecated, but we need it for compatibility until v29
ip := inspect.NetworkSettings.IPAddress
if ip == "" {
// use IP from "Networks" if only single network defined
networks := inspect.NetworkSettings.Networks
if len(networks) == 1 {
for _, v := range networks {
ip = v.IPAddress
var ip string
// IPAddress is deprecated; use IP from "Networks" if only single network defined
networks := inspect.NetworkSettings.Networks
if len(networks) == 1 {
for _, v := range networks {
if v.IPAddress.IsValid() {
ip = v.IPAddress.String()
}
}
}
@@ -522,7 +531,9 @@ func (c *DockerContainer) ContainerIPs(ctx context.Context) ([]string, error) {
networks := inspect.NetworkSettings.Networks
ips := make([]string, 0, len(networks))
for _, nw := range networks {
ips = append(ips, nw.IPAddress)
if nw.IPAddress.IsValid() {
ips = append(ips, nw.IPAddress.String())
}
}
return ips, nil
@@ -564,12 +575,12 @@ func (c *DockerContainer) Exec(ctx context.Context, cmd []string, options ...tce
o.Apply(processOptions)
}
response, err := cli.ContainerExecCreate(ctx, c.ID, processOptions.ExecConfig)
response, err := cli.ExecCreate(ctx, c.ID, processOptions.ExecConfig)
if err != nil {
return 0, nil, fmt.Errorf("container exec create: %w", err)
}
hijack, err := cli.ContainerExecAttach(ctx, response.ID, container.ExecAttachOptions{})
hijack, err := cli.ExecAttach(ctx, response.ID, client.ExecAttachOptions{})
if err != nil {
return 0, nil, fmt.Errorf("container exec attach: %w", err)
}
@@ -584,7 +595,7 @@ func (c *DockerContainer) Exec(ctx context.Context, cmd []string, options ...tce
var exitCode int
for {
execResp, err := cli.ContainerExecInspect(ctx, response.ID)
execResp, err := cli.ExecInspect(ctx, response.ID, client.ExecInspectOptions{})
if err != nil {
return 0, nil, fmt.Errorf("container exec inspect: %w", err)
}
@@ -614,13 +625,15 @@ func (fc *FileFromContainer) Close() error {
}
func (c *DockerContainer) CopyFileFromContainer(ctx context.Context, filePath string) (io.ReadCloser, error) {
r, _, err := c.provider.client.CopyFromContainer(ctx, c.ID, filePath)
r, err := c.provider.client.CopyFromContainer(ctx, c.ID, client.CopyFromContainerOptions{
SourcePath: filePath,
})
if err != nil {
return nil, err
}
defer c.provider.Close()
tarReader := tar.NewReader(r)
tarReader := tar.NewReader(r.Content)
// if we got here we have exactly one file in the TAR-stream
// so we advance the index by one so the next call to Read will start reading it
@@ -630,7 +643,7 @@ func (c *DockerContainer) CopyFileFromContainer(ctx context.Context, filePath st
}
ret := &FileFromContainer{
underlying: &r,
underlying: &r.Content,
tarreader: tarReader,
}
@@ -658,7 +671,10 @@ func (c *DockerContainer) CopyDirToContainer(ctx context.Context, hostDirPath st
// create the directory under its parent
parent := filepath.Dir(containerParentPath)
err = c.provider.client.CopyToContainer(ctx, c.ID, parent, buff, container.CopyToContainerOptions{})
_, err = c.provider.client.CopyToContainer(ctx, c.ID, client.CopyToContainerOptions{
DestinationPath: parent,
Content: buff,
})
if err != nil {
return err
}
@@ -716,7 +732,10 @@ func (c *DockerContainer) copyToContainer(ctx context.Context, fileContent func(
return err
}
err = c.provider.client.CopyToContainer(ctx, c.ID, "/", buffer, container.CopyToContainerOptions{})
_, err = c.provider.client.CopyToContainer(ctx, c.ID, client.CopyToContainerOptions{
DestinationPath: "/",
Content: buffer,
})
if err != nil {
return err
}
@@ -817,7 +836,7 @@ func (c *DockerContainer) logProducer(stdout, stderr io.Writer) {
defer c.provider.Close()
// Setup the log options, start from the beginning.
options := &container.LogsOptions{
options := &client.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
@@ -831,7 +850,7 @@ func (c *DockerContainer) logProducer(stdout, stderr io.Writer) {
// copyLogsTimeout copies logs from the container to stdout and stderr with a timeout.
// It returns true if the log production should be retried, false otherwise.
func (c *DockerContainer) copyLogsTimeout(stdout, stderr io.Writer, options *container.LogsOptions) bool {
func (c *DockerContainer) copyLogsTimeout(stdout, stderr io.Writer, options *client.ContainerLogsOptions) bool {
timeoutCtx, cancel := context.WithTimeout(c.logProductionCtx, *c.logProductionTimeout)
defer cancel()
@@ -858,7 +877,7 @@ func (c *DockerContainer) copyLogsTimeout(stdout, stderr io.Writer, options *con
}
// copyLogs copies logs from the container to stdout and stderr.
func (c *DockerContainer) copyLogs(ctx context.Context, stdout, stderr io.Writer, options container.LogsOptions) error {
func (c *DockerContainer) copyLogs(ctx context.Context, stdout, stderr io.Writer, options client.ContainerLogsOptions) error {
rc, err := c.provider.client.ContainerLogs(ctx, c.GetContainerID(), options)
if err != nil {
return fmt.Errorf("container logs: %w", err)
@@ -966,7 +985,8 @@ func (n *DockerNetwork) Remove(ctx context.Context) error {
defer n.provider.Close()
return n.provider.client.NetworkRemove(ctx, n.ID)
_, err := n.provider.client.NetworkRemove(ctx, n.ID, client.NetworkRemoveOptions{})
return err
}
func (n *DockerNetwork) SetTerminationSignal(signal chan bool) {
@@ -1006,22 +1026,22 @@ var _ ContainerProvider = (*DockerProvider)(nil)
// BuildImage will build and image from context and Dockerfile, then return the tag
func (p *DockerProvider) BuildImage(ctx context.Context, img ImageBuildInfo) (string, error) {
var buildOptions build.ImageBuildOptions
var buildOptions client.ImageBuildOptions
resp, err := backoff.RetryNotifyWithData(
func() (build.ImageBuildResponse, error) {
func() (client.ImageBuildResult, error) {
var err error
buildOptions, err = img.BuildOptions()
if err != nil {
return build.ImageBuildResponse{}, backoff.Permanent(fmt.Errorf("build options: %w", err))
return client.ImageBuildResult{}, backoff.Permanent(fmt.Errorf("build options: %w", err))
}
defer tryClose(buildOptions.Context) // release resources in any case
resp, err := p.client.ImageBuild(ctx, buildOptions.Context, buildOptions)
if err != nil {
if isPermanentClientError(err) {
return build.ImageBuildResponse{}, backoff.Permanent(fmt.Errorf("build image: %w", err))
return client.ImageBuildResult{}, backoff.Permanent(fmt.Errorf("build image: %w", err))
}
return build.ImageBuildResponse{}, err
return client.ImageBuildResult{}, err
}
defer p.Close()
@@ -1037,13 +1057,10 @@ func (p *DockerProvider) BuildImage(ctx context.Context, img ImageBuildInfo) (st
}
defer resp.Body.Close()
output := img.BuildLogWriter()
// Always process the output, even if it is not printed
// to ensure that errors during the build process are
// correctly handled.
termFd, isTerm := term.GetFdInfo(output)
if err = jsonmessage.DisplayJSONMessagesStream(resp.Body, output, termFd, isTerm, nil); err != nil {
if err = jsonmessage.DisplayStream(resp.Body, img.BuildLogWriter()); err != nil {
return "", fmt.Errorf("build image: %w", err)
}
@@ -1155,8 +1172,11 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
}
if shouldPullImage {
pullOpt := image.PullOptions{
Platform: req.ImagePlatform, // may be empty
pullOpt := client.ImagePullOptions{}
if req.ImagePlatform != "" {
if pf, err := platforms.Parse(req.ImagePlatform); err == nil {
pullOpt.Platforms = append(pullOpt.Platforms, pf)
}
}
if err := p.attemptToPullImage(ctx, imageName, pullOpt); err != nil {
return nil, err
@@ -1228,7 +1248,13 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
return nil, err
}
resp, err := p.client.ContainerCreate(ctx, dockerInput, hostConfig, networkingConfig, platform, req.Name)
resp, err := p.client.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: dockerInput,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
Platform: platform,
Name: req.Name,
})
if err != nil {
return nil, fmt.Errorf("container create: %w", err)
}
@@ -1243,7 +1269,10 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
endpointSetting := network.EndpointSettings{
Aliases: req.NetworkAliases[n],
}
err = p.client.NetworkConnect(ctx, nw.ID, resp.ID, &endpointSetting)
_, err = p.client.NetworkConnect(ctx, nw.ID, client.NetworkConnectOptions{
Container: resp.ID,
EndpointConfig: &endpointSetting,
})
if err != nil {
return nil, fmt.Errorf("network connect: %w", err)
}
@@ -1288,15 +1317,17 @@ func (p *DockerProvider) findContainerByName(ctx context.Context, name string) (
}
// Note that, 'name' filter will use regex to find the containers
filter := filters.NewArgs(filters.Arg("name", fmt.Sprintf("^%s$", name)))
containers, err := p.client.ContainerList(ctx, container.ListOptions{All: true, Filters: filter})
containers, err := p.client.ContainerList(ctx, client.ContainerListOptions{
All: true,
Filters: make(client.Filters).Add("name", fmt.Sprintf("^%s$", name)),
})
if err != nil {
return nil, fmt.Errorf("container list: %w", err)
}
defer p.Close()
if len(containers) > 0 {
return &containers[0], nil
if len(containers.Items) > 0 {
return &containers.Items[0], nil
}
return nil, nil
}
@@ -1400,10 +1431,10 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
// a paused container. The Docker Engine returns the "cannot start a paused container,
// try unpause instead" error.
switch dcState.Status {
case "running":
case container.StateRunning:
// cannot re-start a running container, but we still need
// to call the startup hooks.
case "paused":
case container.StatePaused:
// TODO: we should unpause the container here.
return nil, fmt.Errorf("cannot start a paused container: %w", errors.ErrUnsupported)
default:
@@ -1417,7 +1448,7 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
return nil, err
}
dc.isRunning = true
dc.isRunning.Store(true)
err = dc.readiedHook(ctx)
if err != nil {
@@ -1429,17 +1460,16 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
// attemptToPullImage tries to pull the image while respecting the ctx cancellations.
// Besides, if the image cannot be pulled due to ErrorNotFound then no need to retry but terminate immediately.
func (p *DockerProvider) attemptToPullImage(ctx context.Context, tag string, pullOpt image.PullOptions) error {
func (p *DockerProvider) attemptToPullImage(ctx context.Context, tag string, pullOpt client.ImagePullOptions) error {
registry, imageAuth, err := DockerImageAuth(ctx, tag)
if err != nil {
p.Logger.Printf("No image auth found for %s. Setting empty credentials for the image: %s. This is expected for public images. Details: %s", registry, tag, err)
} else {
// see https://github.com/docker/docs/blob/e8e1204f914767128814dca0ea008644709c117f/engine/api/sdk/examples.md?plain=1#L649-L657
encodedJSON, err := json.Marshal(imageAuth)
if err != nil {
if encodedAuth, err := authconfig.Encode(imageAuth); err != nil {
p.Logger.Printf("Failed to marshal image auth. Setting empty credentials for the image: %s. Error is: %s", tag, err)
} else {
pullOpt.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
pullOpt.RegistryAuth = encodedAuth
}
}
@@ -1475,7 +1505,7 @@ func (p *DockerProvider) attemptToPullImage(ctx context.Context, tag string, pul
// Health measure the healthiness of the provider. Right now we leverage the
// docker-client Info endpoint to see if the daemon is reachable.
func (p *DockerProvider) Health(ctx context.Context) error {
_, err := p.client.Info(ctx)
_, err := p.client.Info(ctx, client.InfoOptions{})
defer p.Close()
return err
@@ -1577,7 +1607,7 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest)
req.Labels = make(map[string]string)
}
nc := network.CreateOptions{
nc := client.NetworkCreateOptions{
Driver: req.Driver,
Internal: req.Internal,
EnableIPv6: req.EnableIPv6,
@@ -1629,14 +1659,14 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest)
// GetNetwork returns the object representing the network identified by its name
func (p *DockerProvider) GetNetwork(ctx context.Context, req NetworkRequest) (network.Inspect, error) {
networkResource, err := p.client.NetworkInspect(ctx, req.Name, network.InspectOptions{
networkResource, err := p.client.NetworkInspect(ctx, req.Name, client.NetworkInspectOptions{
Verbose: true,
})
if err != nil {
return network.Inspect{}, err
}
return networkResource, err
return networkResource.Network, err
}
func (p *DockerProvider) GetGatewayIP(ctx context.Context) (string, error) {
@@ -1656,8 +1686,8 @@ func (p *DockerProvider) getGatewayIP(ctx context.Context, defaultNetwork string
var ip string
for _, cfg := range nw.IPAM.Config {
if cfg.Gateway != "" {
ip = cfg.Gateway
if cfg.Gateway.IsValid() {
ip = cfg.Gateway.String()
break
}
}
@@ -1683,7 +1713,7 @@ func (p *DockerProvider) ensureDefaultNetworkLocked(ctx context.Context) (string
return p.defaultNetwork, nil
}
networkResources, err := p.client.NetworkList(ctx, network.ListOptions{})
networkResources, err := p.client.NetworkList(ctx, client.NetworkListOptions{})
if err != nil {
return "", fmt.Errorf("network list: %w", err)
}
@@ -1691,8 +1721,8 @@ func (p *DockerProvider) ensureDefaultNetworkLocked(ctx context.Context) (string
// TODO: remove once we have docker context support via #2810
// Prefer the default bridge network if it exists.
// This makes the results stable as network list order is not guaranteed.
for _, net := range networkResources {
switch net.Name {
for _, nw := range networkResources.Items {
switch nw.Name {
case p.defaultBridgeNetworkName:
p.defaultNetwork = p.defaultBridgeNetworkName
return p.defaultNetwork, nil
@@ -1706,7 +1736,7 @@ func (p *DockerProvider) ensureDefaultNetworkLocked(ctx context.Context) (string
}
// Create a bridge network for the container communications.
_, err = p.client.NetworkCreate(ctx, ReaperDefault, network.CreateOptions{
_, err = p.client.NetworkCreate(ctx, ReaperDefault, client.NetworkCreateOptions{
Driver: Bridge,
Attachable: true,
Labels: GenericLabels(),
@@ -1736,7 +1766,6 @@ func (p *DockerProvider) ContainerFromType(ctx context.Context, response contain
Image: response.Image,
imageWasBuilt: false,
sessionID: response.Labels[core.LabelSessionID],
isRunning: response.State == "running",
exposedPorts: exposedPorts,
provider: p,
logger: p.Logger,
@@ -1744,6 +1773,7 @@ func (p *DockerProvider) ContainerFromType(ctx context.Context, response contain
DefaultLoggingHook(p.Logger),
},
}
ctr.isRunning.Store(response.State == "running")
if err = ctr.connectReaper(ctx); err != nil {
return nil, err
@@ -1755,14 +1785,14 @@ func (p *DockerProvider) ContainerFromType(ctx context.Context, response contain
}(ctr)
// populate the raw representation of the container
jsonRaw, err := ctr.inspectRawContainer(ctx)
resp, err := ctr.inspectRawContainer(ctx)
if err != nil {
// Return the container to allow caller to clean up.
return ctr, fmt.Errorf("inspect raw container: %w", err)
}
// the health status of the container, if any
if health := jsonRaw.State.Health; health != nil {
if health := resp.Container.State.Health; health != nil {
ctr.healthStatus = health.Status
}
@@ -1774,12 +1804,12 @@ func (p *DockerProvider) ContainerFromType(ctx context.Context, response contain
func (p *DockerProvider) ListImages(ctx context.Context) ([]ImageInfo, error) {
images := []ImageInfo{}
imageList, err := p.client.ImageList(ctx, image.ListOptions{})
imageList, err := p.client.ImageList(ctx, client.ImageListOptions{})
if err != nil {
return images, fmt.Errorf("listing images %w", err)
}
for _, img := range imageList {
for _, img := range imageList.Items {
for _, tag := range img.RepoTags {
images = append(images, ImageInfo{ID: img.ID, Name: tag})
}
@@ -1838,7 +1868,7 @@ func SaveDockerImageWithPlatforms(platforms ...specs.Platform) SaveImageOption {
// PullImage pulls image from registry
func (p *DockerProvider) PullImage(ctx context.Context, img string) error {
return p.attemptToPullImage(ctx, img, image.PullOptions{})
return p.attemptToPullImage(ctx, img, client.ImagePullOptions{})
}
var permanentClientErrors = []func(error) bool{
+6 -6
View File
@@ -14,7 +14,8 @@ import (
"sync"
"github.com/cpuguy83/dockercfg"
"github.com/docker/docker/api/types/registry"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client"
"github.com/testcontainers/testcontainers-go/internal/core"
)
@@ -87,18 +88,18 @@ func getRegistryAuth(reg string, cfgs map[string]registry.AuthConfig) (registry.
// It will use the docker daemon to get the default registry, returning "https://index.docker.io/v1/" if
// it fails to get the information from the daemon
func defaultRegistry(ctx context.Context) string {
client, err := NewDockerClientWithOpts(ctx)
apiClient, err := NewDockerClientWithOpts(ctx)
if err != nil {
return core.IndexDockerIO
}
defer client.Close()
defer apiClient.Close()
info, err := client.Info(ctx)
info, err := apiClient.Info(ctx, client.InfoOptions{})
if err != nil {
return core.IndexDockerIO
}
return info.IndexServerAddress
return info.Info.IndexServerAddress
}
// authConfigResult is a result looking up auth details for key.
@@ -205,7 +206,6 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {
ac := registry.AuthConfig{
Auth: v.Auth,
Email: v.Email,
IdentityToken: v.IdentityToken,
Password: v.Password,
RegistryToken: v.RegistryToken,
+25 -25
View File
@@ -6,11 +6,7 @@ import (
"strings"
"sync"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
"github.com/moby/moby/client"
"github.com/testcontainers/testcontainers-go/internal"
"github.com/testcontainers/testcontainers-go/internal/core"
@@ -25,7 +21,7 @@ type DockerClient struct {
var (
// dockerInfo stores the docker info to be reused in the Info method
dockerInfo system.Info
dockerInfo client.SystemInfoResult
dockerInfoSet bool
dockerInfoLock sync.Mutex
)
@@ -34,25 +30,25 @@ var (
var _ client.SystemAPIClient = &DockerClient{}
// Events returns a channel to listen to events that happen to the docker daemon.
func (c *DockerClient) Events(ctx context.Context, options events.ListOptions) (<-chan events.Message, <-chan error) {
func (c *DockerClient) Events(ctx context.Context, options client.EventsListOptions) client.EventsResult {
return c.Client.Events(ctx, options)
}
// Info returns information about the docker server. The result of Info is cached
// and reused every time Info is called.
// It will also print out the docker server info, and the resolved Docker paths, to the default logger.
func (c *DockerClient) Info(ctx context.Context) (system.Info, error) {
func (c *DockerClient) Info(ctx context.Context, options client.InfoOptions) (client.SystemInfoResult, error) {
dockerInfoLock.Lock()
defer dockerInfoLock.Unlock()
if dockerInfoSet {
return dockerInfo, nil
}
info, err := c.Client.Info(ctx)
res, err := c.Client.Info(ctx, options)
if err != nil {
return info, fmt.Errorf("failed to retrieve docker info: %w", err)
return res, fmt.Errorf("failed to retrieve docker info: %w", err)
}
dockerInfo = info
dockerInfo = res
dockerInfoSet = true
infoMessage := `%v - Connected to docker:
@@ -67,23 +63,27 @@ func (c *DockerClient) Info(ctx context.Context) (system.Info, error) {
Test ProcessID: %s
`
infoLabels := ""
if len(dockerInfo.Labels) > 0 {
if len(dockerInfo.Info.Labels) > 0 {
infoLabels = `
Labels:`
var infoLabelsSb72 strings.Builder
for _, lb := range dockerInfo.Labels {
for _, lb := range dockerInfo.Info.Labels {
infoLabelsSb72.WriteString("\n " + lb)
}
infoLabels += infoLabelsSb72.String()
}
host, err := core.ExtractDockerHost(ctx)
if err != nil {
return dockerInfo, err
}
log.Printf(infoMessage, packagePath,
dockerInfo.ServerVersion,
dockerInfo.Info.ServerVersion,
c.ClientVersion(),
dockerInfo.OperatingSystem, dockerInfo.MemTotal/1024/1024,
dockerInfo.Info.OperatingSystem, dockerInfo.Info.MemTotal/1024/1024,
infoLabels,
internal.Version,
core.MustExtractDockerHost(ctx),
host,
core.MustExtractDockerSocket(ctx),
core.SessionID(),
core.ProcessID(),
@@ -93,18 +93,18 @@ func (c *DockerClient) Info(ctx context.Context) (system.Info, error) {
}
// RegistryLogin logs into a Docker registry.
func (c *DockerClient) RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) {
return c.Client.RegistryLogin(ctx, auth)
func (c *DockerClient) RegistryLogin(ctx context.Context, options client.RegistryLoginOptions) (client.RegistryLoginResult, error) {
return c.Client.RegistryLogin(ctx, options)
}
// DiskUsage returns the disk usage of all images.
func (c *DockerClient) DiskUsage(ctx context.Context, options types.DiskUsageOptions) (types.DiskUsage, error) {
func (c *DockerClient) DiskUsage(ctx context.Context, options client.DiskUsageOptions) (client.DiskUsageResult, error) {
return c.Client.DiskUsage(ctx, options)
}
// Ping pings the docker server.
func (c *DockerClient) Ping(ctx context.Context) (types.Ping, error) {
return c.Client.Ping(ctx)
func (c *DockerClient) Ping(ctx context.Context, options client.PingOptions) (client.PingResult, error) {
return c.Client.Ping(ctx, options)
}
// Deprecated: Use NewDockerClientWithOpts instead.
@@ -127,18 +127,18 @@ func NewDockerClientWithOpts(ctx context.Context, opt ...client.Opt) (*DockerCli
Client: dockerClient,
}
if _, err = tcClient.Info(ctx); err != nil {
if _, err = tcClient.Info(ctx, client.InfoOptions{}); err != nil {
// Fallback to environment, including the original options
if len(opt) == 0 {
opt = []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()}
opt = []client.Opt{client.FromEnv}
}
dockerClient, err := client.NewClientWithOpts(opt...)
apiClient, err := client.New(opt...)
if err != nil {
return nil, err
}
tcClient.Client = dockerClient
tcClient.Client = apiClient
}
defer tcClient.Close()
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"errors"
"path/filepath"
"github.com/docker/docker/api/types/mount"
"github.com/moby/moby/api/types/mount"
"github.com/testcontainers/testcontainers-go/log"
)
+4 -4
View File
@@ -6,13 +6,13 @@ import (
"io"
"sync"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/client"
)
// ProcessOptions defines options applicable to the reader processor
type ProcessOptions struct {
ExecConfig container.ExecOptions
ExecConfig client.ExecCreateOptions
Reader io.Reader
}
@@ -23,7 +23,7 @@ type ProcessOptions struct {
// - attach stderr: true
func NewProcessOptions(cmd []string) *ProcessOptions {
return &ProcessOptions{
ExecConfig: container.ExecOptions{
ExecConfig: client.ExecCreateOptions{
Cmd: cmd,
AttachStdout: true,
AttachStderr: true,
+1
View File
@@ -1,3 +1,4 @@
package testcontainers
//go:generate mockery
//go:generate gci write -s standard -s default -s prefix(github.com/testcontainers) .
+1 -1
View File
@@ -3,7 +3,7 @@ package testcontainers
import (
"context"
"github.com/docker/docker/client"
"github.com/moby/moby/client"
)
// ImageInfo represents summary information of an image
@@ -4,7 +4,7 @@ import (
"context"
"path/filepath"
"github.com/docker/docker/client"
"github.com/moby/moby/client"
"github.com/testcontainers/testcontainers-go/internal"
"github.com/testcontainers/testcontainers-go/internal/config"
@@ -12,11 +12,14 @@ import (
// NewClient returns a new docker client extracting the docker host from the different alternatives
func NewClient(ctx context.Context, ops ...client.Opt) (*client.Client, error) {
dockerHost, err := ExtractDockerHost(ctx)
if err != nil {
return nil, err
}
tcConfig := config.Read()
dockerHost := MustExtractDockerHost(ctx)
opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()}
opts := []client.Opt{client.FromEnv}
if dockerHost != "" {
opts = append(opts, client.WithHost(dockerHost))
@@ -41,7 +44,7 @@ func NewClient(ctx context.Context, ops ...client.Opt) (*client.Client, error) {
// passed options have priority over the default ones
opts = append(opts, ops...)
cli, err := client.NewClientWithOpts(opts...)
cli, err := client.New(opts...)
if err != nil {
return nil, err
}
@@ -9,7 +9,7 @@ import (
"strings"
"sync"
"github.com/docker/docker/client"
"github.com/moby/moby/client"
"github.com/testcontainers/testcontainers-go/internal/config"
)
@@ -31,8 +31,9 @@ var (
)
var (
dockerHostCache string
dockerHostOnce sync.Once
dockerHostCache string
dockerHostErrCache error
dockerHostOnce sync.Once
)
var (
@@ -59,13 +60,13 @@ func DefaultGatewayIP() (string, error) {
// dockerHostCheck Use a vanilla Docker client to check if the Docker host is reachable.
// It will avoid recursive calls to this function.
var dockerHostCheck = func(ctx context.Context, host string) error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHost(host), client.WithAPIVersionNegotiation())
cli, err := client.New(client.FromEnv, client.WithHost(host))
if err != nil {
return fmt.Errorf("new client: %w", err)
}
defer cli.Close()
_, err = cli.Info(ctx)
_, err = cli.Info(ctx, client.InfoOptions{})
if err != nil {
return fmt.Errorf("docker info: %w", err)
}
@@ -85,16 +86,18 @@ var dockerHostCheck = func(ctx context.Context, host string) error {
// 6. Rootless docker socket path.
// 7. Else, because the Docker host is not set, it panics.
func MustExtractDockerHost(ctx context.Context) string {
host, err := ExtractDockerHost(ctx)
if err != nil {
panic(err)
}
return host
}
func ExtractDockerHost(ctx context.Context) (string, error) {
dockerHostOnce.Do(func() {
cache, err := extractDockerHost(ctx)
if err != nil {
panic(err)
}
dockerHostCache = cache
dockerHostCache, dockerHostErrCache = extractDockerHost(ctx)
})
return dockerHostCache
return dockerHostCache, dockerHostErrCache
}
// MustExtractDockerSocket Extracts the docker socket from the different alternatives, removing the socket schema and
@@ -198,13 +201,13 @@ func extractDockerSocketFromClient(ctx context.Context, cli client.APIClient) st
return checkDockerSocketFn(testcontainersDockerSocket)
}
info, err := cli.Info(ctx)
info, err := cli.Info(ctx, client.InfoOptions{})
if err != nil {
panic(err) // Docker Info is required to get the Operating System
}
// Because Docker Desktop runs in a VM, we need to use the default docker path for rootless docker
if info.OperatingSystem == "Docker Desktop" {
if info.Info.OperatingSystem == "Docker Desktop" {
if IsWindows() {
return WindowsDockerSocketPath
}
@@ -4,7 +4,7 @@ import (
"net/url"
"strings"
"github.com/docker/docker/client"
"github.com/moby/moby/client"
)
// DockerSocketSchema is the unix schema.
@@ -62,11 +62,11 @@ func MergeCustomLabels(dst, src map[string]string) error {
if dst == nil {
return errors.New("destination map is nil")
}
for key, value := range src {
for key := range src {
if strings.HasPrefix(key, LabelBase) {
return fmt.Errorf("key %q has %q prefix", key, LabelBase)
}
dst[key] = value
}
maps.Copy(dst, src)
return nil
}
@@ -4,8 +4,8 @@ import (
"context"
"fmt"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/testcontainers/testcontainers-go/internal/core"
)
@@ -19,17 +19,17 @@ const (
)
// Get returns a network by its ID.
func Get(ctx context.Context, id string) (network.Inspect, error) {
func Get(ctx context.Context, id string) (network.Summary, error) {
return get(ctx, FilterByID, id)
}
// GetByName returns a network by its name.
func GetByName(ctx context.Context, name string) (network.Inspect, error) {
func GetByName(ctx context.Context, name string) (network.Summary, error) {
return get(ctx, FilterByName, name)
}
func get(ctx context.Context, filter string, value string) (network.Inspect, error) {
var nw network.Inspect // initialize to the zero value
func get(ctx context.Context, filter string, value string) (network.Summary, error) {
var nw network.Summary // initialize to the zero value
cli, err := core.NewClient(ctx)
if err != nil {
@@ -37,16 +37,16 @@ func get(ctx context.Context, filter string, value string) (network.Inspect, err
}
defer cli.Close()
list, err := cli.NetworkList(ctx, network.ListOptions{
Filters: filters.NewArgs(filters.Arg(filter, value)),
list, err := cli.NetworkList(ctx, client.NetworkListOptions{
Filters: make(client.Filters).Add(filter, value),
})
if err != nil {
return nw, fmt.Errorf("failed to list networks: %w", err)
}
if len(list) == 0 {
if len(list.Items) == 0 {
return nw, fmt.Errorf("network %s not found (filtering by %s)", value, filter)
}
return list[0], nil
return list.Items[0], nil
}
+1 -1
View File
@@ -1,4 +1,4 @@
package internal
// Version is the next development version of the application
const Version = "0.41.0"
const Version = "0.42.0"
+57 -36
View File
@@ -9,9 +9,8 @@ import (
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/testcontainers/testcontainers-go/log"
)
@@ -233,7 +232,7 @@ var defaultReadinessHook = func() ContainerLifecycleHooks {
}
}
dockerContainer.isRunning = true
dockerContainer.isRunning.Store(true)
return nil
},
@@ -522,32 +521,29 @@ func (p *DockerProvider) preCreateContainerHook(ctx context.Context, req Contain
networkingConfig.EndpointsConfig = endpointSettings
// Expose ports automatically if the container request exposes zero ports and the container
// does not run in a container network. The NetworkMode check must be done after the pre-creation
// Modifiers are called, so the network mode is already set.
exposedPorts := req.ExposedPorts
// this check must be done after the pre-creation Modifiers are called, so the network mode is already set
if len(exposedPorts) == 0 && !hostConfig.NetworkMode.IsContainer() {
image, err := p.client.ImageInspect(ctx, dockerInput.Image)
if err != nil {
return err
}
for p := range image.Config.ExposedPorts {
exposedPorts = append(exposedPorts, string(p))
exposedPorts = exposedPorts[:0]
for port := range image.Config.ExposedPorts {
exposedPorts = append(exposedPorts, port)
}
}
exposedPortSet, exposedPortMap, err := nat.ParsePortSpecs(exposedPorts)
exposedPortSet, err := parseExposedPorts(exposedPorts)
if err != nil {
return err
}
dockerInput.ExposedPorts = exposedPortSet
// only exposing those ports automatically if the container request exposes zero ports and the container does not run in a container network
if len(exposedPorts) == 0 && !hostConfig.NetworkMode.IsContainer() {
hostConfig.PortBindings = exposedPortMap
} else {
hostConfig.PortBindings = mergePortBindings(hostConfig.PortBindings, exposedPortMap, req.ExposedPorts)
}
hostConfig.PortBindings = mergePortBindings(hostConfig.PortBindings, exposedPortSet)
return nil
}
@@ -597,32 +593,57 @@ func combineContainerHooks(defaultHooks, userDefinedHooks []ContainerLifecycleHo
return hooks
}
func mergePortBindings(configPortMap, exposedPortMap nat.PortMap, exposedPorts []string) nat.PortMap {
if exposedPortMap == nil {
exposedPortMap = make(map[nat.Port][]nat.PortBinding)
}
func parseExposedPorts(specs []string) (network.PortSet, error) {
exposed := make(network.PortSet, len(specs))
for _, s := range specs {
pr, err := network.ParsePortRange(s)
if err != nil {
return nil, fmt.Errorf("invalid exposed port %q: %w", s, err)
}
mappedPorts := make(map[string]struct{}, len(exposedPorts))
for _, p := range exposedPorts {
p = strings.Split(p, "/")[0]
mappedPorts[p] = struct{}{}
}
for k, v := range configPortMap {
if _, ok := mappedPorts[k.Port()]; ok {
exposedPortMap[k] = v
for p := range pr.All() {
exposed[p] = struct{}{}
}
}
return exposed, nil
}
// Fix: Ensure that ports with empty HostPort get "0" for automatic allocation
// This fixes the UDP port binding issue where ports were getting HostPort:0 instead of being allocated
for k, v := range exposedPortMap {
for i := range v {
if v[i].HostPort == "" {
v[i].HostPort = "0" // Tell Docker to allocate a random port
// mergePortBindings returns a PortMap for the given exposedPortSet.
//
// For each port in exposedPortSet, a binding is ensured:
// - If configPortMap contains bindings for that port, those bindings are used.
// - Otherwise, a default binding with HostPort "0" (ephemeral allocation)
// is assigned.
//
// Bindings for ports not present in exposedPortSet are not preserved.
// Any binding with an empty HostPort is normalized to "0".
//
// TODO(thaJeztah): this logic seems the reverse of the docker CLI, which
// exposes ports if the user requests a port-mapping (i.e., if a port-mapping
// is requested, but not exposed, we map the port *and* add an entry to
// ExposedPorts). The logic here is the reverse; any port "mapped" in
// HostConfig.PortBindings is dropped if is not exposed.
func mergePortBindings(configPortMap network.PortMap, exposedPortSet network.PortSet) network.PortMap {
if len(exposedPortSet) == 0 {
return network.PortMap{}
}
exposedPortMap := make(network.PortMap, len(exposedPortSet))
for p := range exposedPortSet {
bindings := configPortMap[p]
if len(bindings) == 0 {
exposedPortMap[p] = []network.PortBinding{{HostPort: "0"}}
continue
}
// Fix: Ensure that ports with empty HostPort get "0" for automatic allocation
// This fixes the UDP port binding issue where ports were getting HostPort:0 instead of being allocated
for i := range bindings {
if bindings[i].HostPort == "" {
bindings[i].HostPort = "0" // Tell Docker to allocate a random port
}
}
exposedPortMap[k] = v
exposedPortMap[p] = bindings
}
return exposedPortMap
+1 -1
View File
@@ -162,4 +162,4 @@ nav:
- Getting help: getting_help.md
edit_uri: edit/main/docs/
extra:
latest_version: v0.41.0
latest_version: v0.42.0
@@ -7,8 +7,8 @@ import (
"io"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/container"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
+1 -1
View File
@@ -3,7 +3,7 @@ package testcontainers
import (
"context"
"github.com/docker/docker/api/types/network"
"github.com/moby/moby/api/types/network"
"github.com/testcontainers/testcontainers-go/internal/core"
)
+2 -2
View File
@@ -9,8 +9,8 @@ import (
"time"
"dario.cat/mergo"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
tcexec "github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/internal/core"
+7 -9
View File
@@ -10,8 +10,8 @@ import (
"sync"
"time"
"github.com/docker/docker/api/types/container"
"github.com/google/uuid"
"github.com/moby/moby/api/types/container"
"golang.org/x/crypto/ssh"
"github.com/testcontainers/testcontainers-go/internal/core/network"
@@ -107,14 +107,12 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) (
return sshdConnectHook, fmt.Errorf("inspect sshd container: %w", err)
}
// TODO: remove once we have docker context support via #2810
//nolint:staticcheck // SA1019: IPAddress is deprecated, but we need it for compatibility until v29
sshdIP := inspect.NetworkSettings.IPAddress
if sshdIP == "" {
single := len(inspect.NetworkSettings.Networks) == 1
for name, network := range inspect.NetworkSettings.Networks {
if name == sshdFirstNetwork || single {
sshdIP = network.IPAddress
var sshdIP string
single := len(inspect.NetworkSettings.Networks) == 1
for name, nw := range inspect.NetworkSettings.Networks {
if name == sshdFirstNetwork || single {
if nw.IPAddress.IsValid() {
sshdIP = nw.IPAddress.String()
break
}
}
+5 -2
View File
@@ -141,15 +141,18 @@ func NewDockerProvider(provOpts ...DockerProviderOption) (*DockerProvider, error
}
ctx := context.Background()
host, err := core.ExtractDockerHost(ctx)
if err != nil {
return nil, err
}
c, err := NewDockerClientWithOpts(ctx)
if err != nil {
return nil, err
}
return &DockerProvider{
DockerProviderOptions: o,
host: core.MustExtractDockerHost(ctx),
client: c,
host: host,
config: config.Read(),
}, nil
}
+23 -28
View File
@@ -15,10 +15,9 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/containerd/errdefs"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/testcontainers/testcontainers-go/internal/config"
"github.com/testcontainers/testcontainers-go/internal/core"
@@ -41,7 +40,7 @@ var (
// defaultReaperPort is the default port that the reaper listens on if not
// overridden by the RYUK_PORT environment variable.
defaultReaperPort = nat.Port("8080/tcp")
defaultReaperPort = network.MustParsePort("8080/tcp")
// errReaperNotFound is returned when no reaper container is found.
errReaperNotFound = errors.New("reaper not found")
@@ -95,9 +94,9 @@ type reaperSpawner struct {
}
// port returns the port that a new reaper should listen on.
func (r *reaperSpawner) port() nat.Port {
func (r *reaperSpawner) port() network.Port {
if port := os.Getenv("RYUK_PORT"); port != "" {
natPort, err := nat.NewPort("tcp", port)
natPort, err := network.ParsePort(port + "/tcp")
if err != nil {
panic(fmt.Sprintf("invalid RYUK_PORT value %q: %s", port, err))
}
@@ -170,14 +169,13 @@ func (r *reaperSpawner) lookupContainer(ctx context.Context, sessionID string) (
provider.SetClient(dockerClient)
opts := container.ListOptions{
opts := client.ContainerListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", fmt.Sprintf("%s=%s", core.LabelSessionID, sessionID)),
filters.Arg("label", fmt.Sprintf("%s=%t", core.LabelReaper, true)),
filters.Arg("label", fmt.Sprintf("%s=%t", core.LabelRyuk, true)),
filters.Arg("name", reaperContainerNameFromSessionID(sessionID)),
),
Filters: make(client.Filters).
Add("label", fmt.Sprintf("%s=%s", core.LabelSessionID, sessionID)).
Add("label", fmt.Sprintf("%s=%t", core.LabelReaper, true)).
Add("label", fmt.Sprintf("%s=%t", core.LabelRyuk, true)).
Add("name", reaperContainerNameFromSessionID(sessionID)),
}
return backoff.RetryWithData(
@@ -187,29 +185,26 @@ func (r *reaperSpawner) lookupContainer(ctx context.Context, sessionID string) (
return nil, fmt.Errorf("container list: %w", err)
}
if len(resp) == 0 {
if len(resp.Items) == 0 {
// No reaper container not found.
return nil, backoff.Permanent(errReaperNotFound)
}
if len(resp) > 1 {
return nil, fmt.Errorf("found %d reaper containers for session ID %q", len(resp), sessionID)
if len(resp.Items) > 1 {
return nil, fmt.Errorf("found %d reaper containers for session ID %q", len(resp.Items), sessionID)
}
r, err := provider.ContainerFromType(ctx, resp[0])
r, err := provider.ContainerFromType(ctx, resp.Items[0])
if err != nil {
return nil, fmt.Errorf("from docker: %w", err)
}
switch {
case r.healthStatus == types.Healthy,
r.healthStatus == types.NoHealthcheck:
switch r.healthStatus {
case "", container.Healthy, container.NoHealthcheck:
return r, nil
case r.healthStatus != "":
default:
return nil, fmt.Errorf("container not healthy: %s", r.healthStatus)
}
return r, nil
},
backoff.WithContext(r.backoff(), ctx),
)
@@ -225,7 +220,7 @@ func (r *reaperSpawner) isRunning(ctx context.Context, ctr Container) error {
if !state.Running {
// Use NotFound error to indicate the container is not running
// and should be recreated.
return errdefs.ErrNotFound.WithMessage("container state: " + state.Status)
return errdefs.ErrNotFound.WithMessage("container state: " + string(state.Status))
}
return nil
@@ -381,9 +376,9 @@ func (r *reaperSpawner) newReaper(ctx context.Context, sessionID string, provide
tcConfig := provider.Config().Config
req := ContainerRequest{
Image: config.ReaperDefaultImage,
ExposedPorts: []string{string(port)},
ExposedPorts: []string{port.String()},
Labels: core.DefaultLabels(sessionID),
WaitingFor: wait.ForListeningPort(port),
WaitingFor: wait.ForListeningPort(port.String()),
Name: reaperContainerNameFromSessionID(sessionID),
HostConfigModifier: func(hc *container.HostConfig) {
hc.AutoRemove = true
@@ -428,7 +423,7 @@ func (r *reaperSpawner) newReaper(ctx context.Context, sessionID string, provide
return nil, fmt.Errorf("run container: %w", err)
}
endpoint, err := c.PortEndpoint(ctx, port, "")
endpoint, err := c.PortEndpoint(ctx, port.String(), "")
if err != nil {
return nil, fmt.Errorf("port endpoint: %w", err)
}
+17 -5
View File
@@ -4,10 +4,13 @@ import (
"context"
"fmt"
"io"
"os"
"regexp"
"strings"
"testing"
"github.com/containerd/errdefs"
"github.com/moby/moby/client"
"github.com/stretchr/testify/require"
)
@@ -39,16 +42,25 @@ func SkipIfProviderIsNotHealthy(t *testing.T) {
}
// SkipIfDockerDesktop is a utility function capable of skipping tests
// if tests are run using Docker Desktop.
// if tests are run using Docker Desktop or another VM-based Docker
// environment (e.g. colima) where host network access is not available.
func SkipIfDockerDesktop(t *testing.T, ctx context.Context) {
t.Helper()
// Colima runs Docker inside a Linux VM, so host networking doesn't work
// the same way as native Docker on Linux. Detect it via DOCKER_HOST which
// typically contains the colima socket path.
if strings.Contains(os.Getenv("DOCKER_HOST"), "colima") {
t.Skip("Skipping test that requires host network access when running in colima")
}
cli, err := NewDockerClientWithOpts(ctx)
require.NoErrorf(t, err, "failed to create docker client: %s", err)
info, err := cli.Info(ctx)
res, err := cli.Info(ctx, client.InfoOptions{})
require.NoErrorf(t, err, "failed to get docker info: %s", err)
if info.OperatingSystem == "Docker Desktop" {
if res.Info.OperatingSystem == "Docker Desktop" {
t.Skip("Skipping test that requires host network access when running in Docker Desktop")
}
}
@@ -60,10 +72,10 @@ func SkipIfNotDockerDesktop(t *testing.T, ctx context.Context) {
cli, err := NewDockerClientWithOpts(ctx)
require.NoErrorf(t, err, "failed to create docker client: %s", err)
info, err := cli.Info(ctx)
res, err := cli.Info(ctx, client.InfoOptions{})
require.NoErrorf(t, err, "failed to get docker info: %s", err)
if info.OperatingSystem != "Docker Desktop" {
if res.Info.OperatingSystem != "Docker Desktop" {
t.Skip("Skipping test that needs Docker Desktop")
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/docker/docker/api/types"
"github.com/moby/moby/api/types/container"
)
// Implement interface
@@ -87,7 +87,7 @@ func (ws *HealthStrategy) WaitUntilReady(ctx context.Context, target StrategyTar
if err := checkState(state); err != nil {
return err
}
if state.Health == nil || state.Health.Status != types.Healthy {
if state.Health == nil || state.Health.Status != container.Healthy {
time.Sleep(ws.PollInterval)
continue
}
+29 -21
View File
@@ -8,7 +8,7 @@ import (
"os"
"time"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/network"
"github.com/testcontainers/testcontainers-go/log"
)
@@ -32,7 +32,7 @@ var (
type HostPortStrategy struct {
// Port is a string containing port number and protocol in the format "80/tcp"
// which
Port nat.Port
Port string
// all WaitStrategies should have a startupTimeout to avoid waiting infinitely
timeout *time.Duration
PollInterval time.Duration
@@ -50,7 +50,7 @@ type HostPortStrategy struct {
// NewHostPortStrategy constructs a default host port strategy that waits for the given
// port to be exposed. The default startup timeout is 60 seconds.
func NewHostPortStrategy(port nat.Port) *HostPortStrategy {
func NewHostPortStrategy(port string) *HostPortStrategy {
return &HostPortStrategy{
Port: port,
PollInterval: defaultPollInterval(),
@@ -64,7 +64,7 @@ func NewHostPortStrategy(port nat.Port) *HostPortStrategy {
// ForListeningPort returns a host port strategy that waits for the given port
// to be exposed and bound internally the container.
// Alias for `NewHostPortStrategy(port)`.
func ForListeningPort(port nat.Port) *HostPortStrategy {
func ForListeningPort(port string) *HostPortStrategy {
return NewHostPortStrategy(port)
}
@@ -76,7 +76,7 @@ func ForExposedPort() *HostPortStrategy {
// ForMappedPort returns a host port strategy that waits for the given port
// to be mapped without accessing the port itself.
func ForMappedPort(port nat.Port) *HostPortStrategy {
func ForMappedPort(port string) *HostPortStrategy {
return NewHostPortStrategy(port).SkipInternalCheck().SkipExternalCheck()
}
@@ -118,7 +118,7 @@ func (hp *HostPortStrategy) Timeout() *time.Duration {
func (hp *HostPortStrategy) String() string {
port := "first exposed port"
if hp.Port != "" {
port = fmt.Sprintf("port %s", hp.Port)
port = "port " + hp.Port
}
var checks string
@@ -139,15 +139,15 @@ func (hp *HostPortStrategy) String() string {
// detectInternalPort returns the lowest internal port that is currently bound.
// If no internal port is found, it returns the zero nat.Port value which
// can be checked against an empty string.
func (hp *HostPortStrategy) detectInternalPort(ctx context.Context, target StrategyTarget) (nat.Port, error) {
var internalPort nat.Port
func (hp *HostPortStrategy) detectInternalPort(ctx context.Context, target StrategyTarget) (network.Port, error) {
var internalPort network.Port
inspect, err := target.Inspect(ctx)
if err != nil {
return internalPort, fmt.Errorf("inspect: %w", err)
}
for port := range inspect.NetworkSettings.Ports {
if internalPort == "" || port.Int() < internalPort.Int() {
if internalPort.IsZero() || port.Num() < internalPort.Num() {
internalPort = port
}
}
@@ -167,9 +167,17 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
waitInterval := hp.PollInterval
internalPort := hp.Port
var internalPort network.Port
if hp.Port != "" {
p, err := network.ParsePort(hp.Port)
if err != nil {
return err
}
internalPort = p
}
i := 0
if internalPort == "" {
if internalPort.IsZero() {
var err error
// Port is not specified, so we need to detect it.
internalPort, err = hp.detectInternalPort(ctx, target)
@@ -177,7 +185,7 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
return fmt.Errorf("detect internal port: %w", err)
}
for internalPort == "" {
for internalPort.IsZero() {
select {
case <-ctx.Done():
return fmt.Errorf("detect internal port: retries: %d, last err: %w, ctx err: %w", i, err, ctx.Err())
@@ -194,10 +202,10 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
}
}
port, err := target.MappedPort(ctx, internalPort)
port, err := target.MappedPort(ctx, internalPort.String())
i = 0
for port == "" {
for port.IsZero() {
i++
select {
@@ -207,7 +215,7 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
if err := checkTarget(ctx, target); err != nil {
return fmt.Errorf("mapped port: check target: retries: %d, port: %q, last err: %w", i, port, err)
}
port, err = target.MappedPort(ctx, internalPort)
port, err = target.MappedPort(ctx, internalPort.String())
if err != nil {
log.Printf("mapped port: retries: %d, port: %q, err: %s\n", i, port, err)
}
@@ -245,7 +253,7 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
return nil
}
func externalCheck(ctx context.Context, ipAddress string, port nat.Port, target StrategyTarget, waitInterval time.Duration) error {
func externalCheck(ctx context.Context, ipAddress string, port network.Port, target StrategyTarget, waitInterval time.Duration) error {
proto := port.Proto()
dialer := net.Dialer{}
@@ -254,7 +262,7 @@ func externalCheck(ctx context.Context, ipAddress string, port nat.Port, target
if err := checkTarget(ctx, target); err != nil {
return fmt.Errorf("check target: retries: %d address: %s: %w", i, address, err)
}
conn, err := dialer.DialContext(ctx, proto, address)
conn, err := dialer.DialContext(ctx, string(proto), address)
if err != nil {
var v *net.OpError
if errors.As(err, &v) {
@@ -269,13 +277,13 @@ func externalCheck(ctx context.Context, ipAddress string, port nat.Port, target
return fmt.Errorf("dial: %w", err)
}
conn.Close()
_ = conn.Close()
return nil
}
}
func internalCheck(ctx context.Context, internalPort nat.Port, target StrategyTarget) error {
command := buildInternalCheckCommand(internalPort.Int())
func internalCheck(ctx context.Context, internalPort network.Port, target StrategyTarget) error {
command := buildInternalCheckCommand(internalPort.Num())
for {
if ctx.Err() != nil {
return ctx.Err()
@@ -302,7 +310,7 @@ func internalCheck(ctx context.Context, internalPort nat.Port, target StrategyTa
}
}
func buildInternalCheckCommand(internalPort int) string {
func buildInternalCheckCommand(internalPort uint16) string {
command := `(
cat /proc/net/tcp* | awk '{print $2}' | grep -i :%04x ||
nc -vz -w 1 localhost %d ||
+22 -18
View File
@@ -14,7 +14,7 @@ import (
"strings"
"time"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/network"
)
// Implement interface
@@ -28,7 +28,7 @@ type HTTPStrategy struct {
timeout *time.Duration
// additional properties
Port nat.Port
Port network.Port
Path string
StatusCodeMatcher func(status int) bool
ResponseMatcher func(body io.Reader) bool
@@ -44,10 +44,10 @@ type HTTPStrategy struct {
ForceIPv4LocalHost bool
}
// NewHTTPStrategy constructs a HTTP strategy waiting on port 80 and status code 200
// NewHTTPStrategy constructs an HTTP strategy waiting on port 80 and status code 200
func NewHTTPStrategy(path string) *HTTPStrategy {
return &HTTPStrategy{
Port: "",
Port: network.Port{},
Path: path,
StatusCodeMatcher: defaultStatusCodeMatcher,
ResponseMatcher: func(_ io.Reader) bool { return true },
@@ -78,8 +78,10 @@ func (ws *HTTPStrategy) WithStartupTimeout(timeout time.Duration) *HTTPStrategy
// WithPort set the port to wait for.
// Default is the lowest numbered port.
func (ws *HTTPStrategy) WithPort(port nat.Port) *HTTPStrategy {
ws.Port = port
func (ws *HTTPStrategy) WithPort(port string) *HTTPStrategy {
if p, err := network.ParsePort(port); err == nil {
ws.Port = p
}
return ws
}
@@ -162,7 +164,7 @@ func (ws *HTTPStrategy) String() string {
}
port := "default"
if ws.Port != "" {
if !ws.Port.IsZero() {
port = ws.Port.Port()
}
@@ -188,8 +190,9 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
ipAddress = strings.Replace(ipAddress, "localhost", "127.0.0.1", 1)
}
var mappedPort nat.Port
if ws.Port == "" {
var mappedPort network.Port
if ws.Port.IsZero() {
// No specific port requested; inspect container to find lowest exposed TCP port.
// We wait one polling interval before we grab the ports
// otherwise they might not be bound yet on startup.
select {
@@ -209,28 +212,29 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
}
// Find the lowest numbered exposed tcp port.
var lowestPort nat.Port
var lowestPort network.Port
var hostPort string
for port, bindings := range inspect.NetworkSettings.Ports {
if len(bindings) == 0 || port.Proto() != "tcp" {
continue
}
if lowestPort == "" || port.Int() < lowestPort.Int() {
if lowestPort.IsZero() || port.Num() < lowestPort.Num() {
lowestPort = port
hostPort = bindings[0].HostPort
}
}
if lowestPort == "" {
if lowestPort.IsZero() {
return errors.New("no exposed tcp ports or mapped ports - cannot wait for status")
}
mappedPort, _ = nat.NewPort(lowestPort.Proto(), hostPort)
hPort, _ := strconv.ParseUint(hostPort, 10, 16)
mappedPort, _ = network.PortFrom(uint16(hPort), lowestPort.Proto())
} else {
mappedPort, err = target.MappedPort(ctx, ws.Port)
for mappedPort == "" {
// Specific port requested; use MappedPort to resolve it.
mappedPort, err = target.MappedPort(ctx, ws.Port.String())
for mappedPort.IsZero() {
select {
case <-ctx.Done():
return fmt.Errorf("%w: %w", ctx.Err(), err)
@@ -239,7 +243,7 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
return err
}
mappedPort, err = target.MappedPort(ctx, ws.Port)
mappedPort, err = target.MappedPort(ctx, ws.Port.String())
}
}
@@ -289,7 +293,7 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
}
client := http.Client{Transport: tripper, Timeout: time.Second}
address := net.JoinHostPort(ipAddress, strconv.Itoa(mappedPort.Int()))
address := net.JoinHostPort(ipAddress, mappedPort.Port())
endpoint, err := url.Parse(ws.Path)
if err != nil {
+8 -5
View File
@@ -5,8 +5,8 @@ import (
"io"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/testcontainers/testcontainers-go/exec"
)
@@ -61,12 +61,15 @@ func (st NopStrategyTarget) Inspect(_ context.Context) (*container.InspectRespon
}
// Deprecated: use Inspect instead
func (st NopStrategyTarget) Ports(_ context.Context) (nat.PortMap, error) {
func (st NopStrategyTarget) Ports(_ context.Context) (network.PortMap, error) {
return nil, nil
}
func (st NopStrategyTarget) MappedPort(_ context.Context, n nat.Port) (nat.Port, error) {
return n, nil
func (st NopStrategyTarget) MappedPort(_ context.Context, n string) (network.Port, error) {
if n == "" {
return network.Port{}, nil
}
return network.ParsePort(n)
}
func (st NopStrategyTarget) Logs(_ context.Context) (io.ReadCloser, error) {
+11 -8
View File
@@ -6,7 +6,7 @@ import (
"fmt"
"time"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/network"
)
var (
@@ -17,7 +17,7 @@ var (
const defaultForSQLQuery = "SELECT 1"
// ForSQL constructs a new waitForSql strategy for the given driver
func ForSQL(port nat.Port, driver string, url func(host string, port nat.Port) string) *waitForSQL {
func ForSQL(port string, driver string, url func(host string, port string) string) *waitForSQL {
return &waitForSQL{
Port: port,
URL: url,
@@ -31,9 +31,9 @@ func ForSQL(port nat.Port, driver string, url func(host string, port nat.Port) s
type waitForSQL struct {
timeout *time.Duration
URL func(host string, port nat.Port) string
URL func(host string, port string) string
Driver string
Port nat.Port
Port string
startupTimeout time.Duration
PollInterval time.Duration
query string
@@ -65,7 +65,10 @@ func (w *waitForSQL) Timeout() *time.Duration {
func (w *waitForSQL) String() string {
port := "default"
if w.Port != "" {
port = w.Port.Port()
p, err := network.ParsePort(w.Port)
if err == nil {
port = p.Port()
}
}
query := ""
@@ -96,10 +99,10 @@ func (w *waitForSQL) WaitUntilReady(ctx context.Context, target StrategyTarget)
ticker := time.NewTicker(w.PollInterval)
defer ticker.Stop()
var port nat.Port
var port network.Port
port, err = target.MappedPort(ctx, w.Port)
for port == "" {
for port.IsZero() {
select {
case <-ctx.Done():
return fmt.Errorf("%w: %w", ctx.Err(), err)
@@ -111,7 +114,7 @@ func (w *waitForSQL) WaitUntilReady(ctx context.Context, target StrategyTarget)
}
}
db, err := sql.Open(w.Driver, w.URL(host, port))
db, err := sql.Open(w.Driver, w.URL(host, port.String()))
if err != nil {
return fmt.Errorf("sql.Open: %w", err)
}
+5 -5
View File
@@ -7,8 +7,8 @@ import (
"io"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/testcontainers/testcontainers-go/exec"
)
@@ -26,8 +26,8 @@ type StrategyTimeout interface {
type StrategyTarget interface {
Host(context.Context) (string, error)
Inspect(context.Context) (*container.InspectResponse, error)
Ports(ctx context.Context) (nat.PortMap, error) // Deprecated: use Inspect instead
MappedPort(context.Context, nat.Port) (nat.Port, error)
Ports(ctx context.Context) (network.PortMap, error) // Deprecated: use Inspect instead
MappedPort(context.Context, string) (network.Port, error)
Logs(context.Context) (io.ReadCloser, error)
Exec(context.Context, []string, ...exec.ProcessOption) (int, io.Reader, error)
State(context.Context) (*container.State, error)
@@ -49,7 +49,7 @@ func checkState(state *container.State) error {
return nil
case state.OOMKilled:
return errors.New("container crashed with out-of-memory (OOMKilled)")
case state.Status == "exited":
case state.Status == container.StateExited:
return fmt.Errorf("container exited with code %d", state.ExitCode)
default:
return fmt.Errorf("unexpected container status %q", state.Status)