From b9fe98b0add352b0d68a5fe446d3fa000603d21c Mon Sep 17 00:00:00 2001 From: Jagpreet Singh Sasan Date: Wed, 17 Mar 2021 12:02:40 +0530 Subject: [PATCH] feat(keychain-vault): add prometheus exporter Primary Change -------------- 1. The keychain-vault plugin now includes the prometheus metrics exporter integration 2. OpenAPI spec now has api endpoint for the getting the prometheus metrics Refactorings that were also necessary to accomodate 1) and 2) ------------------------------------------------------------ 3. GetPrometheusMetricsV1 class is created to handle the corresponding api endpoint 4. IPluginKeychainVaultOptions interface in PluginKeychainVault class now has a prometheusExporter optional field 5. The PluginKeychainMemory class has relevant functions and codes to incorporate prometheus exporter 6. Added Readme.md on the prometheus exporter usage Fixes #536 Signed-off-by: Jagpreet Singh Sasan --- .../cactus-plugin-keychain-vault/README.md | 40 ++- .../package-lock.json | 210 ++++++++++++ .../cactus-plugin-keychain-vault/package.json | 22 ++ .../src/main/json/openapi.json | 32 +- .../generated/openapi/typescript-axios/api.ts | 69 ++++ .../main/typescript/plugin-keychain-vault.ts | 141 +++++++- .../prometheus-exporter/data-fetcher.ts | 12 + .../typescript/prometheus-exporter/metrics.ts | 11 + .../prometheus-exporter.ts | 42 +++ .../prometheus-exporter/response.type.ts | 1 + ...prometheus-exporter-metrics-endpoint-v1.ts | 81 +++++ .../integration/plugin-keychain-vault.test.ts | 319 +++++++++++------- 12 files changed, 845 insertions(+), 135 deletions(-) create mode 100644 packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/data-fetcher.ts create mode 100644 packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/metrics.ts create mode 100644 packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/prometheus-exporter.ts create mode 100644 packages/cactus-plugin-keychain-vault/src/main/typescript/prometheus-exporter/response.type.ts create mode 100644 packages/cactus-plugin-keychain-vault/src/main/typescript/web-services/get-prometheus-exporter-metrics-endpoint-v1.ts 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(); +});