import * as plugins from './plugins.js'; import * as interfaces from './interfaces/index.js'; import { DockerHost } from './classes.host.js'; import { logger } from './logger.js'; /** * represents a docker image on the remote docker host */ export class DockerImage { // STATIC public static async getImages(dockerHost: DockerHost) { const images: DockerImage[] = []; const response = await dockerHost.request('GET', '/images/json'); for (const imageObject of response.body) { images.push(new DockerImage(dockerHost, imageObject)); } return images; } public static async getImageByName(dockerHost: DockerHost, imageNameArg: string) { const images = await this.getImages(dockerHost); const result = images.find((image) => { if (image.RepoTags) { return image.RepoTags.includes(imageNameArg); } else { return false; } }); return result; } public static async createFromRegistry( dockerHostArg: DockerHost, optionsArg: { creationObject: interfaces.IImageCreationDescriptor } ): Promise { // lets create a sanatized imageUrlObject const imageUrlObject: { imageUrl: string; imageTag: string; imageOriginTag: string; } = { imageUrl: optionsArg.creationObject.imageUrl, imageTag: optionsArg.creationObject.imageTag, imageOriginTag: null, }; if (imageUrlObject.imageUrl.includes(':')) { const imageUrl = imageUrlObject.imageUrl.split(':')[0]; const imageTag = imageUrlObject.imageUrl.split(':')[1]; if (imageUrlObject.imageTag) { throw new Error( `imageUrl ${imageUrlObject.imageUrl} can't be tagged with ${imageUrlObject.imageTag} because it is already tagged with ${imageTag}` ); } else { imageUrlObject.imageUrl = imageUrl; imageUrlObject.imageTag = imageTag; } } else if (!imageUrlObject.imageTag) { imageUrlObject.imageTag = 'latest'; } imageUrlObject.imageOriginTag = `${imageUrlObject.imageUrl}:${imageUrlObject.imageTag}`; // lets actually create the image const response = await dockerHostArg.request( 'POST', `/images/create?fromImage=${encodeURIComponent( imageUrlObject.imageUrl )}&tag=${encodeURIComponent(imageUrlObject.imageTag)}` ); if (response.statusCode < 300) { logger.log('info', `Successfully pulled image ${imageUrlObject.imageUrl} from the registry`); const image = await DockerImage.getImageByName(dockerHostArg, imageUrlObject.imageOriginTag); return image; } else { logger.log('error', `Failed at the attempt of creating a new image`); } } /** * * @param dockerHostArg * @param tarStreamArg */ public static async createFromTarStream( dockerHostArg: DockerHost, optionsArg: { creationObject: interfaces.IImageCreationDescriptor; tarStream: plugins.smartstream.stream.Readable; } ): Promise { // Start the request for importing an image const response = await dockerHostArg.requestStreaming( 'POST', '/images/load', optionsArg.tarStream ); /** * Docker typically returns lines like: * {"stream":"Loaded image: myrepo/myimage:latest"} * * So we will collect those lines and parse out the final image name. */ let rawOutput = ''; response.on('data', (chunk) => { rawOutput += chunk.toString(); }); // Wrap the end event in a Promise for easier async/await usage await new Promise((resolve, reject) => { response.on('end', () => { resolve(); }); response.on('error', (err) => { reject(err); }); }); // Attempt to parse each line to find something like "Loaded image: ..." let loadedImageTag: string | undefined; const lines = rawOutput.trim().split('\n').filter(Boolean); for (const line of lines) { try { const jsonLine = JSON.parse(line); if ( jsonLine.stream && (jsonLine.stream.startsWith('Loaded image:') || jsonLine.stream.startsWith('Loaded image ID:')) ) { // Examples: // "Loaded image: your-image:latest" // "Loaded image ID: sha256:...." loadedImageTag = jsonLine.stream .replace('Loaded image: ', '') .replace('Loaded image ID: ', '') .trim(); } } catch { // not valid JSON, ignore } } if (!loadedImageTag) { throw new Error( `Could not parse the loaded image info from Docker response.\nResponse was:\n${rawOutput}` ); } // Now try to look up that image by the "loadedImageTag". // Depending on Docker’s response, it might be something like: // "myrepo/myimage:latest" OR "sha256:someHash..." // If Docker gave you an ID (e.g. "sha256:..."), you may need a separate // DockerImage.getImageById method; or if you prefer, you can treat it as a name. const newlyImportedImage = await DockerImage.getImageByName(dockerHostArg, loadedImageTag); if (!newlyImportedImage) { throw new Error( `Image load succeeded, but no local reference found for "${loadedImageTag}".` ); } logger.log( 'info', `Successfully imported image "${loadedImageTag}".` ); return newlyImportedImage; } public static async tagImageByIdOrName( dockerHost: DockerHost, idOrNameArg: string, newTagArg: string ) { const response = await dockerHost.request( 'POST', `/images/${encodeURIComponent(idOrNameArg)}/${encodeURIComponent(newTagArg)}` ); } public static async buildImage(dockerHostArg: DockerHost, dockerImageTag) { // TODO: implement building an image } // INSTANCE // references public dockerHost: DockerHost; // properties /** * the tags for an image */ public Containers: number; public Created: number; public Id: string; public Labels: interfaces.TLabels; public ParentId: string; public RepoDigests: string[]; public RepoTags: string[]; public SharedSize: number; public Size: number; public VirtualSize: number; constructor(dockerHostArg, dockerImageObjectArg: any) { this.dockerHost = dockerHostArg; Object.keys(dockerImageObjectArg).forEach((keyArg) => { this[keyArg] = dockerImageObjectArg[keyArg]; }); } /** * tag an image * @param newTag */ public async tagImage(newTag) { throw new Error('.tagImage is not yet implemented'); } /** * pulls the latest version from the registry */ public async pullLatestImageFromRegistry(): Promise { const updatedImage = await DockerImage.createFromRegistry(this.dockerHost, { creationObject: { imageUrl: this.RepoTags[0], }, }); Object.assign(this, updatedImage); // TODO: Compare image digists before and after return true; } // get stuff public async getVersion() { if (this.Labels && this.Labels.version) { return this.Labels.version; } else { return '0.0.0'; } } /** * exports an image to a tar ball */ public async exportToTarStream(): Promise { logger.log('info', `Exporting image ${this.RepoTags[0]} to tar stream.`); const response = await this.dockerHost.requestStreaming('GET', `/images/${encodeURIComponent(this.RepoTags[0])}/get`); let counter = 0; const webduplexStream = new plugins.smartstream.SmartDuplex({ writeFunction: async (chunk, tools) => { if (counter % 1000 === 0) console.log(`Got chunk: ${counter}`); counter++; return chunk; } }); response.on('data', (chunk) => { if (!webduplexStream.write(chunk)) { response.pause(); webduplexStream.once('drain', () => { response.resume(); }) }; }); response.on('end', () => { webduplexStream.end(); }) return webduplexStream; } }