From 4cdcf09c438bb2c66112cb38bb04c6f48695afe7 Mon Sep 17 00:00:00 2001 From: Nikola Jokic Date: Mon, 25 Sep 2023 11:49:03 +0200 Subject: [PATCH] Implement yaml extensions overwriting the default pod/container spec (#75) * Implement yaml extensions overwriting the default pod/container spec * format files * Extend specs for container job and include docker and k8s tests in k8s * Create table tests for docker tests * included warnings and extracted append logic as generic * updated merge to allow for file read * reverted back examples and k8s/tests * reverted back docker tests * Tests for extension prepare-job * Fix lint and format and merge error * Added basic test for container step * revert hooklib since new definition for container options is received from a file * revert docker options since create options are a string * Fix revert * Update package locks and deps * included example of extension.yaml. Added side-car container that was missing * Ignore spec modification for the service containers, change selector to * fix lint error * Add missing image override * Add comment explaining merge object meta with job and pod * fix test --- examples/extension.yaml | 30 +++ packages/docker/package-lock.json | 100 ++++++---- packages/hooklib/package-lock.json | 12 +- packages/k8s/package-lock.json | 113 ++++++++--- packages/k8s/package.json | 3 +- packages/k8s/src/hooks/constants.ts | 1 + packages/k8s/src/hooks/prepare-job.ts | 56 +++++- packages/k8s/src/hooks/run-container-step.ts | 33 ++- packages/k8s/src/k8s/index.ts | 37 +++- packages/k8s/src/k8s/utils.ts | 107 ++++++++++ packages/k8s/tests/k8s-utils-test.ts | 188 +++++++++++++++++- packages/k8s/tests/prepare-job-test.ts | 49 ++++- packages/k8s/tests/run-container-step-test.ts | 45 +++++ 13 files changed, 672 insertions(+), 102 deletions(-) create mode 100644 examples/extension.yaml diff --git a/examples/extension.yaml b/examples/extension.yaml new file mode 100644 index 0000000..9e82853 --- /dev/null +++ b/examples/extension.yaml @@ -0,0 +1,30 @@ +metadata: + annotations: + annotated-by: "extension" + labels: + labeled-by: "extension" +spec: + securityContext: + runAsUser: 1000 + runAsGroup: 3000 + restartPolicy: Never + containers: + - name: $job # overwirtes job container + env: + - name: ENV1 + value: "value1" + imagePullPolicy: Always + image: "busybox:1.28" # Ignored + command: + - sh + args: + - -c + - sleep 50 + - name: side-car + image: "ubuntu:latest" # required + command: + - sh + args: + - -c + - sleep 60 + diff --git a/packages/docker/package-lock.json b/packages/docker/package-lock.json index 97c2b38..61d9abc 100644 --- a/packages/docker/package-lock.json +++ b/packages/docker/package-lock.json @@ -136,9 +136,9 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -186,9 +186,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3019,9 +3019,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3848,12 +3848,15 @@ "peer": true }, "node_modules/lru-cache": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", - "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==", + "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": ">=12" + "node": ">=10" } }, "node_modules/make-dir": { @@ -3872,9 +3875,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4497,18 +4500,18 @@ } }, "node_modules/semver": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", - "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { - "lru-cache": "^7.4.0" + "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" }, "engines": { - "node": "^10.0.0 || ^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=10" } }, "node_modules/shebang-command": { @@ -5264,6 +5267,12 @@ "node": ">=10" } }, + "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/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -5380,9 +5389,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -5419,9 +5428,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -7603,9 +7612,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -8253,10 +8262,13 @@ "peer": true }, "lru-cache": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", - "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==", - "dev": true + "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, + "requires": { + "yallist": "^4.0.0" + } }, "make-dir": { "version": "3.1.0", @@ -8268,9 +8280,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -8728,12 +8740,12 @@ } }, "semver": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", - "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { - "lru-cache": "^7.4.0" + "lru-cache": "^6.0.0" } }, "shebang-command": { @@ -9286,6 +9298,12 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/packages/hooklib/package-lock.json b/packages/hooklib/package-lock.json index 30044cd..735e3e0 100644 --- a/packages/hooklib/package-lock.json +++ b/packages/hooklib/package-lock.json @@ -2215,9 +2215,9 @@ } }, "node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4119,9 +4119,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" diff --git a/packages/k8s/package-lock.json b/packages/k8s/package-lock.json index 6985f91..f96a18c 100644 --- a/packages/k8s/package-lock.json +++ b/packages/k8s/package-lock.json @@ -13,7 +13,8 @@ "@actions/exec": "^1.1.1", "@actions/io": "^1.1.2", "@kubernetes/client-node": "^0.18.1", - "hooklib": "file:../hooklib" + "hooklib": "file:../hooklib", + "js-yaml": "^4.1.0" }, "devDependencies": { "@types/jest": "^27.4.1", @@ -3081,9 +3082,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3878,6 +3879,12 @@ "node": ">=0.6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -3949,6 +3956,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -4038,9 +4051,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4368,14 +4381,15 @@ } }, "node_modules/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { "node": ">=6" @@ -4437,9 +4451,9 @@ } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4541,9 +4555,9 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "engines": { "node": ">= 4.0.0" @@ -4557,6 +4571,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -7182,9 +7206,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -7786,6 +7810,12 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -7846,6 +7876,12 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -7911,9 +7947,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "shebang-command": { @@ -8158,14 +8194,15 @@ } }, "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" } }, "tr46": { @@ -8194,9 +8231,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -8269,9 +8306,9 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true }, "uri-js": { @@ -8282,6 +8319,16 @@ "punycode": "^2.1.0" } }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", diff --git a/packages/k8s/package.json b/packages/k8s/package.json index 81d55cd..0e31b5d 100644 --- a/packages/k8s/package.json +++ b/packages/k8s/package.json @@ -17,7 +17,8 @@ "@actions/exec": "^1.1.1", "@actions/io": "^1.1.2", "@kubernetes/client-node": "^0.18.1", - "hooklib": "file:../hooklib" + "hooklib": "file:../hooklib", + "js-yaml": "^4.1.0" }, "devDependencies": { "@types/jest": "^27.4.1", diff --git a/packages/k8s/src/hooks/constants.ts b/packages/k8s/src/hooks/constants.ts index 111f936..a5d63a1 100644 --- a/packages/k8s/src/hooks/constants.ts +++ b/packages/k8s/src/hooks/constants.ts @@ -42,6 +42,7 @@ export function getSecretName(): string { export const MAX_POD_NAME_LENGTH = 63 export const STEP_POD_NAME_SUFFIX_LENGTH = 8 export const JOB_CONTAINER_NAME = 'job' +export const JOB_CONTAINER_EXTENSION_NAME = '$job' export class RunnerInstanceLabel { private podName: string diff --git a/packages/k8s/src/hooks/prepare-job.ts b/packages/k8s/src/hooks/prepare-job.ts index 7d9a168..1a3b0a5 100644 --- a/packages/k8s/src/hooks/prepare-job.ts +++ b/packages/k8s/src/hooks/prepare-job.ts @@ -1,7 +1,12 @@ import * as core from '@actions/core' import * as io from '@actions/io' import * as k8s from '@kubernetes/client-node' -import { ContextPorts, prepareJobArgs, writeToResponseFile } from 'hooklib' +import { + JobContainerInfo, + ContextPorts, + PrepareJobArgs, + writeToResponseFile +} from 'hooklib' import path from 'path' import { containerPorts, @@ -15,12 +20,14 @@ import { DEFAULT_CONTAINER_ENTRY_POINT, DEFAULT_CONTAINER_ENTRY_POINT_ARGS, generateContainerName, + mergeContainerWithOptions, + readExtensionFromFile, PodPhase } from '../k8s/utils' -import { JOB_CONTAINER_NAME } from './constants' +import { JOB_CONTAINER_EXTENSION_NAME, JOB_CONTAINER_NAME } from './constants' export async function prepareJob( - args: prepareJobArgs, + args: PrepareJobArgs, responseFile ): Promise { if (!args.container) { @@ -28,26 +35,46 @@ export async function prepareJob( } await prunePods() + + const extension = readExtensionFromFile() await copyExternalsToRoot() + let container: k8s.V1Container | undefined = undefined if (args.container?.image) { core.debug(`Using image '${args.container.image}' for job image`) - container = createContainerSpec(args.container, JOB_CONTAINER_NAME, true) + container = createContainerSpec( + args.container, + JOB_CONTAINER_NAME, + true, + extension + ) } let services: k8s.V1Container[] = [] if (args.services?.length) { services = args.services.map(service => { core.debug(`Adding service '${service.image}' to pod definition`) - return createContainerSpec(service, generateContainerName(service.image)) + return createContainerSpec( + service, + generateContainerName(service.image), + false, + undefined + ) }) } + if (!container && !services?.length) { throw new Error('No containers exist, skipping hook invocation') } + let createdPod: k8s.V1Pod | undefined = undefined try { - createdPod = await createPod(container, services, args.container.registry) + createdPod = await createPod( + container, + services, + args.container.registry, + extension + ) } catch (err) { await prunePods() throw new Error(`failed to create job pod: ${err}`) @@ -153,9 +180,10 @@ async function copyExternalsToRoot(): Promise { } export function createContainerSpec( - container, + container: JobContainerInfo, name: string, - jobContainer = false + jobContainer = false, + extension?: k8s.V1PodTemplateSpec ): k8s.V1Container { if (!container.entryPoint && jobContainer) { container.entryPoint = DEFAULT_CONTAINER_ENTRY_POINT @@ -193,5 +221,17 @@ export function createContainerSpec( jobContainer ) + if (!extension) { + return podContainer + } + + const from = extension.spec?.containers?.find( + c => c.name === JOB_CONTAINER_EXTENSION_NAME + ) + + if (from) { + mergeContainerWithOptions(podContainer, from) + } + return podContainer } diff --git a/packages/k8s/src/hooks/run-container-step.ts b/packages/k8s/src/hooks/run-container-step.ts index 39b87cf..3979b3f 100644 --- a/packages/k8s/src/hooks/run-container-step.ts +++ b/packages/k8s/src/hooks/run-container-step.ts @@ -10,8 +10,13 @@ import { waitForJobToComplete, waitForPodPhases } from '../k8s' -import { containerVolumes, PodPhase } from '../k8s/utils' -import { JOB_CONTAINER_NAME } from './constants' +import { + containerVolumes, + PodPhase, + mergeContainerWithOptions, + readExtensionFromFile +} from '../k8s/utils' +import { JOB_CONTAINER_EXTENSION_NAME, JOB_CONTAINER_NAME } from './constants' export async function runContainerStep( stepContainer: RunContainerStepArgs @@ -25,10 +30,12 @@ export async function runContainerStep( secretName = await createSecretForEnvs(stepContainer.environmentVariables) } - core.debug(`Created secret ${secretName} for container job envs`) - const container = createPodSpec(stepContainer, secretName) + const extension = readExtensionFromFile() - const job = await createJob(container) + core.debug(`Created secret ${secretName} for container job envs`) + const container = createContainerSpec(stepContainer, secretName, extension) + + const job = await createJob(container, extension) if (!job.metadata?.name) { throw new Error( `Expected job ${JSON.stringify( @@ -69,9 +76,10 @@ export async function runContainerStep( return Number(exitCode) || 1 } -function createPodSpec( +function createContainerSpec( container: RunContainerStepArgs, - secretName?: string + secretName?: string, + extension?: k8s.V1PodTemplateSpec ): k8s.V1Container { const podContainer = new k8s.V1Container() podContainer.name = JOB_CONTAINER_NAME @@ -96,5 +104,16 @@ function createPodSpec( } podContainer.volumeMounts = containerVolumes(undefined, false, true) + if (!extension) { + return podContainer + } + + const from = extension.spec?.containers?.find( + c => c.name === JOB_CONTAINER_EXTENSION_NAME + ) + if (from) { + mergeContainerWithOptions(podContainer, from) + } + return podContainer } diff --git a/packages/k8s/src/k8s/index.ts b/packages/k8s/src/k8s/index.ts index bc7b78d..7a64788 100644 --- a/packages/k8s/src/k8s/index.ts +++ b/packages/k8s/src/k8s/index.ts @@ -10,7 +10,7 @@ import { getVolumeClaimName, RunnerInstanceLabel } from '../hooks/constants' -import { PodPhase } from './utils' +import { PodPhase, mergePodSpecWithOptions, mergeObjectMeta } from './utils' const kc = new k8s.KubeConfig() @@ -58,7 +58,8 @@ export const requiredPermissions = [ export async function createPod( jobContainer?: k8s.V1Container, services?: k8s.V1Container[], - registry?: Registry + registry?: Registry, + extension?: k8s.V1PodTemplateSpec ): Promise { const containers: k8s.V1Container[] = [] if (jobContainer) { @@ -80,6 +81,7 @@ export async function createPod( appPod.metadata.labels = { [instanceLabel.key]: instanceLabel.value } + appPod.metadata.annotations = {} appPod.spec = new k8s.V1PodSpec() appPod.spec.containers = containers @@ -103,12 +105,21 @@ export async function createPod( appPod.spec.imagePullSecrets = [secretReference] } + if (extension?.metadata) { + mergeObjectMeta(appPod, extension.metadata) + } + + if (extension?.spec) { + mergePodSpecWithOptions(appPod.spec, extension.spec) + } + const { body } = await k8sApi.createNamespacedPod(namespace(), appPod) return body } export async function createJob( - container: k8s.V1Container + container: k8s.V1Container, + extension?: k8s.V1PodTemplateSpec ): Promise { const runnerInstanceLabel = new RunnerInstanceLabel() @@ -118,6 +129,7 @@ export async function createJob( job.metadata = new k8s.V1ObjectMeta() job.metadata.name = getStepPodName() job.metadata.labels = { [runnerInstanceLabel.key]: runnerInstanceLabel.value } + job.metadata.annotations = {} job.spec = new k8s.V1JobSpec() job.spec.ttlSecondsAfterFinished = 300 @@ -125,6 +137,9 @@ export async function createJob( job.spec.template = new k8s.V1PodTemplateSpec() job.spec.template.spec = new k8s.V1PodSpec() + job.spec.template.metadata = new k8s.V1ObjectMeta() + job.spec.template.metadata.labels = {} + job.spec.template.metadata.annotations = {} job.spec.template.spec.containers = [container] job.spec.template.spec.restartPolicy = 'Never' job.spec.template.spec.nodeName = await getCurrentNodeName() @@ -137,6 +152,17 @@ export async function createJob( } ] + if (extension) { + if (extension.metadata) { + // apply metadata both to the job and the pod created by the job + mergeObjectMeta(job, extension.metadata) + mergeObjectMeta(job.spec.template, extension.metadata) + } + if (extension.spec) { + mergePodSpecWithOptions(job.spec.template.spec, extension.spec) + } + } + const { body } = await k8sBatchV1Api.createNamespacedJob(namespace(), job) return body } @@ -555,3 +581,8 @@ export function containerPorts( } return ports } + +export async function getPodByName(name): Promise { + const { body } = await k8sApi.readNamespacedPod(name, namespace()) + return body +} diff --git a/packages/k8s/src/k8s/utils.ts b/packages/k8s/src/k8s/utils.ts index ee70fc8..86f3d06 100644 --- a/packages/k8s/src/k8s/utils.ts +++ b/packages/k8s/src/k8s/utils.ts @@ -1,5 +1,7 @@ import * as k8s from '@kubernetes/client-node' import * as fs from 'fs' +import * as yaml from 'js-yaml' +import * as core from '@actions/core' import { Mount } from 'hooklib' import * as path from 'path' import { v1 as uuidv4 } from 'uuid' @@ -8,6 +10,8 @@ import { POD_VOLUME_NAME } from './index' export const DEFAULT_CONTAINER_ENTRY_POINT_ARGS = [`-f`, `/dev/null`] export const DEFAULT_CONTAINER_ENTRY_POINT = 'tail' +export const ENV_HOOK_TEMPLATE_PATH = 'ACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE' + export function containerVolumes( userMountVolumes: Mount[] = [], jobContainer = true, @@ -159,6 +163,100 @@ export function generateContainerName(image: string): string { return name } +// Overwrite or append based on container options +// +// Keep in mind, envs and volumes could be passed as fields in container definition +// so default volume mounts and envs are appended first, and then create options are used +// to append more values +// +// Rest of the fields are just applied +// For example, container.createOptions.container.image is going to overwrite container.image field +export function mergeContainerWithOptions( + base: k8s.V1Container, + from: k8s.V1Container +): void { + for (const [key, value] of Object.entries(from)) { + if (key === 'name') { + core.warning("Skipping name override: name can't be overwritten") + continue + } else if (key === 'image') { + core.warning("Skipping image override: image can't be overwritten") + continue + } else if (key === 'env') { + const envs = value as k8s.V1EnvVar[] + base.env = mergeLists(base.env, envs) + } else if (key === 'volumeMounts' && value) { + const volumeMounts = value as k8s.V1VolumeMount[] + base.volumeMounts = mergeLists(base.volumeMounts, volumeMounts) + } else if (key === 'ports' && value) { + const ports = value as k8s.V1ContainerPort[] + base.ports = mergeLists(base.ports, ports) + } else { + base[key] = value + } + } +} + +export function mergePodSpecWithOptions( + base: k8s.V1PodSpec, + from: k8s.V1PodSpec +): void { + for (const [key, value] of Object.entries(from)) { + if (key === 'containers') { + base.containers.push( + ...from.containers.filter(e => !e.name?.startsWith('$')) + ) + } else if (key === 'volumes' && value) { + const volumes = value as k8s.V1Volume[] + base.volumes = mergeLists(base.volumes, volumes) + } else { + base[key] = value + } + } +} + +export function mergeObjectMeta( + base: { metadata?: k8s.V1ObjectMeta }, + from: k8s.V1ObjectMeta +): void { + if (!base.metadata?.labels || !base.metadata?.annotations) { + throw new Error( + "Can't merge metadata: base.metadata or base.annotations field is undefined" + ) + } + if (from?.labels) { + for (const [key, value] of Object.entries(from.labels)) { + if (base.metadata?.labels?.[key]) { + core.warning(`Label ${key} is already defined and will be overwritten`) + } + base.metadata.labels[key] = value + } + } + + if (from?.annotations) { + for (const [key, value] of Object.entries(from.annotations)) { + if (base.metadata?.annotations?.[key]) { + core.warning( + `Annotation ${key} is already defined and will be overwritten` + ) + } + base.metadata.annotations[key] = value + } + } +} + +export function readExtensionFromFile(): k8s.V1PodTemplateSpec | undefined { + const filePath = process.env[ENV_HOOK_TEMPLATE_PATH] + if (!filePath) { + return undefined + } + const doc = yaml.load(fs.readFileSync(filePath, 'utf8')) + if (!doc || typeof doc !== 'object') { + throw new Error(`Failed to parse ${filePath}`) + } + return doc as k8s.V1PodTemplateSpec +} + export enum PodPhase { PENDING = 'Pending', RUNNING = 'Running', @@ -167,3 +265,12 @@ export enum PodPhase { UNKNOWN = 'Unknown', COMPLETED = 'Completed' } + +function mergeLists(base?: T[], from?: T[]): T[] { + const b: T[] = base || [] + if (!from?.length) { + return b + } + b.push(...from) + return b +} diff --git a/packages/k8s/tests/k8s-utils-test.ts b/packages/k8s/tests/k8s-utils-test.ts index a0c55f4..ad8f39c 100644 --- a/packages/k8s/tests/k8s-utils-test.ts +++ b/packages/k8s/tests/k8s-utils-test.ts @@ -1,10 +1,15 @@ -import * as fs from 'fs' +import * as fs from 'fs' import { containerPorts, POD_VOLUME_NAME } from '../src/k8s' import { containerVolumes, generateContainerName, - writeEntryPointScript + writeEntryPointScript, + mergePodSpecWithOptions, + mergeContainerWithOptions, + readExtensionFromFile, + ENV_HOOK_TEMPLATE_PATH } from '../src/k8s/utils' +import * as k8s from '@kubernetes/client-node' import { TestHelper } from './test-setup' let testHelper: TestHelper @@ -328,4 +333,183 @@ describe('k8s utils', () => { expect(() => generateContainerName(':latest')).toThrow() }) }) + + describe('read extension', () => { + beforeEach(async () => { + testHelper = new TestHelper() + await testHelper.initialize() + }) + + afterEach(async () => { + await testHelper.cleanup() + }) + + it('should throw if env variable is set but file does not exist', () => { + process.env[ENV_HOOK_TEMPLATE_PATH] = + '/path/that/does/not/exist/data.yaml' + expect(() => readExtensionFromFile()).toThrow() + }) + + it('should return undefined if env variable is not set', () => { + delete process.env[ENV_HOOK_TEMPLATE_PATH] + expect(readExtensionFromFile()).toBeUndefined() + }) + + it('should throw if file is empty', () => { + let filePath = testHelper.createFile('data.yaml') + process.env[ENV_HOOK_TEMPLATE_PATH] = filePath + expect(() => readExtensionFromFile()).toThrow() + }) + + it('should throw if file is not valid yaml', () => { + let filePath = testHelper.createFile('data.yaml') + fs.writeFileSync(filePath, 'invalid yaml') + process.env[ENV_HOOK_TEMPLATE_PATH] = filePath + expect(() => readExtensionFromFile()).toThrow() + }) + + it('should return object if file is valid', () => { + let filePath = testHelper.createFile('data.yaml') + fs.writeFileSync( + filePath, + ` +metadata: + labels: + label-name: label-value + annotations: + annotation-name: annotation-value +spec: + containers: + - name: test + image: node:14.16 + - name: job + image: ubuntu:latest` + ) + + process.env[ENV_HOOK_TEMPLATE_PATH] = filePath + const extension = readExtensionFromFile() + expect(extension).toBeDefined() + }) + }) + + it('should merge container spec', () => { + const base = { + image: 'node:14.16', + name: 'test', + env: [ + { + name: 'TEST', + value: 'TEST' + } + ], + ports: [ + { + containerPort: 8080, + hostPort: 8080, + protocol: 'TCP' + } + ] + } as k8s.V1Container + + const from = { + ports: [ + { + containerPort: 9090, + hostPort: 9090, + protocol: 'TCP' + } + ], + env: [ + { + name: 'TEST_TWO', + value: 'TEST_TWO' + } + ], + image: 'ubuntu:latest', + name: 'overwrite' + } as k8s.V1Container + + const expectContainer = { + name: base.name, + image: base.image, + ports: [ + ...(base.ports as k8s.V1ContainerPort[]), + ...(from.ports as k8s.V1ContainerPort[]) + ], + env: [...(base.env as k8s.V1EnvVar[]), ...(from.env as k8s.V1EnvVar[])] + } + + const expectJobContainer = JSON.parse(JSON.stringify(expectContainer)) + expectJobContainer.name = base.name + mergeContainerWithOptions(base, from) + expect(base).toStrictEqual(expectContainer) + }) + + it('should merge pod spec', () => { + const base = { + containers: [ + { + image: 'node:14.16', + name: 'test', + env: [ + { + name: 'TEST', + value: 'TEST' + } + ], + ports: [ + { + containerPort: 8080, + hostPort: 8080, + protocol: 'TCP' + } + ] + } + ], + restartPolicy: 'Never' + } as k8s.V1PodSpec + + const from = { + securityContext: { + runAsUser: 1000, + fsGroup: 2000 + }, + restartPolicy: 'Always', + volumes: [ + { + name: 'work', + emptyDir: {} + } + ], + containers: [ + { + image: 'ubuntu:latest', + name: 'side-car', + env: [ + { + name: 'TEST', + value: 'TEST' + } + ], + ports: [ + { + containerPort: 8080, + hostPort: 8080, + protocol: 'TCP' + } + ] + } + ] + } as k8s.V1PodSpec + + const expected = JSON.parse(JSON.stringify(base)) + expected.securityContext = from.securityContext + expected.restartPolicy = from.restartPolicy + expected.volumes = from.volumes + expected.containers.push(from.containers[0]) + + mergePodSpecWithOptions(base, from) + + expect(base).toStrictEqual(expected) + }) }) diff --git a/packages/k8s/tests/prepare-job-test.ts b/packages/k8s/tests/prepare-job-test.ts index f719a87..adb8c1b 100644 --- a/packages/k8s/tests/prepare-job-test.ts +++ b/packages/k8s/tests/prepare-job-test.ts @@ -3,8 +3,15 @@ import * as path from 'path' import { cleanupJob } from '../src/hooks' import { createContainerSpec, prepareJob } from '../src/hooks/prepare-job' import { TestHelper } from './test-setup' -import { generateContainerName } from '../src/k8s/utils' +import { + ENV_HOOK_TEMPLATE_PATH, + generateContainerName, + readExtensionFromFile +} from '../src/k8s/utils' +import { getPodByName } from '../src/k8s' import { V1Container } from '@kubernetes/client-node' +import * as yaml from 'js-yaml' +import { JOB_CONTAINER_NAME } from '../src/hooks/constants' jest.useRealTimers() @@ -83,6 +90,46 @@ describe('Prepare job', () => { expect(services[0].args).toBe(undefined) }) + it('should run pod with extensions applied', async () => { + process.env[ENV_HOOK_TEMPLATE_PATH] = path.join( + __dirname, + '../../../examples/extension.yaml' + ) + + await expect( + prepareJob(prepareJobData.args, prepareJobOutputFilePath) + ).resolves.not.toThrow() + + delete process.env[ENV_HOOK_TEMPLATE_PATH] + + const content = JSON.parse( + fs.readFileSync(prepareJobOutputFilePath).toString() + ) + + const got = await getPodByName(content.state.jobPod) + + expect(got.metadata?.annotations?.['annotated-by']).toBe('extension') + expect(got.metadata?.labels?.['labeled-by']).toBe('extension') + expect(got.spec?.securityContext?.runAsUser).toBe(1000) + expect(got.spec?.securityContext?.runAsGroup).toBe(3000) + + // job container + expect(got.spec?.containers[0].name).toBe(JOB_CONTAINER_NAME) + expect(got.spec?.containers[0].image).toBe('node:14.16') + expect(got.spec?.containers[0].command).toEqual(['sh']) + expect(got.spec?.containers[0].args).toEqual(['-c', 'sleep 50']) + + // service container + expect(got.spec?.containers[1].image).toBe('redis') + expect(got.spec?.containers[1].command).toBeFalsy() + expect(got.spec?.containers[1].args).toBeFalsy() + // side-car + expect(got.spec?.containers[2].name).toBe('side-car') + expect(got.spec?.containers[2].image).toBe('ubuntu:latest') + expect(got.spec?.containers[2].command).toEqual(['sh']) + expect(got.spec?.containers[2].args).toEqual(['-c', 'sleep 60']) + }) + test.each([undefined, null, []])( 'should not throw exception when portMapping=%p', async pm => { diff --git a/packages/k8s/tests/run-container-step-test.ts b/packages/k8s/tests/run-container-step-test.ts index 4b01321..91b9f7d 100644 --- a/packages/k8s/tests/run-container-step-test.ts +++ b/packages/k8s/tests/run-container-step-test.ts @@ -1,5 +1,9 @@ import { runContainerStep } from '../src/hooks' import { TestHelper } from './test-setup' +import { ENV_HOOK_TEMPLATE_PATH } from '../src/k8s/utils' +import * as fs from 'fs' +import * as yaml from 'js-yaml' +import { JOB_CONTAINER_EXTENSION_NAME } from '../src/hooks/constants' jest.useRealTimers() @@ -23,6 +27,47 @@ describe('Run container step', () => { expect(exitCode).toBe(0) }) + it('should run pod with extensions applied', async () => { + const extension = { + metadata: { + annotations: { + foo: 'bar' + }, + labels: { + bar: 'baz' + } + }, + spec: { + containers: [ + { + name: JOB_CONTAINER_EXTENSION_NAME, + command: ['sh'], + args: ['-c', 'echo test'] + }, + { + name: 'side-container', + image: 'ubuntu:latest', + command: ['sh'], + args: ['-c', 'echo test'] + } + ], + restartPolicy: 'Never', + securityContext: { + runAsUser: 1000, + runAsGroup: 3000 + } + } + } + + let filePath = testHelper.createFile() + fs.writeFileSync(filePath, yaml.dump(extension)) + process.env[ENV_HOOK_TEMPLATE_PATH] = filePath + await expect( + runContainerStep(runContainerStepData.args) + ).resolves.not.toThrow() + delete process.env[ENV_HOOK_TEMPLATE_PATH] + }) + it('should shold have env variables available', async () => { runContainerStepData.args.entryPoint = 'bash' runContainerStepData.args.entryPointArgs = [