diff --git a/automation/capitanodoc/capitanodoc.ts b/automation/capitanodoc/capitanodoc.ts index 8705f304..4a594517 100644 --- a/automation/capitanodoc/capitanodoc.ts +++ b/automation/capitanodoc/capitanodoc.ts @@ -99,6 +99,8 @@ const capitanoDoc = { 'build/commands/device/rm.js', 'build/commands/device/shutdown.js', 'build/commands/device/track-fleet.js', + 'build/commands/device/start-service.js', + 'build/commands/device/stop-service.js', ], }, { diff --git a/completion/_balena b/completion/_balena index 71fe7124..25336988 100644 --- a/completion/_balena +++ b/completion/_balena @@ -14,7 +14,7 @@ _balena() { app_cmds=( create ) block_cmds=( create ) config_cmds=( generate inject read reconfigure write ) - device_cmds=( deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet ) + device_cmds=( deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet ) devices_cmds=( supported ) env_cmds=( add rename rm ) fleet_cmds=( create pin purge rename restart rm track-latest ) diff --git a/completion/balena-completion.bash b/completion/balena-completion.bash index 272a928f..566b28aa 100644 --- a/completion/balena-completion.bash +++ b/completion/balena-completion.bash @@ -13,7 +13,7 @@ _balena_complete() app_cmds="create" block_cmds="create" config_cmds="generate inject read reconfigure write" - device_cmds="deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet" + device_cmds="deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet" devices_cmds="supported" env_cmds="add rename rm" fleet_cmds="create pin purge rename restart rm track-latest" diff --git a/docs/balena-cli.md b/docs/balena-cli.md index 90149f6c..93a98d07 100644 --- a/docs/balena-cli.md +++ b/docs/balena-cli.md @@ -215,6 +215,8 @@ are encouraged to regularly update the balena CLI to the latest version. - [device rm <uuid(s)>](#device-rm-uuid-s) - [device shutdown <uuid>](#device-shutdown-uuid) - [device track-fleet <uuid>](#device-track-fleet-uuid) + - [device start-service <uuid>](#device-start-service-uuid) + - [device stop-service <uuid>](#device-stop-service-uuid) - Devices @@ -1709,6 +1711,54 @@ the uuid of the device to make track the fleet's release ### Options +## device start-service <uuid> + +Start containers on a device. + +Multiple devices and services may be specified with a comma-separated list +of values (no spaces). + +Examples: + + $ balena device start-service 23c73a1 myService + $ balena device start-service 23c73a1 myService1,myService2 + +### Arguments + +#### UUID + +comma-separated list (no blank spaces) of device UUIDs + +#### SERVICE + +comma-separated list (no blank spaces) of service names + +### Options + +## device stop-service <uuid> + +Stop containers on a device. + +Multiple devices and services may be specified with a comma-separated list +of values (no spaces). + +Examples: + + $ balena device stop-service 23c73a1 myService + $ balena device stop-service 23c73a1 myService1,myService2 + +### Arguments + +#### UUID + +comma-separated list (no blank spaces) of device UUIDs + +#### SERVICE + +comma-separated list (no blank spaces) of service names + +### Options + # Devices ## devices diff --git a/lib/commands/device/start-service.ts b/lib/commands/device/start-service.ts new file mode 100644 index 00000000..4637f221 --- /dev/null +++ b/lib/commands/device/start-service.ts @@ -0,0 +1,139 @@ +/** + * @license + * Copyright 2016-2020 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 { Args } from '@oclif/core'; +import Command from '../../command'; +import * as cf from '../../utils/common-flags'; +import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy'; +import type { BalenaSDK } from 'balena-sdk'; + +export default class DeviceStartServiceCmd extends Command { + public static description = stripIndent` + Start containers on a device. + + Start containers on a device. + + Multiple devices and services may be specified with a comma-separated list + of values (no spaces). + `; + public static examples = [ + '$ balena device start-service 23c73a1 myService', + '$ balena device start-service 23c73a1 myService1,myService2', + ]; + + public static args = { + uuid: Args.string({ + description: 'comma-separated list (no blank spaces) of device UUIDs', + required: true, + }), + service: Args.string({ + description: 'comma-separated list (no blank spaces) of service names', + required: true, + }), + }; + + public static usage = 'device start-service '; + + public static flags = { + help: cf.help, + }; + + public static authenticated = true; + + public async run() { + const { args: params } = await this.parse(DeviceStartServiceCmd); + + const balena = getBalenaSdk(); + const ux = getCliUx(); + + const deviceUuids = params.uuid.split(','); + const serviceNames = params.service.split(','); + + // Iterate sequentially through deviceUuids. + // We may later want to add a batching feature, + // so that n devices are processed in parallel + for (const uuid of deviceUuids) { + ux.action.start(`Starting services on device ${uuid}`); + await this.startServices(balena, uuid, serviceNames); + ux.action.stop(); + } + } + + async startServices( + balena: BalenaSDK, + deviceUuid: string, + serviceNames: string[], + ) { + const { ExpectedError } = await import('../../errors'); + const { getExpandedProp } = await import('../../utils/pine'); + + // Get device + const device = await balena.models.device.getWithServiceDetails( + deviceUuid, + { + $expand: { + is_running__release: { $select: 'commit' }, + }, + }, + ); + + const activeReleaseCommit = getExpandedProp( + device.is_running__release, + 'commit', + ); + + // Check specified services exist on this device before startinganything + serviceNames.forEach((service) => { + if (!device.current_services[service]) { + throw new ExpectedError( + `Service ${service} not found on device ${deviceUuid}.`, + ); + } + }); + + // Start services + const startPromises: Array> = []; + for (const serviceName of serviceNames) { + const service = device.current_services[serviceName]; + // Each service is an array of `CurrentServiceWithCommit` + // because when service is updating, it will actually hold 2 services + // Target commit matching `device.is_running__release` + const serviceContainer = service.find((s) => { + return s.commit === activeReleaseCommit; + }); + + if (serviceContainer) { + startPromises.push( + balena.models.device.startService( + deviceUuid, + serviceContainer.image_id, + ), + ); + } + } + + try { + await Promise.all(startPromises); + } catch (e) { + if (e.message.toLowerCase().includes('no online device')) { + throw new ExpectedError(`Device ${deviceUuid} is not online.`); + } else { + throw e; + } + } + } +} diff --git a/lib/commands/device/stop-service.ts b/lib/commands/device/stop-service.ts new file mode 100644 index 00000000..b7aa53dc --- /dev/null +++ b/lib/commands/device/stop-service.ts @@ -0,0 +1,139 @@ +/** + * @license + * Copyright 2016-2020 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 { Args } from '@oclif/core'; +import Command from '../../command'; +import * as cf from '../../utils/common-flags'; +import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy'; +import type { BalenaSDK } from 'balena-sdk'; + +export default class DeviceStopServiceCmd extends Command { + public static description = stripIndent` + Stop containers on a device. + + Stop containers on a device. + + Multiple devices and services may be specified with a comma-separated list + of values (no spaces). + `; + public static examples = [ + '$ balena device stop-service 23c73a1 myService', + '$ balena device stop-service 23c73a1 myService1,myService2', + ]; + + public static args = { + uuid: Args.string({ + description: 'comma-separated list (no blank spaces) of device UUIDs', + required: true, + }), + service: Args.string({ + description: 'comma-separated list (no blank spaces) of service names', + required: true, + }), + }; + + public static usage = 'device stop-service '; + + public static flags = { + help: cf.help, + }; + + public static authenticated = true; + + public async run() { + const { args: params } = await this.parse(DeviceStopServiceCmd); + + const balena = getBalenaSdk(); + const ux = getCliUx(); + + const deviceUuids = params.uuid.split(','); + const serviceNames = params.service.split(','); + + // Iterate sequentially through deviceUuids. + // We may later want to add a batching feature, + // so that n devices are processed in parallel + for (const uuid of deviceUuids) { + ux.action.start(`Stopping services on device ${uuid}`); + await this.stopServices(balena, uuid, serviceNames); + ux.action.stop(); + } + } + + async stopServices( + balena: BalenaSDK, + deviceUuid: string, + serviceNames: string[], + ) { + const { ExpectedError } = await import('../../errors'); + const { getExpandedProp } = await import('../../utils/pine'); + + // Get device + const device = await balena.models.device.getWithServiceDetails( + deviceUuid, + { + $expand: { + is_running__release: { $select: 'commit' }, + }, + }, + ); + + const activeReleaseCommit = getExpandedProp( + device.is_running__release, + 'commit', + ); + + // Check specified services exist on this device before stoppinganything + serviceNames.forEach((service) => { + if (!device.current_services[service]) { + throw new ExpectedError( + `Service ${service} not found on device ${deviceUuid}.`, + ); + } + }); + + // Stop services + const stopPromises: Array> = []; + for (const serviceName of serviceNames) { + const service = device.current_services[serviceName]; + // Each service is an array of `CurrentServiceWithCommit` + // because when service is updating, it will actually hold 2 services + // Target commit matching `device.is_running__release` + const serviceContainer = service.find((s) => { + return s.commit === activeReleaseCommit; + }); + + if (serviceContainer) { + stopPromises.push( + balena.models.device.stopService( + deviceUuid, + serviceContainer.image_id, + ), + ); + } + } + + try { + await Promise.all(stopPromises); + } catch (e) { + if (e.message.toLowerCase().includes('no online device')) { + throw new ExpectedError(`Device ${deviceUuid} is not online.`); + } else { + throw e; + } + } + } +}