From 8291c96e69407a4c691a35c27ff3cd406794e946 Mon Sep 17 00:00:00 2001 From: Akis Kesoglou Date: Mon, 5 Nov 2018 10:18:18 +0200 Subject: [PATCH] Make specifying the version during configuration optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `version` used to be optional but it seems we recently had to make it a required parameter. However it really feels redundant when all it’s used for is to determine whether the command should issue a legacy user API key or a provisioning key. This makes version optional but tries to figure it out by itself by reading os-release from the image's boot partition. This is not foul-proof however, and while it'll work with most recent images it won't work with all and in that case it'll bail out and only then warn the user to specify it via the --version argument. Change-type: minor --- doc/cli.markdown | 25 ++-- lib/actions/command-options.coffee | 115 ------------------ lib/actions/config.coffee | 16 +-- lib/actions/internal.coffee | 9 +- lib/actions/os.coffee | 45 ++++--- lib/utils/config.ts | 15 ++- lib/utils/helpers.ts | 42 ++++--- package.json | 2 +- ...vice-init.d.ts => balena-device-init.d.ts} | 19 ++- 9 files changed, 104 insertions(+), 184 deletions(-) delete mode 100644 lib/actions/command-options.coffee rename typings/{resin-device-init.d.ts => balena-device-init.d.ts} (76%) diff --git a/doc/cli.markdown b/doc/cli.markdown index fed3ec5e..b3a85991 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -973,7 +973,9 @@ the path to the output JSON file Use this command to configure a previously downloaded operating system image for the specific device or for an application generally. -Calling this command with the exact version number of the targeted image is required. +This command will try to automatically determine the operating system version in order +to correctly configure the image. It may fail to do so however, in which case you'll +have to call this command again with the exact version number of the targeted image. Note that device api keys are only supported on balenaOS 2.0.3+. @@ -983,9 +985,10 @@ are passed directly on the command line, but the recommended way is to pass eith Examples: - $ balena os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 - $ balena os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 --device-api-key - $ balena os configure ../path/rpi.img --app MyApp --version 2.12.7 + $ balena os configure ../path/rpi.img --device 7cf02a6 + $ balena os configure ../path/rpi.img --device 7cf02a6 --device-api-key + $ balena os configure ../path/rpi.img --app MyApp + $ balena os configure ../path/rpi.img --app MyApp --version 2.12.7 ### Options @@ -1135,13 +1138,13 @@ that will be asked for the relevant device type. Examples: - $ balena config generate --device 7cf02a6 --version 2.12.7 - $ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key - $ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key - $ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json - $ balena config generate --app MyApp --version 2.12.7 - $ balena config generate --app MyApp --version 2.12.7 --output config.json - $ balena config generate --app MyApp --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1 + $ balena config generate --device 7cf02a6 + $ balena config generate --device 7cf02a6 --generate-device-api-key + $ balena config generate --device 7cf02a6 --device-api-key + $ balena config generate --device 7cf02a6 --output config.json + $ balena config generate --app MyApp + $ balena config generate --app MyApp --output config.json + $ balena config generate --app MyApp --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1 ### Options diff --git a/lib/actions/command-options.coffee b/lib/actions/command-options.coffee deleted file mode 100644 index e94c57a4..00000000 --- a/lib/actions/command-options.coffee +++ /dev/null @@ -1,115 +0,0 @@ -### -Copyright 2016-2017 Balena - -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. -### - -_ = require('lodash') - -exports.yes = - signature: 'yes' - description: 'confirm non interactively' - boolean: true - alias: 'y' - -exports.optionalApplication = - signature: 'application' - parameter: 'application' - description: 'application name' - alias: [ 'a', 'app' ] - -exports.application = _.defaults - required: 'You have to specify an application' -, exports.optionalApplication - -exports.optionalDevice = - signature: 'device' - parameter: 'device' - description: 'device uuid' - alias: 'd' - -exports.optionalDeviceApiKey = - signature: 'deviceApiKey' - description: 'custom device key - note that this is only supported on balenaOS 2.0.3+' - parameter: 'device-api-key' - alias: 'k' - -exports.optionalOsVersion = - signature: 'version' - description: 'a balenaOS version' - parameter: 'version' - -exports.osVersion = _.defaults - required: 'You have to specify an exact os version' -, exports.optionalOsVersion - -exports.booleanDevice = - signature: 'device' - description: 'device' - boolean: true - alias: 'd' - -exports.osVersionOrSemver = - signature: 'version' - description: """ - exact version number, or a valid semver range, - or 'latest' (includes pre-releases), - or 'default' (excludes pre-releases if at least one stable version is available), - or 'recommended' (excludes pre-releases, will fail if only pre-release versions are available), - or 'menu' (will show the interactive menu) - """ - parameter: 'version' - -exports.network = - signature: 'network' - parameter: 'network' - description: 'network type' - alias: 'n' - -exports.wifiSsid = - signature: 'ssid' - parameter: 'ssid' - description: 'wifi ssid, if network is wifi' - alias: 's' - -exports.wifiKey = - signature: 'key' - parameter: 'key' - description: 'wifi key, if network is wifi' - alias: 'k' - -exports.forceUpdateLock = - signature: 'force' - description: 'force action if the update lock is set' - boolean: true - alias: 'f' - -exports.drive = - signature: 'drive' - description: 'the drive to write the image to, like `/dev/sdb` or `/dev/mmcblk0`. - Careful with this as you can erase your hard drive. - Check `balena util available-drives` for available options.' - parameter: 'drive' - alias: 'd' - -exports.advancedConfig = - signature: 'advanced' - description: 'show advanced configuration options' - boolean: true - alias: 'v' - -exports.hostOSAccess = - signature: 'host' - boolean: true - description: 'access host OS (for devices with balenaOS >= 2.7.5)' - alias: 's' diff --git a/lib/actions/config.coffee b/lib/actions/config.coffee index 0932ae9c..9fe833dd 100644 --- a/lib/actions/config.coffee +++ b/lib/actions/config.coffee @@ -231,17 +231,17 @@ exports.generate = Examples: - $ balena config generate --device 7cf02a6 --version 2.12.7 - $ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key - $ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key - $ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json - $ balena config generate --app MyApp --version 2.12.7 - $ balena config generate --app MyApp --version 2.12.7 --output config.json - $ balena config generate --app MyApp --version 2.12.7 \ + $ balena config generate --device 7cf02a6 + $ balena config generate --device 7cf02a6 --generate-device-api-key + $ balena config generate --device 7cf02a6 --device-api-key + $ balena config generate --device 7cf02a6 --output config.json + $ balena config generate --app MyApp + $ balena config generate --app MyApp --output config.json + $ balena config generate --app MyApp \ --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1 ''' options: [ - commandOptions.osVersion + commandOptions.optionalOsVersion commandOptions.optionalApplication commandOptions.optionalDevice commandOptions.optionalDeviceApiKey diff --git a/lib/actions/internal.coffee b/lib/actions/internal.coffee index dbca57d2..7a4a9984 100644 --- a/lib/actions/internal.coffee +++ b/lib/actions/internal.coffee @@ -27,12 +27,13 @@ exports.osInit = root: true action: (params, options, done) -> Promise = require('bluebird') - init = require('resin-device-init') + init = require('balena-device-init') helpers = require('../utils/helpers') - return Promise.try -> - config = JSON.parse(params.config) - init.initialize(params.image, params.type, config) + configPromise = Promise.try -> JSON.parse(params.config) + manifestPromise = helpers.getManifest(params.image, params.type) + Promise.join configPromise, manifestPromise, (config, manifest) -> + init.initialize(params.image, manifest, config) .then(helpers.osProgressHandler) .nodeify(done) diff --git a/lib/actions/os.coffee b/lib/actions/os.coffee index 88c5d4e9..8c8a7262 100644 --- a/lib/actions/os.coffee +++ b/lib/actions/os.coffee @@ -151,7 +151,7 @@ buildConfig = (image, deviceType, advanced = false) -> form = require('resin-cli-form') helpers = require('../utils/helpers') - helpers.getManifest(image, deviceType) + Promise.resolve(helpers.getManifest(image, deviceType)) .get('options') .then (questions) -> if not advanced @@ -203,7 +203,9 @@ exports.configure = Use this command to configure a previously downloaded operating system image for the specific device or for an application generally. - Calling this command with the exact version number of the targeted image is required. + This command will try to automatically determine the operating system version in order + to correctly configure the image. It may fail to do so however, in which case you'll + have to call this command again with the exact version number of the targeted image. Note that device api keys are only supported on balenaOS 2.0.3+. @@ -213,9 +215,10 @@ exports.configure = Examples: - $ balena os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 - $ balena os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 --device-api-key - $ balena os configure ../path/rpi.img --app MyApp --version 2.12.7 + $ balena os configure ../path/rpi.img --device 7cf02a6 + $ balena os configure ../path/rpi.img --device 7cf02a6 --device-api-key + $ balena os configure ../path/rpi.img --app MyApp + $ balena os configure ../path/rpi.img --app MyApp --version 2.12.7 ''' permission: 'user' options: [ @@ -223,7 +226,7 @@ exports.configure = commandOptions.optionalApplication commandOptions.optionalDevice commandOptions.optionalDeviceApiKey - commandOptions.osVersion + commandOptions.optionalOsVersion { signature: 'config' description: 'path to the config JSON file, see `balena os build-config`' @@ -236,7 +239,7 @@ exports.configure = Promise = require('bluebird') readFileAsync = Promise.promisify(fs.readFile) balena = require('balena-sdk').fromSharedOptions() - init = require('resin-device-init') + init = require('balena-device-init') helpers = require('../utils/helpers') patterns = require('../utils/patterns') { generateDeviceConfig, generateApplicationConfig } = require('../utils/config') @@ -265,20 +268,32 @@ exports.configure = balena.models[configurationResourceType].get(uuid || options.application) .then (appOrDevice) -> - Promise.try -> + manifestPromise = helpers.getManifest(params.image, appOrDevice.device_type) + answersPromise = Promise.try -> if options.config return readFileAsync(options.config, 'utf8') .then(JSON.parse) return buildConfig(params.image, appOrDevice.device_type, options.advanced) - .then (answers) -> + Promise.join answersPromise, manifestPromise, (answers, manifest) -> answers.version = options.version - (if configurationResourceType == 'device' - generateDeviceConfig(appOrDevice, deviceApiKey, answers) - else - generateApplicationConfig(appOrDevice, answers) - ).then (config) -> - init.configure(params.image, appOrDevice.device_type, config, answers) + if not answers.version? + answers.version = helpers.getOsVersion(params.image, manifest).tap (version) -> + if not version? + throw new Error( + 'Could not read OS version from the image. ' + + 'Please specify the version manually with the ' + + '--version argument to this command.' + ) + + Promise.props(answers).then (answers) -> + (if configurationResourceType == 'device' + generateDeviceConfig(appOrDevice, deviceApiKey, answers) + else + generateApplicationConfig(appOrDevice, answers) + ) + .then (config) -> + init.configure(params.image, manifest, config, answers) .then(helpers.osProgressHandler) .nodeify(done) diff --git a/lib/utils/config.ts b/lib/utils/config.ts index a24eea61..619dd8fe 100644 --- a/lib/utils/config.ts +++ b/lib/utils/config.ts @@ -72,11 +72,10 @@ export function generateApplicationConfig( options: { version: string }, ) { return generateBaseConfig(application, options).tap(config => { - if (semver.satisfies(options.version, '>=2.7.8')) { - return addProvisioningKey(config, application.id); - } else { + if (semver.satisfies(options.version, '<2.7.8')) { return addApplicationKey(config, application.id); } + return addProvisioningKey(config, application.id); }); } @@ -91,13 +90,13 @@ export function generateDeviceConfig( .get(device.belongs_to__application.__id) .then(application => { return generateBaseConfig(application, options).tap(config => { - if (deviceApiKey) { - return addDeviceKey(config, device.uuid, deviceApiKey); - } else if (semver.satisfies(options.version, '>=2.0.3')) { - return addDeviceKey(config, device.uuid, true); - } else { + if ( + deviceApiKey == null && + semver.satisfies(options.version, '<2.0.3') + ) { return addApplicationKey(config, application.id); } + return addDeviceKey(config, device.uuid, deviceApiKey || true); }); }) .then(config => { diff --git a/lib/utils/helpers.ts b/lib/utils/helpers.ts index 4d905996..5a37da81 100644 --- a/lib/utils/helpers.ts +++ b/lib/utils/helpers.ts @@ -15,16 +15,16 @@ limitations under the License. */ import os = require('os'); -import Promise = require('bluebird'); +import Bluebird = require('bluebird'); import _ = require('lodash'); import chalk from 'chalk'; import rindle = require('rindle'); import visuals = require('resin-cli-visuals'); import BalenaSdk = require('balena-sdk'); -import { InitializeEmitter, OperationState } from 'resin-device-init'; +import { InitializeEmitter, OperationState } from 'balena-device-init'; -const waitStreamAsync = Promise.promisify(rindle.wait); +const waitStreamAsync = Bluebird.promisify(rindle.wait); const balena = BalenaSdk.fromSharedOptions(); @@ -75,27 +75,29 @@ export function sudo( return executeWithPrivileges(command, stderr); } -export function runCommand(command: string): Promise { +export function runCommand(command: string): Bluebird { const capitano = require('capitano'); - return Promise.fromCallback(resolver => capitano.run(command, resolver)); + return Bluebird.fromCallback(resolver => capitano.run(command, resolver)); } -export function getManifest( +export async function getManifest( image: string, deviceType: string, ): Promise { - const imagefs = require('resin-image-fs'); - // Attempt to read manifest from the first - // partition, but fallback to the API if - // we encounter any errors along the way. - return imagefs - .readFile({ - image, - partition: 1, - path: '/device-type.json', - }) - .then(JSON.parse) - .catch(() => balena.models.device.getManifestBySlug(deviceType)); + const init = await import('balena-device-init'); + const manifest = await init.getImageManifest(image); + if (manifest != null) { + return manifest; + } + return balena.models.device.getManifestBySlug(deviceType); +} + +export async function getOsVersion( + image: string, + manifest: BalenaSdk.DeviceType, +): Promise { + const init = await import('balena-device-init'); + return init.getImageOsVersion(image, manifest); } export function osProgressHandler(step: InitializeEmitter) { @@ -121,8 +123,8 @@ export function osProgressHandler(step: InitializeEmitter) { export function getArchAndDeviceType( applicationName: string, -): Promise<{ arch: string; device_type: string }> { - return Promise.join( +): Bluebird<{ arch: string; device_type: string }> { + return Bluebird.join( getApplication(applicationName), balena.models.config.getDeviceTypes(), function(app, deviceTypes) { diff --git a/package.json b/package.json index 6cfb0769..df09e10f 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "any-promise": "^1.3.0", "archiver": "^2.1.0", "balena-config-json": "^2.0.0", + "balena-device-init": "^5.0.0", "balena-image-manager": "^6.0.0", "balena-preload": "^8.0.0", "balena-sdk": "^11.0.0", @@ -155,7 +156,6 @@ "resin-cli-form": "^2.0.0", "resin-cli-visuals": "^1.4.0", "resin-compose-parse": "^2.0.0", - "resin-device-init": "^4.0.0", "resin-doodles": "0.0.1", "resin-image-fs": "^5.0.2", "resin-multibuild": "^0.9.0", diff --git a/typings/resin-device-init.d.ts b/typings/balena-device-init.d.ts similarity index 76% rename from typings/resin-device-init.d.ts rename to typings/balena-device-init.d.ts index a759c16d..205d1da7 100644 --- a/typings/resin-device-init.d.ts +++ b/typings/balena-device-init.d.ts @@ -1,6 +1,7 @@ -declare module 'resin-device-init' { +declare module 'balena-device-init' { import * as Promise from 'bluebird'; import { EventEmitter } from 'events'; + import { DeviceType } from 'balena-sdk'; interface OperationState { operation: @@ -60,9 +61,23 @@ declare module 'resin-device-init' { on(event: 'burn', callback: (state: BurnProgress) => void): void; } + export function configure( + image: string, + manifest: DeviceType, + config: {}, + options?: {}, + ): Promise; + export function initialize( image: string, - deviceType: string, + manifest: DeviceType, config: {}, ): Promise; + + export function getImageOsVersion( + image: string, + manifest: DeviceType, + ): Promise; + + export function getImageManifest(image: string): Promise; }