@facets-cloud/facetsctlv3
Version:
843 lines (842 loc) • 42.8 kB
JavaScript
;
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;