From bab98df87b181b9c1e16853ea5e5c17727a9bd3d Mon Sep 17 00:00:00 2001
From: Nick Neisen <nwneisen@gmail.com>
Date: Mon, 3 Aug 2020 17:09:40 -0600
Subject: [PATCH] env add: Add ability to add env var to multiple locations in
 one command

Change-type: minor
---
 doc/cli.markdown             |  23 +++---
 lib/actions-oclif/env/add.ts | 133 +++++++++++++++++++++++------------
 tests/commands/help.spec.ts  |   2 +-
 3 files changed, 104 insertions(+), 54 deletions(-)

diff --git a/doc/cli.markdown b/doc/cli.markdown
index ffc75769..ffd9ae10 100644
--- a/doc/cli.markdown
+++ b/doc/cli.markdown
@@ -961,15 +961,16 @@ do not prompt for confirmation before deleting the variable
 
 ## env add NAME [VALUE]
 
-Add an environment or config variable to an application, device or service,
-as selected by the respective command-line options. Either the --application
-or the --device option must be provided, and either may be be used alongside
-the --service option to define a service-specific variable. (A service is an
-application container in a "microservices" application.) When the --service
-option is used in conjunction with the --device option, the service variable
-applies to the selected device only. Otherwise, it applies to all devices of
-the selected application (i.e., the application's fleet). If the --service
-option is omitted, the variable applies to all services.
+Add an environment or config variable to one or more applications, devices 
+or services, as selected by the respective command-line options. Either the 
+--application or the --device option must be provided, and either may be be 
+used alongside the --service option to define a service-specific variable. 
+(A service is an application container in a "microservices" application.) 
+When the --service option is used in conjunction with the --device option, 
+the service variable applies to the selected device only. Otherwise, it 
+applies to all devices of the selected application (i.e., the application's 
+fleet). If the --service option is omitted, the variable applies to all 
+services.
 
 If VALUE is omitted, the CLI will attempt to use the value of the environment
 variable of same name in the CLI process' environment. In this case, a warning
@@ -988,9 +989,13 @@ Examples:
 
 	$ balena env add TERM --application MyApp
 	$ balena env add EDITOR vim --application MyApp
+	$ balena env add EDITOR vim --application MyApp,MyApp2
 	$ balena env add EDITOR vim --application MyApp --service MyService
+	$ balena env add EDITOR vim --application MyApp,MyApp2 --service MyService,MyService2
 	$ balena env add EDITOR vim --device 7cf02a6
+	$ balena env add EDITOR vim --device 7cf02a6,d6f1433
 	$ balena env add EDITOR vim --device 7cf02a6 --service MyService
+	$ balena env add EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2
 
 ### Arguments
 
diff --git a/lib/actions-oclif/env/add.ts b/lib/actions-oclif/env/add.ts
index 46df60c1..76a73644 100644
--- a/lib/actions-oclif/env/add.ts
+++ b/lib/actions-oclif/env/add.ts
@@ -39,17 +39,18 @@ interface ArgsDef {
 
 export default class EnvAddCmd extends Command {
 	public static description = stripIndent`
-		Add an environment or config variable to an application, device or service.
+		Add an environment or config variable to one or more applications, devices or services.
 
-		Add an environment or config variable to an application, device or service,
-		as selected by the respective command-line options. Either the --application
-		or the --device option must be provided, and either may be be used alongside
-		the --service option to define a service-specific variable. (A service is an
-		application container in a "microservices" application.) When the --service
-		option is used in conjunction with the --device option, the service variable
-		applies to the selected device only. Otherwise, it applies to all devices of
-		the selected application (i.e., the application's fleet). If the --service
-		option is omitted, the variable applies to all services.
+		Add an environment or config variable to one or more applications, devices 
+		or services, as selected by the respective command-line options. Either the 
+		--application or the --device option must be provided, and either may be be 
+		used alongside the --service option to define a service-specific variable. 
+		(A service is an application container in a "microservices" application.) 
+		When the --service option is used in conjunction with the --device option, 
+		the service variable applies to the selected device only. Otherwise, it 
+		applies to all devices of the selected application (i.e., the application's 
+		fleet). If the --service option is omitted, the variable applies to all 
+		services.
 
 		If VALUE is omitted, the CLI will attempt to use the value of the environment
 		variable of same name in the CLI process' environment. In this case, a warning
@@ -67,9 +68,13 @@ export default class EnvAddCmd extends Command {
 	public static examples = [
 		'$ balena env add TERM --application MyApp',
 		'$ balena env add EDITOR vim --application MyApp',
+		'$ balena env add EDITOR vim --application MyApp,MyApp2',
 		'$ balena env add EDITOR vim --application MyApp --service MyService',
+		'$ balena env add EDITOR vim --application MyApp,MyApp2 --service MyService,MyService2',
 		'$ balena env add EDITOR vim --device 7cf02a6',
+		'$ balena env add EDITOR vim --device 7cf02a6,d6f1433',
 		'$ balena env add EDITOR vim --device 7cf02a6 --service MyService',
+		'$ balena env add EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
 	];
 
 	public static args = [
@@ -147,17 +152,31 @@ export default class EnvAddCmd extends Command {
 
 		const varType = isConfigVar ? 'configVar' : 'envVar';
 		if (options.application) {
-			await balena.models.application[varType].set(
-				options.application,
-				params.name,
-				params.value,
-			);
+			for (const app of options.application.split(',')) {
+				try {
+					await balena.models.application[varType].set(
+						app,
+						params.name,
+						params.value,
+					);
+				} catch (err) {
+					console.error(`${err.message}, app: ${app}`);
+					process.exitCode = 1;
+				}
+			}
 		} else if (options.device) {
-			await balena.models.device[varType].set(
-				options.device,
-				params.name,
-				params.value,
-			);
+			for (const device of options.device.split(',')) {
+				try {
+					await balena.models.device[varType].set(
+						device,
+						params.name,
+						params.value,
+					);
+				} catch (err) {
+					console.error(`${err.message}, device: ${device}`);
+					process.exitCode = 1;
+				}
+			}
 		}
 	}
 }
@@ -171,31 +190,57 @@ async function setServiceVars(
 	options: FlagsDef,
 ) {
 	if (options.application) {
-		const serviceId = await getServiceIdForApp(
-			sdk,
-			options.application,
-			options.service!,
-		);
-		await sdk.models.service.var.set(serviceId, params.name, params.value!);
-	} else {
+		for (const app of options.application.split(',')) {
+			for (const service of options.service!.split(',')) {
+				try {
+					const serviceId = await getServiceIdForApp(sdk, app, service);
+					await sdk.models.service.var.set(
+						serviceId,
+						params.name,
+						params.value!,
+					);
+				} catch (err) {
+					console.error(`${err.message}, application: ${app}`);
+					process.exitCode = 1;
+				}
+			}
+		}
+	} else if (options.device) {
 		const { getDeviceAndAppFromUUID } = await import('../../utils/cloud');
-		const [device, app] = await getDeviceAndAppFromUUID(
-			sdk,
-			options.device!,
-			['id'],
-			['app_name'],
-		);
-		const serviceId = await getServiceIdForApp(
-			sdk,
-			app.app_name,
-			options.service!,
-		);
-		await sdk.models.device.serviceVar.set(
-			device.id,
-			serviceId,
-			params.name,
-			params.value!,
-		);
+		for (const uuid of options.device.split(',')) {
+			let device;
+			let app;
+			try {
+				[device, app] = await getDeviceAndAppFromUUID(
+					sdk,
+					uuid,
+					['id'],
+					['app_name'],
+				);
+			} catch (err) {
+				console.error(`${err.message}, device: ${uuid}`);
+				process.exitCode = 1;
+				continue;
+			}
+			for (const service of options.service!.split(',')) {
+				try {
+					const serviceId = await getServiceIdForApp(
+						sdk,
+						app.app_name,
+						service,
+					);
+					await sdk.models.device.serviceVar.set(
+						device.id,
+						serviceId,
+						params.name,
+						params.value!,
+					);
+				} catch (err) {
+					console.error(`${err.message}, service: ${service}`);
+					process.exitCode = 1;
+				}
+			}
+		}
 	}
 }
 
diff --git a/tests/commands/help.spec.ts b/tests/commands/help.spec.ts
index 36ab885a..910bfcbd 100644
--- a/tests/commands/help.spec.ts
+++ b/tests/commands/help.spec.ts
@@ -67,7 +67,7 @@ Additional commands:
     device rm <uuid(s)>                   remove one or more devices
     device shutdown <uuid>                shutdown a device
     devices supported                     list the supported device types (like 'raspberrypi3' or 'intel-nuc')
-    env add <name> [value]                add an environment or config variable to an application, device or service
+    env add <name> [value]                add an environment or config variable to one or more applications, devices or services
     env rename <id> <value>               change the value of a config or env var for an app, device or service
     env rm <id>                           remove a config or env var from an application, device or service
     envs                                  list the environment or config variables of an application, device or service