build: upgrade dependencies and build with go 1.21

This commit is contained in:
2023-08-21 23:04:28 +02:00
parent ddc5ee91e5
commit 3942b32843
1201 changed files with 129198 additions and 39613 deletions

View File

@@ -8,3 +8,6 @@ site/
src/mkdocs-codeinclude-plugin
src/pip-delete-this-directory.txt
.idea/
.DS_Store
TEST-*.xml

View File

@@ -0,0 +1,16 @@
linters:
enable:
- gci
- gofmt
- misspell
linters-settings:
gci:
sections:
- standard
- default
- prefix(github.com/testcontainters)
run:
timeout: 5m

View File

@@ -1,25 +0,0 @@
language: go
go:
- stable
- 1.x
- 1.14.x
- 1.15.x
install: true
services:
- docker
env:
- GO111MODULE=on
before_script:
- go get gotest.tools/gotestsum
script:
- go mod verify
- go mod tidy
- scripts/checks.sh
- go vet ./...
- gotestsum --format short-verbose ./...

View File

@@ -0,0 +1,13 @@
# Contributing
Please see the [main contributing guidelines](./docs/contributing.md).
There are additional docs describing [contributing documentation changes](./docs/contributing_docs.md).
### GitHub Sponsorship
Testcontainers is [in the GitHub Sponsors program](https://github.com/sponsors/testcontainers)!
This repository is supported by our sponsors, meaning that issues are eligible to have a 'bounty' attached to them by sponsors.
Please see [the bounty policy page](https://golang.testcontainers.org/bounty) if you are interested, either as a sponsor or as a contributor.

View File

@@ -0,0 +1,9 @@
include ./commons-test.mk
.PHONY: test-all
test-all: tools test-unit
.PHONY: test-examples
test-examples:
@echo "Running example tests..."
make -C examples test

View File

@@ -7,6 +7,7 @@ verify_ssl = true
[packages]
mkdocs = "*"
mkdocs-codeinclude-plugin = "*"
mkdocs-material = "*"
mkdocs-markdownextradata-plugin = "*"

View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "39d6d886946223291b5b9c8ee4769e8c9eec313f1e82a02a21dacc890d507fe3"
"sha256": "d3830267ea93391a077a2ba5b93aa8218c0a38002694b404b56df11498da3e56"
},
"pipfile-spec": 6,
"requires": {
@@ -18,159 +18,288 @@
"default": {
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
"sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd",
"sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"
],
"version": "==7.1.2"
"markers": "python_version >= '3.7'",
"version": "==8.1.6"
},
"ghp-import": {
"hashes": [
"sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619",
"sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"
],
"version": "==2.1.0"
},
"importlib-metadata": {
"hashes": [
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
"sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4",
"sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"
],
"markers": "python_version < '3.8'",
"version": "==1.6.0"
"markers": "python_version >= '3.7'",
"version": "==6.7.0"
},
"jinja2": {
"hashes": [
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
"sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8",
"sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"
],
"version": "==2.11.2"
},
"livereload": {
"hashes": [
"sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b",
"sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"
],
"version": "==2.6.1"
"markers": "python_version >= '3.6'",
"version": "==3.0.3"
},
"markdown": {
"hashes": [
"sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17",
"sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59"
"sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6",
"sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"
],
"version": "==3.2.2"
"markers": "python_version >= '3.7'",
"version": "==3.4.4"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
"sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e",
"sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e",
"sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431",
"sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686",
"sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559",
"sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc",
"sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c",
"sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0",
"sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4",
"sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9",
"sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575",
"sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba",
"sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d",
"sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3",
"sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00",
"sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155",
"sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac",
"sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52",
"sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f",
"sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8",
"sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b",
"sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24",
"sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea",
"sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198",
"sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0",
"sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee",
"sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be",
"sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2",
"sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707",
"sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6",
"sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58",
"sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779",
"sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636",
"sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c",
"sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad",
"sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee",
"sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc",
"sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2",
"sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48",
"sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7",
"sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e",
"sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b",
"sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa",
"sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5",
"sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e",
"sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb",
"sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9",
"sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57",
"sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc",
"sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"
],
"version": "==1.1.1"
"markers": "python_version >= '3.7'",
"version": "==2.1.3"
},
"mergedeep": {
"hashes": [
"sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8",
"sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"
],
"markers": "python_version >= '3.6'",
"version": "==1.3.4"
},
"mkdocs": {
"hashes": [
"sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939",
"sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"
"sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1",
"sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"
],
"index": "pypi",
"version": "==1.0.4"
"version": "==1.2.3"
},
"mkdocs-codeinclude-plugin": {
"hashes": [
"sha256:172a917c9b257fa62850b669336151f85d3cd40312b2b52520cbcceab557ea6c",
"sha256:305387f67a885f0e36ec1cf977324fe1fe50d31301147194b63631d0864601b1"
],
"index": "pypi",
"version": "==0.2.1"
},
"mkdocs-markdownextradata-plugin": {
"hashes": [
"sha256:64d1c966b288d653f51f7531c03204eb988d0d77e56055c9d703d99105259a36"
"sha256:9c562e8fe375647d5692d11dfe369a7bdd50302174d35995fce2aeca58036ec6"
],
"index": "pypi",
"version": "==0.0.5"
"version": "==0.2.5"
},
"mkdocs-material": {
"hashes": [
"sha256:524debb6ee8ee89cee08886f2a67c3c3875c0ee9579c598d7448cbd2607cd3b7",
"sha256:62ae84082fa9f077c86b7db63e7bedf392005041b451defc850f8d0887a11e91"
"sha256:20c13aa0a54841e1f1c080edb0e3573407884e4abea51ee25573061189bec83e",
"sha256:3314d94ccc11481b1a3aa4f7babb4fb2bc47daa2fa8ace2463665952116f409b"
],
"index": "pypi",
"version": "==3.2.0"
"version": "==8.2.7"
},
"mkdocs-material-extensions": {
"hashes": [
"sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93",
"sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"
],
"markers": "python_version >= '3.7'",
"version": "==1.1.1"
},
"packaging": {
"hashes": [
"sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
"sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
],
"markers": "python_version >= '3.7'",
"version": "==23.1"
},
"pygments": {
"hashes": [
"sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44",
"sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"
"sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692",
"sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"
],
"version": "==2.6.1"
"markers": "python_version >= '3.7'",
"version": "==2.16.1"
},
"pymdown-extensions": {
"hashes": [
"sha256:5bf93d1ccd8281948cd7c559eb363e59b179b5373478e8a7195cf4b78e3c11b6",
"sha256:8f415b21ee86d80bb2c3676f4478b274d0a8ccb13af672a4c86b9ffd22bd005c"
"sha256:508009b211373058debb8247e168de4cbcb91b1bff7b5e961b2c3e864e00b195",
"sha256:ef25dbbae530e8f67575d222b75ff0649b1e841e22c2ae9a20bad9472c2207dc"
],
"version": "==7.1"
"markers": "python_version >= '3.7'",
"version": "==10.1"
},
"python-dateutil": {
"hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.2"
},
"pyyaml": {
"hashes": [
"sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c",
"sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95",
"sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2",
"sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4",
"sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad",
"sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba",
"sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1",
"sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e",
"sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673",
"sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13",
"sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19"
"sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
"sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
"sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
"sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
"sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
"sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
"sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
"sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
"sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
"sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
"sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
"sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
"sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
"sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
"sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
"sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
"sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
"sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
"sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
"sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
"sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
"sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
"sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
"sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
"sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
"sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
"sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
"sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
"sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
"sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
"sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
"sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
"sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
"sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
"sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
"sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
"sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
"sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
"sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
"sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
],
"index": "pypi",
"version": "==5.1"
"markers": "python_version >= '3.6'",
"version": "==6.0.1"
},
"pyyaml-env-tag": {
"hashes": [
"sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb",
"sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"
],
"markers": "python_version >= '3.6'",
"version": "==0.1"
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"version": "==1.14.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"tornado": {
"typing-extensions": {
"hashes": [
"sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc",
"sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52",
"sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6",
"sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d",
"sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b",
"sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673",
"sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9",
"sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a",
"sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"
"sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36",
"sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"
],
"version": "==6.0.4"
"markers": "python_version < '3.8'",
"version": "==4.7.1"
},
"watchdog": {
"hashes": [
"sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a",
"sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100",
"sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8",
"sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc",
"sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae",
"sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41",
"sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0",
"sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f",
"sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c",
"sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9",
"sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3",
"sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709",
"sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83",
"sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759",
"sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9",
"sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3",
"sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7",
"sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f",
"sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346",
"sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674",
"sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397",
"sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96",
"sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d",
"sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a",
"sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64",
"sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44",
"sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"
],
"markers": "python_version >= '3.7'",
"version": "==3.0.0"
},
"zipp": {
"hashes": [
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
"sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b",
"sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"
],
"version": "==3.1.0"
"markers": "python_version >= '3.7'",
"version": "==3.15.0"
}
},
"develop": {}

View File

