276 lines
7.9 KiB
TypeScript
276 lines
7.9 KiB
TypeScript
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<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.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<DockerImage> {
|
||
// 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<void>((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<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;
|
||
}
|
||
|
||
// 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`);
|
||
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;
|
||
}
|
||
}
|