From f77156772aa810f2f25259d7ab4a087336916083 Mon Sep 17 00:00:00 2001 From: Cameron Diver Date: Mon, 27 May 2019 11:32:47 +0100 Subject: [PATCH] Add the ability to specify an environment variable when pushing to local mode device Closes: #1255 Change-type: minor Signed-off-by: Cameron Diver --- doc/cli.markdown | 11 +++++++ lib/actions/push.ts | 26 ++++++++++++++- lib/utils/device/deploy.ts | 67 ++++++++++++++++++++++++++++++++++++++ lib/utils/device/live.ts | 4 +-- 4 files changed, 105 insertions(+), 3 deletions(-) diff --git a/doc/cli.markdown b/doc/cli.markdown index 214f6d4c..3034e1d2 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -1478,6 +1478,7 @@ Examples: $ balena push 10.0.0.1 $ balena push 10.0.0.1 --source $ balena push 10.0.0.1 --service my-service + $ balena push 10.0.0.1 --env MY_ENV_VAR=value --env my-service:SERVICE_VAR=value $ balena push 23c73a1.local --system $ balena push 23c73a1.local --system --service my-service @@ -1529,6 +1530,16 @@ Only valid when pushing to a local mode device. Only show system logs. This can be used in combination with --service. Only valid when pushing to a local mode device. +#### --env <env> + +When performing a push to device, run the built containers with environment +variables provided with this argument. Environment variables can be applied +to individual services by adding their service name before the argument, +separated by a colon, e.g: + --env main:MY_ENV=value +Note that if the service name cannot be found in the composition, the entire +left hand side of the = character will be treated as the variable name. + # Settings ## settings diff --git a/lib/actions/push.ts b/lib/actions/push.ts index 7df75563..593148be 100644 --- a/lib/actions/push.ts +++ b/lib/actions/push.ts @@ -1,5 +1,5 @@ /* -Copyright 2016-2018 Balena Ltd. +Copyright 2016-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. @@ -114,6 +114,7 @@ export const push: CommandDefinition< detached: boolean; service: string; system: boolean; + env: string | string[]; } > = { signature: 'push ', @@ -153,6 +154,7 @@ export const push: CommandDefinition< $ balena push 10.0.0.1 $ balena push 10.0.0.1 --source $ balena push 10.0.0.1 --service my-service + $ balena push 10.0.0.1 --env MY_ENV_VAR=value --env my-service:SERVICE_VAR=value $ balena push 23c73a1.local --system $ balena push 23c73a1.local --system --service my-service @@ -224,6 +226,19 @@ export const push: CommandDefinition< Only valid when pushing to a local mode device.`, boolean: true, }, + { + signature: 'env', + parameter: 'env', + description: stripIndent` + When performing a push to device, run the built containers with environment + variables provided with this argument. Environment variables can be applied + to individual services by adding their service name before the argument, + separated by a colon, e.g: + --env main:MY_ENV=value + Note that if the service name cannot be found in the composition, the entire + left hand side of the = character will be treated as the variable name. + `, + }, ], async action(params, options, done) { const sdk = (await import('balena-sdk')).fromSharedOptions(); @@ -282,6 +297,11 @@ export const push: CommandDefinition< 'The --system flag is only valid when pushing to a local mode device.', ); } + if (options.env) { + exitWithExpectedError( + 'The --env flag is only valid when pushing to a local mode device.', + ); + } const app = appOrDevice; await exitIfNotLoggedIn(); @@ -324,6 +344,10 @@ export const push: CommandDefinition< detached: options.detached || false, service: options.service, system: options.system || false, + env: + typeof options.env === 'string' + ? [options.env] + : options.env || [], }), ) .catch(BuildError, e => { diff --git a/lib/utils/device/deploy.ts b/lib/utils/device/deploy.ts index 3e28528a..7beec4b6 100644 --- a/lib/utils/device/deploy.ts +++ b/lib/utils/device/deploy.ts @@ -50,6 +50,11 @@ export interface DeviceDeployOptions { detached: boolean; service?: string; system: boolean; + env: string[]; +} + +interface ParsedEnvironment { + [serviceName: string]: { [key: string]: string }; } async function checkSource(source: string): Promise { @@ -57,6 +62,58 @@ async function checkSource(source: string): Promise { return (await fs.exists(source)) && (await fs.stat(source)).isDirectory(); } +async function environmentFromInput( + envs: string[], + serviceNames: string[], + logger: Logger, +): Promise { + const { exitWithExpectedError } = await import('../patterns'); + // A normal environment variable regex, with an added part + // to find a colon followed servicename at the start + const varRegex = /^(?:([^\s:]+):)?([^\s]+?)=(.*)$/; + + const ret: ParsedEnvironment = {}; + // Propolulate the object with the servicenames, as it + // also means that we can do a fast lookup of whether a + // service exists + for (const service of serviceNames) { + ret[service] = {}; + } + + for (const env of envs) { + const maybeMatch = env.match(varRegex); + if (maybeMatch == null) { + exitWithExpectedError(`Unable to parse environment variable: ${env}`); + } + const match = maybeMatch!; + let service: string | undefined; + if (match[1]) { + // This is for a service, we check that it actually + // exists + if (!(match[1] in ret)) { + logger.logDebug( + `Warning: Cannot find a service with name ${ + match[1] + }. Treating the string as part of the environment variable name.`, + ); + match[2] = `${match[1]}:${match[2]}`; + } else { + service = match[1]; + } + } + + if (service != null) { + ret[service][match[2]] = match[3]; + } else { + for (const serviceName of serviceNames) { + ret[serviceName][match[2]] = match[3]; + } + } + } + + return ret; +} + export async function deployToDevice(opts: DeviceDeployOptions): Promise { const { loadProject, tarDirectory } = await import('../compose'); const { exitWithExpectedError } = await import('../patterns'); @@ -136,6 +193,12 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise { buildLogs, ); + const envs = await environmentFromInput( + opts.env, + Object.getOwnPropertyNames(project.composition.services), + globalLogger, + ); + globalLogger.logDebug('Setting device state...'); // Now set the target state on the device @@ -144,6 +207,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise { const targetState = generateTargetState( currentTargetState, project.composition, + envs, ); globalLogger.logDebug(`Sending target state: ${JSON.stringify(targetState)}`); @@ -376,6 +440,7 @@ function generateImageName(serviceName: string): string { export function generateTargetState( currentTargetState: any, composition: Composition, + env: ParsedEnvironment, ): any { const services: { [serviceId: string]: any } = {}; let idx = 1; @@ -390,6 +455,8 @@ export function generateTargetState( labels: {}, }; + opts.environment = _.merge(opts.environment, env[name]); + services[idx] = _.merge(defaults, opts, { imageId: idx, serviceName: name, diff --git a/lib/utils/device/live.ts b/lib/utils/device/live.ts index fe2790cd..e17b003c 100644 --- a/lib/utils/device/live.ts +++ b/lib/utils/device/live.ts @@ -348,7 +348,7 @@ export class LivepushManager { // we rebuilt const comp = _.cloneDeep(this.composition); delete comp.services[serviceName]; - const intermediateState = generateTargetState(currentState, comp); + const intermediateState = generateTargetState(currentState, comp, {}); await this.api.setTargetState(intermediateState); // Now we wait for the device state to settle @@ -356,7 +356,7 @@ export class LivepushManager { // And re-set the target state await this.api.setTargetState( - generateTargetState(currentState, this.composition), + generateTargetState(currentState, this.composition, {}), ); await this.awaitDeviceStateSettle();