@@ -1,70 +1,17 @@
[![Build Status](https://travis-ci.org/testcontainers/testcontainers-go.svg?branch=master)](https://travis-ci.org/testcontainers/testcontainers-go)
# Testcontainers
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=141451032&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=EastUs)
[![Main pipeline](https://github.com/testcontainers/testcontainers-go/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/testcontainers/testcontainers-go/actions/workflows/ci.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/testcontainers/testcontainers-go)](https://goreportcard.com/report/github.com/testcontainers/testcontainers-go)
[![GoDoc Reference](https://camo.githubusercontent.com/8609cfcb531fa0f5598a3d4353596fae9336cce3/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f79616e6777656e6d61692f686f772d746f2d6164642d62616467652d696e2d6769746875622d726561646d653f7374617475732e737667)](https://godoc.org/github.com/testcontainers/testcontainers-go)
[![GoDoc Reference](https://camo.githubusercontent.com/8609cfcb531fa0f5598a3d4353596fae9336cce3/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f79616e6777656e6d61692f686f772d746f2d6164642d62616467652d696e2d6769746875622d726561646d653f7374617475732e737667)](https://pkg.go.dev/github.com/testcontainers/testcontainers-go)
_Testcontainers for Go_ is a Go package that makes it simple to create and clean up container-based dependencies for
automated integration/smoke tests. The clean, easy-to-use API enables developers to programmatically define containers
that should be run as part of a test and clean up those resources when the test is done.
When I was working on a Zipkin PR I discovered a nice Java library called
[Testcontainers](https://www.testcontainers.org/).
You can find more information about _Testcontainers for Go_ at [golang.testcontainers.org](https://golang.testcontainers.org), which is rendered from the [./docs](./docs) directory.
It provides an easy and clean API over the go docker sdk to run, terminate and
connect to containers in your tests.
## Using _Testcontainers for Go_
I found myself comfortable programmatically writing the containers I need to run
an integration/smoke tests. So I started porting this library in Go.
This is an example:
```go
package main
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestNginxLatestReturn(t *testing.T) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "nginx",
ExposedPorts: []string{"80/tcp"},
WaitingFor: wait.ForHTTP("/"),
}
nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Error(err)
}
defer nginxC.Terminate(ctx)
ip, err := nginxC.Host(ctx)
if err != nil {
t.Error(err)
}
port, err := nginxC.MappedPort(ctx, "80")
if err != nil {
t.Error(err)
}
resp, err := http.Get(fmt.Sprintf("http://%s:%s", ip, port.Port()))
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode)
}
}
```
This is a simple example, you can create one container in my case using the
`nginx` image. You can get its IP `ip, err := nginxC.GetContainerIpAddress(ctx)` and you
can use it to make a GET: `resp, err := http.Get(fmt.Sprintf("http://%s", ip))`
To clean your environment you can defer the container termination `defer
nginxC.Terminate(ctx, t)`. `t` is `*testing.T` and it is used to notify is the
`defer` failed marking the test as failed.
## Documentation
The documentation lives in [./docs](./docs) and it is rendered at
[golang.testcontainers.org](https://golang.testcontainers.org).
Please visit [the quickstart guide](https://golang.testcontainers.org/quickstart) to understand how to add the dependency to your Go project.

View File

@@ -0,0 +1,203 @@
# Releasing Testcontainers for Go
In order to create a release, we have added a shell script that performs all the tasks for you, allowing a dry-run mode for checking it before creating the release. We are going to explain how to use it in this document.
## Prerequisites
First, it's really important that you first check that the [version.go](./internal/version.go) file is up-to-date, containing the right version you want to create. That file will be used by the automation to perform the release.
Once the version file is correct in the repository:
Second, check that the git remote for the `origin` is pointing to `github.com/testcontainers/testcontainers-go`. You can check it by running:
```shell
git remote -v
```
## Prepare the release
Once the remote is properly set, please follow these steps:
- Run the [pre-release.sh](./scripts/pre-release.sh) shell script to run it in dry-run mode.
- You can use the `DRY_RUN` variable to enable or disable the dry-run mode. By default, it's enabled.
- To prepare for a release, updating the _Testcontainers for Go_ dependency for all the modules and examples, without performing any Git operation:
DRY_RUN="false" ./scripts/pre-release.sh
- The script will update the [mkdocs.yml](./mkdocks.yml) file, updating the `latest_version` field to the current version.
- The script will update the `go.mod` files for each Go modules and example modules under the examples and modules directories, updating the version of the testcontainers-go dependency to the recently created tag.
- The script will modify the docs for the each Go module **that was not released yet**, updating the version of _Testcontainers for Go_ where it was added to the recently created tag.
An example execution, with dry-run mode enabled:
```shell
sed "s/latest_version: .*/latest_version: v0.20.1/g" /Users/mdelapenya/sourcecode/src/github.com/testcontainers/testcontainers-go/mkdocs.yml > /Users/mdelapenya/sourcecode/src/github.com/testcontainers/testcontainers-go/mkdocs.yml.tmp
mv /Users/mdelapenya/sourcecode/src/github.com/testcontainers/testcontainers-go/mkdocs.yml.tmp /Users/mdelapenya/sourcecode/src/github.com/testcontainers/testcontainers-go/mkdocs.yml
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" bigtable/go.mod > bigtable/go.mod.tmp
mv bigtable/go.mod.tmp bigtable/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" cockroachdb/go.mod > cockroachdb/go.mod.tmp
mv cockroachdb/go.mod.tmp cockroachdb/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" consul/go.mod > consul/go.mod.tmp
mv consul/go.mod.tmp consul/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" datastore/go.mod > datastore/go.mod.tmp
mv datastore/go.mod.tmp datastore/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" firestore/go.mod > firestore/go.mod.tmp
mv firestore/go.mod.tmp firestore/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" mongodb/go.mod > mongodb/go.mod.tmp
mv mongodb/go.mod.tmp mongodb/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" nginx/go.mod > nginx/go.mod.tmp
mv nginx/go.mod.tmp nginx/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" pubsub/go.mod > pubsub/go.mod.tmp
mv pubsub/go.mod.tmp pubsub/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" spanner/go.mod > spanner/go.mod.tmp
mv spanner/go.mod.tmp spanner/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" toxiproxy/go.mod > toxiproxy/go.mod.tmp
mv toxiproxy/go.mod.tmp toxiproxy/go.mod
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" compose/go.mod > compose/go.mod.tmp
mv compose/go.mod.tmp compose/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" couchbase/go.mod > couchbase/go.mod.tmp
mv couchbase/go.mod.tmp couchbase/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" localstack/go.mod > localstack/go.mod.tmp
mv localstack/go.mod.tmp localstack/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" mysql/go.mod > mysql/go.mod.tmp
mv mysql/go.mod.tmp mysql/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" neo4j/go.mod > neo4j/go.mod.tmp
mv neo4j/go.mod.tmp neo4j/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" postgres/go.mod > postgres/go.mod.tmp
mv postgres/go.mod.tmp postgres/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" pulsar/go.mod > pulsar/go.mod.tmp
mv pulsar/go.mod.tmp pulsar/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" redis/go.mod > redis/go.mod.tmp
mv redis/go.mod.tmp redis/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" redpanda/go.mod > redpanda/go.mod.tmp
mv redpanda/go.mod.tmp redpanda/go.mod
sed "s/testcontainers-go v.*/testcontainers-go v0.20.1/g" vault/go.mod > vault/go.mod.tmp
mv vault/go.mod.tmp vault/go.mod
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
go mod tidy
sed "s/Not available until the next release of testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\"><span class=\"tc-version\">:material-tag: main<\/span><\/a>/Since testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\/releases\/tag\/v0.20.1\"><span class=\"tc-version\">:material-tag: v0.20.1<\/span><\/a>/g" couchbase.md > couchbase.md.tmp
mv couchbase.md.tmp couchbase.md
sed "s/Not available until the next release of testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\"><span class=\"tc-version\">:material-tag: main<\/span><\/a>/Since testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\/releases\/tag\/v0.20.1\"><span class=\"tc-version\">:material-tag: v0.20.1<\/span><\/a>/g" localstack.md > localstack.md.tmp
mv localstack.md.tmp localstack.md
sed "s/Not available until the next release of testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\"><span class=\"tc-version\">:material-tag: main<\/span><\/a>/Since testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\/releases\/tag\/v0.20.1\"><span class=\"tc-version\">:material-tag: v0.20.1<\/span><\/a>/g" mysql.md > mysql.md.tmp
mv mysql.md.tmp mysql.md
sed "s/Not available until the next release of testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\"><span class=\"tc-version\">:material-tag: main<\/span><\/a>/Since testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\/releases\/tag\/v0.20.1\"><span class=\"tc-version\">:material-tag: v0.20.1<\/span><\/a>/g" neo4j.md > neo4j.md.tmp
mv neo4j.md.tmp neo4j.md
sed "s/Not available until the next release of testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\"><span class=\"tc-version\">:material-tag: main<\/span><\/a>/Since testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\/releases\/tag\/v0.20.1\"><span class=\"tc-version\">:material-tag: v0.20.1<\/span><\/a>/g" postgres.md > postgres.md.tmp
mv postgres.md.tmp postgres.md
sed "s/Not available until the next release of testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\"><span class=\"tc-version\">:material-tag: main<\/span><\/a>/Since testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\/releases\/tag\/v0.20.1\"><span class=\"tc-version\">:material-tag: v0.20.1<\/span><\/a>/g" pulsar.md > pulsar.md.tmp
mv pulsar.md.tmp pulsar.md
sed "s/Not available until the next release of testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\"><span class=\"tc-version\">:material-tag: main<\/span><\/a>/Since testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\/releases\/tag\/v0.20.1\"><span class=\"tc-version\">:material-tag: v0.20.1<\/span><\/a>/g" redis.md > redis.md.tmp
mv redis.md.tmp redis.md
sed "s/Not available until the next release of testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\"><span class=\"tc-version\">:material-tag: main<\/span><\/a>/Since testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\/releases\/tag\/v0.20.1\"><span class=\"tc-version\">:material-tag: v0.20.1<\/span><\/a>/g" redpanda.md > redpanda.md.tmp
mv redpanda.md.tmp redpanda.md
sed "s/Not available until the next release of testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\"><span class=\"tc-version\">:material-tag: main<\/span><\/a>/Since testcontainers-go <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-go\/releases\/tag\/v0.20.1\"><span class=\"tc-version\">:material-tag: v0.20.1<\/span><\/a>/g" vault.md > vault.md.tmp
mv vault.md.tmp vault.md
```
## Performing a release
Once you are satisfied with the modified files in the git state:
- Run the [release.sh](./scripts/release.sh) shell script to create the release in dry-run mode.
- You can use the `DRY_RUN` variable to enable or disable the dry-run mode. By default, it's enabled.
DRY_RUN="false" ./scripts/release.sh
- You can define the bump type, using the `BUMP_TYPE` environment variable. The default value is `minor`, but you can also use `major` or `patch` (the script will fail if the value is not one of these three):
BUMP_TYPE="major" ./scripts/release.sh
- The script will commit the current state of the git repository, if the `DRY_RUN` variable is set to `false`. The modified files are the ones modified by the `pre-release.sh` script.
- The script will create a git tag with the current value of the [version.go](./internal/version.go) file, starting with `v`: e.g. `v0.18.0`, for the following Go modules:
- the root module, representing the Testcontainers for Go library.
- all the Go modules living in both the `examples` and `modules` directory. The git tag value for these Go modules will be created using this name convention:
"${directory}/${module_name}/${version}", e.g. "examples/mysql/v0.18.0", "modules/compose/v0.18.0"
- The script will update the [version.go](./internal/version.go) file, setting the next development version to the value defined in the `BUMP_TYPE` environment variable. For example, if the current version is `v0.18.0`, the script will update the [version.go](./internal/version.go) file with the next development version `v0.19.0`.
- The script will create a commit in the **main** branch if the `DRY_RUN` variable is set to `false`.
- The script will push the main branch including the tags to the upstream repository, https://github.com/testcontainers/testcontainers-go, if the `DRY_RUN` variable is set to `false`.
- Finally, the script will trigger the Golang proxy to update the modules in https://proxy.golang.org/, if the `DRY_RUN` variable is set to `false`.
An example execution, with dry-run mode enabled:
```
$ ./scripts/release.sh
Current version: v0.20.1
git add /Users/mdelapenya/sourcecode/src/github.com/testcontainers/testcontainers-go/internal/version.go
git add /Users/mdelapenya/sourcecode/src/github.com/testcontainers/testcontainers-go/mkdocs.yml
git add examples/**/go.*
git add modules/**/go.*
git commit -m chore: use new version (v0.20.1) in modules and examples
git tag v0.20.1
git tag examples/bigtable/v0.20.1
git tag examples/cockroachdb/v0.20.1
git tag examples/consul/v0.20.1
git tag examples/datastore/v0.20.1
git tag examples/firestore/v0.20.1
git tag examples/mongodb/v0.20.1
git tag examples/nginx/v0.20.1
git tag examples/pubsub/v0.20.1
git tag examples/spanner/v0.20.1
git tag examples/toxiproxy/v0.20.1
git tag modules/compose/v0.20.1
git tag modules/couchbase/v0.20.1
git tag modules/localstack/v0.20.1
git tag modules/mysql/v0.20.1
git tag modules/neo4j/v0.20.1
git tag modules/postgres/v0.20.1
git tag modules/pulsar/v0.20.1
git tag modules/redis/v0.20.1
git tag modules/redpanda/v0.20.1
git tag modules/vault/v0.20.1
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
Producing a minor bump of the version, from 0.20.1 to 0.21.0
sed "s/const Version = ".*"/const Version = "0.21.0"/g" /Users/mdelapenya/sourcecode/src/github.com/testcontainers/testcontainers-go/internal/version.go > /Users/mdelapenya/sourcecode/src/github.com/testcontainers/testcontainers-go/internal/version.go.tmp
mv /Users/mdelapenya/sourcecode/src/github.com/testcontainers/testcontainers-go/internal/version.go.tmp /Users/mdelapenya/sourcecode/src/github.com/testcontainers/testcontainers-go/internal/version.go
git add /Users/mdelapenya/sourcecode/src/github.com/testcontainers/testcontainers-go/internal/version.go
git commit -m chore: prepare for next minor development cycle (0.21.0)
git push origin main --tags
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/examples/bigtable/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/examples/cockroachdb/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/examples/consul/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/examples/datastore/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/examples/firestore/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/examples/mongodb/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/examples/nginx/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/examples/pubsub/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/examples/spanner/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/examples/toxiproxy/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/modules/compose/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/modules/couchbase/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/modules/localstack/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/modules/mysql/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/modules/neo4j/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/modules/postgres/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/modules/pulsar/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/modules/redis/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/modules/redpanda/@v/v0.20.1.info
curl https://proxy.golang.org/github.com/testcontainers/testcontainers-go/modules/vault/@v/v0.20.1.info
```
Right after that, you have to:
- Verify that the commits are in the upstream repository, otherwise, update it with the current state of the main branch.

View File

@@ -0,0 +1,23 @@
.PHONY: dependencies-scan
dependencies-scan:
@echo ">> Scanning dependencies in $(CURDIR)..."
go list -json -m all | docker run --rm -i sonatypecommunity/nancy:latest sleuth --skip-update-check
.PHONY: test-%
test-%:
@echo "Running $* tests..."
gotestsum \
--format short-verbose \
--rerun-fails=5 \
--packages="./..." \
--junitfile TEST-$*.xml \
-- \
-timeout=30m
.PHONY: tools
tools:
go mod download
.PHONY: tools-tidy
tools-tidy:
go mod tidy

View File

@@ -1,269 +0,0 @@
package testcontainers
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"gopkg.in/yaml.v2"
)
const (
envProjectName = "COMPOSE_PROJECT_NAME"
envComposeFile = "COMPOSE_FILE"
)
// DockerCompose defines the contract for running Docker Compose
type DockerCompose interface {
Down() ExecError
Invoke() ExecError
WithCommand([]string) DockerCompose
WithEnv(map[string]string) DockerCompose
}
// LocalDockerCompose represents a Docker Compose execution using local binary
// docker-compose or docker-compose.exe, depending on the underlying platform
type LocalDockerCompose struct {
Executable string
ComposeFilePaths []string
absComposeFilePaths []string
Identifier string
Cmd []string
Env map[string]string
Services map[string]interface{}
}
// NewLocalDockerCompose returns an instance of the local Docker Compose, using an
// array of Docker Compose file paths and an identifier for the Compose execution.
//
// It will iterate through the array adding '-f compose-file-path' flags to the local
// Docker Compose execution. The identifier represents the name of the execution,
// which will define the name of the underlying Docker network and the name of the
// running Compose services.
func NewLocalDockerCompose(filePaths []string, identifier string) *LocalDockerCompose {
dc := &LocalDockerCompose{}
dc.Executable = "docker-compose"
if runtime.GOOS == "windows" {
dc.Executable = "docker-compose.exe"
}
dc.ComposeFilePaths = filePaths
dc.absComposeFilePaths = make([]string, len(filePaths))
for i, cfp := range dc.ComposeFilePaths {
abs, _ := filepath.Abs(cfp)
dc.absComposeFilePaths[i] = abs
}
dc.validate()
dc.Identifier = strings.ToLower(identifier)
return dc
}
// Down executes docker-compose down
func (dc *LocalDockerCompose) Down() ExecError {
return executeCompose(dc, []string{"down"})
}
func (dc *LocalDockerCompose) getDockerComposeEnvironment() map[string]string {
environment := map[string]string{}
composeFileEnvVariableValue := ""
for _, abs := range dc.absComposeFilePaths {
composeFileEnvVariableValue += abs + string(os.PathListSeparator)
}
environment[envProjectName] = dc.Identifier
environment[envComposeFile] = composeFileEnvVariableValue
return environment
}
// Invoke invokes the docker compose
func (dc *LocalDockerCompose) Invoke() ExecError {
return executeCompose(dc, dc.Cmd)
}
// WithCommand assigns the command
func (dc *LocalDockerCompose) WithCommand(cmd []string) DockerCompose {
dc.Cmd = cmd
return dc
}
// WithEnv assigns the environment
func (dc *LocalDockerCompose) WithEnv(env map[string]string) DockerCompose {
dc.Env = env
return dc
}
// validate checks if the files to be run in the compose are valid YAML files, setting up
// references to all services in them
func (dc *LocalDockerCompose) validate() error {
type compose struct {
Services map[string]interface{}
}
for _, abs := range dc.absComposeFilePaths {
c := compose{}
yamlFile, err := ioutil.ReadFile(abs)
if err != nil {
return err
}
err = yaml.Unmarshal(yamlFile, &c)
if err != nil {
return err
}
dc.Services = c.Services
}
return nil
}
// ExecError is super struct that holds any information about an execution error, so the client code
// can handle the result
type ExecError struct {
Command []string
Error error
Stdout error
Stderr error
}
// execute executes a program with arguments and environment variables inside a specific directory
func execute(
dirContext string, environment map[string]string, binary string, args []string) ExecError {
var errStdout, errStderr error
cmd := exec.Command(binary, args...)
cmd.Dir = dirContext
cmd.Env = os.Environ()
for key, value := range environment {
cmd.Env = append(cmd.Env, key+"="+value)
}
stdoutIn, _ := cmd.StdoutPipe()
stderrIn, _ := cmd.StderrPipe()
stdout := newCapturingPassThroughWriter(os.Stdout)
stderr := newCapturingPassThroughWriter(os.Stderr)
err := cmd.Start()
if err != nil {
execCmd := []string{"Starting command", dirContext, binary}
execCmd = append(execCmd, args...)
return ExecError{
// add information about the CMD and arguments used
Command: execCmd,
Error: err,
Stderr: errStderr,
Stdout: errStdout,
}
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
_, errStdout = io.Copy(stdout, stdoutIn)
wg.Done()
}()
_, errStderr = io.Copy(stderr, stderrIn)
wg.Wait()
err = cmd.Wait()
execCmd := []string{"Reading std", dirContext, binary}
execCmd = append(execCmd, args...)
return ExecError{
Command: execCmd,
Error: err,
Stderr: errStderr,
Stdout: errStdout,
}
}
func executeCompose(dc *LocalDockerCompose, args []string) ExecError {
if which(dc.Executable) != nil {
return ExecError{
Command: []string{dc.Executable},
Error: fmt.Errorf("Local Docker Compose not found. Is %s on the PATH?", dc.Executable),
}
}
environment := dc.getDockerComposeEnvironment()
for k, v := range dc.Env {
environment[k] = v
}
cmds := []string{}
pwd := "."
if len(dc.absComposeFilePaths) > 0 {
pwd, _ = filepath.Split(dc.absComposeFilePaths[0])
for _, abs := range dc.absComposeFilePaths {
cmds = append(cmds, "-f", abs)
}
} else {
cmds = append(cmds, "-f", "docker-compose.yml")
}
cmds = append(cmds, args...)
execErr := execute(pwd, environment, dc.Executable, cmds)
err := execErr.Error
if err != nil {
args := strings.Join(dc.Cmd, " ")
return ExecError{
Command: []string{dc.Executable},
Error: fmt.Errorf("Local Docker compose exited abnormally whilst running %s: [%v]. %s", dc.Executable, args, err.Error()),
}
}
return execErr
}
// capturingPassThroughWriter is a writer that remembers
// data written to it and passes it to w
type capturingPassThroughWriter struct {
buf bytes.Buffer
w io.Writer
}
// newCapturingPassThroughWriter creates new capturingPassThroughWriter
func newCapturingPassThroughWriter(w io.Writer) *capturingPassThroughWriter {
return &capturingPassThroughWriter{
w: w,
}
}
func (w *capturingPassThroughWriter) Write(d []byte) (int, error) {
w.buf.Write(d)
return w.w.Write(d)
}
// Bytes returns bytes written to the writer
func (w *capturingPassThroughWriter) Bytes() []byte {
return w.buf.Bytes()
}
// Which checks if a binary is present in PATH
func which(binary string) error {
_, err := exec.LookPath(binary)
return err
}

View File

@@ -0,0 +1,29 @@
package testcontainers
import (
"github.com/testcontainers/testcontainers-go/internal/config"
)
// TestcontainersConfig represents the configuration for Testcontainers
type TestcontainersConfig struct {
Host string `properties:"docker.host,default="` // Deprecated: use Config.Host instead
TLSVerify int `properties:"docker.tls.verify,default=0"` // Deprecated: use Config.TLSVerify instead
CertPath string `properties:"docker.cert.path,default="` // Deprecated: use Config.CertPath instead
RyukDisabled bool `properties:"ryuk.disabled,default=false"` // Deprecated: use Config.RyukDisabled instead
RyukPrivileged bool `properties:"ryuk.container.privileged,default=false"` // Deprecated: use Config.RyukPrivileged instead
Config config.Config
}
// ReadConfig reads from testcontainers properties file, storing the result in a singleton instance
// of the TestcontainersConfig struct
func ReadConfig() TestcontainersConfig {
cfg := config.Read()
return TestcontainersConfig{
Host: cfg.Host,
TLSVerify: cfg.TLSVerify,
CertPath: cfg.CertPath,
RyukDisabled: cfg.RyukDisabled,
RyukPrivileged: cfg.RyukPrivileged,
Config: cfg,
}
}

View File

@@ -2,14 +2,20 @@ package testcontainers
import (
"context"
"errors"
"fmt"
"io"
"path/filepath"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/archive"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
tcexec "github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
"github.com/testcontainers/testcontainers-go/wait"
)
@@ -22,13 +28,6 @@ type DeprecatedContainer interface {
Terminate(ctx context.Context) error
}
// ContainerProvider allows the creation of containers on an arbitrary system
type ContainerProvider interface {
CreateContainer(context.Context, ContainerRequest) (Container, error) // create a container without starting it
RunContainer(context.Context, ContainerRequest) (Container, error) // create a container and start it
Health(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
@@ -38,89 +37,124 @@ type Container interface {
MappedPort(context.Context, nat.Port) (nat.Port, error) // get externally mapped port for a container port
Ports(context.Context) (nat.PortMap, error) // get all exposed ports
SessionID() string // get session id
Start(context.Context) error // start the container
Terminate(context.Context) error // terminate the container
Logs(context.Context) (io.ReadCloser, error) // Get logs of the container
IsRunning() bool
Start(context.Context) error // start the container
Stop(context.Context, *time.Duration) error // stop the container
Terminate(context.Context) error // terminate the container
Logs(context.Context) (io.ReadCloser, error) // Get logs of the container
FollowOutput(LogConsumer)
StartLogProducer(context.Context) error
StopLogProducer() error
Name(context.Context) (string, error) // get container name
State(context.Context) (*types.ContainerState, error) // returns container's running state
Networks(context.Context) ([]string, error) // get container networks
NetworkAliases(context.Context) (map[string][]string, error) // get container network aliases for a network
Exec(ctx context.Context, cmd []string) (int, error)
ContainerIP(context.Context) (string, error) // get container ip
Exec(ctx context.Context, cmd []string, options ...tcexec.ProcessOption) (int, io.Reader, error)
ContainerIP(context.Context) (string, error) // get container ip
ContainerIPs(context.Context) ([]string, error) // get all container IPs
CopyToContainer(ctx context.Context, fileContent []byte, containerFilePath string, fileMode int64) error
CopyDirToContainer(ctx context.Context, hostDirPath string, containerParentPath string, fileMode int64) error
CopyFileToContainer(ctx context.Context, hostFilePath string, containerFilePath string, fileMode int64) error
CopyFileFromContainer(ctx context.Context, filePath string) (io.ReadCloser, error)
}
// ImageBuildInfo defines what is needed to build an image
type ImageBuildInfo interface {
GetContext() (io.Reader, error) // the path to the build context
GetDockerfile() string // the relative path to the Dockerfile, including the fileitself
ShouldBuildImage() bool // return true if the image needs to be built
GetContext() (io.Reader, error) // the path to the build context
GetDockerfile() string // the relative path to the Dockerfile, including the fileitself
ShouldPrintBuildLog() bool // allow build log to be printed to stdout
ShouldBuildImage() bool // return true if the image needs to be built
GetBuildArgs() map[string]*string // return the environment args used to build the from Dockerfile
GetAuthConfigs() map[string]registry.AuthConfig // 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
// rather than using a pre-built one
type FromDockerfile struct {
Context string // the path to the context of of the docker build
ContextArchive io.Reader // the tar archive file to send to docker that contains the build context
Dockerfile string // the path from the context to the Dockerfile for the image, defaults to "Dockerfile"
Context string // the path to the context of of the docker build
ContextArchive io.Reader // the tar archive file to send to docker that contains the build context
Dockerfile string // the path from the context to the Dockerfile for the image, defaults to "Dockerfile"
BuildArgs map[string]*string // enable user to pass build args to docker daemon
PrintBuildLog bool // enable user to print build log
AuthConfigs map[string]registry.AuthConfig // Deprecated. Testcontainers will detect registry credentials automatically. Enable auth configs to be able to pull from an authenticated docker registry
}
type ContainerFile struct {
HostFilePath string
ContainerFilePath string
FileMode int64
}
// ContainerRequest represents the parameters used to get a running container
type ContainerRequest struct {
FromDockerfile
Image string
Entrypoint []string
Env map[string]string
ExposedPorts []string // allow specifying protocol info
Cmd []string
Labels map[string]string
BindMounts map[string]string
VolumeMounts map[string]string
Tmpfs map[string]string
RegistryCred string
WaitingFor wait.Strategy
Name string // for specifying container name
Hostname string
Privileged bool // for starting privileged container
Networks []string // for specifying network names
NetworkAliases map[string][]string // for specifying network aliases
SkipReaper bool // indicates whether we skip setting up a reaper for this
ReaperImage string // alternative reaper image
AutoRemove bool // if set to true, the container will be removed from the host when stopped
NetworkMode container.NetworkMode
AlwaysPullImage bool // Always pull image
Image string
Entrypoint []string
Env map[string]string
ExposedPorts []string // allow specifying protocol info
Cmd []string
Labels map[string]string
Mounts ContainerMounts
Tmpfs map[string]string
RegistryCred string // Deprecated: Testcontainers will detect registry credentials automatically
WaitingFor wait.Strategy
Name string // for specifying container name
Hostname string
ExtraHosts []string // Deprecated: Use HostConfigModifier instead
Privileged bool // For starting privileged container
Networks []string // for specifying network names
NetworkAliases map[string][]string // for specifying network aliases
NetworkMode container.NetworkMode // Deprecated: Use HostConfigModifier instead
Resources container.Resources // Deprecated: Use HostConfigModifier instead
Files []ContainerFile // files which will be copied when container starts
User string // for specifying uid:gid
SkipReaper bool // Deprecated: The reaper is globally controlled by the .testcontainers.properties file or the TESTCONTAINERS_RYUK_DISABLED environment variable
ReaperImage string // Deprecated: use WithImageName ContainerOption instead. Alternative reaper image
ReaperOptions []ContainerOption // options for the reaper
AutoRemove bool // Deprecated: Use HostConfigModifier instead. If set to true, the container will be removed from the host when stopped
AlwaysPullImage bool // Always pull image
ImagePlatform string // ImagePlatform describes the platform which the image runs on.
Binds []string // Deprecated: Use HostConfigModifier instead
ShmSize int64 // Amount of memory shared with the host (in bytes)
CapAdd []string // Deprecated: Use HostConfigModifier instead. Add Linux capabilities
CapDrop []string // Deprecated: Use HostConfigModifier instead. Drop Linux capabilities
ConfigModifier func(*container.Config) // Modifier for the config before container creation
HostConfigModifier func(*container.HostConfig) // Modifier for the host config before container creation
EnpointSettingsModifier func(map[string]*network.EndpointSettings) // Modifier for the network settings before container creation
LifecycleHooks []ContainerLifecycleHooks // define hooks to be executed during container lifecycle
}
// ProviderType is an enum for the possible providers
type ProviderType int
// containerOptions functional options for a container
type containerOptions struct {
ImageName string
RegistryCredentials string // Deprecated: Testcontainers will detect registry credentials automatically
}
// possible provider types
const (
ProviderDocker ProviderType = iota // Docker is default = 0
)
// functional option for setting the reaper image
type ContainerOption func(*containerOptions)
// GetProvider provides the provider implementation for a certain type
func (t ProviderType) GetProvider() (GenericProvider, error) {
switch t {
case ProviderDocker:
provider, err := NewDockerProvider()
if err != nil {
return nil, errors.Wrap(err, "failed to create Docker provider")
}
return provider, nil
// WithImageName sets the reaper image name
func WithImageName(imageName string) ContainerOption {
return func(o *containerOptions) {
o.ImageName = imageName
}
return nil, errors.New("unknown provider")
}
// Validate ensures that the ContainerRequest does not have invalid paramters configured to it
// Deprecated: Testcontainers will detect registry credentials automatically
// WithRegistryCredentials sets the reaper registry credentials
func WithRegistryCredentials(registryCredentials string) ContainerOption {
return func(o *containerOptions) {
o.RegistryCredentials = registryCredentials
}
}
// Validate ensures that the ContainerRequest does not have invalid parameters configured to it
// ex. make sure you are not specifying both an image as well as a context
func (c *ContainerRequest) Validate() error {
validationMethods := []func() error{
c.validateContextAndImage,
c.validateContexOrImageIsSpecified,
c.validateContextOrImageIsSpecified,
c.validateMounts,
}
var err error
@@ -140,6 +174,13 @@ func (c *ContainerRequest) GetContext() (io.Reader, error) {
return c.ContextArchive, nil
}
// always pass context as absolute path
abs, err := filepath.Abs(c.Context)
if err != nil {
return nil, fmt.Errorf("error getting absolute path: %w", err)
}
c.Context = abs
buildContext, err := archive.TarWithOptions(c.Context, &archive.TarOptions{})
if err != nil {
return nil, err
@@ -148,6 +189,11 @@ func (c *ContainerRequest) GetContext() (io.Reader, error) {
return buildContext, nil
}
// GetBuildArgs returns the env args to be used when creating from Dockerfile
func (c *ContainerRequest) GetBuildArgs() map[string]*string {
return c.FromDockerfile.BuildArgs
}
// GetDockerfile returns the Dockerfile from the ContainerRequest, defaults to "Dockerfile"
func (c *ContainerRequest) GetDockerfile() string {
f := c.FromDockerfile.Dockerfile
@@ -158,10 +204,34 @@ func (c *ContainerRequest) GetDockerfile() string {
return f
}
// GetAuthConfigs returns the auth configs to be able to pull from an authenticated docker registry
func (c *ContainerRequest) GetAuthConfigs() map[string]registry.AuthConfig {
images, err := testcontainersdocker.ExtractImagesFromDockerfile(filepath.Join(c.Context, c.GetDockerfile()), c.GetBuildArgs())
if err != nil {
return map[string]registry.AuthConfig{}
}
authConfigs := map[string]registry.AuthConfig{}
for _, image := range images {
registry, authConfig, err := DockerImageAuth(context.Background(), image)
if err != nil {
continue
}
authConfigs[registry] = authConfig
}
return authConfigs
}
func (c *ContainerRequest) ShouldBuildImage() bool {
return c.FromDockerfile.Context != "" || c.FromDockerfile.ContextArchive != nil
}
func (c *ContainerRequest) ShouldPrintBuildLog() bool {
return c.FromDockerfile.PrintBuildLog
}
func (c *ContainerRequest) validateContextAndImage() error {
if c.FromDockerfile.Context != "" && c.Image != "" {
return errors.New("you cannot specify both an Image and Context in a ContainerRequest")
@@ -170,10 +240,25 @@ func (c *ContainerRequest) validateContextAndImage() error {
return nil
}
func (c *ContainerRequest) validateContexOrImageIsSpecified() error {
func (c *ContainerRequest) validateContextOrImageIsSpecified() error {
if c.FromDockerfile.Context == "" && c.FromDockerfile.ContextArchive == nil && c.Image == "" {
return errors.New("you must specify either a build context or an image")
}
return nil
}
func (c *ContainerRequest) validateMounts() error {
targets := make(map[string]bool, len(c.Mounts))
for idx := range c.Mounts {
m := c.Mounts[idx]
targetPath := m.Target.Target()
if targets[targetPath] {
return fmt.Errorf("%w: %s", ErrDuplicateMountTarget, targetPath)
} else {
targets[targetPath] = true
}
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,118 @@
package testcontainers
import (
"context"
"encoding/base64"
"encoding/json"
"os"
"github.com/cpuguy83/dockercfg"
"github.com/docker/docker/api/types/registry"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
)
// DockerImageAuth returns the auth config for the given Docker image, extracting first its Docker registry.
// Finally, it will use the credential helpers to extract the information from the docker config file
// for that registry, if it exists.
func DockerImageAuth(ctx context.Context, image string) (string, registry.AuthConfig, error) {
defaultRegistry := defaultRegistry(ctx)
reg := testcontainersdocker.ExtractRegistry(image, defaultRegistry)
cfgs, err := getDockerAuthConfigs()
if err != nil {
return reg, registry.AuthConfig{}, err
}
if cfg, ok := cfgs[reg]; ok {
return reg, cfg, nil
}
return reg, registry.AuthConfig{}, dockercfg.ErrCredentialsNotFound
}
// defaultRegistry returns the default registry to use when pulling images
// 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 := testcontainersdocker.NewClient(ctx)
if err != nil {
return testcontainersdocker.IndexDockerIO
}
defer client.Close()
info, err := client.Info(ctx)
if err != nil {
return testcontainersdocker.IndexDockerIO
}
return info.IndexServerAddress
}
// getDockerAuthConfigs returns a map with the auth configs from the docker config file
// using the registry as the key
func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {
cfg, err := getDockerConfig()
if err != nil {
return nil, err
}
cfgs := map[string]registry.AuthConfig{}
for k, v := range cfg.AuthConfigs {
ac := registry.AuthConfig{
Auth: v.Auth,
Email: v.Email,
IdentityToken: v.IdentityToken,
Password: v.Password,
RegistryToken: v.RegistryToken,
ServerAddress: v.ServerAddress,
Username: v.Username,
}
if v.Username == "" && v.Password == "" {
u, p, _ := dockercfg.GetRegistryCredentials(k)
ac.Username = u
ac.Password = p
}
if v.Auth == "" {
ac.Auth = base64.StdEncoding.EncodeToString([]byte(ac.Username + ":" + ac.Password))
}
cfgs[k] = ac
}
// in the case where the auth field in the .docker/conf.json is empty, and the user has credential helpers registered
// the auth comes from there
for k := range cfg.CredentialHelpers {
ac := registry.AuthConfig{}
u, p, _ := dockercfg.GetRegistryCredentials(k)
ac.Username = u
ac.Password = p
cfgs[k] = ac
}
return cfgs, nil
}
// getDockerConfig returns the docker config file. It will internally check, in this particular order:
// 1. the DOCKER_AUTH_CONFIG environment variable, unmarshalling it into a dockercfg.Config
// 2. the DOCKER_CONFIG environment variable, as the path to the config file
// 3. else it will load the default config file, which is ~/.docker/config.json
func getDockerConfig() (dockercfg.Config, error) {
dockerAuthConfig := os.Getenv("DOCKER_AUTH_CONFIG")
if dockerAuthConfig != "" {
cfg := dockercfg.Config{}
err := json.Unmarshal([]byte(dockerAuthConfig), &cfg)
if err == nil {
return cfg, nil
}
}
cfg, err := dockercfg.LoadDefaultConfig()
if err != nil {
return cfg, err
}
return cfg, nil
}

View File

@@ -0,0 +1,116 @@
package testcontainers
import "github.com/docker/docker/api/types/mount"
var (
mountTypeMapping = map[MountType]mount.Type{
MountTypeBind: mount.TypeBind,
MountTypeVolume: mount.TypeVolume,
MountTypeTmpfs: mount.TypeTmpfs,
MountTypePipe: mount.TypeNamedPipe,
}
)
// BindMounter can optionally be implemented by mount sources
// to support advanced scenarios based on mount.BindOptions
type BindMounter interface {
GetBindOptions() *mount.BindOptions
}
// VolumeMounter can optionally be implemented by mount sources
// to support advanced scenarios based on mount.VolumeOptions
type VolumeMounter interface {
GetVolumeOptions() *mount.VolumeOptions
}
// TmpfsMounter can optionally be implemented by mount sources
// to support advanced scenarios based on mount.TmpfsOptions
type TmpfsMounter interface {
GetTmpfsOptions() *mount.TmpfsOptions
}
type DockerBindMountSource struct {
*mount.BindOptions
// HostPath is the path mounted into the container
// the same host path might be mounted to multiple locations within a single container
HostPath string
}
func (s DockerBindMountSource) Source() string {
return s.HostPath
}
func (DockerBindMountSource) Type() MountType {
return MountTypeBind
}
func (s DockerBindMountSource) GetBindOptions() *mount.BindOptions {
return s.BindOptions
}
type DockerVolumeMountSource struct {
*mount.VolumeOptions
// Name refers to the name of the volume to be mounted
// the same volume might be mounted to multiple locations within a single container
Name string
}
func (s DockerVolumeMountSource) Source() string {
return s.Name
}
func (DockerVolumeMountSource) Type() MountType {
return MountTypeVolume
}
func (s DockerVolumeMountSource) GetVolumeOptions() *mount.VolumeOptions {
return s.VolumeOptions
}
type DockerTmpfsMountSource struct {
GenericTmpfsMountSource
*mount.TmpfsOptions
}
func (s DockerTmpfsMountSource) GetTmpfsOptions() *mount.TmpfsOptions {
return s.TmpfsOptions
}
// mapToDockerMounts maps the given []ContainerMount to the corresponding
// []mount.Mount for further processing
func mapToDockerMounts(containerMounts ContainerMounts) []mount.Mount {
mounts := make([]mount.Mount, 0, len(containerMounts))
for idx := range containerMounts {
m := containerMounts[idx]
var mountType mount.Type
if mt, ok := mountTypeMapping[m.Source.Type()]; ok {
mountType = mt
} else {
continue
}
containerMount := mount.Mount{
Type: mountType,
Source: m.Source.Source(),
ReadOnly: m.ReadOnly,
Target: m.Target.Target(),
}
switch typedMounter := m.Source.(type) {
case BindMounter:
containerMount.BindOptions = typedMounter.GetBindOptions()
case VolumeMounter:
containerMount.VolumeOptions = typedMounter.GetVolumeOptions()
case TmpfsMounter:
containerMount.TmpfsOptions = typedMounter.GetTmpfsOptions()
}
mounts = append(mounts, containerMount)
}
return mounts
}

View File

@@ -0,0 +1,44 @@
package exec
import (
"bytes"
"io"
"github.com/docker/docker/pkg/stdcopy"
)
// ProcessOptions defines options applicable to the reader processor
type ProcessOptions struct {
Reader io.Reader
}
// ProcessOption defines a common interface to modify the reader processor
// These options can be passed to the Exec function in a variadic way to customize the returned Reader instance
type ProcessOption interface {
Apply(opts *ProcessOptions)
}
type ProcessOptionFunc func(opts *ProcessOptions)
func (fn ProcessOptionFunc) Apply(opts *ProcessOptions) {
fn(opts)
}
func Multiplexed() ProcessOption {
return ProcessOptionFunc(func(opts *ProcessOptions) {
done := make(chan struct{})
var outBuff bytes.Buffer
var errBuff bytes.Buffer
go func() {
if _, err := stdcopy.StdCopy(&outBuff, &errBuff, opts.Reader); err != nil {
return
}
close(done)
}()
<-done
opts.Reader = &outBuff
})
}

View File

@@ -0,0 +1,141 @@
package testcontainers
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func isDir(path string) (bool, error) {
file, err := os.Open(path)
if err != nil {
return false, err
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
return false, err
}
if fileInfo.IsDir() {
return true, nil
}
return false, nil
}
// tarDir compress a directory using tar + gzip algorithms
func tarDir(src string, fileMode int64) (*bytes.Buffer, error) {
// always pass src as absolute path
abs, err := filepath.Abs(src)
if err != nil {
return &bytes.Buffer{}, fmt.Errorf("error getting absolute path: %w", err)
}
src = abs
buffer := &bytes.Buffer{}
fmt.Printf(">> creating TAR file from directory: %s\n", src)
// tar > gzip > buffer
zr := gzip.NewWriter(buffer)
tw := tar.NewWriter(zr)
_, baseDir := filepath.Split(src)
// keep the path relative to the parent directory
index := strings.LastIndex(src, baseDir)
// walk through every file in the folder
err = filepath.Walk(src, func(file string, fi os.FileInfo, errFn error) error {
if errFn != nil {
return fmt.Errorf("error traversing the file system: %w", errFn)
}
// if a symlink, skip file
if fi.Mode().Type() == os.ModeSymlink {
fmt.Printf(">> skipping symlink: %s\n", file)
return nil
}
// generate tar header
header, err := tar.FileInfoHeader(fi, file)
if err != nil {
return fmt.Errorf("error getting file info header: %w", err)
}
// see https://pkg.go.dev/archive/tar#FileInfoHeader:
// Since fs.FileInfo's Name method only returns the base name of the file it describes,
// it may be necessary to modify Header.Name to provide the full path name of the file.
header.Name = filepath.ToSlash(file[index:])
header.Mode = fileMode
// write header
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("error writing header: %w", err)
}
// if not a dir, write file content
if !fi.IsDir() {
data, err := os.Open(file)
if err != nil {
return fmt.Errorf("error opening file: %w", err)
}
defer data.Close()
if _, err := io.Copy(tw, data); err != nil {
return fmt.Errorf("error compressing file: %w", err)
}
}
return nil
})
if err != nil {
return buffer, err
}
// produce tar
if err := tw.Close(); err != nil {
return buffer, fmt.Errorf("error closing tar file: %w", err)
}
// produce gzip
if err := zr.Close(); err != nil {
return buffer, fmt.Errorf("error closing gzip file: %w", err)
}
return buffer, nil
}
// tarFile compress a single file using tar + gzip algorithms
func tarFile(fileContent []byte, basePath string, fileMode int64) (*bytes.Buffer, error) {
buffer := &bytes.Buffer{}
zr := gzip.NewWriter(buffer)
tw := tar.NewWriter(zr)
hdr := &tar.Header{
Name: filepath.Base(basePath),
Mode: fileMode,
Size: int64(len(fileContent)),
}
if err := tw.WriteHeader(hdr); err != nil {
return buffer, err
}
if _, err := tw.Write(fileContent); err != nil {
return buffer, err
}
// produce tar
if err := tw.Close(); err != nil {
return buffer, fmt.Errorf("error closing tar file: %w", err)
}
// produce gzip
if err := zr.Close(); err != nil {
return buffer, fmt.Errorf("error closing gzip file: %w", err)
}
return buffer, nil
}

View File

@@ -2,8 +2,20 @@ package testcontainers
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/pkg/errors"
"dario.cat/mergo"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/testcontainers/testcontainers-go/wait"
)
var (
reuseContainerMx sync.Mutex
ErrReuseEmptyName = errors.New("with reuse option a container name mustn't be empty")
)
// GenericContainerRequest represents parameters to a generic container
@@ -11,6 +23,73 @@ type GenericContainerRequest struct {
ContainerRequest // embedded request for provider
Started bool // whether to auto-start the container
ProviderType ProviderType // which provider to use, Docker if empty
Logger Logging // provide a container specific Logging - use default global logger if empty
Reuse bool // reuse an existing container if it exists or create a new one. a container name mustn't be empty
}
// ContainerCustomizer is an interface that can be used to configure the Testcontainers container
// request. The passed request will be merged with the default one.
type ContainerCustomizer interface {
Customize(req *GenericContainerRequest)
}
// CustomizeRequestOption is a type that can be used to configure the Testcontainers container request.
// The passed request will be merged with the default one.
type CustomizeRequestOption func(req *GenericContainerRequest)
func (opt CustomizeRequestOption) Customize(req *GenericContainerRequest) {
opt(req)
}
// CustomizeRequest returns a function that can be used to merge the passed container request with the one that is used by the container.
// Slices and Maps will be appended.
func CustomizeRequest(src GenericContainerRequest) CustomizeRequestOption {
return func(req *GenericContainerRequest) {
if err := mergo.Merge(req, &src, mergo.WithOverride, mergo.WithAppendSlice); err != nil {
fmt.Printf("error merging container request, keeping the original one. Error: %v", err)
return
}
}
}
// WithImage sets the image for a container
func WithImage(image string) CustomizeRequestOption {
return func(req *GenericContainerRequest) {
req.Image = image
}
}
// WithConfigModifier allows to override the default container config
func WithConfigModifier(modifier func(config *container.Config)) CustomizeRequestOption {
return func(req *GenericContainerRequest) {
req.ConfigModifier = modifier
}
}
// WithEndpointSettingsModifier allows to override the default endpoint settings
func WithEndpointSettingsModifier(modifier func(settings map[string]*network.EndpointSettings)) CustomizeRequestOption {
return func(req *GenericContainerRequest) {
req.EnpointSettingsModifier = modifier
}
}
// WithHostConfigModifier allows to override the default host config
func WithHostConfigModifier(modifier func(hostConfig *container.HostConfig)) CustomizeRequestOption {
return func(req *GenericContainerRequest) {
req.HostConfigModifier = modifier
}
}
// WithWaitStrategy sets the wait strategy for a container, using 60 seconds as deadline
func WithWaitStrategy(strategies ...wait.Strategy) CustomizeRequestOption {
return WithWaitStrategyAndDeadline(60*time.Second, strategies...)
}
// WithWaitStrategyAndDeadline sets the wait strategy for a container, including deadline
func WithWaitStrategyAndDeadline(deadline time.Duration, strategies ...wait.Strategy) CustomizeRequestOption {
return func(req *GenericContainerRequest) {
req.WaitingFor = wait.ForAll(strategies...).WithDeadline(deadline)
}
}
// GenericNetworkRequest represents parameters to a generic network
@@ -27,7 +106,7 @@ func GenericNetwork(ctx context.Context, req GenericNetworkRequest) (Network, er
}
network, err := provider.CreateNetwork(ctx, req.NetworkRequest)
if err != nil {
return nil, errors.Wrap(err, "failed to create network")
return nil, fmt.Errorf("%w: failed to create network", err)
}
return network, nil
@@ -35,22 +114,40 @@ func GenericNetwork(ctx context.Context, req GenericNetworkRequest) (Network, er
// GenericContainer creates a generic container with parameters
func GenericContainer(ctx context.Context, req GenericContainerRequest) (Container, error) {
provider, err := req.ProviderType.GetProvider()
if req.Reuse && req.Name == "" {
return nil, ErrReuseEmptyName
}
logging := req.Logger
if logging == nil {
logging = Logger
}
provider, err := req.ProviderType.GetProvider(WithLogger(logging))
if err != nil {
return nil, err
}
defer provider.Close()
c, err := provider.CreateContainer(ctx, req.ContainerRequest)
var c Container
if req.Reuse {
// we must protect the reusability of the container in the case it's invoked
// in a parallel execution, via ParallelContainers or t.Parallel()
reuseContainerMx.Lock()
defer reuseContainerMx.Unlock()
c, err = provider.ReuseOrCreateContainer(ctx, req.ContainerRequest)
} else {
c, err = provider.CreateContainer(ctx, req.ContainerRequest)
}
if err != nil {
return nil, errors.Wrap(err, "failed to create container")
return nil, fmt.Errorf("%w: failed to create container", err)
}
if req.Started {
if req.Started && !c.IsRunning() {
if err := c.Start(ctx); err != nil {
return c, errors.Wrap(err, "failed to start container")
return c, fmt.Errorf("%w: failed to start container", err)
}
}
return c, nil
}

View File

@@ -0,0 +1,100 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strconv"
"sync"
"github.com/magiconair/properties"
)
var tcConfig Config
var tcConfigOnce *sync.Once = new(sync.Once)
// Config represents the configuration for Testcontainers
// testcontainersConfig {
type Config struct {
Host string `properties:"docker.host,default="`
TLSVerify int `properties:"docker.tls.verify,default=0"`
CertPath string `properties:"docker.cert.path,default="`
RyukDisabled bool `properties:"ryuk.disabled,default=false"`
RyukPrivileged bool `properties:"ryuk.container.privileged,default=false"`
TestcontainersHost string `properties:"tc.host,default="`
}
// }
// Read reads from testcontainers properties file, if it exists
// it is possible that certain values get overridden when set as environment variables
func Read() Config {
tcConfigOnce.Do(func() {
tcConfig = read()
if tcConfig.RyukDisabled {
ryukDisabledMessage := `
**********************************************************************************************
Ryuk has been disabled for the current execution. This can cause unexpected behavior in your environment.
More on this: https://golang.testcontainers.org/features/garbage_collector/
**********************************************************************************************`
fmt.Println(ryukDisabledMessage)
}
})
return tcConfig
}
// Reset resets the singleton instance of the Config struct,
// allowing to read the configuration again.
// Handy for testing, so do not use it in production code
// This function is not thread-safe
func Reset() {
tcConfigOnce = new(sync.Once)
}
func read() Config {
config := Config{}
applyEnvironmentConfiguration := func(config Config) Config {
ryukDisabledEnv := os.Getenv("TESTCONTAINERS_RYUK_DISABLED")
if parseBool(ryukDisabledEnv) {
config.RyukDisabled = ryukDisabledEnv == "true"
}
ryukPrivilegedEnv := os.Getenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED")
if parseBool(ryukPrivilegedEnv) {
config.RyukPrivileged = ryukPrivilegedEnv == "true"
}
return config
}
home, err := os.UserHomeDir()
if err != nil {
return applyEnvironmentConfiguration(config)
}
tcProp := filepath.Join(home, ".testcontainers.properties")
// init from a file
properties, err := properties.LoadFile(tcProp, properties.UTF8)
if err != nil {
return applyEnvironmentConfiguration(config)
}
if err := properties.Decode(&config); err != nil {
fmt.Printf("invalid testcontainers properties file, returning an empty Testcontainers configuration: %v\n", err)
return applyEnvironmentConfiguration(config)
}
fmt.Printf("Testcontainers properties file has been found: %s\n", tcProp)
return applyEnvironmentConfiguration(config)
}
func parseBool(input string) bool {
if _, err := strconv.ParseBool(input); err == nil {
return true
}
return false
}

View File

@@ -0,0 +1,67 @@
package testcontainersdocker
import (
"context"
"path/filepath"
"github.com/docker/docker/client"
"github.com/testcontainers/testcontainers-go/internal"
"github.com/testcontainers/testcontainers-go/internal/config"
"github.com/testcontainers/testcontainers-go/internal/testcontainerssession"
)
// 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) {
tcConfig := config.Read()
dockerHost := ExtractDockerHost(ctx)
opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()}
if dockerHost != "" {
opts = append(opts, client.WithHost(dockerHost))
// for further information, read https://docs.docker.com/engine/security/protect-access/
if tcConfig.TLSVerify == 1 {
cacertPath := filepath.Join(tcConfig.CertPath, "ca.pem")
certPath := filepath.Join(tcConfig.CertPath, "cert.pem")
keyPath := filepath.Join(tcConfig.CertPath, "key.pem")
opts = append(opts, client.WithTLSClientConfig(cacertPath, certPath, keyPath))
}
}
opts = append(opts, client.WithHTTPHeaders(
map[string]string{
"x-tc-sid": testcontainerssession.String(),
"User-Agent": "tc-go/" + internal.Version,
}),
)
// passed options have priority over the default ones
opts = append(opts, ops...)
cli, err := client.NewClientWithOpts(opts...)
if err != nil {
return nil, err
}
if _, err = cli.Ping(context.Background()); err != nil {
// Fallback to environment, including the original options
cli, err = defaultClient(context.Background(), ops...)
if err != nil {
return nil, err
}
}
defer cli.Close()
return cli, nil
}
// defaultClient returns a plain, new docker client with the default options
func defaultClient(ctx context.Context, ops ...client.Opt) (*client.Client, error) {
if len(ops) == 0 {
ops = []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()}
}
return client.NewClientWithOpts(ops...)
}

View File

@@ -0,0 +1,269 @@
package testcontainersdocker
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"sync"
"github.com/docker/docker/client"
"github.com/testcontainers/testcontainers-go/internal/config"
)
type dockerHostContext string
var DockerHostContextKey = dockerHostContext("docker_host")
var (
ErrDockerHostNotSet = errors.New("DOCKER_HOST is not set")
ErrDockerSocketOverrideNotSet = errors.New("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE is not set")
ErrDockerSocketNotSetInContext = errors.New("socket not set in context")
ErrDockerSocketNotSetInProperties = errors.New("socket not set in ~/.testcontainers.properties")
ErrNoUnixSchema = errors.New("URL schema is not unix")
ErrSocketNotFound = errors.New("socket not found")
ErrSocketNotFoundInPath = errors.New("docker socket not found in " + DockerSocketPath)
// ErrTestcontainersHostNotSetInProperties this error is specific to Testcontainers
ErrTestcontainersHostNotSetInProperties = errors.New("tc.host not set in ~/.testcontainers.properties")
)
var dockerHostCache string
var dockerHostOnce sync.Once
var dockerSocketPathCache string
var dockerSocketPathOnce sync.Once
// deprecated
// see https://github.com/testcontainers/testcontainers-java/blob/main/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L46
func DefaultGatewayIP() (string, error) {
// see https://github.com/testcontainers/testcontainers-java/blob/3ad8d80e2484864e554744a4800a81f6b7982168/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L27
cmd := exec.Command("sh", "-c", "ip route|awk '/default/ { print $3 }'")
stdout, err := cmd.Output()
if err != nil {
return "", errors.New("failed to detect docker host")
}
ip := strings.TrimSpace(string(stdout))
if len(ip) == 0 {
return "", errors.New("failed to parse default gateway IP")
}
return ip, nil
}
// ExtractDockerHost Extracts the docker host from the different alternatives, caching the result to avoid unnecessary
// calculations. Use this function to get the actual Docker host. This function does not consider Windows containers at the moment.
// The possible alternatives are:
//
// 1. Docker host from the "tc.host" property in the ~/.testcontainers.properties file.
// 2. DOCKER_HOST environment variable.
// 3. Docker host from context.
// 4. Docker host from the default docker socket path, without the unix schema.
// 5. Docker host from the "docker.host" property in the ~/.testcontainers.properties file.
// 6. Rootless docker socket path.
// 7. Else, the default Docker socket including schema will be returned.
func ExtractDockerHost(ctx context.Context) string {
dockerHostOnce.Do(func() {
dockerHostCache = extractDockerHost(ctx)
})
return dockerHostCache
}
// ExtractDockerSocket Extracts the docker socket from the different alternatives, removing the socket schema and
// caching the result to avoid unnecessary calculations. Use this function to get the docker socket path,
// not the host (e.g. mounting the socket in a container). This function does not consider Windows containers at the moment.
// The possible alternatives are:
//
// 1. Docker host from the "tc.host" property in the ~/.testcontainers.properties file.
// 2. The TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE environment variable.
// 3. Using a Docker client, check if the Info().OperativeSystem is "Docker Desktop" and return the default docker socket path for rootless docker.
// 4. Else, Get the current Docker Host from the existing strategies: see ExtractDockerHost.
// 5. If the socket contains the unix schema, the schema is removed (e.g. unix:///var/run/docker.sock -> /var/run/docker.sock)
// 6. Else, the default location of the docker socket is used (/var/run/docker.sock)
//
// In any case, if the docker socket schema is "tcp://", the default docker socket path will be returned.
func ExtractDockerSocket(ctx context.Context) string {
dockerSocketPathOnce.Do(func() {
dockerSocketPathCache = extractDockerSocket(ctx)
})
return dockerSocketPathCache
}
// extractDockerHost Extracts the docker host from the different alternatives, without caching the result.
// This internal method is handy for testing purposes.
func extractDockerHost(ctx context.Context) string {
dockerHostFns := []func(context.Context) (string, error){
testcontainersHostFromProperties,
dockerHostFromEnv,
dockerHostFromContext,
dockerSocketPath,
dockerHostFromProperties,
rootlessDockerSocketPath,
}
outerErr := ErrSocketNotFound
for _, dockerHostFn := range dockerHostFns {
dockerHost, err := dockerHostFn(ctx)
if err != nil {
outerErr = fmt.Errorf("%w: %v", outerErr, err)
continue
}
return dockerHost
}
// We are not supporting Windows containers at the moment
return DockerSocketPathWithSchema
}
// extractDockerHost Extracts the docker socket from the different alternatives, without caching the result.
// It will internally use the default Docker client, calling the internal method extractDockerSocketFromClient with it.
// This internal method is handy for testing purposes.
// If a Docker client cannot be created, the program will panic.
func extractDockerSocket(ctx context.Context) string {
cli, err := NewClient(ctx)
if err != nil {
panic(err) // a Docker client is required to get the Docker info
}
defer cli.Close()
return extractDockerSocketFromClient(ctx, cli)
}
// extractDockerSocketFromClient Extracts the docker socket from the different alternatives, without caching the result,
// and receiving an instance of the Docker API client interface.
// This internal method is handy for testing purposes, passing a mock type simulating the desired behaviour.
func extractDockerSocketFromClient(ctx context.Context, cli client.APIClient) string {
// check that the socket is not a tcp or unix socket
checkDockerSocketFn := func(socket string) string {
// this use case will cover the case when the docker host is a tcp socket
if strings.HasPrefix(socket, TCPSchema) {
return DockerSocketPath
}
if strings.HasPrefix(socket, DockerSocketSchema) {
return strings.Replace(socket, DockerSocketSchema, "", 1)
}
return socket
}
tcHost, err := testcontainersHostFromProperties(ctx)
if err == nil {
return checkDockerSocketFn(tcHost)
}
testcontainersDockerSocket, err := dockerSocketOverridePath(ctx)
if err == nil {
return checkDockerSocketFn(testcontainersDockerSocket)
}
info, err := cli.Info(ctx)
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 IsWindows() {
return WindowsDockerSocketPath
}
return DockerSocketPath
}
dockerHost := extractDockerHost(ctx)
return checkDockerSocketFn(dockerHost)
}
// dockerHostFromEnv returns the docker host from the DOCKER_HOST environment variable, if it's not empty
func dockerHostFromEnv(ctx context.Context) (string, error) {
if dockerHostPath := os.Getenv("DOCKER_HOST"); dockerHostPath != "" {
return dockerHostPath, nil
}
return "", ErrDockerHostNotSet
}
// dockerHostFromContext returns the docker host from the Go context, if it's not empty
func dockerHostFromContext(ctx context.Context) (string, error) {
if socketPath, ok := ctx.Value(DockerHostContextKey).(string); ok && socketPath != "" {
parsed, err := parseURL(socketPath)
if err != nil {
return "", err
}
return parsed, nil
}
return "", ErrDockerSocketNotSetInContext
}
// dockerHostFromProperties returns the docker host from the ~/.testcontainers.properties file, if it's not empty
func dockerHostFromProperties(ctx context.Context) (string, error) {
cfg := config.Read()
socketPath := cfg.Host
if socketPath != "" {
parsed, err := parseURL(socketPath)
if err != nil {
return "", err
}
return parsed, nil
}
return "", ErrDockerSocketNotSetInProperties
}
// dockerSocketOverridePath returns the docker socket from the TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE environment variable,
// if it's not empty
func dockerSocketOverridePath(ctx context.Context) (string, error) {
if dockerHostPath, exists := os.LookupEnv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE"); exists {
return dockerHostPath, nil
}
return "", ErrDockerSocketOverrideNotSet
}
// dockerSocketPath returns the docker socket from the default docker socket path, if it's not empty
// and the socket exists
func dockerSocketPath(ctx context.Context) (string, error) {
if fileExists(DockerSocketPath) {
return DockerSocketPathWithSchema, nil
}
return "", ErrSocketNotFoundInPath
}
// testcontainersHostFromProperties returns the testcontainers host from the ~/.testcontainers.properties file, if it's not empty
func testcontainersHostFromProperties(ctx context.Context) (string, error) {
cfg := config.Read()
testcontainersHost := cfg.TestcontainersHost
if testcontainersHost != "" {
parsed, err := parseURL(testcontainersHost)
if err != nil {
return "", err
}
return parsed, nil
}
return "", ErrTestcontainersHostNotSetInProperties
}
// InAContainer returns true if the code is running inside a container
// See https://github.com/docker/docker/blob/a9fa38b1edf30b23cae3eade0be48b3d4b1de14b/daemon/initlayer/setup_unix.go#L25
func InAContainer() bool {
return inAContainer("/.dockerenv")
}
func inAContainer(path string) bool {
// see https://github.com/testcontainers/testcontainers-java/blob/3ad8d80e2484864e554744a4800a81f6b7982168/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L15
if _, err := os.Stat(path); err == nil {
return true
}
return false
}

View File

@@ -0,0 +1,146 @@
package testcontainersdocker
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
)
var (
ErrRootlessDockerNotFound = errors.New("rootless Docker not found")
ErrRootlessDockerNotFoundHomeDesktopDir = errors.New("checked path: ~/.docker/desktop/docker.sock")
ErrRootlessDockerNotFoundHomeRunDir = errors.New("checked path: ~/.docker/run/docker.sock")
ErrRootlessDockerNotFoundRunDir = errors.New("checked path: /run/user/${uid}/docker.sock")
ErrRootlessDockerNotFoundXDGRuntimeDir = errors.New("checked path: $XDG_RUNTIME_DIR")
ErrRootlessDockerNotSupportedWindows = errors.New("rootless Docker is not supported on Windows")
ErrXDGRuntimeDirNotSet = errors.New("XDG_RUNTIME_DIR is not set")
)
// baseRunDir is the base directory for the "/run/user/${uid}" directory.
// It is a variable so it can be modified for testing.
var baseRunDir = "/run"
// IsWindows returns if the current OS is Windows. For that it checks the GOOS environment variable or the runtime.GOOS constant.
func IsWindows() bool {
return os.Getenv("GOOS") == "windows" || runtime.GOOS == "windows"
}
// rootlessDockerSocketPath returns if the path to the rootless Docker socket exists.
// The rootless socket path is determined by the following order:
//
// 1. XDG_RUNTIME_DIR environment variable.
// 2. ~/.docker/run/docker.sock file.
// 3. ~/.docker/desktop/docker.sock file.
// 4. /run/user/${uid}/docker.sock file.
// 5. Else, return ErrRootlessDockerNotFound, wrapping secific errors for each of the above paths.
//
// It should include the Docker socket schema (unix://) in the returned path.
func rootlessDockerSocketPath(_ context.Context) (string, error) {
// adding a manner to test it on non-windows machines, setting the GOOS env var to windows
// This is needed because runtime.GOOS is a constant that returns the OS of the machine running the test
if IsWindows() {
return "", ErrRootlessDockerNotSupportedWindows
}
socketPathFns := []func() (string, error){
rootlessSocketPathFromEnv,
rootlessSocketPathFromHomeRunDir,
rootlessSocketPathFromHomeDesktopDir,
rootlessSocketPathFromRunDir,
}
outerErr := ErrRootlessDockerNotFound
for _, socketPathFn := range socketPathFns {
s, err := socketPathFn()
if err != nil {
outerErr = fmt.Errorf("%w: %v", outerErr, err)
continue
}
return DockerSocketSchema + s, nil
}
return "", outerErr
}
func fileExists(f string) bool {
_, err := os.Stat(f)
return err == nil
}
func parseURL(s string) (string, error) {
var hostURL *url.URL
if u, err := url.Parse(s); err != nil {
return "", err
} else {
hostURL = u
}
switch hostURL.Scheme {
case "unix", "npipe":
return hostURL.Path, nil
case "tcp":
// return the original URL, as it is a valid TCP URL
return s, nil
default:
return "", ErrNoUnixSchema
}
}
// rootlessSocketPathFromEnv returns the path to the rootless Docker socket from the XDG_RUNTIME_DIR environment variable.
// It should include the Docker socket schema (unix://) in the returned path.
func rootlessSocketPathFromEnv() (string, error) {
xdgRuntimeDir, exists := os.LookupEnv("XDG_RUNTIME_DIR")
if exists {
f := filepath.Join(xdgRuntimeDir, "docker.sock")
if fileExists(f) {
return f, nil
}
return "", ErrRootlessDockerNotFoundXDGRuntimeDir
}
return "", ErrXDGRuntimeDirNotSet
}
// rootlessSocketPathFromHomeRunDir returns the path to the rootless Docker socket from the ~/.docker/run/docker.sock file.
func rootlessSocketPathFromHomeRunDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
f := filepath.Join(home, ".docker", "run", "docker.sock")
if fileExists(f) {
return f, nil
}
return "", ErrRootlessDockerNotFoundHomeRunDir
}
// rootlessSocketPathFromHomeDesktopDir returns the path to the rootless Docker socket from the ~/.docker/desktop/docker.sock file.
func rootlessSocketPathFromHomeDesktopDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
f := filepath.Join(home, ".docker", "desktop", "docker.sock")
if fileExists(f) {
return f, nil
}
return "", ErrRootlessDockerNotFoundHomeDesktopDir
}
// rootlessSocketPathFromRunDir returns the path to the rootless Docker socket from the /run/user/<uid>/docker.sock file.
func rootlessSocketPathFromRunDir() (string, error) {
uid := os.Getuid()
f := filepath.Join(baseRunDir, "user", fmt.Sprintf("%d", uid), "docker.sock")
if fileExists(f) {
return f, nil
}
return "", ErrRootlessDockerNotFoundRunDir
}

View File

@@ -0,0 +1,49 @@
package testcontainersdocker
import (
"net/url"
"strings"
"github.com/docker/docker/client"
)
// DockerSocketSchema is the unix schema.
var DockerSocketSchema = "unix://"
// DockerSocketPath is the path to the docker socket under unix systems.
var DockerSocketPath = "/var/run/docker.sock"
// DockerSocketPathWithSchema is the path to the docker socket under unix systems with the unix schema.
var DockerSocketPathWithSchema = DockerSocketSchema + DockerSocketPath
// TCPSchema is the tcp schema.
var TCPSchema = "tcp://"
// WindowsDockerSocketPath is the path to the docker socket under windows systems.
var WindowsDockerSocketPath = "//var/run/docker.sock"
func init() {
const DefaultDockerHost = client.DefaultDockerHost
u, err := url.Parse(DefaultDockerHost)
if err != nil {
// unsupported default host specified by the docker client package,
// so revert to the default unix docker socket path
return
}
switch u.Scheme {
case "unix", "npipe":
DockerSocketSchema = u.Scheme + "://"
DockerSocketPath = u.Path
if !strings.HasPrefix(DockerSocketPath, "/") {
// seeing as the code in this module depends on DockerSocketPath having
// a slash (`/`) prefix, we add it here if it is missing.
// for the known environments, we do not foresee how the socket-path
// should miss the slash, however this extra if-condition is worth to
// save future pain from innocent users.
DockerSocketPath = "/" + DockerSocketPath
}
DockerSocketPathWithSchema = DockerSocketSchema + DockerSocketPath
}
}

View File

@@ -0,0 +1,126 @@
package testcontainersdocker
import (
"bufio"
"net/url"
"os"
"regexp"
"strings"
"unicode/utf8"
)
const (
IndexDockerIO = "https://index.docker.io/v1/"
maxURLRuneCount = 2083
minURLRuneCount = 3
URLSchema = `((ftp|tcp|udp|wss?|https?):\/\/)`
URLUsername = `(\S+(:\S*)?@)`
URLIP = `([1-9]\d?|1\d\d|2[01]\d|22[0-3]|24\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-5]))`
IP = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
URLSubdomain = `((www\.)|([a-zA-Z0-9]+([-_\.]?[a-zA-Z0-9])*[a-zA-Z0-9]\.[a-zA-Z0-9]+))`
URLPath = `((\/|\?|#)[^\s]*)`
URLPort = `(:(\d{1,5}))`
URL = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$`
)
var rxURL = regexp.MustCompile(URL)
func ExtractImagesFromDockerfile(dockerfile string, buildArgs map[string]*string) ([]string, error) {
var images []string
file, err := os.Open(dockerfile)
if err != nil {
return nil, err
}
defer file.Close()
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if scanner.Err() != nil {
return nil, scanner.Err()
}
// extract images from dockerfile
for _, line := range lines {
line = strings.TrimSpace(line)
if !strings.HasPrefix(strings.ToUpper(line), "FROM") {
continue
}
// remove FROM
line = strings.TrimPrefix(line, "FROM")
parts := strings.Split(strings.TrimSpace(line), " ")
if len(parts) == 0 {
continue
}
// interpolate build args
for k, v := range buildArgs {
if v != nil {
parts[0] = strings.Replace(parts[0], "${"+k+"}", *v, -1)
}
}
images = append(images, parts[0])
}
return images, nil
}
// ExtractRegistry extracts the registry from the image name, using a regular expression to extract the registry from the image name.
// regular expression to extract the registry from the image name
// the regular expression is based on the grammar defined in
// - image:tag
// - image
// - repository/image:tag
// - repository/image
// - registry/image:tag
// - registry/image
// - registry/repository/image:tag
// - registry/repository/image
// - registry:port/repository/image:tag
// - registry:port/repository/image
// - registry:port/image:tag
// - registry:port/image
// Once extracted the registry, it is validated to check if it is a valid URL or an IP address.
func ExtractRegistry(image string, fallback string) string {
exp := regexp.MustCompile(`^(?:(?P<registry>(https?://)?[^/]+)(?::(?P<port>\d+))?/)?(?:(?P<repository>[^/]+)/)?(?P<image>[^:]+)(?::(?P<tag>.+))?$`).FindStringSubmatch(image)
if len(exp) == 0 {
return ""
}
registry := exp[1]
if IsURL(registry) {
return registry
}
return fallback
}
// IsURL checks if the string is an URL.
// Extracted from https://github.com/asaskevich/govalidator/blob/f21760c49a8d/validator.go#L104
func IsURL(str string) bool {
if str == "" || utf8.RuneCountInString(str) >= maxURLRuneCount || len(str) <= minURLRuneCount || strings.HasPrefix(str, ".") {
return false
}
strTemp := str
if strings.Contains(str, ":") && !strings.Contains(str, "://") {
// support no indicated urlscheme but with colon for port number
// http:// is appended so url.Parse will succeed, strTemp used so it does not impact rxURL.MatchString
strTemp = "http://" + str
}
u, err := url.Parse(strTemp)
if err != nil {
return false
}
if strings.HasPrefix(u.Host, ".") {
return false
}
if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) {
return false
}
return rxURL.MatchString(str)
}

View File

@@ -0,0 +1,19 @@
package testcontainersdocker
import "github.com/testcontainers/testcontainers-go/internal"
const (
LabelBase = "org.testcontainers"
LabelLang = LabelBase + ".lang"
LabelReaper = LabelBase + ".reaper"
LabelSessionID = LabelBase + ".sessionId"
LabelVersion = LabelBase + ".version"
)
func DefaultLabels() map[string]string {
return map[string]string{
LabelBase: "true",
LabelLang: "go",
LabelVersion: internal.Version,
}
}

View File

@@ -0,0 +1,22 @@
package testcontainerssession
import (
"sync"
"github.com/google/uuid"
)
var id uuid.UUID
var idOnce sync.Once
func ID() uuid.UUID {
idOnce.Do(func() {
id = uuid.New()
})
return id
}
func String() string {
return ID().String()
}

View File

@@ -0,0 +1,4 @@
package internal
// Version is the next development version of the application
const Version = "0.23.0"

View File

@@ -0,0 +1,379 @@
package testcontainers
import (
"context"
"io"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat"
"golang.org/x/exp/slices"
)
// ContainerRequestHook is a hook that will be called before a container is created.
// It can be used to modify container configuration before it is created,
// using the different lifecycle hooks that are available:
// - Creating
// For that, it will receive a ContainerRequest, modify it and return an error if needed.
type ContainerRequestHook func(ctx context.Context, req ContainerRequest) error
// ContainerHook is a hook that will be called after a container is created
// It can be used to modify the state of the container after it is created,
// using the different lifecycle hooks that are available:
// - Created
// - Starting
// - Started
// - Stopping
// - Stopped
// - Terminating
// - Terminated
// For that, it will receive a Container, modify it and return an error if needed.
type ContainerHook func(ctx context.Context, container Container) error
// ContainerLifecycleHooks is a struct that contains all the hooks that can be used
// to modify the container lifecycle. All the container lifecycle hooks except the PreCreates hooks
// will be passed to the container once it's created
type ContainerLifecycleHooks struct {
PreCreates []ContainerRequestHook
PostCreates []ContainerHook
PreStarts []ContainerHook
PostStarts []ContainerHook
PreStops []ContainerHook
PostStops []ContainerHook
PreTerminates []ContainerHook
PostTerminates []ContainerHook
}
var DefaultLoggingHook = func(logger Logging) ContainerLifecycleHooks {
shortContainerID := func(c Container) string {
return c.GetContainerID()[:12]
}
return ContainerLifecycleHooks{
PreCreates: []ContainerRequestHook{
func(ctx context.Context, req ContainerRequest) error {
logger.Printf("🐳 Creating container for image %s", req.Image)
return nil
},
},
PostCreates: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("✅ Container created: %s", shortContainerID(c))
return nil
},
},
PreStarts: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("🐳 Starting container: %s", shortContainerID(c))
return nil
},
},
PostStarts: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("✅ Container started: %s", shortContainerID(c))
return nil
},
},
PreStops: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("🐳 Stopping container: %s", shortContainerID(c))
return nil
},
},
PostStops: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("✋ Container stopped: %s", shortContainerID(c))
return nil
},
},
PreTerminates: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("🐳 Terminating container: %s", shortContainerID(c))
return nil
},
},
PostTerminates: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("🚫 Container terminated: %s", shortContainerID(c))
return nil
},
},
}
}
// creatingHook is a hook that will be called before a container is created.
func (req ContainerRequest) creatingHook(ctx context.Context) error {
for _, lifecycleHooks := range req.LifecycleHooks {
err := lifecycleHooks.Creating(ctx)(req)
if err != nil {
return err
}
}
return nil
}
// createdHook is a hook that will be called after a container is created
func (c *DockerContainer) createdHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PostCreates)(c)
if err != nil {
return err
}
}
return nil
}
// startingHook is a hook that will be called before a container is started
func (c *DockerContainer) startingHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PreStarts)(c)
if err != nil {
c.printLogs(ctx)
return err
}
}
return nil
}
// startedHook is a hook that will be called after a container is started
func (c *DockerContainer) startedHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PostStarts)(c)
if err != nil {
c.printLogs(ctx)
return err
}
}
return nil
}
// printLogs is a helper function that will print the logs of a Docker container
// We are going to use this helper function to inform the user of the logs when an error occurs
func (c *DockerContainer) printLogs(ctx context.Context) {
reader, err := c.Logs(ctx)
if err != nil {
c.logger.Printf("failed accessing container logs: %w\n", err)
return
}
b, err := io.ReadAll(reader)
if err != nil {
c.logger.Printf("failed reading container logs: %w\n", err)
return
}
c.logger.Printf("container logs:\n%s", b)
}
// stoppingHook is a hook that will be called before a container is stopped
func (c *DockerContainer) stoppingHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PreStops)(c)
if err != nil {
return err
}
}
return nil
}
// stoppedHook is a hook that will be called after a container is stopped
func (c *DockerContainer) stoppedHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PostStops)(c)
if err != nil {
return err
}
}
return nil
}
// terminatingHook is a hook that will be called before a container is terminated
func (c *DockerContainer) terminatingHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PreTerminates)(c)
if err != nil {
return err
}
}
return nil
}
// terminatedHook is a hook that will be called after a container is terminated
func (c *DockerContainer) terminatedHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PostTerminates)(c)
if err != nil {
return err
}
}
return nil
}
// Creating is a hook that will be called before a container is created.
func (c ContainerLifecycleHooks) Creating(ctx context.Context) func(req ContainerRequest) error {
return func(req ContainerRequest) error {
for _, hook := range c.PreCreates {
if err := hook(ctx, req); err != nil {
return err
}
}
return nil
}
}
// containerHookFn is a helper function that will create a function to be returned by all the different
// container lifecycle hooks. The created function will iterate over all the hooks and call them one by one.
func containerHookFn(ctx context.Context, containerHook []ContainerHook) func(container Container) error {
return func(container Container) error {
for _, hook := range containerHook {
if err := hook(ctx, container); err != nil {
return err
}
}
return nil
}
}
// Created is a hook that will be called after a container is created
func (c ContainerLifecycleHooks) Created(ctx context.Context) func(container Container) error {
return containerHookFn(ctx, c.PostCreates)
}
// Starting is a hook that will be called before a container is started
func (c ContainerLifecycleHooks) Starting(ctx context.Context) func(container Container) error {
return containerHookFn(ctx, c.PreStarts)
}
// Started is a hook that will be called after a container is started
func (c ContainerLifecycleHooks) Started(ctx context.Context) func(container Container) error {
return containerHookFn(ctx, c.PostStarts)
}
// Stopping is a hook that will be called before a container is stopped
func (c ContainerLifecycleHooks) Stopping(ctx context.Context) func(container Container) error {
return containerHookFn(ctx, c.PreStops)
}
// Stopped is a hook that will be called after a container is stopped
func (c ContainerLifecycleHooks) Stopped(ctx context.Context) func(container Container) error {
return containerHookFn(ctx, c.PostStops)
}
// Terminating is a hook that will be called before a container is terminated
func (c ContainerLifecycleHooks) Terminating(ctx context.Context) func(container Container) error {
return containerHookFn(ctx, c.PreTerminates)
}
// Terminated is a hook that will be called after a container is terminated
func (c ContainerLifecycleHooks) Terminated(ctx context.Context) func(container Container) error {
return containerHookFn(ctx, c.PostTerminates)
}
func (p *DockerProvider) preCreateContainerHook(ctx context.Context, req ContainerRequest, dockerInput *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig) error {
// prepare mounts
hostConfig.Mounts = mapToDockerMounts(req.Mounts)
endpointSettings := map[string]*network.EndpointSettings{}
// #248: Docker allows only one network to be specified during container creation
// If there is more than one network specified in the request container should be attached to them
// once it is created. We will take a first network if any specified in the request and use it to create container
if len(req.Networks) > 0 {
attachContainerTo := req.Networks[0]
nw, err := p.GetNetwork(ctx, NetworkRequest{
Name: attachContainerTo,
})
if err == nil {
aliases := []string{}
if _, ok := req.NetworkAliases[attachContainerTo]; ok {
aliases = req.NetworkAliases[attachContainerTo]
}
endpointSetting := network.EndpointSettings{
Aliases: aliases,
NetworkID: nw.ID,
}
endpointSettings[attachContainerTo] = &endpointSetting
}
}
if req.ConfigModifier != nil {
req.ConfigModifier(dockerInput)
}
if req.HostConfigModifier == nil {
req.HostConfigModifier = defaultHostConfigModifier(req)
}
req.HostConfigModifier(hostConfig)
if req.EnpointSettingsModifier != nil {
req.EnpointSettingsModifier(endpointSettings)
}
networkingConfig.EndpointsConfig = endpointSettings
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.ImageInspectWithRaw(ctx, dockerInput.Image)
if err != nil {
return err
}
for p := range image.ContainerConfig.ExposedPorts {
exposedPorts = append(exposedPorts, string(p))
}
}
exposedPortSet, exposedPortMap, err := nat.ParsePortSpecs(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)
}
return nil
}
func mergePortBindings(configPortMap, exposedPortMap nat.PortMap, exposedPorts []string) nat.PortMap {
if exposedPortMap == nil {
exposedPortMap = make(map[nat.Port][]nat.PortBinding)
}
for k, v := range configPortMap {
if slices.Contains(exposedPorts, strings.Split(string(k), "/")[0]) {
exposedPortMap[k] = v
}
}
return exposedPortMap
}
// defaultHostConfigModifier provides a default modifier including the deprecated fields
func defaultHostConfigModifier(req ContainerRequest) func(hostConfig *container.HostConfig) {
return func(hostConfig *container.HostConfig) {
hostConfig.AutoRemove = req.AutoRemove
hostConfig.CapAdd = req.CapAdd
hostConfig.CapDrop = req.CapDrop
hostConfig.Binds = req.Binds
hostConfig.ExtraHosts = req.ExtraHosts
hostConfig.NetworkMode = req.NetworkMode
hostConfig.Resources = req.Resources
}
}

