diff --git a/.vscode/launch.json b/.vscode/launch.json index 81aa20a2..6a876024 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,7 @@ { "version": "0.2.0", "configurations": [ + { "type": "node", "request": "attach", @@ -64,6 +65,16 @@ "NODE_EXTRA_CA_CERTS": "${workspaceFolder}/.env.ca" } }, + { + "type": "node", + "request": "launch", + "name": "Debug operator", + "runtimeExecutable": "npm", + "runtimeArgs": ["run-script", "operator:secrets-dev"], + "cwd": "${workspaceRoot}", + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/.env", + }, { "type": "node", "request": "launch", diff --git a/package-lock.json b/package-lock.json index c4dbb3bb..c693978c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@apidevtools/json-schema-ref-parser": "9.0.6", + "@dot-i/k8s-operator": "^1.3.8", "@kubernetes/client-node": "0.12.3", "@redkubes/gitea-client-node": "1.19.1", "@redkubes/harbor-client-node": "^2.2.1", @@ -767,6 +768,135 @@ "node": ">=12" } }, + "node_modules/@dot-i/k8s-operator": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@dot-i/k8s-operator/-/k8s-operator-1.3.8.tgz", + "integrity": "sha512-GuRTDiLZ9dT4CclEJ6kMWEAbHpa5D23cqJgLpMLrxjWbwiAraLiWGxftwCJxGaTOv6Lsck1XU+GwYe/t9/p2HA==", + "dependencies": { + "@kubernetes/client-node": "^0.18.1", + "async": "^3.2.4", + "gaxios": "^5.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dot-i/k8s-operator/node_modules/@kubernetes/client-node": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.18.1.tgz", + "integrity": "sha512-F3JiK9iZnbh81O/da1tD0h8fQMi/MDttWc/JydyUVnjPEom55wVfnpl4zQ/sWD4uKB8FlxYRPiLwV2ZXB+xPKw==", + "dependencies": { + "@types/js-yaml": "^4.0.1", + "@types/node": "^18.11.17", + "@types/request": "^2.47.1", + "@types/ws": "^8.5.3", + "byline": "^5.0.0", + "isomorphic-ws": "^5.0.0", + "js-yaml": "^4.1.0", + "jsonpath-plus": "^7.2.0", + "request": "^2.88.0", + "rfc4648": "^1.3.0", + "stream-buffers": "^3.0.2", + "tar": "^6.1.11", + "tmp-promise": "^3.0.2", + "tslib": "^2.4.1", + "underscore": "^1.13.6", + "ws": "^8.11.0" + }, + "optionalDependencies": { + "openid-client": "^5.3.0" + } + }, + "node_modules/@dot-i/k8s-operator/node_modules/@types/js-yaml": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.8.tgz", + "integrity": "sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA==" + }, + "node_modules/@dot-i/k8s-operator/node_modules/@types/node": { + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" + }, + "node_modules/@dot-i/k8s-operator/node_modules/@types/ws": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz", + "integrity": "sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@dot-i/k8s-operator/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/@dot-i/k8s-operator/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==", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/@dot-i/k8s-operator/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@dot-i/k8s-operator/node_modules/jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@dot-i/k8s-operator/node_modules/openid-client": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", + "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", + "optional": true, + "dependencies": { + "jose": "^4.15.1", + "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/@dot-i/k8s-operator/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@dot-i/k8s-operator/node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "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/@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -2000,6 +2130,38 @@ "integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=", "dev": true }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -2263,6 +2425,11 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "node_modules/async-retry": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.1.tgz", @@ -5604,6 +5771,50 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6430,6 +6641,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -7176,9 +7420,9 @@ } }, "node_modules/jose": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.3.7.tgz", - "integrity": "sha512-S7Xfsy8nN9Iw/AZxk+ZxEbd5ImIwJPM0TfAo8zI8FF+3lidQ2yiK4dqzsaPKSbZD0woNVSY0KCql6rlKc5V7ug==", + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -9431,9 +9675,9 @@ } }, "node_modules/object-hash": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz", - "integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "engines": { "node": ">= 6" } @@ -9530,9 +9774,9 @@ } }, "node_modules/oidc-token-hash": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz", - "integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==", + "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==", "engines": { "node": "^10.13.0 || >=12.0.0" } @@ -12307,9 +12551,9 @@ } }, "node_modules/underscore": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz", - "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==" + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, "node_modules/union-value": { "version": "1.0.1", @@ -13384,6 +13628,107 @@ "@cspotcode/source-map-consumer": "0.8.0" } }, + "@dot-i/k8s-operator": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@dot-i/k8s-operator/-/k8s-operator-1.3.8.tgz", + "integrity": "sha512-GuRTDiLZ9dT4CclEJ6kMWEAbHpa5D23cqJgLpMLrxjWbwiAraLiWGxftwCJxGaTOv6Lsck1XU+GwYe/t9/p2HA==", + "requires": { + "@kubernetes/client-node": "^0.18.1", + "async": "^3.2.4", + "gaxios": "^5.1.0" + }, + "dependencies": { + "@kubernetes/client-node": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.18.1.tgz", + "integrity": "sha512-F3JiK9iZnbh81O/da1tD0h8fQMi/MDttWc/JydyUVnjPEom55wVfnpl4zQ/sWD4uKB8FlxYRPiLwV2ZXB+xPKw==", + "requires": { + "@types/js-yaml": "^4.0.1", + "@types/node": "^18.11.17", + "@types/request": "^2.47.1", + "@types/ws": "^8.5.3", + "byline": "^5.0.0", + "isomorphic-ws": "^5.0.0", + "js-yaml": "^4.1.0", + "jsonpath-plus": "^7.2.0", + "openid-client": "^5.3.0", + "request": "^2.88.0", + "rfc4648": "^1.3.0", + "stream-buffers": "^3.0.2", + "tar": "^6.1.11", + "tmp-promise": "^3.0.2", + "tslib": "^2.4.1", + "underscore": "^1.13.6", + "ws": "^8.11.0" + } + }, + "@types/js-yaml": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.8.tgz", + "integrity": "sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA==" + }, + "@types/node": { + "version": "18.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" + }, + "@types/ws": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz", + "integrity": "sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==", + "requires": { + "@types/node": "*" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "requires": {} + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==" + }, + "openid-client": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", + "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", + "optional": true, + "requires": { + "jose": "^4.15.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "requires": {} + } + } + }, "@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -14426,6 +14771,29 @@ "integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=", "dev": true }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -14621,6 +14989,11 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "async-retry": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.1.tgz", @@ -17274,6 +17647,32 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "dependencies": { + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + } + } + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -17892,6 +18291,30 @@ } } }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -18444,9 +18867,9 @@ "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" }, "jose": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.3.7.tgz", - "integrity": "sha512-S7Xfsy8nN9Iw/AZxk+ZxEbd5ImIwJPM0TfAo8zI8FF+3lidQ2yiK4dqzsaPKSbZD0woNVSY0KCql6rlKc5V7ug==" + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==" }, "js-tokens": { "version": "4.0.0", @@ -20174,9 +20597,9 @@ } }, "object-hash": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz", - "integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" }, "object-inspect": { "version": "1.11.0", @@ -20243,9 +20666,9 @@ } }, "oidc-token-hash": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz", - "integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==" + "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==" }, "on-finished": { "version": "2.3.0", @@ -22361,9 +22784,9 @@ } }, "underscore": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz", - "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==" + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, "union-value": { "version": "1.0.1", diff --git a/package.json b/package.json index 9b0bc87e..e212200d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@apidevtools/json-schema-ref-parser": "9.0.6", + "@dot-i/k8s-operator": "^1.3.8", "@kubernetes/client-node": "0.12.3", "@redkubes/gitea-client-node": "1.19.1", "@redkubes/harbor-client-node": "^2.2.1", @@ -136,6 +137,8 @@ "tasks:otomi-chart": "node dist/tasks/otomi/otomi-chart.js", "tasks:wait-for-dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 ts-node-dev ./src/tasks/otomi/wait-for.ts", "tasks:wait-for": "node dist/tasks/otomi/wait-for.js", + "operator:secrets-dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 ts-node-dev ./src/operator/secrets.ts", + "operator:secrets": "node dist/operator/secrets.js", "test": "NODE_ENV=test mocha -r ts-node/register -r ts-custom-error --exit src/**/*.test.*" }, "standard-version": { diff --git a/src/operator/secrets.ts b/src/operator/secrets.ts new file mode 100644 index 00000000..d351cbc3 --- /dev/null +++ b/src/operator/secrets.ts @@ -0,0 +1,125 @@ +import Operator, { ResourceEventType } from '@dot-i/k8s-operator' +import { KubernetesObject } from '@dot-i/k8s-operator/node_modules/@kubernetes/client-node/dist' +import * as k8s from '@kubernetes/client-node' +import { KubeConfig } from '@kubernetes/client-node' + +// added the type property which was missing in the original KubernetesObject +interface CustomKubernetesObject extends KubernetesObject { + type: string +} + +async function createNamespacedSecret( + metadata: k8s.V1ObjectMeta | undefined, + targetNamespace: string, + secretType: string, +) { + if (!metadata) return + const simpleSecret = new k8s.V1Secret() + simpleSecret.metadata = { name: `copy-${metadata?.namespace}-${metadata?.name}`, namespace: targetNamespace } + simpleSecret.type = secretType + try { + try { + simpleSecret.data = (await k8sApi.readNamespacedSecret(metadata.name!, metadata.namespace!)).body.data + } catch (error) { + console.debug(`Secret '${metadata.name!}' cannot be found in namespace '${metadata.namespace!}'`) + } + await k8sApi.createNamespacedSecret(targetNamespace, simpleSecret) + console.debug(`Secret '${simpleSecret.metadata.name!}' successfully created in namespace '${targetNamespace}'`) + } catch (err) { + // we know 409 indicates that secret already exists, ignore this code because it will only happen during start of the operator + if (err.response.body.code === 409) return + console.debug(`Error copying secret: statuscode: ${err.response.body.code} - message: ${err.response.body.message}`) + } +} + +const kc = new KubeConfig() +kc.loadFromDefault() +const k8sApi = kc.makeApiClient(k8s.CoreV1Api) +export default class MyOperator extends Operator { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + protected async init() { + await this.watchResource('', 'v1', 'secrets', async (e) => { + const { object } = e + const { metadata, type } = object as CustomKubernetesObject + if (metadata && !metadata.namespace?.startsWith('team-')) return + if (type !== 'kubernetes.io/dockerconfigjson' && type !== 'kubernetes.io/tls') return + const targetNamespace = type === 'kubernetes.io/dockerconfigjson' ? 'argocd' : 'istio-system' + switch (e.type) { + case ResourceEventType.Deleted: { + try { + await k8sApi.deleteNamespacedSecret(`copy-${metadata?.namespace}-${metadata?.name}`, targetNamespace) + console.debug( + `Secret 'copy-${metadata?.namespace}-${metadata?.name}' successfully deleted in namespace '${targetNamespace}'`, + ) + } catch (err) { + console.debug( + `Error deleting copied secret: statuscode: ${err.response.body.code} - message: ${err.response.body.message}`, + ) + } + break + } + case ResourceEventType.Modified: { + const simpleSecret = new k8s.V1Secret() + simpleSecret.metadata = { name: `copy-${metadata?.namespace}-${metadata?.name}`, namespace: targetNamespace } + simpleSecret.type = type + try { + const headers = { 'content-type': 'application/strategic-merge-patch+json' } + try { + simpleSecret.data = (await k8sApi.readNamespacedSecret(metadata!.name!, metadata!.namespace!)).body.data + } catch (error) { + console.debug(`Secret '${metadata!.name!}' cannot be found in namespace '${metadata!.namespace!}'`) + } + await k8sApi.patchNamespacedSecret( + simpleSecret.metadata.name!, + targetNamespace, + simpleSecret, + undefined, + undefined, + undefined, + undefined, + { headers }, + ) + console.debug( + `Secret '${simpleSecret.metadata.name!}' successfully patched in namespace '${targetNamespace}'`, + ) + break + } catch (err) { + console.debug( + `Error patching copied secret: statuscode: ${err.response.body.code} - message: ${err.response.body.message}`, + ) + // we know 404 indicates that a secret does not exist, in this case we recreate a new one because otherwise it will not create a copy + if (err.response.body.code !== 404) break + console.debug('Recreating a copy of the secret') + await createNamespacedSecret(metadata, targetNamespace, type) + break + } + } + case ResourceEventType.Added: { + await createNamespacedSecret(metadata, targetNamespace, type) + break + } + default: + break + } + }) + } +} + +async function main(): Promise { + const operator = new MyOperator() + console.info(`Listening to secrets changes in all namespaces`) + console.info('Setting up namespace prefix filter to "team-"') + await operator.start() + // load teams + // load secrets + const exit = (reason: string) => { + operator.stop() + process.exit(0) + } + + process.on('SIGTERM', () => exit('SIGTERM')).on('SIGINT', () => exit('SIGINT')) +} + +if (typeof require !== 'undefined' && require.main === module) { + main() +} diff --git a/src/tasks/otomi/copy-certs-argo.ts b/src/tasks/otomi/copy-certs-argo.ts deleted file mode 100644 index 50a4f017..00000000 --- a/src/tasks/otomi/copy-certs-argo.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { V1Secret, V1SecretList } from '@kubernetes/client-node' -import { IncomingMessage } from 'http' -import { k8s } from '../../k8s' -import { cleanEnv, TEAM_IDS } from '../../validators' - -const env = cleanEnv({ - TEAM_IDS, -}) - -const targetNamespace = 'argocd' - -const processed: string[] = [] - -export const targetPullSecretsFilter = ({ metadata }: V1Secret): boolean => metadata!.name!.indexOf(`copy-`) === 0 - -// Returns list of names of all pull secrets in the target namespace that were created before. -export const getTargetPullSecretNames = async (): Promise => { - const targetPullSecretsRes = await k8s - .core() - .listNamespacedSecret(targetNamespace, undefined, undefined, undefined, 'type=kubernetes.io/dockerconfigjson') - const { body: pullSecrets }: { body: V1SecretList } = targetPullSecretsRes - const targetPullSecretNames = pullSecrets.items - .filter(targetPullSecretsFilter) - .map((s: V1Secret) => s.metadata!.name!) - console.debug(`Found the following pull secrets in the namespace "${targetNamespace}": ${targetPullSecretNames}`) - return targetPullSecretNames -} - -export const createTargetPullSecret = ( - name: string, - teamId: string, - data: Record, -): Promise<{ response: IncomingMessage; body: V1Secret }> => { - console.info(`Creating Pull secret "${targetNamespace}/${name}"`) - const newSecret: V1Secret = { - ...new V1Secret(), - metadata: { - namespace: targetNamespace, - name, - annotations: { - 'app.kubernetes.io/managed-by': 'otomi', - 'log.otomi.io/copied-from-namespace': teamId, - }, - }, - type: 'kubernetes.io/dockerconfigjson', - data, - } - return k8s.core().createNamespacedSecret(targetNamespace, newSecret) -} - -export const copyTeamPullSecrets = async (teamId: string, targetPullSecretNames: string[]): Promise => { - console.info(`Copying Pull secrets from team-${teamId} to ${targetNamespace} namespace`) - const namespace = `team-${teamId}` - const getTargetSecretName = (name) => `copy-team-${teamId}-${name}` - // get all team namespace Pull secrets - const { - body: { items: teamPullSecrets }, - } = await k8s - .core() - .listNamespacedSecret(namespace, undefined, undefined, undefined, 'type=kubernetes.io/dockerconfigjson') - // create new ones if not existing - await Promise.all( - teamPullSecrets - .filter(({ metadata }) => !targetPullSecretNames.includes(getTargetSecretName(metadata!.name))) - .map(({ metadata, data }) => { - const name = getTargetSecretName(metadata!.name) - return createTargetPullSecret(name, teamId, data as Record) - }), - ) - console.info(`Finished copying Pull secrets from team-${teamId}`) - // update processed list for pruning later - teamPullSecrets.map(({ metadata }) => processed.push(getTargetSecretName(metadata!.name as string))) -} - -export const prunePullSecrets = async (targetPullSecretNames: string[]): Promise => { - const prunableTargetSecrets = targetPullSecretNames.filter((name) => !processed.includes(name)) - await Promise.all( - prunableTargetSecrets.map((name) => { - console.info(`Pruning Harbor pull secret "${targetNamespace}/${name}"`) - return k8s.core().deleteNamespacedSecret(name, targetNamespace) - }), - ) -} - -const main = async (): Promise => { - try { - const targetPullSecretNames = await getTargetPullSecretNames() - await Promise.all( - env.TEAM_IDS.map((teamId) => { - return copyTeamPullSecrets(teamId, targetPullSecretNames) - }), - ) - await prunePullSecrets(targetPullSecretNames) - } catch (e) { - throw new Error(`One or more errors occurred copying pull secrets: ${JSON.stringify(e)}`) - } -} - -main() diff --git a/src/tasks/otomi/copy-certs.test.ts b/src/tasks/otomi/copy-certs.test.ts deleted file mode 100644 index ddba8220..00000000 --- a/src/tasks/otomi/copy-certs.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { V1ObjectMeta, V1Secret } from '@kubernetes/client-node' -import { expect } from 'chai' -import { targetTlsSecretsFilter } from './copy-certs' - -const secretDataBla = { - secret: 'bla', -} -const secretDataDida = { - secret: 'dida', -} -const secretBla = { - ...new V1Secret(), - data: secretDataBla, - metadata: { - ...new V1ObjectMeta(), - annotations: { - 'app.kubernetes.io/managed-by': 'otomi', - }, - name: 'copy-dev-bla', - }, -} -const secretDida = { - ...new V1Secret(), - data: secretDataDida, - metadata: { - ...new V1ObjectMeta(), - annotations: { - 'app.kubernetes.io/managed-by': 'otomi', - }, - name: 'copy-demo-dida', - }, -} -const secretNo = { - ...new V1Secret(), - data: secretDataDida, - metadata: { - ...new V1ObjectMeta(), - name: 'nomatch', - }, -} - -const targetTlsSecretsFiltered = [secretBla, secretDida] -const targetTlsSecrets = [secretBla, secretDida, secretNo] - -describe('Task: copy certs', () => { - it('targetTlsSecretsFilter should filter copied secrets', () => { - expect(targetTlsSecrets.filter(targetTlsSecretsFilter)).to.deep.equal(targetTlsSecretsFiltered) - }) -}) diff --git a/src/tasks/otomi/copy-certs.ts b/src/tasks/otomi/copy-certs.ts deleted file mode 100644 index f9bb0855..00000000 --- a/src/tasks/otomi/copy-certs.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { V1Secret, V1SecretList } from '@kubernetes/client-node' -import { IncomingMessage } from 'http' -import { k8s } from '../../k8s' -import { cleanEnv, OTOMI_FLAGS, TEAM_IDS } from '../../validators' - -const env = cleanEnv({ - OTOMI_FLAGS, - TEAM_IDS, -}) - -let targetNamespace = 'istio-system' - -const processed: string[] = [] - -export const targetTlsSecretsFilter = ({ metadata }: V1Secret): boolean => - metadata!.name!.indexOf(`copy-`) === 0 && metadata!.annotations!['app.kubernetes.io/managed-by'] === 'otomi' - -// Returns list of names of all TLS secrets in the target namespace that were created before. -export const getTargetTlsSecretNames = async (): Promise => { - const targetTlsSecretsRes = await k8s - .core() - .listNamespacedSecret(targetNamespace, undefined, undefined, undefined, 'type=kubernetes.io/tls') - const { body: tlsSecrets }: { body: V1SecretList } = targetTlsSecretsRes - const targetTlsSecretNames = tlsSecrets.items.filter(targetTlsSecretsFilter).map((s: V1Secret) => s.metadata!.name!) - console.debug(`Found the following TLS secrets in the namespace "${targetNamespace}": ${targetTlsSecretNames}`) - return targetTlsSecretNames -} - -export const createTargetTlsSecret = ( - name: string, - teamId: string, - data: Record, -): Promise<{ response: IncomingMessage; body: V1Secret }> => { - console.info(`Creating TLS secret "${targetNamespace}/${name}"`) - const newSecret: V1Secret = { - ...new V1Secret(), - metadata: { - namespace: targetNamespace, - name, - annotations: { - 'app.kubernetes.io/managed-by': 'otomi', - 'log.otomi.io/copied-from-namespace': teamId, - }, - }, - type: 'kubernetes.io/tls', - data, - } - return k8s.core().createNamespacedSecret(targetNamespace, newSecret) -} - -export const copyTeamTlsSecrets = async (teamId: string, targetTlsSecretNames: string[]): Promise => { - console.info(`Copying TLS secrets from team-${teamId} to ${targetNamespace} namespace`) - const namespace = `team-${teamId}` - const getTargetSecretName = (name) => `copy-${teamId}-${name}` - // get all target namespace TLS secrets - const { - body: { items: teamTlsSecrets }, - } = await k8s.core().listNamespacedSecret(namespace, undefined, undefined, undefined, 'type=kubernetes.io/tls') - // create new ones if not existing - await Promise.all( - teamTlsSecrets - .filter(({ metadata }) => !targetTlsSecretNames.includes(getTargetSecretName(metadata!.name))) - .map(({ metadata, data }) => { - const name = getTargetSecretName(metadata!.name) - return createTargetTlsSecret(name, teamId, data as Record) - }), - ) - console.info(`Finished copying TLS secrets from team-${teamId}`) - // update processed list for pruning later - teamTlsSecrets.map(({ metadata }) => processed.push(getTargetSecretName(metadata!.name as string))) -} - -export const pruneTlsSecrets = async (targetTlsSecretNames: string[]): Promise => { - const prunableTargetSecrets = targetTlsSecretNames.filter((name) => !processed.includes(name)) - await Promise.all( - prunableTargetSecrets.map((name) => { - console.info(`Pruning TLS secret "${targetNamespace}/${name}"`) - return k8s.core().deleteNamespacedSecret(name, targetNamespace) - }), - ) -} - -const main = async (): Promise => { - if (env.OTOMI_FLAGS.hasCloudLB) targetNamespace = 'ingress' - try { - const targetTlsSecretNames = await getTargetTlsSecretNames() - await Promise.all( - env.TEAM_IDS.map((teamId) => { - return copyTeamTlsSecrets(teamId, targetTlsSecretNames) - }), - ) - await pruneTlsSecrets(targetTlsSecretNames) - } catch (e) { - throw new Error(`One or more errors occurred copying TLS secrets: ${JSON.stringify(e)}`) - } -} - -main()