From aa4976463285f2053c45850d9be6be2dfbab0114 Mon Sep 17 00:00:00 2001 From: Sidwebworks Date: Sun, 22 Jun 2025 04:29:31 +0530 Subject: [PATCH] fix: yaml secret file parsing --- .../src/components/utilities/parseSecrets.ts | 88 +++++++++++++++++++ .../SecretDropzone/SecretDropzone.tsx | 31 +++++-- 2 files changed, 111 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/utilities/parseSecrets.ts b/frontend/src/components/utilities/parseSecrets.ts index d1df7cd6870..3e5a57edf0c 100644 --- a/frontend/src/components/utilities/parseSecrets.ts +++ b/frontend/src/components/utilities/parseSecrets.ts @@ -77,3 +77,91 @@ export const parseJson = (src: ArrayBuffer | string) => { }); return env; }; + +/** + * Parses simple flat YAML with support for multiline strings using |, |-, and >. + * @param {ArrayBuffer | string} src + * @returns {Record} + */ +export function parseYaml(src: ArrayBuffer | string) { + const result: Record = {}; + + const content = src.toString().replace(/\r\n?/g, "\n"); + const lines = content.split("\n"); + + let i = 0; + let comments: string[] = []; + + while (i < lines.length) { + const line = lines[i].trim(); + + // Collect comment + if (line.startsWith("#")) { + comments.push(line.slice(1).trim()); + i += 1; // move to next line + } else { + // Match key: value or key: |, key: >, etc. + const keyMatch = lines[i].match(/^([\w.-]+)\s*:\s*(.*)$/); + if (keyMatch) { + const [, key, rawValue] = keyMatch; + let value = rawValue.trim(); + + // Multiline string handling + if (value === "|-" || value === "|" || value === ">") { + const isFolded = value === ">"; + + const baseIndent = lines[i + 1]?.match(/^(\s*)/)?.[1]?.length ?? 0; + const collectedLines: string[] = []; + + i += 1; // move to first content line + + while (i < lines.length) { + const current = lines[i]; + const currentIndent = current.match(/^(\s*)/)?.[1]?.length ?? 0; + + if (current.trim() === "" || currentIndent >= baseIndent) { + collectedLines.push(current.slice(baseIndent)); + i += 1; // move to next line + } else { + break; + } + } + + if (isFolded) { + // Join lines with space for `>` folded style + value = collectedLines.map((l) => l.trim()).join(" "); + } else { + // Keep lines with newlines for `|` and `|-` + value = collectedLines.join("\n"); + } + } else { + // Inline value — strip quotes and inline comment + const commentIndex = value.indexOf(" #"); + if (commentIndex !== -1) { + value = value.slice(0, commentIndex).trim(); + } + + // Remove surrounding quotes + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + + i += 1; // advance to next line + } + + result[key] = { + value, + comments: [...comments] + }; + comments = []; // reset + } else { + i += 1; // skip unknown line + } + } + } + + return result; +} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx index de40439758c..dca0f07a3e8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretDropzone/SecretDropzone.tsx @@ -9,7 +9,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; // TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality -import { parseDotEnv, parseJson } from "@app/components/utilities/parseSecrets"; +import { parseDotEnv, parseJson, parseYaml } from "@app/components/utilities/parseSecrets"; import { Button, Modal, ModalContent } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; @@ -127,7 +127,7 @@ export const SecretDropzone = ({ } }; - const parseFile = (file?: File, isJson?: boolean) => { + const parseFile = (file?: File) => { const reader = new FileReader(); if (!file) { createNotification({ @@ -140,10 +140,25 @@ export const SecretDropzone = ({ setIsLoading.on(); reader.onload = (event) => { if (!event?.target?.result) return; - // parse function's argument looks like to be ArrayBuffer - const env = isJson - ? parseJson(event.target.result as ArrayBuffer) - : parseDotEnv(event.target.result as ArrayBuffer); + + let env: TParsedEnv; + + const src = event.target.result as ArrayBuffer; + + switch (file.type) { + case "application/json": + env = parseJson(src); + break; + case "text/yaml": + case "application/x-yaml": + case "application/yaml": + env = parseYaml(src); + break; + + default: + env = parseDotEnv(src); + break; + } setIsLoading.off(); handleParsedEnv(env); }; @@ -165,12 +180,12 @@ export const SecretDropzone = ({ e.dataTransfer.dropEffect = "copy"; setDragActive.off(); - parseFile(e.dataTransfer.files[0], e.dataTransfer.files[0].type === "application/json"); + parseFile(e.dataTransfer.files[0]); }; const handleFileUpload = (e: ChangeEvent) => { e.preventDefault(); - parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json"); + parseFile(e.target?.files?.[0]); }; const handleSaveSecrets = async () => {