View File

@@ -0,0 +1,81 @@
package testcontainers
import (
"context"
"log"
"os"
"testing"
"github.com/docker/docker/client"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
)
// Logger is the default log instance
var Logger Logging = log.New(os.Stderr, "", log.LstdFlags)
// Logging defines the Logger interface
type Logging interface {
Printf(format string, v ...interface{})
}
// LogDockerServerInfo logs the docker server info using the provided logger and Docker client
func LogDockerServerInfo(ctx context.Context, client client.APIClient, logger Logging) {
infoMessage := `%v - Connected to docker:
Server Version: %v
API Version: %v
Operating System: %v
Total Memory: %v MB
Resolved Docker Host: %s
Resolved Docker Socket Path: %s
`
info, err := client.Info(ctx)
if err != nil {
logger.Printf("failed getting information about docker server: %s", err)
return
}
logger.Printf(infoMessage, packagePath,
info.ServerVersion, client.ClientVersion(),
info.OperatingSystem, info.MemTotal/1024/1024,
testcontainersdocker.ExtractDockerHost(ctx),
testcontainersdocker.ExtractDockerSocket(ctx),
)
}
// TestLogger returns a Logging implementation for testing.TB
// This way logs from testcontainers are part of the test output of a test suite or test case
func TestLogger(tb testing.TB) Logging {
tb.Helper()
return testLogger{TB: tb}
}
// WithLogger is a generic option that implements GenericProviderOption, DockerProviderOption
// It replaces the global Logging implementation with a user defined one e.g. to aggregate logs from testcontainers
// with the logs of specific test case
func WithLogger(logger Logging) LoggerOption {
return LoggerOption{
logger: logger,
}
}
type LoggerOption struct {
logger Logging
}
func (o LoggerOption) ApplyGenericTo(opts *GenericProviderOptions) {
opts.Logger = o.logger
}
func (o LoggerOption) ApplyDockerTo(opts *DockerProviderOptions) {
opts.Logger = o.logger
}
type testLogger struct {
testing.TB
}
func (t testLogger) Printf(format string, v ...interface{}) {
t.Helper()
t.Logf(format, v...)
}

