From cb9c8a02ffaf1640054f390c7ebfd637821ea5e9 Mon Sep 17 00:00:00 2001 From: Gage Krumbach Date: Wed, 22 May 2024 12:25:41 -0500 Subject: [PATCH] Add s3 storage artifact route and ui integration of it chore: Enable fetching markdown from storage for CompareRunsMetricsSection Add minio dependency chore: Update minio dependency to version 7.1.3 --- backend/package-lock.json | 275 +++++++++++++++++- backend/package.json | 1 + backend/src/routes/api/storage/index.ts | 47 +++ .../src/routes/api/storage/storageUtils.ts | 87 ++++++ backend/src/types.ts | 74 +++++ .../content/artifacts/ArtifactUriLink.tsx | 41 +++ .../pipelines/content/artifacts/utils.ts | 13 + .../markdown/MarkdownCompare.tsx | 22 +- .../ArtifactOverviewDetails.tsx | 5 +- .../experiments/artifacts/ArtifactsTable.tsx | 5 +- .../compareRuns/CompareRunsMetricsSection.tsx | 1 - frontend/src/services/storageService.ts | 19 ++ 12 files changed, 571 insertions(+), 19 deletions(-) create mode 100644 backend/src/routes/api/storage/index.ts create mode 100644 backend/src/routes/api/storage/storageUtils.ts create mode 100644 frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx create mode 100644 frontend/src/concepts/pipelines/content/artifacts/utils.ts create mode 100644 frontend/src/services/storageService.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 80d86d65cc..4731f4ce4d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -22,6 +22,7 @@ "http-errors": "^1.8.0", "js-yaml": "^4.0.0", "lodash": "^4.17.21", + "minio": "^7.1.3", "pino": "^8.11.0", "prom-client": "^14.0.1", "ts-node": "^10.9.1" @@ -2450,6 +2451,12 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "devOptional": true }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "optional": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2741,6 +2748,11 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2758,7 +2770,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -3026,6 +3037,27 @@ "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" }, + "node_modules/block-stream2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", + "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, + "node_modules/block-stream2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3047,6 +3079,11 @@ "node": ">=8" } }, + "node_modules/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==" + }, "node_modules/browserslist": { "version": "4.21.9", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", @@ -3123,6 +3160,14 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3180,7 +3225,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "optional": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -3535,6 +3579,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -5379,6 +5431,27 @@ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.2.0.tgz", "integrity": "sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==" }, + "node_modules/fast-xml-parser": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz", + "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastify": { "version": "4.22.0", "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.22.0.tgz", @@ -5570,6 +5643,14 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/find-my-way": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.6.0.tgz", @@ -5618,7 +5699,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "optional": true, "dependencies": { "is-callable": "^1.1.3" } @@ -5790,7 +5870,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "optional": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -5956,7 +6035,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "optional": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -6078,7 +6156,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -6090,7 +6167,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "optional": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -6431,6 +6507,21 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -6495,7 +6586,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -6571,6 +6661,20 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6698,7 +6802,6 @@ "version": "1.1.10", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "optional": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -8652,6 +8755,11 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "devOptional": true }, + "node_modules/json-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-stream/-/json-stream-1.0.0.tgz", + "integrity": "sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -8941,6 +9049,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minio": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minio/-/minio-7.1.3.tgz", + "integrity": "sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==", + "dependencies": { + "async": "^3.2.4", + "block-stream2": "^2.1.0", + "browser-or-node": "^2.1.1", + "buffer-crc32": "^0.2.13", + "fast-xml-parser": "^4.2.2", + "ipaddr.js": "^2.0.1", + "json-stream": "^1.0.0", + "lodash": "^4.17.21", + "mime-types": "^2.1.35", + "query-string": "^7.1.3", + "through2": "^4.0.2", + "web-encoding": "^1.1.5", + "xml": "^1.0.1", + "xml2js": "^0.5.0" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/minio/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "engines": { + "node": ">= 10" + } + }, "node_modules/minipass": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.5.tgz", @@ -9715,6 +9855,23 @@ "node": ">=0.6" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10131,6 +10288,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -10310,6 +10472,14 @@ "source-map": "^0.6.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "engines": { + "node": ">=6" + } + }, "node_modules/split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", @@ -10384,11 +10554,18 @@ "node": ">= 0.10.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -10515,6 +10692,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -10692,6 +10874,27 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "optional": true }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tiny-lru": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.0.1.tgz", @@ -11106,11 +11309,22 @@ "punycode": "^2.1.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { "version": "3.4.0", @@ -11182,6 +11396,17 @@ "makeerror": "1.0.12" } }, + "node_modules/web-encoding": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", + "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", + "dependencies": { + "util": "^0.12.3" + }, + "optionalDependencies": { + "@zxing/text-encoding": "0.9.0" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -11213,7 +11438,6 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "optional": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -11338,6 +11562,31 @@ } } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/package.json b/backend/package.json index 10fe6d13fe..5a0dc59e4e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -50,6 +50,7 @@ "http-errors": "^1.8.0", "js-yaml": "^4.0.0", "lodash": "^4.17.21", + "minio": "^7.1.3", "pino": "^8.11.0", "prom-client": "^14.0.1", "ts-node": "^10.9.1" diff --git a/backend/src/routes/api/storage/index.ts b/backend/src/routes/api/storage/index.ts new file mode 100644 index 0000000000..c63c4328cd --- /dev/null +++ b/backend/src/routes/api/storage/index.ts @@ -0,0 +1,47 @@ +import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; +import { createMinioClient, getObjectStream } from './storageUtils'; + +export default async (fastify: FastifyInstance): Promise => { + fastify.get('/:namespace/:bucket', async (request: FastifyRequest, reply: FastifyReply) => { + try { + const { namespace, bucket } = request.params as { + namespace: string; + bucket: string; + key: string; + }; + const query = request.query as { [key: string]: string }; + const key = query.key; + + const stream = await getObjectStream({ + bucket, + client: await createMinioClient(fastify, namespace), + key, + }); + + reply.type('text/plain'); + + await new Promise((resolve, reject) => { + stream.on('data', (chunk) => { + reply.raw.write(chunk); + }); + + stream.on('end', () => { + reply.raw.end(); + resolve(); + }); + + stream.on('error', (err) => { + fastify.log.error('Stream error:', err); + reply.raw.statusCode = 500; + reply.raw.end(err.message); + reject(err); + }); + }); + + return; + } catch (err) { + reply.code(500).send(err.message); + return reply; + } + }); +}; diff --git a/backend/src/routes/api/storage/storageUtils.ts b/backend/src/routes/api/storage/storageUtils.ts new file mode 100644 index 0000000000..55c474f7ad --- /dev/null +++ b/backend/src/routes/api/storage/storageUtils.ts @@ -0,0 +1,87 @@ +import { Client as MinioClient } from 'minio'; +import { DSPipelineKind, KubeFastifyInstance } from '../../../types'; +import { Transform, PassThrough } from 'stream'; + +/** + * Create minio client with aws instance profile credentials if needed. + * @param config minio client options where `accessKey` and `secretKey` are optional. + */ +export async function createMinioClient( + fastify: KubeFastifyInstance, + namespace: string, +): Promise { + try { + const dspaResponse = await fastify.kube.customObjectsApi + .listNamespacedCustomObject( + 'datasciencepipelinesapplications.opendatahub.io', + 'v1alpha1', + namespace, + 'datasciencepipelinesapplications', + ) + .catch((e) => { + throw `A ${ + e.statusCode + } error occurred when trying to fetch dspa aws storage credentials: ${ + e.response?.body?.message || e?.response?.statusMessage + }`; + }); + + const dspas = ( + dspaResponse?.body as { + items: DSPipelineKind[]; + } + )?.items; + + if (!dspas || !dspas.length) { + throw 'No Data Science Pipeline Application found'; + } + + // always get the first one + const externalStorage = dspas[0].spec.objectStorage.externalStorage; + + if (externalStorage) { + const { region, host: endPoint, s3CredentialsSecret } = externalStorage; + + // get secret + const secret = await fastify.kube.coreV1Api.readNamespacedSecret( + s3CredentialsSecret.secretName, + namespace, + ); + const accessKey = atob(secret.body.data[s3CredentialsSecret.accessKey]); + const secretKey = atob(secret.body.data[s3CredentialsSecret.secretKey]); + + if (!accessKey || !secretKey) { + throw 'Access key or secret key is empty'; + } + + // sessionToken + return new MinioClient({ accessKey, secretKey, endPoint, region }); + } + } catch (err) { + console.error('Unable to create minio client: ', err); + } +} + +/** MinioRequestConfig describes the info required to retrieve an artifact. */ +export interface MinioRequestConfig { + bucket: string; + key: string; + client: MinioClient; +} + +/** + * Returns a stream from an object in a s3 compatible object store (e.g. minio). + * + * @param param.bucket Bucket name to retrieve the object from. + * @param param.key Key of the object to retrieve. + * @param param.client Minio client. + * + */ +export async function getObjectStream({ + bucket, + key, + client, +}: MinioRequestConfig): Promise { + const stream = await client.getObject(bucket, key); + return stream.pipe(new PassThrough()); +} diff --git a/backend/src/types.ts b/backend/src/types.ts index 6f702af60a..9b8bc05619 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -1014,9 +1014,83 @@ export type K8sCondition = { lastHeartbeatTime?: string; }; +export type DSPipelineExternalStorageKind = { + bucket: string; + host: string; + port?: ''; + scheme: string; + region: string; + s3CredentialsSecret: { + accessKey: string; + secretKey: string; + secretName: string; + }; +}; + export type DSPipelineKind = K8sResourceCommon & { + metadata: { + name: string; + namespace: string; + }; spec: { dspVersion: string; + apiServer?: Partial<{ + apiServerImage: string; + artifactImage: string; + artifactScriptConfigMap: Partial<{ + key: string; + name: string; + }>; + enableSamplePipeline: boolean; + }>; + database?: Partial<{ + externalDB: Partial<{ + host: string; + passwordSecret: Partial<{ + key: string; + name: string; + }>; + pipelineDBName: string; + port: string; + username: string; + }>; + image: string; + mariaDB: Partial<{ + image: string; + passwordSecret: Partial<{ + key: string; + name: string; + }>; + pipelineDBName: string; + username: string; + }>; + }>; + mlpipelineUI?: { + configMap?: string; + image: string; + }; + persistentAgent?: Partial<{ + image: string; + pipelineAPIServerName: string; + }>; + scheduledWorkflow?: Partial<{ + image: string; + }>; + objectStorage: Partial<{ + externalStorage: DSPipelineExternalStorageKind; + minio: Partial<{ + bucket: string; + image: string; + s3CredentialsSecret: Partial<{ + accessKey: string; + secretKey: string; + secretName: string; + }>; + }>; + }>; + viewerCRD?: Partial<{ + image: string; + }>; }; status?: { conditions?: K8sCondition[]; diff --git a/frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx b/frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx new file mode 100644 index 0000000000..56239c53b1 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { extractS3UriComponents } from './utils'; + +interface ArtifactUriLinkProps { + uri: string; +} + +export const ArtifactUriLink: React.FC = ({ uri }) => { + const { namespace } = usePipelinesAPI(); + + const url = React.useMemo(() => { + // Check if the uri starts with http or https return it as is + if (uri.startsWith('http://') || uri.startsWith('https://')) { + return uri; + } + + // Otherwise check if the uri is s3 + // If it is not s3, return undefined as we only support fetching from s3 buckets + const uriComponents = extractS3UriComponents(uri); + if (!uriComponents) { + return; + } + + const { bucket, path } = uriComponents; + + // /api/storage/${namespace}/${bucket}?key=${path} + return `/api/storage/${namespace}/${bucket}?key=${encodeURIComponent(path)}`; + }, [namespace, uri]); + + if (!url) { + return uri; + } + + return ( + + {uri} + + ); +}; diff --git a/frontend/src/concepts/pipelines/content/artifacts/utils.ts b/frontend/src/concepts/pipelines/content/artifacts/utils.ts new file mode 100644 index 0000000000..a8033d607f --- /dev/null +++ b/frontend/src/concepts/pipelines/content/artifacts/utils.ts @@ -0,0 +1,13 @@ +export function extractS3UriComponents(uri: string): { bucket: string; path: string } | undefined { + const s3Prefix = 's3://'; + if (!uri.startsWith(s3Prefix)) { + return; + } + + const s3UrlWithoutPrefix = uri.slice(s3Prefix.length); + const firstSlashIndex = s3UrlWithoutPrefix.indexOf('/'); + const bucket = s3UrlWithoutPrefix.substring(0, firstSlashIndex); + const path = s3UrlWithoutPrefix.substring(firstSlashIndex + 1); + + return { bucket, path }; +} diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx index 322a379fc5..9a0069568d 100644 --- a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx @@ -22,6 +22,9 @@ import { import { CompareRunsEmptyState } from '~/concepts/pipelines/content/compareRuns/CompareRunsEmptyState'; import { PipelineRunArtifactSelect } from '~/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect'; import MarkdownView from '~/components/MarkdownView'; +import { fetchStorageObject } from '~/services/storageService'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { extractS3UriComponents } from '~/concepts/pipelines/content/artifacts/utils'; type MarkdownCompareProps = { runArtifacts?: RunArtifact[]; @@ -35,6 +38,7 @@ export type MarkdownAndTitle = { const MarkdownCompare: React.FC = ({ runArtifacts, isLoaded }) => { const [expandedGraph, setExpandedGraph] = React.useState(undefined); + const { namespace } = usePipelinesAPI(); const fullArtifactPaths: FullArtifactPath[] = React.useMemo(() => { if (!runArtifacts) { @@ -56,13 +60,25 @@ const MarkdownCompare: React.FC = ({ runArtifacts, isLoade })) .filter((markdown) => !!markdown.uri) .forEach(async ({ uri, title, run }) => { - const data = uri; // TODO: fetch data from uri: https://issues.redhat.com/browse/RHOAIENG-7206 + const uriComponents = extractS3UriComponents(uri); + if (!uriComponents) { + return; + } + const text = await fetchStorageObject( + namespace, + uriComponents.bucket, + uriComponents.path, + ).catch(() => null); + + if (text === null) { + return; + } runMapBuilder[run.run_id] = run; const config = { title, - config: data, + config: text, }; if (run.run_id in configMapBuilder) { @@ -73,7 +89,7 @@ const MarkdownCompare: React.FC = ({ runArtifacts, isLoade }); return { configMap: configMapBuilder, runMap: runMapBuilder }; - }, [fullArtifactPaths]); + }, [fullArtifactPaths, namespace]); if (!isLoaded) { return ( diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx index 7697eaa293..4cc242a507 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx @@ -12,6 +12,7 @@ import { } from '@patternfly/react-core'; import { Artifact } from '~/third_party/mlmd'; +import { ArtifactUriLink } from '~/concepts/pipelines/content/artifacts/ArtifactUriLink'; import { ArtifactPropertyDescriptionList } from './ArtifactPropertyDescriptionList'; interface ArtifactOverviewDetailsProps { @@ -32,7 +33,9 @@ export const ArtifactOverviewDetails: React.FC = ( {artifact?.uri && ( <> URI - {artifact.uri} + + + )} diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx index 1e3a65eed7..4285892466 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx @@ -13,6 +13,7 @@ import SimpleDropdownSelect from '~/components/SimpleDropdownSelect'; import { ArtifactType } from '~/concepts/pipelines/kfTypes'; import { useMlmdListContext, usePipelinesAPI } from '~/concepts/pipelines/context'; import { artifactsDetailsRoute } from '~/routes'; +import { ArtifactUriLink } from '~/concepts/pipelines/content/artifacts/ArtifactUriLink'; import { FilterOptions, columns, initialFilterData, options } from './constants'; import { getArtifactName } from './utils'; @@ -149,7 +150,9 @@ export const ArtifactsTable: React.FC = ({ {artifact.id} {artifact.type} - {artifact.uri} + + + diff --git a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx index c6ecf5535e..cbb37e7b0a 100644 --- a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx +++ b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx @@ -94,7 +94,6 @@ export const CompareRunMetricsSection: React.FunctionComponent = () => { {MetricSectionTabLabels.MARKDOWN}} - isDisabled // TODO enable when markdown can be fetched from storage (s3): https://issues.redhat.com/browse/RHOAIENG-7206 > diff --git a/frontend/src/services/storageService.ts b/frontend/src/services/storageService.ts new file mode 100644 index 0000000000..b8658d82a9 --- /dev/null +++ b/frontend/src/services/storageService.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; + +export const fetchStorageObject = ( + namespace: string, + bucket: string, + key: string, +): Promise => { + const url = `/api/storage/${namespace}/${bucket}`; + return axios + .get(url, { + params: { + key, + }, + }) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +};