Skip to content

Commit 249228a

Browse files
committed
fix: prevent colisions by appending image hash
1 parent a87af72 commit 249228a

1 file changed

Lines changed: 119 additions & 18 deletions

File tree

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

Lines changed: 119 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
*/
2626

2727
const path = require('path');
28+
const https = require('https');
2829

2930
// Load models from parent directory
3031
const db = require(path.join(__dirname, '..', 'models'));
@@ -33,6 +34,90 @@ const { Container, Node, Site } = db;
3334
// Load utilities
3435
const { parseArgs } = require(path.join(__dirname, '..', 'utils', 'cli'));
3536

37+
/**
38+
* Fetch JSON from a URL with optional headers
39+
* @param {string} url - The URL to fetch
40+
* @param {object} headers - Optional headers
41+
* @returns {Promise<object>} Parsed JSON response
42+
*/
43+
function fetchJson(url, headers = {}) {
44+
return new Promise((resolve, reject) => {
45+
const req = https.get(url, { headers }, (res) => {
46+
let data = '';
47+
res.on('data', chunk => data += chunk);
48+
res.on('end', () => {
49+
if (res.statusCode >= 400) {
50+
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
51+
} else {
52+
try {
53+
resolve(JSON.parse(data));
54+
} catch (e) {
55+
reject(new Error(`Failed to parse JSON: ${e.message}`));
56+
}
57+
}
58+
});
59+
});
60+
req.on('error', reject);
61+
});
62+
}
63+
64+
/**
65+
* Get the digest (sha256 hash) of a Docker/OCI image from the registry
66+
* Handles both single-arch and multi-arch (manifest list) images
67+
* @param {string} registry - Registry hostname (e.g., 'docker.io')
68+
* @param {string} repo - Repository (e.g., 'library/nginx')
69+
* @param {string} tag - Tag (e.g., 'latest')
70+
* @returns {Promise<string>} Short digest (first 12 chars of sha256)
71+
*/
72+
async function getImageDigest(registry, repo, tag) {
73+
let headers = {};
74+
75+
// Docker Hub requires auth token
76+
if (registry === 'docker.io' || registry === 'registry-1.docker.io') {
77+
const tokenUrl = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull`;
78+
const tokenData = await fetchJson(tokenUrl);
79+
headers['Authorization'] = `Bearer ${tokenData.token}`;
80+
}
81+
82+
const registryHost = registry === 'docker.io' ? 'registry-1.docker.io' : registry;
83+
84+
// Fetch manifest - accept both single manifest and manifest list
85+
headers['Accept'] = [
86+
'application/vnd.docker.distribution.manifest.v2+json',
87+
'application/vnd.oci.image.manifest.v1+json',
88+
'application/vnd.docker.distribution.manifest.list.v2+json',
89+
'application/vnd.oci.image.index.v1+json'
90+
].join(', ');
91+
92+
const manifestUrl = `https://${registryHost}/v2/${repo}/manifests/${tag}`;
93+
let manifest = await fetchJson(manifestUrl, headers);
94+
95+
// Handle manifest list (multi-arch) - select amd64/linux
96+
if (manifest.manifests && Array.isArray(manifest.manifests)) {
97+
const amd64Manifest = manifest.manifests.find(m =>
98+
m.platform?.architecture === 'amd64' && m.platform?.os === 'linux'
99+
);
100+
if (!amd64Manifest) {
101+
throw new Error('No amd64/linux manifest found in manifest list');
102+
}
103+
104+
// Fetch the actual manifest for amd64
105+
headers['Accept'] = 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json';
106+
const archManifestUrl = `https://${registryHost}/v2/${repo}/manifests/${amd64Manifest.digest}`;
107+
manifest = await fetchJson(archManifestUrl, headers);
108+
}
109+
110+
// Get config digest from manifest
111+
const configDigest = manifest.config?.digest;
112+
if (!configDigest) {
113+
throw new Error('No config digest in manifest');
114+
}
115+
116+
// Return short hash (sha256:abc123... -> abc123...)
117+
const hash = configDigest.replace('sha256:', '');
118+
return hash.substring(0, 12);
119+
}
120+
36121
/**
37122
* Check if a template is a Docker image reference (contains '/')
38123
* @param {string} template - The template string
@@ -63,14 +148,15 @@ function parseDockerRef(ref) {
63148

64149
/**
65150
* Generate a filename for a pulled Docker image
66-
* Replaces special chars with underscores
151+
* Replaces special chars with underscores, includes digest for cache busting
67152
* Note: Proxmox automatically appends .tar, so we don't include it here
68153
* @param {object} parsed - Parsed Docker ref components
69-
* @returns {string} Sanitized filename (e.g., "docker.io_library_nginx_latest")
154+
* @param {string} digest - Short digest hash
155+
* @returns {string} Sanitized filename (e.g., "docker.io_library_nginx_latest_abc123def456")
70156
*/
71-
function generateImageFilename(parsed) {
157+
function generateImageFilename(parsed, digest) {
72158
const { registry, namespace, image, tag } = parsed;
73-
const sanitized = `${registry}_${namespace}_${image}_${tag}`.replace(/[/:]/g, '_');
159+
const sanitized = `${registry}_${namespace}_${image}_${tag}_${digest}`.replace(/[/:]/g, '_');
74160
return sanitized;
75161
}
76162

@@ -199,24 +285,39 @@ async function main() {
199285
const parsed = parseDockerRef(container.template);
200286
console.log(`Docker image: ${parsed.registry}/${parsed.namespace}/${parsed.image}:${parsed.tag}`);
201287

202-
const filename = generateImageFilename(parsed);
203-
console.log(`Target filename: ${filename}`);
204-
205288
const storage = node.imageStorage || 'local';
206289
console.log(`Using storage: ${storage}`);
207290

208-
// Pull the image from OCI registry using full image reference
209-
const imageRef = container.template;
210-
console.log(`Pulling image ${imageRef}...`);
211-
const pullUpid = await client.pullOciImage(node.name, storage, {
212-
reference: imageRef,
213-
filename
214-
});
215-
console.log(`Pull task started: ${pullUpid}`);
291+
// Get image digest from registry to create unique filename
292+
const repo = parsed.namespace ? `${parsed.namespace}/${parsed.image}` : parsed.image;
293+
console.log(`Fetching digest for ${parsed.registry}/${repo}:${parsed.tag}...`);
294+
const digest = await getImageDigest(parsed.registry, repo, parsed.tag);
295+
console.log(`Image digest: ${digest}`);
216296

217-
// Wait for pull to complete
218-
await client.waitForTask(node.name, pullUpid);
219-
console.log('Image pulled successfully');
297+
const filename = generateImageFilename(parsed, digest);
298+
console.log(`Target filename: ${filename}`);
299+
300+
// Check if image already exists in storage
301+
const existingContents = await client.storageContents(node.name, storage, 'vztmpl');
302+
const expectedVolid = `${storage}:vztmpl/${filename}.tar`;
303+
const imageExists = existingContents.some(item => item.volid === expectedVolid);
304+
305+
if (imageExists) {
306+
console.log(`Image already exists in storage: ${expectedVolid}`);
307+
} else {
308+
// Pull the image from OCI registry
309+
const imageRef = container.template;
310+
console.log(`Pulling image ${imageRef}...`);
311+
const pullUpid = await client.pullOciImage(node.name, storage, {
312+
reference: imageRef,
313+
filename
314+
});
315+
console.log(`Pull task started: ${pullUpid}`);
316+
317+
// Wait for pull to complete
318+
await client.waitForTask(node.name, pullUpid);
319+
console.log('Image pulled successfully');
320+
}
220321

221322
// Create container from the pulled image (Proxmox adds .tar to the filename)
222323
console.log(`Creating container from ${filename}.tar...`);

0 commit comments

Comments
 (0)