UNPKG

@lxdhub/api

Version:

Display, search and copy LXD-images using a web interface.

199 lines (180 loc) 5.77 kB
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'; @Injectable() export class LXDService { private logger: LogService; constructor( @Inject(AxiosToken) private readonly axios: typeof Axios, private sourceImageFactory: SourceImageFactory, @Inject('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 } ))); } }