From c4d7f5d1106b4ae9f469f28e75f81cc600b872e2 Mon Sep 17 00:00:00 2001 From: Joe Bowbeer Date: Tue, 12 Nov 2024 01:01:15 -0800 Subject: [PATCH 1/2] Add K3s module Signed-off-by: Joe Bowbeer --- docs/modules/k3s.md | 19 + mkdocs.yml | 1 + package-lock.json | 491 ++++++++++++++++-- packages/modules/k3s/jest.config.ts | 11 + packages/modules/k3s/package.json | 37 ++ packages/modules/k3s/src/index.ts | 1 + .../modules/k3s/src/k3s-container.test.ts | 80 +++ packages/modules/k3s/src/k3s-container.ts | 78 +++ packages/modules/k3s/tsconfig.build.json | 13 + packages/modules/k3s/tsconfig.json | 21 + 10 files changed, 706 insertions(+), 46 deletions(-) create mode 100644 docs/modules/k3s.md create mode 100644 packages/modules/k3s/jest.config.ts create mode 100644 packages/modules/k3s/package.json create mode 100644 packages/modules/k3s/src/index.ts create mode 100644 packages/modules/k3s/src/k3s-container.test.ts create mode 100644 packages/modules/k3s/src/k3s-container.ts create mode 100644 packages/modules/k3s/tsconfig.build.json create mode 100644 packages/modules/k3s/tsconfig.json diff --git a/docs/modules/k3s.md b/docs/modules/k3s.md new file mode 100644 index 000000000..aa8220c90 --- /dev/null +++ b/docs/modules/k3s.md @@ -0,0 +1,19 @@ +# K3s Module + +[K3s](https://k3s.io/) is a highly available, certified Kubernetes distribution designed for production workloads in unattended, resource-constrained, remote locations or inside IoT appliances. + +## Install + +```bash +npm install @testcontainers/k3s --save-dev +``` + +## Examples + + +[Starting a K3S server](../../packages/modules/k3s/src/k3s-container.test.ts) inside_block:starting_k3s + + + +[Connecting to the server](../../packages/modules/k3s/src/k3s-container.test.ts) inside_block:connecting_with_client + diff --git a/mkdocs.yml b/mkdocs.yml index 463b63136..65cdb41c4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -50,6 +50,7 @@ nav: - Elasticsearch: modules/elasticsearch.md - GCloud: modules/gcloud.md - HiveMQ: modules/hivemq.md + - K3s: modules/k3s.md - Kafka: modules/kafka.md - Localstack: modules/localstack.md - MariaDB: modules/mariadb.md diff --git a/package-lock.json b/package-lock.json index 42e839ee4..f0217a1e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2326,13 +2326,12 @@ "dev": true }, "node_modules/@bundled-es-modules/cookie": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", - "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", "dev": true, - "license": "ISC", "dependencies": { - "cookie": "^0.5.0" + "cookie": "^0.7.2" } }, "node_modules/@bundled-es-modules/statuses": { @@ -3072,6 +3071,18 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz", @@ -3528,6 +3539,236 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "dev": true, + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "dev": true, + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@kubernetes/client-node": { + "version": "1.0.0-rc7", + "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-1.0.0-rc7.tgz", + "integrity": "sha512-s0U74yQ/nTq13xk3YI8P2y02pUm9TritjhsCIbtPYbOrG/5ti2bY2WhxxQWGx1LhiTZPEdoULtsyXI2XrIESJw==", + "dev": true, + "dependencies": { + "@types/js-yaml": "^4.0.1", + "@types/node": "^22.0.0", + "@types/node-fetch": "^2.6.9", + "@types/stream-buffers": "^3.0.3", + "@types/tar": "^6.1.1", + "@types/ws": "^8.5.4", + "form-data": "^4.0.0", + "isomorphic-ws": "^5.0.0", + "js-yaml": "^4.1.0", + "jsonpath-plus": "^10.0.0", + "node-fetch": "^2.6.9", + "openid-client": "^5.6.5", + "rfc4648": "^1.3.0", + "stream-buffers": "^3.0.2", + "tar": "^7.0.0", + "tmp-promise": "^3.0.2", + "tslib": "^2.5.0", + "url-parse": "^1.4.3", + "ws": "^8.18.0" + } + }, + "node_modules/@kubernetes/client-node/node_modules/@types/node": { + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@kubernetes/client-node/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@kubernetes/client-node/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@kubernetes/client-node/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@kubernetes/client-node/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@kubernetes/client-node/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@kubernetes/client-node/node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@kubernetes/client-node/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@kubernetes/client-node/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@kubernetes/client-node/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@kubernetes/client-node/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/@kubernetes/client-node/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/@kubernetes/client-node/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@kubernetes/client-node/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", @@ -5440,6 +5681,10 @@ "resolved": "packages/modules/hivemq", "link": true }, + "node_modules/@testcontainers/k3s": { + "resolved": "packages/modules/k3s", + "link": true + }, "node_modules/@testcontainers/kafka": { "resolved": "packages/modules/kafka", "link": true @@ -5821,6 +6066,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -5883,6 +6134,16 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/pg": { "version": "8.11.6", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz", @@ -6052,6 +6313,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/stream-buffers": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.7.tgz", + "integrity": "sha512-azOCy05sXVXrO+qklf0c/B07H/oHaIuDDAiHPVwlk3A9Ek+ksHyTeMajLZl3r76FxpPpxem//4Te61G1iW3Giw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "minipass": "^4.0.0" + } + }, "node_modules/@types/tar-fs": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/tar-fs/-/tar-fs-2.0.4.tgz", @@ -6071,6 +6351,15 @@ "@types/node": "*" } }, + "node_modules/@types/tar/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@types/tedious": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", @@ -6771,16 +7060,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/archiver/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==", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, "node_modules/are-we-there-yet": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-4.0.2.tgz", @@ -8280,11 +8559,10 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -8818,6 +9096,21 @@ "tar-stream": "^2.0.0" } }, + "node_modules/dockerode/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -12259,6 +12552,15 @@ "whatwg-fetch": "^3.4.1" } }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "dev": true, + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -12945,9 +13247,9 @@ "dev": true }, "node_modules/jose": { - "version": "4.15.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", - "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "dev": true, "funding": { "url": "https://github.com/sponsors/panva" @@ -12999,6 +13301,15 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "dev": true }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "dev": true, + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -13144,6 +13455,24 @@ "node >= 0.2.0" ] }, + "node_modules/jsonpath-plus": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.1.0.tgz", + "integrity": "sha512-gHfV1IYqH8uJHYVTs8BJX1XKy2/rR93+f8QQi0xhx95aCiXn1ettYAd5T+7FU6wfqyDoX/wy0pm/fL3jOKJ9Lg==", + "dev": true, + "dependencies": { + "@jsep-plugin/assignment": "^1.2.1", + "@jsep-plugin/regex": "^1.0.3", + "jsep": "^1.3.9" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -15808,6 +16137,15 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "dev": true, + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -15848,6 +16186,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openid-client": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.0.tgz", + "integrity": "sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==", + "dev": true, + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/openid-client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -16142,11 +16522,10 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", - "dev": true, - "license": "MIT" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true }, "node_modules/path-type": { "version": "4.0.0", @@ -17342,6 +17721,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfc4648": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz", + "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==", + "dev": true + }, "node_modules/rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", @@ -18040,6 +18425,15 @@ "npm": ">=6" } }, + "node_modules/stream-buffers": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.3.tgz", + "integrity": "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/stream-events": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", @@ -18438,31 +18832,16 @@ "bare-path": "^2.1.0" } }, - "node_modules/tar-fs/node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "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==", "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -18651,6 +19030,15 @@ "node": ">=14.14" } }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "dependencies": { + "tmp": "^0.2.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -19993,6 +20381,17 @@ "mqtt": "^4.3.8" } }, + "packages/modules/k3s": { + "name": "@testcontainers/k3s", + "version": "10.14.0", + "license": "MIT", + "dependencies": { + "testcontainers": "^10.14.0" + }, + "devDependencies": { + "@kubernetes/client-node": "^1.0.0-rc7" + } + }, "packages/modules/kafka": { "name": "@testcontainers/kafka", "version": "10.14.0", diff --git a/packages/modules/k3s/jest.config.ts b/packages/modules/k3s/jest.config.ts new file mode 100644 index 000000000..1f677baaf --- /dev/null +++ b/packages/modules/k3s/jest.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "jest"; +import * as path from "path"; + +const config: Config = { + preset: "ts-jest", + moduleNameMapper: { + "^testcontainers$": path.resolve(__dirname, "../../testcontainers/src"), + }, +}; + +export default config; diff --git a/packages/modules/k3s/package.json b/packages/modules/k3s/package.json new file mode 100644 index 000000000..472ee012d --- /dev/null +++ b/packages/modules/k3s/package.json @@ -0,0 +1,37 @@ +{ + "name": "@testcontainers/k3s", + "version": "10.14.0", + "license": "MIT", + "keywords": [ + "k3s", + "testing", + "docker", + "testcontainers" + ], + "description": "K3s module for Testcontainers", + "homepage": "https://github.com/testcontainers/testcontainers-node#readme", + "repository": { + "type": "git", + "url": "https://github.com/testcontainers/testcontainers-node" + }, + "bugs": { + "url": "https://github.com/testcontainers/testcontainers-node/issues" + }, + "main": "build/index.js", + "files": [ + "build" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "prepack": "shx cp ../../../README.md . && shx cp ../../../LICENSE .", + "build": "tsc --project tsconfig.build.json" + }, + "dependencies": { + "testcontainers": "^10.14.0" + }, + "devDependencies": { + "@kubernetes/client-node": "^1.0.0-rc7" + } +} diff --git a/packages/modules/k3s/src/index.ts b/packages/modules/k3s/src/index.ts new file mode 100644 index 000000000..980f342f4 --- /dev/null +++ b/packages/modules/k3s/src/index.ts @@ -0,0 +1 @@ +export { K3sContainer, StartedK3sContainer } from "./k3s-container"; diff --git a/packages/modules/k3s/src/k3s-container.test.ts b/packages/modules/k3s/src/k3s-container.test.ts new file mode 100644 index 000000000..adb8b1145 --- /dev/null +++ b/packages/modules/k3s/src/k3s-container.test.ts @@ -0,0 +1,80 @@ +import { K3sContainer } from "./k3s-container"; +import * as k8s from "@kubernetes/client-node"; +import { setTimeout } from "node:timers/promises"; + +describe("K3s", () => { + jest.setTimeout(150_000); + + it("should start and have listable node", async () => { + // starting_k3s { + const container = await new K3sContainer().start(); + // } + + // connecting_with_client { + // obtain a kubeconfig file which allows us to connect to k3s + const kubeConfig = container.getKubeConfig(); + + const kc = new k8s.KubeConfig(); + kc.loadFromString(kubeConfig); + + const client = kc.makeApiClient(k8s.CoreV1Api); + + // interact with the running K3s server, e.g.: + const nodeList = await client.listNode(); + // } + + expect(nodeList.items).toHaveLength(1); + + await container.stop(); + }); + + it("should start a pod", async () => { + const container = await new K3sContainer().start(); + const kc = new k8s.KubeConfig(); + kc.loadFromString(container.getKubeConfig()); + + const pod = { + metadata: { + name: "helloworld", + }, + spec: { + containers: [ + { + name: "helloworld", + image: "testcontainers/helloworld:1.1.0", + ports: [ + { + containerPort: 8080, + }, + ], + readinessProbe: { + tcpSocket: { + port: 8080, + }, + }, + }, + ], + }, + }; + + const client = kc.makeApiClient(k8s.CoreV1Api); + await client.createNamespacedPod({ namespace: "default", body: pod }); + + // wait for pod to be ready + let ready = false; + for (const startTime = Date.now(); Date.now() - startTime < 60_000; ) { + const podList = await client.listNamespacedPod({ namespace: "default" }); + const pod = podList.items.find((pod) => pod.metadata?.name === "helloworld"); + const status = pod?.status; + ready = + status?.phase === "Running" && + !!status?.conditions?.some((cond) => cond.type === "Ready" && cond.status === "True"); + if (ready) break; + await setTimeout(3_000); + } + + expect(ready).toBe(true); + + await container.stop(); + }); +}); diff --git a/packages/modules/k3s/src/k3s-container.ts b/packages/modules/k3s/src/k3s-container.ts new file mode 100644 index 000000000..3c3c0cae1 --- /dev/null +++ b/packages/modules/k3s/src/k3s-container.ts @@ -0,0 +1,78 @@ +import { AbstractStartedContainer, GenericContainer, Wait, type StartedTestContainer } from "testcontainers"; +import tar from "tar-stream"; +import { basename } from "node:path"; + +// TODO: Update @types/dockerode +// https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71160 +type CgroupnsModeConfig = { CgroupnsMode?: "private" | "host" }; + +const KUBE_CONFIG_PATH = "/etc/rancher/k3s/k3s.yaml"; +const KUBE_SECURE_PORT = 6443; +const RANCHER_WEBHOOK_PORT = 8443; + +/** Path to the k3s manifests directory. These are applied automatically on startup. */ +export const K3S_SERVER_MANIFESTS = "/var/lib/rancher/k3s/server/manifests/"; + +export class K3sContainer extends GenericContainer { + constructor(image = "rancher/k3s:v1.31.2-k3s1") { + super(image); + (this.hostConfig as CgroupnsModeConfig).CgroupnsMode = "host"; + this.withExposedPorts(KUBE_SECURE_PORT, RANCHER_WEBHOOK_PORT) + .withPrivilegedMode() + // TODO: Determine if/when bind mount is needed + .withBindMounts([{ mode: "rw", source: "/sys/fs/cgroup", target: "/sys/fs/cgroup" }]) + .withTmpFs({ "/run": "rw" }) + .withTmpFs({ "/var/run": "rw" }) + // TODO: If tls-san is desirable, determine how to obtain the host address + // .withCommand(["server", "--disable=traefik", `--tls-san=${this.getHost()}`]) + .withCommand(["server", "--disable=traefik"]) + .withWaitStrategy(Wait.forLogMessage("Node controller sync successful")) + .withStartupTimeout(120_000); + } + + public override async start(): Promise { + const container = await super.start(); + const tarStream = await container.copyArchiveFromContainer(KUBE_CONFIG_PATH); + const kubeConfig = await extractFromTarStream(tarStream, basename(KUBE_CONFIG_PATH)); + return new StartedK3sContainer(container, kubeConfig); + } +} + +export class StartedK3sContainer extends AbstractStartedContainer { + constructor(startedTestContainer: StartedTestContainer, private readonly kubeConfig: string) { + super(startedTestContainer); + } + + public getKubeConfig(): string { + const serverUrl = `https://${this.getHost()}:${this.getMappedPort(KUBE_SECURE_PORT)}`; + return kubeConfigWithServerUrl(this.kubeConfig, serverUrl); + } +} + +async function extractFromTarStream(tarStream: NodeJS.ReadableStream, entryName: string): Promise { + const extract = tar.extract(); + tarStream.pipe(extract); + + let extracted = undefined; + for await (const entry of extract) { + const { header } = entry; + if (header.type === "file" && header.name === entryName) { + const chunks = []; + for await (const chunk of entry) { + chunks.push(chunk); + } + extracted = Buffer.concat(chunks).toString("utf-8"); + } else { + entry.resume(); + } + } + + if (!extracted) { + throw new Error(`Failed to extract ${entryName} from archive`); + } + return extracted; +} + +export function kubeConfigWithServerUrl(kubeConfig: string, server: string): string { + return kubeConfig.replace(/server:\s?[:/.\d\w]+/, `server: ${server}`); +} diff --git a/packages/modules/k3s/tsconfig.build.json b/packages/modules/k3s/tsconfig.build.json new file mode 100644 index 000000000..0222f6ff1 --- /dev/null +++ b/packages/modules/k3s/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "build", + "jest.config.ts", + "src/**/*.test.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file diff --git a/packages/modules/k3s/tsconfig.json b/packages/modules/k3s/tsconfig.json new file mode 100644 index 000000000..39b165817 --- /dev/null +++ b/packages/modules/k3s/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "paths": { + "testcontainers": [ + "../../testcontainers/src" + ] + } + }, + "exclude": [ + "build", + "jest.config.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file From 4d9e5ec95b6426840ef9abe8a482bd0544a5707f Mon Sep 17 00:00:00 2001 From: Joe Bowbeer Date: Sun, 17 Nov 2024 01:37:33 -0800 Subject: [PATCH 2/2] Add tls-san, disable rootful tests --- docs/modules/k3s.md | 11 +- .../modules/k3s/src/k3s-container.test.ts | 165 +++++++++++------- packages/modules/k3s/src/k3s-container.ts | 40 +++-- 3 files changed, 134 insertions(+), 82 deletions(-) diff --git a/docs/modules/k3s.md b/docs/modules/k3s.md index aa8220c90..0ed673142 100644 --- a/docs/modules/k3s.md +++ b/docs/modules/k3s.md @@ -11,9 +11,16 @@ npm install @testcontainers/k3s --save-dev ## Examples -[Starting a K3S server](../../packages/modules/k3s/src/k3s-container.test.ts) inside_block:starting_k3s +[Starting a K3s server:](../../packages/modules/k3s/src/k3s-container.test.ts) inside_block:starting_k3s -[Connecting to the server](../../packages/modules/k3s/src/k3s-container.test.ts) inside_block:connecting_with_client +[Connecting to the server using the Kubernetes JavaScript client:](../../packages/modules/k3s/src/k3s-container.test.ts) inside_block:connecting_with_client + +## Known limitations + +!!! warning + * K3sContainer runs as a privileged container and needs to be able to spawn its own containers. For these reasons, + K3sContainer will not work in certain rootless Docker, Docker-in-Docker, or other environments where privileged + containers are disallowed. diff --git a/packages/modules/k3s/src/k3s-container.test.ts b/packages/modules/k3s/src/k3s-container.test.ts index adb8b1145..ecf01d2bc 100644 --- a/packages/modules/k3s/src/k3s-container.test.ts +++ b/packages/modules/k3s/src/k3s-container.test.ts @@ -1,80 +1,117 @@ import { K3sContainer } from "./k3s-container"; import * as k8s from "@kubernetes/client-node"; import { setTimeout } from "node:timers/promises"; +import { GenericContainer, Network, Wait } from "testcontainers"; describe("K3s", () => { - jest.setTimeout(150_000); + jest.setTimeout(120_000); - it("should start and have listable node", async () => { - // starting_k3s { - const container = await new K3sContainer().start(); - // } + it("should construct", () => { + new K3sContainer("rancher/k3s:v1.31.2-k3s1"); + }); - // connecting_with_client { - // obtain a kubeconfig file which allows us to connect to k3s - const kubeConfig = container.getKubeConfig(); + // K3sContainer runs as a privileged container + if (!process.env["CI_ROOTLESS"]) { + it("should start and have listable node", async () => { + // starting_k3s { + const container = await new K3sContainer("rancher/k3s:v1.31.2-k3s1").start(); + // } - const kc = new k8s.KubeConfig(); - kc.loadFromString(kubeConfig); + // connecting_with_client { + // obtain a kubeconfig file that allows us to connect to k3s + const kubeConfig = container.getKubeConfig(); - const client = kc.makeApiClient(k8s.CoreV1Api); + const kc = new k8s.KubeConfig(); + kc.loadFromString(kubeConfig); - // interact with the running K3s server, e.g.: - const nodeList = await client.listNode(); - // } + const client = kc.makeApiClient(k8s.CoreV1Api); - expect(nodeList.items).toHaveLength(1); + // interact with the running K3s server, e.g.: + const nodeList = await client.listNode(); + // } - await container.stop(); - }); + expect(nodeList.items).toHaveLength(1); - it("should start a pod", async () => { - const container = await new K3sContainer().start(); - const kc = new k8s.KubeConfig(); - kc.loadFromString(container.getKubeConfig()); - - const pod = { - metadata: { - name: "helloworld", - }, - spec: { - containers: [ - { - name: "helloworld", - image: "testcontainers/helloworld:1.1.0", - ports: [ - { - containerPort: 8080, - }, - ], - readinessProbe: { - tcpSocket: { - port: 8080, + await container.stop(); + }); + + it("should expose kubeconfig for a network alias", async () => { + const network = await new Network().start(); + const container = await new K3sContainer("rancher/k3s:v1.31.2-k3s1") + .withNetwork(network) + .withNetworkAliases("k3s") + .start(); + + // obtain a kubeconfig that allows us to connect on the custom network + const kubeConfig = container.getAliasedKubeConfig("k3s"); + + const kubectlContainer = await new GenericContainer("rancher/kubectl:v1.31.2") + .withNetwork(network) + .withCopyContentToContainer([{ content: kubeConfig, target: "/home/kubectl/.kube/config" }]) + .withCommand(["get", "namespaces"]) + .withWaitStrategy(Wait.forOneShotStartup()) + .withStartupTimeout(30_000) + .start(); + + const chunks = []; + for await (const chunk of await kubectlContainer.logs()) { + chunks.push(chunk); + } + expect(chunks).toEqual(expect.arrayContaining([expect.stringContaining("kube-system")])); + + await kubectlContainer.stop(); + await container.stop(); + await network.stop(); + }); + + it("should start a pod", async () => { + const container = await new K3sContainer("rancher/k3s:v1.31.2-k3s1").start(); + const kc = new k8s.KubeConfig(); + kc.loadFromString(container.getKubeConfig()); + + const pod = { + metadata: { + name: "helloworld", + }, + spec: { + containers: [ + { + name: "helloworld", + image: "testcontainers/helloworld:1.1.0", + ports: [ + { + containerPort: 8080, + }, + ], + readinessProbe: { + tcpSocket: { + port: 8080, + }, }, }, - }, - ], - }, - }; - - const client = kc.makeApiClient(k8s.CoreV1Api); - await client.createNamespacedPod({ namespace: "default", body: pod }); - - // wait for pod to be ready - let ready = false; - for (const startTime = Date.now(); Date.now() - startTime < 60_000; ) { - const podList = await client.listNamespacedPod({ namespace: "default" }); - const pod = podList.items.find((pod) => pod.metadata?.name === "helloworld"); - const status = pod?.status; - ready = - status?.phase === "Running" && - !!status?.conditions?.some((cond) => cond.type === "Ready" && cond.status === "True"); - if (ready) break; - await setTimeout(3_000); - } - - expect(ready).toBe(true); - - await container.stop(); - }); + ], + }, + }; + + const client = kc.makeApiClient(k8s.CoreV1Api); + await client.createNamespacedPod({ namespace: "default", body: pod }); + + // wait for pod to be ready + expect(await podIsReady(client, "default", "helloworld", 60_000)).toBe(true); + + await container.stop(); + }); + } }); + +async function podIsReady(client: k8s.CoreV1Api, namespace: string, name: string, timeout: number): Promise { + for (const startTime = Date.now(); Date.now() - startTime < timeout; ) { + const res = await client.readNamespacedPodStatus({ namespace, name }); + const ready = + res.status?.phase === "Running" && + !!res.status?.conditions?.some((cond) => cond.type === "Ready" && cond.status === "True"); + if (ready) return true; + await setTimeout(3_000); + } + return false; +} diff --git a/packages/modules/k3s/src/k3s-container.ts b/packages/modules/k3s/src/k3s-container.ts index 3c3c0cae1..41d178ed6 100644 --- a/packages/modules/k3s/src/k3s-container.ts +++ b/packages/modules/k3s/src/k3s-container.ts @@ -1,8 +1,8 @@ -import { AbstractStartedContainer, GenericContainer, Wait, type StartedTestContainer } from "testcontainers"; +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; import tar from "tar-stream"; import { basename } from "node:path"; -// TODO: Update @types/dockerode +// TODO: Implement GenericContainer.withCgroupnsMode // https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71160 type CgroupnsModeConfig = { CgroupnsMode?: "private" | "host" }; @@ -10,22 +10,16 @@ const KUBE_CONFIG_PATH = "/etc/rancher/k3s/k3s.yaml"; const KUBE_SECURE_PORT = 6443; const RANCHER_WEBHOOK_PORT = 8443; -/** Path to the k3s manifests directory. These are applied automatically on startup. */ -export const K3S_SERVER_MANIFESTS = "/var/lib/rancher/k3s/server/manifests/"; - export class K3sContainer extends GenericContainer { - constructor(image = "rancher/k3s:v1.31.2-k3s1") { + constructor(image: string) { super(image); (this.hostConfig as CgroupnsModeConfig).CgroupnsMode = "host"; this.withExposedPorts(KUBE_SECURE_PORT, RANCHER_WEBHOOK_PORT) .withPrivilegedMode() - // TODO: Determine if/when bind mount is needed - .withBindMounts([{ mode: "rw", source: "/sys/fs/cgroup", target: "/sys/fs/cgroup" }]) + // Why do Java and .NET implementations bind cgroup but Golang does not? + .withBindMounts([{ source: "/sys/fs/cgroup", target: "/sys/fs/cgroup" }]) .withTmpFs({ "/run": "rw" }) .withTmpFs({ "/var/run": "rw" }) - // TODO: If tls-san is desirable, determine how to obtain the host address - // .withCommand(["server", "--disable=traefik", `--tls-san=${this.getHost()}`]) - .withCommand(["server", "--disable=traefik"]) .withWaitStrategy(Wait.forLogMessage("Node controller sync successful")) .withStartupTimeout(120_000); } @@ -33,19 +27,33 @@ export class K3sContainer extends GenericContainer { public override async start(): Promise { const container = await super.start(); const tarStream = await container.copyArchiveFromContainer(KUBE_CONFIG_PATH); - const kubeConfig = await extractFromTarStream(tarStream, basename(KUBE_CONFIG_PATH)); - return new StartedK3sContainer(container, kubeConfig); + const rawKubeConfig = await extractFromTarStream(tarStream, basename(KUBE_CONFIG_PATH)); + return new StartedK3sContainer(container, rawKubeConfig); + } + + protected override async beforeContainerCreated() { + let command = this.createOpts.Cmd ?? ["server", "--disable=traefik"]; + if (this.networkMode && this.networkAliases.length > 0) { + const aliases = this.networkAliases.join(); + command = [...command, `--tls-san=${aliases}`]; + } + this.withCommand(command); } } export class StartedK3sContainer extends AbstractStartedContainer { - constructor(startedTestContainer: StartedTestContainer, private readonly kubeConfig: string) { + constructor(startedTestContainer: StartedTestContainer, private readonly rawKubeConfig: string) { super(startedTestContainer); } public getKubeConfig(): string { const serverUrl = `https://${this.getHost()}:${this.getMappedPort(KUBE_SECURE_PORT)}`; - return kubeConfigWithServerUrl(this.kubeConfig, serverUrl); + return kubeConfigWithServerUrl(this.rawKubeConfig, serverUrl); + } + + public getAliasedKubeConfig(networkAlias: string) { + const serverUrl = `https://${networkAlias}:${KUBE_SECURE_PORT}`; + return kubeConfigWithServerUrl(this.rawKubeConfig, serverUrl); } } @@ -73,6 +81,6 @@ async function extractFromTarStream(tarStream: NodeJS.ReadableStream, entryName: return extracted; } -export function kubeConfigWithServerUrl(kubeConfig: string, server: string): string { +function kubeConfigWithServerUrl(kubeConfig: string, server: string): string { return kubeConfig.replace(/server:\s?[:/.\d\w]+/, `server: ${server}`); }