From 3a0853f7e386257ecf2721283f721f5dba861038 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Wed, 17 Jun 2026 20:56:16 +0100 Subject: [PATCH 1/4] Replace ESM-only archive and port dependencies --- package-lock.json | 166 ++++++++++----- .../src/azure-cosmosdb-emulator-container.ts | 13 +- .../azurite/src/azurite-container.test.ts | 9 +- packages/modules/couchbase/package.json | 3 +- .../couchbase/src/couchbase-container.test.ts | 4 +- packages/testcontainers/package.json | 5 +- .../generic-container.test.ts | 53 ++++- .../generic-container/generic-container.ts | 32 +-- .../started-generic-container.ts | 21 +- packages/testcontainers/src/index.ts | 2 +- .../port-forwarder-reuse.test.ts | 10 +- .../src/port-forwarder/port-forwarder.test.ts | 10 +- .../testcontainers/src/reaper/reaper.test.ts | 6 +- .../src/utils/port-generator.test.ts | 10 +- .../src/utils/port-generator.ts | 26 ++- .../src/utils/tar-archive.test.ts | 95 +++++++++ .../testcontainers/src/utils/tar-archive.ts | 201 ++++++++++++++++++ 17 files changed, 514 insertions(+), 152 deletions(-) create mode 100644 packages/testcontainers/src/utils/tar-archive.test.ts create mode 100644 packages/testcontainers/src/utils/tar-archive.ts diff --git a/package-lock.json b/package-lock.json index 11cb91924..001c48ff4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3971,6 +3971,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -3987,6 +3988,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3999,6 +4001,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -4692,6 +4695,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "optional": true, "engines": { "node": ">=14" @@ -5837,15 +5841,6 @@ "@types/node": "*" } }, - "node_modules/@types/readdir-glob": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", - "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/request": { "version": "2.48.13", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", @@ -5990,10 +5985,11 @@ } }, "node_modules/@types/tar-stream": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-3.1.3.tgz", - "integrity": "sha512-Zbnx4wpkWBMBSu5CytMbrT5ZpMiF55qgM+EpHzR4yIDu7mv52cej8hTkOc6K+LzpkOAbxwn/m7j3iO+/l42YkQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-3.1.4.tgz", + "integrity": "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -6498,6 +6494,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -6665,6 +6662,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, "license": "MIT", "dependencies": { "archiver-utils": "^5.0.2", @@ -6683,6 +6681,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", @@ -6700,6 +6699,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -6709,6 +6709,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, "funding": [ { "type": "github", @@ -6732,6 +6733,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -6752,6 +6754,7 @@ "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" @@ -6767,6 +6770,7 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", @@ -6783,6 +6787,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, "funding": [ { "type": "github", @@ -6806,6 +6811,7 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", @@ -6878,7 +6884,8 @@ "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true }, "node_modules/async-lock": { "version": "1.4.1", @@ -6971,25 +6978,26 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/bare-events": { "version": "2.5.4", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", - "license": "Apache-2.0", - "optional": true + "license": "Apache-2.0" }, "node_modules/bare-fs": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.4.tgz", - "integrity": "sha512-r8+26Voz8dGX3AYpJdFb1ZPaUSM8XOLCZvy+YGpRTmwPHIxA7Z3Jov/oMPtV7hfRQbOnH8qGlLTzQAbgtdNN0Q==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", + "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", - "bare-stream": "^2.6.4" + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" }, "engines": { "bare": ">=1.16.0" @@ -7008,7 +7016,6 @@ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", "license": "Apache-2.0", - "optional": true, "engines": { "bare": ">=1.14.0" } @@ -7018,7 +7025,6 @@ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-os": "^3.0.1" } @@ -7028,7 +7034,6 @@ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", "license": "Apache-2.0", - "optional": true, "dependencies": { "streamx": "^2.21.0" }, @@ -7045,6 +7050,15 @@ } } }, + "node_modules/bare-url": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -7271,6 +7285,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, "engines": { "node": ">=8.0.0" } @@ -7948,6 +7963,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", @@ -7963,6 +7979,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, "funding": [ { "type": "github", @@ -7986,6 +8003,7 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", @@ -8054,7 +8072,8 @@ "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true }, "node_modules/couchbase": { "version": "4.7.0", @@ -8108,6 +8127,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, "bin": { "crc32": "bin/crc32.njs" }, @@ -8119,6 +8139,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" @@ -8131,6 +8152,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, "funding": [ { "type": "github", @@ -8154,6 +8176,7 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", @@ -8185,6 +8208,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -8521,7 +8545,8 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", @@ -8542,7 +8567,8 @@ "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/end-of-stream": { "version": "1.4.4", @@ -8972,6 +8998,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, "engines": { "node": ">=6" } @@ -8987,6 +9014,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, "engines": { "node": ">=0.8.x" } @@ -9707,6 +9735,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -9722,6 +9751,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "engines": { "node": ">=14" }, @@ -9977,6 +10007,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==", + "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -10954,6 +10985,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "engines": { "node": ">=8" }, @@ -11008,7 +11040,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/isomorphic-ws": { "version": "5.0.0", @@ -11062,6 +11095,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -11397,6 +11431,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, "dependencies": { "readable-stream": "^2.0.5" }, @@ -11407,12 +11442,14 @@ "node_modules/lazystream/node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true }, "node_modules/lazystream/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -11426,12 +11463,14 @@ "node_modules/lazystream/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -11876,6 +11915,7 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -12070,6 +12110,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, "license": "ISC" }, "node_modules/lru-memoizer": { @@ -12361,6 +12402,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -13081,6 +13123,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -13418,7 +13461,8 @@ "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true }, "node_modules/pako": { "version": "1.0.11", @@ -13455,6 +13499,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -13470,6 +13515,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -13764,6 +13810,7 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, "engines": { "node": ">= 0.6.0" } @@ -13771,7 +13818,8 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true }, "node_modules/proper-lockfile": { "version": "4.1.2", @@ -14062,6 +14110,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, "dependencies": { "minimatch": "^5.1.0" } @@ -14070,6 +14119,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -14079,6 +14129,7 @@ "version": "5.1.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -14581,6 +14632,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -14592,6 +14644,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -15179,6 +15232,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -15196,6 +15250,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -15208,12 +15263,14 @@ "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -15222,6 +15279,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -15234,6 +15292,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -15260,6 +15319,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -15426,11 +15486,13 @@ } }, "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "license": "MIT", "dependencies": { "b4a": "^1.6.4", + "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } @@ -16363,6 +16425,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -16472,6 +16535,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -16489,6 +16553,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -16504,12 +16569,14 @@ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -16518,6 +16585,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -16531,6 +16599,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -16543,6 +16612,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "engines": { "node": ">=12" }, @@ -16554,6 +16624,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -16758,6 +16829,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", @@ -16771,6 +16843,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, "funding": [ { "type": "github", @@ -16794,6 +16867,7 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", @@ -16913,8 +16987,7 @@ "testcontainers": "^12.0.3" }, "devDependencies": { - "couchbase": "^4.7.0", - "get-port": "^7.2.0" + "couchbase": "^4.7.0" } }, "packages/modules/couchdb": { @@ -17327,41 +17400,30 @@ "dependencies": { "@balena/dockerignore": "^1.0.2", "@types/dockerode": "^4.0.1", - "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.4.3", "docker-compose": "^1.4.2", "dockerode": "^5.0.0", - "get-port": "^7.2.0", "proper-lockfile": "^4.1.2", "properties-reader": "^3.0.1", "ssh-remote-port-forward": "^1.0.4", "tar-fs": "^3.1.2", + "tar-stream": "^3.2.0", "tmp": "^0.2.7", "undici": "^8.3.0" }, "devDependencies": { - "@types/archiver": "^7.0.0", "@types/async-lock": "^1.4.2", "@types/byline": "^4.2.36", "@types/debug": "^4.1.13", "@types/proper-lockfile": "^4.1.4", "@types/properties-reader": "^2.1.3", "@types/tar-fs": "^2.0.4", + "@types/tar-stream": "^3.1.4", "@types/tmp": "^0.2.6" } }, - "packages/testcontainers/node_modules/@types/archiver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", - "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/readdir-glob": "*" - } - }, "packages/testcontainers/node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", diff --git a/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts b/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts index 5cfc257d3..8f80ecbd9 100644 --- a/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts +++ b/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts @@ -1,11 +1,4 @@ -import { - AbstractStartedContainer, - GenericContainer, - PortGenerator, - RandomPortGenerator, - StartedTestContainer, - Wait, -} from "testcontainers"; +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait, getRandomPort } from "testcontainers"; type Protocol = "http" | "https"; @@ -21,11 +14,9 @@ export class AzureCosmosDbEmulatorContainer extends GenericContainer { private protocol: Protocol = DEFAULT_PROTOCOL; private telemetryEnabled = DEFAULT_TELEMETRY_ENABLED; private explorerEnabled = DEFAULT_EXPLORER_ENABLED; - private portGenerator: PortGenerator; constructor(image: string) { super(image); - this.portGenerator = new RandomPortGenerator(); this.withWaitStrategy(Wait.forLogMessage(COSMOS_READY_LOG_MESSAGE)); } @@ -40,7 +31,7 @@ export class AzureCosmosDbEmulatorContainer extends GenericContainer { } public override async start(): Promise { - const port = await this.portGenerator.generatePort(); + const port = await getRandomPort(); this.withExposedPorts({ host: port, container: port, diff --git a/packages/modules/azurite/src/azurite-container.test.ts b/packages/modules/azurite/src/azurite-container.test.ts index 3bb92753e..b294a4143 100644 --- a/packages/modules/azurite/src/azurite-container.test.ts +++ b/packages/modules/azurite/src/azurite-container.test.ts @@ -3,7 +3,7 @@ import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-bl import { QueueServiceClient } from "@azure/storage-queue"; import fs from "node:fs"; import path from "node:path"; -import { RandomPortGenerator } from "testcontainers"; +import { getRandomPort } from "testcontainers"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { AzuriteContainer } from "./azurite-container"; import { createOAuthToken, createTokenCredential, getTlsPipelineOptions } from "./azurite-test-utils"; @@ -99,10 +99,9 @@ describe("AzuriteContainer", { timeout: 240_000 }, () => { it("should be able to specify custom ports", async () => { // customPorts { - const portGenerator = new RandomPortGenerator(); - const blobPort = await portGenerator.generatePort(); - const queuePort = await portGenerator.generatePort(); - const tablePort = await portGenerator.generatePort(); + const blobPort = await getRandomPort(); + const queuePort = await getRandomPort(); + const tablePort = await getRandomPort(); await using container = await new AzuriteContainer(IMAGE) .withSkipApiVersionCheck() diff --git a/packages/modules/couchbase/package.json b/packages/modules/couchbase/package.json index 3c94e6aff..291742133 100644 --- a/packages/modules/couchbase/package.json +++ b/packages/modules/couchbase/package.json @@ -32,7 +32,6 @@ "testcontainers": "^12.0.3" }, "devDependencies": { - "couchbase": "^4.7.0", - "get-port": "^7.2.0" + "couchbase": "^4.7.0" } } diff --git a/packages/modules/couchbase/src/couchbase-container.test.ts b/packages/modules/couchbase/src/couchbase-container.test.ts index d93c44ba8..c1d3e4b91 100644 --- a/packages/modules/couchbase/src/couchbase-container.test.ts +++ b/packages/modules/couchbase/src/couchbase-container.test.ts @@ -1,5 +1,5 @@ import couchbase, { Bucket, Cluster } from "couchbase"; -import getPort from "get-port"; +import { getRandomPort } from "testcontainers"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { BucketDefinition } from "./bucket-definition"; import { CouchbaseContainer } from "./couchbase-container"; @@ -118,7 +118,7 @@ describe("CouchbaseContainer", { timeout: 180_000 }, () => { }); it("should preserve fixed host port binding", async () => { - const hostPort = await getPort(); + const hostPort = await getRandomPort(); await using container = await new CouchbaseContainer(COMMUNITY_IMAGE) .withEnabledServices(CouchbaseService.KV) .withExposedPorts({ container: PORTS.MGMT_PORT, host: hostPort }) diff --git a/packages/testcontainers/package.json b/packages/testcontainers/package.json index ab0668fb8..3644e76b0 100644 --- a/packages/testcontainers/package.json +++ b/packages/testcontainers/package.json @@ -32,28 +32,27 @@ "dependencies": { "@balena/dockerignore": "^1.0.2", "@types/dockerode": "^4.0.1", - "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.4.3", "docker-compose": "^1.4.2", "dockerode": "^5.0.0", - "get-port": "^7.2.0", "proper-lockfile": "^4.1.2", "properties-reader": "^3.0.1", "ssh-remote-port-forward": "^1.0.4", "tar-fs": "^3.1.2", + "tar-stream": "^3.2.0", "tmp": "^0.2.7", "undici": "^8.3.0" }, "devDependencies": { - "@types/archiver": "^7.0.0", "@types/async-lock": "^1.4.2", "@types/byline": "^4.2.36", "@types/debug": "^4.1.13", "@types/proper-lockfile": "^4.1.4", "@types/properties-reader": "^2.1.3", "@types/tar-fs": "^2.0.4", + "@types/tar-stream": "^3.1.4", "@types/tmp": "^0.2.6" } } diff --git a/packages/testcontainers/src/generic-container/generic-container.test.ts b/packages/testcontainers/src/generic-container/generic-container.test.ts index 2e7f999a0..e2394d7a7 100644 --- a/packages/testcontainers/src/generic-container/generic-container.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container.test.ts @@ -1,8 +1,8 @@ -import archiver from "archiver"; -import getPort from "get-port"; import path from "path"; +import * as tarStream from "tar-stream"; import { RandomUuid } from "../common"; import { getContainerRuntimeClient } from "../container-runtime"; +import { getRandomPort } from "../utils/port-generator"; import { PullPolicy } from "../utils/pull-policy"; import { checkContainerIsHealthy, @@ -38,7 +38,7 @@ describe("GenericContainer", { timeout: 180_000 }, () => { }); it("should bind to specified host port", async () => { - const hostPort = await getPort(); + const hostPort = await getRandomPort(); await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") .withExposedPorts({ container: 8080, @@ -51,7 +51,7 @@ describe("GenericContainer", { timeout: 180_000 }, () => { }); it("should bind to specified host port with a different protocol", async () => { - const hostPort = await getPort(); + const hostPort = await getRandomPort(); await using container = await new GenericContainer("mendhak/udp-listener") .withWaitStrategy(Wait.forLogMessage("Listening on UDP port 5005")) .withExposedPorts({ @@ -473,6 +473,19 @@ describe("GenericContainer", { timeout: 180_000 }, () => { expect((await container.exec("cat /tmp/test.txt")).output).toEqual(expect.stringContaining("hello world")); }); + it("should copy directory to started container with permissions", async () => { + const source = path.resolve(fixtures, "docker"); + const target = "/tmp/started-newdir"; + const mode = parseInt("0777", 8); + await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") + .withExposedPorts(8080) + .start(); + + await container.copyDirectoriesToContainer([{ source, target, mode }]); + + expect((await container.exec(`stat -c "%a %n" /tmp/started-newdir/test.txt`)).output).toContain("777"); + }); + it("should copy content to container", async () => { const content = "hello world"; const target = "/tmp/test.txt"; @@ -521,9 +534,7 @@ describe("GenericContainer", { timeout: 180_000 }, () => { .withExposedPorts(8080) .start(); - const tar = archiver("tar"); - tar.append("hello world", { name: targetWithCopyOwnership.slice(1), uid, gid } as archiver.EntryData); - tar.finalize(); + const tar = createArchiveWithOwnership(targetWithCopyOwnership, uid, gid); await container.copyArchiveToContainer(tar, "/", { copyUIDGID: true }); @@ -536,9 +547,7 @@ describe("GenericContainer", { timeout: 180_000 }, () => { const uid = 4242; const gid = 4343; const targetWithCopyOwnership = "/tmp/with-copy-archives-copyuidgid.txt"; - const tar = archiver("tar"); - tar.append("hello world", { name: targetWithCopyOwnership.slice(1), uid, gid } as archiver.EntryData); - tar.finalize(); + const tar = createArchiveWithOwnership(targetWithCopyOwnership, uid, gid); await using containerWithCopyOwnership = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") .withCopyArchivesToContainer([ @@ -672,3 +681,27 @@ describe("GenericContainer", { timeout: 180_000 }, () => { await GenericContainer.fromDockerfile(context).withTarget("invalid").build(); }); }); + +const createArchiveWithOwnership = (target: string, uid: number, gid: number) => { + const content = "hello world"; + const tar = tarStream.pack(); + + tar.entry( + { + name: target.slice(1), + uid, + gid, + size: Buffer.byteLength(content), + }, + content, + (err) => { + if (err) { + tar.destroy(err); + } else { + tar.finalize(); + } + } + ); + + return tar; +}; diff --git a/packages/testcontainers/src/generic-container/generic-container.ts b/packages/testcontainers/src/generic-container/generic-container.ts index 7d8aba8f5..a914c0b62 100644 --- a/packages/testcontainers/src/generic-container/generic-container.ts +++ b/packages/testcontainers/src/generic-container/generic-container.ts @@ -1,7 +1,5 @@ -import archiver from "archiver"; import AsyncLock from "async-lock"; import { Container, ContainerCreateOptions, ContainerInspectInfo, HostConfig } from "dockerode"; -import { promises as fs } from "fs"; import { Readable } from "stream"; import { containerLog, hash, log, toNanos } from "../common"; import { ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime"; @@ -31,6 +29,7 @@ import { createLabels, LABEL_TESTCONTAINERS_CONTAINER_HASH, LABEL_TESTCONTAINERS import { mapInspectResult } from "../utils/map-inspect-result"; import { getContainerPort, getProtocol, hasHostBinding, PortWithOptionalBinding } from "../utils/port"; import { ImagePullPolicy, PullPolicy } from "../utils/pull-policy"; +import { createTarArchive } from "../utils/tar-archive"; import { selectWaitStrategy } from "../wait-strategies/utils/wait-strategy-selector"; import { waitForContainer } from "../wait-strategies/wait-for-container"; import { WaitStrategy } from "../wait-strategies/wait-strategy"; @@ -198,8 +197,11 @@ export class GenericContainer implements TestContainer { } if (this.filesToCopy.length > 0 || this.directoriesToCopy.length > 0 || this.contentsToCopy.length > 0) { - const archive = await this.createArchiveToCopyToContainer(); - archive.finalize(); + const archive = await createTarArchive({ + filesToCopy: this.filesToCopy, + directoriesToCopy: this.directoriesToCopy, + contentsToCopy: this.contentsToCopy, + }); await client.container.putArchive(container, archive, "/", this.copyToContainerOptions); } @@ -275,28 +277,6 @@ export class GenericContainer implements TestContainer { } } - private async createArchiveToCopyToContainer(): Promise { - const tar = archiver("tar"); - const filesToCopyWithStats = await Promise.all( - this.filesToCopy.map(async (fileToCopy) => ({ - ...fileToCopy, - stats: await fs.stat(fileToCopy.source), - })) - ); - - for (const { source, target, mode, stats } of filesToCopyWithStats) { - tar.file(source, { name: target, mode, stats }); - } - for (const { source, target, mode } of this.directoriesToCopy) { - tar.directory(source, target, { mode }); - } - for (const { content, target, mode } of this.contentsToCopy) { - tar.append(content, { name: target, mode }); - } - - return tar; - } - protected containerStarted?( container: StartedTestContainer, inspectResult: InspectResult, diff --git a/packages/testcontainers/src/generic-container/started-generic-container.ts b/packages/testcontainers/src/generic-container/started-generic-container.ts index 1b9c48421..bb2e64747 100644 --- a/packages/testcontainers/src/generic-container/started-generic-container.ts +++ b/packages/testcontainers/src/generic-container/started-generic-container.ts @@ -1,7 +1,5 @@ -import archiver from "archiver"; import AsyncLock from "async-lock"; import Dockerode, { ContainerInspectInfo } from "dockerode"; -import { promises as fs } from "fs"; import { Readable } from "stream"; import { containerLog, log } from "../common"; import { ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime"; @@ -21,6 +19,7 @@ import { BoundPorts } from "../utils/bound-ports"; import { LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels"; import { mapInspectResult } from "../utils/map-inspect-result"; import { PortWithOptionalBinding } from "../utils/port"; +import { createTarArchive } from "../utils/tar-archive"; import { waitForContainer } from "../wait-strategies/wait-for-container"; import { WaitStrategy } from "../wait-strategies/wait-strategy"; import { inspectContainerUntilPortsExposed } from "./inspect-container-util-ports-exposed"; @@ -192,15 +191,7 @@ export class StartedGenericContainer implements StartedTestContainer { public async copyFilesToContainer(filesToCopy: FileToCopy[]): Promise { log.debug(`Copying files to container...`, { containerId: this.container.id }); const client = await getContainerRuntimeClient(); - const tar = archiver("tar"); - const filesToCopyWithStats = await Promise.all( - filesToCopy.map(async (fileToCopy) => ({ - ...fileToCopy, - stats: await fs.stat(fileToCopy.source), - })) - ); - filesToCopyWithStats.forEach(({ source, target, mode, stats }) => tar.file(source, { name: target, mode, stats })); - tar.finalize(); + const tar = await createTarArchive({ filesToCopy }); await client.container.putArchive(this.container, tar, "/"); log.debug(`Copied files to container`, { containerId: this.container.id }); } @@ -208,9 +199,7 @@ export class StartedGenericContainer implements StartedTestContainer { public async copyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): Promise { log.debug(`Copying directories to container...`, { containerId: this.container.id }); const client = await getContainerRuntimeClient(); - const tar = archiver("tar"); - directoriesToCopy.forEach(({ source, target }) => tar.directory(source, target)); - tar.finalize(); + const tar = await createTarArchive({ directoriesToCopy }); await client.container.putArchive(this.container, tar, "/"); log.debug(`Copied directories to container`, { containerId: this.container.id }); } @@ -218,9 +207,7 @@ export class StartedGenericContainer implements StartedTestContainer { public async copyContentToContainer(contentsToCopy: ContentToCopy[]): Promise { log.debug(`Copying content to container...`, { containerId: this.container.id }); const client = await getContainerRuntimeClient(); - const tar = archiver("tar"); - contentsToCopy.forEach(({ content, target, mode }) => tar.append(content, { name: target, mode: mode })); - tar.finalize(); + const tar = await createTarArchive({ contentsToCopy }); await client.container.putArchive(this.container, tar, "/"); log.debug(`Copied content to container`, { containerId: this.container.id }); } diff --git a/packages/testcontainers/src/index.ts b/packages/testcontainers/src/index.ts index 8997e8153..557f8e46c 100644 --- a/packages/testcontainers/src/index.ts +++ b/packages/testcontainers/src/index.ts @@ -23,7 +23,7 @@ export { CommitOptions, Content, CopyToContainerOptions, ExecOptions, ExecResult export { BoundPorts } from "./utils/bound-ports"; export { LABEL_TESTCONTAINERS_SESSION_ID } from "./utils/labels"; export { PortWithBinding, PortWithOptionalBinding, getContainerPort, hasHostBinding } from "./utils/port"; -export { PortGenerator, RandomPortGenerator } from "./utils/port-generator"; +export { PortGenerator, RandomPortGenerator, getRandomPort } from "./utils/port-generator"; export { ImagePullPolicy, PullPolicy } from "./utils/pull-policy"; export { HttpWaitStrategyOptions } from "./wait-strategies/http-wait-strategy"; export { StartupCheckStrategy, StartupStatus } from "./wait-strategies/startup-check-strategy"; diff --git a/packages/testcontainers/src/port-forwarder/port-forwarder-reuse.test.ts b/packages/testcontainers/src/port-forwarder/port-forwarder-reuse.test.ts index 607ec95f6..de95c9ac0 100644 --- a/packages/testcontainers/src/port-forwarder/port-forwarder-reuse.test.ts +++ b/packages/testcontainers/src/port-forwarder/port-forwarder-reuse.test.ts @@ -1,14 +1,12 @@ import { GenericContainer } from "../generic-container/generic-container"; -import { RandomPortGenerator } from "../utils/port-generator"; +import { getRandomPort } from "../utils/port-generator"; import { createTestServer } from "../utils/test-helper"; describe.sequential("Port Forwarder reuse", { timeout: 180_000 }, () => { - const portGen = new RandomPortGenerator(); - it("should expose additional ports", async () => { const { TestContainers: TC1 } = await import("../test-containers.js"); const { PortForwarderInstance: PFI1 } = await import("../port-forwarder/port-forwarder.js"); - const port1 = await portGen.generatePort(); + const port1 = await getRandomPort(); const server1 = await createTestServer(port1); await TC1.exposeHostPorts(port1); const portForwarder1ContainerId = (await PFI1.getInstance()).getContainerId(); @@ -16,7 +14,7 @@ describe.sequential("Port Forwarder reuse", { timeout: 180_000 }, () => { vi.resetModules(); const { TestContainers: TC2 } = await import("../test-containers.js"); const { PortForwarderInstance: PFI2 } = await import("../port-forwarder/port-forwarder.js"); - const port2 = await portGen.generatePort(); + const port2 = await getRandomPort(); const server2 = await createTestServer(port2); await TC2.exposeHostPorts(port2); const portForwarder2ContainerId = (await PFI2.getInstance()).getContainerId(); @@ -34,7 +32,7 @@ describe.sequential("Port Forwarder reuse", { timeout: 180_000 }, () => { }); it("should reuse same ports", async () => { - const port = await portGen.generatePort(); + const port = await getRandomPort(); const server = await createTestServer(port); const { TestContainers: TC1 } = await import("../test-containers.js"); diff --git a/packages/testcontainers/src/port-forwarder/port-forwarder.test.ts b/packages/testcontainers/src/port-forwarder/port-forwarder.test.ts index 21f0defde..57fe65ccf 100644 --- a/packages/testcontainers/src/port-forwarder/port-forwarder.test.ts +++ b/packages/testcontainers/src/port-forwarder/port-forwarder.test.ts @@ -1,14 +1,12 @@ import { GenericContainer } from "../generic-container/generic-container"; import { Network } from "../network/network"; import { TestContainers } from "../test-containers"; -import { RandomPortGenerator } from "../utils/port-generator"; +import { getRandomPort } from "../utils/port-generator"; import { createTestServer } from "../utils/test-helper"; describe("PortForwarder", { timeout: 180_000 }, () => { - const portGen = new RandomPortGenerator(); - it("should expose host ports to the container", async () => { - const randomPort = await portGen.generatePort(); + const randomPort = await getRandomPort(); await using _ = await createTestServer(randomPort); await TestContainers.exposeHostPorts(randomPort); @@ -19,7 +17,7 @@ describe("PortForwarder", { timeout: 180_000 }, () => { }); it("should expose host ports to the container with custom network", async () => { - const randomPort = await portGen.generatePort(); + const randomPort = await getRandomPort(); await using _ = await createTestServer(randomPort); await TestContainers.exposeHostPorts(randomPort); @@ -33,7 +31,7 @@ describe("PortForwarder", { timeout: 180_000 }, () => { }); it("should expose host ports to the container with custom network and network alias", async () => { - const randomPort = await portGen.generatePort(); + const randomPort = await getRandomPort(); await using _ = await createTestServer(randomPort); await TestContainers.exposeHostPorts(randomPort); diff --git a/packages/testcontainers/src/reaper/reaper.test.ts b/packages/testcontainers/src/reaper/reaper.test.ts index 7fa2c10ac..b0ed658f8 100644 --- a/packages/testcontainers/src/reaper/reaper.test.ts +++ b/packages/testcontainers/src/reaper/reaper.test.ts @@ -1,5 +1,5 @@ import { ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime"; -import { RandomPortGenerator } from "../utils/port-generator"; +import { getRandomPort } from "../utils/port-generator"; describe.sequential("Reaper", { timeout: 120_000 }, () => { let client: ContainerRuntimeClient; @@ -56,7 +56,7 @@ describe.sequential("Reaper", { timeout: 120_000 }, () => { it("should create new reaper container when existing reaper cannot be reached", async () => { const reaper = await getReaper(); vi.resetModules(); - const unreachablePort = await new RandomPortGenerator().generatePort(); + const unreachablePort = await getRandomPort(); const reaperContainerInfo = (await client.container.list()).filter((c) => c.Id === reaper.containerId)[0]; reaperContainerInfo.Labels["TESTCONTAINERS_RYUK_TEST_LABEL"] = "false"; const reaperPort = reaperContainerInfo.Ports.find((port) => port.PrivatePort == 8080); @@ -72,7 +72,7 @@ describe.sequential("Reaper", { timeout: 120_000 }, () => { }); it("should use custom port when TESTCONTAINERS_RYUK_PORT is set", async () => { - const customPort = (await new RandomPortGenerator().generatePort()).toString(); + const customPort = (await getRandomPort()).toString(); vi.stubEnv("TESTCONTAINERS_RYUK_PORT", customPort); vi.spyOn(client.container, "list").mockResolvedValue([]); diff --git a/packages/testcontainers/src/utils/port-generator.test.ts b/packages/testcontainers/src/utils/port-generator.test.ts index 56d52e122..b2e8c5cc0 100644 --- a/packages/testcontainers/src/utils/port-generator.test.ts +++ b/packages/testcontainers/src/utils/port-generator.test.ts @@ -1,4 +1,4 @@ -import { FixedPortGenerator, RandomPortGenerator } from "./port-generator"; +import { FixedPortGenerator, getRandomPort } from "./port-generator"; describe("PortGenerator", () => { describe("FixedPortGenerator", () => { @@ -10,12 +10,10 @@ describe("PortGenerator", () => { }); }); - describe("RandomPortGenerator", () => { + describe("getRandomPort", () => { it("should generate a random available port", async () => { - const randomPortGenerator = new RandomPortGenerator(); - - const port1 = await randomPortGenerator.generatePort(); - const port2 = await randomPortGenerator.generatePort(); + const port1 = await getRandomPort(); + const port2 = await getRandomPort(); expect(port1).toBeDefined(); expect(port2).toBeDefined(); diff --git a/packages/testcontainers/src/utils/port-generator.ts b/packages/testcontainers/src/utils/port-generator.ts index c03b8359a..a46678143 100644 --- a/packages/testcontainers/src/utils/port-generator.ts +++ b/packages/testcontainers/src/utils/port-generator.ts @@ -1,14 +1,36 @@ +import net from "net"; + export interface PortGenerator { generatePort(): Promise; } export class RandomPortGenerator { public async generatePort(): Promise { - const { default: getPort } = await import("get-port"); - return getPort(); + const server = net.createServer(); + server.unref(); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, () => resolve()); + }); + + const address = server.address(); + if (address === null || typeof address === "string") { + throw new Error("Failed to allocate a random TCP port"); + } + + await new Promise((resolve, reject) => { + server.close((err) => (err ? reject(err) : resolve())); + }); + + return address.port; } } +const randomPortGenerator = new RandomPortGenerator(); + +export const getRandomPort = (): Promise => randomPortGenerator.generatePort(); + export class FixedPortGenerator implements PortGenerator { private portIndex = 0; diff --git a/packages/testcontainers/src/utils/tar-archive.test.ts b/packages/testcontainers/src/utils/tar-archive.test.ts new file mode 100644 index 000000000..dcae25517 --- /dev/null +++ b/packages/testcontainers/src/utils/tar-archive.test.ts @@ -0,0 +1,95 @@ +import { promises as fs } from "fs"; +import { tmpdir } from "os"; +import path from "path"; +import { Readable } from "stream"; +import * as tarStream from "tar-stream"; +import { createTarArchive } from "./tar-archive"; + +describe("createTarArchive", () => { + it("should reject if a file to copy cannot be read", async () => { + await expect( + createTarArchive({ + filesToCopy: [{ source: path.join(__dirname, "does-not-exist"), target: "/tmp/does-not-exist" }], + }) + ).rejects.toThrow(); + }); + + it("should normalize Windows-style target paths", async () => { + const source = await fs.mkdtemp(path.join(tmpdir(), "testcontainers-tar-archive-")); + await fs.writeFile(path.join(source, "test.txt"), "hello world"); + + try { + const archive = await createTarArchive({ + directoriesToCopy: [{ source, target: "\\tmp\\windows-dir" }], + }); + + const entries = await getTarEntries(archive); + + expect(entries.map((entry) => entry.name)).toEqual(["tmp/windows-dir/test.txt"]); + } finally { + await fs.rm(source, { recursive: true, force: true }); + } + }); + + it("should preserve symlinks in copied directories without following them", async () => { + const source = await fs.mkdtemp(path.join(tmpdir(), "testcontainers-tar-archive-")); + const outside = await fs.mkdtemp(path.join(tmpdir(), "testcontainers-tar-archive-outside-")); + const outsideFile = path.join(outside, "secret.txt"); + await fs.writeFile(outsideFile, "do not archive"); + + try { + const fileSymlinkCreated = await createSymlinkOrSkip(outsideFile, path.join(source, "linked-file"), "file"); + const directorySymlinkCreated = await createSymlinkOrSkip(outside, path.join(source, "linked-dir"), "dir"); + if (!fileSymlinkCreated || !directorySymlinkCreated) { + return; + } + + const archive = await createTarArchive({ + directoriesToCopy: [{ source, target: "/tmp/symlinks" }], + }); + + const entries = await getTarEntries(archive); + + expect(entries.map((entry) => entry.name)).not.toContain("tmp/symlinks/linked-dir/secret.txt"); + expect(entries.map(({ linkname, name, type }) => ({ linkname, name, type })).sort(byName)).toEqual([ + { linkname: outside, name: "tmp/symlinks/linked-dir", type: "symlink" }, + { linkname: outsideFile, name: "tmp/symlinks/linked-file", type: "symlink" }, + ]); + } finally { + await fs.rm(source, { recursive: true, force: true }); + await fs.rm(outside, { recursive: true, force: true }); + } + }); +}); + +const createSymlinkOrSkip = async (target: string, symlinkPath: string, type: "dir" | "file"): Promise => { + try { + await fs.symlink(target, symlinkPath, type); + return true; + } catch (err) { + const code = (err as { code?: string }).code; + if (code === "EPERM" || code === "EACCES" || code === "ENOTSUP") { + return false; + } + throw err; + } +}; + +const byName = (a: { name: string }, b: { name: string }): number => a.name.localeCompare(b.name); + +const getTarEntries = (archive: Readable): Promise => + new Promise((resolve, reject) => { + const extract = tarStream.extract(); + const entries: tarStream.Headers[] = []; + + extract.on("entry", (header, stream, next) => { + entries.push(header); + stream.resume(); + stream.on("end", () => next()); + stream.on("error", reject); + }); + extract.on("finish", () => resolve(entries)); + extract.on("error", reject); + archive.on("error", reject); + archive.pipe(extract); + }); diff --git a/packages/testcontainers/src/utils/tar-archive.ts b/packages/testcontainers/src/utils/tar-archive.ts new file mode 100644 index 000000000..7f4ab649b --- /dev/null +++ b/packages/testcontainers/src/utils/tar-archive.ts @@ -0,0 +1,201 @@ +import { createReadStream, promises as fs, Stats } from "fs"; +import path from "path"; +import { Readable } from "stream"; +import * as tarStream from "tar-stream"; +import { Content, ContentToCopy, DirectoryToCopy, FileToCopy } from "../types"; + +export type TarArchiveOptions = { + filesToCopy?: FileToCopy[]; + directoriesToCopy?: DirectoryToCopy[]; + contentsToCopy?: ContentToCopy[]; +}; + +type TarArchiveEntry = + | { + header: tarStream.Headers; + source: string; + } + | { + header: tarStream.Headers; + content: Buffer; + }; + +export const createTarArchive = async (options: TarArchiveOptions): Promise => { + const entries = await collectTarArchiveEntries(options); + const tar = tarStream.pack(); + + void populateTarArchive(tar, entries).catch((err) => { + tar.destroy(err instanceof Error ? err : new Error(String(err))); + }); + + return tar; +}; + +const collectTarArchiveEntries = async (options: TarArchiveOptions): Promise => { + const entries: TarArchiveEntry[] = []; + + for (const fileToCopy of options.filesToCopy ?? []) { + entries.push(await createFileArchiveEntry(fileToCopy.source, fileToCopy.target, fileToCopy.mode)); + } + + for (const directoryToCopy of options.directoriesToCopy ?? []) { + await addDirectoryEntries(entries, directoryToCopy.source, directoryToCopy.target, directoryToCopy.mode, false); + } + + for (const contentToCopy of options.contentsToCopy ?? []) { + entries.push(await createContentArchiveEntry(contentToCopy.content, contentToCopy.target, contentToCopy.mode)); + } + + return entries; +}; + +const populateTarArchive = async (tar: tarStream.Pack, entries: TarArchiveEntry[]): Promise => { + for (const entry of entries) { + if ("source" in entry) { + await addFileToArchive(tar, entry.source, entry.header); + } else { + await addBufferEntry(tar, entry.content, entry.header); + } + } + + tar.finalize(); +}; + +const addDirectoryEntries = async ( + entries: TarArchiveEntry[], + source: string, + target: string, + mode: number | undefined, + includeSelf: boolean +): Promise => { + const stats = await fs.lstat(source); + if (stats.isSymbolicLink()) { + entries.push(await createSymlinkArchiveEntry(source, target, stats, mode)); + return; + } + + if (includeSelf) { + entries.push(createDirectoryArchiveEntry(target, stats, mode)); + } + + const directoryEntries = await fs.readdir(source, { withFileTypes: true }); + for (const directoryEntry of directoryEntries) { + const sourcePath = path.join(source, directoryEntry.name); + const targetPath = joinTarPaths(target, directoryEntry.name); + const entryStats = await fs.lstat(sourcePath); + + if (directoryEntry.isSymbolicLink()) { + entries.push(await createSymlinkArchiveEntry(sourcePath, targetPath, entryStats, mode)); + } else if (entryStats.isDirectory()) { + await addDirectoryEntries(entries, sourcePath, targetPath, mode, true); + } else if (entryStats.isFile()) { + entries.push(createFileArchiveEntryFromStats(sourcePath, targetPath, mode, entryStats)); + } + } +}; + +const createDirectoryArchiveEntry = (target: string, stats: Stats, mode: number | undefined): TarArchiveEntry => ({ + content: Buffer.alloc(0), + header: { + name: normalizeTarPath(target), + type: "directory", + mode: getEntryMode(stats, mode), + mtime: stats.mtime, + uid: stats.uid, + gid: stats.gid, + size: 0, + }, +}); + +const createFileArchiveEntry = async (source: string, target: string, mode?: number): Promise => { + const stats = await fs.stat(source); + + return createFileArchiveEntryFromStats(source, target, mode, stats); +}; + +const createFileArchiveEntryFromStats = ( + source: string, + target: string, + mode: number | undefined, + stats: Stats +): TarArchiveEntry => ({ + source, + header: { + name: normalizeTarPath(target), + mode: getEntryMode(stats, mode), + mtime: stats.mtime, + uid: stats.uid, + gid: stats.gid, + size: stats.size, + }, +}); + +const createSymlinkArchiveEntry = async ( + source: string, + target: string, + stats: Stats, + mode: number | undefined +): Promise => ({ + content: Buffer.alloc(0), + header: { + name: normalizeTarPath(target), + type: "symlink", + linkname: await fs.readlink(source), + mode: getEntryMode(stats, mode), + mtime: stats.mtime, + uid: stats.uid, + gid: stats.gid, + size: 0, + }, +}); + +const addFileToArchive = async (tar: tarStream.Pack, source: string, header: tarStream.Headers): Promise => { + await new Promise((resolve, reject) => { + const entry = tar.entry(header, (err) => (err ? reject(err) : resolve())); + const file = createReadStream(source); + + file.on("error", reject); + entry.on("error", reject); + file.pipe(entry); + }); +}; + +const createContentArchiveEntry = async (content: Content, target: string, mode?: number): Promise => { + const buffer = await toBuffer(content); + + return { + content: buffer, + header: { + name: normalizeTarPath(target), + mode, + size: buffer.length, + }, + }; +}; + +const addBufferEntry = (tar: tarStream.Pack, content: Buffer, header: tarStream.Headers): Promise => + new Promise((resolve, reject) => { + tar.entry(header, content, (err) => (err ? reject(err) : resolve())); + }); + +const toBuffer = async (content: Content): Promise => { + if (Buffer.isBuffer(content)) { + return content; + } + + if (typeof content === "string") { + return Buffer.from(content); + } + + const chunks: Buffer[] = []; + for await (const chunk of content) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return Buffer.concat(chunks); +}; + +const getEntryMode = (stats: Stats, mode?: number): number => mode ?? stats.mode & 0o7777; + +const joinTarPaths = (base: string, child: string): string => path.posix.join(normalizeTarPath(base), child); + +const normalizeTarPath = (entryPath: string): string => entryPath.replace(/\\/g, "/").replace(/^\/+/, ""); From 1270f2ca74fe310ba6761f944ea6e073a84cfd2a Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Thu, 18 Jun 2026 10:04:20 +0100 Subject: [PATCH 2/4] Keep Azure Cosmos port allocation compatible --- .../src/azure-cosmosdb-emulator-container.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts b/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts index 8f80ecbd9..5cfc257d3 100644 --- a/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts +++ b/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts @@ -1,4 +1,11 @@ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait, getRandomPort } from "testcontainers"; +import { + AbstractStartedContainer, + GenericContainer, + PortGenerator, + RandomPortGenerator, + StartedTestContainer, + Wait, +} from "testcontainers"; type Protocol = "http" | "https"; @@ -14,9 +21,11 @@ export class AzureCosmosDbEmulatorContainer extends GenericContainer { private protocol: Protocol = DEFAULT_PROTOCOL; private telemetryEnabled = DEFAULT_TELEMETRY_ENABLED; private explorerEnabled = DEFAULT_EXPLORER_ENABLED; + private portGenerator: PortGenerator; constructor(image: string) { super(image); + this.portGenerator = new RandomPortGenerator(); this.withWaitStrategy(Wait.forLogMessage(COSMOS_READY_LOG_MESSAGE)); } @@ -31,7 +40,7 @@ export class AzureCosmosDbEmulatorContainer extends GenericContainer { } public override async start(): Promise { - const port = await getRandomPort(); + const port = await this.portGenerator.generatePort(); this.withExposedPorts({ host: port, container: port, From eefa0960ec9bda1579b62e5cd6ebd10c1143ed2b Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Thu, 18 Jun 2026 11:02:33 +0100 Subject: [PATCH 3/4] Avoid reusing recently allocated random ports --- .../src/utils/port-generator.test.ts | 33 +++++++++++++ .../src/utils/port-generator.ts | 46 +++++++++++++------ 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/packages/testcontainers/src/utils/port-generator.test.ts b/packages/testcontainers/src/utils/port-generator.test.ts index b2e8c5cc0..deb04dbec 100644 --- a/packages/testcontainers/src/utils/port-generator.test.ts +++ b/packages/testcontainers/src/utils/port-generator.test.ts @@ -19,5 +19,38 @@ describe("PortGenerator", () => { expect(port2).toBeDefined(); expect(port1).not.toBe(port2); }); + + it("should not return a recently generated port again", async () => { + vi.resetModules(); + const offeredPorts = [1000, 1000, 1001]; + const createServer = vi.fn(() => createServerOfferingPort(offeredPorts.shift()!)); + vi.doMock("net", () => ({ + default: { createServer }, + })); + + const { getRandomPort } = await import("./port-generator.js"); + + await expect(getRandomPort()).resolves.toBe(1000); + await expect(getRandomPort()).resolves.toBe(1001); + expect(createServer).toHaveBeenCalledTimes(3); + + vi.doUnmock("net"); + vi.resetModules(); + }); }); }); + +const createServerOfferingPort = (port: number) => { + const server = { + address: vi.fn(() => ({ port })), + close: vi.fn((callback: (err?: Error) => void) => callback()), + listen: vi.fn((_port: number, callback: () => void) => { + callback(); + return server; + }), + once: vi.fn(), + unref: vi.fn(), + }; + + return server; +}; diff --git a/packages/testcontainers/src/utils/port-generator.ts b/packages/testcontainers/src/utils/port-generator.ts index a46678143..108694cb3 100644 --- a/packages/testcontainers/src/utils/port-generator.ts +++ b/packages/testcontainers/src/utils/port-generator.ts @@ -4,28 +4,46 @@ export interface PortGenerator { generatePort(): Promise; } +const lockedPorts = new Set(); +const releaseLockedPortAfterMs = 15_000; + export class RandomPortGenerator { public async generatePort(): Promise { + let port = await getAvailablePort(); + while (isLockedPort(port)) { + port = await getAvailablePort(); + } + + lockPort(port); + return port; + } +} + +const getAvailablePort = async (): Promise => { + return new Promise((resolve, reject) => { const server = net.createServer(); server.unref(); - await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, () => resolve()); - }); - - const address = server.address(); - if (address === null || typeof address === "string") { - throw new Error("Failed to allocate a random TCP port"); - } + server.once("error", reject); + server.listen(0, () => { + const address = server.address(); + if (address === null || typeof address === "string") { + reject(new Error("Failed to allocate a random TCP port")); + return; + } - await new Promise((resolve, reject) => { - server.close((err) => (err ? reject(err) : resolve())); + server.close((err) => (err ? reject(err) : resolve(address.port))); }); + }); +}; - return address.port; - } -} +const isLockedPort = (port: number): boolean => lockedPorts.has(port); + +const lockPort = (port: number): void => { + lockedPorts.add(port); + const releaseLockedPortTimeout = setTimeout(() => lockedPorts.delete(port), releaseLockedPortAfterMs); + releaseLockedPortTimeout.unref(); +}; const randomPortGenerator = new RandomPortGenerator(); From 4e29c7dd34480931bc1698ab966afbc31acefbff Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Thu, 18 Jun 2026 11:21:22 +0100 Subject: [PATCH 4/4] Normalize copied directory symlink targets --- .../src/utils/tar-archive.test.ts | 29 ++++++++++ .../testcontainers/src/utils/tar-archive.ts | 57 ++++++++++++++++--- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/packages/testcontainers/src/utils/tar-archive.test.ts b/packages/testcontainers/src/utils/tar-archive.test.ts index dcae25517..0f8f55a4b 100644 --- a/packages/testcontainers/src/utils/tar-archive.test.ts +++ b/packages/testcontainers/src/utils/tar-archive.test.ts @@ -60,6 +60,35 @@ describe("createTarArchive", () => { await fs.rm(outside, { recursive: true, force: true }); } }); + + it("should normalize absolute symlink targets inside copied directories", async () => { + const source = await fs.mkdtemp(path.join(tmpdir(), "testcontainers-tar-archive-")); + const nested = path.join(source, "nested"); + const target = path.join(source, "target.txt"); + const symlink = path.join(nested, "linked-target.txt"); + + try { + await fs.mkdir(nested); + await fs.writeFile(target, "hello world"); + const symlinkCreated = await createSymlinkOrSkip(target, symlink, "file"); + if (!symlinkCreated) { + return; + } + + const archive = await createTarArchive({ + directoriesToCopy: [{ source, target: "/tmp/copied" }], + }); + + const entries = await getTarEntries(archive); + + expect(entries.find((entry) => entry.name === "tmp/copied/nested/linked-target.txt")).toMatchObject({ + linkname: "../target.txt", + type: "symlink", + }); + } finally { + await fs.rm(source, { recursive: true, force: true }); + } + }); }); const createSymlinkOrSkip = async (target: string, symlinkPath: string, type: "dir" | "file"): Promise => { diff --git a/packages/testcontainers/src/utils/tar-archive.ts b/packages/testcontainers/src/utils/tar-archive.ts index 7f4ab649b..b98e96434 100644 --- a/packages/testcontainers/src/utils/tar-archive.ts +++ b/packages/testcontainers/src/utils/tar-archive.ts @@ -20,6 +20,11 @@ type TarArchiveEntry = content: Buffer; }; +type DirectoryArchiveContext = { + rootSource: string; + rootTarget: string; +}; + export const createTarArchive = async (options: TarArchiveOptions): Promise => { const entries = await collectTarArchiveEntries(options); const tar = tarStream.pack(); @@ -39,7 +44,10 @@ const collectTarArchiveEntries = async (options: TarArchiveOptions): Promise => { const stats = await fs.lstat(source); if (stats.isSymbolicLink()) { - entries.push(await createSymlinkArchiveEntry(source, target, stats, mode)); + entries.push(await createSymlinkArchiveEntry(source, target, stats, mode, context)); return; } @@ -85,9 +94,9 @@ const addDirectoryEntries = async ( const entryStats = await fs.lstat(sourcePath); if (directoryEntry.isSymbolicLink()) { - entries.push(await createSymlinkArchiveEntry(sourcePath, targetPath, entryStats, mode)); + entries.push(await createSymlinkArchiveEntry(sourcePath, targetPath, entryStats, mode, context)); } else if (entryStats.isDirectory()) { - await addDirectoryEntries(entries, sourcePath, targetPath, mode, true); + await addDirectoryEntries(entries, sourcePath, targetPath, mode, true, context); } else if (entryStats.isFile()) { entries.push(createFileArchiveEntryFromStats(sourcePath, targetPath, mode, entryStats)); } @@ -134,13 +143,14 @@ const createSymlinkArchiveEntry = async ( source: string, target: string, stats: Stats, - mode: number | undefined + mode: number | undefined, + context: DirectoryArchiveContext ): Promise => ({ content: Buffer.alloc(0), header: { name: normalizeTarPath(target), type: "symlink", - linkname: await fs.readlink(source), + linkname: await getSymlinkLinkname(source, target, context), mode: getEntryMode(stats, mode), mtime: stats.mtime, uid: stats.uid, @@ -149,6 +159,26 @@ const createSymlinkArchiveEntry = async ( }, }); +const getSymlinkLinkname = async ( + source: string, + target: string, + context: DirectoryArchiveContext +): Promise => { + const linkname = await fs.readlink(source); + if (!path.isAbsolute(linkname)) { + return normalizeTarLinkname(linkname); + } + + const resolvedLinkTarget = path.resolve(linkname); + if (!isPathInside(context.rootSource, resolvedLinkTarget)) { + return normalizeTarLinkname(linkname); + } + + const sourceRelativeLinkTarget = path.relative(context.rootSource, resolvedLinkTarget); + const archiveLinkTarget = joinTarPaths(context.rootTarget, sourceRelativeLinkTarget); + return path.posix.relative(path.posix.dirname(normalizeTarPath(target)), archiveLinkTarget) || "."; +}; + const addFileToArchive = async (tar: tarStream.Pack, source: string, header: tarStream.Headers): Promise => { await new Promise((resolve, reject) => { const entry = tar.entry(header, (err) => (err ? reject(err) : resolve())); @@ -196,6 +226,17 @@ const toBuffer = async (content: Content): Promise => { const getEntryMode = (stats: Stats, mode?: number): number => mode ?? stats.mode & 0o7777; -const joinTarPaths = (base: string, child: string): string => path.posix.join(normalizeTarPath(base), child); +const joinTarPaths = (base: string, child: string): string => + path.posix.join(normalizeTarPath(base), normalizeTarPath(child)); const normalizeTarPath = (entryPath: string): string => entryPath.replace(/\\/g, "/").replace(/^\/+/, ""); + +const normalizeTarLinkname = (linkname: string): string => linkname.replace(/\\/g, "/"); + +const isPathInside = (parent: string, child: string): boolean => { + const relativePath = path.relative(parent, child); + return ( + relativePath === "" || + (relativePath !== ".." && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath)) + ); +};