mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
add authentik
This commit is contained in:
@@ -46,6 +46,6 @@ export default tseslint.config(
|
||||
},
|
||||
...compat.extends('plugin:prettier/recommended'),
|
||||
{
|
||||
ignores: ['**/node_modules/', '**/dist/', '**/.turbo/', '**/generated/'],
|
||||
ignores: ['**/node_modules/', '**/dist/', '**/.turbo/', '**/generated/', '**/clients/*.types.ts'],
|
||||
},
|
||||
);
|
||||
|
||||
16
package.json
16
package.json
@@ -4,15 +4,16 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"nodemon": "^3.1.10",
|
||||
"@eslint/eslintrc": "3.3.1",
|
||||
"@eslint/js": "9.32.0",
|
||||
"@pnpm/find-workspace-packages": "6.0.9",
|
||||
"@types/bun": "latest",
|
||||
"eslint": "9.32.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-prettier": "5.5.3",
|
||||
"nodemon": "^3.1.10",
|
||||
"openapi-typescript": "^7.8.0",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.38.0"
|
||||
@@ -21,17 +22,24 @@
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@goauthentik/api": "2025.6.3-1751754396",
|
||||
"@kubernetes/client-node": "^1.3.0",
|
||||
"@sinclair/typebox": "^0.34.38",
|
||||
"dotenv": "^17.2.1",
|
||||
"knex": "^3.1.0",
|
||||
"openapi-fetch": "^0.14.0",
|
||||
"pg": "^8.16.3",
|
||||
"sqlite3": "^5.1.7"
|
||||
"sqlite3": "^5.1.7",
|
||||
"yaml": "^2.8.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.0",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"sqlite3"
|
||||
]
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"@kubernetes/client-node": "patches/@kubernetes__client-node.patch"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo 'No tests'",
|
||||
|
||||
14
patches/@kubernetes__client-node.patch
Normal file
14
patches/@kubernetes__client-node.patch
Normal file
@@ -0,0 +1,14 @@
|
||||
diff --git a/dist/gen/models/ObjectSerializer.js b/dist/gen/models/ObjectSerializer.js
|
||||
index 1d798b6a2d7c059165d1df9fbb77b89a8317ebca..c8bacfdc95be0f0146c6505f89a9372e013afea4 100644
|
||||
--- a/dist/gen/models/ObjectSerializer.js
|
||||
+++ b/dist/gen/models/ObjectSerializer.js
|
||||
@@ -2216,6 +2216,9 @@ export class ObjectSerializer {
|
||||
return transformedData;
|
||||
}
|
||||
else if (type === "Date") {
|
||||
+ if (typeof data === "string") {
|
||||
+ return data;
|
||||
+ }
|
||||
if (format == "date") {
|
||||
let month = data.getMonth() + 1;
|
||||
month = month < 10 ? "0" + month.toString() : month.toString();
|
||||
239
pnpm-lock.yaml
generated
239
pnpm-lock.yaml
generated
@@ -4,25 +4,42 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
'@kubernetes/client-node':
|
||||
hash: 0b0e5d32aa2930107c8c9b45df2639faf53fa12a389a551885d6e42d71f9429d
|
||||
path: patches/@kubernetes__client-node.patch
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@goauthentik/api':
|
||||
specifier: 2025.6.3-1751754396
|
||||
version: 2025.6.3-1751754396
|
||||
'@kubernetes/client-node':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0(encoding@0.1.13)
|
||||
version: 1.3.0(patch_hash=0b0e5d32aa2930107c8c9b45df2639faf53fa12a389a551885d6e42d71f9429d)(encoding@0.1.13)
|
||||
'@sinclair/typebox':
|
||||
specifier: ^0.34.38
|
||||
version: 0.34.38
|
||||
dotenv:
|
||||
specifier: ^17.2.1
|
||||
version: 17.2.1
|
||||
knex:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0(pg@8.16.3)(sqlite3@5.1.7)
|
||||
openapi-fetch:
|
||||
specifier: ^0.14.0
|
||||
version: 0.14.0
|
||||
pg:
|
||||
specifier: ^8.16.3
|
||||
version: 8.16.3
|
||||
sqlite3:
|
||||
specifier: ^5.1.7
|
||||
version: 5.1.7
|
||||
yaml:
|
||||
specifier: ^2.8.0
|
||||
version: 2.8.0
|
||||
devDependencies:
|
||||
'@eslint/eslintrc':
|
||||
specifier: 3.3.1
|
||||
@@ -51,6 +68,9 @@ importers:
|
||||
nodemon:
|
||||
specifier: ^3.1.10
|
||||
version: 3.1.10
|
||||
openapi-typescript:
|
||||
specifier: ^7.8.0
|
||||
version: 7.8.0(typescript@5.8.3)
|
||||
prettier:
|
||||
specifier: 3.6.2
|
||||
version: 3.6.2
|
||||
@@ -112,6 +132,9 @@ packages:
|
||||
'@gar/promisify@1.1.3':
|
||||
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
|
||||
|
||||
'@goauthentik/api@2025.6.3-1751754396':
|
||||
resolution: {integrity: sha512-JL7V1OTRIYJYzWvqoo2TNEQZirFHHkUt2tludO82luVEPDZwWQ8NwvwcFvO74v/SDSqg9VGN6H9BDoUrPD6KHQ==}
|
||||
|
||||
'@gwhitney/detect-indent@7.0.1':
|
||||
resolution: {integrity: sha512-7bQW+gkKa2kKZPeJf6+c6gFK9ARxQfn+FKy9ScTBppyKRWH2KzsmweXUoklqeEiHiNVWaeP5csIdsNq6w7QhzA==}
|
||||
engines: {node: '>=12.20'}
|
||||
@@ -316,6 +339,16 @@ packages:
|
||||
resolution: {integrity: sha512-zU4vDfBUx/jUBPmR4CzCqPDOPObb/7iLT3UZvhXSJ8ZXDo9214V6agnJvxQ6bYBcypdiKva0hnb3tmo1chQBYg==}
|
||||
engines: {node: '>=16.14'}
|
||||
|
||||
'@redocly/ajv@8.11.2':
|
||||
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
|
||||
|
||||
'@redocly/config@0.22.2':
|
||||
resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==}
|
||||
|
||||
'@redocly/openapi-core@1.34.5':
|
||||
resolution: {integrity: sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==}
|
||||
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
|
||||
|
||||
'@rtsao/scc@1.1.0':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
@@ -458,6 +491,10 @@ packages:
|
||||
ansi-align@3.0.1:
|
||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||
|
||||
ansi-colors@4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ansi-diff@1.2.0:
|
||||
resolution: {integrity: sha512-BIXwHKpjzghBjcwEV10Y4b17tjHfK4nhEqK3LqyQ3JgcMcjmi3DIevozNgrOpfvBMmrq9dfvrPJSu5/5vNUBQg==}
|
||||
|
||||
@@ -650,6 +687,9 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
change-case@5.4.4:
|
||||
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
|
||||
|
||||
char-regex@1.0.2:
|
||||
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -692,6 +732,9 @@ packages:
|
||||
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
|
||||
hasBin: true
|
||||
|
||||
colorette@1.4.0:
|
||||
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
|
||||
|
||||
colorette@2.0.19:
|
||||
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
|
||||
|
||||
@@ -801,6 +844,10 @@ packages:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
dotenv@17.2.1:
|
||||
resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1164,6 +1211,10 @@ packages:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
@@ -1201,6 +1252,10 @@ packages:
|
||||
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
index-to-position@1.1.0:
|
||||
resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
individual@3.0.0:
|
||||
resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==}
|
||||
|
||||
@@ -1377,6 +1432,10 @@ packages:
|
||||
jose@6.0.12:
|
||||
resolution: {integrity: sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==}
|
||||
|
||||
js-levenshtein@1.1.6:
|
||||
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -1400,6 +1459,9 @@ packages:
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
@@ -1530,6 +1592,10 @@ packages:
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
minimatch@5.1.6:
|
||||
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -1679,6 +1745,18 @@ packages:
|
||||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
openapi-fetch@0.14.0:
|
||||
resolution: {integrity: sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==}
|
||||
|
||||
openapi-typescript-helpers@0.0.15:
|
||||
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
|
||||
|
||||
openapi-typescript@7.8.0:
|
||||
resolution: {integrity: sha512-1EeVWmDzi16A+siQlo/SwSGIT7HwaFAVjvMA7/jG5HMLSnrUOzPL7uSTRZZa4v/LCRxHTApHKtNY6glApEoiUQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ^5.x
|
||||
|
||||
openid-client@6.6.2:
|
||||
resolution: {integrity: sha512-Xya5TNMnnZuTM6DbHdB4q0S3ig2NTAELnii/ASie1xDEr8iiB8zZbO871OWBdrw++sd3hW6bqWjgcmSy1RTWHA==}
|
||||
|
||||
@@ -1722,6 +1800,10 @@ packages:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
parse-json@8.3.0:
|
||||
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
parse-ms@2.1.0:
|
||||
resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1796,6 +1878,10 @@ packages:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1913,6 +1999,10 @@ packages:
|
||||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2145,6 +2235,10 @@ packages:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
supports-color@10.0.0:
|
||||
resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
supports-color@5.5.0:
|
||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2230,6 +2324,10 @@ packages:
|
||||
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2281,6 +2379,9 @@ packages:
|
||||
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
uri-js-replace@1.0.1:
|
||||
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
@@ -2367,6 +2468,18 @@ packages:
|
||||
yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
yaml-ast-parser@0.0.43:
|
||||
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
|
||||
|
||||
yaml@2.8.0:
|
||||
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2391,7 +2504,7 @@ snapshots:
|
||||
'@eslint/config-array@0.21.0':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.6
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
minimatch: 3.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -2405,7 +2518,7 @@ snapshots:
|
||||
'@eslint/eslintrc@3.3.1':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
espree: 10.4.0
|
||||
globals: 14.0.0
|
||||
ignore: 5.3.2
|
||||
@@ -2428,6 +2541,8 @@ snapshots:
|
||||
'@gar/promisify@1.1.3':
|
||||
optional: true
|
||||
|
||||
'@goauthentik/api@2025.6.3-1751754396': {}
|
||||
|
||||
'@gwhitney/detect-indent@7.0.1': {}
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
@@ -2451,7 +2566,7 @@ snapshots:
|
||||
dependencies:
|
||||
jsep: 1.4.0
|
||||
|
||||
'@kubernetes/client-node@1.3.0(encoding@0.1.13)':
|
||||
'@kubernetes/client-node@1.3.0(patch_hash=0b0e5d32aa2930107c8c9b45df2639faf53fa12a389a551885d6e42d71f9429d)(encoding@0.1.13)':
|
||||
dependencies:
|
||||
'@types/js-yaml': 4.0.9
|
||||
'@types/node': 22.16.5
|
||||
@@ -2736,6 +2851,29 @@ snapshots:
|
||||
write-file-atomic: 5.0.1
|
||||
write-yaml-file: 5.0.0
|
||||
|
||||
'@redocly/ajv@8.11.2':
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
uri-js-replace: 1.0.1
|
||||
|
||||
'@redocly/config@0.22.2': {}
|
||||
|
||||
'@redocly/openapi-core@1.34.5(supports-color@10.0.0)':
|
||||
dependencies:
|
||||
'@redocly/ajv': 8.11.2
|
||||
'@redocly/config': 0.22.2
|
||||
colorette: 1.4.0
|
||||
https-proxy-agent: 7.0.6(supports-color@10.0.0)
|
||||
js-levenshtein: 1.1.6
|
||||
js-yaml: 4.1.0
|
||||
minimatch: 5.1.6
|
||||
pluralize: 8.0.0
|
||||
yaml-ast-parser: 0.0.43
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@sinclair/typebox@0.34.38': {}
|
||||
@@ -2805,7 +2943,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.38.0
|
||||
'@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3)
|
||||
'@typescript-eslint/visitor-keys': 8.38.0
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
eslint: 9.32.0
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
@@ -2815,7 +2953,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3)
|
||||
'@typescript-eslint/types': 8.38.0
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -2834,7 +2972,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.38.0
|
||||
'@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.38.0(eslint@9.32.0)(typescript@5.8.3)
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
eslint: 9.32.0
|
||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
@@ -2849,7 +2987,7 @@ snapshots:
|
||||
'@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3)
|
||||
'@typescript-eslint/types': 8.38.0
|
||||
'@typescript-eslint/visitor-keys': 8.38.0
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
fast-glob: 3.3.3
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
@@ -2890,7 +3028,7 @@ snapshots:
|
||||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
@@ -2919,6 +3057,8 @@ snapshots:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-diff@1.2.0:
|
||||
dependencies:
|
||||
ansi-split: 1.0.1
|
||||
@@ -3164,6 +3304,8 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
change-case@5.4.4: {}
|
||||
|
||||
char-regex@1.0.2: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
@@ -3203,6 +3345,8 @@ snapshots:
|
||||
color-support@1.1.3:
|
||||
optional: true
|
||||
|
||||
colorette@1.4.0: {}
|
||||
|
||||
colorette@2.0.19: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
@@ -3259,6 +3403,16 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.1(supports-color@10.0.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 10.0.0
|
||||
|
||||
debug@4.4.1(supports-color@5.5.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -3300,6 +3454,8 @@ snapshots:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dotenv@17.2.1: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -3500,7 +3656,7 @@ snapshots:
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.4.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
@@ -3757,7 +3913,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@tootallnate/once': 1.1.2
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
@@ -3765,11 +3921,18 @@ snapshots:
|
||||
https-proxy-agent@5.0.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
https-proxy-agent@7.0.6(supports-color@10.0.0):
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
humanize-ms@1.2.1:
|
||||
@@ -3800,6 +3963,8 @@ snapshots:
|
||||
indent-string@4.0.0:
|
||||
optional: true
|
||||
|
||||
index-to-position@1.1.0: {}
|
||||
|
||||
individual@3.0.0: {}
|
||||
|
||||
infer-owner@1.0.4:
|
||||
@@ -3968,6 +4133,8 @@ snapshots:
|
||||
|
||||
jose@6.0.12: {}
|
||||
|
||||
js-levenshtein@1.1.6: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.1.0:
|
||||
@@ -3984,6 +4151,8 @@ snapshots:
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json-stringify-safe@5.0.1: {}
|
||||
@@ -4114,6 +4283,10 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
minimatch@5.1.6:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
@@ -4289,6 +4462,22 @@ snapshots:
|
||||
dependencies:
|
||||
mimic-fn: 2.1.0
|
||||
|
||||
openapi-fetch@0.14.0:
|
||||
dependencies:
|
||||
openapi-typescript-helpers: 0.0.15
|
||||
|
||||
openapi-typescript-helpers@0.0.15: {}
|
||||
|
||||
openapi-typescript@7.8.0(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@redocly/openapi-core': 1.34.5(supports-color@10.0.0)
|
||||
ansi-colors: 4.1.3
|
||||
change-case: 5.4.4
|
||||
parse-json: 8.3.0
|
||||
supports-color: 10.0.0
|
||||
typescript: 5.8.3
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
openid-client@6.6.2:
|
||||
dependencies:
|
||||
jose: 6.0.12
|
||||
@@ -4341,6 +4530,12 @@ snapshots:
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
lines-and-columns: 1.2.4
|
||||
|
||||
parse-json@8.3.0:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
index-to-position: 1.1.0
|
||||
type-fest: 4.41.0
|
||||
|
||||
parse-ms@2.1.0: {}
|
||||
|
||||
path-absolute@1.0.1: {}
|
||||
@@ -4401,6 +4596,8 @@ snapshots:
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postgres-array@2.0.0: {}
|
||||
@@ -4521,6 +4718,8 @@ snapshots:
|
||||
gopd: 1.2.0
|
||||
set-function-name: 2.0.2
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
@@ -4667,7 +4866,7 @@ snapshots:
|
||||
socks-proxy-agent@6.2.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
socks: 2.8.6
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -4676,7 +4875,7 @@ snapshots:
|
||||
socks-proxy-agent@8.0.5:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
debug: 4.4.1
|
||||
socks: 2.8.6
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -4790,6 +4989,8 @@ snapshots:
|
||||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
supports-color@10.0.0: {}
|
||||
|
||||
supports-color@5.5.0:
|
||||
dependencies:
|
||||
has-flag: 3.0.0
|
||||
@@ -4889,6 +5090,8 @@ snapshots:
|
||||
|
||||
type-fest@0.6.0: {}
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -4962,6 +5165,8 @@ snapshots:
|
||||
dependencies:
|
||||
crypto-random-string: 2.0.0
|
||||
|
||||
uri-js-replace@1.0.1: {}
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
@@ -5063,4 +5268,10 @@ snapshots:
|
||||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yaml-ast-parser@0.0.43: {}
|
||||
|
||||
yaml@2.8.0: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: 'homelab.mortenolsen.pro/v1';
|
||||
kind: 'PostgresDatabase';
|
||||
name: 'test2';
|
||||
namespace: 'playground';
|
||||
foo: 'bar';
|
||||
foo: 'bar';
|
||||
{
|
||||
}
|
||||
24
scripts/create-clients.ts
Normal file
24
scripts/create-clients.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import fs from 'node:fs';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
import YAML from 'yaml';
|
||||
import openapiTS, { astToString } from 'openapi-typescript';
|
||||
|
||||
const schemaRequest = await fetch('https://authentik.olsen.cloud/api/v3/schema/');
|
||||
if (!schemaRequest.ok) {
|
||||
console.error(schemaRequest.status, schemaRequest.statusText);
|
||||
throw new Error('Failed to fetch schema');
|
||||
}
|
||||
const schemaYaml = await schemaRequest.text();
|
||||
const schema = YAML.parse(schemaYaml);
|
||||
|
||||
const ast = await openapiTS(schema);
|
||||
const contents = astToString(ast);
|
||||
|
||||
const targetLocation = resolve(import.meta.dirname, '..', 'src', 'clients', 'authentik', 'authentik.types.d.ts');
|
||||
await mkdir(dirname(targetLocation), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
targetLocation,
|
||||
['// This file is generated by scripts/create-clients.ts', '/* eslint-disable */', contents].join('\n'),
|
||||
);
|
||||
2
scripts/recreate.bash
Executable file
2
scripts/recreate.bash
Executable file
@@ -0,0 +1,2 @@
|
||||
kubectl delete -f "$1"
|
||||
kubectl apply -f "$1"
|
||||
33
src/clients/authentik/authentik.ts
Normal file
33
src/clients/authentik/authentik.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
Configuration,
|
||||
CoreApi,
|
||||
FlowsApi,
|
||||
PropertymappingsApi,
|
||||
ProvidersApi,
|
||||
instanceOfErrorDetail,
|
||||
} from '@goauthentik/api';
|
||||
|
||||
type CreateAuthentikClientOptions = {
|
||||
baseUrl: string;
|
||||
token: string;
|
||||
};
|
||||
const createAuthentikClient = ({ baseUrl, token }: CreateAuthentikClientOptions) => {
|
||||
const config = new Configuration({
|
||||
basePath: baseUrl,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const client = {
|
||||
core: new CoreApi(config),
|
||||
providers: new ProvidersApi(config),
|
||||
propertymappings: new PropertymappingsApi(config),
|
||||
flows: new FlowsApi(config),
|
||||
};
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
type AuthentikClient = ReturnType<typeof createAuthentikClient>;
|
||||
|
||||
export { createAuthentikClient, type AuthentikClient, instanceOfErrorDetail };
|
||||
58661
src/clients/authentik/authentik.types.d.ts
vendored
Normal file
58661
src/clients/authentik/authentik.types.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
68
src/crds/authentik/client/client.ts
Normal file
68
src/crds/authentik/client/client.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { SubModeEnum } from '@goauthentik/api';
|
||||
|
||||
import { CustomResource, type CustomResourceHandlerOptions } from '../../../custom-resource/custom-resource.base.ts';
|
||||
import { AuthentikService } from '../../../services/authentik/authentik.service.ts';
|
||||
|
||||
const authentikClientSpec = Type.Object({
|
||||
subMode: Type.Optional(Type.Unsafe<SubModeEnum>(Type.String())),
|
||||
clientType: Type.Optional(
|
||||
Type.Unsafe<'confidential' | 'public'>(
|
||||
Type.String({
|
||||
enum: ['confidential', 'public'],
|
||||
}),
|
||||
),
|
||||
),
|
||||
redirectUris: Type.Array(
|
||||
Type.Object({
|
||||
url: Type.String(),
|
||||
matchingMode: Type.Unsafe<'strict' | 'regex'>(
|
||||
Type.String({
|
||||
enum: ['strict', 'regex'],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
});
|
||||
const authentikClientSecret = Type.Object({
|
||||
clientSecret: Type.String(),
|
||||
});
|
||||
|
||||
class AuthentikClient extends CustomResource<typeof authentikClientSpec> {
|
||||
constructor() {
|
||||
super({
|
||||
kind: 'AuthentikClient',
|
||||
names: {
|
||||
singular: 'authentikclient',
|
||||
plural: 'authentikclients',
|
||||
},
|
||||
spec: authentikClientSpec,
|
||||
});
|
||||
}
|
||||
|
||||
public update = async (options: CustomResourceHandlerOptions<typeof authentikClientSpec>) => {
|
||||
const { request, services, ensureSecret } = options;
|
||||
const authentikService = services.get(AuthentikService);
|
||||
const { clientSecret } = await ensureSecret({
|
||||
name: `authentik-client-${request.metadata.name}`,
|
||||
namespace: request.metadata.namespace ?? 'default',
|
||||
schema: authentikClientSecret,
|
||||
generator: async () => ({
|
||||
clientSecret: crypto.randomUUID(),
|
||||
}),
|
||||
});
|
||||
const client = await authentikService.upsertClient({
|
||||
name: request.metadata.name,
|
||||
secret: clientSecret,
|
||||
subMode: request.spec.subMode,
|
||||
clientType: request.spec.clientType,
|
||||
redirectUris: request.spec.redirectUris.map((rule) => ({
|
||||
url: rule.url,
|
||||
matchingMode: rule.matchingMode ?? 'strict',
|
||||
})),
|
||||
});
|
||||
console.log(client.config);
|
||||
};
|
||||
}
|
||||
|
||||
export { AuthentikClient };
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { ApiException, type V1Secret } from '@kubernetes/client-node';
|
||||
|
||||
import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts';
|
||||
import { K8sService } from '../../services/k8s.ts';
|
||||
import type { CustomResourceRequest } from '../../custom-resource/custom-resource.request.ts';
|
||||
import { PostgresService } from '../../services/postgres/postgres.service.ts';
|
||||
|
||||
const postgresDatabaseSpecSchema = Type.Object({});
|
||||
@@ -20,99 +17,39 @@ class PostgresDatabase extends CustomResource<typeof postgresDatabaseSpecSchema>
|
||||
});
|
||||
}
|
||||
|
||||
#getVariables = async (request: CustomResourceRequest<typeof postgresDatabaseSpecSchema>) => {
|
||||
const { metadata, services } = request;
|
||||
const k8sService = services.get(K8sService);
|
||||
|
||||
const secretName = `postgres-database-${metadata.name}`;
|
||||
let secret: V1Secret | undefined;
|
||||
|
||||
try {
|
||||
secret = await k8sService.api.readNamespacedSecret({
|
||||
name: secretName,
|
||||
namespace: metadata.namespace ?? 'default',
|
||||
});
|
||||
} catch (error) {
|
||||
if (!(error instanceof ApiException && error.code === 404)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (secret && request.isOwnerOf(secret) && secret.data) {
|
||||
services.log.debug('PostgresRole secret found', { secret });
|
||||
return secret.data;
|
||||
}
|
||||
|
||||
if (secret && !request.isOwnerOf(secret)) {
|
||||
throw new Error('The secret is not owned by this resource');
|
||||
}
|
||||
|
||||
const data = {
|
||||
name: Buffer.from(`${metadata.namespace}_${metadata.name}`).toString('base64'),
|
||||
user: Buffer.from(metadata.name).toString('base64'),
|
||||
password: Buffer.from(crypto.randomUUID()).toString('base64'),
|
||||
};
|
||||
const namespace = metadata.namespace ?? 'default';
|
||||
|
||||
services.log.debug('Creating secret', { data });
|
||||
const response = await k8sService.api.createNamespacedSecret({
|
||||
namespace,
|
||||
body: {
|
||||
kind: 'Secret',
|
||||
metadata: {
|
||||
name: secretName,
|
||||
namespace,
|
||||
ownerReferences: [
|
||||
{
|
||||
apiVersion: request.apiVersion,
|
||||
kind: request.kind,
|
||||
name: metadata.name,
|
||||
uid: metadata.uid,
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'Opaque',
|
||||
data,
|
||||
},
|
||||
});
|
||||
services.log.debug('Secret created', { response });
|
||||
return response.data!;
|
||||
};
|
||||
|
||||
public update = async (options: CustomResourceHandlerOptions<typeof postgresDatabaseSpecSchema>) => {
|
||||
const { request, services } = options;
|
||||
const status = await request.getStatus();
|
||||
|
||||
try {
|
||||
const variables = await this.#getVariables(request);
|
||||
const { request, services, ensureSecret } = options;
|
||||
const variables = await ensureSecret({
|
||||
name: `postgres-database-${request.metadata.name}`,
|
||||
namespace: request.metadata.namespace ?? 'default',
|
||||
schema: Type.Object({
|
||||
name: Type.String(),
|
||||
user: Type.String(),
|
||||
password: Type.String(),
|
||||
}),
|
||||
generator: async () => ({
|
||||
name: `${request.metadata.namespace || 'default'}_${request.metadata.name}`,
|
||||
user: `${request.metadata.namespace || 'default'}_${request.metadata.name}`,
|
||||
password: `password_${Buffer.from(crypto.getRandomValues(new Uint8Array(12))).toString('hex')}`,
|
||||
}),
|
||||
});
|
||||
const postgresService = services.get(PostgresService);
|
||||
await postgresService.upsertRole({
|
||||
name: Buffer.from(variables.user!, 'base64').toString('utf-8'),
|
||||
password: Buffer.from(variables.password!, 'base64').toString('utf-8'),
|
||||
name: variables.user,
|
||||
password: variables.password,
|
||||
});
|
||||
|
||||
await postgresService.upsertDatabase({
|
||||
name: Buffer.from(variables.name!, 'base64').toString('utf-8'),
|
||||
owner: Buffer.from(variables.user!, 'base64').toString('utf-8'),
|
||||
name: variables.name,
|
||||
owner: variables.user,
|
||||
});
|
||||
|
||||
status.setCondition('Ready', {
|
||||
status: 'True',
|
||||
reason: 'Ready',
|
||||
message: 'Role created',
|
||||
await request.addEvent({
|
||||
type: 'Normal',
|
||||
reason: 'DatabaseUpserted',
|
||||
message: 'Database has been upserted',
|
||||
action: 'UPSERT',
|
||||
});
|
||||
services.log.info('PostgresRole updated', { status });
|
||||
return await status.save();
|
||||
} catch (error) {
|
||||
const status = await request.getStatus();
|
||||
status.setCondition('Ready', {
|
||||
status: 'False',
|
||||
reason: 'Error',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
services.log.error('Error updating PostgresRole', { error });
|
||||
return await status.save();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { ApiException, type V1Secret } from '@kubernetes/client-node';
|
||||
|
||||
import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts';
|
||||
import { K8sService } from '../../services/k8s.ts';
|
||||
|
||||
const stringValueSchema = Type.String({
|
||||
const stringValueSchema = Type.Object({
|
||||
key: Type.String(),
|
||||
chars: Type.Optional(Type.String()),
|
||||
length: Type.Optional(Type.Number()),
|
||||
@@ -33,71 +31,18 @@ class SecretRequest extends CustomResource<typeof secretRequestSpec> {
|
||||
});
|
||||
}
|
||||
|
||||
#createSecret = async (options: CustomResourceHandlerOptions<typeof secretRequestSpec>) => {
|
||||
const { request, services } = options;
|
||||
const { apiVersion, kind, spec, metadata } = request;
|
||||
const { secretName = metadata.name } = spec;
|
||||
const { namespace = 'default' } = metadata;
|
||||
const k8sService = services.get(K8sService);
|
||||
let current: V1Secret | undefined;
|
||||
try {
|
||||
current = await k8sService.api.readNamespacedSecret({
|
||||
name: secretName,
|
||||
namespace,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!(error instanceof ApiException && error.code === 404)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (current) {
|
||||
services.log.debug('secret already exists', { current });
|
||||
// TODO: Add update logic
|
||||
return;
|
||||
}
|
||||
await k8sService.api.createNamespacedSecret({
|
||||
namespace,
|
||||
body: {
|
||||
kind: 'Secret',
|
||||
metadata: {
|
||||
name: secretName,
|
||||
namespace,
|
||||
ownerReferences: [
|
||||
{
|
||||
apiVersion,
|
||||
kind,
|
||||
name: metadata.name,
|
||||
uid: metadata.uid,
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'Opaque',
|
||||
data: {
|
||||
// TODO: generate data from spec
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
public update = async (options: CustomResourceHandlerOptions<typeof secretRequestSpec>) => {
|
||||
const { request } = options;
|
||||
const status = await request.getStatus();
|
||||
try {
|
||||
await this.#createSecret(options);
|
||||
status.setCondition('Ready', {
|
||||
status: 'True',
|
||||
reason: 'SecretCreated',
|
||||
message: 'Secret created',
|
||||
const { request, ensureSecret } = options;
|
||||
const { secretName = request.metadata.name } = request.spec;
|
||||
const { namespace = request.metadata.namespace ?? 'default' } = request.metadata;
|
||||
await ensureSecret({
|
||||
name: secretName,
|
||||
namespace,
|
||||
schema: Type.Object({}, { additionalProperties: true }),
|
||||
generator: async () => ({
|
||||
hello: 'world',
|
||||
}),
|
||||
});
|
||||
return await status.save();
|
||||
} catch {
|
||||
status.setCondition('Ready', {
|
||||
status: 'False',
|
||||
reason: 'SecretNotCreated',
|
||||
message: 'Secret not created',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { type TSchema } from '@sinclair/typebox';
|
||||
import { type Static, type TObject, type TSchema } from '@sinclair/typebox';
|
||||
|
||||
import { GROUP } from '../utils/consts.ts';
|
||||
import type { Services } from '../utils/service.ts';
|
||||
|
||||
import { statusSchema } from './custom-resource.status.ts';
|
||||
import type { CustomResourceRequest } from './custom-resource.request.ts';
|
||||
import { customResourceStatusSchema, type CustomResourceRequest } from './custom-resource.request.ts';
|
||||
|
||||
type EnsureSecretOptions<T extends TObject> = {
|
||||
schema: T;
|
||||
name: string;
|
||||
namespace: string;
|
||||
generator: () => Promise<Static<T>>;
|
||||
};
|
||||
|
||||
type CustomResourceHandlerOptions<TSpec extends TSchema> = {
|
||||
request: CustomResourceRequest<TSpec>;
|
||||
ensureSecret: <T extends TObject>(options: EnsureSecretOptions<T>) => Promise<Static<T>>;
|
||||
services: Services;
|
||||
};
|
||||
|
||||
@@ -82,7 +89,7 @@ abstract class CustomResource<TSpec extends TSchema> {
|
||||
type: 'object',
|
||||
properties: {
|
||||
spec: this.spec,
|
||||
status: statusSchema,
|
||||
status: customResourceStatusSchema as ExpectedAny,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -96,4 +103,4 @@ abstract class CustomResource<TSpec extends TSchema> {
|
||||
};
|
||||
}
|
||||
|
||||
export { CustomResource, type CustomResourceConstructor, type CustomResourceHandlerOptions };
|
||||
export { CustomResource, type CustomResourceConstructor, type CustomResourceHandlerOptions, type EnsureSecretOptions };
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { ApiException, Watch } from '@kubernetes/client-node';
|
||||
import { type TObject } from '@sinclair/typebox';
|
||||
|
||||
import { K8sService } from '../services/k8s.ts';
|
||||
import type { Services } from '../utils/service.ts';
|
||||
import { isSchemaValid } from '../utils/schemas.ts';
|
||||
|
||||
import { type CustomResource } from './custom-resource.base.ts';
|
||||
import { type CustomResource, type EnsureSecretOptions } from './custom-resource.base.ts';
|
||||
import { CustomResourceRequest } from './custom-resource.request.ts';
|
||||
|
||||
class CustomResourceRegistry {
|
||||
#services: Services;
|
||||
#resources = new Set<CustomResource<any>>();
|
||||
#resources = new Set<CustomResource<ExpectedAny>>();
|
||||
#watchers = new Map<string, AbortController>();
|
||||
|
||||
constructor(services: Services) {
|
||||
@@ -23,11 +24,11 @@ class CustomResourceRegistry {
|
||||
return Array.from(this.#resources).find((r) => r.kind === kind);
|
||||
};
|
||||
|
||||
public register = (resource: CustomResource<any>) => {
|
||||
public register = (resource: CustomResource<ExpectedAny>) => {
|
||||
this.#resources.add(resource);
|
||||
};
|
||||
|
||||
public unregister = (resource: CustomResource<any>) => {
|
||||
public unregister = (resource: CustomResource<ExpectedAny>) => {
|
||||
this.#resources.delete(resource);
|
||||
this.#watchers.forEach((controller, kind) => {
|
||||
if (kind === resource.kind) {
|
||||
@@ -50,7 +51,70 @@ class CustomResourceRegistry {
|
||||
}
|
||||
};
|
||||
|
||||
#onResourceEvent = async (type: string, obj: any) => {
|
||||
#ensureSecret =
|
||||
(request: CustomResourceRequest<ExpectedAny>) =>
|
||||
async <T extends TObject>(options: EnsureSecretOptions<T>) => {
|
||||
const { schema, name, namespace, generator } = options;
|
||||
const { metadata } = request;
|
||||
const k8sService = this.#services.get(K8sService);
|
||||
let exists = false;
|
||||
try {
|
||||
const secret = await k8sService.api.readNamespacedSecret({
|
||||
name,
|
||||
namespace,
|
||||
});
|
||||
|
||||
exists = true;
|
||||
if (secret?.data) {
|
||||
const decoded = Object.fromEntries(
|
||||
Object.entries(secret.data).map(([key, value]) => [key, Buffer.from(value, 'base64').toString('utf-8')]),
|
||||
);
|
||||
if (isSchemaValid(schema, decoded)) {
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!(error instanceof ApiException && error.code === 404)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const value = await generator();
|
||||
const data = Object.fromEntries(
|
||||
Object.entries(value).map(([key, value]) => [key, Buffer.from(value as string).toString('base64')]),
|
||||
);
|
||||
const body = {
|
||||
kind: 'Secret',
|
||||
metadata: {
|
||||
name,
|
||||
namespace,
|
||||
ownerReferences: [
|
||||
{
|
||||
apiVersion: request.apiVersion,
|
||||
kind: request.kind,
|
||||
name: metadata.name,
|
||||
uid: metadata.uid,
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'Opaque',
|
||||
data,
|
||||
};
|
||||
if (exists) {
|
||||
await k8sService.api.replaceNamespacedSecret({
|
||||
name,
|
||||
namespace,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
const response = await k8sService.api.createNamespacedSecret({
|
||||
namespace,
|
||||
body,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
#onResourceEvent = async (type: string, obj: ExpectedAny) => {
|
||||
const { kind } = obj;
|
||||
const crd = this.getByKind(kind);
|
||||
if (!crd) {
|
||||
@@ -65,31 +129,81 @@ class CustomResourceRegistry {
|
||||
});
|
||||
|
||||
const status = await request.getStatus();
|
||||
if (status && (type === 'ADDED' || type === 'MODIFIED')) {
|
||||
if (status.observedGeneration === obj.metadata.generation) {
|
||||
this.#services.log.debug('Skipping resource update', {
|
||||
kind,
|
||||
name: obj.metadata.name,
|
||||
namespace: obj.metadata.namespace,
|
||||
observedGeneration: status.observedGeneration,
|
||||
generation: obj.metadata.generation,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.#services.log.debug('Updating resource', {
|
||||
type,
|
||||
kind,
|
||||
name: obj.metadata.name,
|
||||
namespace: obj.metadata.namespace,
|
||||
observedGeneration: status?.observedGeneration,
|
||||
generation: obj.metadata.generation,
|
||||
});
|
||||
|
||||
if (type === 'ADDED' || type === 'MODIFIED') {
|
||||
await request.markSeen();
|
||||
}
|
||||
|
||||
if (type === 'ADDED' && crd.create) {
|
||||
handler = crd.create;
|
||||
}
|
||||
|
||||
try {
|
||||
await handler?.({
|
||||
request,
|
||||
services: this.#services,
|
||||
ensureSecret: this.#ensureSecret(request) as ExpectedAny,
|
||||
});
|
||||
if (type === 'ADDED' || type === 'MODIFIED') {
|
||||
await request.setCondition({
|
||||
type: 'Ready',
|
||||
status: 'True',
|
||||
message: 'Resource created',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
let message = 'Unknown error';
|
||||
|
||||
if (error instanceof ApiException) {
|
||||
message = error.body;
|
||||
this.#services.log.error('Error handling resource', { reason: error.body });
|
||||
} else if (error instanceof Error) {
|
||||
message = error.message;
|
||||
this.#services.log.error('Error handling resource', { reason: error.message });
|
||||
} else {
|
||||
message = String(error);
|
||||
this.#services.log.error('Error handling resource', { reason: String(error) });
|
||||
}
|
||||
if (type === 'ADDED' || type === 'MODIFIED') {
|
||||
await request.setCondition({
|
||||
type: 'Ready',
|
||||
status: 'False',
|
||||
reason: 'Error',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#onError = (error: any) => {
|
||||
console.error(error);
|
||||
#onError = (error: ExpectedAny) => {
|
||||
this.#services.log.error('Error watching resource', { error });
|
||||
};
|
||||
|
||||
public install = async (replace = false) => {
|
||||
const k8sService = this.#services.get(K8sService);
|
||||
for (const crd of this.#resources) {
|
||||
try {
|
||||
const manifest = crd.toManifest();
|
||||
try {
|
||||
await k8sService.extensionsApi.createCustomResourceDefinition({
|
||||
@@ -107,6 +221,12 @@ class CustomResourceRegistry {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ApiException) {
|
||||
throw new Error(`Failed to install ${crd.kind}: ${error.body}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { Static, TSchema } from '@sinclair/typebox';
|
||||
import { ApiException, PatchStrategy, setHeaderOptions } from '@kubernetes/client-node';
|
||||
import { Type, type Static, type TSchema } from '@sinclair/typebox';
|
||||
import { ApiException, PatchStrategy, setHeaderOptions, V1MicroTime } from '@kubernetes/client-node';
|
||||
|
||||
import type { Services } from '../utils/service.ts';
|
||||
import { K8sService } from '../services/k8s.ts';
|
||||
import { GROUP } from '../utils/consts.ts';
|
||||
|
||||
import { CustomResourceRegistry } from './custom-resource.registry.ts';
|
||||
import { CustomResourceStatus, type CustomResourceStatusType } from './custom-resource.status.ts';
|
||||
|
||||
type CustomResourceRequestOptions = {
|
||||
type: 'ADDED' | 'DELETED' | 'MODIFIED';
|
||||
manifest: any;
|
||||
manifest: ExpectedAny;
|
||||
services: Services;
|
||||
};
|
||||
|
||||
@@ -24,6 +24,30 @@ type CustomResourceRequestMetadata = Record<string, string> & {
|
||||
generation: number;
|
||||
};
|
||||
|
||||
type CustomResourceEvent = {
|
||||
reason: string;
|
||||
message: string;
|
||||
action: string;
|
||||
type: 'Normal' | 'Warning' | 'Error';
|
||||
};
|
||||
|
||||
const customResourceStatusSchema = Type.Object({
|
||||
observedGeneration: Type.Number(),
|
||||
conditions: Type.Array(
|
||||
Type.Object({
|
||||
type: Type.String(),
|
||||
status: Type.String({
|
||||
enum: ['True', 'False', 'Unknown'],
|
||||
}),
|
||||
lastTransitionTime: Type.String({ format: 'date-time' }),
|
||||
reason: Type.Optional(Type.String()),
|
||||
message: Type.Optional(Type.String()),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
type CustomResourceStatus = Static<typeof customResourceStatusSchema>;
|
||||
|
||||
class CustomResourceRequest<TSpec extends TSchema> {
|
||||
#options: CustomResourceRequestOptions;
|
||||
|
||||
@@ -59,10 +83,10 @@ class CustomResourceRequest<TSpec extends TSchema> {
|
||||
return this.#options.manifest.metadata;
|
||||
}
|
||||
|
||||
public isOwnerOf = (manifest: any) => {
|
||||
public isOwnerOf = (manifest: ExpectedAny) => {
|
||||
const ownerRef = manifest?.metadata?.ownerReferences || [];
|
||||
return ownerRef.some(
|
||||
(ref: any) =>
|
||||
(ref: ExpectedAny) =>
|
||||
ref.apiVersion === this.apiVersion &&
|
||||
ref.kind === this.kind &&
|
||||
ref.name === this.metadata.name &&
|
||||
@@ -70,11 +94,73 @@ class CustomResourceRequest<TSpec extends TSchema> {
|
||||
);
|
||||
};
|
||||
|
||||
public setStatus = async (status: CustomResourceStatusType) => {
|
||||
public markSeen = async () => {
|
||||
const { manifest } = this.#options;
|
||||
await this.setStatus({
|
||||
observedGeneration: manifest.metadata.generation,
|
||||
});
|
||||
};
|
||||
|
||||
public setCondition = async (condition: Omit<CustomResourceStatus['conditions'][number], 'lastTransitionTime'>) => {
|
||||
const fullCondition = {
|
||||
...condition,
|
||||
lastTransitionTime: new Date().toISOString(),
|
||||
};
|
||||
const current = await this.getCurrent();
|
||||
const conditions: CustomResourceStatus['conditions'] = current?.status?.conditions || [];
|
||||
const index = conditions.findIndex((c) => c.type === condition.type);
|
||||
if (index === -1) {
|
||||
conditions.push(fullCondition);
|
||||
} else {
|
||||
conditions[index] = fullCondition;
|
||||
}
|
||||
await this.setStatus({
|
||||
conditions,
|
||||
});
|
||||
};
|
||||
|
||||
public getStatus = async () => {
|
||||
const current = await this.getCurrent();
|
||||
return current?.status as CustomResourceStatus | undefined;
|
||||
};
|
||||
|
||||
public addEvent = async (event: CustomResourceEvent) => {
|
||||
const { manifest, services } = this.#options;
|
||||
const k8sService = services.get(K8sService);
|
||||
|
||||
await k8sService.eventsApi.createNamespacedEvent({
|
||||
namespace: manifest.metadata.namespace,
|
||||
body: {
|
||||
kind: 'Event',
|
||||
metadata: {
|
||||
name: `${manifest.metadata.name}-${Date.now()}`,
|
||||
namespace: manifest.metadata.namespace,
|
||||
},
|
||||
eventTime: new V1MicroTime(),
|
||||
note: event.message,
|
||||
action: event.action,
|
||||
reason: event.reason,
|
||||
type: event.type,
|
||||
reportingController: GROUP,
|
||||
reportingInstance: manifest.metadata.name,
|
||||
regarding: {
|
||||
apiVersion: manifest.apiVersion,
|
||||
resourceVersion: manifest.metadata.resourceVersion,
|
||||
kind: manifest.kind,
|
||||
name: manifest.metadata.name,
|
||||
namespace: manifest.metadata.namespace,
|
||||
uid: manifest.metadata.uid,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
public setStatus = async (status: Partial<CustomResourceStatus>) => {
|
||||
const { manifest, services } = this.#options;
|
||||
const { kind, metadata } = manifest;
|
||||
const registry = services.get(CustomResourceRegistry);
|
||||
const crd = registry.getByKind(kind);
|
||||
const current = await this.getCurrent();
|
||||
if (!crd) {
|
||||
throw new Error(`Custom resource ${kind} not found`);
|
||||
}
|
||||
@@ -90,7 +176,14 @@ class CustomResourceRequest<TSpec extends TSchema> {
|
||||
namespace,
|
||||
plural: crd.names.plural,
|
||||
name,
|
||||
body: { status },
|
||||
body: {
|
||||
status: {
|
||||
observedGeneration: manifest.metadata.generation,
|
||||
conditions: current?.status?.conditions || [],
|
||||
...current?.status,
|
||||
...status,
|
||||
},
|
||||
},
|
||||
fieldValidation: 'Strict',
|
||||
},
|
||||
setHeaderOptions('Content-Type', PatchStrategy.MergePatch),
|
||||
@@ -119,7 +212,7 @@ class CustomResourceRequest<TSpec extends TSchema> {
|
||||
kind: string;
|
||||
metadata: CustomResourceRequestMetadata;
|
||||
spec: Static<TSpec>;
|
||||
status: CustomResourceStatusType;
|
||||
status: CustomResourceStatus;
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ApiException && error.code === 404) {
|
||||
@@ -128,25 +221,6 @@ class CustomResourceRequest<TSpec extends TSchema> {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
public getStatus = async () => {
|
||||
const resource = await this.getCurrent();
|
||||
if (!resource || !resource.status) {
|
||||
return new CustomResourceStatus({
|
||||
status: {
|
||||
observedGeneration: 0,
|
||||
conditions: [],
|
||||
},
|
||||
generation: 0,
|
||||
save: this.setStatus,
|
||||
});
|
||||
}
|
||||
return new CustomResourceStatus({
|
||||
status: { ...resource.status, observedGeneration: resource.status.observedGeneration },
|
||||
generation: resource.metadata.generation,
|
||||
save: this.setStatus,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export { CustomResourceRequest };
|
||||
export { CustomResourceRequest, customResourceStatusSchema };
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Type, type Static } from '@sinclair/typebox';
|
||||
|
||||
type CustomResourceStatusType = Static<typeof statusSchema>;
|
||||
|
||||
const statusSchema = Type.Object({
|
||||
observedGeneration: Type.Number(),
|
||||
conditions: Type.Array(
|
||||
Type.Object({
|
||||
type: Type.String(),
|
||||
status: Type.String({
|
||||
enum: ['True', 'False', 'Unknown'],
|
||||
}),
|
||||
lastTransitionTime: Type.String(),
|
||||
reason: Type.String(),
|
||||
message: Type.String(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
type CustomResourceStatusOptions = {
|
||||
status?: CustomResourceStatusType;
|
||||
generation: number;
|
||||
save: (status: CustomResourceStatusType) => Promise<void>;
|
||||
};
|
||||
|
||||
class CustomResourceStatus {
|
||||
#status: CustomResourceStatusType;
|
||||
#generation: number;
|
||||
#save: (status: CustomResourceStatusType) => Promise<void>;
|
||||
|
||||
constructor(options: CustomResourceStatusOptions) {
|
||||
this.#save = options.save;
|
||||
this.#status = {
|
||||
observedGeneration: options.status?.observedGeneration ?? 0,
|
||||
conditions: options.status?.conditions ?? [],
|
||||
};
|
||||
this.#generation = options.generation;
|
||||
}
|
||||
|
||||
public get generation() {
|
||||
return this.#generation;
|
||||
}
|
||||
|
||||
public get observedGeneration() {
|
||||
return this.#status.observedGeneration;
|
||||
}
|
||||
|
||||
public set observedGeneration(observedGeneration: number) {
|
||||
this.#status.observedGeneration = observedGeneration;
|
||||
}
|
||||
|
||||
public getCondition = (type: string) => {
|
||||
return this.#status.conditions?.find((condition) => condition.type === type)?.status;
|
||||
};
|
||||
|
||||
public setCondition = (
|
||||
type: string,
|
||||
condition: Omit<CustomResourceStatusType['conditions'][number], 'type' | 'lastTransitionTime'>,
|
||||
) => {
|
||||
const currentCondition = this.getCondition(type);
|
||||
const newCondition = {
|
||||
...condition,
|
||||
type,
|
||||
lastTransitionTime: new Date().toISOString(),
|
||||
};
|
||||
if (currentCondition) {
|
||||
this.#status.conditions = this.#status.conditions.map((c) => (c.type === type ? newCondition : c));
|
||||
} else {
|
||||
this.#status.conditions.push(newCondition);
|
||||
}
|
||||
};
|
||||
|
||||
public save = async () => {
|
||||
await this.#save({
|
||||
...this.#status,
|
||||
observedGeneration: this.#generation,
|
||||
});
|
||||
};
|
||||
|
||||
public toJSON = () => {
|
||||
return this.#status;
|
||||
};
|
||||
}
|
||||
|
||||
export { CustomResourceStatus, statusSchema, type CustomResourceStatusType };
|
||||
33
src/index.ts
33
src/index.ts
@@ -1,11 +1,44 @@
|
||||
import 'dotenv/config';
|
||||
import { ApiException } from '@kubernetes/client-node';
|
||||
|
||||
import { CustomResourceRegistry } from './custom-resource/custom-resource.registry.ts';
|
||||
import { Services } from './utils/service.ts';
|
||||
import { SecretRequest } from './crds/secrets/secrets.request.ts';
|
||||
import { PostgresDatabase } from './crds/postgres/postgres.database.ts';
|
||||
import { AuthentikService } from './services/authentik/authentik.service.ts';
|
||||
import { AuthentikClient } from './crds/authentik/client/client.ts';
|
||||
|
||||
const services = new Services();
|
||||
const registry = services.get(CustomResourceRegistry);
|
||||
registry.register(new SecretRequest());
|
||||
registry.register(new PostgresDatabase());
|
||||
registry.register(new AuthentikClient());
|
||||
await registry.install(true);
|
||||
await registry.watch();
|
||||
|
||||
const authentikService = services.get(AuthentikService);
|
||||
await authentikService.upsertClient({
|
||||
name: 'foo',
|
||||
secret: 'foo',
|
||||
redirectUris: [{ url: 'http://localhost:3000/api/auth/callback', matchingMode: 'strict' }],
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.log('UNCAUGHT EXCEPTION');
|
||||
if (error instanceof ApiException) {
|
||||
return console.error(error.body);
|
||||
}
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (error) => {
|
||||
console.log('UNHANDLED REJECTION');
|
||||
if (error instanceof Error) {
|
||||
// show stack trace
|
||||
console.error(error.stack);
|
||||
}
|
||||
if (error instanceof ApiException) {
|
||||
return console.error(error.body);
|
||||
}
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
216
src/services/authentik/authentik.service.ts
Normal file
216
src/services/authentik/authentik.service.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import type { Services } from '../../utils/service.ts';
|
||||
import { ConfigService } from '../config/config.ts';
|
||||
import { createAuthentikClient, type AuthentikClient } from '../../clients/authentik/authentik.ts';
|
||||
|
||||
import type { UpsertClientRequest, UpsertGroupRequest } from './authentik.types.ts';
|
||||
|
||||
const DEFAULT_AUTHORIZATION_FLOW = 'default-provider-authorization-implicit-consent';
|
||||
const DEFAULT_INVALIDATION_FLOW = 'default-invalidation-flow';
|
||||
const DEFAULT_SCOPES = ['openid', 'email', 'profile', 'offline_access'];
|
||||
|
||||
class AuthentikService {
|
||||
#client: AuthentikClient;
|
||||
#services: Services;
|
||||
|
||||
constructor(services: Services) {
|
||||
const config = services.get(ConfigService);
|
||||
this.#client = createAuthentikClient({
|
||||
baseUrl: new URL('api/v3', config.authentik.url).toString(),
|
||||
token: config.authentik.token,
|
||||
});
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
public get url() {
|
||||
const config = this.#services.get(ConfigService);
|
||||
return config.authentik.url;
|
||||
}
|
||||
|
||||
#upsertApplication = async (request: UpsertClientRequest, provider: number, pk?: string) => {
|
||||
if (!pk) {
|
||||
return await this.#client.core.coreApplicationsCreate({
|
||||
applicationRequest: {
|
||||
name: request.name,
|
||||
slug: request.name,
|
||||
provider,
|
||||
},
|
||||
});
|
||||
}
|
||||
return await this.#client.core.coreApplicationsUpdate({
|
||||
slug: request.name,
|
||||
applicationRequest: {
|
||||
name: request.name,
|
||||
slug: request.name,
|
||||
provider,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
#upsertProvider = async (request: UpsertClientRequest, pk?: number) => {
|
||||
const flows = await this.getFlows();
|
||||
const authorizationFlow = flows.results.find(
|
||||
(flow) => flow.slug === (request.flows?.authorization ?? DEFAULT_AUTHORIZATION_FLOW),
|
||||
);
|
||||
const invalidationFlow = flows.results.find(
|
||||
(flow) => flow.slug === (request.flows?.invalidation ?? DEFAULT_INVALIDATION_FLOW),
|
||||
);
|
||||
if (!authorizationFlow || !invalidationFlow) {
|
||||
throw new Error('Authorization and invalidation flows not found');
|
||||
}
|
||||
const scopes = await this.getScopePropertyMappings();
|
||||
const scopePropertyMapping = (request.scopes ?? DEFAULT_SCOPES)
|
||||
.map((scope) => scopes.results.find((mapping) => mapping.scopeName === scope)?.pk)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (!pk) {
|
||||
return await this.#client.providers.providersOauth2Create({
|
||||
oAuth2ProviderRequest: {
|
||||
name: request.name,
|
||||
clientId: request.name,
|
||||
clientSecret: request.secret,
|
||||
redirectUris: request.redirectUris,
|
||||
authorizationFlow: authorizationFlow.pk,
|
||||
invalidationFlow: invalidationFlow.pk,
|
||||
propertyMappings: scopePropertyMapping,
|
||||
clientType: request.clientType,
|
||||
subMode: request.subMode,
|
||||
accessCodeValidity: request.timing?.accessCodeValidity,
|
||||
accessTokenValidity: request.timing?.accessTokenValidity,
|
||||
refreshTokenValidity: request.timing?.refreshTokenValidity,
|
||||
},
|
||||
});
|
||||
}
|
||||
return await this.#client.providers.providersOauth2Update({
|
||||
id: pk,
|
||||
oAuth2ProviderRequest: {
|
||||
name: request.name,
|
||||
clientId: request.name,
|
||||
clientSecret: request.secret,
|
||||
redirectUris: request.redirectUris,
|
||||
authorizationFlow: authorizationFlow.pk,
|
||||
invalidationFlow: invalidationFlow.pk,
|
||||
propertyMappings: scopePropertyMapping,
|
||||
clientType: request.clientType,
|
||||
subMode: request.subMode,
|
||||
accessCodeValidity: request.timing?.accessCodeValidity,
|
||||
accessTokenValidity: request.timing?.accessTokenValidity,
|
||||
refreshTokenValidity: request.timing?.refreshTokenValidity,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
public getGroupFromName = async (name: string) => {
|
||||
const groups = await this.#client.core.coreGroupsList({
|
||||
search: name,
|
||||
});
|
||||
return groups.results.find((group) => group.name === name);
|
||||
};
|
||||
|
||||
public getScopePropertyMappings = async () => {
|
||||
const mappings = await this.#client.propertymappings.propertymappingsProviderScopeList({});
|
||||
return mappings;
|
||||
};
|
||||
|
||||
public getApplicationFromSlug = async (slug: string) => {
|
||||
const applications = await this.#client.core.coreApplicationsList({
|
||||
search: slug,
|
||||
});
|
||||
const application = applications.results.find((app) => app.slug === slug);
|
||||
return application;
|
||||
};
|
||||
|
||||
public getProviderFromClientId = async (clientId: string) => {
|
||||
const providers = await this.#client.providers.providersOauth2List({
|
||||
clientId,
|
||||
});
|
||||
return providers.results.find((provider) => provider.clientId === clientId);
|
||||
};
|
||||
|
||||
public getFlows = async () => {
|
||||
const flows = await this.#client.flows.flowsInstancesList();
|
||||
return flows;
|
||||
};
|
||||
|
||||
public upsertClient = async (request: UpsertClientRequest) => {
|
||||
try {
|
||||
let provider = await this.getProviderFromClientId(request.name);
|
||||
provider = await this.#upsertProvider(request, provider?.pk);
|
||||
let application = await this.getApplicationFromSlug(request.name);
|
||||
application = await this.#upsertApplication(request, provider.pk, application?.pk);
|
||||
const config = {
|
||||
provider: {
|
||||
id: provider.pk,
|
||||
name: provider.name,
|
||||
clientId: provider.clientId,
|
||||
clientSecret: provider.clientSecret,
|
||||
clientType: provider.clientType,
|
||||
subMode: provider.subMode,
|
||||
redirectUris: provider.redirectUris,
|
||||
scopes: provider.propertyMappings,
|
||||
timing: {
|
||||
accessCodeValidity: provider.accessCodeValidity,
|
||||
accessTokenValidity: provider.accessTokenValidity,
|
||||
refreshTokenValidity: provider.refreshTokenValidity,
|
||||
},
|
||||
},
|
||||
application: {
|
||||
id: application.pk,
|
||||
name: application.name,
|
||||
slug: application.slug,
|
||||
provider: provider.pk,
|
||||
},
|
||||
urls: {
|
||||
configuration: new URL(
|
||||
`/application/o/${provider.name}/.well-known/openid-configuration`,
|
||||
this.url,
|
||||
).toString(),
|
||||
configurationIssuer: new URL(`/application/o/${provider.name}/`, this.url).toString(),
|
||||
authorization: new URL(`/application/o/${provider.name}/authorize/`, this.url).toString(),
|
||||
token: new URL(`/application/o/${provider.name}/token/`, this.url).toString(),
|
||||
userinfo: new URL(`/application/o/${provider.name}/userinfo/`, this.url).toString(),
|
||||
endSession: new URL(`/application/o/${provider.name}/end-session/`, this.url).toString(),
|
||||
jwks: new URL(`/application/o/${provider.name}/jwks/`, this.url).toString(),
|
||||
},
|
||||
};
|
||||
return { provider, application, config };
|
||||
} catch (error: ExpectedAny) {
|
||||
if ('response' in error) {
|
||||
throw new Error(await error.response.text());
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
public deleteClient = async (name: string) => {
|
||||
const provider = await this.getProviderFromClientId(name);
|
||||
if (provider) {
|
||||
await this.#client.providers.providersOauth2Destroy({ id: provider.pk });
|
||||
}
|
||||
const application = await this.getApplicationFromSlug(name);
|
||||
if (application) {
|
||||
await this.#client.core.coreApplicationsDestroy({ slug: application.name });
|
||||
}
|
||||
};
|
||||
|
||||
public upsertGroup = async (request: UpsertGroupRequest) => {
|
||||
const group = await this.getGroupFromName(request.name);
|
||||
if (!group) {
|
||||
await this.#client.core.coreGroupsCreate({
|
||||
groupRequest: {
|
||||
name: request.name,
|
||||
attributes: request.attributes,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.#client.core.coreGroupsUpdate({
|
||||
groupUuid: group.pk,
|
||||
groupRequest: {
|
||||
name: request.name,
|
||||
attributes: request.attributes,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { AuthentikService };
|
||||
29
src/services/authentik/authentik.types.ts
Normal file
29
src/services/authentik/authentik.types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ClientTypeEnum, SubModeEnum } from '@goauthentik/api';
|
||||
|
||||
type UpsertClientRequest = {
|
||||
name: string;
|
||||
secret: string;
|
||||
scopes?: string[];
|
||||
flows?: {
|
||||
authorization: string;
|
||||
invalidation: string;
|
||||
};
|
||||
clientType?: ClientTypeEnum;
|
||||
subMode?: SubModeEnum;
|
||||
redirectUris: {
|
||||
url: string;
|
||||
matchingMode: 'strict' | 'regex';
|
||||
}[];
|
||||
timing?: {
|
||||
accessCodeValidity?: string;
|
||||
accessTokenValidity?: string;
|
||||
refreshTokenValidity?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type UpsertGroupRequest = {
|
||||
name: string;
|
||||
attributes?: Record<string, string[]>;
|
||||
};
|
||||
|
||||
export type { UpsertClientRequest, UpsertGroupRequest };
|
||||
@@ -11,6 +11,17 @@ class ConfigService {
|
||||
|
||||
return { host, user, password, port };
|
||||
}
|
||||
|
||||
public get authentik() {
|
||||
const url = process.env.AUTHENTIK_URL;
|
||||
const token = process.env.AUTHENTIK_TOKEN;
|
||||
|
||||
if (!url || !token) {
|
||||
throw new Error('AUTHENTIK_URL and AUTHENTIK_TOKEN must be set');
|
||||
}
|
||||
|
||||
return { url, token };
|
||||
}
|
||||
}
|
||||
|
||||
export { ConfigService };
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { KubeConfig, CoreV1Api, ApiextensionsV1Api, CustomObjectsApi } from '@kubernetes/client-node';
|
||||
import {
|
||||
KubeConfig,
|
||||
CoreV1Api,
|
||||
ApiextensionsV1Api,
|
||||
CustomObjectsApi,
|
||||
EventsV1Api,
|
||||
KubernetesObjectApi,
|
||||
} from '@kubernetes/client-node';
|
||||
|
||||
class K8sService {
|
||||
#kc: KubeConfig;
|
||||
#k8sApi: CoreV1Api;
|
||||
#k8sExtensionsApi: ApiextensionsV1Api;
|
||||
#k8sCustomObjectsApi: CustomObjectsApi;
|
||||
#k8sEventsApi: EventsV1Api;
|
||||
#k8sObjectsApi: KubernetesObjectApi;
|
||||
|
||||
constructor() {
|
||||
this.#kc = new KubeConfig();
|
||||
@@ -12,6 +21,8 @@ class K8sService {
|
||||
this.#k8sApi = this.#kc.makeApiClient(CoreV1Api);
|
||||
this.#k8sExtensionsApi = this.#kc.makeApiClient(ApiextensionsV1Api);
|
||||
this.#k8sCustomObjectsApi = this.#kc.makeApiClient(CustomObjectsApi);
|
||||
this.#k8sEventsApi = this.#kc.makeApiClient(EventsV1Api);
|
||||
this.#k8sObjectsApi = this.#kc.makeApiClient(KubernetesObjectApi);
|
||||
}
|
||||
|
||||
public get config() {
|
||||
@@ -29,6 +40,14 @@ class K8sService {
|
||||
public get customObjectsApi() {
|
||||
return this.#k8sCustomObjectsApi;
|
||||
}
|
||||
|
||||
public get eventsApi() {
|
||||
return this.#k8sEventsApi;
|
||||
}
|
||||
|
||||
public get objectsApi() {
|
||||
return this.#k8sObjectsApi;
|
||||
}
|
||||
}
|
||||
|
||||
export { K8sService };
|
||||
|
||||
9
src/utils/schemas.ts
Normal file
9
src/utils/schemas.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { Static, TSchema } from '@sinclair/typebox';
|
||||
import { TypeCompiler } from '@sinclair/typebox/compiler';
|
||||
|
||||
const isSchemaValid = <T extends TSchema>(schema: T, data: unknown): data is Static<T> => {
|
||||
const compiler = TypeCompiler.Compile(schema);
|
||||
return compiler.Check(data);
|
||||
};
|
||||
|
||||
export { isSchemaValid };
|
||||
6
src/utils/types.d.ts
vendored
Normal file
6
src/utils/types.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ExpectedAny = any;
|
||||
}
|
||||
|
||||
export {};
|
||||
8
test-manifests/authentik-client.yaml
Normal file
8
test-manifests/authentik-client.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: homelab.mortenolsen.pro/v1
|
||||
kind: AuthentikClient
|
||||
metadata:
|
||||
name: foobas
|
||||
spec:
|
||||
redirectUris:
|
||||
- url: http://localhost:3000/api/auth/callback
|
||||
matchingMode: strict
|
||||
5
test-manifests/postgres-database.yaml
Normal file
5
test-manifests/postgres-database.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
apiVersion: 'homelab.mortenolsen.pro/v1'
|
||||
kind: 'PostgresDatabase'
|
||||
metadata:
|
||||
name: 'test2'
|
||||
namespace: 'playground'
|
||||
@@ -16,9 +16,8 @@
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
// "noUncheckedIndexedAccess": true,
|
||||
// "noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
|
||||
Reference in New Issue
Block a user