UNPKG

portainer-stack-recreate

Version:

A Node.js CLI tool to recreate Portainer Docker stacks with latest images

201 lines (159 loc) 7.01 kB
#!/usr/bin/env node const { Command } = require('commander'); const axios = require('axios'); const chalk = require('chalk'); const program = new Command(); program .name('portainer-stack-recreate') .description('Recreate Portainer Docker stacks with latest images') .version('1.0.1') .requiredOption('-u, --url <url>', 'Portainer instance URL') .requiredOption('-k, --api-key <key>', 'Portainer API key') .requiredOption('-s, --stack-name <name>', 'Name of the stack to update') .option('--insecure', 'Allow insecure HTTPS connections (skip SSL verification)', false) .parse(); const options = program.opts(); // Configure axios defaults const axiosConfig = { headers: { 'X-API-Key': options.apiKey, 'Content-Type': 'application/json' } }; if (options.insecure) { axiosConfig.httpsAgent = new (require('https').Agent)({ rejectUnauthorized: false }); } const api = axios.create(axiosConfig); async function fetchStackList() { console.log(chalk.blue('🔍 Fetching stack list from'), chalk.cyan(options.url), '...'); try { const response = await api.get(`${options.url}/api/stacks`); return response.data; } catch (error) { console.error(chalk.red('❌ Failed to fetch stack list:'), error.message); process.exit(1); } } async function getStackData(stackId) { console.log(chalk.blue('📋 Getting stack data for'), chalk.cyan(options.stackName), `(ID: ${stackId})...`); try { const response = await api.get(`${options.url}/api/stacks/${stackId}`); return response.data; } catch (error) { console.error(chalk.red('❌ Failed to get stack data for stack ID:'), stackId, error.message); process.exit(1); } } async function getStackFile(stackId) { console.log(chalk.blue('📄 Getting stack file for'), chalk.cyan(options.stackName), `(ID: ${stackId})...`); try { const response = await api.get(`${options.url}/api/stacks/${stackId}/file`); return response.data; } catch (error) { console.error(chalk.red('❌ Failed to get stack file for stack ID:'), stackId, error.message); process.exit(1); } } async function updateStack(stackId, endpointId, stackFileContent, stackEnv) { console.log(chalk.blue('🔄 Updating stack'), chalk.cyan(options.stackName), `(ID: ${stackId}) on endpoint ${endpointId}...`); try { const response = await api.put(`${options.url}/api/stacks/${stackId}?endpointId=${endpointId}`, { StackFileContent: stackFileContent, Env: stackEnv, Prune: false, PullImage: false }); console.log(chalk.green('✅ Stack'), chalk.cyan(options.stackName), `(ID: ${stackId}) updated successfully`); return response.data; } catch (error) { console.error(chalk.red('❌ Stack update failed for stack ID:'), stackId); console.error(chalk.red('Response:'), error.response?.data || error.message); process.exit(1); } } async function getAllContainers(endpointId) { console.log(chalk.blue('🔍 Getting all containers from endpoint'), chalk.cyan(endpointId), '...'); try { const response = await api.get(`${options.url}/api/endpoints/${endpointId}/docker/containers/json`); console.log(chalk.blue('📦 Total containers found:'), chalk.cyan(response.data.length)); return response.data; } catch (error) { console.error(chalk.red('❌ Failed to get containers from endpoint'), endpointId, error.message); process.exit(1); } } function filterStackContainers(containers, stackName) { console.log(chalk.blue('🔍 Filtering containers for stack'), chalk.cyan(stackName), '...'); let stackContainers = containers.filter(container => container.Labels && container.Labels['com.docker.compose.project'] === stackName ); if (stackContainers.length === 0) { console.log(chalk.yellow('⚠️ No containers found for stack'), chalk.cyan(stackName), 'trying alternative approach...'); stackContainers = containers.filter(container => container.Names && container.Names[0] && container.Names[0].includes(stackName) ); if (stackContainers.length === 0) { console.error(chalk.red('❌ No containers found for stack'), chalk.cyan(stackName)); console.log(chalk.blue('Available containers:')); containers.forEach(container => { const labels = container.Labels ? JSON.stringify(container.Labels) : '{}'; console.log(chalk.cyan(container.Names[0]), `(Labels: ${labels})`); }); process.exit(1); } } console.log(chalk.blue('📦 Found'), chalk.cyan(stackContainers.length), 'containers in stack', chalk.cyan(stackName)); return stackContainers; } async function recreateContainer(container, endpointId) { const containerId = container.Id; const containerName = container.Names[0]; console.log(chalk.blue('🔄 Recreating container'), chalk.cyan(containerName), `(ID: ${containerId}) with PullImage=true...`); try { const response = await api.post(`${options.url}/api/docker/${endpointId}/containers/${containerId}/recreate`, { PullImage: true }); console.log(chalk.green('✅ Container'), chalk.cyan(containerName), `(ID: ${containerId}) recreated successfully`); return response.data; } catch (error) { console.error(chalk.red('❌ Container recreation failed for'), chalk.cyan(containerName), `(ID: ${containerId})`); console.error(chalk.red('Response:'), error.response?.data || error.message); process.exit(1); } } async function main() { try { // Fetch stack list const stackList = await fetchStackList(); // Find the target stack const targetStack = stackList.find(stack => stack.Name === options.stackName); if (!targetStack) { console.error(chalk.red('❌ Stack'), chalk.cyan(options.stackName), 'not found'); process.exit(1); } const stackId = targetStack.Id; const endpointId = targetStack.EndpointId; console.log(chalk.blue('📋 Found stack:'), chalk.cyan(options.stackName), `(ID: ${stackId}, Endpoint: ${endpointId})`); // Get stack data and file const stackData = await getStackData(stackId); const stackFile = await getStackFile(stackId); // Update the stack await updateStack(stackId, endpointId, stackFile.StackFileContent, stackData.Env); // Get all containers const allContainers = await getAllContainers(endpointId); // Filter containers for the stack const stackContainers = filterStackContainers(allContainers, options.stackName); // Recreate each container for (const container of stackContainers) { await recreateContainer(container, endpointId); } console.log(chalk.green('✅ All containers in stack'), chalk.cyan(options.stackName), `(ID: ${stackId}) recreated successfully with latest images`); } catch (error) { console.error(chalk.red('❌ Unexpected error:'), error.message); process.exit(1); } } // Run the main function main();