2024-06-05 14:10:44 +02:00
|
|
|
import * as plugins from './plugins.js';
|
|
|
|
|
import * as paths from './paths.js';
|
2024-06-08 15:03:19 +02:00
|
|
|
import { logger } from './logger.js';
|
2024-06-05 23:56:02 +02:00
|
|
|
import type { DockerHost } from './classes.host.js';
|
2024-06-05 14:10:44 +02:00
|
|
|
|
2026-03-28 05:39:48 +00:00
|
|
|
const smartfileFactory = plugins.smartfile.SmartFileFactory.nodeFs();
|
|
|
|
|
|
2024-06-05 14:10:44 +02:00
|
|
|
export interface IDockerImageStoreConstructorOptions {
|
2024-06-06 00:32:50 +02:00
|
|
|
/**
|
|
|
|
|
* used for preparing images for longer term storage
|
|
|
|
|
*/
|
|
|
|
|
localDirPath: string;
|
|
|
|
|
/**
|
|
|
|
|
* a smartbucket dir for longer term storage.
|
|
|
|
|
*/
|
|
|
|
|
bucketDir: plugins.smartbucket.Directory;
|
2024-06-05 14:10:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class DockerImageStore {
|
|
|
|
|
public options: IDockerImageStoreConstructorOptions;
|
|
|
|
|
|
2024-10-13 13:14:35 +02:00
|
|
|
constructor(optionsArg: IDockerImageStoreConstructorOptions) {
|
2024-06-05 14:10:44 +02:00
|
|
|
this.options = optionsArg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Method to store tar stream
|
2025-08-19 01:46:37 +00:00
|
|
|
public async storeImage(
|
|
|
|
|
imageName: string,
|
|
|
|
|
tarStream: plugins.smartstream.stream.Readable,
|
|
|
|
|
): Promise<void> {
|
2024-06-08 15:03:19 +02:00
|
|
|
logger.log('info', `Storing image ${imageName}...`);
|
|
|
|
|
const uniqueProcessingId = plugins.smartunique.shortId();
|
2024-06-05 14:10:44 +02:00
|
|
|
|
2025-08-19 01:46:37 +00:00
|
|
|
const initialTarDownloadPath = plugins.path.join(
|
|
|
|
|
this.options.localDirPath,
|
|
|
|
|
`${uniqueProcessingId}.tar`,
|
|
|
|
|
);
|
|
|
|
|
const extractionDir = plugins.path.join(
|
|
|
|
|
this.options.localDirPath,
|
|
|
|
|
uniqueProcessingId,
|
|
|
|
|
);
|
2024-06-05 14:10:44 +02:00
|
|
|
// Create a write stream to store the tar file
|
2026-03-28 05:39:48 +00:00
|
|
|
const writeStream = plugins.fs.createWriteStream(initialTarDownloadPath);
|
2024-06-05 14:10:44 +02:00
|
|
|
|
2024-06-08 15:03:19 +02:00
|
|
|
// lets wait for the write stream to finish
|
2026-03-28 05:39:48 +00:00
|
|
|
await new Promise<void>((resolve, reject) => {
|
2024-06-05 14:10:44 +02:00
|
|
|
tarStream.pipe(writeStream);
|
2026-03-28 05:39:48 +00:00
|
|
|
writeStream.on('finish', () => resolve());
|
2024-06-05 14:10:44 +02:00
|
|
|
writeStream.on('error', reject);
|
|
|
|
|
});
|
2025-08-19 01:46:37 +00:00
|
|
|
logger.log(
|
|
|
|
|
'info',
|
|
|
|
|
`Image ${imageName} stored locally for processing. Extracting...`,
|
|
|
|
|
);
|
2024-06-08 15:03:19 +02:00
|
|
|
|
|
|
|
|
// lets process the image
|
2026-03-28 05:39:48 +00:00
|
|
|
await plugins.smartarchive.SmartArchive.create()
|
|
|
|
|
.file(initialTarDownloadPath)
|
|
|
|
|
.extract(extractionDir);
|
2024-06-08 15:03:19 +02:00
|
|
|
logger.log('info', `Image ${imageName} extracted.`);
|
2026-03-28 05:39:48 +00:00
|
|
|
await plugins.fs.promises.rm(initialTarDownloadPath, { force: true });
|
2024-06-08 15:03:19 +02:00
|
|
|
logger.log('info', `deleted original tar to save space.`);
|
|
|
|
|
logger.log('info', `now repackaging for s3...`);
|
2026-03-28 05:39:48 +00:00
|
|
|
const smartfileIndexJson = await smartfileFactory.fromFilePath(
|
2025-08-19 01:46:37 +00:00
|
|
|
plugins.path.join(extractionDir, 'index.json'),
|
|
|
|
|
);
|
2026-03-28 05:39:48 +00:00
|
|
|
const smartfileManifestJson = await smartfileFactory.fromFilePath(
|
|
|
|
|
plugins.path.join(extractionDir, 'manifest.json'),
|
|
|
|
|
);
|
|
|
|
|
const smartfileOciLayoutJson = await smartfileFactory.fromFilePath(
|
|
|
|
|
plugins.path.join(extractionDir, 'oci-layout'),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// repositories file is optional in OCI image tars
|
|
|
|
|
const repositoriesPath = plugins.path.join(extractionDir, 'repositories');
|
|
|
|
|
const hasRepositories = plugins.fs.existsSync(repositoriesPath);
|
|
|
|
|
const smartfileRepositoriesJson = hasRepositories
|
|
|
|
|
? await smartfileFactory.fromFilePath(repositoriesPath)
|
|
|
|
|
: null;
|
|
|
|
|
|
2024-06-08 15:03:19 +02:00
|
|
|
const indexJson = JSON.parse(smartfileIndexJson.contents.toString());
|
|
|
|
|
const manifestJson = JSON.parse(smartfileManifestJson.contents.toString());
|
2025-08-19 01:46:37 +00:00
|
|
|
const ociLayoutJson = JSON.parse(
|
|
|
|
|
smartfileOciLayoutJson.contents.toString(),
|
|
|
|
|
);
|
2024-06-08 15:03:19 +02:00
|
|
|
|
2026-03-28 05:39:48 +00:00
|
|
|
if (indexJson.manifests?.[0]?.annotations) {
|
|
|
|
|
indexJson.manifests[0].annotations['io.containerd.image.name'] = imageName;
|
|
|
|
|
}
|
|
|
|
|
if (manifestJson?.[0]?.RepoTags) {
|
|
|
|
|
manifestJson[0].RepoTags[0] = imageName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (smartfileRepositoriesJson) {
|
|
|
|
|
const repositoriesJson = JSON.parse(
|
|
|
|
|
smartfileRepositoriesJson.contents.toString(),
|
|
|
|
|
);
|
|
|
|
|
const repoFirstKey = Object.keys(repositoriesJson)[0];
|
|
|
|
|
const repoFirstValue = repositoriesJson[repoFirstKey];
|
|
|
|
|
repositoriesJson[imageName] = repoFirstValue;
|
|
|
|
|
delete repositoriesJson[repoFirstKey];
|
|
|
|
|
smartfileRepositoriesJson.contents = Buffer.from(
|
|
|
|
|
JSON.stringify(repositoriesJson, null, 2),
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-06-08 15:03:19 +02:00
|
|
|
|
2025-08-19 01:46:37 +00:00
|
|
|
smartfileIndexJson.contents = Buffer.from(
|
|
|
|
|
JSON.stringify(indexJson, null, 2),
|
|
|
|
|
);
|
|
|
|
|
smartfileManifestJson.contents = Buffer.from(
|
|
|
|
|
JSON.stringify(manifestJson, null, 2),
|
|
|
|
|
);
|
|
|
|
|
smartfileOciLayoutJson.contents = Buffer.from(
|
|
|
|
|
JSON.stringify(ociLayoutJson, null, 2),
|
|
|
|
|
);
|
2026-03-28 05:39:48 +00:00
|
|
|
|
|
|
|
|
const writePromises = [
|
2024-06-08 15:03:19 +02:00
|
|
|
smartfileIndexJson.write(),
|
|
|
|
|
smartfileManifestJson.write(),
|
|
|
|
|
smartfileOciLayoutJson.write(),
|
2026-03-28 05:39:48 +00:00
|
|
|
];
|
|
|
|
|
if (smartfileRepositoriesJson) {
|
|
|
|
|
writePromises.push(smartfileRepositoriesJson.write());
|
|
|
|
|
}
|
|
|
|
|
await Promise.all(writePromises);
|
2024-06-08 15:03:19 +02:00
|
|
|
|
|
|
|
|
logger.log('info', 'repackaging archive for s3...');
|
|
|
|
|
const tartools = new plugins.smartarchive.TarTools();
|
2026-03-28 05:39:48 +00:00
|
|
|
const newTarPack = await tartools.getDirectoryPackStream(extractionDir);
|
2024-06-08 15:03:19 +02:00
|
|
|
const finalTarName = `${uniqueProcessingId}.processed.tar`;
|
2025-08-19 01:46:37 +00:00
|
|
|
const finalTarPath = plugins.path.join(
|
|
|
|
|
this.options.localDirPath,
|
|
|
|
|
finalTarName,
|
|
|
|
|
);
|
2026-03-28 05:39:48 +00:00
|
|
|
const finalWriteStream = plugins.fs.createWriteStream(finalTarPath);
|
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
2024-06-08 15:03:19 +02:00
|
|
|
newTarPack.pipe(finalWriteStream);
|
2026-03-28 05:39:48 +00:00
|
|
|
finalWriteStream.on('finish', () => resolve());
|
2024-06-08 15:03:19 +02:00
|
|
|
finalWriteStream.on('error', reject);
|
|
|
|
|
});
|
|
|
|
|
logger.log('ok', `Repackaged image ${imageName} for s3.`);
|
2026-03-28 05:39:48 +00:00
|
|
|
await plugins.fs.promises.rm(extractionDir, { recursive: true, force: true });
|
|
|
|
|
// Remove existing file in bucket if it exists (smartbucket v4 no longer silently overwrites)
|
|
|
|
|
try {
|
|
|
|
|
await this.options.bucketDir.fastRemove({ path: `${imageName}.tar` });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// File may not exist, which is fine
|
|
|
|
|
}
|
|
|
|
|
const finalTarReadStream = plugins.fs.createReadStream(finalTarPath);
|
2024-06-10 00:15:01 +02:00
|
|
|
await this.options.bucketDir.fastPutStream({
|
|
|
|
|
stream: finalTarReadStream,
|
|
|
|
|
path: `${imageName}.tar`,
|
|
|
|
|
});
|
2026-03-28 05:39:48 +00:00
|
|
|
await plugins.fs.promises.rm(finalTarPath, { force: true });
|
2024-06-05 14:10:44 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-08 15:03:19 +02:00
|
|
|
public async start() {
|
2026-03-28 05:39:48 +00:00
|
|
|
// Ensure the local directory exists and is empty
|
|
|
|
|
await plugins.fs.promises.rm(this.options.localDirPath, { recursive: true, force: true });
|
|
|
|
|
await plugins.fs.promises.mkdir(this.options.localDirPath, { recursive: true });
|
2024-06-08 15:03:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async stop() {}
|
|
|
|
|
|
2024-06-05 14:10:44 +02:00
|
|
|
// Method to retrieve tar stream
|
2025-08-19 01:46:37 +00:00
|
|
|
public async getImage(
|
|
|
|
|
imageName: string,
|
|
|
|
|
): Promise<plugins.smartstream.stream.Readable> {
|
|
|
|
|
const imagePath = plugins.path.join(
|
|
|
|
|
this.options.localDirPath,
|
|
|
|
|
`${imageName}.tar`,
|
|
|
|
|
);
|
2024-06-05 14:10:44 +02:00
|
|
|
|
2026-03-28 05:39:48 +00:00
|
|
|
if (!plugins.fs.existsSync(imagePath)) {
|
2024-06-05 14:10:44 +02:00
|
|
|
throw new Error(`Image ${imageName} does not exist.`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 05:39:48 +00:00
|
|
|
return plugins.fs.createReadStream(imagePath);
|
2024-06-05 14:10:44 +02:00
|
|
|
}
|
|
|
|
|
}
|