View File

@@ -1,39 +1,109 @@
site_name: Testcontainers-Go
site_name: Testcontainers for Go
site_url: https://golang.testcontainers.org
plugins:
- search
- codeinclude
- markdownextradata
theme:
name: 'material'
name: material
custom_dir: docs/theme
palette:
primary: 'blue'
accent: 'teal'
font: false
logo: 'logo.svg'
favicon: 'favicon.ico'
scheme: testcontainers
font:
text: Roboto
code: Roboto Mono
logo: logo.svg
favicon: favicon.ico
extra_css:
- 'css/extra.css'
repo_name: 'testcontainers-go'
repo_url: 'https://github.com/testcontainers/testcontainers-go'
- css/extra.css
- css/tc-header.css
repo_name: testcontainers-go
repo_url: https://github.com/testcontainers/testcontainers-go
markdown_extensions:
- admonition
- codehilite:
linenums: False
linenums: false
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- pymdownx.snippets
- toc:
permalink: true
- attr_list
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
nav:
- Home: index.md
- Quickstart:
- quickstart/gotest.md
- Quickstart: quickstart.md
- Features:
- features/creating_container.md
- features/garbage_collector.md
- features/build_from_dockerfile.md
- features/docker_compose.md
- features/follow_logs.md
- features/override_container_command.md
- features/creating_container.md
- features/files_and_mounts.md
- features/configuration.md
- features/networking.md
- features/garbage_collector.md
- features/build_from_dockerfile.md
- features/docker_auth.md
- features/docker_compose.md
- features/follow_logs.md
- features/override_container_command.md
- features/copy_file.md
- Wait Strategies:
- Introduction: features/wait/introduction.md
- Exec: features/wait/exec.md
- Exit: features/wait/exit.md
- Health: features/wait/health.md
- HostPort: features/wait/host_port.md
- HTTP: features/wait/http.md
- Log: features/wait/log.md
- Multi: features/wait/multi.md
- SQL: features/wait/sql.md
- Modules:
- modules/index.md
- modules/artemis.md
- modules/clickhouse.md
- modules/couchbase.md
- modules/k3s.md
- modules/localstack.md
- modules/mongodb.md
- modules/mysql.md
- modules/neo4j.md
- modules/postgres.md
- modules/pulsar.md
- modules/redis.md
- modules/redpanda.md
- modules/vault.md
- Examples:
- examples/index.md
- examples/bigtable.md
- examples/cockroachdb.md
- examples/consul.md
- examples/datastore.md
- examples/firestore.md
- examples/nats.md
- examples/nginx.md
- examples/pubsub.md
- examples/spanner.md
- examples/toxiproxy.md
- System Requirements:
- system_requirements/index.md
- system_requirements/docker.md
- Continuous Integration:
- system_requirements/ci/aws_codebuild.md
- system_requirements/ci/bitbucket_pipelines.md
- system_requirements/ci/circle_ci.md
- system_requirements/ci/concourse_ci.md
- system_requirements/ci/dind_patterns.md
- system_requirements/ci/drone.md
- system_requirements/ci/gitlab_ci.md
- system_requirements/ci/tekton.md
- system_requirements/ci/travis.md
- system_requirements/using_colima.md
- system_requirements/using_podman.md
- Contributing:
- contributing.md
- contributing_docs.md
- contributing.md
- contributing_docs.md
- Getting help: getting_help.md
edit_uri: edit/main/docs/
extra:
latest_version: 1.14.1
latest_version: v0.23.0

