sc-portal/app/src/app/instances/helpers/instances.service.ts

276 lines
11 KiB
TypeScript

import { Injectable } from '@angular/core';
import { forkJoin, from, Observable, Subject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Instance } from '../models/instance';
import { concatMap, delay, filter, map, repeatWhen, take, tap } from 'rxjs/operators';
import { InstanceRequest } from '../models/instance';
import { Cacheable } from 'ts-cacheable';
import { volumesCacheBuster$ } from '../../volumes/helpers/volumes.service';
const instancesCacheBuster$ = new Subject<void>();
@Injectable({
providedIn: 'root'
})
export class InstancesService
{
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly httpClient: HttpClient) { }
// ----------------------------------------------------------------------------------------------------------------
@Cacheable({
cacheBusterObserver: instancesCacheBuster$
})
get(): Observable<Instance[]>
{
return this.httpClient.get<Instance[]>(`/api/my/machines`);
}
// ----------------------------------------------------------------------------------------------------------------
@Cacheable({
cacheBusterObserver: instancesCacheBuster$
})
getById(instanceId: string): Observable<Instance>
{
return this.httpClient.get<Instance>(`/api/my/machines/${instanceId}`);
}
// ----------------------------------------------------------------------------------------------------------------
getInstanceUntilExpectedState(instance: Instance, expectedStates: string[], callbackFn?: InstanceCallbackFunction, maxRetries = 30): Observable<Instance>
{
// Keep polling the instance until it reaches the expected state
return this.httpClient.get<Instance>(`/api/my/machines/${instance.id}`)
.pipe(
tap(x => callbackFn && callbackFn(x)),
repeatWhen(x =>
{
let retries = 0;
return x.pipe(
delay(3000),
map(y =>
{
if (retries++ === maxRetries)
throw { error: `Failed to retrieve the current status for machine "${instance.name}"` };
return y;
}));
}),
filter(x => expectedStates.includes(x.state)),
take(1) // needed to stop the repeatWhen loop
);
}
// ----------------------------------------------------------------------------------------------------------------
getInstanceUntilNicRemoved(instance: any, networkName: string, callbackFn?: InstanceCallbackFunction, maxRetries = 30): Observable<Instance>
{
networkName = networkName.toLocaleLowerCase();
// Keep polling the instance until it reaches the expected state
return this.httpClient.get<Instance>(`/api/my/machines/${instance.id}`)
.pipe(
tap(instance => callbackFn && callbackFn(instance)),
repeatWhen(x =>
{
let retries = 0;
return x.pipe(
delay(3000),
map(() =>
{
if (retries++ === maxRetries)
throw { error: `Failed to retrieve the current status for machine "${instance.name}"` };
})
);
}),
filter(x => x.state === 'running' && !x.dns_names.some(d => d.toLocaleLowerCase().indexOf(networkName) >= 0)),
take(1), // needed to stop the repeatWhen loop
map(x =>
{
if (callbackFn)
callbackFn(x);
return instance;
})
);
}
// ----------------------------------------------------------------------------------------------------------------
add(instance: InstanceRequest): Observable<Instance>
{
return this.httpClient.post<Instance>(`/api/my/machines`, instance)
.pipe(tap(() =>
{
instancesCacheBuster$.next();
if (instance.volumes?.length)
volumesCacheBuster$.next();
}));
}
// ----------------------------------------------------------------------------------------------------------------
delete(instanceId: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}`)
.pipe(tap(() => instancesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
start(instanceId: string): Observable<Instance>
{
return this.httpClient.post<Instance>(`/api/my/machines/${instanceId}?action=start`, {})
.pipe(tap(() => instancesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
stop(instanceId: string): Observable<Instance>
{
return this.httpClient.post<Instance>(`/api/my/machines/${instanceId}?action=stop`, {})
.pipe(tap(() => instancesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
reboot(instanceId: string): Observable<Instance>
{
return this.httpClient.post<Instance>(`/api/my/machines/${instanceId}?action=reboot`, {})
.pipe(tap(() => instancesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
resize(instanceId: string, packageId: string): Observable<any>
{
return this.httpClient.post(`/api/my/machines/${instanceId}?action=resize&package=${packageId}`, {})
.pipe(tap(() => instancesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
rename(instanceId: string, name: string): Observable<Instance>
{
if (!name)
throw 'Name cannot be empty';
return this.httpClient.post<Instance>(`/api/my/machines/${instanceId}?action=rename&name=${name}`, {})
.pipe(tap(() => instancesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
toggleFirewall(instanceId: string, enable: boolean): Observable<any>
{
return this.httpClient.post(`/api/my/machines/${instanceId}?action=${enable ? 'enable' : 'disable'}_firewall`, {});
}
// ----------------------------------------------------------------------------------------------------------------
toggleDeletionProtection(instanceId: string, enable: boolean): Observable<any>
{
return this.httpClient.post(`/api/my/machines/${instanceId}?action=${enable ? 'enable' : 'disable'}_deletion_protection`, {});
}
// ----------------------------------------------------------------------------------------------------------------
getTags(instanceId: string): Observable<any>
{
return this.httpClient.get(`/api/my/machines/${instanceId}/tags`);
}
// ----------------------------------------------------------------------------------------------------------------
getTag(instanceId: string, key: string): Observable<any>
{
return this.httpClient.get(`/api/my/machines/${instanceId}/tags/${key}`);
}
// ----------------------------------------------------------------------------------------------------------------
addTags(instanceId: string, tags: any): Observable<any>
{
return this.httpClient.post(`/api/my/machines/${instanceId}/tags`, tags);
}
// ----------------------------------------------------------------------------------------------------------------
replaceTags(instanceId: string, tags: any): Observable<any>
{
return this.httpClient.put(`/api/my/machines/${instanceId}/tags`, tags);
}
// ----------------------------------------------------------------------------------------------------------------
deleteAllTags(instanceId: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}/tags`);
}
// ----------------------------------------------------------------------------------------------------------------
deleteTag(instanceId: string, key: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}/tags/${key}`);
}
// ----------------------------------------------------------------------------------------------------------------
getMetadata(instanceId: string): Observable<any>
{
return this.httpClient.get(`/api/my/machines/${instanceId}/metadata`);
}
// ----------------------------------------------------------------------------------------------------------------
getMetadataValue(instanceId: string, key: string): Observable<any>
{
return this.httpClient.get(`/api/my/machines/${instanceId}/metadata/${key}`);
}
// ----------------------------------------------------------------------------------------------------------------
replaceMetadata(instanceId: string, metadata: any): Observable<any>
{
// First retrieve current metadata
return this.httpClient.get(`/api/my/machines/${instanceId}/metadata`)
.pipe(concatMap(existingMetadata =>
{
// Compute which metadata the user chose to remove
const obsoleteMetadata: Observable<any>[] = [];
for (const key of Object.keys(existingMetadata))
if (!metadata.hasOwnProperty(key) && key !== 'root_authorized_keys') // root_authorized_keys is readonly
obsoleteMetadata.push(this.httpClient.delete(`/api/my/machines/${instanceId}/metadata/${key}`));
// Any metadata keys passed in here are created if they do not exist, and overwritten if they do.
const metadataToUpsert = this.httpClient.post(`/api/my/machines/${instanceId}/metadata`, metadata);
if (obsoleteMetadata.length)
{
// In multiple concurrent requests delete the obsolete metadata, then upsert the remaining ones
return forkJoin(obsoleteMetadata).pipe(concatMap(() => metadataToUpsert.pipe(map(() => metadata))));
}
else
return metadataToUpsert;
}));
}
// ----------------------------------------------------------------------------------------------------------------
deleteAllMetadata(instanceId: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}/metadata`);
}
// ----------------------------------------------------------------------------------------------------------------
getAudit(instanceId: string): Observable<any>
{
return this.httpClient.get(`/api/my/machines/${instanceId}/audit`);
}
// ----------------------------------------------------------------------------------------------------------------
@Cacheable()
getImagesRates(): Observable<any>
{
return this.httpClient.get(`./assets/data/images.json`);
}
// ----------------------------------------------------------------------------------------------------------------
@Cacheable()
getPackagesRates(): Observable<any>
{
return this.httpClient.get(`./assets/data/packages.json`);
}
// ----------------------------------------------------------------------------------------------------------------
clearCache()
{
instancesCacheBuster$.next();
}
}
export type InstanceCallbackFunction = ((instance: Instance) => void);