misc: add limiter and updated label

This commit is contained in:
Sheen Capadngan
2025-10-16 03:49:20 +08:00
parent 4e5134d2dc
commit efb8616c63
2 changed files with 152 additions and 97 deletions
@@ -25,6 +25,44 @@ import {
THCVaultMountResponse THCVaultMountResponse
} from "./hc-vault-connection-types"; } from "./hc-vault-connection-types";
// Concurrency limit for HC Vault API requests to avoid rate limiting
const HC_VAULT_CONCURRENCY_LIMIT = 20;
/**
* Creates a concurrency limiter that restricts the number of concurrent async operations
* @param limit - Maximum number of concurrent operations
* @returns A function that takes an async function and executes it with concurrency control
*/
const createConcurrencyLimiter = (limit: number) => {
let activeCount = 0;
const queue: Array<() => void> = [];
const next = () => {
activeCount -= 1;
if (queue.length > 0) {
const resolve = queue.shift();
resolve?.();
}
};
return async <T>(fn: () => Promise<T>): Promise<T> => {
// If we're at the limit, wait in queue
if (activeCount >= limit) {
await new Promise<void>((resolve) => {
queue.push(resolve);
});
}
activeCount += 1;
try {
return await fn();
} finally {
next();
}
};
};
export const getHCVaultInstanceUrl = async (config: THCVaultConnectionConfig) => { export const getHCVaultInstanceUrl = async (config: THCVaultConnectionConfig) => {
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
@@ -215,8 +253,11 @@ export const listHCVaultPolicies = async (
const policyNames = listData.data.policies || []; const policyNames = listData.data.policies || [];
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
const policies = await Promise.all( const policies = await Promise.all(
policyNames.map(async (policyName) => { policyNames.map((policyName) =>
limiter(async () => {
try { try {
const { data: policyData } = await requestWithHCVaultGateway<{ const { data: policyData } = await requestWithHCVaultGateway<{
data: { data: {
@@ -244,6 +285,7 @@ export const listHCVaultPolicies = async (
}; };
} }
}) })
)
); );
return policies; return policies;
@@ -304,19 +346,21 @@ export const listHCVaultNamespaces = async (
} }
}; };
// Recursive function to get all namespaces at all depths // Recursive function to get all namespaces at all depths with controlled parallelization
const recursivelyGetAllNamespaces = async (parentPath: string): Promise<string[]> => { const recursivelyGetAllNamespaces = async (
parentPath: string,
limiter: ReturnType<typeof createConcurrencyLimiter>
): Promise<string[]> => {
const childKeys = await fetchNamespacesAtPath(parentPath); const childKeys = await fetchNamespacesAtPath(parentPath);
if (childKeys === null || childKeys.length === 0) { if (childKeys === null || childKeys.length === 0) {
return []; return [];
} }
const allNamespaces: string[] = []; // Process namespaces in parallel with concurrency control
const namespacesArrays = await Promise.all(
// Process namespaces sequentially to maintain order childKeys.map((namespaceKey) =>
// eslint-disable-next-line no-restricted-syntax limiter(async () => {
for (const namespaceKey of childKeys) {
// Remove trailing slash from the key // Remove trailing slash from the key
const cleanNamespaceKey = namespaceKey.replace(/\/$/, ""); const cleanNamespaceKey = namespaceKey.replace(/\/$/, "");
@@ -328,21 +372,25 @@ export const listHCVaultNamespaces = async (
fullNamespacePath = `${parentPath}/${cleanNamespaceKey}`; fullNamespacePath = `${parentPath}/${cleanNamespaceKey}`;
} }
// Add this namespace to our results
allNamespaces.push(fullNamespacePath);
// Recursively fetch child namespaces // Recursively fetch child namespaces
// eslint-disable-next-line no-await-in-loop const childNamespaces = await recursivelyGetAllNamespaces(fullNamespacePath, limiter);
const childNamespaces = await recursivelyGetAllNamespaces(fullNamespacePath);
allNamespaces.push(...childNamespaces);
}
return allNamespaces; // Return this namespace and all its children
return [fullNamespacePath, ...childNamespaces];
})
)
);
// Flatten the arrays into a single array
return namespacesArrays.flat();
}; };
try { try {
// Create concurrency limiter to avoid overwhelming the Vault instance
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
// Get all namespaces starting from currentNamespace // Get all namespaces starting from currentNamespace
const childNamespaces = await recursivelyGetAllNamespaces(currentNamespace); const childNamespaces = await recursivelyGetAllNamespaces(currentNamespace, limiter);
// Build the result array with full paths // Build the result array with full paths
const namespaces = childNamespaces.map((path) => ({ const namespaces = childNamespaces.map((path) => ({
@@ -446,10 +494,11 @@ export const listHCVaultSecretPaths = async (
} }
}; };
// Recursive function to get all secret paths in a mount // Recursive function to get all secret paths in a mount with controlled parallelization
const recursivelyGetAllPaths = async ( const recursivelyGetAllPaths = async (
mountPath: string, mountPath: string,
kvVersion: "1" | "2", kvVersion: "1" | "2",
limiter: ReturnType<typeof createConcurrencyLimiter>,
currentPath: string = "" currentPath: string = ""
): Promise<string[]> => { ): Promise<string[]> => {
const paths = await getPaths(mountPath, currentPath, kvVersion); const paths = await getPaths(mountPath, currentPath, kvVersion);
@@ -458,26 +507,25 @@ export const listHCVaultSecretPaths = async (
return []; return [];
} }
const allSecrets: string[] = []; // Process paths in parallel with concurrency control
const secretPathsArrays = await Promise.all(
// Process paths sequentially to maintain tree traversal order paths.map((path) =>
// eslint-disable-next-line no-restricted-syntax limiter(async () => {
for (const path of paths) {
const cleanPath = path.endsWith("/") ? path.slice(0, -1) : path; const cleanPath = path.endsWith("/") ? path.slice(0, -1) : path;
const fullItemPath = currentPath ? `${currentPath}/${cleanPath}` : cleanPath; const fullItemPath = currentPath ? `${currentPath}/${cleanPath}` : cleanPath;
if (path.endsWith("/")) { if (path.endsWith("/")) {
// it's a folder so we recurse into it // it's a folder so we recurse into it
// eslint-disable-next-line no-await-in-loop return recursivelyGetAllPaths(mountPath, kvVersion, limiter, fullItemPath);
const subSecrets = await recursivelyGetAllPaths(mountPath, kvVersion, fullItemPath);
allSecrets.push(...subSecrets);
} else {
// it's a secret so we add it to our results
allSecrets.push(`${mountPath}/${fullItemPath}`);
}
} }
// it's a secret so we return it
return [`${mountPath}/${fullItemPath}`];
})
)
);
return allSecrets; // Flatten the arrays into a single array
return secretPathsArrays.flat();
}; };
// Get all mounts // Get all mounts
@@ -486,12 +534,15 @@ export const listHCVaultSecretPaths = async (
// Filter for KV mounts (kv, kv-v1, kv-v2) // Filter for KV mounts (kv, kv-v1, kv-v2)
const kvMounts = mounts.filter((mount) => mount.type === "kv" || mount.type.startsWith("kv")); const kvMounts = mounts.filter((mount) => mount.type === "kv" || mount.type.startsWith("kv"));
// Create concurrency limiter to avoid overwhelming the Vault instance
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
// Collect all secret paths from all KV mounts in parallel // Collect all secret paths from all KV mounts in parallel
const allSecretPathsArrays = await Promise.all( const allSecretPathsArrays = await Promise.all(
kvMounts.map(async (mount) => { kvMounts.map(async (mount) => {
const kvVersion = mount.version === "2" ? "2" : "1"; const kvVersion = mount.version === "2" ? "2" : "1";
const cleanMountPath = mount.path.replace(/\/$/, ""); // Remove trailing slash const cleanMountPath = mount.path.replace(/\/$/, ""); // Remove trailing slash
return recursivelyGetAllPaths(cleanMountPath, kvVersion); return recursivelyGetAllPaths(cleanMountPath, kvVersion, limiter);
}) })
); );
@@ -692,8 +743,11 @@ export const getHCVaultKubernetesAuthRoles = async (
return []; return [];
} }
// 3. Fetch details for each role // 3. Fetch details for each role with concurrency control
const roleDetailsPromises = roleNames.map(async (roleName) => { const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
const roleDetailsPromises = roleNames.map((roleName) =>
limiter(async () => {
const { data: roleResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesAuthRole }>( const { data: roleResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesAuthRole }>(
connection, connection,
gatewayService, gatewayService,
@@ -714,7 +768,8 @@ export const getHCVaultKubernetesAuthRoles = async (
config: kubernetesConfig, config: kubernetesConfig,
mountPath: cleanMountPath mountPath: cleanMountPath
} as THCVaultKubernetesAuthRoleWithConfig; } as THCVaultKubernetesAuthRoleWithConfig;
}); })
);
const roles = await Promise.all(roleDetailsPromises); const roles = await Promise.all(roleDetailsPromises);
@@ -282,8 +282,8 @@ export const IdentityKubernetesAuthForm = ({
} }
createNotification({ createNotification({
type: "success", type: "info",
text: `Successfully imported Kubernetes auth configuration from Vault role: ${role.name}` text: `Successfully prefilled values from Kubernetes auth role: ${role.name}`
}); });
} catch (err) { } catch (err) {
console.error("Import error:", err); console.error("Import error:", err);