portainer-stack-recreate
Version:
A Node.js CLI tool to recreate Portainer Docker stacks with latest images
201 lines (159 loc) • 7.01 kB
JavaScript
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();