Make specifying the version during configuration optional

`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
This commit is contained in:
Akis Kesoglou 2018-11-05 10:18:18 +02:00
parent 561325e66d
commit 8291c96e69
9 changed files with 104 additions and 184 deletions

View File

@ -973,7 +973,9 @@ the path to the output JSON file
Use this command to configure a previously downloaded operating system image for Use this command to configure a previously downloaded operating system image for
the specific device or for an application generally. 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+. 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: Examples:
$ balena os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 $ balena os configure ../path/rpi.img --device 7cf02a6
$ balena os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey> $ balena os configure ../path/rpi.img --device 7cf02a6 --device-api-key <existingDeviceKey>
$ balena os configure ../path/rpi.img --app MyApp --version 2.12.7 $ balena os configure ../path/rpi.img --app MyApp
$ balena os configure ../path/rpi.img --app MyApp --version 2.12.7
### Options ### Options
@ -1135,13 +1138,13 @@ that will be asked for the relevant device type.
Examples: Examples:
$ balena config generate --device 7cf02a6 --version 2.12.7 $ balena config generate --device 7cf02a6
$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key $ balena config generate --device 7cf02a6 --generate-device-api-key
$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey> $ balena config generate --device 7cf02a6 --device-api-key <existingDeviceKey>
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json $ balena config generate --device 7cf02a6 --output config.json
$ balena config generate --app MyApp --version 2.12.7 $ balena config generate --app MyApp
$ balena config generate --app MyApp --version 2.12.7 --output config.json $ balena config generate --app MyApp --output config.json
$ balena config generate --app MyApp --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1 $ balena config generate --app MyApp --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
### Options ### Options

View File

@ -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'

View File

@ -231,17 +231,17 @@ exports.generate =
Examples: Examples:
$ balena config generate --device 7cf02a6 --version 2.12.7 $ balena config generate --device 7cf02a6
$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key $ balena config generate --device 7cf02a6 --generate-device-api-key
$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey> $ balena config generate --device 7cf02a6 --device-api-key <existingDeviceKey>
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json $ balena config generate --device 7cf02a6 --output config.json
$ balena config generate --app MyApp --version 2.12.7 $ balena config generate --app MyApp
$ balena config generate --app MyApp --version 2.12.7 --output config.json $ balena config generate --app MyApp --output config.json
$ balena config generate --app MyApp --version 2.12.7 \ $ balena config generate --app MyApp \
--network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
''' '''
options: [ options: [
commandOptions.osVersion commandOptions.optionalOsVersion
commandOptions.optionalApplication commandOptions.optionalApplication
commandOptions.optionalDevice commandOptions.optionalDevice
commandOptions.optionalDeviceApiKey commandOptions.optionalDeviceApiKey

View File

@ -27,12 +27,13 @@ exports.osInit =
root: true root: true
action: (params, options, done) -> action: (params, options, done) ->
Promise = require('bluebird') Promise = require('bluebird')
init = require('resin-device-init') init = require('balena-device-init')
helpers = require('../utils/helpers') helpers = require('../utils/helpers')
return Promise.try -> configPromise = Promise.try -> JSON.parse(params.config)
config = JSON.parse(params.config) manifestPromise = helpers.getManifest(params.image, params.type)
init.initialize(params.image, params.type, config) Promise.join configPromise, manifestPromise, (config, manifest) ->
init.initialize(params.image, manifest, config)
.then(helpers.osProgressHandler) .then(helpers.osProgressHandler)
.nodeify(done) .nodeify(done)

View File

@ -151,7 +151,7 @@ buildConfig = (image, deviceType, advanced = false) ->
form = require('resin-cli-form') form = require('resin-cli-form')
helpers = require('../utils/helpers') helpers = require('../utils/helpers')
helpers.getManifest(image, deviceType) Promise.resolve(helpers.getManifest(image, deviceType))
.get('options') .get('options')
.then (questions) -> .then (questions) ->
if not advanced if not advanced
@ -203,7 +203,9 @@ exports.configure =
Use this command to configure a previously downloaded operating system image for Use this command to configure a previously downloaded operating system image for
the specific device or for an application generally. 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+. Note that device api keys are only supported on balenaOS 2.0.3+.
@ -213,9 +215,10 @@ exports.configure =
Examples: Examples:
$ balena os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 $ balena os configure ../path/rpi.img --device 7cf02a6
$ balena os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey> $ balena os configure ../path/rpi.img --device 7cf02a6 --device-api-key <existingDeviceKey>
$ balena os configure ../path/rpi.img --app MyApp --version 2.12.7 $ balena os configure ../path/rpi.img --app MyApp
$ balena os configure ../path/rpi.img --app MyApp --version 2.12.7
''' '''
permission: 'user' permission: 'user'
options: [ options: [
@ -223,7 +226,7 @@ exports.configure =
commandOptions.optionalApplication commandOptions.optionalApplication
commandOptions.optionalDevice commandOptions.optionalDevice
commandOptions.optionalDeviceApiKey commandOptions.optionalDeviceApiKey
commandOptions.osVersion commandOptions.optionalOsVersion
{ {
signature: 'config' signature: 'config'
description: 'path to the config JSON file, see `balena os build-config`' description: 'path to the config JSON file, see `balena os build-config`'
@ -236,7 +239,7 @@ exports.configure =
Promise = require('bluebird') Promise = require('bluebird')
readFileAsync = Promise.promisify(fs.readFile) readFileAsync = Promise.promisify(fs.readFile)
balena = require('balena-sdk').fromSharedOptions() balena = require('balena-sdk').fromSharedOptions()
init = require('resin-device-init') init = require('balena-device-init')
helpers = require('../utils/helpers') helpers = require('../utils/helpers')
patterns = require('../utils/patterns') patterns = require('../utils/patterns')
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config') { generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
@ -265,20 +268,32 @@ exports.configure =
balena.models[configurationResourceType].get(uuid || options.application) balena.models[configurationResourceType].get(uuid || options.application)
.then (appOrDevice) -> .then (appOrDevice) ->
Promise.try -> manifestPromise = helpers.getManifest(params.image, appOrDevice.device_type)
answersPromise = Promise.try ->
if options.config if options.config
return readFileAsync(options.config, 'utf8') return readFileAsync(options.config, 'utf8')
.then(JSON.parse) .then(JSON.parse)
return buildConfig(params.image, appOrDevice.device_type, options.advanced) return buildConfig(params.image, appOrDevice.device_type, options.advanced)
.then (answers) -> Promise.join answersPromise, manifestPromise, (answers, manifest) ->
answers.version = options.version answers.version = options.version
(if configurationResourceType == 'device' if not answers.version?
generateDeviceConfig(appOrDevice, deviceApiKey, answers) answers.version = helpers.getOsVersion(params.image, manifest).tap (version) ->
else if not version?
generateApplicationConfig(appOrDevice, answers) throw new Error(
).then (config) -> 'Could not read OS version from the image. ' +
init.configure(params.image, appOrDevice.device_type, config, answers) '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) .then(helpers.osProgressHandler)
.nodeify(done) .nodeify(done)

View File

@ -72,11 +72,10 @@ export function generateApplicationConfig(
options: { version: string }, options: { version: string },
) { ) {
return generateBaseConfig(application, options).tap(config => { return generateBaseConfig(application, options).tap(config => {
if (semver.satisfies(options.version, '>=2.7.8')) { if (semver.satisfies(options.version, '<2.7.8')) {
return addProvisioningKey(config, application.id);
} else {
return addApplicationKey(config, application.id); return addApplicationKey(config, application.id);
} }
return addProvisioningKey(config, application.id);
}); });
} }
@ -91,13 +90,13 @@ export function generateDeviceConfig(
.get(device.belongs_to__application.__id) .get(device.belongs_to__application.__id)
.then(application => { .then(application => {
return generateBaseConfig(application, options).tap(config => { return generateBaseConfig(application, options).tap(config => {
if (deviceApiKey) { if (
return addDeviceKey(config, device.uuid, deviceApiKey); deviceApiKey == null &&
} else if (semver.satisfies(options.version, '>=2.0.3')) { semver.satisfies(options.version, '<2.0.3')
return addDeviceKey(config, device.uuid, true); ) {
} else {
return addApplicationKey(config, application.id); return addApplicationKey(config, application.id);
} }
return addDeviceKey(config, device.uuid, deviceApiKey || true);
}); });
}) })
.then(config => { .then(config => {

View File

@ -15,16 +15,16 @@ limitations under the License.
*/ */
import os = require('os'); import os = require('os');
import Promise = require('bluebird'); import Bluebird = require('bluebird');
import _ = require('lodash'); import _ = require('lodash');
import chalk from 'chalk'; import chalk from 'chalk';
import rindle = require('rindle'); import rindle = require('rindle');
import visuals = require('resin-cli-visuals'); import visuals = require('resin-cli-visuals');
import BalenaSdk = require('balena-sdk'); 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(); const balena = BalenaSdk.fromSharedOptions();
@ -75,27 +75,29 @@ export function sudo(
return executeWithPrivileges(command, stderr); return executeWithPrivileges(command, stderr);
} }
export function runCommand(command: string): Promise<void> { export function runCommand(command: string): Bluebird<void> {
const capitano = require('capitano'); 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, image: string,
deviceType: string, deviceType: string,
): Promise<BalenaSdk.DeviceType> { ): Promise<BalenaSdk.DeviceType> {
const imagefs = require('resin-image-fs'); const init = await import('balena-device-init');
// Attempt to read manifest from the first const manifest = await init.getImageManifest(image);
// partition, but fallback to the API if if (manifest != null) {
// we encounter any errors along the way. return manifest;
return imagefs }
.readFile({ return balena.models.device.getManifestBySlug(deviceType);
image, }
partition: 1,
path: '/device-type.json', export async function getOsVersion(
}) image: string,
.then(JSON.parse) manifest: BalenaSdk.DeviceType,
.catch(() => balena.models.device.getManifestBySlug(deviceType)); ): Promise<string | null> {
const init = await import('balena-device-init');
return init.getImageOsVersion(image, manifest);
} }
export function osProgressHandler(step: InitializeEmitter) { export function osProgressHandler(step: InitializeEmitter) {
@ -121,8 +123,8 @@ export function osProgressHandler(step: InitializeEmitter) {
export function getArchAndDeviceType( export function getArchAndDeviceType(
applicationName: string, applicationName: string,
): Promise<{ arch: string; device_type: string }> { ): Bluebird<{ arch: string; device_type: string }> {
return Promise.join( return Bluebird.join(
getApplication(applicationName), getApplication(applicationName),
balena.models.config.getDeviceTypes(), balena.models.config.getDeviceTypes(),
function(app, deviceTypes) { function(app, deviceTypes) {

View File

@ -103,6 +103,7 @@
"any-promise": "^1.3.0", "any-promise": "^1.3.0",
"archiver": "^2.1.0", "archiver": "^2.1.0",
"balena-config-json": "^2.0.0", "balena-config-json": "^2.0.0",
"balena-device-init": "^5.0.0",
"balena-image-manager": "^6.0.0", "balena-image-manager": "^6.0.0",
"balena-preload": "^8.0.0", "balena-preload": "^8.0.0",
"balena-sdk": "^11.0.0", "balena-sdk": "^11.0.0",
@ -155,7 +156,6 @@
"resin-cli-form": "^2.0.0", "resin-cli-form": "^2.0.0",
"resin-cli-visuals": "^1.4.0", "resin-cli-visuals": "^1.4.0",
"resin-compose-parse": "^2.0.0", "resin-compose-parse": "^2.0.0",
"resin-device-init": "^4.0.0",
"resin-doodles": "0.0.1", "resin-doodles": "0.0.1",
"resin-image-fs": "^5.0.2", "resin-image-fs": "^5.0.2",
"resin-multibuild": "^0.9.0", "resin-multibuild": "^0.9.0",

View File

@ -1,6 +1,7 @@
declare module 'resin-device-init' { declare module 'balena-device-init' {
import * as Promise from 'bluebird'; import * as Promise from 'bluebird';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { DeviceType } from 'balena-sdk';
interface OperationState { interface OperationState {
operation: operation:
@ -60,9 +61,23 @@ declare module 'resin-device-init' {
on(event: 'burn', callback: (state: BurnProgress) => void): void; on(event: 'burn', callback: (state: BurnProgress) => void): void;
} }
export function configure(
image: string,
manifest: DeviceType,
config: {},
options?: {},
): Promise<InitializeEmitter>;
export function initialize( export function initialize(
image: string, image: string,
deviceType: string, manifest: DeviceType,
config: {}, config: {},
): Promise<InitializeEmitter>; ): Promise<InitializeEmitter>;
export function getImageOsVersion(
image: string,
manifest: DeviceType,
): Promise<string | null>;
export function getImageManifest(image: string): Promise<DeviceType | null>;
} }