Skip to content

Commit 469e39b

Browse files
committed
wip: feat: add env and entrypoint options for OCI containers
1 parent 5a4d5f3 commit 469e39b

10 files changed

Lines changed: 544 additions & 106 deletions

File tree

create-a-container/bin/create-container.js

Lines changed: 15 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,8 @@ const path = require('path');
3030
const db = require(path.join(__dirname, '..', 'models'));
3131
const { Container, Node, Site } = db;
3232

33-
/**
34-
* Parse command line arguments
35-
* @returns {object} Parsed arguments
36-
*/
37-
function parseArgs() {
38-
const args = {};
39-
for (const arg of process.argv.slice(2)) {
40-
const match = arg.match(/^--([^=]+)=(.+)$/);
41-
if (match) {
42-
args[match[1]] = match[2];
43-
}
44-
}
45-
return args;
46-
}
33+
// Load utilities
34+
const { parseArgs } = require(path.join(__dirname, '..', 'utils', 'cli'));
4735

4836
/**
4937
* Check if a template is a Docker image reference (contains '/')
@@ -86,51 +74,6 @@ function generateImageFilename(parsed) {
8674
return sanitized;
8775
}
8876

89-
/**
90-
* Parse command line arguments
91-
* @returns {object} Parsed arguments
92-
*/
93-
function parseArgs() {
94-
const args = {};
95-
for (const arg of process.argv.slice(2)) {
96-
const match = arg.match(/^--([^=]+)=(.+)$/);
97-
if (match) {
98-
args[match[1]] = match[2];
99-
}
100-
}
101-
return args;
102-
}
103-
104-
/**
105-
* Wait for a Proxmox task to complete
106-
* @param {ProxmoxApi} client - The Proxmox API client
107-
* @param {string} nodeName - The node name
108-
* @param {string} upid - The task UPID
109-
* @param {number} pollInterval - Polling interval in ms (default 2000)
110-
* @param {number} timeout - Timeout in ms (default 300000 = 5 minutes)
111-
* @returns {Promise<object>} The final task status
112-
*/
113-
async function waitForTask(client, nodeName, upid, pollInterval = 2000, timeout = 300000) {
114-
const startTime = Date.now();
115-
while (true) {
116-
const status = await client.taskStatus(nodeName, upid);
117-
console.log(`Task ${upid}: status=${status.status}, exitstatus=${status.exitstatus || 'N/A'}`);
118-
119-
if (status.status === 'stopped') {
120-
if (status.exitstatus && status.exitstatus !== 'OK') {
121-
throw new Error(`Task failed with status: ${status.exitstatus}`);
122-
}
123-
return status;
124-
}
125-
126-
if (Date.now() - startTime > timeout) {
127-
throw new Error(`Task ${upid} timed out after ${timeout}ms`);
128-
}
129-
130-
await new Promise(resolve => setTimeout(resolve, pollInterval));
131-
}
132-
}
133-
13477
/**
13578
* Query IP address from Proxmox interfaces API with retries
13679
* @param {ProxmoxApi} client - The Proxmox API client
@@ -272,7 +215,7 @@ async function main() {
272215
console.log(`Pull task started: ${pullUpid}`);
273216

274217
// Wait for pull to complete
275-
await waitForTask(client, node.name, pullUpid);
218+
await client.waitForTask(node.name, pullUpid);
276219
console.log('Image pulled successfully');
277220

278221
// Create container from the pulled image (Proxmox adds .tar to the filename)
@@ -297,7 +240,7 @@ async function main() {
297240
console.log(`Create task started: ${createUpid}`);
298241

299242
// Wait for create to complete
300-
await waitForTask(client, node.name, createUpid);
243+
await client.waitForTask(node.name, createUpid);
301244
console.log('Container created successfully');
302245

303246
} else {
@@ -323,7 +266,7 @@ async function main() {
323266
console.log(`Clone task started: ${cloneUpid}`);
324267

325268
// Wait for clone to complete
326-
await waitForTask(client, node.name, cloneUpid);
269+
await client.waitForTask(node.name, cloneUpid);
327270
console.log('Clone completed successfully');
328271

329272
// Configure the container (Docker containers are configured at creation time)
@@ -341,6 +284,15 @@ async function main() {
341284
console.log('Container configured');
342285
}
343286

287+
// Apply environment variables and entrypoint if set
288+
const envConfig = container.buildLxcEnvConfig();
289+
if (Object.keys(envConfig).length > 0) {
290+
console.log('Applying environment variables and entrypoint...');
291+
console.log('Config:', JSON.stringify(envConfig, null, 2));
292+
await client.updateLxcConfig(node.name, vmid, envConfig);
293+
console.log('Environment/entrypoint configuration applied');
294+
}
295+
344296
// Store the VMID now that creation succeeded
345297
await container.update({ containerId: vmid });
346298
console.log(`Container VMID ${vmid} stored in database`);
@@ -351,7 +303,7 @@ async function main() {
351303
console.log(`Start task started: ${startUpid}`);
352304

353305
// Wait for start to complete
354-
await waitForTask(client, node.name, startUpid);
306+
await client.waitForTask(node.name, startUpid);
355307
console.log('Container started successfully');
356308

357309
// Get MAC address from config
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env node
2+
/**
3+
* reconfigure-container.js
4+
*
5+
* Background job script that applies configuration changes and restarts a container.
6+
* This script is executed by the job-runner when environment variables or entrypoint
7+
* are changed on an existing container.
8+
*
9+
* Usage: node bin/reconfigure-container.js --container-id=<id>
10+
*
11+
* The script will:
12+
* 1. Load the container record from the database
13+
* 2. Apply env and entrypoint config via Proxmox API
14+
* 3. Stop the container
15+
* 4. Start the container
16+
* 5. Update the container status to 'running'
17+
*
18+
* All output is logged to STDOUT for capture by the job-runner.
19+
* Exit code 0 = success, non-zero = failure.
20+
*/
21+
22+
const path = require('path');
23+
24+
// Load models from parent directory
25+
const db = require(path.join(__dirname, '..', 'models'));
26+
const { Container, Node, Site } = db;
27+
28+
// Load utilities
29+
const { parseArgs } = require(path.join(__dirname, '..', 'utils', 'cli'));
30+
31+
/**
32+
* Main function
33+
*/
34+
async function main() {
35+
const args = parseArgs();
36+
37+
if (!args['container-id']) {
38+
console.error('Usage: node reconfigure-container.js --container-id=<id>');
39+
process.exit(1);
40+
}
41+
42+
const containerId = parseInt(args['container-id'], 10);
43+
console.log(`Starting container reconfiguration for container ID: ${containerId}`);
44+
45+
// Load the container record with its node and site
46+
const container = await Container.findByPk(containerId, {
47+
include: [{
48+
model: Node,
49+
as: 'node',
50+
include: [{
51+
model: Site,
52+
as: 'site'
53+
}]
54+
}]
55+
});
56+
57+
if (!container) {
58+
console.error(`Container with ID ${containerId} not found`);
59+
process.exit(1);
60+
}
61+
62+
if (!container.containerId) {
63+
console.error('Container has no Proxmox VMID - cannot reconfigure');
64+
process.exit(1);
65+
}
66+
67+
const node = container.node;
68+
69+
if (!node) {
70+
console.error('Container has no associated node');
71+
process.exit(1);
72+
}
73+
74+
console.log(`Container: ${container.hostname}`);
75+
console.log(`Node: ${node.name}`);
76+
console.log(`VMID: ${container.containerId}`);
77+
78+
try {
79+
// Get the Proxmox API client
80+
const client = await node.api();
81+
console.log('Proxmox API client initialized');
82+
83+
// Build config from environment variables and entrypoint
84+
const lxcConfig = container.buildLxcEnvConfig();
85+
86+
if (Object.keys(lxcConfig).length > 0) {
87+
console.log('Applying LXC configuration...');
88+
console.log('Config:', JSON.stringify(lxcConfig, null, 2));
89+
await client.updateLxcConfig(node.name, container.containerId, lxcConfig);
90+
console.log('Configuration applied');
91+
} else {
92+
console.log('No configuration changes to apply');
93+
}
94+
95+
// Stop the container
96+
console.log('Stopping container...');
97+
const stopUpid = await client.stopLxc(node.name, container.containerId);
98+
console.log(`Stop task started: ${stopUpid}`);
99+
100+
// Wait for stop to complete (shorter timeout for stop/start)
101+
await client.waitForTask(node.name, stopUpid, 2000, 60000);
102+
console.log('Container stopped');
103+
104+
// Start the container
105+
console.log('Starting container...');
106+
const startUpid = await client.startLxc(node.name, container.containerId);
107+
console.log(`Start task started: ${startUpid}`);
108+
109+
// Wait for start to complete
110+
await client.waitForTask(node.name, startUpid, 2000, 60000);
111+
console.log('Container started');
112+
113+
// Update status to running
114+
await container.update({ status: 'running' });
115+
console.log('Status updated to: running');
116+
117+
console.log('Container reconfiguration completed successfully!');
118+
process.exit(0);
119+
} catch (err) {
120+
console.error('Container reconfiguration failed:', err.message);
121+
122+
// Log axios error details if available
123+
if (err.response?.data) {
124+
console.error('API Error Details:', JSON.stringify(err.response.data, null, 2));
125+
}
126+
127+
// Update status to failed
128+
try {
129+
await container.update({ status: 'failed' });
130+
console.log('Status updated to: failed');
131+
} catch (updateErr) {
132+
console.error('Failed to update container status:', updateErr.message);
133+
}
134+
135+
process.exit(1);
136+
}
137+
}
138+
139+
// Run the main function
140+
main().catch(err => {
141+
console.error('Unhandled error:', err);
142+
process.exit(1);
143+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
/** @type {import('sequelize-cli').Migration} */
4+
module.exports = {
5+
async up(queryInterface, Sequelize) {
6+
await queryInterface.addColumn('Containers', 'environmentVars', {
7+
type: Sequelize.TEXT,
8+
allowNull: true,
9+
defaultValue: null
10+
});
11+
12+
await queryInterface.addColumn('Containers', 'entrypoint', {
13+
type: Sequelize.STRING(2000),
14+
allowNull: true,
15+
defaultValue: null
16+
});
17+
},
18+
19+
async down(queryInterface, Sequelize) {
20+
await queryInterface.removeColumn('Containers', 'entrypoint');
21+
await queryInterface.removeColumn('Containers', 'environmentVars');
22+
}
23+
};

create-a-container/models/container.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,41 @@ module.exports = (sequelize, DataTypes) => {
1717
// a container may have a creation job
1818
Container.belongsTo(models.Job, { foreignKey: 'creationJobId', as: 'creationJob' });
1919
}
20+
21+
/**
22+
* Build LXC config object for environment variables and entrypoint
23+
* Returns config suitable for Proxmox API updateLxcConfig
24+
* @returns {object} Config object with 'env' and 'entrypoint' properties
25+
*/
26+
buildLxcEnvConfig() {
27+
const config = {};
28+
29+
// Parse environment variables from JSON and format as NUL-separated list
30+
// Format: KEY1=value1\0KEY2=value2\0KEY3=value3
31+
if (this.environmentVars) {
32+
try {
33+
const envObj = JSON.parse(this.environmentVars);
34+
const envPairs = [];
35+
for (const [key, value] of Object.entries(envObj)) {
36+
if (key && value !== undefined) {
37+
envPairs.push(`${key}=${value}`);
38+
}
39+
}
40+
if (envPairs.length > 0) {
41+
config['env'] = envPairs.join('\0');
42+
}
43+
} catch (err) {
44+
console.error('Failed to parse environment variables JSON:', err.message);
45+
}
46+
}
47+
48+
// Set entrypoint command
49+
if (this.entrypoint && this.entrypoint.trim()) {
50+
config['entrypoint'] = this.entrypoint.trim();
51+
}
52+
53+
return config;
54+
}
2055
}
2156
Container.init({
2257
hostname: {
@@ -71,6 +106,16 @@ module.exports = (sequelize, DataTypes) => {
71106
type: DataTypes.STRING(50),
72107
allowNull: false,
73108
defaultValue: 'N'
109+
},
110+
environmentVars: {
111+
type: DataTypes.TEXT,
112+
allowNull: true,
113+
defaultValue: null
114+
},
115+
entrypoint: {
116+
type: DataTypes.STRING(2000),
117+
allowNull: true,
118+
defaultValue: null
74119
}
75120
}, {
76121
sequelize,

0 commit comments

Comments
 (0)