Files
docker/ts/classes.image.ts

346 lines
10 KiB
TypeScript

import * as plugins from './plugins.js';
import * as interfaces from './interfaces/index.js';
import { DockerHost } from './classes.host.js';
import { DockerResource } from './classes.base.js';
import { logger } from './logger.js';
/**
* represents a docker image on the remote docker host
*/
export class DockerImage extends DockerResource {
// STATIC (Internal - prefixed with _ to indicate internal use)
/**
* Internal: Get all images
* Public API: Use dockerHost.getImages() instead
*/
public static async _list(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;
}
/**
* Internal: Get image by name
* Public API: Use dockerHost.getImageByName(name) instead
*/
public static async _fromName(
dockerHost: DockerHost,
imageNameArg: string,
) {
const images = await this._list(dockerHost);
const result = images.find((image) => {
if (image.RepoTags) {
return image.RepoTags.includes(imageNameArg);
} else {
return false;
}
});
return result;
}
/**
* Internal: Create image from registry
* Public API: Use dockerHost.createImageFromRegistry(descriptor) instead
*/
public static async _createFromRegistry(
dockerHostArg: DockerHost,
optionsArg: {
creationObject: interfaces.IImageCreationDescriptor;
},
): Promise<DockerImage> {
// 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._fromName(
dockerHostArg,
imageUrlObject.imageOriginTag,
);
return image;
} else {
logger.log('error', `Failed at the attempt of creating a new image`);
}
}
/**
* Internal: Create image from tar stream
* Public API: Use dockerHost.createImageFromTarStream(stream, descriptor) instead
*/
public static async _createFromTarStream(
dockerHostArg: DockerHost,
optionsArg: {
creationObject: interfaces.IImageCreationDescriptor;
tarStream: plugins.smartstream.stream.Readable;
},
): Promise<DockerImage> {
// Start the request for importing an image
const response = await dockerHostArg.requestStreaming(
'POST',
'/images/load',
optionsArg.tarStream,
);
// requestStreaming now returns Node.js stream
const nodeStream = response as plugins.smartstream.stream.Readable;
/**
* 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 = '';
nodeStream.on('data', (chunk) => {
rawOutput += chunk.toString();
});
// Wrap the end event in a Promise for easier async/await usage
await new Promise<void>((resolve, reject) => {
nodeStream.on('end', () => {
resolve();
});
nodeStream.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._fromName(
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)}`,
);
}
/**
* Internal: Build image from Dockerfile
* Public API: Use dockerHost.buildImage(tag) instead
*/
public static async _build(dockerHostArg: DockerHost, dockerImageTag) {
// TODO: implement building an image
}
// INSTANCE 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: DockerHost, dockerImageObjectArg: any) {
super(dockerHostArg);
Object.keys(dockerImageObjectArg).forEach((keyArg) => {
this[keyArg] = dockerImageObjectArg[keyArg];
});
}
// INSTANCE METHODS
/**
* Refreshes this image's state from the Docker daemon
*/
public async refresh(): Promise<void> {
if (!this.RepoTags || this.RepoTags.length === 0) {
throw new Error('Cannot refresh image without RepoTags');
}
const updated = await DockerImage._fromName(this.dockerHost, this.RepoTags[0]);
if (updated) {
Object.assign(this, updated);
}
}
/**
* 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<boolean> {
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;
}
/**
* Removes this image from the Docker daemon
*/
public async remove(options?: { force?: boolean; noprune?: boolean }): Promise<void> {
const queryParams = new URLSearchParams();
if (options?.force) queryParams.append('force', '1');
if (options?.noprune) queryParams.append('noprune', '1');
const queryString = queryParams.toString();
const response = await this.dockerHost.request(
'DELETE',
`/images/${encodeURIComponent(this.Id)}${queryString ? '?' + queryString : ''}`,
);
if (response.statusCode >= 300) {
throw new Error(`Failed to remove image: ${response.statusCode}`);
}
}
// 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<plugins.smartstream.stream.Readable> {
logger.log('info', `Exporting image ${this.RepoTags[0]} to tar stream.`);
const response = await this.dockerHost.requestStreaming(
'GET',
`/images/${encodeURIComponent(this.RepoTags[0])}/get`,
);
// requestStreaming now returns Node.js stream
const nodeStream = response as plugins.smartstream.stream.Readable;
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;
},
});
nodeStream.on('data', (chunk) => {
if (!webduplexStream.write(chunk)) {
nodeStream.pause();
webduplexStream.once('drain', () => {
nodeStream.resume();
});
}
});
nodeStream.on('end', () => {
webduplexStream.end();
});
nodeStream.on('error', (error) => {
logger.log('error', `Error during image export: ${error.message}`);
webduplexStream.destroy(error);
});
return webduplexStream;
}
}