104 lines
3.0 KiB
TypeScript
104 lines
3.0 KiB
TypeScript
|
|
import type { IBalenaDeviceState, IBalenaStateStatus } from './types.ts';
|
||
|
|
|
||
|
|
export interface ISupervisorClientOptions {
|
||
|
|
address?: string;
|
||
|
|
apiKey?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class SupervisorClient {
|
||
|
|
public readonly address?: string;
|
||
|
|
private readonly apiKey?: string;
|
||
|
|
|
||
|
|
constructor(optionsArg: ISupervisorClientOptions = {}) {
|
||
|
|
this.address = optionsArg.address || Deno.env.get('BALENA_SUPERVISOR_ADDRESS') || undefined;
|
||
|
|
this.apiKey = optionsArg.apiKey || Deno.env.get('BALENA_SUPERVISOR_API_KEY') || undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
public isConfigured() {
|
||
|
|
return Boolean(this.address);
|
||
|
|
}
|
||
|
|
|
||
|
|
public async isAvailable() {
|
||
|
|
if (!this.address) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const response = await fetch(this.buildUrl('/ping', false));
|
||
|
|
const text = await response.text();
|
||
|
|
return response.ok && text.trim() === 'OK';
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public async getDeviceState(): Promise<IBalenaDeviceState> {
|
||
|
|
return await this.requestJson<IBalenaDeviceState>('/v1/device');
|
||
|
|
}
|
||
|
|
|
||
|
|
public async getStateStatus(): Promise<IBalenaStateStatus> {
|
||
|
|
return await this.requestJson<IBalenaStateStatus>('/v2/state/status');
|
||
|
|
}
|
||
|
|
|
||
|
|
public async getSupervisorVersion(): Promise<string | undefined> {
|
||
|
|
const response = await this.requestJson<{ version?: string }>('/v2/version');
|
||
|
|
return response.version;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async triggerUpdate(optionsArg: { force?: boolean; cancel?: boolean } = {}) {
|
||
|
|
await this.requestEmpty('/v1/update', {
|
||
|
|
method: 'POST',
|
||
|
|
body: JSON.stringify({
|
||
|
|
force: Boolean(optionsArg.force),
|
||
|
|
cancel: Boolean(optionsArg.cancel),
|
||
|
|
}),
|
||
|
|
headers: {
|
||
|
|
'content-type': 'application/json',
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public async reboot(optionsArg: { force?: boolean } = {}) {
|
||
|
|
await this.requestEmpty('/v1/reboot', {
|
||
|
|
method: 'POST',
|
||
|
|
body: JSON.stringify({
|
||
|
|
force: Boolean(optionsArg.force),
|
||
|
|
}),
|
||
|
|
headers: {
|
||
|
|
'content-type': 'application/json',
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private async requestJson<TResponse>(
|
||
|
|
pathArg: string,
|
||
|
|
initArg: RequestInit = {},
|
||
|
|
): Promise<TResponse> {
|
||
|
|
const response = await fetch(this.buildUrl(pathArg, true), initArg);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Supervisor request failed: ${pathArg} -> HTTP ${response.status}`);
|
||
|
|
}
|
||
|
|
return await response.json() as TResponse;
|
||
|
|
}
|
||
|
|
|
||
|
|
private async requestEmpty(pathArg: string, initArg: RequestInit = {}) {
|
||
|
|
const response = await fetch(this.buildUrl(pathArg, true), initArg);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Supervisor request failed: ${pathArg} -> HTTP ${response.status}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private buildUrl(pathArg: string, includeApiKeyArg: boolean) {
|
||
|
|
if (!this.address) {
|
||
|
|
throw new Error('Balena Supervisor address is not configured');
|
||
|
|
}
|
||
|
|
const url = new URL(pathArg, this.address.endsWith('/') ? this.address : `${this.address}/`);
|
||
|
|
if (includeApiKeyArg) {
|
||
|
|
if (!this.apiKey) {
|
||
|
|
throw new Error('Balena Supervisor API key is not configured');
|
||
|
|
}
|
||
|
|
url.searchParams.set('apikey', this.apiKey);
|
||
|
|
}
|
||
|
|
return url;
|
||
|
|
}
|
||
|
|
}
|