View File

@@ -0,0 +1,116 @@
package testcontainers
const (
MountTypeBind MountType = iota
MountTypeVolume
MountTypeTmpfs
MountTypePipe
)
var (
_ ContainerMountSource = (*GenericBindMountSource)(nil)
_ ContainerMountSource = (*GenericVolumeMountSource)(nil)
_ ContainerMountSource = (*GenericTmpfsMountSource)(nil)
)
type (
// ContainerMounts represents a collection of mounts for a container
ContainerMounts []ContainerMount
MountType uint
)
// ContainerMountSource is the base for all mount sources
type ContainerMountSource interface {
// Source will be used as Source field in the final mount
// this might either be a volume name, a host path or might be empty e.g. for Tmpfs
Source() string
// Type determines the final mount type
// possible options are limited by the Docker API
Type() MountType
}
// GenericBindMountSource implements ContainerMountSource and represents a bind mount
// Optionally mount.BindOptions might be added for advanced scenarios
type GenericBindMountSource struct {
// HostPath is the path mounted into the container
// the same host path might be mounted to multiple locations within a single container
HostPath string
}
func (s GenericBindMountSource) Source() string {
return s.HostPath
}
func (GenericBindMountSource) Type() MountType {
return MountTypeBind
}
// GenericVolumeMountSource implements ContainerMountSource and represents a volume mount
type GenericVolumeMountSource struct {
// Name refers to the name of the volume to be mounted
// the same volume might be mounted to multiple locations within a single container
Name string
}
func (s GenericVolumeMountSource) Source() string {
return s.Name
}
func (GenericVolumeMountSource) Type() MountType {
return MountTypeVolume
}
// GenericTmpfsMountSource implements ContainerMountSource and represents a TmpFS mount
// Optionally mount.TmpfsOptions might be added for advanced scenarios
type GenericTmpfsMountSource struct {
}
func (s GenericTmpfsMountSource) Source() string {
return ""
}
func (GenericTmpfsMountSource) Type() MountType {
return MountTypeTmpfs
}
// ContainerMountTarget represents the target path within a container where the mount will be available
// Note that mount targets must be unique. It's not supported to mount different sources to the same target.
type ContainerMountTarget string
func (t ContainerMountTarget) Target() string {
return string(t)
}
// BindMount returns a new ContainerMount with a GenericBindMountSource as source
// This is a convenience method to cover typical use cases.
func BindMount(hostPath string, mountTarget ContainerMountTarget) ContainerMount {
return ContainerMount{
Source: GenericBindMountSource{HostPath: hostPath},
Target: mountTarget,
}
}
// VolumeMount returns a new ContainerMount with a GenericVolumeMountSource as source
// This is a convenience method to cover typical use cases.
func VolumeMount(volumeName string, mountTarget ContainerMountTarget) ContainerMount {
return ContainerMount{
Source: GenericVolumeMountSource{Name: volumeName},
Target: mountTarget,
}
}
// Mounts returns a ContainerMounts to support a more fluent API
func Mounts(mounts ...ContainerMount) ContainerMounts {
return mounts
}
// ContainerMount models a mount into a container
type ContainerMount struct {
// Source is typically either a GenericBindMountSource or a GenericVolumeMountSource
Source ContainerMountSource
// Target is the path where the mount should be mounted within the container
Target ContainerMountTarget
// ReadOnly determines if the mount should be read-only
ReadOnly bool
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
)
// NetworkProvider allows the creation of networks on an arbitrary system
@@ -17,6 +18,16 @@ type Network interface {
Remove(context.Context) error // removes the network
}
type DefaultNetwork string
func (n DefaultNetwork) ApplyGenericTo(opts *GenericProviderOptions) {
opts.DefaultNetwork = string(n)
}
func (n DefaultNetwork) ApplyDockerTo(opts *DockerProviderOptions) {
opts.DefaultNetwork = string(n)
}
// NetworkRequest represents the parameters used to get a network
type NetworkRequest struct {
Driver string
@@ -26,7 +37,9 @@ type NetworkRequest struct {
Name string
Labels map[string]string
Attachable bool
IPAM *network.IPAM
SkipReaper bool // indicates whether we skip setting up a reaper for this
ReaperImage string //alternative reaper registry
SkipReaper bool // Deprecated: The reaper is globally controlled by the .testcontainers.properties file or the TESTCONTAINERS_RYUK_DISABLED environment variable
ReaperImage string // Deprecated: use WithImageName ContainerOption instead. Alternative reaper registry
ReaperOptions []ContainerOption // Reaper options to use for this network
}

View File

@@ -0,0 +1,122 @@
package testcontainers
import (
"context"
"fmt"
"sync"
)
const (
defaultWorkersCount = 8
)
type ParallelContainerRequest []GenericContainerRequest
// ParallelContainersOptions represents additional options for parallel running
type ParallelContainersOptions struct {
WorkersCount int // count of parallel workers. If field empty(zero), default value will be 'defaultWorkersCount'
}
// ParallelContainersRequestError represents error from parallel request
type ParallelContainersRequestError struct {
Request GenericContainerRequest
Error error
}
type ParallelContainersError struct {
Errors []ParallelContainersRequestError
}
func (gpe ParallelContainersError) Error() string {
return fmt.Sprintf("%v", gpe.Errors)
}
func parallelContainersRunner(
ctx context.Context,
requests <-chan GenericContainerRequest,
errors chan<- ParallelContainersRequestError,
containers chan<- Container,
wg *sync.WaitGroup) {
for req := range requests {
c, err := GenericContainer(ctx, req)
if err != nil {
errors <- ParallelContainersRequestError{
Request: req,
Error: err,
}
continue
}
containers <- c
}
wg.Done()
}
// ParallelContainers creates a generic containers with parameters and run it in parallel mode
func ParallelContainers(ctx context.Context, reqs ParallelContainerRequest, opt ParallelContainersOptions) ([]Container, error) {
if opt.WorkersCount == 0 {
opt.WorkersCount = defaultWorkersCount
}
tasksChanSize := opt.WorkersCount
if tasksChanSize > len(reqs) {
tasksChanSize = len(reqs)
}
tasksChan := make(chan GenericContainerRequest, tasksChanSize)
errsChan := make(chan ParallelContainersRequestError)
resChan := make(chan Container)
waitRes := make(chan struct{})
containers := make([]Container, 0)
errors := make([]ParallelContainersRequestError, 0)
wg := sync.WaitGroup{}
wg.Add(tasksChanSize)
// run workers
for i := 0; i < tasksChanSize; i++ {
go parallelContainersRunner(ctx, tasksChan, errsChan, resChan, &wg)
}
go func() {
for {
select {
case c, ok := <-resChan:
if !ok {
resChan = nil
} else {
containers = append(containers, c)
}
case e, ok := <-errsChan:
if !ok {
errsChan = nil
} else {
errors = append(errors, e)
}
}
if resChan == nil && errsChan == nil {
waitRes <- struct{}{}
break
}
}
}()
for _, req := range reqs {
tasksChan <- req
}
close(tasksChan)
wg.Wait()
close(resChan)
close(errsChan)
<-waitRes
if len(errors) != 0 {
return containers, ParallelContainersError{Errors: errors}
}
return containers, nil
}

View File

@@ -0,0 +1,163 @@
package testcontainers
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
)
// possible provider types
const (
ProviderDefault ProviderType = iota // default will auto-detect provider from DOCKER_HOST environment variable
ProviderDocker
ProviderPodman
)
type (
// ProviderType is an enum for the possible providers
ProviderType int
// GenericProviderOptions defines options applicable to all providers
GenericProviderOptions struct {
Logger Logging
DefaultNetwork string
}
// GenericProviderOption defines a common interface to modify GenericProviderOptions
// These options can be passed to GetProvider in a variadic way to customize the returned GenericProvider instance
GenericProviderOption interface {
ApplyGenericTo(opts *GenericProviderOptions)
}
// GenericProviderOptionFunc is a shorthand to implement the GenericProviderOption interface
GenericProviderOptionFunc func(opts *GenericProviderOptions)
// DockerProviderOptions defines options applicable to DockerProvider
DockerProviderOptions struct {
defaultBridgeNetworkName string
*GenericProviderOptions
}
// DockerProviderOption defines a common interface to modify DockerProviderOptions
// These can be passed to NewDockerProvider in a variadic way to customize the returned DockerProvider instance
DockerProviderOption interface {
ApplyDockerTo(opts *DockerProviderOptions)
}
// DockerProviderOptionFunc is a shorthand to implement the DockerProviderOption interface
DockerProviderOptionFunc func(opts *DockerProviderOptions)
)
func (f DockerProviderOptionFunc) ApplyDockerTo(opts *DockerProviderOptions) {
f(opts)
}
func Generic2DockerOptions(opts ...GenericProviderOption) []DockerProviderOption {
converted := make([]DockerProviderOption, 0, len(opts))
for _, o := range opts {
switch c := o.(type) {
case DockerProviderOption:
converted = append(converted, c)
default:
converted = append(converted, DockerProviderOptionFunc(func(opts *DockerProviderOptions) {
o.ApplyGenericTo(opts.GenericProviderOptions)
}))
}
}
return converted
}
func WithDefaultBridgeNetwork(bridgeNetworkName string) DockerProviderOption {
return DockerProviderOptionFunc(func(opts *DockerProviderOptions) {
opts.defaultBridgeNetworkName = bridgeNetworkName
})
}
func (f GenericProviderOptionFunc) ApplyGenericTo(opts *GenericProviderOptions) {
f(opts)
}
// ContainerProvider allows the creation of containers on an arbitrary system
type ContainerProvider interface {
Close() error // close the provider
CreateContainer(context.Context, ContainerRequest) (Container, error) // create a container without starting it
ReuseOrCreateContainer(context.Context, ContainerRequest) (Container, error) // reuses a container if it exists or creates a container without starting
RunContainer(context.Context, ContainerRequest) (Container, error) // create a container and start it
Health(context.Context) error
Config() TestcontainersConfig
}
// GetProvider provides the provider implementation for a certain type
func (t ProviderType) GetProvider(opts ...GenericProviderOption) (GenericProvider, error) {
opt := &GenericProviderOptions{
Logger: Logger,
}
for _, o := range opts {
o.ApplyGenericTo(opt)
}
pt := t
if pt == ProviderDefault && strings.Contains(os.Getenv("DOCKER_HOST"), "podman.sock") {
pt = ProviderPodman
}
switch pt {
case ProviderDefault, ProviderDocker:
providerOptions := append(Generic2DockerOptions(opts...), WithDefaultBridgeNetwork(Bridge))
provider, err := NewDockerProvider(providerOptions...)
if err != nil {
return nil, fmt.Errorf("%w, failed to create Docker provider", err)
}
return provider, nil
case ProviderPodman:
providerOptions := append(Generic2DockerOptions(opts...), WithDefaultBridgeNetwork(Podman))
provider, err := NewDockerProvider(providerOptions...)
if err != nil {
return nil, fmt.Errorf("%w, failed to create Docker provider", err)
}
return provider, nil
}
return nil, errors.New("unknown provider")
}
// NewDockerProvider creates a Docker provider with the EnvClient
func NewDockerProvider(provOpts ...DockerProviderOption) (*DockerProvider, error) {
o := &DockerProviderOptions{
GenericProviderOptions: &GenericProviderOptions{
Logger: Logger,
},
}
for idx := range provOpts {
provOpts[idx].ApplyDockerTo(o)
}
c, err := NewDockerClient()
if err != nil {
return nil, err
}
tcConfig := ReadConfig()
dockerHost := testcontainersdocker.ExtractDockerHost(context.Background())
p := &DockerProvider{
DockerProviderOptions: o,
host: dockerHost,
client: c,
config: tcConfig,
}
// log docker server info only once
logOnce.Do(func() {
LogDockerServerInfo(context.Background(), p.client, p.Logger)
})
return p, nil
}

