UNPKG

balena-cli

Version:

The official balena Command Line Interface

253 lines (223 loc) • 5.72 kB
/** * @license * Copyright 2019 Balena Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { retry } from '../helpers'; import Logger = require('../logger'); import * as ApiErrors from './errors'; import { getBalenaSdk } from '../lazy'; import type { BalenaSDK } from 'balena-sdk'; export interface DeviceResponse { [key: string]: any; status: 'success' | 'failed'; message?: string; } export interface DeviceInfo { deviceType: string; arch: string; } export interface Status { appState: 'applied' | 'applying'; overallDownloadProgress: null | number; containers: Array<{ status: string; serviceName: string; appId: number; imageId: number; serviceId: number; containerId: string; createdAt: string; }>; images: Array<{ name: string; appId: number; serviceName: string; imageId: number; dockerImageId: string; status: string; downloadProgress: null | number; }>; } const deviceEndpoints = { setTargetState: 'v2/local/target-state', getTargetState: 'v2/local/target-state', getDeviceInformation: 'v2/local/device-info', logs: 'v2/local/logs', ping: 'ping', version: 'v2/version', status: 'v2/state/status', containerId: 'v2/containerId', }; export class DeviceAPI { private deviceAddress: string; public constructor( private logger: Logger, addr: string, port = 48484, ) { this.deviceAddress = `http://${addr}:${port}/`; } // Either return nothing, or throw an error with the info public async setTargetState(state: Record<string, any>) { const url = this.getUrlForAction('setTargetState'); await DeviceAPI.sendRequest( { method: 'POST', url, json: true, body: state, }, this.logger, ); } public async getTargetState() { const url = this.getUrlForAction('getTargetState'); return await DeviceAPI.sendRequest( { method: 'GET', url, json: true, }, this.logger, ).then(({ state }: { state: Record<string, any> }) => { return state; }); } public async getDeviceInformation() { const url = this.getUrlForAction('getDeviceInformation'); return await DeviceAPI.sendRequest( { method: 'GET', url, json: true, }, this.logger, ).then(({ info }: { info: DeviceInfo }) => { return info; }); } public async getContainerId(serviceName: string): Promise<string> { const url = this.getUrlForAction('containerId'); const body = await DeviceAPI.sendRequest( { method: 'GET', url, json: true, qs: { serviceName, }, }, this.logger, ); if (body.status !== 'success') { throw new ApiErrors.DeviceAPIError( 'Non-successful response from supervisor containerId endpoint', ); } return body.containerId; } public async ping() { const url = this.getUrlForAction('ping'); await DeviceAPI.sendRequest( { method: 'GET', url, }, this.logger, ); } public async getVersion(): Promise<string> { const url = this.getUrlForAction('version'); return await DeviceAPI.sendRequest({ method: 'GET', url, json: true, }).then((body) => { if (body.status !== 'success') { throw new ApiErrors.DeviceAPIError( 'Non-successful response from supervisor version endpoint', ); } return body.version; }); } public async getStatus() { const url = this.getUrlForAction('status'); return await DeviceAPI.sendRequest({ method: 'GET', url, json: true, }).then((body) => { if (body.status !== 'success') { throw new ApiErrors.DeviceAPIError( 'Non-successful response from supervisor status endpoint', ); } delete body.status; return body as Status; }); } public async getLogStream() { const url = this.getUrlForAction('logs'); const sdk = getBalenaSdk(); const stream = await sdk.request.stream({ url }); stream.on('response', (res) => { if (res.statusCode !== 200) { throw new ApiErrors.DeviceAPIError( 'Non-200 response from log streaming endpoint', ); } res.socket.setKeepAlive(true, 1000); }); return stream; } private getUrlForAction(action: keyof typeof deviceEndpoints) { return `${this.deviceAddress}${deviceEndpoints[action]}`; } // A helper method for promisifying general (non-streaming) requests. Streaming // requests should use a seperate setup private static async sendRequest( opts: Parameters<BalenaSDK['request']['send']>[number], logger?: Logger, ) { if (logger != null && opts.url != null) { logger.logDebug(`Sending request to ${opts.url}`); } const sdk = getBalenaSdk(); const doRequest = async () => { const response = await sdk.request.send(opts); const bodyError = typeof response.body === 'string' ? response.body : response.body.message; switch (response.statusCode) { case 200: return response.body; case 400: throw new ApiErrors.BadRequestDeviceAPIError(bodyError); case 503: throw new ApiErrors.ServiceUnavailableAPIError(bodyError); default: new ApiErrors.DeviceAPIError(bodyError); } }; return await retry({ func: doRequest, initialDelayMs: 2000, maxAttempts: 6, label: `Supervisor API (${opts.method} ${opts.url})`, }); } }