diff --git a/packages/cactus-plugin-keychain-vault/README.md b/packages/cactus-plugin-keychain-vault/README.md index 979c23345a..da0802eb83 100644 --- a/packages/cactus-plugin-keychain-vault/README.md +++ b/packages/cactus-plugin-keychain-vault/README.md @@ -1,9 +1,41 @@ # `@hyperledger/cactus-plugin-keychain-vault` -> TODO: description +## Prometheus Exporter -## Usage +This class creates a prometheus exporter, which scrapes the transactions (total transaction count) for the use cases incorporating the use of Keychain vault plugin. + +### Usage +The prometheus exporter object is initialized in the `PluginKeychainVault` class constructor itself, so instantiating the object of the `PluginKeychainVault` class, gives access to the exporter object. +You can also initialize the prometheus exporter object seperately and then pass it to the `IPluginKeychainVaultOptions` interface for `PluginKeychainVault` constructor. + +`getPrometheusExporterMetricsV1` function returns the prometheus exporter metrics, currently displaying the total key count, which currently increments everytime the `set()` and `delete()` method of the `PluginKeychainVault` class is called. + +### Prometheus Integration +To use Prometheus with this exporter make sure to install [Prometheus main component](https://prometheus.io/download/). +Once Prometheus is setup, the corresponding scrape_config needs to be added to the prometheus.yml + +```(yaml) +- job_name: 'keychain_vault_exporter' + metrics_path: api/v1/plugins/@hyperledger/cactus-plugin-keychain-vault/get-prometheus-exporter-metrics + scrape_interval: 5s + static_configs: + - targets: ['{host}:{port}'] ``` -// TODO: DEMONSTRATE API -``` + +Here the `host:port` is where the prometheus exporter metrics are exposed. The test cases (For example, packages/cactus-plugin-keychain-vault/src/test/typescript/integration/plugin-keychain-vault.test.ts) exposes it over `0.0.0.0` and a random port(). The random port can be found in the running logs of the test case and looks like (42379 in the below mentioned URL) +`Metrics URL: http://0.0.0.0:42379/api/v1/plugins/@hyperledger/cactus-plugin-keychain-plugin/get-prometheus-exporter-metrics` + +Once edited, you can start the prometheus service by referencing the above edited prometheus.yml file. +On the prometheus graphical interface (defaulted to http://localhost:9090), choose **Graph** from the menu bar, then select the **Console** tab. From the **Insert metric at cursor** drop down, select **cactus_keychain_vault_total_key_count** and click **execute** + +### Helper code + +###### response.type.ts +This file contains the various responses of the metrics. + +###### data-fetcher.ts +This file contains functions encasing the logic to process the data points + +###### metrics.ts +This file lists all the prometheus metrics and what they are used for. diff --git a/packages/cactus-plugin-keychain-vault/package-lock.json b/packages/cactus-plugin-keychain-vault/package-lock.json index af1e1f2525..80fd421c9f 100644 --- a/packages/cactus-plugin-keychain-vault/package-lock.json +++ b/packages/cactus-plugin-keychain-vault/package-lock.json @@ -186,6 +186,11 @@ "tweetnacl": "^0.14.3" } }, + "bintrees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -249,6 +254,17 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -265,6 +281,15 @@ "ms": "2.0.0" } }, + "default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "requires": { + "execa": "^5.0.0" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -309,6 +334,23 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "execa": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", + "integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==", + "dev": true, + "requires": { + "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" + } + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -410,6 +452,12 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "get-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", + "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", + "dev": true + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -454,6 +502,17 @@ "sshpk": "^1.7.0" } }, + "http-status-codes": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.1.4.tgz", + "integrity": "sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg==" + }, + "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 + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -467,16 +526,55 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "internal-ip": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", + "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", + "dev": true, + "requires": { + "default-gateway": "^6.0.0", + "ipaddr.js": "^1.9.1", + "is-ip": "^3.1.0", + "p-event": "^4.2.0" + } + }, + "ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "dev": true, + "requires": { + "ip-regex": "^4.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -528,6 +626,12 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -551,6 +655,12 @@ "mime-db": "1.45.0" } }, + "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 + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -588,6 +698,15 @@ } } }, + "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, + "requires": { + "path-key": "^3.0.0" + } + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -601,17 +720,56 @@ "ee-first": "1.1.1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "openapi-types": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-7.2.2.tgz", "integrity": "sha512-MJribYRLdEcnKX7SN+CKJfI0cZaPxk4mp71uE9gUWkh+6xnR9LDq2otVUly4/vtULa/wwvmOsx29lQCPy2jyAA==", "dev": true }, + "p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "dev": true, + "requires": { + "p-timeout": "^3.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -622,6 +780,14 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "prom-client": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-13.1.0.tgz", + "integrity": "sha512-jT9VccZCWrJWXdyEtQddCDszYsiuWj5T0ekrPszi/WEegj3IZy6Mm09iOOVM86A4IKMWq8hZkT2dD9MaSe+sng==", + "requires": { + "tdigest": "^0.1.1" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -767,6 +933,27 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -793,6 +980,20 @@ "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" }, + "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 + }, + "tdigest": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", + "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=", + "requires": { + "bintrees": "1.0.1" + } + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -883,6 +1084,15 @@ "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } } } } diff --git a/packages/cactus-plugin-keychain-vault/package.json b/packages/cactus-plugin-keychain-vault/package.json index adecdd83c6..cb0b55c690 100644 --- a/packages/cactus-plugin-keychain-vault/package.json +++ b/packages/cactus-plugin-keychain-vault/package.json @@ -14,6 +14,7 @@ "scripts": { "generate-sdk": "openapi-generator generate --input-spec src/main/json/openapi.json -g typescript-axios -o ./src/main/typescript/generated/openapi/typescript-axios/", "tsc": "tsc --project ./tsconfig.json", + "watch": "npm-watch", "pretsc": "npm run generate-sdk", "webpack": "npm-run-all webpack:dev webpack:prod", "webpack:dev": "npm-run-all webpack:dev:node webpack:dev:web", @@ -23,6 +24,24 @@ "webpack:prod:web": "webpack --env=prod --target=web --config ../../webpack.config.js", "webpack:prod:node": "webpack --env=prod --target=node --config ../../webpack.config.js" }, + "watch": { + "tsc": { + "patterns": [ + "src/", + "src/*/json/**/openapi*" + ], + "ignore": [ + "src/**/generated/*" + ], + "extensions": [ + "ts", + "json" + ], + "quiet": true, + "verbose": false, + "runOnChangeOnly": true + } + }, "publishConfig": { "access": "public" }, @@ -67,8 +86,10 @@ "@hyperledger/cactus-common": "0.3.0", "@hyperledger/cactus-core": "0.3.0", "@hyperledger/cactus-core-api": "0.3.0", + "prom-client": "13.1.0", "axios": "0.21.1", "express": "4.17.1", + "http-status-codes": "2.1.4", "node-vault": "0.9.21", "typescript-optional": "2.0.1" }, @@ -77,6 +98,7 @@ "@types/express": "4.17.8", "@types/express-serve-static-core": "4.17.17", "@types/request": "2.48.5", + "internal-ip": "6.2.0", "openapi-types": "7.2.2" } } diff --git a/packages/cactus-plugin-keychain-vault/src/main/json/openapi.json b/packages/cactus-plugin-keychain-vault/src/main/json/openapi.json index 7ef540d825..4eedf801b7 100644 --- a/packages/cactus-plugin-keychain-vault/src/main/json/openapi.json +++ b/packages/cactus-plugin-keychain-vault/src/main/json/openapi.json @@ -10,7 +10,12 @@ } }, "components": { - "schemas": {} + "schemas": { + "PrometheusExporterMetricsResponse": { + "type": "string", + "nullable": false + } + } }, "paths": { "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-vault/get-keychain-entry": { @@ -75,6 +80,31 @@ } } } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-vault/get-prometheus-exporter-metrics": { + "get": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "get", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-vault/get-prometheus-exporter-metrics" + } + }, + "operationId": "getPrometheusExporterMetricsV1", + "summary": "Get the Prometheus Metrics", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/PrometheusExporterMetricsResponse" + } + } + } + } + } + } } } } \ No newline at end of file diff --git a/packages/cactus-plugin-keychain-vault/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-keychain-vault/src/main/typescript/generated/openapi/typescript-axios/api.ts index 73497ffc4f..12b90d4ef2 100644 --- a/packages/cactus-plugin-keychain-vault/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-keychain-vault/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -135,6 +135,42 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @summary Get the Prometheus Metrics + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPrometheusExporterMetricsV1: async (options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-keychain-vault/get-prometheus-exporter-metrics`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, 'https://example.com'); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + const query = new URLSearchParams(localVarUrlObj.search); + for (const key in localVarQueryParameter) { + query.set(key, localVarQueryParameter[key]); + } + for (const key in options.query) { + query.set(key, options.query[key]); + } + localVarUrlObj.search = (new URLSearchParams(query)).toString(); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, /** * * @summary Sets a value under a key on the keychain backend. @@ -203,6 +239,19 @@ export const DefaultApiFp = function(configuration?: Configuration) { return axios.request(axiosRequestArgs); }; }, + /** + * + * @summary Get the Prometheus Metrics + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPrometheusExporterMetricsV1(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await DefaultApiAxiosParamCreator(configuration).getPrometheusExporterMetricsV1(options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; + }, /** * * @summary Sets a value under a key on the keychain backend. @@ -236,6 +285,15 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa getKeychainEntry(getKeychainEntryRequest: GetKeychainEntryRequest, options?: any): AxiosPromise { return DefaultApiFp(configuration).getKeychainEntry(getKeychainEntryRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Get the Prometheus Metrics + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPrometheusExporterMetricsV1(options?: any): AxiosPromise { + return DefaultApiFp(configuration).getPrometheusExporterMetricsV1(options).then((request) => request(axios, basePath)); + }, /** * * @summary Sets a value under a key on the keychain backend. @@ -268,6 +326,17 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).getKeychainEntry(getKeychainEntryRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Get the Prometheus Metrics + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getPrometheusExporterMetricsV1(options?: any) { + return DefaultApiFp(this.configuration).getPrometheusExporterMetricsV1(options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Sets a value under a key on the keychain backend. diff --git a/packages/cactus-plugin-keychain-vault/src/main/typescript/plugin-keychain-vault.ts b/packages/cactus-plugin-keychain-vault/src/main/typescript/plugin-keychain-vault.ts index 5f8326b973..20c1e1d63c 100644 --- a/packages/cactus-plugin-keychain-vault/src/main/typescript/plugin-keychain-vault.ts +++ b/packages/cactus-plugin-keychain-vault/src/main/typescript/plugin-keychain-vault.ts @@ -4,6 +4,7 @@ import { Server as SecureServer } from "https"; import { Express } from "express"; import { Optional } from "typescript-optional"; import Vault from "node-vault"; +import HttpStatus from "http-status-codes"; import { Logger, @@ -18,8 +19,18 @@ import { IWebServiceEndpoint, PluginAspect, } from "@hyperledger/cactus-core-api"; -import { GetKeychainEntryEndpointV1 } from "./web-services/get-keychain-entry-endpoint-v1"; -import { SetKeychainEntryEndpointV1 } from "./web-services/set-keychain-entry-endpoint-v1"; + +// TODO: Writing the getExpressRequestHandler() method for +// GetKeychainEntryEndpointV1 and SetKeychainEntryEndpointV1 +// import { GetKeychainEntryEndpointV1 } from "./web-services/get-keychain-entry-endpoint-v1"; +// import { SetKeychainEntryEndpointV1 } from "./web-services/set-keychain-entry-endpoint-v1"; + +import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; + +import { + IGetPrometheusExporterMetricsEndpointV1Options, + GetPrometheusExporterMetricsEndpointV1, +} from "./web-services/get-prometheus-exporter-metrics-endpoint-v1"; export interface IPluginKeychainVaultOptions extends ICactusPluginOptions { logLevel?: LogLevelDesc; @@ -38,8 +49,20 @@ export interface IPluginKeychainVaultOptions extends ICactusPluginOptions { * The `VAULT_TOKEN` which the backing Vault instance will accept as valid. */ token: string; + /** + * Prometheus Exporter object for metrics monitoring + */ + + prometheusExporter?: PrometheusExporter; + + /** + * The HTTP path prefix where the KV Secrets Engine is mounted. + */ + kvSecretsMountPath?: string; } +export const K_DEFAULT_KV_SECRETS_MOUNT_PATH = "secret/"; + export class PluginKeychainVault implements ICactusPlugin, IPluginWebService { public static readonly CLASS_NAME = "PluginKeychainVault"; @@ -48,7 +71,9 @@ export class PluginKeychainVault implements ICactusPlugin, IPluginWebService { private readonly endpoint: string; private readonly log: Logger; private readonly instanceId: string; + private readonly kvSecretsMountPath: string; private readonly backend: Vault.client; + public prometheusExporter: PrometheusExporter; public get className() { return PluginKeychainVault.CLASS_NAME; @@ -73,33 +98,67 @@ export class PluginKeychainVault implements ICactusPlugin, IPluginWebService { this.endpoint = this.opts.endpoint; this.apiVersion = this.opts.apiVersion || "v1"; + this.kvSecretsMountPath = + opts.kvSecretsMountPath || K_DEFAULT_KV_SECRETS_MOUNT_PATH; + this.log.info(`this.kvSecretsMountPath=${this.kvSecretsMountPath}`); + this.backend = Vault({ apiVersion: this.apiVersion, endpoint: this.endpoint, token: this.token, }); + + this.prometheusExporter = + opts.prometheusExporter || + new PrometheusExporter({ pollingIntervalInMin: 1 }); + Checks.truthy( + this.prometheusExporter, + `${fnTag} options.prometheusExporter`, + ); + this.log.info(`Created Vault backend OK. Endpoint=${this.endpoint}`); this.log.info(`Created ${this.className}. KeychainID=${opts.keychainId}`); } + public getPrometheusExporter(): PrometheusExporter { + return this.prometheusExporter; + } + + public async getPrometheusExporterMetrics(): Promise { + const res: string = await this.prometheusExporter.getPrometheusMetrics(); + this.log.debug(`getPrometheusExporterMetrics() response: %o`, res); + return res; + } + public async installWebServices( expressApp: Express, ): Promise { const endpoints: IWebServiceEndpoint[] = []; - { - const ep = new GetKeychainEntryEndpointV1({ - logLevel: this.opts.logLevel, - }); - ep.registerExpress(expressApp); - endpoints.push(ep); - } + // TODO: Writing the getExpressRequestHandler() method for + // GetKeychainEntryEndpointV1 and SetKeychainEntryEndpointV1 + // { + // const ep = new GetKeychainEntryEndpointV1({ + // logLevel: this.opts.logLevel, + // }); + // ep.registerExpress(expressApp); + // endpoints.push(ep); + // } + // { + // const ep = new SetKeychainEntryEndpointV1({ + // logLevel: this.opts.logLevel, + // }); + // ep.registerExpress(expressApp); + // endpoints.push(ep); + // } { - const ep = new SetKeychainEntryEndpointV1({ + const opts: IGetPrometheusExporterMetricsEndpointV1Options = { + plugin: this, logLevel: this.opts.logLevel, - }); + }; + const ep = new GetPrometheusExporterMetricsEndpointV1(opts); ep.registerExpress(expressApp); endpoints.push(ep); } @@ -139,21 +198,69 @@ export class PluginKeychainVault implements ICactusPlugin, IPluginWebService { return null as any; } + protected pathFor(key: string): string { + return `${this.kvSecretsMountPath}${key}`; + } + async get(key: string): Promise { - const value = await this.backend.read(key); - return value; + const fnTag = `${this.className}#get(key: string)`; + const path = this.pathFor(key); + try { + const res = await this.backend.read(path); + this.log.debug(`Response from Vault: %o`, () => JSON.stringify(res)); + if (res?.data?.data?.value) { + return res.data.data.value; + } else { + throw new Error( + `${fnTag}: Invalid response received from Vault. Expected "response.data.data.value" property chain to be truthy`, + ); + } + } catch (ex) { + if (ex?.response?.statusCode === HttpStatus.NOT_FOUND) { + return (null as unknown) as T; + } else { + this.log.error(`Retrieval of "${key}" crashed:`, ex); + throw ex; + } + } } + /** + * Detects the presence of a key by trying to read it and then + * observing whether an HTTP 404 NOT FOUND error is returned or + * not and deciding whether the keychain has the entry ot not + * based on this. + */ async has(key: string): Promise { - const list = await this.backend.list(key); - return list.length > 0; + const path = this.pathFor(key); + try { + const res = await this.backend.read(path); + return res; + } catch (ex) { + // We have to make sure that the exception is either an expected + // or an unexpected one where the expeted exception is what we + // get when the key is not present in the keychain and anything + // else being an unexpected exception that we do not want to + // handle nor suppress under any circumstances since doing so + // would lead to silent failures or worse. + if (ex?.response?.statusCode === HttpStatus.NOT_FOUND) { + return false; + } else { + this.log.error(`Presence check of "${key}" crashed:`, ex); + throw ex; + } + } } async set(key: string, value: T): Promise { - await this.backend.write(key, value); + const path = this.pathFor(key); + await this.backend.write(path, { data: { value } }); + this.prometheusExporter.setTotalKeyCounter(key, "set"); } async delete(key: string): Promise { - await this.backend.delete(key); + const path = this.pathFor(key); + await this.backend.delete(path); + this.prometheusExporter.setTotalKeyCounter(key, "delete"); } } diff --git a/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/data-fetcher.ts b/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/data-fetcher.ts new file mode 100644 index 0000000000..2868b41cf9 --- /dev/null +++ b/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/data-fetcher.ts @@ -0,0 +1,12 @@ +import { VaultKeys } from "./response.type"; + +import { + totalKeyCount, + K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT, +} from "./metrics"; + +export async function collectMetrics(vaultKeys: VaultKeys) { + totalKeyCount + .labels(K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT) + .set(vaultKeys.size); +} diff --git a/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/metrics.ts b/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/metrics.ts new file mode 100644 index 0000000000..2f65f2e0c7 --- /dev/null +++ b/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/metrics.ts @@ -0,0 +1,11 @@ +import { Gauge } from "prom-client"; + +export const K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT = + "cactus_keychain_vault_managed_key_count"; + +export const totalKeyCount = new Gauge({ + name: K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT, + help: + "The number of keys that were set in the backing Vault deployment via this specific keychain plugin instance", + labelNames: ["type"], +}); diff --git a/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/prometheus-exporter.ts b/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/prometheus-exporter.ts new file mode 100644 index 0000000000..f25017e07c --- /dev/null +++ b/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/prometheus-exporter.ts @@ -0,0 +1,42 @@ +import promClient from "prom-client"; +import { VaultKeys } from "./response.type"; +import { collectMetrics } from "./data-fetcher"; +import { K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT } from "./metrics"; + +export interface IPrometheusExporterOptions { + pollingIntervalInMin?: number; +} + +export class PrometheusExporter { + public readonly metricsPollingIntervalInMin: number; + public readonly vaultKeys: VaultKeys = new Map(); + + constructor( + public readonly prometheusExporterOptions: IPrometheusExporterOptions, + ) { + this.metricsPollingIntervalInMin = + prometheusExporterOptions.pollingIntervalInMin || 1; + } + + public setTotalKeyCounter(key: string, operation: string): void { + if (operation === "set") { + this.vaultKeys.set(key, "keychain-vault"); + } else { + this.vaultKeys.delete(key); + } + collectMetrics(this.vaultKeys); + } + + public async getPrometheusMetrics(): Promise { + const result = await promClient.register.getSingleMetricAsString( + K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT, + ); + return result; + } + + public startMetricsCollection(): void { + const Registry = promClient.Registry; + const register = new Registry(); + promClient.collectDefaultMetrics({ register }); + } +} diff --git a/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/response.type.ts b/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/response.type.ts new file mode 100644 index 0000000000..7af951c533 --- /dev/null +++ b/packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/response.type.ts @@ -0,0 +1 @@ +export type VaultKeys = Map; diff --git a/packages/cactus-plugin-keychain-vault/src/main/typescript/web-services/get-prometheus-exporter-metrics-endpoint-v1.ts b/packages/cactus-plugin-keychain-vault/src/main/typescript/web-services/get-prometheus-exporter-metrics-endpoint-v1.ts new file mode 100644 index 0000000000..8cd2fcffb4 --- /dev/null +++ b/packages/cactus-plugin-keychain-vault/src/main/typescript/web-services/get-prometheus-exporter-metrics-endpoint-v1.ts @@ -0,0 +1,81 @@ +import { Express, Request, Response } from "express"; + +import { + Logger, + LoggerProvider, + LogLevelDesc, + Checks, +} from "@hyperledger/cactus-common"; + +import { + IWebServiceEndpoint, + IExpressRequestHandler, +} from "@hyperledger/cactus-core-api/"; + +import OAS from "../../json/openapi.json"; + +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; + +import { PluginKeychainVault } from "../plugin-keychain-vault"; + +export interface IGetPrometheusExporterMetricsEndpointV1Options { + logLevel?: LogLevelDesc; + plugin: PluginKeychainVault; +} + +export class GetPrometheusExporterMetricsEndpointV1 + implements IWebServiceEndpoint { + private readonly log: Logger; + + constructor( + public readonly opts: IGetPrometheusExporterMetricsEndpointV1Options, + ) { + const fnTag = "GetPrometheusExporterMetricsEndpointV1#constructor()"; + + Checks.truthy(opts, `${fnTag} options`); + Checks.truthy(opts.plugin, `${fnTag} options.plugin`); + + this.log = LoggerProvider.getOrCreate({ + label: "get-prometheus-exporter-metrics-v1", + level: opts.logLevel || "INFO", + }); + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public getPath(): string { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-vault/get-prometheus-exporter-metrics" + ].get["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-vault/get-prometheus-exporter-metrics" + ].get["x-hyperledger-cactus"].http.verbLowerCase; + } + + public registerExpress(app: Express): IWebServiceEndpoint { + registerWebServiceEndpoint(app, this); + return this; + } + + async handleRequest(req: Request, res: Response): Promise { + const fnTag = "GetPrometheusExporterMetrics#handleRequest()"; + const verbUpper = this.getVerbLowerCase().toUpperCase(); + this.log.debug(`${verbUpper} ${this.getPath()}`); + + try { + const resBody = await this.opts.plugin.getPrometheusExporterMetrics(); + res.status(200); + res.send(resBody); + } catch (ex) { + this.log.error(`${fnTag} failed to serve request`, ex); + res.status(500); + res.statusMessage = ex.message; + res.json({ error: ex.stack }); + } + } +} diff --git a/packages/cactus-plugin-keychain-vault/src/test/typescript/integration/plugin-keychain-vault.test.ts b/packages/cactus-plugin-keychain-vault/src/test/typescript/integration/plugin-keychain-vault.test.ts index 5383897416..402a0d2d64 100644 --- a/packages/cactus-plugin-keychain-vault/src/test/typescript/integration/plugin-keychain-vault.test.ts +++ b/packages/cactus-plugin-keychain-vault/src/test/typescript/integration/plugin-keychain-vault.test.ts @@ -1,113 +1,206 @@ -// import test, { Test } from "tape-promise/tape"; - -// import { v4 as uuidv4 } from "uuid"; -// import { -// IPluginKeychainVaultOptions, -// PluginKeychainVault, -// } from "../../../main/typescript/public-api"; - -// test("PluginKeychainVault", (t1: Test) => { -// t1.doesNotThrow( -// () => new PluginKeychainVault({ instanceId: "a", keychainId: "a" }) -// ); - -// test("Validates constructor arg instanceId", (t: Test) => { -// t.throws( -// () => -// new PluginKeychainVault({ -// instanceId: null as any, -// keychainId: "valid-value", -// }) -// ); -// t.throws( -// () => -// new PluginKeychainVault({ -// instanceId: "", -// keychainId: "valid-value", -// }) -// ); -// t.end(); -// }); - -// test("Validates constructor arg keychainId", (t: Test) => { -// t.throws( -// () => -// new PluginKeychainVault({ -// instanceId: "valid-value", -// keychainId: null as any, -// }) -// ); -// t.throws( -// () => -// new PluginKeychainVault({ -// instanceId: "valid-value", -// keychainId: "", -// }) -// ); -// t.end(); -// }); - -// test("get,set,has,delete alters state as expected", async (t: Test) => { -// const options: IPluginKeychainVaultOptions = { -// instanceId: uuidv4(), -// keychainId: uuidv4(), -// }; -// const plugin = new PluginKeychainVault(options); -// t.equal(plugin.getKeychainId(), options.keychainId, "Keychain ID set OK"); -// t.equal(plugin.getInstanceId(), options.instanceId, "Instance ID set OK"); - -// const key = uuidv4(); -// const value = uuidv4(); - -// const hasPrior = await plugin.has(key); -// t.false(hasPrior, "hasPrior === false OK"); - -// await plugin.set(key, value); - -// const hasAfter = await plugin.has(key); -// t.true(hasAfter, "hasAfter === true OK"); - -// const valueAfter = await plugin.get(key); -// t.ok(valueAfter, "valueAfter truthy OK"); -// t.equal(valueAfter, value, "valueAfter === value OK"); - -// await plugin.delete(key); - -// const hasAfterDelete = await plugin.has(key); -// t.false(hasAfterDelete, "hasAfterDelete === false OK"); - -// const valueAfterDelete = await plugin.get(key); -// t.notok(valueAfterDelete, "valueAfterDelete falsy OK"); - -// t.end(); -// }); - -// test("rotateEncryptionKeys() fails fast", async (t: Test) => { -// const options: IPluginKeychainVaultOptions = { -// instanceId: uuidv4(), -// keychainId: uuidv4(), -// }; -// const plugin = new PluginKeychainVault(options); - -// const promise = plugin.rotateEncryptionKeys(); -// const expected = /not implemented/; -// await t.rejects(promise, expected, "rotateEncryptionKeys() rejects OK"); - -// t.end(); -// }); - -// test("getEncryptionAlgorithm() returns null", (t: Test) => { -// const options: IPluginKeychainVaultOptions = { -// instanceId: uuidv4(), -// keychainId: uuidv4(), - -// }; -// const plugin = new PluginKeychainVault(options); - -// t.notok(plugin.getEncryptionAlgorithm(), "encryption algorithm falsy OK"); - -// t.end(); -// }); - -// t1.end(); -// }); +import test, { Test } from "tape-promise/tape"; +import { v4 as internalIpV4 } from "internal-ip"; + +import express from "express"; +import bodyParser from "body-parser"; +import http from "http"; +import { AddressInfo } from "net"; + +import { + Containers, + K_DEFAULT_VAULT_DEV_ROOT_TOKEN, + K_DEFAULT_VAULT_HTTP_PORT, + VaultTestServer, +} from "@hyperledger/cactus-test-tooling"; + +import { v4 as uuidv4 } from "uuid"; + +import { + LogLevelDesc, + IListenOptions, + Servers, +} from "@hyperledger/cactus-common"; + +import { + IPluginKeychainVaultOptions, + PluginKeychainVault, +} from "../../../main/typescript/public-api"; + +import { K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT } from "../../../main/typescript/prometheus-exporter/metrics"; + +import { DefaultApi as KeychainVaultApi } from "../../../main/typescript/public-api"; + +const logLevel: LogLevelDesc = "TRACE"; + +test("get,set,has,delete alters state as expected", async (t: Test) => { + const vaultTestContainer = new VaultTestServer({}); + await vaultTestContainer.start(); + + const ci = await Containers.getById(vaultTestContainer.containerId); + const vaultIpAddr = await internalIpV4(); + const hostPort = await Containers.getPublicPort( + K_DEFAULT_VAULT_HTTP_PORT, + ci, + ); + const vaultHost = `http://${vaultIpAddr}:${hostPort}`; + + test.onFinish(async () => { + await vaultTestContainer.stop(); + await vaultTestContainer.destroy(); + }); + + const options: IPluginKeychainVaultOptions = { + instanceId: uuidv4(), + keychainId: uuidv4(), + endpoint: vaultHost, + token: K_DEFAULT_VAULT_DEV_ROOT_TOKEN, + apiVersion: "v1", + kvSecretsMountPath: "secret/data/", + logLevel, + }; + const plugin = new PluginKeychainVault(options); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + const server = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "0.0.0.0", + port: 0, + server, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + test.onFinish(async () => await Servers.shutdown(server)); + const { address, port } = addressInfo; + const apiHost = `http://${address}:${port}`; + t.comment( + `Metrics URL: ${apiHost}/api/v1/plugins/@hyperledger/cactus-plugin-keychain-vault/get-prometheus-exporter-metrics`, + ); + const apiClient = new KeychainVaultApi({ basePath: apiHost }); + + await plugin.installWebServices(expressApp); + + t.equal(plugin.getKeychainId(), options.keychainId, "Keychain ID set OK"); + t.equal(plugin.getInstanceId(), options.instanceId, "Instance ID set OK"); + + const key1 = uuidv4(); + const value1 = uuidv4(); + + const hasPrior1 = await plugin.has(key1); + + t.false(hasPrior1, "hasPrior === false OK"); + + await plugin.set(key1, value1); + + const hasAfter1 = await plugin.has(key1); + t.true(hasAfter1, "hasAfter === true OK"); + + const valueAfter1 = await plugin.get(key1); + t.ok(valueAfter1, "valueAfter truthy OK"); + t.equal(valueAfter1, value1, "valueAfter === value OK"); + + await plugin.delete(key1); + + const hasAfterDelete1 = await plugin.has(key1); + t.false(hasAfterDelete1, "hasAfterDelete === false OK"); + + const valueAfterDelete1 = await plugin.get(key1); + t.notok(valueAfterDelete1, "valueAfterDelete falsy OK"); + + { + const res = await apiClient.getPrometheusExporterMetricsV1(); + const promMetricsOutput = + "# HELP " + + K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT + + " The number of keys that were set in the backing Vault deployment via this specific keychain plugin instance\n" + + "# TYPE " + + K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT + + " gauge\n" + + K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT + + '{type="' + + K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT + + '"} 0'; + t.ok(res); + t.ok(res.data); + t.equal(res.status, 200); + t.true( + res.data.includes(promMetricsOutput), + "Total Key Count 0 recorded as expected. RESULT OK", + ); + } + + const key2 = uuidv4(); + const value2 = uuidv4(); + + const hasPrior2 = await plugin.has(key2); + + t.false(hasPrior2, "hasPrior === false OK"); + + await plugin.set(key2, value2); + + const hasAfter2 = await plugin.has(key2); + t.true(hasAfter2, "hasAfter === true OK"); + + const valueAfter2 = await plugin.get(key2); + t.ok(valueAfter2, "valueAfter truthy OK"); + t.equal(valueAfter2, value2, "valueAfter === value OK"); + + { + const res = await apiClient.getPrometheusExporterMetricsV1(); + const promMetricsOutput = + "# HELP " + + K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT + + " The number of keys that were set in the backing Vault deployment via this specific keychain plugin instance\n" + + "# TYPE " + + K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT + + " gauge\n" + + K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT + + '{type="' + + K_CACTUS_KEYCHAIN_VAULT_MANAGED_KEY_COUNT + + '"} 1'; + t.ok(res); + t.ok(res.data); + t.equal(res.status, 200); + t.true( + res.data.includes(promMetricsOutput), + "Total Key Count 1 recorded as expected. RESULT OK", + ); + } + + t.end(); +}); + +// FIXME: Writing the getExpressRequestHandler() method for +// GetKeychainEntryEndpointV1 and SetKeychainEntryEndpointV1 + +test.skip("rotateEncryptionKeys() fails fast", async (t: Test) => { + const options: IPluginKeychainVaultOptions = { + instanceId: uuidv4(), + keychainId: uuidv4(), + endpoint: "http://127.0.0.1:9200", + token: "root", + }; + const plugin = new PluginKeychainVault(options); + + const promise = plugin.rotateEncryptionKeys(); + const expected = /not implemented/; + await t.rejects(promise, expected, "rotateEncryptionKeys() rejects OK"); + + t.end(); +}); + +// FIXME: Writing the getExpressRequestHandler() method for +// GetKeychainEntryEndpointV1 and SetKeychainEntryEndpointV1 + +test.skip("getEncryptionAlgorithm() returns null", (t: Test) => { + const options: IPluginKeychainVaultOptions = { + instanceId: uuidv4(), + keychainId: uuidv4(), + endpoint: "http://127.0.0.1:9200", + token: "root", + }; + const plugin = new PluginKeychainVault(options); + + t.notok(plugin.getEncryptionAlgorithm(), "encryption algorithm falsy OK"); + + t.end(); +});