View File

@@ -4,81 +4,128 @@ import (
"bufio"
"context"
"fmt"
"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go/wait"
"net"
"strings"
"sync"
"time"
"github.com/pkg/errors"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
"github.com/testcontainers/testcontainers-go/wait"
)
const (
TestcontainerLabel = "org.testcontainers.golang"
// Deprecated: it has been replaced by the internal testcontainersdocker.LabelLang
TestcontainerLabel = "org.testcontainers.golang"
// Deprecated: it has been replaced by the internal testcontainersdocker.LabelSessionID
TestcontainerLabelSessionID = TestcontainerLabel + ".sessionId"
TestcontainerLabelIsReaper = TestcontainerLabel + ".reaper"
// Deprecated: it has been replaced by the internal testcontainersdocker.LabelReaper
TestcontainerLabelIsReaper = TestcontainerLabel + ".reaper"
ReaperDefaultImage = "quay.io/testcontainers/ryuk:0.2.3"
ReaperDefaultImage = "docker.io/testcontainers/ryuk:0.5.1"
)
var reaper *Reaper // We would like to create reaper only once
var mutex sync.Mutex
var (
reaperInstance *Reaper // We would like to create reaper only once
mutex sync.Mutex
)
// ReaperProvider represents a provider for the reaper to run itself with
// The ContainerProvider interface should usually satisfy this as well, so it is pluggable
type ReaperProvider interface {
RunContainer(ctx context.Context, req ContainerRequest) (Container, error)
}
// Reaper is used to start a sidecar container that cleans up resources
type Reaper struct {
Provider ReaperProvider
SessionID string
Endpoint string
Config() TestcontainersConfig
}
// NewReaper creates a Reaper with a sessionID to identify containers and a provider to use
// Deprecated: it's not possible to create a reaper anymore.
func NewReaper(ctx context.Context, sessionID string, provider ReaperProvider, reaperImageName string) (*Reaper, error) {
return reuseOrCreateReaper(ctx, sessionID, provider, WithImageName(reaperImageName))
}
// reuseOrCreateReaper returns an existing Reaper instance if it exists and is running. Otherwise, a new Reaper instance
// will be created with a sessionID to identify containers and a provider to use
func reuseOrCreateReaper(ctx context.Context, sessionID string, provider ReaperProvider, opts ...ContainerOption) (*Reaper, error) {
mutex.Lock()
defer mutex.Unlock()
// If reaper already exists re-use it
if reaper != nil {
return reaper, nil
// If reaper already exists and healthy, re-use it
if reaperInstance != nil {
// Verify this instance is still running by checking state.
// Can't use Container.IsRunning because the bool is not updated when Reaper is terminated
state, err := reaperInstance.container.State(ctx)
if err == nil && state.Running {
return reaperInstance, nil
}
}
// Otherwise create a new one
reaper = &Reaper{
r, err := newReaper(ctx, sessionID, provider, opts...)
if err != nil {
return nil, err
}
reaperInstance = r
return reaperInstance, nil
}
// newReaper creates a Reaper with a sessionID to identify containers and a provider to use
// Should only be used internally and instead use reuseOrCreateReaper to prefer reusing an existing Reaper instance
func newReaper(ctx context.Context, sessionID string, provider ReaperProvider, opts ...ContainerOption) (*Reaper, error) {
dockerHostMount := testcontainersdocker.ExtractDockerSocket(ctx)
reaper := &Reaper{
Provider: provider,
SessionID: sessionID,
}
listeningPort := nat.Port("8080/tcp")
tcConfig := provider.Config().Config
reaperOpts := containerOptions{}
for _, opt := range opts {
opt(&reaperOpts)
}
req := ContainerRequest{
Image: reaperImage(reaperImageName),
Image: reaperImage(reaperOpts.ImageName),
ExposedPorts: []string{string(listeningPort)},
Labels: map[string]string{
TestcontainerLabel: "true",
TestcontainerLabelIsReaper: "true",
TestcontainerLabelIsReaper: "true",
testcontainersdocker.LabelReaper: "true",
},
SkipReaper: true,
BindMounts: map[string]string{
"/var/run/docker.sock": "/var/run/docker.sock",
Mounts: Mounts(BindMount(dockerHostMount, "/var/run/docker.sock")),
Privileged: tcConfig.RyukPrivileged,
WaitingFor: wait.ForListeningPort(listeningPort),
ReaperOptions: opts,
HostConfigModifier: func(hc *container.HostConfig) {
hc.AutoRemove = true
hc.NetworkMode = Bridge
},
AutoRemove: true,
WaitingFor: wait.ForListeningPort(listeningPort),
}
// keep backwards compatibility
req.ReaperImage = req.Image
// include reaper-specific labels to the reaper container
for k, v := range reaper.Labels() {
if k == TestcontainerLabelSessionID || k == testcontainersdocker.LabelSessionID {
continue
}
req.Labels[k] = v
}
// Attach reaper container to a requested network if it is specified
if p, ok := provider.(*DockerProvider); ok {
req.Networks = append(req.Networks, p.defaultNetwork)
req.Networks = append(req.Networks, p.DefaultNetwork)
}
c, err := provider.RunContainer(ctx, req)
if err != nil {
return nil, err
}
reaper.container = c
endpoint, err := c.PortEndpoint(ctx, "8080", "")
if err != nil {
@@ -89,19 +136,19 @@ func NewReaper(ctx context.Context, sessionID string, provider ReaperProvider, r
return reaper, nil
}
func reaperImage(reaperImageName string) string {
if reaperImageName == "" {
return ReaperDefaultImage
} else {
return reaperImageName
}
// Reaper is used to start a sidecar container that cleans up resources
type Reaper struct {
Provider ReaperProvider
SessionID string
Endpoint string
container Container
}
// Connect runs a goroutine which can be terminated by sending true into the returned channel
func (r *Reaper) Connect() (chan bool, error) {
conn, err := net.DialTimeout("tcp", r.Endpoint, 10*time.Second)
if err != nil {
return nil, errors.Wrap(err, "Connecting to Ryuk on "+r.Endpoint+" failed")
return nil, fmt.Errorf("%w: Connecting to Ryuk on %s failed", err, r.Endpoint)
}
terminationSignal := make(chan bool)
@@ -118,8 +165,14 @@ func (r *Reaper) Connect() (chan bool, error) {
for retryLimit > 0 {
retryLimit--
sock.WriteString(strings.Join(labelFilters, "&"))
sock.WriteString("\n")
if _, err := sock.WriteString(strings.Join(labelFilters, "&")); err != nil {
continue
}
if _, err := sock.WriteString("\n"); err != nil {
continue
}
if err := sock.Flush(); err != nil {
continue
}
@@ -128,6 +181,7 @@ func (r *Reaper) Connect() (chan bool, error) {
if err != nil {
continue
}
if resp == "ACK\n" {
break
}
@@ -141,7 +195,15 @@ func (r *Reaper) Connect() (chan bool, error) {
// Labels returns the container labels to use so that this Reaper cleans them up
func (r *Reaper) Labels() map[string]string {
return map[string]string{
TestcontainerLabel: "true",
TestcontainerLabelSessionID: r.SessionID,
TestcontainerLabel: "true",
TestcontainerLabelSessionID: r.SessionID,
testcontainersdocker.LabelSessionID: r.SessionID,
}
}
func reaperImage(reaperImageName string) string {
if reaperImageName == "" {
return ReaperDefaultImage
}
return reaperImageName
}

View File

@@ -1,4 +1,4 @@
mkdocs==1.0.4
mkdocs-material==4.6.0
mkdocs-markdownextradata-plugin==0.1.1
markdown>=3.1,<3.2
mkdocs==1.5.2
mkdocs-codeinclude-plugin==0.2.1
mkdocs-material==8.2.7
mkdocs-markdownextradata-plugin==0.2.5

View File

@@ -1 +1 @@
3.7
3.8

View File

@@ -0,0 +1 @@
package testcontainers

View File

@@ -3,12 +3,14 @@ package testcontainers
import (
"context"
"testing"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
)
// SkipIfProviderIsNotHealthy is a utility function capable of skipping tests
// if the provider is not healthy, or running at all.
// This is a function designed to be used in your test, when Docker is not mandatory for CI/CD.
// In this way tests that depend on testcontainers won't run if the provider is provisioned correctly.
// In this way tests that depend on Testcontainers won't run if the provider is provisioned correctly.
func SkipIfProviderIsNotHealthy(t *testing.T) {
ctx := context.Background()
provider, err := ProviderDocker.GetProvider()
@@ -20,3 +22,21 @@ func SkipIfProviderIsNotHealthy(t *testing.T) {
t.Skipf("Docker is not running. TestContainers can't perform is work without it: %s", err)
}
}
// SkipIfDockerDesktop is a utility function capable of skipping tests
// if tests are run using Docker Desktop.
func SkipIfDockerDesktop(t *testing.T, ctx context.Context) {
cli, err := testcontainersdocker.NewClient(ctx)
if err != nil {
t.Fatalf("failed to create docker client: %s", err)
}
info, err := cli.Info(ctx)
if err != nil {
t.Fatalf("failed to get docker info: %s", err)
}
if info.OperatingSystem == "Docker Desktop" {
t.Skip("Skipping test that requires host network access when running in Docker Desktop")
}
}

View File

@@ -0,0 +1,80 @@
package wait
import (
"context"
"fmt"
"time"
)
// Implement interface
var _ Strategy = (*MultiStrategy)(nil)
var _ StrategyTimeout = (*MultiStrategy)(nil)
type MultiStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
timeout *time.Duration
deadline *time.Duration
// additional properties
Strategies []Strategy
}
// WithStartupTimeoutDefault sets the default timeout for all inner wait strategies
func (ms *MultiStrategy) WithStartupTimeoutDefault(timeout time.Duration) *MultiStrategy {
ms.timeout = &timeout
return ms
}
// WithStartupTimeout sets a time.Duration which limits all wait strategies
//
// Deprecated: use WithDeadline
func (ms *MultiStrategy) WithStartupTimeout(timeout time.Duration) Strategy {
return ms.WithDeadline(timeout)
}
// WithDeadline sets a time.Duration which limits all wait strategies
func (ms *MultiStrategy) WithDeadline(deadline time.Duration) *MultiStrategy {
ms.deadline = &deadline
return ms
}
func ForAll(strategies ...Strategy) *MultiStrategy {
return &MultiStrategy{
Strategies: strategies,
}
}
func (ms *MultiStrategy) Timeout() *time.Duration {
return ms.timeout
}
func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
var cancel context.CancelFunc
if ms.deadline != nil {
ctx, cancel = context.WithTimeout(ctx, *ms.deadline)
defer cancel()
}
if len(ms.Strategies) == 0 {
return fmt.Errorf("no wait strategy supplied")
}
for _, strategy := range ms.Strategies {
strategyCtx := ctx
// Set default Timeout when strategy implements StrategyTimeout
if st, ok := strategy.(StrategyTimeout); ok {
if ms.Timeout() != nil && st.Timeout() == nil {
strategyCtx, cancel = context.WithTimeout(ctx, *ms.Timeout())
defer cancel()
}
}
err := strategy.WaitUntilReady(strategyCtx, target)
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package wait

View File

@@ -0,0 +1,99 @@
package wait
import (
"context"
"io"
"time"
tcexec "github.com/testcontainers/testcontainers-go/exec"
)
// Implement interface
var _ Strategy = (*ExecStrategy)(nil)
var _ StrategyTimeout = (*ExecStrategy)(nil)
type ExecStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
timeout *time.Duration
cmd []string
// additional properties
ExitCodeMatcher func(exitCode int) bool
ResponseMatcher func(body io.Reader) bool
PollInterval time.Duration
}
// NewExecStrategy constructs an Exec strategy ...
func NewExecStrategy(cmd []string) *ExecStrategy {
return &ExecStrategy{
cmd: cmd,
ExitCodeMatcher: defaultExitCodeMatcher,
ResponseMatcher: func(body io.Reader) bool { return true },
PollInterval: defaultPollInterval(),
}
}
func defaultExitCodeMatcher(exitCode int) bool {
return exitCode == 0
}
// WithStartupTimeout can be used to change the default startup timeout
func (ws *ExecStrategy) WithStartupTimeout(startupTimeout time.Duration) *ExecStrategy {
ws.timeout = &startupTimeout
return ws
}
func (ws *ExecStrategy) WithExitCodeMatcher(exitCodeMatcher func(exitCode int) bool) *ExecStrategy {
ws.ExitCodeMatcher = exitCodeMatcher
return ws
}
func (ws *ExecStrategy) WithResponseMatcher(matcher func(body io.Reader) bool) *ExecStrategy {
ws.ResponseMatcher = matcher
return ws
}
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (ws *ExecStrategy) WithPollInterval(pollInterval time.Duration) *ExecStrategy {
ws.PollInterval = pollInterval
return ws
}
// ForExec is a convenience method to assign ExecStrategy
func ForExec(cmd []string) *ExecStrategy {
return NewExecStrategy(cmd)
}
func (ws *ExecStrategy) Timeout() *time.Duration {
return ws.timeout
}
func (ws *ExecStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
timeout := defaultStartupTimeout()
if ws.timeout != nil {
timeout = *ws.timeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(ws.PollInterval):
exitCode, resp, err := target.Exec(ctx, ws.cmd, tcexec.Multiplexed())
if err != nil {
return err
}
if !ws.ExitCodeMatcher(exitCode) {
continue
}
if ws.ResponseMatcher != nil && !ws.ResponseMatcher(resp) {
continue
}
return nil
}
}
}

View File

@@ -0,0 +1,89 @@
package wait
import (
"context"
"strings"
"time"
)
// Implement interface
var _ Strategy = (*ExitStrategy)(nil)
var _ StrategyTimeout = (*ExitStrategy)(nil)
// ExitStrategy will wait until container exit
type ExitStrategy struct {
// all Strategies should have a timeout to avoid waiting infinitely
timeout *time.Duration
// additional properties
PollInterval time.Duration
}
// NewExitStrategy constructs with polling interval of 100 milliseconds without timeout by default
func NewExitStrategy() *ExitStrategy {
return &ExitStrategy{
PollInterval: defaultPollInterval(),
}
}
// fluent builders for each property
// since go has neither covariance nor generics, the return type must be the type of the concrete implementation
// this is true for all properties, even the "shared" ones
// WithExitTimeout can be used to change the default exit timeout
func (ws *ExitStrategy) WithExitTimeout(exitTimeout time.Duration) *ExitStrategy {
ws.timeout = &exitTimeout
return ws
}
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (ws *ExitStrategy) WithPollInterval(pollInterval time.Duration) *ExitStrategy {
ws.PollInterval = pollInterval
return ws
}
// ForExit is the default construction for the fluid interface.
//
// For Example:
//
// wait.
// ForExit().
// WithPollInterval(1 * time.Second)
func ForExit() *ExitStrategy {
return NewExitStrategy()
}
func (ws *ExitStrategy) Timeout() *time.Duration {
return ws.timeout
}
// WaitUntilReady implements Strategy.WaitUntilReady
func (ws *ExitStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
if ws.timeout != nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, *ws.timeout)
defer cancel()
}
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
state, err := target.State(ctx)
if err != nil {
if !strings.Contains(err.Error(), "No such container") {
return err
} else {
return nil
}
}
if state.Running {
time.Sleep(ws.PollInterval)
continue
}
return nil
}
}
}

View File

@@ -0,0 +1,91 @@
package wait
import (
"context"
"time"
"github.com/docker/docker/api/types"
)
// Implement interface
var _ Strategy = (*HealthStrategy)(nil)
var _ StrategyTimeout = (*HealthStrategy)(nil)
// HealthStrategy will wait until the container becomes healthy
type HealthStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
timeout *time.Duration
// additional properties
PollInterval time.Duration
}
// NewHealthStrategy constructs with polling interval of 100 milliseconds and startup timeout of 60 seconds by default
func NewHealthStrategy() *HealthStrategy {
return &HealthStrategy{
PollInterval: defaultPollInterval(),
}
}
// fluent builders for each property
// since go has neither covariance nor generics, the return type must be the type of the concrete implementation
// this is true for all properties, even the "shared" ones like startupTimeout
// WithStartupTimeout can be used to change the default startup timeout
func (ws *HealthStrategy) WithStartupTimeout(startupTimeout time.Duration) *HealthStrategy {
ws.timeout = &startupTimeout
return ws
}
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (ws *HealthStrategy) WithPollInterval(pollInterval time.Duration) *HealthStrategy {
ws.PollInterval = pollInterval
return ws
}
// ForHealthCheck is the default construction for the fluid interface.
//
// For Example:
//
// wait.
// ForHealthCheck().
// WithPollInterval(1 * time.Second)
func ForHealthCheck() *HealthStrategy {
return NewHealthStrategy()
}
func (ws *HealthStrategy) Timeout() *time.Duration {
return ws.timeout
}
// WaitUntilReady implements Strategy.WaitUntilReady
func (ws *HealthStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
timeout := defaultStartupTimeout()
if ws.timeout != nil {
timeout = *ws.timeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
state, err := target.State(ctx)
if err != nil {
return err
}
if err := checkState(state); err != nil {
return err
}
if state.Health == nil || state.Health.Status != types.Healthy {
time.Sleep(ws.PollInterval)
continue
}
return nil
}
}
}

View File

@@ -2,31 +2,37 @@ package wait
import (
"context"
"errors"
"fmt"
"log"
"net"
"os"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/docker/go-connections/nat"
)
// Implement interface
var _ Strategy = (*HostPortStrategy)(nil)
var _ StrategyTimeout = (*HostPortStrategy)(nil)
var errShellNotExecutable = errors.New("/bin/sh command not executable")
type HostPortStrategy struct {
// Port is a string containing port number and protocol in the format "80/tcp"
// which
Port nat.Port
// all WaitStrategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
timeout *time.Duration
PollInterval time.Duration
}
// NewHostPortStrategy constructs a default host port strategy
func NewHostPortStrategy(port nat.Port) *HostPortStrategy {
return &HostPortStrategy{
Port: port,
startupTimeout: defaultStartupTimeout(),
Port: port,
PollInterval: defaultPollInterval(),
}
}
@@ -40,70 +46,150 @@ func ForListeningPort(port nat.Port) *HostPortStrategy {
return NewHostPortStrategy(port)
}
// ForExposedPort constructs an exposed port strategy. Alias for `NewHostPortStrategy("")`.
// This strategy waits for the first port exposed in the Docker container.
func ForExposedPort() *HostPortStrategy {
return NewHostPortStrategy("")
}
// WithStartupTimeout can be used to change the default startup timeout
func (hp *HostPortStrategy) WithStartupTimeout(startupTimeout time.Duration) *HostPortStrategy {
hp.startupTimeout = startupTimeout
hp.timeout = &startupTimeout
return hp
}
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (hp *HostPortStrategy) WithPollInterval(pollInterval time.Duration) *HostPortStrategy {
hp.PollInterval = pollInterval
return hp
}
func (hp *HostPortStrategy) Timeout() *time.Duration {
return hp.timeout
}
// WaitUntilReady implements Strategy.WaitUntilReady
func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
// limit context to startupTimeout
ctx, cancelContext := context.WithTimeout(ctx, hp.startupTimeout)
defer cancelContext()
timeout := defaultStartupTimeout()
if hp.timeout != nil {
timeout = *hp.timeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ipAddress, err := target.Host(ctx)
if err != nil {
return
}
port, err := target.MappedPort(ctx, hp.Port)
if err != nil {
var waitInterval = hp.PollInterval
internalPort := hp.Port
if internalPort == "" {
var ports nat.PortMap
ports, err = target.Ports(ctx)
if err != nil {
return
}
if len(ports) > 0 {
for p := range ports {
internalPort = p
break
}
}
}
if internalPort == "" {
err = fmt.Errorf("no port to wait for")
return
}
var port nat.Port
port, err = target.MappedPort(ctx, internalPort)
var i = 0
for port == "" {
i++
select {
case <-ctx.Done():
return fmt.Errorf("%s:%w", ctx.Err(), err)
case <-time.After(waitInterval):
if err := checkTarget(ctx, target); err != nil {
return err
}
port, err = target.MappedPort(ctx, internalPort)
if err != nil {
fmt.Printf("(%d) [%s] %s\n", i, port, err)
}
}
}
if err := externalCheck(ctx, ipAddress, port, target, waitInterval); err != nil {
return err
}
err = internalCheck(ctx, internalPort, target)
if err != nil && errors.Is(errShellNotExecutable, err) {
log.Println("Shell not executable in container, only external port check will be performed")
} else {
return err
}
return nil
}
func externalCheck(ctx context.Context, ipAddress string, port nat.Port, target StrategyTarget, waitInterval time.Duration) error {
proto := port.Proto()
portNumber := port.Int()
portString := strconv.Itoa(portNumber)
//external check
dialer := net.Dialer{}
address := net.JoinHostPort(ipAddress, portString)
for {
if err := checkTarget(ctx, target); err != nil {
return err
}
conn, err := dialer.DialContext(ctx, proto, address)
if err != nil {
if v, ok := err.(*net.OpError); ok {
if v2, ok := (v.Err).(*os.SyscallError); ok {
if isConnRefusedErr(v2.Err) {
time.Sleep(100 * time.Millisecond)
time.Sleep(waitInterval)
continue
}
}
}
return err
} else {
conn.Close()
_ = conn.Close()
break
}
}
return nil
}
//internal check
command := buildInternalCheckCommand(hp.Port.Int())
func internalCheck(ctx context.Context, internalPort nat.Port, target StrategyTarget) error {
command := buildInternalCheckCommand(internalPort.Int())
for {
if ctx.Err() != nil {
return ctx.Err()
}
exitCode, err := target.Exec(ctx, []string{"/bin/sh", "-c", command})
if err := checkTarget(ctx, target); err != nil {
return err
}
exitCode, _, err := target.Exec(ctx, []string{"/bin/sh", "-c", command})
if err != nil {
return errors.Wrapf(err, "host port waiting failed")
return fmt.Errorf("%w, host port waiting failed", err)
}
if exitCode == 0 {
break
} else if exitCode == 126 {
return errors.New("/bin/sh command not executable")
return errShellNotExecutable
}
}
return nil
}

View File

@@ -1,6 +1,7 @@
package wait
import (
"bytes"
"context"
"crypto/tls"
"errors"
@@ -8,6 +9,7 @@ import (
"io"
"net"
"net/http"
"net/url"
"strconv"
"time"
@@ -16,10 +18,11 @@ import (
// Implement interface
var _ Strategy = (*HTTPStrategy)(nil)
var _ StrategyTimeout = (*HTTPStrategy)(nil)
type HTTPStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
timeout *time.Duration
// additional properties
Port nat.Port
@@ -32,13 +35,13 @@ type HTTPStrategy struct {
Method string // http method
Body io.Reader // http request body
PollInterval time.Duration
UserInfo *url.Userinfo
}
// NewHTTPStrategy constructs a HTTP strategy waiting on port 80 and status code 200
func NewHTTPStrategy(path string) *HTTPStrategy {
return &HTTPStrategy{
startupTimeout: defaultStartupTimeout(),
Port: "80/tcp",
Port: "",
Path: path,
StatusCodeMatcher: defaultStatusCodeMatcher,
ResponseMatcher: func(body io.Reader) bool { return true },
@@ -47,6 +50,7 @@ func NewHTTPStrategy(path string) *HTTPStrategy {
Method: http.MethodGet,
Body: nil,
PollInterval: defaultPollInterval(),
UserInfo: nil,
}
}
@@ -58,8 +62,9 @@ func defaultStatusCodeMatcher(status int) bool {
// since go has neither covariance nor generics, the return type must be the type of the concrete implementation
// this is true for all properties, even the "shared" ones like startupTimeout
func (ws *HTTPStrategy) WithStartupTimeout(startupTimeout time.Duration) *HTTPStrategy {
ws.startupTimeout = startupTimeout
// WithStartupTimeout can be used to change the default startup timeout
func (ws *HTTPStrategy) WithStartupTimeout(timeout time.Duration) *HTTPStrategy {
ws.timeout = &timeout
return ws
}
@@ -101,6 +106,11 @@ func (ws *HTTPStrategy) WithBody(reqdata io.Reader) *HTTPStrategy {
return ws
}
func (ws *HTTPStrategy) WithBasicAuth(username, password string) *HTTPStrategy {
ws.UserInfo = url.UserPassword(username, password)
return ws
}
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (ws *HTTPStrategy) WithPollInterval(pollInterval time.Duration) *HTTPStrategy {
ws.PollInterval = pollInterval
@@ -113,24 +123,71 @@ func ForHTTP(path string) *HTTPStrategy {
return NewHTTPStrategy(path)
}
func (ws *HTTPStrategy) Timeout() *time.Duration {
return ws.timeout
}
// WaitUntilReady implements Strategy.WaitUntilReady
func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
// limit context to startupTimeout
ctx, cancelContext := context.WithTimeout(ctx, ws.startupTimeout)
defer cancelContext()
timeout := defaultStartupTimeout()
if ws.timeout != nil {
timeout = *ws.timeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ipAddress, err := target.Host(ctx)
if err != nil {
return
}
port, err := target.MappedPort(ctx, ws.Port)
if err != nil {
return
}
var mappedPort nat.Port
if ws.Port == "" {
ports, err := target.Ports(ctx)
for err != nil {
select {
case <-ctx.Done():
return fmt.Errorf("%s:%w", ctx.Err(), err)
case <-time.After(ws.PollInterval):
if err := checkTarget(ctx, target); err != nil {
return err
}
if port.Proto() != "tcp" {
return errors.New("Cannot use HTTP client on non-TCP ports")
ports, err = target.Ports(ctx)
}
}
for k, bindings := range ports {
if len(bindings) == 0 || k.Proto() != "tcp" {
continue
}
mappedPort, _ = nat.NewPort(k.Proto(), bindings[0].HostPort)
break
}
if mappedPort == "" {
return errors.New("No exposed tcp ports or mapped ports - cannot wait for status")
}
} else {
mappedPort, err = target.MappedPort(ctx, ws.Port)
for mappedPort == "" {
select {
case <-ctx.Done():
return fmt.Errorf("%s:%w", ctx.Err(), err)
case <-time.After(ws.PollInterval):
if err := checkTarget(ctx, target); err != nil {
return err
}
mappedPort, err = target.MappedPort(ctx, ws.Port)
}
}
if mappedPort.Proto() != "tcp" {
return errors.New("Cannot use HTTP client on non-TCP ports")
}
}
switch ws.Method {
@@ -174,15 +231,36 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
}
client := http.Client{Transport: tripper, Timeout: time.Second}
address := net.JoinHostPort(ipAddress, strconv.Itoa(port.Int()))
endpoint := fmt.Sprintf("%s://%s%s", proto, address, ws.Path)
address := net.JoinHostPort(ipAddress, strconv.Itoa(mappedPort.Int()))
endpoint := url.URL{
Scheme: proto,
Host: address,
Path: ws.Path,
}
if ws.UserInfo != nil {
endpoint.User = ws.UserInfo
}
// cache the body into a byte-slice so that it can be iterated over multiple times
var body []byte
if ws.Body != nil {
body, err = io.ReadAll(ws.Body)
if err != nil {
return
}
}
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(ws.PollInterval):
req, err := http.NewRequestWithContext(ctx, ws.Method, endpoint, ws.Body)
if err := checkTarget(ctx, target); err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, ws.Method, endpoint.String(), bytes.NewReader(body))
if err != nil {
return err
}
@@ -191,9 +269,11 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
continue
}
if ws.StatusCodeMatcher != nil && !ws.StatusCodeMatcher(resp.StatusCode) {
_ = resp.Body.Close()
continue
}
if ws.ResponseMatcher != nil && !ws.ResponseMatcher(resp.Body) {
_ = resp.Body.Close()
continue
}
if err := resp.Body.Close(); err != nil {

View File

@@ -2,18 +2,19 @@ package wait
import (
"context"
"io/ioutil"
"io"
"strings"
"time"
)
// Implement interface
var _ Strategy = (*LogStrategy)(nil)
var _ StrategyTimeout = (*LogStrategy)(nil)
// LogStrategy will wait until a given log entry shows up in the docker logs
type LogStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
timeout *time.Duration
// additional properties
Log string
@@ -21,15 +22,13 @@ type LogStrategy struct {
PollInterval time.Duration
}
// NewLogStrategy constructs a HTTP strategy waiting on port 80 and status code 200
// NewLogStrategy constructs with polling interval of 100 milliseconds and startup timeout of 60 seconds by default
func NewLogStrategy(log string) *LogStrategy {
return &LogStrategy{
startupTimeout: defaultStartupTimeout(),
Log: log,
Occurrence: 1,
PollInterval: defaultPollInterval(),
Log: log,
Occurrence: 1,
PollInterval: defaultPollInterval(),
}
}
// fluent builders for each property
@@ -37,8 +36,8 @@ func NewLogStrategy(log string) *LogStrategy {
// this is true for all properties, even the "shared" ones like startupTimeout
// WithStartupTimeout can be used to change the default startup timeout
func (ws *LogStrategy) WithStartupTimeout(startupTimeout time.Duration) *LogStrategy {
ws.startupTimeout = startupTimeout
func (ws *LogStrategy) WithStartupTimeout(timeout time.Duration) *LogStrategy {
ws.timeout = &timeout
return ws
}
@@ -49,7 +48,7 @@ func (ws *LogStrategy) WithPollInterval(pollInterval time.Duration) *LogStrategy
}
func (ws *LogStrategy) WithOccurrence(o int) *LogStrategy {
// the number of occurence needs to be positive
// the number of occurrence needs to be positive
if o <= 0 {
o = 1
}
@@ -60,19 +59,29 @@ func (ws *LogStrategy) WithOccurrence(o int) *LogStrategy {
// ForLog is the default construction for the fluid interface.
//
// For Example:
// wait.
// ForLog("some text").
// WithPollInterval(1 * time.Second)
//
// wait.
// ForLog("some text").
// WithPollInterval(1 * time.Second)
func ForLog(log string) *LogStrategy {
return NewLogStrategy(log)
}
func (ws *LogStrategy) Timeout() *time.Duration {
return ws.timeout
}
// WaitUntilReady implements Strategy.WaitUntilReady
func (ws *LogStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
// limit context to startupTimeout
ctx, cancelContext := context.WithTimeout(ctx, ws.startupTimeout)
defer cancelContext()
currentOccurence := 0
timeout := defaultStartupTimeout()
if ws.timeout != nil {
timeout = *ws.timeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
length := 0
LOOP:
for {
@@ -80,20 +89,27 @@ LOOP:
case <-ctx.Done():
return ctx.Err()
default:
reader, err := target.Logs(ctx)
checkErr := checkTarget(ctx, target)
reader, err := target.Logs(ctx)
if err != nil {
time.Sleep(ws.PollInterval)
continue
}
b, err := ioutil.ReadAll(reader)
b, err := io.ReadAll(reader)
if err != nil {
time.Sleep(ws.PollInterval)
continue
}
logs := string(b)
if strings.Contains(logs, ws.Log) {
currentOccurence++
if ws.Occurrence == 0 || currentOccurence >= ws.Occurrence-1 {
break LOOP
}
if length == len(logs) && checkErr != nil {
return checkErr
} else if strings.Count(logs, ws.Log) >= ws.Occurrence {
break LOOP
} else {
length = len(logs)
time.Sleep(ws.PollInterval)
continue
}

View File

@@ -1,47 +0,0 @@
package wait
import (
"context"
"fmt"
"time"
)
// Implement interface
var _ Strategy = (*MultiStrategy)(nil)
type MultiStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
// additional properties
Strategies []Strategy
}
func (ms *MultiStrategy) WithStartupTimeout(startupTimeout time.Duration) *MultiStrategy {
ms.startupTimeout = startupTimeout
return ms
}
func ForAll(strategies ...Strategy) *MultiStrategy {
return &MultiStrategy{
startupTimeout: defaultStartupTimeout(),
Strategies: strategies,
}
}
func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
ctx, cancelContext := context.WithTimeout(ctx, ms.startupTimeout)
defer cancelContext()
if len(ms.Strategies) == 0 {
return fmt.Errorf("no wait strategy supplied")
}
for _, strategy := range ms.Strategies {
err := strategy.WaitUntilReady(ctx, target)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,69 @@
package wait
import (
"context"
"io"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go/exec"
)
var _ Strategy = (*NopStrategy)(nil)
var _ StrategyTimeout = (*NopStrategy)(nil)
type NopStrategy struct {
timeout *time.Duration
waitUntilReady func(context.Context, StrategyTarget) error
}
func ForNop(
waitUntilReady func(context.Context, StrategyTarget) error,
) *NopStrategy {
return &NopStrategy{
waitUntilReady: waitUntilReady,
}
}
func (ws *NopStrategy) Timeout() *time.Duration {
return ws.timeout
}
func (ws *NopStrategy) WithStartupTimeout(timeout time.Duration) *NopStrategy {
ws.timeout = &timeout
return ws
}
func (ws *NopStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
return ws.waitUntilReady(ctx, target)
}
type NopStrategyTarget struct {
ReaderCloser io.ReadCloser
ContainerState types.ContainerState
}
func (st NopStrategyTarget) Host(_ context.Context) (string, error) {
return "", nil
}
func (st NopStrategyTarget) Ports(_ context.Context) (nat.PortMap, error) {
return nil, nil
}
func (st NopStrategyTarget) MappedPort(_ context.Context, n nat.Port) (nat.Port, error) {
return n, nil
}
func (st NopStrategyTarget) Logs(_ context.Context) (io.ReadCloser, error) {
return st.ReaderCloser, nil
}
func (st NopStrategyTarget) Exec(_ context.Context, _ []string, _ ...exec.ProcessOption) (int, io.Reader, error) {
return 0, nil, nil
}
func (st NopStrategyTarget) State(_ context.Context) (*types.ContainerState, error) {
return &st.ContainerState, nil
}

View File

@@ -9,52 +9,92 @@ import (
"github.com/docker/go-connections/nat"
)
//ForSQL constructs a new waitForSql strategy for the given driver
func ForSQL(port nat.Port, driver string, url func(nat.Port) string) *waitForSql {
var _ Strategy = (*waitForSql)(nil)
var _ StrategyTimeout = (*waitForSql)(nil)
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 {
return &waitForSql{
Port: port,
URL: url,
Driver: driver,
startupTimeout: defaultStartupTimeout(),
PollInterval: defaultPollInterval(),
query: defaultForSqlQuery,
}
}
type waitForSql struct {
URL func(port nat.Port) string
timeout *time.Duration
URL func(host string, port nat.Port) string
Driver string
Port nat.Port
startupTimeout time.Duration
PollInterval time.Duration
query string
}
//Timeout sets the maximum waiting time for the strategy after which it'll give up and return an error
func (w *waitForSql) Timeout(duration time.Duration) *waitForSql {
w.startupTimeout = duration
// WithStartupTimeout can be used to change the default startup timeout
func (w *waitForSql) WithStartupTimeout(timeout time.Duration) *waitForSql {
w.timeout = &timeout
return w
}
//WithPollInterval can be used to override the default polling interval of 100 milliseconds
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (w *waitForSql) WithPollInterval(pollInterval time.Duration) *waitForSql {
w.PollInterval = pollInterval
return w
}
//WaitUntilReady repeatedly tries to run "SELECT 1" query on the given port using sql and driver.
// If the it doesn't succeed until the timeout value which defaults to 60 seconds, it will return an error
// WithQuery can be used to override the default query used in the strategy.
func (w *waitForSql) WithQuery(query string) *waitForSql {
w.query = query
return w
}
func (w *waitForSql) Timeout() *time.Duration {
return w.timeout
}
// WaitUntilReady repeatedly tries to run "SELECT 1" or user defined query on the given port using sql and driver.
//
// If it doesn't succeed until the timeout value which defaults to 60 seconds, it will return an error.
func (w *waitForSql) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
ctx, cancel := context.WithTimeout(ctx, w.startupTimeout)
timeout := defaultStartupTimeout()
if w.timeout != nil {
timeout = *w.timeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
host, err := target.Host(ctx)
if err != nil {
return
}
ticker := time.NewTicker(w.PollInterval)
defer ticker.Stop()
port, err := target.MappedPort(ctx, w.Port)
if err != nil {
return fmt.Errorf("target.MappedPort: %v", err)
var port nat.Port
port, err = target.MappedPort(ctx, w.Port)
for port == "" {
select {
case <-ctx.Done():
return fmt.Errorf("%s:%w", ctx.Err(), err)
case <-ticker.C:
if err := checkTarget(ctx, target); err != nil {
return err
}
port, err = target.MappedPort(ctx, w.Port)
}
}
db, err := sql.Open(w.Driver, w.URL(port))
db, err := sql.Open(w.Driver, w.URL(host, port))
if err != nil {
return fmt.Errorf("sql.Open: %v", err)
}
@@ -64,8 +104,10 @@ func (w *waitForSql) WaitUntilReady(ctx context.Context, target StrategyTarget)
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
if _, err := db.ExecContext(ctx, "SELECT 1"); err != nil {
if err := checkTarget(ctx, target); err != nil {
return err
}
if _, err := db.ExecContext(ctx, w.query); err != nil {
continue
}
return nil

View File

@@ -2,21 +2,55 @@ package wait
import (
"context"
"errors"
"fmt"
"io"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go/exec"
)
// Strategy defines the basic interface for a Wait Strategy
type Strategy interface {
WaitUntilReady(context.Context, StrategyTarget) error
}
// StrategyTimeout allows MultiStrategy to configure a Strategy's Timeout
type StrategyTimeout interface {
Timeout() *time.Duration
}
type StrategyTarget interface {
Host(context.Context) (string, error)
Ports(ctx context.Context) (nat.PortMap, error)
MappedPort(context.Context, nat.Port) (nat.Port, error)
Logs(context.Context) (io.ReadCloser, error)
Exec(ctx context.Context, cmd []string) (int, error)
Exec(context.Context, []string, ...exec.ProcessOption) (int, io.Reader, error)
State(context.Context) (*types.ContainerState, error)
}
func checkTarget(ctx context.Context, target StrategyTarget) error {
state, err := target.State(ctx)
if err != nil {
return err
}
return checkState(state)
}
func checkState(state *types.ContainerState) error {
switch {
case state.Running:
return nil
case state.OOMKilled:
return errors.New("container crashed with out-of-memory (OOMKilled)")
case state.Status == "exited":
return fmt.Errorf("container exited with code %d", state.ExitCode)
default:
return fmt.Errorf("unexpected container status %q", state.Status)
}
}
func defaultStartupTimeout() time.Duration {