@lxdhub/api
Version:
Display, search and copy LXD-images using a web interface.
199 lines (180 loc) • 5.77 kB
text/typescript
import { Agent, AgentOptions } from 'https';
import { Image, Remote } from '@lxdhub/db';
import {
Injectable,
Inject,
InternalServerErrorException,
ConflictException
} from '@nestjs/common';
import Axios, { AxiosInstance } from 'axios';
import { LXDHubAPISettings } from '..';
import { SourceImageFactory } from './factories';
import { LogService } from '../log';
import { AxiosToken } from '../third-party';
()
export class LXDService {
private logger: LogService;
constructor(
(AxiosToken)
private readonly axios: typeof Axios,
private sourceImageFactory: SourceImageFactory,
('LXDHubAPISettings')
private settings: LXDHubAPISettings
) {
this.logger = new LogService(this.constructor.name);
}
private getAxiosHttpsInstance(): AxiosInstance {
const options: AgentOptions = {
rejectUnauthorized: false
};
if (this.settings.lxd) {
if (this.settings.lxd.key) {
options.key = this.settings.lxd.key;
}
if (this.settings.lxd.cert) {
options.cert = this.settings.lxd.cert;
}
}
return this.axios.create({
httpsAgent: new Agent(options)
});
}
/**
* Requests a new image on the given remote
* @param url The url to which the image should be request
* @param sourceImageDto The image
*/
private async requestNewImage(url: string, sourceImageDto) {
const axios = this.getAxiosHttpsInstance();
return (await axios.post(url, sourceImageDto)).data;
}
/**
* Waits for the given operation to end
* @param url The operation url
*/
private async waitForOperation(url) {
const axios = this.getAxiosHttpsInstance();
return (await axios.get(url)).data;
}
/**
* Clones the image from the given sourceRemote to the given destinationRemote
* @param image The image to be cloned
* @param sourceRemote The source remote, from which the images comes from
* @param destinationRemote The destination Remote
*/
async cloneImage(
image: Image,
sourceRemote: Remote,
destinationRemote: Remote
): Promise<string> {
const sourceImageDto = this.sourceImageFactory.entityToDto(
image,
sourceRemote,
destinationRemote
);
const url = destinationRemote.serverUrl;
// Start operation
try {
const operation = await this.requestNewImage(
`${url}/1.0/images`,
sourceImageDto
);
// The operation uuid
return operation.metadata.id;
} catch (err) {
if (err?.error_code === 403) {
throw new InternalServerErrorException(
'Server certificate is not valid. Contact a server administrator'
);
}
if (err?.error_code === 500) {
throw new InternalServerErrorException(
'The destination LXD remote is not reachable'
);
}
throw err;
}
}
/**
* Waits for the clone operation and returns the result
* @param destinationRemote The destination remote
* @param operation The operation UUID from the LXD server
*/
async wait(remote: string, operation: string) {
return await this.waitForOperation(
`${remote}/1.0/operations/${operation}/wait`
);
}
/**
* Adds an image to a any given remote
* @param image The image binary data
* @param remote The remote to add the image to (format: https://localhost:8334)
*/
async importImage(remote: string, image: any) {
const axios = this.getAxiosHttpsInstance();
const { data: { operation } } = (await axios.post(`${remote}/1.0/images`, image.buffer, {
headers: {
'X-LXD-Public': '1',
},
maxContentLength: Infinity
}));
const result = (await axios.get(`${remote}/${operation}/wait`)).data;
if (result?.medatada?.status === 'Failure') {
if ((result.metadata?.err ?? '').includes('fingerprint already exists')) {
// image already exists
throw new ConflictException(
result.metadata.err
);
}
throw new InternalServerErrorException(result.metadata.err);
}
return result.metadata.metadata.fingerprint;
}
/**
* Tags an image with some aliases
* @param remote The remote to add the aliases to (format: https://localhost:8334)
* @param fingerprint The SHA-fingerprint of the image to add the alias to
* @param aliases The aliases to add
* @param force Wether to overwrite already existing aliases
*/
async addImageAlias(remote: string, fingerprint: string, aliases: string[], force = false) {
const axios = this.getAxiosHttpsInstance();
const existingAliases = (await axios.get(`${remote}/1.0/images/aliases`)).data.metadata;
const [newAliases, conflictingAliases] = [[], []];
for (const alias of aliases) {
const exists = existingAliases.some((existingAlias) =>
existingAlias.endsWith(alias)
);
if (exists) {
conflictingAliases.push(alias);
} else {
newAliases.push(alias);
}
}
if (conflictingAliases.length) {
if (!force) {
throw new ConflictException(
`Aliases ${conflictingAliases.join(',')} already exist`
);
} else {
// overwrite aliases
await Promise.all(conflictingAliases.map((alias: string) => axios.put(
`${remote}/1.0/images/aliases/${alias}`,
{
description: `Alias ${alias} for ${fingerprint}`,
target: fingerprint,
}
)));
}
}
// add new aliases
await Promise.all(newAliases.map((alias: string) => axios.post(
`${remote}/1.0/images/aliases`,
{
description: `Alias ${alias} for ${fingerprint}`,
target: fingerprint,
name: alias
}
)));
}
}