From 0ef5731cb18d8a469ffd5cbac3de609b7e7f4603 Mon Sep 17 00:00:00 2001 From: schaeferka Date: Tue, 24 Sep 2024 11:21:12 -0600 Subject: [PATCH] feat: add alias prefixing to pino logs (#916) ## Description We need to verify that we are able to inject the name of the .Alias into logs of the callback function. If it were possible it would need to be done with the Log object (import it into the mutate/validate/watch processor) ```ts const Log = pino({ transport, timestamp: pinoTimeFunction, }); ``` and import that into the [mutate processor](https://github.com/defenseunicorns/pepr/blob/0ec535efc6547f15fb4a02c35f6c3b8b4e5ecc22/src/lib/mutate-processor.ts#L80) and set set Log.prefix = the bindings alias before executing the callback Checklist - [x] Ensure `npx pepr monitor` is working and soak - [ ] soak and verify with user base that feature is performing as expected ## Related Issue Fixes #858 Relates to #676 ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Other (security config, docs update, etc) ## Checklist before merging - [ ] Test, docs, adr added or updated as needed - [ ] [Contributor Guide Steps](https://docs.pepr.dev/main/contribute/#submitting-a-pull-request) followed --------- Co-authored-by: Case Wylie --- .../050_using-alias-child-logger.md | 87 +++ package-lock.json | 467 ++++++------- src/lib/capability.test.ts | 655 ++++++++++++++++++ src/lib/capability.ts | 76 +- src/lib/mutate-processor.ts | 3 +- src/lib/schedule.ts | 2 +- src/lib/types.ts | 26 +- 7 files changed, 1045 insertions(+), 271 deletions(-) create mode 100644 docs/030_user-guide/030_actions/050_using-alias-child-logger.md create mode 100644 src/lib/capability.test.ts diff --git a/docs/030_user-guide/030_actions/050_using-alias-child-logger.md b/docs/030_user-guide/030_actions/050_using-alias-child-logger.md new file mode 100644 index 00000000..5f0107af --- /dev/null +++ b/docs/030_user-guide/030_actions/050_using-alias-child-logger.md @@ -0,0 +1,87 @@ +# Using Alias Child Logger in Actions + +You can use the Alias function to include a user-defined alias in the logs for Mutate, Validate, and Watch actions. This can make for easier debugging since your user-defined alias will be included in the action's logs. This is especially useful when you have multiple actions of the same type in a single module. + +The below example uses Mutate, Validate, and Watch actions with the Alias function: + +```ts +When(a.Pod) + .IsCreatedOrUpdated() + .Alias("mutate") + .Mutate((po, logger) => { + logger.info(`alias: mutate ${po.Raw.metadata.name}`); + }); +When(a.Pod) + .IsCreatedOrUpdated() + .Alias("validate") + .Validate((po, logger) => { + logger.info(`alias: validate ${po.Raw.metadata.name}`); + return po.Approve(); + }); +When(a.Pod) + .IsCreatedOrUpdated() + .Alias("watch") + .Watch((po, _, logger) => { + logger.info(`alias: watch ${po.metadata.name}`); + }); +When(a.Pod) + .IsCreatedOrUpdated() + .Alias("reconcile") + .Reconcile((po, _, logger) => { + logger.info(`alias: reconcile ${po.metadata.name}`); + }); +``` + +This will result in log entries when creating a Pod with the that include the alias: + +**Logs for Mutate When Pod `red` is Created:** + +```bash +{"level":30,"time":1726632368808,"pid":16,"hostname":"pepr-static-test-6786948977-6hbnt","uid":"b2221631-e87c-41a2-94c8-cdaef15e7b5f","namespace":"pepr-demo","name":"/red","gvk":{"group":"","version":"v1","kind":"Pod"},"operation":"CREATE","admissionKind":"Mutate","msg":"Incoming request"} +{"level":30,"time":1726632368808,"pid":16,"hostname":"pepr-static-test-6786948977-6hbnt","uid":"b2221631-e87c-41a2-94c8-cdaef15e7b5f","namespace":"pepr-demo","name":"/red","msg":"Processing request"} +{"level":30,"time":1726632368808,"pid":16,"hostname":"pepr-static-test-6786948977-6hbnt","msg":"Executing mutation action with alias: mutate"} +{"level":30,"time":1726632368808,"pid":16,"hostname":"pepr-static-test-6786948977-6hbnt","alias":"mutate","msg":"alias: mutate red"} +{"level":30,"time":1726632368808,"pid":16,"hostname":"pepr-static-test-6786948977-6hbnt","uid":"b2221631-e87c-41a2-94c8-cdaef15e7b5f","namespace":"pepr-demo","name":"hello-pepr","msg":"Mutation action succeeded (mutateCallback)"} +{"level":30,"time":1726632368808,"pid":16,"hostname":"pepr-static-test-6786948977-6hbnt","uid":"b2221631-e87c-41a2-94c8-cdaef15e7b5f","namespace":"pepr-demo","name":"/red","res":{"uid":"b2221631-e87c-41a2-94c8-cdaef15e7b5f","allowed":true,"patchType":"JSONPatch","patch":"W3sib3AiOiJhZGQiLCJwYXRoIjoiL21ldGFkYXRhL2Fubm90YXRpb25zL3N0YXRpYy10ZXN0LnBlcHIuZGV2fjFoZWxsby1wZXByIiwidmFsdWUiOiJzdWNjZWVkZWQifV0="},"msg":"Check response"} +{"level":30,"time":1726632368809,"pid":16,"hostname":"pepr-static-test-6786948977-6hbnt","uid":"b2221631-e87c-41a2-94c8-cdaef15e7b5f","method":"POST","url":"/mutate/c1a7fb6e3f2ab9dc08909d2de4166987520f317d53b759ab882dfd0b1c198479?timeout=10s","status":200,"duration":"1 ms"} +``` + +**Logs for Validate When Pod `red` is Created:** + +```bash +{"level":30,"time":1726631437605,"pid":16,"hostname":"pepr-static-test-6786948977-j7f9h","uid":"731eff93-d457-4ffc-a98c-0bcbe4c1727a","namespace":"pepr-demo","name":"/red","gvk":{"group":"","version":"v1","kind":"Pod"},"operation":"CREATE","admissionKind":"Validate","msg":"Incoming request"} +{"level":30,"time":1726631437606,"pid":16,"hostname":"pepr-static-test-6786948977-j7f9h","uid":"731eff93-d457-4ffc-a98c-0bcbe4c1727a","namespace":"pepr-demo","name":"/red","msg":"Processing validation request"} +{"level":30,"time":1726631437606,"pid":16,"hostname":"pepr-static-test-6786948977-j7f9h","uid":"731eff93-d457-4ffc-a98c-0bcbe4c1727a","namespace":"pepr-demo","name":"hello-pepr","msg":"Processing validation action (validateCallback)"} +{"level":30,"time":1726631437606,"pid":16,"hostname":"pepr-static-test-6786948977-j7f9h","msg":"Executing validate action with alias: validate"} +{"level":30,"time":1726631437606,"pid":16,"hostname":"pepr-static-test-6786948977-j7f9h","alias":"validate","msg":"alias: validate red"} +{"level":30,"time":1726631437606,"pid":16,"hostname":"pepr-static-test-6786948977-j7f9h","uid":"731eff93-d457-4ffc-a98c-0bcbe4c1727a","namespace":"pepr-demo","name":"hello-pepr","msg":"Validation action complete (validateCallback): allowed"} +{"level":30,"time":1726631437606,"pid":16,"hostname":"pepr-static-test-6786948977-j7f9h","uid":"731eff93-d457-4ffc-a98c-0bcbe4c1727a","namespace":"pepr-demo","name":"/red","res":{"uid":"731eff93-d457-4ffc-a98c-0bcbe4c1727a","allowed":true},"msg":"Check response"} +{"level":30,"time":1726631437606,"pid":16,"hostname":"pepr-static-test-6786948977-j7f9h","uid":"731eff93-d457-4ffc-a98c-0bcbe4c1727a","method":"POST","url":"/validate/c1a7fb6e3f2ab9dc08909d2de4166987520f317d53b759ab882dfd0b1c198479?timeout=10s","status":200,"duration":"5 ms"} +``` + +**Logs for Watch and Reconcile When Pod `red` is Created:** + +```bash +{"level":30,"time":1726798504518,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","msg":"Executing reconcile action with alias: reconcile"} +{"level":30,"time":1726798504518,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","alias":"reconcile","msg":"alias: reconcile red"} +{"level":30,"time":1726798504518,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","msg":"Executing watch action with alias: watch"} +{"level":30,"time":1726798504518,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","alias":"watch","msg":"alias: watch red"} +{"level":30,"time":1726798504521,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","msg":"Executing reconcile action with alias: reconcile"} +{"level":30,"time":1726798504521,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","alias":"reconcile","msg":"alias: reconcile red"} +{"level":30,"time":1726798504521,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","msg":"Executing watch action with alias: watch"} +{"level":30,"time":1726798504521,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","alias":"watch","msg":"alias: watch red"} +{"level":30,"time":1726798504528,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","msg":"Executing reconcile action with alias: reconcile"} +{"level":30,"time":1726798504528,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","alias":"reconcile","msg":"alias: reconcile red"} +{"level":30,"time":1726798504528,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","msg":"Executing watch action with alias: watch"} +{"level":30,"time":1726798504528,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","alias":"watch","msg":"alias: watch red"} +{"level":30,"time":1726798510464,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","msg":"Executing watch action with alias: watch"} +{"level":30,"time":1726798510464,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","alias":"watch","msg":"alias: watch red"} +{"level":30,"time":1726798510466,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","msg":"Executing reconcile action with alias: reconcile"} +{"level":30,"time":1726798510466,"pid":16,"hostname":"pepr-static-test-watcher-6dc69654c9-5ql6b","alias":"reconcile","msg":"alias: reconcile red"} +``` + +**Note:** The Alias function is optional and can be used to provide additional context in the logs. You must pass the logger object as shown above to the action to use the Alias function. + +## See Also + +Looking for some more generic helpers? Check out the [Module Author SDK](../130_sdk.md) for information on other things that Pepr can help with. diff --git a/package-lock.json b/package-lock.json index ed72944c..a2766b87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,9 +84,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -132,12 +132,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dev": true, "dependencies": { - "@babel/types": "^7.25.0", + "@babel/types": "^7.25.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -252,13 +252,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", "dev": true, "dependencies": { "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/types": "^7.25.6" }, "engines": { "node": ">=6.9.0" @@ -336,12 +336,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "dev": true, "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.25.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -402,12 +402,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", + "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -558,12 +558,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", - "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", + "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -587,16 +587,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -614,9 +614,9 @@ } }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.24.8", @@ -1273,9 +1273,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "peer": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -1468,9 +1468,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "engines": { "node": ">=12" }, @@ -2290,9 +2290,9 @@ } }, "node_modules/@kubernetes/client-node/node_modules/@types/node": { - "version": "20.16.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", - "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "version": "20.16.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.6.tgz", + "integrity": "sha512-T7PpxM/6yeDE+AdlVysT62BX6/bECZOmQAgiFg5NoBd5MQheZ3tzal7f1wvzfiEcmrcJNRi2zRr2nY2zF+0uqw==", "dependencies": { "undici-types": "~6.19.2" } @@ -2453,9 +2453,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@types/express": { @@ -2581,9 +2581,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", "dev": true }, "node_modules/@types/ramda": { @@ -3015,9 +3015,9 @@ } }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true }, "node_modules/asynckit": { @@ -3424,9 +3424,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001663", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz", + "integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==", "dev": true, "funding": [ { @@ -3488,9 +3488,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", "dev": true }, "node_modules/cliui": { @@ -3817,11 +3817,11 @@ } }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3982,9 +3982,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.11", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.11.tgz", - "integrity": "sha512-R1CccCDYqndR25CaXFd6hp/u9RaaMcftMkphmvuepXr5b1vfLkRml6aWVeBhXJ7rbevHkKEMJtz8XqPf7ffmew==", + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz", + "integrity": "sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw==", "dev": true }, "node_modules/emittery": { @@ -4005,9 +4005,9 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -4097,9 +4097,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "engines": { "node": ">=6" } @@ -4448,6 +4448,29 @@ "node": ">=0.8.x" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -4522,14 +4545,6 @@ "ms": "2.0.0" } }, - "node_modules/express/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4717,14 +4732,6 @@ "ms": "2.0.0" } }, - "node_modules/finalhandler/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4787,6 +4794,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -4887,6 +4905,18 @@ "node": ">=8.0.0" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/git-raw-commits": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", @@ -5112,6 +5142,15 @@ "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==" }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/husky": { "version": "9.1.6", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", @@ -5265,9 +5304,9 @@ "dev": true }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { "hasown": "^2.0.2" @@ -5343,6 +5382,18 @@ "node": ">=8" } }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-text-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", @@ -5582,113 +5633,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-changed-files/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/jest-changed-files/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-changed-files/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/jest-changed-files/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==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-changed-files/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/jest-changed-files/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-changed-files/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-changed-files/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/jest-changed-files/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/jest-circus": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", @@ -7337,6 +7281,15 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -7435,9 +7388,9 @@ } }, "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==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -7515,6 +7468,18 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-hash": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", @@ -7569,12 +7534,27 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openid-client": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", - "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.0.tgz", + "integrity": "sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==", "dependencies": { - "jose": "^4.15.5", + "jose": "^4.15.9", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -7802,9 +7782,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true }, "node_modules/picomatch": { @@ -8064,9 +8044,9 @@ } }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -8365,9 +8345,9 @@ ] }, "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "engines": { "node": ">=10" } @@ -8429,10 +8409,13 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } }, "node_modules/serve-static": { "version": "1.16.2", @@ -8448,14 +8431,6 @@ "node": ">= 0.8.0" } }, - "node_modules/serve-static/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -8514,15 +8489,10 @@ } }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "node_modules/sisteransi": { "version": "1.0.5", @@ -8538,9 +8508,9 @@ } }, "node_modules/sonic-boom": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", - "integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.1.0.tgz", + "integrity": "sha512-NGipjjRicyJJ03rPiZCJYjwlsuP2d1/5QUviozRXC7S3WdVWNK5e3Ojieb9CCyfhq2UC+3+SRd9nG3I2lPRvUw==", "dependencies": { "atomic-sleep": "^1.0.0" } @@ -8695,6 +8665,15 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8969,9 +8948,9 @@ "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==" }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/type-check": { "version": "0.4.0", @@ -9350,12 +9329,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/write-file-atomic/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -9391,9 +9364,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", - "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", "bin": { "yaml": "bin.mjs" }, diff --git a/src/lib/capability.test.ts b/src/lib/capability.test.ts new file mode 100644 index 00000000..ebc9eb50 --- /dev/null +++ b/src/lib/capability.test.ts @@ -0,0 +1,655 @@ +import { Capability } from "./capability"; +import Log from "./logger"; +import { CapabilityCfg, FinalizeAction, MutateAction, ValidateAction, WatchLogAction } from "./types"; +import { a } from "../lib"; +import { V1Pod } from "@kubernetes/client-node"; +import { expect, describe, jest, beforeEach, it } from "@jest/globals"; +import { PeprMutateRequest } from "./mutate-request"; +import { PeprValidateRequest } from "./validate-request"; +import { Operation, AdmissionRequest } from "./k8s"; +import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types"; +import { Event } from "./types"; +import { GenericClass } from "kubernetes-fluent-client"; +import { Schedule } from "./schedule"; +import { OnSchedule } from "./schedule"; + +// Mocking isBuildMode, isWatchMode, and isDevMode globally +jest.mock("./module", () => ({ + isBuildMode: jest.fn(() => true), + isWatchMode: jest.fn(() => true), + isDevMode: jest.fn(() => true), +})); + +// Mock logger globally +jest.mock("./logger", () => ({ + __esModule: true, + default: { + info: jest.fn(), + debug: jest.fn(), + child: jest.fn().mockReturnThis(), + }, +})); + +// Mock Storage and OnSchedule +jest.mock("./storage", () => ({ + Storage: jest.fn(() => ({ + onReady: jest.fn(), + })), +})); + +// Mock OnSchedule and ensure it has a mock setStore method +jest.mock("./schedule", () => ({ + OnSchedule: jest.fn().mockImplementation(() => ({ + setStore: jest.fn(), // Ensure setStore is a mocked function + })), +})); + +const mockLog = Log as jest.Mocked; + +describe("Capability", () => { + let mockRequest: AdmissionRequest; + + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + + mockRequest = { + operation: Operation.CREATE, + object: { + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "test-pod", + namespace: "default", + labels: { + "existing-label": "true", + }, + annotations: { + "existing-annotation": "true", + }, + }, + spec: { + containers: [], + }, + }, + dryRun: false, + uid: "test-uid", + name: "test-pod", + kind: { group: "", version: "v1", kind: "Pod" }, + resource: { group: "", version: "v1", resource: "pods" }, + userInfo: { username: "test-user" }, + oldObject: undefined, + }; + }); + + const capabilityConfig: CapabilityCfg = { + name: "test-capability", + description: "Test capability description", + namespaces: ["default"], + }; + + it("should initialize with given configuration", () => { + const capability = new Capability(capabilityConfig); + expect(capability.name).toBe(capabilityConfig.name); + expect(capability.description).toBe(capabilityConfig.description); + expect(capability.namespaces).toEqual(capabilityConfig.namespaces); + expect(mockLog.info).toHaveBeenCalledWith(`Capability ${capabilityConfig.name} registered`); + }); + + it("should register store and schedule store", () => { + const capability = new Capability(capabilityConfig); + + const storeResult = capability.registerStore(); + expect(storeResult).toHaveProperty("store"); + expect(mockLog.info).toHaveBeenCalledWith(`Registering store for ${capabilityConfig.name}`); + + const scheduleStoreResult = capability.registerScheduleStore(); + expect(scheduleStoreResult).toHaveProperty("scheduleStore"); + expect(mockLog.info).toHaveBeenCalledWith(`Registering schedule store for ${capabilityConfig.name}`); + }); + + it("should throw an error if store is registered twice", () => { + const capability = new Capability(capabilityConfig); + + capability.registerStore(); + expect(() => capability.registerStore()).toThrowError("Store already registered for test-capability"); + }); + + it("should throw an error if schedule store is registered twice", () => { + const capability = new Capability(capabilityConfig); + + capability.registerScheduleStore(); + expect(() => capability.registerScheduleStore()).toThrowError( + "Schedule store already registered for test-capability", + ); + }); + + it("should correctly chain When, InNamespace, WithLabel, and Mutate methods", async () => { + const capability = new Capability(capabilityConfig); + + const mockMutateCallback: MutateAction = jest.fn( + async (req: PeprMutateRequest, logger: typeof Log = mockLog) => { + logger.info("Executing mutation action"); + }, + ); + + capability + .When(a.Pod) + .IsCreatedOrUpdated() + .InNamespace("default") + .WithLabel("test-label", "value") + .Alias("test-alias") + .Mutate(mockMutateCallback); + + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; + expect(binding.filters.namespaces).toContain("default"); + expect(binding.filters.labels).toHaveProperty("test-label", "value"); + expect(binding.alias).toBe("test-alias"); + + // Simulate the mutation action + const peprRequest = new PeprMutateRequest(mockRequest); + + if (binding.mutateCallback) { + await binding.mutateCallback(peprRequest); + } + + expect(mockMutateCallback).toHaveBeenCalledWith(peprRequest, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ alias: "test-alias" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: test-alias"); + }); + + it("should use child logger for mutate callback", async () => { + const capability = new Capability(capabilityConfig); + + const mockMutateCallback: MutateAction = jest.fn( + (req: PeprMutateRequest, logger: typeof Log = mockLog) => { + logger.info("Mutate action log"); + }, + ); + + capability + .When(a.Pod) + .IsCreatedOrUpdated() + .InNamespace("default") + .WithLabel("test-label", "value") + .Alias("test-alias") + .Mutate(mockMutateCallback); + + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; + + // Simulate the mutation action + const peprRequest = new PeprMutateRequest(mockRequest); + + if (binding.mutateCallback) { + await binding.mutateCallback(peprRequest); + } + + expect(mockMutateCallback).toHaveBeenCalledWith(peprRequest, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ alias: "test-alias" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: test-alias"); + expect(mockLog.info).toHaveBeenCalledWith("Mutate action log"); + }); + + it("should handle complex alias and logging correctly", async () => { + const complexCapabilityConfig: CapabilityCfg = { + name: "complex-capability", + description: "Test complex capability description", + namespaces: ["pepr-demo", "pepr-demo-2"], + }; + + const capability = new Capability(complexCapabilityConfig); + + const mockMutateCallback: MutateAction = jest.fn( + async (po: PeprMutateRequest, logger: typeof Log = mockLog) => { + logger.info(`SNAKES ON A PLANE! ${po.Raw.metadata?.name}`); + }, + ); + + capability + .When(a.Pod) + .IsCreatedOrUpdated() + .InNamespace("pepr-demo") + .WithLabel("white") + .Alias("reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation") + .Mutate(mockMutateCallback); + + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; + expect(binding.filters.namespaces).toContain("pepr-demo"); + expect(binding.filters.labels).toHaveProperty("white", ""); + expect(binding.alias).toBe("reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation"); + + // Simulate the mutation action + const peprRequest = new PeprMutateRequest(mockRequest); + + if (binding.mutateCallback) { + await binding.mutateCallback(peprRequest); + } + + expect(mockMutateCallback).toHaveBeenCalledWith(peprRequest, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ + alias: "reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation", + }); + expect(mockLog.info).toHaveBeenCalledWith( + "Executing mutation action with alias: reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation", + ); + expect(mockLog.info).toHaveBeenCalledWith(`SNAKES ON A PLANE! ${mockRequest.object.metadata?.name}`); + }); + + it("should reset the alias before each mutation", async () => { + const capability = new Capability(capabilityConfig); + + const firstMutateCallback: MutateAction = jest.fn( + async (req: PeprMutateRequest, logger: typeof Log = mockLog) => { + logger.info("First mutation action"); + }, + ); + + const secondMutateCallback: MutateAction = jest.fn( + async (req: PeprMutateRequest, logger: typeof Log = mockLog) => { + logger.info("Second mutation action"); + }, + ); + + // First mutation with an alias + capability.When(a.Pod).IsCreatedOrUpdated().InNamespace("default").Alias("first-alias").Mutate(firstMutateCallback); + + // Second mutation without an alias (should use "no alias provided") + capability.When(a.Pod).IsCreatedOrUpdated().InNamespace("default").Mutate(secondMutateCallback); + + expect(capability.bindings).toHaveLength(2); + + // Simulate the first mutation action + const peprRequest1 = new PeprMutateRequest(mockRequest); + if (capability.bindings[0].mutateCallback) { + await capability.bindings[0].mutateCallback(peprRequest1); + } + + expect(firstMutateCallback).toHaveBeenCalledWith(peprRequest1, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ alias: "first-alias" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: first-alias"); + + // Simulate the second mutation action + const peprRequest2 = new PeprMutateRequest(mockRequest); + if (capability.bindings[1].mutateCallback) { + await capability.bindings[1].mutateCallback(peprRequest2); + } + + expect(secondMutateCallback).toHaveBeenCalledWith(peprRequest2, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: no alias provided"); + }); + + it("should use child logger for validate callback", async () => { + const capability = new Capability(capabilityConfig); + + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); + + capability + .When(a.Pod) + .IsCreatedOrUpdated() + .InNamespace("default") + .Alias("test-alias") + .Validate(mockValidateCallback); + + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; + + // Simulate the validation action + const mockPeprRequest = new PeprValidateRequest(mockRequest); + + if (binding.validateCallback) { + await binding.validateCallback(mockPeprRequest); + } + + expect(mockValidateCallback).toHaveBeenCalledWith(mockPeprRequest, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ alias: "test-alias" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing validate action with alias: test-alias"); + expect(mockLog.info).toHaveBeenCalledWith("Validate action log"); + }); + + it("should log 'no alias provided' if alias is not set in validate callback", async () => { + const capability = new Capability(capabilityConfig); + + // Mock the validate callback + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); + + // Do not set alias, to trigger "no alias provided" + capability.When(a.Pod).IsCreatedOrUpdated().Validate(mockValidateCallback); + + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; + + // Simulate the validation action + const mockPeprRequest = new PeprValidateRequest(mockRequest); + + if (binding.validateCallback) { + await binding.validateCallback(mockPeprRequest); + } + + // Expect the log to contain "no alias provided" + expect(mockLog.info).toHaveBeenCalledWith("Executing validate action with alias: no alias provided"); + expect(mockLog.info).toHaveBeenCalledWith("Validate action log"); + }); + + it("should register a Watch action and execute it with the logger", async () => { + const capability = new Capability(capabilityConfig); + + // Mock Watch callback function + const mockWatchCallback: WatchLogAction = jest.fn( + async (update, phase, logger: typeof Log = mockLog) => { + logger.info("Watch action executed"); + }, + ); + + // Chain the When and Watch methods + capability.When(a.Pod).IsCreated().Watch(mockWatchCallback); + + // Log the bindings to ensure they are being added + console.log("Bindings after watch registration: ", capability.bindings); + + // Retrieve the registered binding + const binding = capability.bindings.find(b => b.isWatch === true); + + // Check that the watch callback was registered + expect(binding).toBeDefined(); + expect(binding?.isWatch).toBe(true); + + // Simulate calling the watch callback with test data + const testPod = new V1Pod(); + await binding?.watchCallback?.(testPod, WatchPhase.Added, mockLog); + + // Ensure that the logger's `info` method was called + expect(mockLog.info).toHaveBeenCalledWith("Watch action executed"); + expect(mockWatchCallback).toHaveBeenCalledWith(testPod, WatchPhase.Added, mockLog); + }); + + it("should pass the correct parameters to the Watch action", async () => { + const capability = new Capability(capabilityConfig); + + const mockWatchCallback: WatchLogAction = jest.fn( + async (update, phase, logger: typeof Log = mockLog) => { + logger.info("Watch action executed"); + }, + ); + + capability.When(a.Pod).IsCreated().Watch(mockWatchCallback); + + const binding = capability.bindings.find(b => b.isWatch); + expect(binding).toBeDefined(); + + const testPod = new V1Pod(); + const testPhase = WatchPhase.Modified; + + // Call the watch callback with custom data + await binding?.watchCallback?.(testPod, testPhase, mockLog); + + expect(mockWatchCallback).toHaveBeenCalledWith(testPod, testPhase, mockLog); + expect(mockLog.info).toHaveBeenCalledWith("Watch action executed"); + }); + + it("should use child logger for reconcile callback", async () => { + const capability = new Capability(capabilityConfig); + + const mockReconcileCallback: WatchLogAction = jest.fn( + async (update, phase, logger: typeof Log = mockLog) => { + logger.info("Reconcile action log"); + }, + ); + + capability.When(a.Pod).IsCreatedOrUpdated().Reconcile(mockReconcileCallback); + + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; + + // Simulate calling the reconcile action + const testPod = new V1Pod(); + const testPhase = WatchPhase.Modified; + + if (binding.watchCallback) { + await binding.watchCallback(testPod, testPhase); + } + + expect(mockReconcileCallback).toHaveBeenCalledWith(testPod, testPhase, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing reconcile action with alias: no alias provided"); + expect(mockLog.info).toHaveBeenCalledWith("Reconcile action log"); + }); + + it("should use child logger for finalize callback", async () => { + const capability = new Capability(capabilityConfig); + + const mockFinalizeCallback: FinalizeAction = jest.fn(async (update, logger: typeof Log = mockLog) => { + logger.info("Finalize action log"); + }); + + // Create a mock WatchLogAction function that matches the expected signature + const mockWatchCallback: WatchLogAction = jest.fn( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async (update: V1Pod, phase: WatchPhase, logger?: typeof Log) => {}, + ); + + // Chain .Watch() with the correct function signature before .Finalize() + capability.When(a.Pod).IsCreatedOrUpdated().Watch(mockWatchCallback).Finalize(mockFinalizeCallback); + + // Find the finalize binding + const finalizeBinding = capability.bindings.find(binding => binding.finalizeCallback); + + expect(finalizeBinding).toBeDefined(); // Ensure the finalize binding exists + + // Simulate calling the finalize action + const testPod = new V1Pod(); + + if (finalizeBinding?.finalizeCallback) { + await finalizeBinding.finalizeCallback(testPod); + } + + expect(mockFinalizeCallback).toHaveBeenCalledWith(testPod, expect.anything()); + expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing finalize action with alias: no alias provided"); + expect(mockLog.info).toHaveBeenCalledWith("Finalize action log"); + }); + + it("should add deletionTimestamp filter", () => { + const capability = new Capability(capabilityConfig); + + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); + + capability.When(a.Pod).IsCreatedOrUpdated().WithDeletionTimestamp().Validate(mockValidateCallback); + + expect(capability.bindings).toHaveLength(1); // Ensure binding is created + expect(capability.bindings[0].filters.deletionTimestamp).toBe(true); + }); + + it("should add name filter", () => { + const capability = new Capability(capabilityConfig); + + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); + + capability.When(a.Pod).IsCreatedOrUpdated().WithName("test-name").Validate(mockValidateCallback); + + expect(capability.bindings).toHaveLength(1); // Ensure binding is created + expect(capability.bindings[0].filters.name).toBe("test-name"); + }); + + it("should add annotation filter", () => { + const capability = new Capability(capabilityConfig); + + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); + + capability.When(a.Pod).IsCreatedOrUpdated().WithAnnotation("test-key", "test-value").Validate(mockValidateCallback); + + expect(capability.bindings).toHaveLength(1); // Ensure binding is created + expect(capability.bindings[0].filters.annotations["test-key"]).toBe("test-value"); + }); + + it("should bind an update event", () => { + const capability = new Capability(capabilityConfig); + + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); + + capability.When(a.Pod).IsUpdated().InNamespace("default").Validate(mockValidateCallback); + + expect(capability.bindings).toHaveLength(1); // Ensure binding is created + expect(capability.bindings[0].event).toBe(Event.Update); + }); + + it("should bind a delete event", async () => { + const capability = new Capability(capabilityConfig); + + const mockValidateCallback: ValidateAction = jest.fn( + async (req: PeprValidateRequest, logger: typeof Log = mockLog) => { + logger.info("Validate action log"); + return { allowed: true }; + }, + ); + + capability.When(a.Pod).IsDeleted().InNamespace("default").Validate(mockValidateCallback); + + expect(capability.bindings).toHaveLength(1); + + expect(capability.bindings).toHaveLength(1); // Ensure binding is created + expect(capability.bindings[0].event).toBe(Event.Delete); + }); + + it("should throw an error if neither matchedKind nor kind is provided", () => { + const capability = new Capability(capabilityConfig); + + // Mock a model with just a name, missing the kind + const mockModel: { name: string } = { + name: "InvalidModel", + }; + + // Expect an error when neither matchedKind nor kind is provided + expect(() => { + capability.When(mockModel as unknown as GenericClass); // Cast to the expected type without using 'any' + }).toThrowError(`Kind not specified for ${mockModel.name}`); + }); + + it("should create a new schedule and watch the schedule store when PEPR_WATCH_MODE is 'true'", () => { + // Set the environment variable + process.env.PEPR_WATCH_MODE = "true"; + + const capability = new Capability(capabilityConfig); + + const mockSchedule: Schedule = { + name: "test-schedule", + every: 5, + unit: "minutes", + run: jest.fn(), + startTime: new Date(), + completions: 1, + }; + + // Call OnSchedule with a mock schedule + capability.OnSchedule(mockSchedule); + + // Ensure that the schedule store's `onReady` method is called with the correct callback + const scheduleStoreInstance = capability.getScheduleStore(); + expect(scheduleStoreInstance.onReady).toHaveBeenCalledWith(expect.any(Function)); + + // Simulate the `onReady` callback being invoked + const onReadyCallback = (scheduleStoreInstance.onReady as jest.Mock).mock.calls[0][0] as () => void; + onReadyCallback(); // The callback function is now invoked as a type of `() => void` + + // Ensure the new OnSchedule instance is created with the correct schedule data + expect(OnSchedule).toHaveBeenCalledWith(mockSchedule); + + // Clean up environment variables after the test + delete process.env.PEPR_WATCH_MODE; + }); + + it("should not create a new schedule or watch the schedule store when PEPR_WATCH_MODE is not set", () => { + // Ensure environment variables are not set + delete process.env.PEPR_WATCH_MODE; + delete process.env.PEPR_MODE; + + const capability = new Capability(capabilityConfig); + + const mockSchedule: Schedule = { + name: "test-schedule", + every: 5, + unit: "minutes", + run: jest.fn(), + startTime: new Date(), + completions: 1, + }; + + // Call OnSchedule with a mock schedule + capability.OnSchedule(mockSchedule); + + // Ensure that the schedule store's `onReady` method is not called + const scheduleStoreInstance = capability.getScheduleStore(); + expect(scheduleStoreInstance.onReady).not.toHaveBeenCalled(); + + // Ensure that OnSchedule was not called + expect(OnSchedule).not.toHaveBeenCalled(); + }); + + it("should use aliasLogger if no logger is provided in watch callback", async () => { + const capability = new Capability(capabilityConfig); + + // Mock the watch callback + const mockWatchCallback: WatchLogAction = jest.fn( + async (update: V1Pod, phase: WatchPhase, logger?: typeof Log) => { + logger?.info("Watch action log"); + }, + ); + + // Chain Watch without providing an explicit logger + capability.When(a.Pod).IsCreatedOrUpdated().Watch(mockWatchCallback); + + expect(capability.bindings).toHaveLength(1); + const binding = capability.bindings[0]; + + // Simulate the watch action without passing a logger, so aliasLogger is used + const testPod = new V1Pod(); + await binding.watchCallback?.(testPod, WatchPhase.Added); // No logger passed + + // Assert that aliasLogger was used + expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" }); + expect(mockLog.info).toHaveBeenCalledWith("Executing watch action with alias: no alias provided"); + expect(mockLog.info).toHaveBeenCalledWith("Watch action log"); + }); + + it("should add annotation with an empty value when no value is provided in WithAnnotation", () => { + const capability = new Capability(capabilityConfig); + + // Chain WithAnnotation without providing a value (default to empty string) + capability.When(a.Pod).IsCreatedOrUpdated().WithAnnotation("test-annotation"); + + expect(capability.bindings).toHaveLength(0); + }); +}); diff --git a/src/lib/capability.ts b/src/lib/capability.ts index 1473cc40..1e4e13da 100644 --- a/src/lib/capability.ts +++ b/src/lib/capability.ts @@ -2,9 +2,7 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { GenericClass, GroupVersionKind, modelToGroupVersionKind } from "kubernetes-fluent-client"; -import { WatchAction } from "kubernetes-fluent-client/dist/fluent/types"; import { pickBy } from "ramda"; - import Log from "./logger"; import { isBuildMode, isDevMode, isWatchMode } from "./module"; import { PeprStore, Storage } from "./storage"; @@ -20,6 +18,7 @@ import { MutateActionChain, ValidateAction, ValidateActionChain, + WatchLogAction, FinalizeAction, FinalizeActionChain, WhenSelector, @@ -72,6 +71,10 @@ export class Capability implements CapabilityExport { } }; + public getScheduleStore() { + return this.#scheduleStore; + } + /** * Store is a key-value data store that can be used to persist data that should be shared * between requests. Each capability has its own store, and the data is persisted in Kubernetes @@ -209,7 +212,7 @@ export class Capability implements CapabilityExport { const bindings = this.#bindings; const prefix = `${this.#name}: ${model.name}`; - const commonChain = { WithLabel, WithAnnotation, WithDeletionTimestamp, Mutate, Validate, Watch, Reconcile }; + const commonChain = { WithLabel, WithAnnotation, WithDeletionTimestamp, Mutate, Validate, Watch, Reconcile, Alias }; const isNotEmpty = (value: object) => Object.keys(value).length > 0; const log = (message: string, cbString: string) => { const filteredObj = pickBy(isNotEmpty, binding.filters); @@ -223,12 +226,18 @@ export class Capability implements CapabilityExport { if (registerAdmission) { log("Validate Action", validateCallback.toString()); + // Create the child logger + const aliasLogger = Log.child({ alias: binding.alias || "no alias provided" }); + // Push the binding to the list of bindings for this capability as a new BindingAction // with the callback function to preserve bindings.push({ ...binding, isValidate: true, - validateCallback, + validateCallback: async (req, logger = aliasLogger) => { + Log.info(`Executing validate action with alias: ${binding.alias || "no alias provided"}`); + return await validateCallback(req, logger); + }, }); } @@ -239,12 +248,18 @@ export class Capability implements CapabilityExport { if (registerAdmission) { log("Mutate Action", mutateCallback.toString()); + // Create the child logger + const aliasLogger = Log.child({ alias: binding.alias || "no alias provided" }); + // Push the binding to the list of bindings for this capability as a new BindingAction // with the callback function to preserve bindings.push({ ...binding, isMutate: true, - mutateCallback, + mutateCallback: async (req, logger = aliasLogger) => { + Log.info(`Executing mutation action with alias: ${binding.alias || "no alias provided"}`); + await mutateCallback(req, logger); + }, }); } @@ -252,39 +267,56 @@ export class Capability implements CapabilityExport { return { Watch, Validate, Reconcile }; } - function Watch(watchCallback: WatchAction): FinalizeActionChain { + function Watch(watchCallback: WatchLogAction): FinalizeActionChain { if (registerWatch) { log("Watch Action", watchCallback.toString()); + // Create the child logger and cast it to the expected type + const aliasLogger = Log.child({ alias: binding.alias || "no alias provided" }) as typeof Log; + + // Push the binding to the list of bindings for this capability as a new BindingAction + // with the callback function to preserve bindings.push({ ...binding, isWatch: true, - watchCallback, + watchCallback: async (update, phase, logger = aliasLogger) => { + Log.info(`Executing watch action with alias: ${binding.alias || "no alias provided"}`); + await watchCallback(update, phase, logger); + }, }); } - return { Finalize }; } - function Reconcile(watchCallback: WatchAction): FinalizeActionChain { + function Reconcile(reconcileCallback: WatchLogAction): FinalizeActionChain { if (registerWatch) { - log("Reconcile Action", watchCallback.toString()); + log("Reconcile Action", reconcileCallback.toString()); + + // Create the child logger and cast it to the expected type + const aliasLogger = Log.child({ alias: binding.alias || "no alias provided" }) as typeof Log; + // Push the binding to the list of bindings for this capability as a new BindingAction + // with the callback function to preserve bindings.push({ ...binding, isWatch: true, isQueue: true, - watchCallback, + watchCallback: async (update, phase, logger = aliasLogger) => { + Log.info(`Executing reconcile action with alias: ${binding.alias || "no alias provided"}`); + await reconcileCallback(update, phase, logger); + }, }); } - return { Finalize }; } - function Finalize(finalizeCallback: FinalizeAction) { + function Finalize(finalizeCallback: FinalizeAction): void { log("Finalize Action", finalizeCallback.toString()); - // add binding to inject pepr finalizer during admission (Mutate) + // Create the child logger and cast it to the expected type + const aliasLogger = Log.child({ alias: binding.alias || "no alias provided" }) as typeof Log; + + // Add binding to inject Pepr finalizer during admission (Mutate) if (registerAdmission) { const mutateBinding = { ...binding, @@ -296,19 +328,20 @@ export class Capability implements CapabilityExport { bindings.push(mutateBinding); } - // add binding to process finalizer callback / remove pepr finalizer (Watch) + // Add binding to process finalizer callback / remove Pepr finalizer (Watch) if (registerWatch) { const watchBinding = { ...binding, isWatch: true, isFinalize: true, event: Event.Update, - finalizeCallback, + finalizeCallback: async (update: InstanceType, logger = aliasLogger) => { + Log.info(`Executing finalize action with alias: ${binding.alias || "no alias provided"}`); + await finalizeCallback(update, logger); + }, }; bindings.push(watchBinding); } - - return { Finalize }; } function InNamespace(...namespaces: string[]): BindingWithName { @@ -353,6 +386,12 @@ export class Capability implements CapabilityExport { return commonChain; } + function Alias(alias: string) { + Log.debug(`Adding prefix alias ${alias}`, prefix); + binding.alias = alias; + return commonChain; + } + function bindEvent(event: Event) { binding.event = event; return { @@ -362,6 +401,7 @@ export class Capability implements CapabilityExport { WithName, WithNameRegex, WithDeletionTimestamp, + Alias, }; } diff --git a/src/lib/mutate-processor.ts b/src/lib/mutate-processor.ts index f0f95a51..00d90376 100644 --- a/src/lib/mutate-processor.ts +++ b/src/lib/mutate-processor.ts @@ -56,7 +56,6 @@ export async function mutateProcessor( const label = action.mutateCallback.name; Log.info(actionMetadata, `Processing mutation action (${label})`); - matchedAction = true; // Add annotations to the request to indicate that the capability started processing @@ -79,6 +78,7 @@ export async function mutateProcessor( // Run the action await action.mutateCallback(wrapped); + // Log on success Log.info(actionMetadata, `Mutation action succeeded (${label})`); // Add annotations to the request to indicate that the capability succeeded @@ -99,6 +99,7 @@ export async function mutateProcessor( errorMessage = "An error occurred with the mutate action."; } + // Log on failure Log.error(actionMetadata, `Action failed: ${errorMessage}`); response.warnings.push(`Action failed: ${errorMessage}`); diff --git a/src/lib/schedule.ts b/src/lib/schedule.ts index a293b474..19db58cf 100644 --- a/src/lib/schedule.ts +++ b/src/lib/schedule.ts @@ -3,7 +3,7 @@ import { PeprStore } from "./storage"; -type Unit = "seconds" | "second" | "minute" | "minutes" | "hours" | "hour"; +export type Unit = "seconds" | "second" | "minute" | "minutes" | "hours" | "hour"; export interface Schedule { /** diff --git a/src/lib/types.ts b/src/lib/types.ts index eb2aa7fb..1dfbd42e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -2,11 +2,15 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { GenericClass, GroupVersionKind, KubernetesObject } from "kubernetes-fluent-client"; -import { WatchAction } from "kubernetes-fluent-client/dist/fluent/types"; + +import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types"; + import { PeprMutateRequest } from "./mutate-request"; import { PeprValidateRequest } from "./validate-request"; import { Answers } from "prompts"; +import { Logger } from "pino"; + export enum Operation { CREATE = "CREATE", UPDATE = "UPDATE", @@ -108,9 +112,10 @@ export type Binding = { annotations: Record; deletionTimestamp: boolean; }; + alias?: string; readonly mutateCallback?: MutateAction>; readonly validateCallback?: ValidateAction>; - readonly watchCallback?: WatchAction>; + readonly watchCallback?: WatchLogAction>; readonly finalizeCallback?: FinalizeAction>; }; @@ -179,6 +184,7 @@ export type CommonActionChain = MutateActionChain & { * @param action The action to be executed when the Kubernetes resource is processed by the AdmissionController. */ Mutate: (action: MutateAction>) => MutateActionChain; + Alias: (alias: string) => BindingFilter; }; export type ValidateActionChain = { @@ -193,7 +199,8 @@ export type ValidateActionChain = { * @param action * @returns */ - Watch: (action: WatchAction>) => FinalizeActionChain; + + Watch: (action: WatchLogAction>) => FinalizeActionChain; /** * Establish a reconcile for the specified resource. The callback function will be executed after the admission controller has @@ -206,7 +213,8 @@ export type ValidateActionChain = { * @param action * @returns */ - Reconcile: (action: WatchAction>) => FinalizeActionChain; + + Reconcile: (action: WatchLogAction>) => FinalizeActionChain; }; export type MutateActionChain = ValidateActionChain & { @@ -236,12 +244,21 @@ export type MutateActionChain = ValidateActionChain & export type MutateAction> = ( req: PeprMutateRequest, + logger?: Logger, ) => Promise | void | Promise> | PeprMutateRequest; export type ValidateAction> = ( req: PeprValidateRequest, + logger?: Logger, ) => Promise | ValidateActionResponse; +// Define WatchLogAction by adding an optional logger parameter to the WatchAction +export type WatchLogAction> = ( + update: K, + phase: WatchPhase, + logger?: Logger, +) => Promise | void; + export type ValidateActionResponse = { allowed: boolean; statusCode?: number; @@ -250,6 +267,7 @@ export type ValidateActionResponse = { export type FinalizeAction> = ( update: K, + logger?: Logger, ) => Promise | void; export type FinalizeActionChain = {