1010 *
1111 * The script will:
1212 * 1. Load the container record from the database
13- * 2. Clone the template in Proxmox
13+ * 2. Either clone a Proxmox template OR pull a Docker image and create from it
1414 * 3. Configure the container (cores, memory, network)
1515 * 4. Start the container
1616 * 5. Query MAC address from Proxmox config
1717 * 6. Query IP address from Proxmox interfaces API
1818 * 7. Update the container record with MAC, IP, and status='running'
1919 *
20+ * Docker images are detected by the presence of '/' in the template field.
21+ * Format: host/org/image:tag (e.g., docker.io/library/nginx:latest)
22+ *
2023 * All output is logged to STDOUT for capture by the job-runner.
2124 * Exit code 0 = success, non-zero = failure.
2225 */
@@ -42,6 +45,61 @@ function parseArgs() {
4245 return args ;
4346}
4447
48+ /**
49+ * Check if a template is a Docker image reference (contains '/')
50+ * @param {string } template - The template string
51+ * @returns {boolean } True if Docker image, false if Proxmox template
52+ */
53+ function isDockerImage ( template ) {
54+ return template . includes ( '/' ) ;
55+ }
56+
57+ /**
58+ * Parse a normalized Docker image reference into components
59+ * Format: host/org/image:tag
60+ * @param {string } ref - The normalized Docker reference
61+ * @returns {object } Parsed components: { registry, namespace, image, tag }
62+ */
63+ function parseDockerRef ( ref ) {
64+ // Split off tag
65+ const [ imagePart , tag ] = ref . split ( ':' ) ;
66+ const parts = imagePart . split ( '/' ) ;
67+
68+ // Format is always host/org/image after normalization
69+ const registry = parts [ 0 ] ;
70+ const image = parts [ parts . length - 1 ] ;
71+ const namespace = parts . slice ( 1 , - 1 ) . join ( '/' ) ;
72+
73+ return { registry, namespace, image, tag } ;
74+ }
75+
76+ /**
77+ * Generate a filename for a pulled Docker image
78+ * Replaces special chars with underscores
79+ * @param {object } parsed - Parsed Docker ref components
80+ * @returns {string } Sanitized filename (e.g., "docker.io_library_nginx_latest.tar")
81+ */
82+ function generateImageFilename ( parsed ) {
83+ const { registry, namespace, image, tag } = parsed ;
84+ const sanitized = `${ registry } _${ namespace } _${ image } _${ tag } ` . replace ( / [ / : ] / g, '_' ) ;
85+ return `${ sanitized } .tar` ;
86+ }
87+
88+ /**
89+ * Parse command line arguments
90+ * @returns {object } Parsed arguments
91+ */
92+ function parseArgs ( ) {
93+ const args = { } ;
94+ for ( const arg of process . argv . slice ( 2 ) ) {
95+ const match = arg . match ( / ^ - - ( [ ^ = ] + ) = ( .+ ) $ / ) ;
96+ if ( match ) {
97+ args [ match [ 1 ] ] = match [ 2 ] ;
98+ }
99+ }
100+ return args ;
101+ }
102+
45103/**
46104 * Wait for a Proxmox task to complete
47105 * @param {ProxmoxApi } client - The Proxmox API client
@@ -175,6 +233,9 @@ async function main() {
175233 console . log ( `Site: ${ site . name } (${ site . internalDomain } )` ) ;
176234 console . log ( `Template: ${ container . template } ` ) ;
177235
236+ const isDocker = isDockerImage ( container . template ) ;
237+ console . log ( `Template type: ${ isDocker ? 'Docker image' : 'Proxmox template' } ` ) ;
238+
178239 try {
179240 // Update status to 'creating'
180241 await container . update ( { status : 'creating' } ) ;
@@ -184,54 +245,105 @@ async function main() {
184245 const client = await node . api ( ) ;
185246 console . log ( 'Proxmox API client initialized' ) ;
186247
187- // Find the template VMID by matching the template name
188- console . log ( `Looking for template: ${ container . template } ` ) ;
189- const templates = await client . getLxcTemplates ( node . name ) ;
190- const templateContainer = templates . find ( t => t . name === container . template ) ;
191-
192- if ( ! templateContainer ) {
193- throw new Error ( `Template "${ container . template } " not found on node ${ node . name } ` ) ;
194- }
195-
196- const templateVmid = templateContainer . vmid ;
197- console . log ( `Found template VMID: ${ templateVmid } ` ) ;
198-
199- // Allocate VMID right before cloning to minimize race condition window
248+ // Allocate VMID right before creating to minimize race condition window
200249 console . log ( 'Allocating VMID from Proxmox...' ) ;
201250 const vmid = await client . nextId ( ) ;
202251 console . log ( `Allocated VMID: ${ vmid } ` ) ;
203252
204- // Clone the template
205- console . log ( `Cloning template ${ templateVmid } to VMID ${ vmid } ...` ) ;
206- const cloneUpid = await client . cloneLxc ( node . name , templateVmid , vmid , {
207- hostname : container . hostname ,
208- description : `Cloned from template ${ container . template } ` ,
209- full : 1
210- } ) ;
211- console . log ( `Clone task started: ${ cloneUpid } ` ) ;
212-
213- // Wait for clone to complete
214- await waitForTask ( client , node . name , cloneUpid ) ;
215- console . log ( 'Clone completed successfully' ) ;
253+ if ( isDocker ) {
254+ // Docker image: pull from OCI registry, then create container
255+ const parsed = parseDockerRef ( container . template ) ;
256+ console . log ( `Docker image: ${ parsed . registry } /${ parsed . namespace } /${ parsed . image } :${ parsed . tag } ` ) ;
257+
258+ const filename = generateImageFilename ( parsed ) ;
259+ console . log ( `Target filename: ${ filename } ` ) ;
260+
261+ const storage = node . imageStorage || 'local' ;
262+ console . log ( `Using storage: ${ storage } ` ) ;
263+
264+ // Pull the image from OCI registry using full image reference
265+ const imageRef = container . template ;
266+ console . log ( `Pulling image ${ imageRef } ...` ) ;
267+ const pullUpid = await client . pullOciImage ( node . name , storage , {
268+ reference : imageRef ,
269+ filename
270+ } ) ;
271+ console . log ( `Pull task started: ${ pullUpid } ` ) ;
272+
273+ // Wait for pull to complete
274+ await waitForTask ( client , node . name , pullUpid ) ;
275+ console . log ( 'Image pulled successfully' ) ;
276+
277+ // Create container from the pulled image
278+ console . log ( `Creating container from ${ filename } ...` ) ;
279+ const ostemplate = `${ storage } :vztmpl/${ filename } ` ;
280+ const createUpid = await client . createLxc ( node . name , {
281+ vmid,
282+ hostname : container . hostname ,
283+ ostemplate,
284+ description : `Created from Docker image ${ container . template } ` ,
285+ cores : 4 ,
286+ features : 'nesting=1,keyctl=1,fuse=1' ,
287+ memory : 4096 ,
288+ net0 : 'name=eth0,ip=dhcp,bridge=vmbr0,host-managed=1' ,
289+ searchdomain : site . internalDomain ,
290+ swap : 0 ,
291+ onboot : 1 ,
292+ tags : container . username ,
293+ unprivileged : 1 ,
294+ storage : 'local-lvm'
295+ } ) ;
296+ console . log ( `Create task started: ${ createUpid } ` ) ;
297+
298+ // Wait for create to complete
299+ await waitForTask ( client , node . name , createUpid ) ;
300+ console . log ( 'Container created successfully' ) ;
301+
302+ } else {
303+ // Proxmox template: clone existing container
304+ console . log ( `Looking for template: ${ container . template } ` ) ;
305+ const templates = await client . getLxcTemplates ( node . name ) ;
306+ const templateContainer = templates . find ( t => t . name === container . template ) ;
307+
308+ if ( ! templateContainer ) {
309+ throw new Error ( `Template "${ container . template } " not found on node ${ node . name } ` ) ;
310+ }
311+
312+ const templateVmid = templateContainer . vmid ;
313+ console . log ( `Found template VMID: ${ templateVmid } ` ) ;
314+
315+ // Clone the template
316+ console . log ( `Cloning template ${ templateVmid } to VMID ${ vmid } ...` ) ;
317+ const cloneUpid = await client . cloneLxc ( node . name , templateVmid , vmid , {
318+ hostname : container . hostname ,
319+ description : `Cloned from template ${ container . template } ` ,
320+ full : 1
321+ } ) ;
322+ console . log ( `Clone task started: ${ cloneUpid } ` ) ;
323+
324+ // Wait for clone to complete
325+ await waitForTask ( client , node . name , cloneUpid ) ;
326+ console . log ( 'Clone completed successfully' ) ;
327+
328+ // Configure the container (Docker containers are configured at creation time)
329+ console . log ( 'Configuring container...' ) ;
330+ await client . updateLxcConfig ( node . name , vmid , {
331+ cores : 4 ,
332+ features : 'nesting=1,keyctl=1,fuse=1' ,
333+ memory : 4096 ,
334+ net0 : 'name=eth0,ip=dhcp,bridge=vmbr0' ,
335+ searchdomain : site . internalDomain ,
336+ swap : 0 ,
337+ onboot : 1 ,
338+ tags : container . username
339+ } ) ;
340+ console . log ( 'Container configured' ) ;
341+ }
216342
217- // Store the VMID now that clone succeeded
343+ // Store the VMID now that creation succeeded
218344 await container . update ( { containerId : vmid } ) ;
219345 console . log ( `Container VMID ${ vmid } stored in database` ) ;
220346
221- // Configure the container
222- console . log ( 'Configuring container...' ) ;
223- await client . updateLxcConfig ( node . name , vmid , {
224- cores : 4 ,
225- features : 'nesting=1,keyctl=1,fuse=1' ,
226- memory : 4096 ,
227- net0 : 'name=eth0,ip=dhcp,bridge=vmbr0' ,
228- searchdomain : site . internalDomain ,
229- swap : 0 ,
230- onboot : 1 ,
231- tags : container . username
232- } ) ;
233- console . log ( 'Container configured' ) ;
234-
235347 // Start the container
236348 console . log ( 'Starting container...' ) ;
237349 const startUpid = await client . startLxc ( node . name , vmid ) ;
0 commit comments