UNPKG

@facets-cloud/facetsctlv3

Version:
843 lines (842 loc) 42.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const core_1 = require("@oclif/core"); const chalk_1 = __importDefault(require("chalk")); const inquirer_1 = __importDefault(require("inquirer")); const FacetsAPI = __importStar(require("../../services/facets-api")); const config_service_1 = require("../../services/config-service"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const os = __importStar(require("os")); // Add spinner helper class class StaticSpinner { spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; frameIndex = 0; intervalId = null; message = ''; active = false; lastLineLength = 0; start(initialMessage) { this.message = initialMessage; this.active = true; this.lastLineLength = initialMessage.length + 2; // +2 for spinner and space if (this.intervalId) { clearInterval(this.intervalId); } // Don't print a newline, just write the initial state process.stdout.write(`${this.spinnerFrames[this.frameIndex]} ${this.message}`); this.intervalId = setInterval(() => { if (!this.active) return; this.frameIndex = (this.frameIndex + 1) % this.spinnerFrames.length; // Clear the current line by moving cursor to beginning and writing spaces process.stdout.write('\r'); process.stdout.write(' '.repeat(this.lastLineLength)); process.stdout.write('\r'); // Write the new spinner state const output = `${this.spinnerFrames[this.frameIndex]} ${this.message}`; process.stdout.write(output); this.lastLineLength = output.length; }, 80); } update(message) { this.message = message; if (this.active) { // Clear the current line by moving cursor to beginning and writing spaces process.stdout.write('\r'); process.stdout.write(' '.repeat(this.lastLineLength)); process.stdout.write('\r'); // Write the new spinner state const output = `${this.spinnerFrames[this.frameIndex]} ${this.message}`; process.stdout.write(output); this.lastLineLength = output.length; } } stop(finalMessage) { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } this.active = false; // Clear the current line and write the final message with a newline process.stdout.write('\r'); process.stdout.write(' '.repeat(this.lastLineLength)); process.stdout.write('\r'); process.stdout.write(`${finalMessage || this.message}\n`); } } // Add concurrency limiter class class ConcurrencyLimiter { maxConcurrency; running = 0; queue = []; constructor(maxConcurrency) { this.maxConcurrency = maxConcurrency; } async run(fn) { // Wait until we can execute if (this.running >= this.maxConcurrency) { await new Promise(resolve => { this.queue.push({ resolve }); }); } this.running++; try { return await fn(); } finally { this.running--; // Process next item in queue if any if (this.queue.length > 0) { const next = this.queue.shift(); if (next) { next.resolve(); } } } } } class ClusterManage extends core_1.Command { static description = 'Interactive management of Kubernetes clusters with advanced functionality'; static examples = [ '<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --all', '<%= config.bin %> <%= command.id %> --search production', '<%= config.bin %> <%= command.id %> --all --refresh', '<%= config.bin %> <%= command.id %> --all --download', ]; static flags = { 'all': core_1.Flags.boolean({ char: 'a', description: 'Process all available clusters', default: false, }), 'search': core_1.Flags.string({ char: 's', description: 'Search for specific clusters by name, ID, or stack name', }), 'refresh': core_1.Flags.boolean({ char: 'r', description: 'Refresh kubernetes credentials for selected clusters', default: false, }), 'download': core_1.Flags.boolean({ char: 'd', description: 'Download kubeconfig files for selected clusters', default: false, }), 'verbose': core_1.Flags.boolean({ char: 'v', description: 'Show detailed output and metrics', default: false, }), }; async run() { const { flags } = await this.parse(ClusterManage); let config; try { const configFilePath = config_service_1.ConfigService.findConfigFile(); if (!configFilePath) { this.error('Configuration file not found. Please login first.', { exit: 2 }); } config = config_service_1.ConfigService.readConfig(configFilePath); } catch (error) { this.error('Error while fetching user credentials! Make sure you are logged in first.', { exit: 2 }); } // Create a spinner for showing progress const spinner = new StaticSpinner(); spinner.start('Fetching stacks from control plane'); let allClusters = []; try { // First, get all stacks const stacksResponse = await FacetsAPI.getAllStacks(config.ControlPlaneURL, config.Username, config.AccessToken); const stacks = stacksResponse.data || []; if (stacks.length === 0) { spinner.stop('No stacks found'); this.log(chalk_1.default.yellow('No stacks found in the control plane.')); return; } spinner.stop(`Found ${stacks.length} stacks`); this.log('Fetching clusters from all stacks in parallel...'); // Create a new spinner for cluster fetching const clusterSpinner = new StaticSpinner(); clusterSpinner.start(`Processing: 0/${stacks.length} stacks`); // Track currently processing stacks const processingStacks = new Set(); let completedStacks = 0; // Store results for summary const stackResults = []; const stackErrors = []; // Create concurrency limiter with max 5 concurrent requests const limiter = new ConcurrencyLimiter(5); // Use the limiter to control concurrency of fetch operations const fetchPromises = stacks.map((stack) => limiter.run(async () => { // Add to processing set and update spinner const stackName = stack.name || stack.id || ''; processingStacks.add(stackName); const processingList = Array.from(processingStacks).slice(0, 3).join(', '); const moreCount = processingStacks.size > 3 ? ` +${processingStacks.size - 3} more` : ''; clusterSpinner.update(`Processing: ${completedStacks}/${stacks.length} stacks | Current: ${processingList}${moreCount}`); try { const result = await this.fetchClustersForStack(config, stackName, stack); // Store result for summary if (result && result.length > 0) { stackResults.push({ stackName, clusterCount: result.length }); } return result; } catch (error) { // Store error for summary stackErrors.push({ stackName, error: error.message || 'Unknown error' }); return []; } finally { // Remove from processing set processingStacks.delete(stackName); // Update progress completedStacks++; // Update spinner with new progress const processingList = Array.from(processingStacks).slice(0, 3).join(', '); const moreCount = processingStacks.size > 3 ? ` +${processingStacks.size - 3} more` : ''; clusterSpinner.update(`Processing: ${completedStacks}/${stacks.length} stacks | Current: ${processingList}${moreCount}`); } })); const results = await Promise.allSettled(fetchPromises); // Stop the spinner clusterSpinner.stop(`Completed: ${stacks.length} stacks processed`); // Collect all clusters from successful fetches first results.forEach(result => { if (result.status === 'fulfilled' && result.value && result.value.length > 0) { allClusters.push(...result.value); } }); // Ensure stackResults is accurate by rebuilding it based on allClusters const stackCounts = new Map(); allClusters.forEach(cluster => { if (cluster.stackName) { stackCounts.set(cluster.stackName, (stackCounts.get(cluster.stackName) || 0) + 1); } }); // Rebuild stackResults from the accurate counts const updatedStackResults = Array.from(stackCounts.entries()).map(([stackName, count]) => ({ stackName, clusterCount: count })); // Calculate the total number of clusters found across all stacks const totalClusterCount = allClusters.length; const successfulStackCount = updatedStackResults.length; // Simple summary for non-verbose mode if (!flags.verbose) { this.log(chalk_1.default.green(`Found ${totalClusterCount} total clusters across ${successfulStackCount} stacks`)); } // Display stack results summary if (updatedStackResults.length > 0 && flags.verbose) { this.log('\n' + chalk_1.default.cyan('=== Stack Results ===')); updatedStackResults.forEach(({ stackName, clusterCount }) => { this.log(`Found ${chalk_1.default.cyan(clusterCount.toString())} clusters in stack ${chalk_1.default.cyan(stackName)}`); }); } // Display stack errors if any if (stackErrors.length > 0 && flags.verbose) { this.log('\n' + chalk_1.default.yellow('=== Stack Errors ===')); stackErrors.forEach(({ stackName, error }) => { this.log(chalk_1.default.yellow(`Error fetching clusters for stack ${stackName}: ${error}`)); }); } // Log status statistics const statusValues = new Map(); allClusters.forEach(cluster => { // Check if status exists and is not null/undefined let status = 'unknown'; if (cluster.status !== undefined && cluster.status !== null) { status = String(cluster.status).toLowerCase(); // If status is empty string, mark as unknown if (status.trim() === '') { status = 'unknown'; } } statusValues.set(status, (statusValues.get(status) || 0) + 1); }); if (flags.verbose) { // Format the status statistics with better labels const statusStats = Array.from(statusValues.entries()) .map(([status, count]) => { // Provide a more descriptive label for unknown status if (status === 'unknown') { return `not reported: ${count}`; } return `${status}: ${count}`; }) .join(', '); this.log(`Cluster status statistics: ${statusStats}`); } if (allClusters.length === 0) { this.log(chalk_1.default.yellow('No clusters found in any stack. Exiting.')); return; } // Apply search filter if provided let filteredClusters = allClusters; if (flags.search) { const searchTerm = flags.search.toLowerCase(); filteredClusters = allClusters.filter(cluster => (cluster.name && cluster.name.toLowerCase().includes(searchTerm)) || (cluster.id && cluster.id.toLowerCase().includes(searchTerm)) || (cluster.stackName && cluster.stackName.toLowerCase().includes(searchTerm))); this.log(chalk_1.default.cyan(`Search results: Found ${filteredClusters.length} clusters matching '${flags.search}' (out of ${allClusters.length} total clusters)`)); if (filteredClusters.length === 0) { this.log(chalk_1.default.yellow(`No clusters found matching the search term '${flags.search}'. Exiting.`)); return; } } else { // Only show this if not already shown in the non-verbose summary if (flags.verbose) { this.log(chalk_1.default.green(`Total clusters found: ${allClusters.length}`)); } } // Format clusters for display const clusterChoices = filteredClusters.map((cluster) => ({ name: `${cluster.name} (${cluster.id})${cluster.stackName ? ` [Blueprint: ${cluster.stackName}]` : ''}${cluster.status ? ` [Status: ${cluster.status}]` : ''}`, value: cluster.id, short: cluster.name, })); let selectedClusterIds = []; if (flags.all) { selectedClusterIds = filteredClusters.map((c) => c.id); this.log(chalk_1.default.cyan(`Processing all ${selectedClusterIds.length} clusters`)); } else { // Add search option to the prompt let searchInPrompt = false; let promptChoices = clusterChoices; if (!flags.search && filteredClusters.length > 10) { searchInPrompt = true; const { wantsToSearch } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'wantsToSearch', message: `There are ${filteredClusters.length} clusters. Would you like to search to narrow down the list?`, default: true, }, ]); if (wantsToSearch) { const { searchQuery } = await inquirer_1.default.prompt([ { type: 'input', name: 'searchQuery', message: 'Enter search term to filter clusters (name, ID, or blueprint):', }, ]); if (searchQuery) { const term = searchQuery.toLowerCase(); promptChoices = clusterChoices.filter(choice => choice.name.toLowerCase().includes(term) || choice.short.toLowerCase().includes(term)); this.log(chalk_1.default.cyan(`Filtered to ${promptChoices.length} clusters matching '${searchQuery}'`)); if (promptChoices.length === 0) { this.log(chalk_1.default.yellow('No clusters match your search. Please try again with a different term.')); return; } } } } // Prompt user to select clusters const response = await inquirer_1.default.prompt([ { type: 'checkbox', name: 'clusterIds', message: 'Select clusters to manage:', choices: promptChoices, pageSize: 20, validate: (answer) => { if (answer.length < 1) { return 'You must choose at least one cluster.'; } return true; }, }, ]); selectedClusterIds = response.clusterIds; } if (selectedClusterIds.length === 0) { this.log(chalk_1.default.yellow('No clusters selected. Exiting.')); return; } // Handle direct action flags if (flags.refresh) { await this.refreshCredentials(config, filteredClusters.filter((c) => selectedClusterIds.includes(c.id)), flags.verbose); return; } if (flags.download) { await this.downloadKubeconfigs(config, filteredClusters.filter((c) => selectedClusterIds.includes(c.id)), flags.verbose); return; } // Ask what action to perform if no direct action flags are set const { action } = await inquirer_1.default.prompt([ { type: 'list', name: 'action', message: 'What would you like to do with the selected clusters?', choices: [ { name: 'Download kubeconfig files', value: 'download' }, { name: 'Refresh kubernetes credentials', value: 'refresh' }, ], }, ]); const selectedClusters = filteredClusters.filter((c) => selectedClusterIds.includes(c.id)); if (action === 'download') { await this.downloadKubeconfigs(config, selectedClusters, flags.verbose); } else if (action === 'refresh') { await this.refreshCredentials(config, selectedClusters, flags.verbose); } } catch (error) { spinner.stop(chalk_1.default.red('Failed')); // Don't show error details for HTTP status code errors if (!error.response) { this.log(chalk_1.default.red(`API error details: ${JSON.stringify(error)}`)); } // Log error but don't exit with error code this.log(chalk_1.default.red(`Error encountered: ${error.message}`)); this.log(chalk_1.default.yellow('Command completed with some errors. Please check the output above for details.')); } } async fetchClustersForStack(config, stackName, stack) { try { // Add timeout for fetch operation const timeoutMs = 10000; // 10 seconds timeout (changed from 30s) const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Timeout fetching clusters for stack ${stackName}`)); }, timeoutMs); }); // Race between the actual fetch and its timeout const fetchPromise = FacetsAPI.getAllClustersByStackName(config.ControlPlaneURL, config.Username, config.AccessToken, stackName); const clustersResponse = await Promise.race([fetchPromise, timeoutPromise]); const stackClusters = clustersResponse.data || []; if (stackClusters.length > 0) { // Add stack info to each cluster stackClusters.forEach((cluster) => { cluster.stackName = stack.name || stack.id; // Ensure status field exists if (cluster.status === undefined || cluster.status === null) { cluster.status = 'active'; // Assume active as default status } }); // Don't log during processing as it interferes with the spinner // Store the count for summary later return stackClusters; } return []; } catch (error) { // Handle timeout specifically if (error.message && error.message.includes('Timeout fetching clusters')) { // Don't log during processing as it interferes with the spinner // Just return empty array } else if (!error.response) { // Don't log errors for HTTP status code issues // Don't log during processing as it interferes with the spinner } return []; } } async downloadKubeconfigs(config, clusters, verbose = false) { this.log(chalk_1.default.green(`Downloading kubeconfig for ${clusters.length} cluster(s)...`)); const downloadFolderPath = this.getDownloadFolderPath(); // Use concurrency limiter instead of manual batching const limiter = new ConcurrencyLimiter(10); // Process 10 cluster at a time let completedClusters = 0; const timedOutClusters = []; const errorMessages = []; // Collect error messages const downloadedFiles = []; // Track downloaded files // Create a spinner for showing progress const spinner = new StaticSpinner(); spinner.start(`Processing: 0/${clusters.length} complete`); // Track currently processing clusters const processingClusters = new Set(); const downloadPromises = clusters.map(cluster => limiter.run(async () => { // Define timeout value outside try block so it's available in catch const timeoutMs = 10000; // 10 seconds timeout per download // Add to processing set and update spinner processingClusters.add(cluster.name); const processingList = Array.from(processingClusters).slice(0, 5).join(', '); const moreCount = processingClusters.size > 5 ? ` +${processingClusters.size - 5} more` : ''; spinner.update(`Processing: ${completedClusters}/${clusters.length} complete | Current: ${processingList}${moreCount}`); try { // Each individual download gets its own timeout const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error('timeout')); }, timeoutMs); }); // Race between the actual download and its timeout const filePath = await Promise.race([ this.downloadSingleKubeconfig(config, cluster, downloadFolderPath), timeoutPromise ]); // Track the downloaded file downloadedFiles.push({ cluster: cluster.name, path: filePath }); return { success: true, cluster }; } catch (error) { if (error.message === 'timeout') { // Just collect the cluster name, don't log immediately timedOutClusters.push(cluster.name); // Store error message for later errorMessages.push(`${chalk_1.default.red('✗')} Error downloading kubeconfig for cluster ${chalk_1.default.red(cluster.name)}: timeout of ${timeoutMs}ms exceeded`); return { success: false, timedOut: true, cluster }; } else if (error.skipNotify) { return { success: false, skipped: true, cluster }; } else { // Store error message for later errorMessages.push(`${chalk_1.default.red('✗')} Error downloading kubeconfig for cluster ${chalk_1.default.red(cluster.name)}: ${error.message}`); return { success: false, error, cluster }; } } finally { // Remove from processing set processingClusters.delete(cluster.name); // Update progress completedClusters++; // Update spinner with new progress const processingList = Array.from(processingClusters).slice(0, 5).join(', '); const moreCount = processingClusters.size > 5 ? ` +${processingClusters.size - 5} more` : ''; spinner.update(`Processing: ${completedClusters}/${clusters.length} complete | Current: ${processingList}${moreCount}`); } })); try { // Wait for all downloads to complete const results = await Promise.all(downloadPromises); // Stop the spinner with final message spinner.stop(`Completed: ${completedClusters}/${clusters.length} clusters processed`); // Report results const successCount = results.filter(r => r.success).length; const skippedCount = results.filter(r => r.skipped).length; const timedOutCount = timedOutClusters.length; const failedCount = clusters.length - successCount - skippedCount - timedOutCount; // Show minimal metrics if not in verbose mode if (!verbose) { this.log(chalk_1.default.green(`Successfully downloaded ${successCount} of ${clusters.length} kubeconfig files`)); if (downloadedFiles.length > 0) { // Show only the count of downloaded files in non-verbose mode this.log(chalk_1.default.green(`Downloaded ${downloadedFiles.length} file(s) to ${downloadFolderPath}`)); } if (failedCount > 0 || timedOutCount > 0) { this.log(chalk_1.default.yellow(`Some clusters failed or timed out. Use -v for details.`)); } } else { // Show detailed metrics in verbose mode this.log('\n' + chalk_1.default.cyan('=== Final Results ===')); this.log(chalk_1.default.green(`Successfully downloaded ${successCount} of ${clusters.length} kubeconfig files`)); if (skippedCount > 0) { this.log(chalk_1.default.yellow(`${skippedCount} cluster(s) do not have available kubeconfig files`)); } if (failedCount > 0) { this.log(chalk_1.default.red(`Failed to download ${failedCount} kubeconfig file(s) due to errors`)); } if (timedOutClusters.length > 0) { this.log(chalk_1.default.yellow(`${timedOutClusters.length} cluster(s) timed out during download:`)); timedOutClusters.forEach(clusterName => { this.log(chalk_1.default.yellow(` - ${clusterName}`)); }); } // Print downloaded file paths if (downloadedFiles.length > 0) { this.log('\n' + chalk_1.default.green('=== Downloaded Files ===')); downloadedFiles.forEach(({ cluster, path }) => { this.log(`${chalk_1.default.cyan(cluster)}: ${path}`); }); } // Print all error messages at the end if (errorMessages.length > 0) { this.log('\n' + chalk_1.default.red('=== Error Details ===')); errorMessages.forEach(msg => this.log(msg)); } } } catch (error) { // Stop the spinner spinner.stop(`Error occurred after processing ${completedClusters}/${clusters.length} clusters`); // Log error but don't fail the command this.log(chalk_1.default.red(`An error occurred during kubeconfig download: ${error.message}`)); // Show detailed error information only in verbose mode if (verbose) { if (timedOutClusters.length > 0) { this.log(chalk_1.default.yellow(`${timedOutClusters.length} cluster(s) timed out during download:`)); timedOutClusters.forEach(clusterName => { this.log(chalk_1.default.yellow(` - ${clusterName}`)); }); } // Print downloaded file paths if (downloadedFiles.length > 0) { this.log('\n' + chalk_1.default.green('=== Downloaded Files ===')); downloadedFiles.forEach(({ cluster, path }) => { this.log(`${chalk_1.default.cyan(cluster)}: ${path}`); }); } // Print all error messages at the end if (errorMessages.length > 0) { this.log('\n' + chalk_1.default.red('=== Error Details ===')); errorMessages.forEach(msg => this.log(msg)); } } else { // Show minimal error information if (timedOutClusters.length > 0) { this.log(chalk_1.default.yellow(`${timedOutClusters.length} cluster(s) timed out. Use -v for details.`)); } if (downloadedFiles.length > 0) { // Show only the count of downloaded files in non-verbose mode this.log(chalk_1.default.green(`Downloaded ${downloadedFiles.length} file(s) to ${downloadFolderPath}`)); } } } finally { // Force Node.js to clean up any remaining handles if (global.gc) { global.gc(); } } } async downloadSingleKubeconfig(config, cluster, downloadFolderPath) { // Simplified method - no logging during execution to avoid cluttering output try { const kubeConfigFileContent = await FacetsAPI.getKubeConfigFileContent(config.ControlPlaneURL, config.Username, config.AccessToken, cluster.id); // Include stack name in the file name if available const stackName = cluster.stackName || 'unknown-stack'; // Sanitize stackName and cluster.name to remove/replace invalid characters const sanitizeFileName = (name) => { return name .replace(/[\/\\:*?"<>|]/g, '-') // Replace reserved characters with hyphens .replace(/\s+/g, '-') // Replace spaces with hyphens .trim(); // Remove leading/trailing whitespace }; const sanitizedStackName = sanitizeFileName(stackName); const sanitizedClusterName = sanitizeFileName(cluster.name); const tempFileName = `${sanitizedStackName}-${sanitizedClusterName}-kubeconfig`; const destinationPath = this.getUniqueFilePath(downloadFolderPath, tempFileName); // Check if file already exists (for reporting purposes only) const fileExists = fs.existsSync(destinationPath); // Use async file write instead of sync await fs.promises.writeFile(destinationPath, kubeConfigFileContent.data); // Return the destination path so it can be used in the summary // Include a note if the file was overwritten return fileExists ? `${destinationPath} (overwritten)` : destinationPath; } catch (error) { // Handle any HTTP status code error as "not available" if (error.response) { // Custom error object to indicate this was an expected error const skipError = new Error('Kubeconfig not available'); skipError.skipNotify = true; throw skipError; } throw error; } } async refreshCredentials(config, clusters, verbose = false) { this.log(chalk_1.default.cyan(`Refreshing credentials for ${clusters.length} cluster(s)...`)); // Use concurrency limiter const limiter = new ConcurrencyLimiter(10); // Process 10 clusters at a time let completedClusters = 0; const timedOutClusters = []; const errorMessages = []; // Collect error messages // Create a spinner for showing progress const spinner = new StaticSpinner(); spinner.start(`Processing: 0/${clusters.length} complete`); // Track currently processing clusters const processingClusters = new Set(); // Create promises for each cluster with concurrency control const refreshPromises = clusters.map(cluster => limiter.run(async () => { // Define timeout value outside try block so it's available in catch const clusterTimeoutMs = 10000; // 10 seconds timeout per cluster // Add to processing set and update spinner processingClusters.add(cluster.name); const processingList = Array.from(processingClusters).slice(0, 5).join(', '); const moreCount = processingClusters.size > 5 ? ` +${processingClusters.size - 5} more` : ''; spinner.update(`Processing: ${completedClusters}/${clusters.length} complete | Current: ${processingList}${moreCount}`); try { // Add timeout for individual cluster refresh const clusterTimeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error('timeout')); }, clusterTimeoutMs); }); await Promise.race([ this.refreshSingleClusterCredentials(config, cluster), clusterTimeoutPromise ]); return { success: true, cluster }; } catch (error) { if (error.message === 'timeout') { // Just collect the cluster name, don't log immediately timedOutClusters.push(cluster.name); // Store error message for later errorMessages.push(`${chalk_1.default.red('✗')} Error refreshing credentials for cluster ${chalk_1.default.red(cluster.name)}: timeout of ${clusterTimeoutMs}ms exceeded`); return { success: false, timedOut: true, cluster }; } else if (error.skipNotify) { return { success: false, skipped: true, cluster }; } else { // Store error message for later errorMessages.push(`${chalk_1.default.red('✗')} Error refreshing credentials for cluster ${chalk_1.default.red(cluster.name)}: ${error.message}`); return { success: false, error, cluster }; } } finally { // Remove from processing set processingClusters.delete(cluster.name); // Update progress completedClusters++; // Update spinner with new progress const processingList = Array.from(processingClusters).slice(0, 5).join(', '); const moreCount = processingClusters.size > 5 ? ` +${processingClusters.size - 5} more` : ''; spinner.update(`Processing: ${completedClusters}/${clusters.length} complete | Current: ${processingList}${moreCount}`); } })); try { // Wait for all refresh operations to complete const results = await Promise.all(refreshPromises); // Stop the spinner with final message spinner.stop(`Completed: ${completedClusters}/${clusters.length} clusters processed`); // Report results const successCount = results.filter(r => r.success).length; const skippedCount = results.filter(r => r.skipped).length; const timedOutCount = timedOutClusters.length; const failedCount = clusters.length - successCount - skippedCount - timedOutCount; // Show minimal metrics if not in verbose mode if (!verbose) { this.log(chalk_1.default.green(`Successfully refreshed credentials for ${successCount} of ${clusters.length} clusters`)); if (failedCount > 0 || timedOutCount > 0) { this.log(chalk_1.default.yellow(`Some clusters failed or timed out. Use -v for details.`)); } } else { // Show detailed metrics in verbose mode this.log('\n' + chalk_1.default.cyan('=== Final Results ===')); this.log(chalk_1.default.green(`Successfully refreshed credentials for ${successCount} of ${clusters.length} clusters`)); if (skippedCount > 0) { this.log(chalk_1.default.yellow(`${skippedCount} cluster(s) do not have credentials available to refresh`)); } if (failedCount > 0) { this.log(chalk_1.default.red(`Failed to refresh ${failedCount} cluster(s) due to errors`)); } if (timedOutClusters.length > 0) { this.log(chalk_1.default.yellow(`${timedOutClusters.length} cluster(s) timed out during refresh:`)); timedOutClusters.forEach(clusterName => { this.log(chalk_1.default.yellow(` - ${clusterName}`)); }); } // Print all error messages at the end if (errorMessages.length > 0) { this.log('\n' + chalk_1.default.red('=== Error Details ===')); errorMessages.forEach(msg => this.log(msg)); } } } catch (error) { // Stop the spinner spinner.stop(`Error occurred after processing ${completedClusters}/${clusters.length} clusters`); // Log error but don't fail the command this.log(chalk_1.default.red(`An error occurred during credential refresh: ${error.message}`)); // Show detailed error information only in verbose mode if (verbose) { if (timedOutClusters.length > 0) { this.log(chalk_1.default.yellow(`${timedOutClusters.length} cluster(s) timed out during refresh:`)); timedOutClusters.forEach(clusterName => { this.log(chalk_1.default.yellow(` - ${clusterName}`)); }); } // Print all error messages at the end if (errorMessages.length > 0) { this.log('\n' + chalk_1.default.red('=== Error Details ===')); errorMessages.forEach(msg => this.log(msg)); } } else { // Show minimal error information if (timedOutClusters.length > 0) { this.log(chalk_1.default.yellow(`${timedOutClusters.length} cluster(s) timed out. Use -v for details.`)); } } } } async refreshSingleClusterCredentials(config, cluster) { // Simplified method - no logging during execution to avoid cluttering output try { const result = await FacetsAPI.refreshCredentials(config.ControlPlaneURL, config.Username, config.AccessToken, cluster.id); // Check if we got a successful response if (!(result && result.status >= 200 && result.status < 300)) { // Unexpected response const skipError = new Error('Credentials not available'); skipError.skipNotify = true; throw skipError; } } catch (error) { // Handle any HTTP status code error as "not available" if (error.response) { // Custom error object to indicate this was an expected error const skipError = new Error('Credentials not available'); skipError.skipNotify = true; throw skipError; } throw error; } } getDownloadFolderPath() { const homeDirectory = os.homedir(); const downloadDirectory = path.join(homeDirectory, '.facetsctl'); if (!fs.existsSync(downloadDirectory)) { fs.mkdirSync(downloadDirectory, { recursive: true }); } return downloadDirectory; } getUniqueFilePath(directory, filename) { // Simply join the directory and filename without checking for existence // This will overwrite any existing file with the same name return path.join(directory, filename); } } exports.default = ClusterManage;