Merge pull request #2420 from balena-io/v13

Release CLI v13
This commit is contained in:
bulldozer-balena[bot] 2021-12-23 19:45:50 +00:00 committed by GitHub
commit 66608b32e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1191 additions and 3074 deletions

View File

@ -267,4 +267,3 @@ gotchas to bear in mind:
replace: `spec: 'tests/**/*.spec.ts',`
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`

View File

@ -36,19 +36,12 @@ const capitanoDoc = {
{
title: 'Fleet',
files: [
'build/commands/apps.js',
'build/commands/fleets.js',
'build/commands/app/index.js',
'build/commands/fleet/index.js',
'build/commands/app/create.js',
'build/commands/fleet/create.js',
'build/commands/app/purge.js',
'build/commands/fleet/purge.js',
'build/commands/app/rename.js',
'build/commands/fleet/rename.js',
'build/commands/app/restart.js',
'build/commands/fleet/restart.js',
'build/commands/app/rm.js',
'build/commands/fleet/rm.js',
],
},

View File

@ -8,10 +8,9 @@ _balena() {
local context state line curcontext="$curcontext"
# Valid top-level completions
main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util )
main_commands=( build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util )
# Sub-completions
api_key_cmds=( generate )
app_cmds=( create purge rename restart rm )
config_cmds=( generate inject read reconfigure write )
device_cmds=( deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown )
devices_cmds=( supported )
@ -44,9 +43,6 @@ _balena_sec_cmds() {
"api-key")
_describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0
;;
"app")
_describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0
;;
"config")
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
;;

View File

@ -7,10 +7,9 @@ _balena_complete()
local cur prev
# Valid top-level completions
main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util"
main_commands="build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util"
# Sub-completions
api_key_cmds="generate"
app_cmds="create purge rename restart rm"
config_cmds="generate inject read reconfigure write"
device_cmds="deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown"
devices_cmds="supported"
@ -38,9 +37,6 @@ _balena_complete()
api-key)
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
;;
app)
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
;;
config)
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
;;

View File

@ -166,19 +166,12 @@ are encouraged to regularly update the balena CLI to the latest version.
- Fleet
- [apps](#apps)
- [fleets](#fleets)
- [app <fleet>](#app-fleet)
- [fleet <fleet>](#fleet-fleet)
- [app create <name>](#app-create-name)
- [fleet create <name>](#fleet-create-name)
- [app purge <fleet>](#app-purge-fleet)
- [fleet purge <fleet>](#fleet-purge-fleet)
- [app rename <fleet> [newname]](#app-rename-fleet-newname)
- [fleet rename <fleet> [newname]](#fleet-rename-fleet-newname)
- [app restart <fleet>](#app-restart-fleet)
- [fleet restart <fleet>](#fleet-restart-fleet)
- [app rm <fleet>](#app-rm-fleet)
- [fleet rm <fleet>](#fleet-rm-fleet)
- Authentication
@ -327,22 +320,6 @@ the API key name
# Fleet
## apps
Renaming notice: The 'apps' command was renamed to 'fleets', and 'apps'
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
of the balena CLI (so that a different 'apps' command can be implemented
in the future). Use 'fleets' instead of 'apps' to avoid this warning.
Find out more at: https://git.io/JRuZr
For command usage, see 'balena help fleets'
### Options
#### -v, --verbose
No-op since release v12.0.0
## fleets
List all your balena fleets.
@ -356,28 +333,6 @@ Examples:
### Options
#### -v, --verbose
No-op since release v12.0.0
## app <fleet>
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
of the balena CLI (so that a different 'app' command can be implemented
in the future). Use 'fleet' instead of 'app' to avoid this warning.
Find out more at: https://git.io/JRuZr
For command usage, see 'balena help fleet'
### Arguments
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
## fleet <fleet>
Display detailed information about a single fleet.
@ -407,32 +362,6 @@ fleet name, slug (preferred), or numeric ID (deprecated)
### Options
## app create <name>
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
of the balena CLI (so that a different 'app' command can be implemented
in the future). Use 'fleet' instead of 'app' to avoid this warning.
Find out more at: https://git.io/JRuZr
For command usage, see 'balena help fleet create'
### Arguments
#### NAME
fleet name
### Options
#### -o, --organization ORGANIZATION
handle of the organization the fleet should belong to
#### -t, --type TYPE
fleet device type (Check available types with `balena devices supported`)
## fleet create <name>
Create a new balena fleet.
@ -474,24 +403,6 @@ handle of the organization the fleet should belong to
fleet device type (Check available types with `balena devices supported`)
## app purge <fleet>
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
of the balena CLI (so that a different 'app' command can be implemented
in the future). Use 'fleet' instead of 'app' to avoid this warning.
Find out more at: https://git.io/JRuZr
For command usage, see 'balena help fleet purge'
### Arguments
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
## fleet purge <fleet>
Purge data from all devices belonging to a fleet.
@ -522,28 +433,6 @@ fleet name, slug (preferred), or numeric ID (deprecated)
### Options
## app rename <fleet> [newName]
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
of the balena CLI (so that a different 'app' command can be implemented
in the future). Use 'fleet' instead of 'app' to avoid this warning.
Find out more at: https://git.io/JRuZr
For command usage, see 'balena help fleet rename'
### Arguments
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
#### NEWNAME
the new name for the fleet
### Options
## fleet rename <fleet> [newName]
Rename a fleet.
@ -581,24 +470,6 @@ the new name for the fleet
### Options
## app restart <fleet>
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
of the balena CLI (so that a different 'app' command can be implemented
in the future). Use 'fleet' instead of 'app' to avoid this warning.
Find out more at: https://git.io/JRuZr
For command usage, see 'balena help fleet restart'
### Arguments
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
## fleet restart <fleet>
Restart all devices belonging to a fleet.
@ -628,28 +499,6 @@ fleet name, slug (preferred), or numeric ID (deprecated)
### Options
## app rm <fleet>
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
of the balena CLI (so that a different 'app' command can be implemented
in the future). Use 'fleet' instead of 'app' to avoid this warning.
Find out more at: https://git.io/JRuZr
For command usage, see 'balena help fleet rm'
### Arguments
#### FLEET
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
#### -y, --yes
answer "yes" to all questions (non interactive use)
## fleet rm <fleet>
Permanently remove a fleet.
@ -795,37 +644,18 @@ Examples:
### Options
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### --app APP
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### -j, --json
produce JSON output instead of tabular output
#### --v13
enable selected balena CLI v13 pre-release features, like the renaming
from "application" to "fleet" in command output
## devices supported
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
The --verbose option may add extra columns/fields to the output. Currently
this includes the "STATE" column which is DEPRECATED and whose values are one
of 'new', 'released' or 'discontinued'. However, 'discontinued' device types
are only listed if the '--discontinued' option is also used, and this option
is also DEPRECATED.
The --json option is recommended when scripting the output of this command,
because the JSON format is less likely to change and it better represents data
types like lists and empty strings (for example, the ALIASES column contains a
@ -835,23 +665,14 @@ list of zero or more values). The 'jq' utility may be helpful in shell scripts
Examples:
$ balena devices supported
$ balena devices supported --verbose
$ balena devices supported -vj
$ balena devices supported --json
### Options
#### --discontinued
include "discontinued" device types (DEPRECATED)
#### -j, --json
produce JSON output instead of tabular output
#### -v, --verbose
add extra columns in the tabular output (DEPRECATED)
## device <uuid>
Show information about a single device.
@ -868,11 +689,6 @@ the device uuid
### Options
#### --v13
enable selected balena CLI v13 pre-release features, like the renaming
from "application" to "fleet" in command output
## device deactivate <uuid>
Deactivate a device.
@ -961,17 +777,9 @@ Examples:
### Options
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### --app APP
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### -y, --yes
@ -1068,17 +876,9 @@ comma-separated list (no blank spaces) of device UUIDs to be moved
### Options
#### --app APP
DEPRECATED alias for -f, --fleet
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
## device os-update <uuid>
@ -1348,7 +1148,7 @@ Examples:
#### FLEET
fleet name or slug
fleet name or slug (preferred)
### Options
@ -1445,13 +1245,6 @@ environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Renaming notice: The 'app' or 'application' words in table headers
or in JSON object keys/properties will be replaced with 'fleet' in
the next major version of the CLI (v13). The --v13 option may be used
to enable the new names already now, and suppress a warning message.
(The --v13 option will be silently ignored in CLI v13.)
Find out more at: https://git.io/JRuZr
Examples:
$ balena envs --fleet myorg/myfleet
@ -1466,17 +1259,9 @@ Examples:
### Options
#### --all
No-op since balena CLI v12.0.0.
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### -c, --config
@ -1490,19 +1275,10 @@ device UUID
produce JSON output instead of tabular output
#### -v, --verbose
produce verbose output
#### -s, --service SERVICE
service name
#### --v13
enable selected balena CLI v13 pre-release features, like the renaming
from "application" to "fleet" in command output
## env rm <id>
Remove a configuration or environment variable from a fleet, device
@ -1631,13 +1407,9 @@ variable value; if omitted, use value from this process' environment
### Options
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -1742,17 +1514,9 @@ Examples:
### Options
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### --app APP
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -1794,17 +1558,9 @@ the key string of the tag
### Options
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### --app APP
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -1857,17 +1613,9 @@ the optional value associated with the tag
### Options
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### --app APP
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -2388,9 +2136,6 @@ are multiple files to inject. See connection profile examples and reference at:
https://www.balena.io/docs/reference/OS/network/2.x/
https://developer.gnome.org/NetworkManager/stable/ref-settings.html
The --device-api-key option is deprecated and will be removed in a future release.
A suitable key is automatically generated or fetched if this option is omitted.
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
@ -2411,7 +2156,6 @@ https://docs.microsoft.com/en-us/windows/wsl/about
Examples:
$ balena os configure ../path/rpi3.img --device 7cf02a6
$ balena os configure ../path/rpi3.img --device 7cf02a6 --device-api-key <existingDeviceKey>
$ balena os configure ../path/rpi3.img --fleet myorg/myfleet
$ balena os configure ../path/rpi3.img --fleet MyFleet --version 2.12.7
$ balena os configure ../path/rpi3.img -f MyFinFleet --device-type raspberrypi3
@ -2429,17 +2173,9 @@ path to a balenaOS image file, e.g. "rpi3.img"
ask advanced configuration questions (when in interactive mode)
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### --app APP
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### --config CONFIG
@ -2465,10 +2201,6 @@ WiFi SSID (network name) (non-interactive configuration)
device UUID
#### -k, --device-api-key DEVICE-API-KEY
custom device API key (DEPRECATED and only supported with balenaOS 2.0.3+)
#### --device-type DEVICE-TYPE
device type slug (e.g. "raspberrypi3") to override the fleet device type
@ -2568,21 +2300,9 @@ Examples:
a balenaOS version
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### --app APP
DEPRECATED alias for -f, --fleet
#### --appUpdatePollInterval APPUPDATEPOLLINTERVAL
supervisor cloud polling interval in minutes (e.g. for variable updates)
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -2616,6 +2336,10 @@ the wifi ssid to use (used only if --network is set to wifi)
the wifi key to use (used only if --network is set to wifi)
#### --appUpdatePollInterval APPUPDATEPOLLINTERVAL
supervisor cloud polling interval in minutes (e.g. for device variables)
#### --provisioning-key-name PROVISIONING-KEY-NAME
custom key name assigned to generated provisioning api key
@ -2796,13 +2520,9 @@ the image file path
### Options
#### -a, --app APP
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### -c, --commit COMMIT
@ -2935,15 +2655,12 @@ compatibility with the standard docker-compose tool, while still allowing a
root .dockerignore file (at the overall project root) to filter files and
folders that are outside service subdirectories.
balena CLI releases older than v12.0.0 also took .gitignore files into account.
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
option if compatibility is required. This option is mutually exclusive with
--multi-dockerignore (-m) and will be removed in the CLI's next major version
release (v13).
balena CLI v11 also took .gitignore files into account. This behavior was
deprecated in CLI v12 and removed in CLI v13. Please use .dockerignore files
instead.
Default .dockerignore patterns
When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
A few default/hardcoded dockerignore patterns are "merged" (in memory) with the
patterns found in the applicable .dockerignore files, in the following order:
```
**/.git
@ -2956,7 +2673,7 @@ patterns found in the applicable .dockerignore files, in the following order:
```
These patterns always apply, whether or not .dockerignore files exist in the
project. If necessary, the effect of the `**/.git` pattern may be modified by
adding counter patterns to the applicable .dockerignore file(s), for example
adding exception patterns to the applicable .dockerignore file(s), for example
`!mysubmodule/.git`. For documentation on pattern format, see:
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore
@ -3064,10 +2781,6 @@ separated by a colon, e.g:
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.
#### -l, --convert-eol
No-op and deprecated since balena CLI v12.0.0
#### --noconvert-eol
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
@ -3076,16 +2789,6 @@ Don't convert line endings from CRLF (Windows format) to LF (Unix format).
Have each service use its own .dockerignore file. See "balena help push".
#### -g, --gitignore
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted.
#### -G, --nogitignore
No-op (default behavior) since balena CLI v12.0.0. See "balena help push".
#### --release-tag RELEASE-TAG
Set release tags if the image build is successful (balenaCloud only). Multiple
@ -3228,15 +2931,12 @@ compatibility with the standard docker-compose tool, while still allowing a
root .dockerignore file (at the overall project root) to filter files and
folders that are outside service subdirectories.
balena CLI releases older than v12.0.0 also took .gitignore files into account.
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
option if compatibility is required. This option is mutually exclusive with
--multi-dockerignore (-m) and will be removed in the CLI's next major version
release (v13).
balena CLI v11 also took .gitignore files into account. This behavior was
deprecated in CLI v12 and removed in CLI v13. Please use .dockerignore files
instead.
Default .dockerignore patterns
When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
A few default/hardcoded dockerignore patterns are "merged" (in memory) with the
patterns found in the applicable .dockerignore files, in the following order:
```
**/.git
@ -3249,7 +2949,7 @@ patterns found in the applicable .dockerignore files, in the following order:
```
These patterns always apply, whether or not .dockerignore files exist in the
project. If necessary, the effect of the `**/.git` pattern may be modified by
adding counter patterns to the applicable .dockerignore file(s), for example
adding exception patterns to the applicable .dockerignore file(s), for example
`!mysubmodule/.git`. For documentation on pattern format, see:
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore
@ -3279,13 +2979,9 @@ the architecture to build for
the type of device this build is for
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### -e, --emulated
@ -3303,16 +2999,6 @@ No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by defau
Hide the image build log output (produce less verbose output)
#### -g, --gitignore
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
until your project can be adapted.
#### -G, --nogitignore
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
#### -m, --multi-dockerignore
Have each service use its own .dockerignore file. See "balena help build".
@ -3325,10 +3011,6 @@ Disable project validation check of 'docker-compose.yml' file in parent folder
Path to a YAML or JSON file with passwords for a private Docker registry
#### -l, --convert-eol
No-op and deprecated since balena CLI v12.0.0
#### --noconvert-eol
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
@ -3458,15 +3140,12 @@ compatibility with the standard docker-compose tool, while still allowing a
root .dockerignore file (at the overall project root) to filter files and
folders that are outside service subdirectories.
balena CLI releases older than v12.0.0 also took .gitignore files into account.
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
option if compatibility is required. This option is mutually exclusive with
--multi-dockerignore (-m) and will be removed in the CLI's next major version
release (v13).
balena CLI v11 also took .gitignore files into account. This behavior was
deprecated in CLI v12 and removed in CLI v13. Please use .dockerignore files
instead.
Default .dockerignore patterns
When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
A few default/hardcoded dockerignore patterns are "merged" (in memory) with the
patterns found in the applicable .dockerignore files, in the following order:
```
**/.git
@ -3479,7 +3158,7 @@ patterns found in the applicable .dockerignore files, in the following order:
```
These patterns always apply, whether or not .dockerignore files exist in the
project. If necessary, the effect of the `**/.git` pattern may be modified by
adding counter patterns to the applicable .dockerignore file(s), for example
adding exception patterns to the applicable .dockerignore file(s), for example
`!mysubmodule/.git`. For documentation on pattern format, see:
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore
@ -3544,16 +3223,6 @@ No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by defau
Hide the image build log output (produce less verbose output)
#### -g, --gitignore
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
until your project can be adapted.
#### -G, --nogitignore
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
#### -m, --multi-dockerignore
Have each service use its own .dockerignore file. See "balena help build".
@ -3566,10 +3235,6 @@ Disable project validation check of 'docker-compose.yml' file in parent folder
Path to a YAML or JSON file with passwords for a private Docker registry
#### -l, --convert-eol
No-op and deprecated since balena CLI v12.0.0
#### --noconvert-eol
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
@ -3676,13 +3341,9 @@ the IP or hostname of device
### Options
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
fleet name, slug (preferred), or numeric ID
fleet name, slug (preferred), or numeric ID (deprecated)
#### -i, --pollInterval POLLINTERVAL
@ -3768,13 +3429,9 @@ enable|disable support access
comma-separated list (no spaces) of device UUIDs
#### -a, --application APPLICATION
DEPRECATED alias for -f, --fleet
#### -f, --fleet FLEET
comma-separated list (no spaces) of fleet names or org/name slugs
comma-separated list (no spaces) of fleet names or slugs (preferred)
#### -t, --duration DURATION

View File

@ -1,181 +0,0 @@
/**
* @license
* Copyright 2016-2021 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 { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import type { Application } from 'balena-sdk';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { appToFleetCmdMsg, warnify } from '../../utils/messages';
interface FlagsDef {
organization?: string;
type?: string; // application device type
help: void;
}
interface ArgsDef {
name: string;
}
export class FleetCreateCmd extends Command {
public static description = stripIndent`
Create a fleet.
Create a new balena fleet.
You can specify the organization the fleet should belong to using
the \`--organization\` option. The organization's handle, not its name,
should be provided. Organization handles can be listed with the
\`balena orgs\` command.
The fleet's default device type is specified with the \`--type\` option.
The \`balena devices supported\` command can be used to list the available
device types.
Interactive dropdowns will be shown for selection if no device type or
organization is specified and there are multiple options to choose from.
If there is a single option to choose from, it will be chosen automatically.
This interactive behavior can be disabled by explicitly specifying a device
type and organization.
`;
public static examples = [
'$ balena fleet create MyFleet',
'$ balena fleet create MyFleet --organization mmyorg',
'$ balena fleet create MyFleet -o myorg --type raspberry-pi',
];
public static args = [
{
name: 'name',
description: 'fleet name',
required: true,
},
];
public static usage = 'fleet create <name>';
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description: 'handle of the organization the fleet should belong to',
}),
type: flags.string({
char: 't',
description:
'fleet device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params, flags: options } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCreateCmd);
// Ascertain device type
const deviceType =
options.type ||
(await (await import('../../utils/patterns')).selectDeviceType());
// Ascertain organization
const organization =
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
let application: Application;
try {
application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
});
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: fleet "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
throw new ExpectedError(
`Error: You are not authorized to create fleets in organization "${organization}".`,
);
}
throw err;
}
// Output
const { isV13 } = await import('../../utils/version');
console.log(
isV13()
? `Fleet created: slug "${application.slug}", device type "${deviceType}"`
: `Fleet created: ${application.slug} (${deviceType}, id ${application.id})`,
);
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk());
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
const { selectOrganization } = await import('../../utils/patterns');
return selectOrganization(organizations);
}
}
}
export default class AppCreateCmd extends FleetCreateCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet create' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet create'
`;
public static examples = [];
public static usage = 'app create <name>';
public static args = FleetCreateCmd.args;
public static flags = FleetCreateCmd.flags;
public static authenticated = FleetCreateCmd.authenticated;
public static primary = FleetCreateCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCreateCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,135 +0,0 @@
/**
* @license
* Copyright 2016-2021 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 type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import type { Release } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import type { DataOutputOptions } from '../../framework';
interface FlagsDef extends DataOutputOptions {
help: void;
}
interface ArgsDef {
fleet: string;
}
export class FleetCmd extends Command {
public static description = stripIndent`
Display information about a single fleet.
Display detailed information about a single fleet.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet MyFleet',
'$ balena fleet myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
...(isV13() ? cf.dataOutputFlags : {}),
};
public static authenticated = true;
public static primary = true;
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params, flags: options } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCmd);
const { getApplication } = await import('../../utils/sdk');
const application = (await getApplication(getBalenaSdk(), params.fleet, {
$expand: {
is_for__device_type: { $select: 'slug' },
should_be_running__release: { $select: 'commit' },
},
})) as ApplicationWithDeviceType & {
should_be_running__release: [Release?];
// For display purposes:
device_type: string;
commit?: string;
};
application.device_type = application.is_for__device_type[0].slug;
application.commit = application.should_be_running__release[0]?.commit;
if (isV13()) {
await this.outputData(
application,
['app_name', 'id', 'device_type', 'slug', 'commit'],
options,
);
} else {
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
console.log(`== ${application.app_name}`);
console.log(
getVisuals().table.vertical(application, [
'id',
'device_type',
'slug',
'commit',
]),
);
}
}
}
export default class AppCmd extends FleetCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet'
`;
public static examples = [];
public static usage = 'app <fleet>';
public static args = FleetCmd.args;
public static flags = FleetCmd.flags;
public static authenticated = FleetCmd.authenticated;
public static primary = FleetCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,115 +0,0 @@
/**
* @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 type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export class FleetPurgeCmd extends Command {
public static description = stripIndent`
Purge data from a fleet.
Purge data from all devices belonging to a fleet.
This will clear the fleet's '/data' directory.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet purge MyFleet',
'$ balena fleet purge myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet purge <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// balena.models.application.purge only accepts a numeric id
// so we must first fetch the app to get it's id,
const application = await getApplication(balena, params.fleet);
try {
await balena.models.application.purge(application.id);
} catch (e) {
if (e.message.toLowerCase().includes('no online device(s) found')) {
// application.purge throws an error if no devices are online
// ignore in this case.
} else {
throw e;
}
}
}
}
export default class AppPurgeCmd extends FleetPurgeCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet purge' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet purge'
`;
public static examples = [];
public static usage = 'app purge <fleet>';
public static args = FleetPurgeCmd.args;
public static flags = FleetPurgeCmd.flags;
public static authenticated = FleetPurgeCmd.authenticated;
public static primary = FleetPurgeCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppPurgeCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,170 +0,0 @@
/**
* @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 type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import type { ApplicationType } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
newName?: string;
}
export class FleetRenameCmd extends Command {
public static description = stripIndent`
Rename a fleet.
Rename a fleet.
Note, if the \`newName\` parameter is omitted, it will be
prompted for interactively.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet rename OldName',
'$ balena fleet rename OldName NewName',
'$ balena fleet rename myorg/oldname NewName',
];
public static args = [
ca.fleetRequired,
{
name: 'newName',
description: 'the new name for the fleet',
},
];
public static usage = 'fleet rename <fleet> [newName]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
const { validateApplicationName } = await import('../../utils/validation');
const { ExpectedError } = await import('../../errors');
const balena = getBalenaSdk();
// Disambiguate target application (if params.params is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, params.fleet, {
$expand: {
application_type: {
$select: ['is_legacy'],
},
},
});
// Check app exists
if (!application) {
throw new ExpectedError(`Error: fleet ${params.fleet} not found.`);
}
// Check app supports renaming
const appType = (application.application_type as ApplicationType[])?.[0];
if (appType.is_legacy) {
throw new ExpectedError(
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
);
}
// Ascertain new name
const newName =
params.newName ||
(await getCliForm().ask({
message: 'Please enter the new name for this fleet:',
type: 'input',
validate: validateApplicationName,
})) ||
'';
// Rename
try {
await balena.models.application.rename(application.id, newName);
} catch (e) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
if ((e.message || '').toLowerCase().includes('unique')) {
throw new ExpectedError(`Error: fleet ${newName} already exists.`);
}
throw e;
}
// Get application again, to be sure of results
const renamedApplication = await balena.models.application.get(
application.id,
);
// Output result
console.log(`Fleet renamed`);
console.log('From:');
console.log(`\tname: ${application.app_name}`);
console.log(`\tslug: ${application.slug}`);
console.log('To:');
console.log(`\tname: ${renamedApplication.app_name}`);
console.log(`\tslug: ${renamedApplication.slug}`);
}
}
export default class AppRenameCmd extends FleetRenameCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet rename' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet rename'
`;
public static examples = [];
public static usage = 'app rename <fleet> [newName]';
public static args = FleetRenameCmd.args;
public static flags = FleetRenameCmd.flags;
public static authenticated = FleetRenameCmd.authenticated;
public static primary = FleetRenameCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,104 +0,0 @@
/**
* @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 type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export class FleetRestartCmd extends Command {
public static description = stripIndent`
Restart a fleet.
Restart all devices belonging to a fleet.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet restart MyFleet',
'$ balena fleet restart myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet restart <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRestartCmd);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const application = await getApplication(balena, params.fleet);
await balena.models.application.restart(application.id);
}
}
export default class AppRestartCmd extends FleetRestartCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet restart' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet restart'
`;
public static examples = [];
public static usage = 'app restart <fleet>';
public static args = FleetRestartCmd.args;
public static flags = FleetRestartCmd.flags;
public static authenticated = FleetRestartCmd.authenticated;
public static primary = FleetRestartCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,116 +0,0 @@
/**
* @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 type { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetCmdMsg,
warnify,
} from '../../utils/messages';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
fleet: string;
}
export class FleetRmCmd extends Command {
public static description = stripIndent`
Remove a fleet.
Permanently remove a fleet.
The --yes option may be used to avoid interactive confirmation.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet rm MyFleet',
'$ balena fleet rm MyFleet --yes',
'$ balena fleet rm myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet rm <fleet>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
const { args: params, flags: options } =
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRmCmd);
const { confirm } = await import('../../utils/patterns');
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// Confirm
await confirm(
options.yes ?? false,
`Are you sure you want to delete fleet ${params.fleet}?`,
);
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const application = await getApplication(balena, params.fleet);
// Remove
await balena.models.application.remove(application.id);
}
}
export default class AppRmCmd extends FleetRmCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleet rm' command
${appToFleetCmdMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleet rm'
`;
public static examples = [];
public static usage = 'app rm <fleet>';
public static args = FleetRmCmd.args;
public static flags = FleetRmCmd.flags;
public static authenticated = FleetRmCmd.authenticated;
public static primary = FleetRmCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRmCmd);
if (process.stderr.isTTY) {
console.error(warnify(appToFleetCmdMsg));
}
await super.run(parserOutput);
}
}

View File

@ -1,152 +0,0 @@
/**
* @license
* Copyright 2016-2021 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 { flags } from '@oclif/command';
import type { Output as ParserOutput } from '@oclif/parser';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { appToFleetCmdMsg, warnify } from '../utils/messages';
import { isV13 } from '../utils/version';
import type { DataSetOutputOptions } from '../framework';
interface ExtendedApplication extends ApplicationWithDeviceType {
device_count: number;
online_devices: number;
device_type?: string;
}
interface FlagsDef extends DataSetOutputOptions {
help: void;
verbose?: boolean;
}
export class FleetsCmd extends Command {
public static description = stripIndent`
List all fleets.
List all your balena fleets.
For detailed information on a particular fleet, use
\`balena fleet <fleet>\`
`;
public static examples = ['$ balena fleets'];
public static usage = 'fleets';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
verbose: flags.boolean({
default: false,
char: 'v',
description: 'No-op since release v12.0.0',
}),
}),
...(isV13() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public static primary = true;
protected useAppWord = false;
public async run(_parserOutput?: ParserOutput<FlagsDef, {}>) {
_parserOutput ||= this.parse<FlagsDef, {}>(FleetsCmd);
const balena = getBalenaSdk();
// Get applications
const applications = (await balena.models.application.getAll({
$select: ['id', 'app_name', 'slug'],
$expand: {
is_for__device_type: { $select: 'slug' },
owns__device: { $select: 'is_online' },
},
})) as ExtendedApplication[];
// Add extended properties
applications.forEach((application) => {
application.device_count = application.owns__device?.length ?? 0;
application.online_devices =
application.owns__device?.filter((d) => d.is_online).length || 0;
application.device_type = application.is_for__device_type[0].slug;
});
if (isV13()) {
await this.outputData(
applications,
[
'id',
'app_name',
'slug',
'device_type',
'device_count',
'online_devices',
],
_parserOutput.flags,
);
} else {
console.log(
getVisuals().table.horizontal(applications, [
'id',
this.useAppWord ? 'app_name' : 'app_name => NAME',
'slug',
'device_type',
'online_devices',
'device_count',
]),
);
}
}
}
const appsToFleetsRenameMsg = appToFleetCmdMsg
.replace(/'app'/g, "'apps'")
.replace(/'fleet'/g, "'fleets'");
export default class AppsCmd extends FleetsCmd {
public static description = stripIndent`
DEPRECATED alias for the 'fleets' command
${appsToFleetsRenameMsg
.split('\n')
.map((l) => `\t\t${l}`)
.join('\n')}
For command usage, see 'balena help fleets'
`;
public static examples = [];
public static usage = 'apps';
public static args = FleetsCmd.args;
public static flags = FleetsCmd.flags;
public static authenticated = FleetsCmd.authenticated;
public static primary = FleetsCmd.primary;
public async run() {
// call this.parse() before deprecation message to parse '-h'
const parserOutput = this.parse<FlagsDef, {}>(AppsCmd);
if (process.stderr.isTTY) {
console.error(warnify(appsToFleetsRenameMsg));
}
this.useAppWord = true;
await super.run(parserOutput);
}
}

View File

@ -22,22 +22,18 @@ import * as cf from '../utils/common-flags';
import * as compose from '../utils/compose';
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
import {
appToFleetFlagMsg,
buildArgDeprecation,
dockerignoreHelp,
registrySecretsHelp,
warnify,
} from '../utils/messages';
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
import { buildProject, composeCliFlags } from '../utils/compose_ts';
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
import { dockerCliFlags } from '../utils/docker';
import { isV13 } from '../utils/version';
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
arch?: string;
deviceType?: string;
application?: string;
fleet?: string;
source?: string; // Not part of command profile - source param copied here.
help: void;
@ -96,7 +92,6 @@ ${dockerignoreHelp}
description: 'the type of device this build is for',
char: 'd',
}),
...(isV13() ? {} : { application: cf.application }),
fleet: cf.fleet,
...composeCliFlags,
...dockerCliFlags,
@ -112,12 +107,7 @@ ${dockerignoreHelp}
BuildCmd,
);
if (options.application && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
await Command.checkLoggedInIf(!!options.application);
await Command.checkLoggedInIf(!!options.fleet);
(await import('events')).defaultMaxListeners = 1000;
@ -161,10 +151,8 @@ ${dockerignoreHelp}
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
// Validate option combinations
if (
(opts.application == null &&
(opts.arch == null || opts.deviceType == null)) ||
(opts.application != null &&
(opts.arch != null || opts.deviceType != null))
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
(opts.fleet != null && (opts.arch != null || opts.deviceType != null))
) {
const { ExpectedError } = await import('../errors');
throw new ExpectedError(
@ -189,9 +177,9 @@ ${dockerignoreHelp}
}
protected async getAppAndResolveArch(opts: FlagsDef) {
if (opts.application) {
if (opts.fleet) {
const { getAppWithArch } = await import('../utils/helpers');
const app = await getAppWithArch(opts.application);
const app = await getAppWithArch(opts.fleet);
opts.arch = app.arch;
opts.deviceType = app.is_for__device_type[0].slug;
return app;
@ -271,7 +259,6 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore, // v13: delete this line
multiDockerignore: composeOpts.multiDockerignore,
});
}

View File

@ -19,18 +19,11 @@ import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo } from '../../utils/messages';
import type { PineDeferred } from 'balena-sdk';
interface FlagsDef {
version: string; // OS version
application?: string;
app?: string; // application alias
fleet?: string;
device?: string;
deviceApiKey?: string;
@ -82,22 +75,10 @@ export default class ConfigGenerateCmd extends Command {
description: 'a balenaOS version',
required: true,
}),
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device'],
},
app: { ...cf.app, exclusive: ['application', 'fleet', 'device'] },
appUpdatePollInterval: flags.string({
description: 'DEPRECATED alias for --updatePollInterval',
}),
}),
fleet: { ...cf.fleet, exclusive: ['application', 'app', 'device'] },
fleet: { ...cf.fleet, exclusive: ['device'] },
device: {
...cf.device,
exclusive: ['application', 'app', 'fleet', 'provisioning-key-name'],
exclusive: ['fleet', 'provisioning-key-name'],
},
deviceApiKey: flags.string({
description:
@ -130,7 +111,7 @@ export default class ConfigGenerateCmd extends Command {
}),
appUpdatePollInterval: flags.string({
description:
'supervisor cloud polling interval in minutes (e.g. for variable updates)',
'supervisor cloud polling interval in minutes (e.g. for device variables)',
}),
'provisioning-key-name': flags.string({
description: 'custom key name assigned to generated provisioning api key',
@ -173,7 +154,7 @@ export default class ConfigGenerateCmd extends Command {
resourceDeviceType = device.is_of__device_type[0].slug;
} else {
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
application = (await getApplication(balena, options.application!, {
application = (await getApplication(balena, options.fleet!, {
$expand: {
is_for__device_type: { $select: 'slug' },
},
@ -188,7 +169,7 @@ export default class ConfigGenerateCmd extends Command {
);
// Check compatibility if application and deviceType provided
if (options.application && options.deviceType) {
if (options.fleet && options.deviceType) {
const appDeviceManifest = await balena.models.device.getManifestBySlug(
resourceDeviceType,
);
@ -198,7 +179,7 @@ export default class ConfigGenerateCmd extends Command {
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
) {
throw new balena.errors.BalenaInvalidDeviceType(
`Device type ${options.deviceType} is incompatible with fleet ${options.application}`,
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
);
}
}
@ -207,7 +188,7 @@ export default class ConfigGenerateCmd extends Command {
// Pass params as an override: if there is any param with exactly the same name as a
// required option, that value is used (and the corresponding question is not asked)
const answers = await getCliForm().run(deviceManifest.options, {
override: options,
override: { ...options, app: options.fleet, application: options.fleet },
});
answers.version = options.version;
answers.provisioningKeyName = options['provisioning-key-name'];
@ -253,18 +234,11 @@ export default class ConfigGenerateCmd extends Command {
protected async validateOptions(options: FlagsDef) {
const { ExpectedError } = await import('../../errors');
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
// Prefer options.application over options.app
delete options.app;
if (options.device == null && options.application == null) {
if (options.device == null && options.fleet == null) {
throw new ExpectedError(this.missingDeviceOrAppMessage);
}
if (!options.application && options.deviceType) {
if (!options.fleet && options.deviceType) {
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
}
}

View File

@ -319,7 +319,6 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore, // v13: delete this line
multiDockerignore: composeOpts.multiDockerignore,
});
builtImagesByService = _.keyBy(builtImages, 'serviceName');

View File

@ -21,15 +21,13 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { appToFleetOutputMsg, warnify } from '../../utils/messages';
import { tryAsInteger } from '../../utils/validation';
import { isV13 } from '../../utils/version';
import type { Application, Release } from 'balena-sdk';
interface ExtendedDevice extends DeviceWithDeviceType {
dashboard_url?: string;
application_name?: string;
fleet: string; // 'org/name' slug
device_type?: string;
commit?: string;
last_seen?: string;
@ -46,7 +44,6 @@ interface ExtendedDevice extends DeviceWithDeviceType {
interface FlagsDef {
help: void;
v13: boolean;
}
interface ArgsDef {
@ -74,17 +71,13 @@ export default class DeviceCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
v13: cf.v13,
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceCmd,
);
const useAppWord = !options.v13 && !isV13();
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
const balena = getBalenaSdk();
@ -121,8 +114,8 @@ export default class DeviceCmd extends Command {
const belongsToApplication =
device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
device.fleet = belongsToApplication?.[0]
? belongsToApplication[0].slug
: 'N/a';
device.device_type = device.is_of__device_type[0].slug;
@ -170,10 +163,6 @@ export default class DeviceCmd extends Command {
);
}
if (useAppWord && process.stderr.isTTY) {
console.error(warnify(appToFleetOutputMsg));
}
console.log(
getVisuals().table.vertical(device, [
`$${device.device_name}$`,
@ -184,7 +173,7 @@ export default class DeviceCmd extends Command {
'ip_address',
'public_address',
'mac_address',
useAppWord ? 'application_name' : 'application_name => FLEET',
'fleet',
'last_seen',
'uuid',
'commit',

View File

@ -19,17 +19,10 @@ import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { applicationIdInfo } from '../../utils/messages';
import { runCommand } from '../../utils/helpers';
import { isV13 } from '../../utils/version';
interface FlagsDef {
application?: string;
app?: string;
fleet?: string;
yes: boolean;
advanced: boolean;
@ -82,12 +75,6 @@ export default class DeviceInitCmd extends Command {
public static usage = 'device init';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
application: cf.application,
app: cf.app,
}),
fleet: cf.fleet,
yes: cf.yes,
advanced: flags.boolean({
@ -130,17 +117,10 @@ export default class DeviceInitCmd extends Command {
const logger = await Command.getLogger();
const balena = getBalenaSdk();
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
// Consolidate application options
options.application ||= options.app || options.fleet;
delete options.app;
// Get application and
const application = (await getApplication(
balena,
options['application'] ||
options.fleet ||
(
await (await import('../../utils/patterns')).selectApplication()
).id,
@ -155,7 +135,7 @@ export default class DeviceInitCmd extends Command {
// Register new device
const deviceUuid = balena.models.device.generateUniqueKey();
console.info(`Registering to ${application.app_name}: ${deviceUuid}`);
console.info(`Registering to ${application.slug}: ${deviceUuid}`);
await balena.models.device.register(application.id, deviceUuid);
const device = await balena.models.device.get(deviceUuid);

View File

@ -27,12 +27,7 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo } from '../../utils/messages';
type ExtendedDevice = PineTypedResult<
Device,
@ -42,8 +37,6 @@ type ExtendedDevice = PineTypedResult<
};
interface FlagsDef {
application?: string;
app?: string;
fleet?: string;
help: void;
}
@ -82,7 +75,6 @@ export default class DeviceMoveCmd extends Command {
public static usage = 'device move <uuid(s)>';
public static flags: flags.Input<FlagsDef> = {
...(isV13() ? {} : { app: cf.app, application: cf.application }),
fleet: cf.fleet,
help: cf.help,
};
@ -94,11 +86,6 @@ export default class DeviceMoveCmd extends Command {
DeviceMoveCmd,
);
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
const balena = getBalenaSdk();
const { tryAsInteger } = await import('../../utils/validation');
@ -132,8 +119,8 @@ export default class DeviceMoveCmd extends Command {
const { getApplication } = await import('../../utils/sdk');
// Get destination application
const application = options.application
? await getApplication(balena, options.application)
const application = options.fleet
? await getApplication(balena, options.fleet)
: await this.interactivelySelectApplication(balena, devices);
// Move each device

View File

@ -75,7 +75,7 @@ export default class DeviceRegisterCmd extends Command {
const application = await getApplication(balena, params.fleet);
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
console.info(`Registering to ${application.app_name}: ${uuid}`);
console.info(`Registering to ${application.slug}: ${uuid}`);
const result = await balena.models.device.register(application.id, uuid);

View File

@ -20,30 +20,20 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
appToFleetOutputMsg,
jsonInfo,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
import type { Application } from 'balena-sdk';
interface ExtendedDevice extends DeviceWithDeviceType {
dashboard_url?: string;
application_name?: string | null;
fleet?: string | null; // 'org/name' slug
device_type?: string | null;
}
interface FlagsDef {
application?: string;
app?: string;
fleet?: string;
help: void;
json: boolean;
v13: boolean;
}
export default class DevicesCmd extends Command {
@ -67,50 +57,25 @@ export default class DevicesCmd extends Command {
public static usage = 'devices';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'v13'],
},
app: { ...cf.app, exclusive: ['application', 'fleet', 'v13'] },
}),
fleet: { ...cf.fleet, exclusive: ['app', 'application'] },
fleet: cf.fleet,
json: cf.json,
help: cf.help,
v13: cf.v13,
};
public static primary = true;
public static authenticated = true;
protected useAppWord = false;
protected hasWarned = false;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
this.useAppWord = !options.fleet && !options.v13 && !isV13();
const balena = getBalenaSdk();
if (
(options.application || options.app) &&
!options.json &&
process.stderr.isTTY
) {
this.hasWarned = true;
console.error(warnify(appToFleetFlagMsg));
}
// Consolidate application options
options.application ||= options.app || options.fleet;
let devices;
if (options.application != null) {
if (options.fleet != null) {
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, options.application);
const application = await getApplication(balena, options.fleet);
devices = (await balena.models.device.getAllByApplication(
application.id,
expandForAppName,
@ -126,7 +91,7 @@ export default class DevicesCmd extends Command {
const belongsToApplication =
device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]?.app_name || null;
device.fleet = belongsToApplication?.[0]?.slug || null;
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
@ -134,16 +99,12 @@ export default class DevicesCmd extends Command {
return device;
});
const jName = this.useAppWord ? 'application_name' : 'fleet_name';
const tName = this.useAppWord ? 'APPLICATION NAME' : 'FLEET';
const fields = [
'id',
'uuid',
'device_name',
'device_type',
options.json
? `application_name => ${jName}`
: `application_name => ${tName}`,
'fleet',
'status',
'is_online',
'supervisor_version',
@ -156,9 +117,6 @@ export default class DevicesCmd extends Command {
const mapped = devices.map((device) => pickAndRename(device, fields));
console.log(JSON.stringify(mapped, null, 4));
} else {
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
console.error(warnify(appToFleetOutputMsg));
}
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(

View File

@ -21,33 +21,18 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
import { isV13 } from '../../utils/version';
interface FlagsDef {
discontinued: boolean;
help: void;
json?: boolean;
verbose?: boolean;
}
const deprecatedInfo = isV13()
? ''
: `
The --verbose option may add extra columns/fields to the output. Currently
this includes the "STATE" column which is DEPRECATED and whose values are one
of 'new', 'released' or 'discontinued'. However, 'discontinued' device types
are only listed if the '--discontinued' option is also used, and this option
is also DEPRECATED.
`
.split('\n')
.join(`\n\t\t`);
export default class DevicesSupportedCmd extends Command {
public static description = stripIndent`
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
${deprecatedInfo}
The --json option is recommended when scripting the output of this command,
because the JSON format is less likely to change and it better represents data
types like lists and empty strings (for example, the ALIASES column contains a
@ -56,8 +41,7 @@ export default class DevicesSupportedCmd extends Command {
`;
public static examples = [
'$ balena devices supported',
'$ balena devices supported --verbose',
'$ balena devices supported -vj',
'$ balena devices supported --json',
];
public static usage = (
@ -66,22 +50,11 @@ export default class DevicesSupportedCmd extends Command {
).trim();
public static flags: flags.Input<FlagsDef> = {
discontinued: flags.boolean({
description: isV13()
? 'No effect (DEPRECATED)'
: 'include "discontinued" device types (DEPRECATED)',
}),
help: cf.help,
json: flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
}),
verbose: flags.boolean({
char: 'v',
description: isV13()
? 'No effect (DEPRECATED)'
: 'add extra columns in the tabular output (DEPRECATED)',
}),
};
public async run() {
@ -95,53 +68,28 @@ export default class DevicesSupportedCmd extends Command {
]);
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
const discontinuedDTs = isV13()
? []
: configDTs.filter((dt) => dt.state === 'DISCONTINUED');
const discontinuedDTsBySlug = _.keyBy(discontinuedDTs, (dt) => dt.slug);
// set of slugs from models.deviceType.getAllSupported() plus slugs of
// discontinued device types as per models.config.getDeviceTypes()
const slugsOfInterest = new Set([
...Object.keys(dtsBySlug),
...Object.keys(discontinuedDTsBySlug),
]);
interface DT {
slug: string;
aliases: string[];
arch: string;
state?: string; // to be removed in CLI v13
name: string;
}
let deviceTypes: DT[] = [];
for (const slug of slugsOfInterest) {
for (const slug of Object.keys(dtsBySlug)) {
const configDT: Partial<typeof configDTs[0]> =
configDTsBySlug[slug] || {};
if (configDT.state === 'DISCONTINUED' && !options.discontinued) {
continue;
}
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
const aliases = (configDT.aliases || []).filter(
(alias) => alias !== slug,
);
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
deviceTypes.push({
slug,
aliases: options.json ? aliases : [aliases.join(', ')],
arch:
(dt.is_of__cpu_architecture as any)?.[0]?.slug ||
configDT.arch ||
'n/a',
// 'BETA' renamed to 'NEW'
// https://www.flowdock.com/app/rulemotion/i-cli/threads/1svvyaf8FAZeSdG4dPJc4kHOvJU
state: isV13()
? undefined
: (configDT.state || 'NEW').replace('BETA', 'NEW'),
name: dt.name || configDT.name || 'N/A',
arch: (dt.is_of__cpu_architecture as any)?.[0]?.slug || 'n/a',
name: dt.name || 'N/A',
});
}
const fields =
options.verbose && !isV13()
? ['slug', 'aliases', 'arch', 'state', 'name']
: ['slug', 'aliases', 'arch', 'name'];
const fields = ['slug', 'aliases', 'arch', 'name'];
deviceTypes = _.sortBy(deviceTypes, fields);
if (options.json) {
console.log(JSON.stringify(deviceTypes, null, 4));

View File

@ -21,15 +21,9 @@ import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
application?: string;
fleet?: string;
device?: string; // device UUID
help: void;
@ -101,11 +95,8 @@ export default class EnvAddCmd extends Command {
public static usage = 'env add <name> [value]';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: { application: { ...cf.application, exclusive: ['fleet', 'device'] } }),
fleet: { ...cf.fleet, exclusive: ['application', 'device'] },
device: { ...cf.device, exclusive: ['application', 'fleet'] },
fleet: { ...cf.fleet, exclusive: ['device'] },
device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
quiet: cf.quiet,
service: cf.service,
@ -117,11 +108,7 @@ export default class EnvAddCmd extends Command {
);
const cmd = this;
if (options.application && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
if (!options.application && !options.device) {
if (!options.fleet && !options.device) {
throw new ExpectedError(
'Either the --fleet or the --device option must be specified',
);
@ -163,11 +150,12 @@ export default class EnvAddCmd extends Command {
}
const varType = isConfigVar ? 'configVar' : 'envVar';
if (options.application) {
for (const app of options.application.split(',')) {
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
for (const app of options.fleet.split(',')) {
try {
await balena.models.application[varType].set(
app,
await getFleetSlug(balena, app),
params.name,
params.value,
);
@ -201,8 +189,8 @@ async function setServiceVars(
params: ArgsDef,
options: FlagsDef,
) {
if (options.application) {
for (const app of options.application.split(',')) {
if (options.fleet) {
for (const app of options.fleet.split(',')) {
for (const service of options.service!.split(',')) {
try {
const serviceId = await getServiceIdForApp(sdk, app, service);

View File

@ -21,42 +21,33 @@ import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
appToFleetOutputMsg,
warnify,
} from '../utils/messages';
import { isV13 } from '../utils/version';
import { applicationIdInfo } from '../utils/messages';
interface FlagsDef {
application?: string;
fleet?: string;
config: boolean;
device?: string; // device UUID
json: boolean;
help: void;
service?: string; // service name
verbose: boolean;
v13: boolean;
}
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
appName?: string | null; // application name
fleet?: string | null; // fleet slug
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
interface DeviceServiceEnvironmentVariableInfo
extends SDK.DeviceServiceEnvironmentVariable {
appName?: string; // application name
fleet?: string; // fleet slug
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
interface ServiceEnvironmentVariableInfo
extends SDK.ServiceEnvironmentVariable {
appName?: string; // application name
fleet?: string; // fleet slug
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
@ -96,8 +87,6 @@ export default class EnvsCmd extends Command {
in case the current user was removed from the fleet by the fleet's owner).
${applicationIdInfo.split('\n').join('\n\t\t')}
${appToFleetOutputMsg.split('\n').join('\n\t\t')}
`;
public static examples = [
@ -115,57 +104,35 @@ export default class EnvsCmd extends Command {
public static usage = 'envs';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
all: flags.boolean({
default: false,
description: 'No-op since balena CLI v12.0.0.',
hidden: true,
}),
application: {
exclusive: ['device', 'fleet', 'v13'],
...cf.application,
},
}),
fleet: { exclusive: ['device', 'application'], ...cf.fleet },
fleet: { ...cf.fleet, exclusive: ['device'] },
config: flags.boolean({
default: false,
char: 'c',
description: 'show configuration variables only',
exclusive: ['service'],
}),
device: { exclusive: ['fleet', 'application'], ...cf.device },
device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
json: cf.json,
verbose: cf.verbose,
service: { exclusive: ['config'], ...cf.service },
v13: cf.v13,
service: { ...cf.service, exclusive: ['config'] },
};
protected useAppWord = false;
protected hasWarned = false;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
this.useAppWord = !options.fleet && !options.v13 && !isV13();
const variables: EnvironmentVariableInfo[] = [];
await Command.checkLoggedIn();
if (options.application && !options.json && process.stderr.isTTY) {
this.hasWarned = true;
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
if (!options.application && !options.device) {
if (!options.fleet && !options.device) {
throw new ExpectedError('Missing --fleet or --device option');
}
const balena = getBalenaSdk();
let appNameOrSlug = options.application;
let fleetSlug: string | undefined = options.fleet
? await (await import('../utils/sdk')).getFleetSlug(balena, options.fleet)
: undefined;
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
if (options.device) {
@ -178,23 +145,23 @@ export default class EnvsCmd extends Command {
);
fullUUID = device.uuid;
if (app) {
appNameOrSlug = app.slug;
fleetSlug = app.slug;
}
}
if (appNameOrSlug && options.service) {
await validateServiceName(balena, options.service, appNameOrSlug);
if (fleetSlug && options.service) {
await validateServiceName(balena, options.service, fleetSlug);
}
variables.push(...(await getAppVars(balena, appNameOrSlug, options)));
variables.push(...(await getAppVars(balena, fleetSlug, options)));
if (fullUUID) {
variables.push(
...(await getDeviceVars(balena, fullUUID, appNameOrSlug, options)),
...(await getDeviceVars(balena, fullUUID, fleetSlug, options)),
);
}
if (!options.json && variables.length === 0) {
const target =
(options.service ? `service "${options.service}" of ` : '') +
(options.application
? `fleet "${options.application}"`
(options.fleet
? `fleet "${options.fleet}"`
: `device "${options.device}"`);
throw new ExpectedError(`No environment variables found for ${target}`);
}
@ -206,24 +173,14 @@ export default class EnvsCmd extends Command {
varArray: EnvironmentVariableInfo[],
options: FlagsDef,
) {
const fields = ['id', 'name', 'value'];
const fields = ['id', 'name', 'value', 'fleet'];
// Replace undefined app names with 'N/A' or null
varArray = varArray.map((i: EnvironmentVariableInfo) => {
if (i.appName) {
// use slug in v13, app name in v12 for compatibility
i.appName = isV13()
? i.appName
: i.appName.substring(i.appName.indexOf('/') + 1);
} else {
i.appName = options.json ? null : 'N/A';
}
i.fleet ||= options.json ? null : 'N/A';
return i;
});
const jName = this.useAppWord ? 'appName' : 'fleetName';
const tName = this.useAppWord ? 'APPLICATION' : 'FLEET';
fields.push(options.json ? `appName => ${jName}` : `appName => ${tName}`);
if (options.device) {
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
}
@ -236,9 +193,6 @@ export default class EnvsCmd extends Command {
const mapped = varArray.map((o) => pickAndRename(o, fields));
this.log(JSON.stringify(mapped, null, 4));
} else {
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
console.error(warnify(appToFleetOutputMsg));
}
this.log(
getVisuals().table.horizontal(
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
@ -252,14 +206,14 @@ export default class EnvsCmd extends Command {
async function validateServiceName(
sdk: SDK.BalenaSDK,
serviceName: string,
appName: string,
fleetSlug: string,
) {
const services = await sdk.models.service.getAllByApplication(appName, {
const services = await sdk.models.service.getAllByApplication(fleetSlug, {
$filter: { service_name: serviceName },
});
if (services.length === 0) {
throw new ExpectedError(
`Service "${serviceName}" not found for fleet "${appName}"`,
`Service "${serviceName}" not found for fleet "${fleetSlug}"`,
);
}
}
@ -273,17 +227,17 @@ async function validateServiceName(
*/
async function getAppVars(
sdk: SDK.BalenaSDK,
appNameOrSlug: string | undefined,
fleetSlug: string | undefined,
options: FlagsDef,
): Promise<EnvironmentVariableInfo[]> {
const appVars: EnvironmentVariableInfo[] = [];
if (!appNameOrSlug) {
if (!fleetSlug) {
return appVars;
}
const vars = await sdk.models.application[
options.config ? 'configVar' : 'envVar'
].getAllByApplication(appNameOrSlug);
fillInInfoFields(vars, appNameOrSlug);
].getAllByApplication(fleetSlug);
fillInInfoFields(vars, fleetSlug);
appVars.push(...vars);
if (!options.config) {
const pineOpts: SDK.PineOptions<SDK.ServiceEnvironmentVariable> = {
@ -299,10 +253,10 @@ async function getAppVars(
};
}
const serviceVars = await sdk.models.service.var.getAllByApplication(
appNameOrSlug,
fleetSlug,
pineOpts,
);
fillInInfoFields(serviceVars, appNameOrSlug);
fillInInfoFields(serviceVars, fleetSlug);
appVars.push(...serviceVars);
}
return appVars;
@ -315,7 +269,7 @@ async function getAppVars(
async function getDeviceVars(
sdk: SDK.BalenaSDK,
fullUUID: string,
appNameOrSlug: string | undefined,
fleetSlug: string | undefined,
options: FlagsDef,
): Promise<EnvironmentVariableInfo[]> {
const printedUUID = options.json ? fullUUID : options.device!;
@ -324,7 +278,7 @@ async function getDeviceVars(
const deviceConfigVars = await sdk.models.device.configVar.getAllByDevice(
fullUUID,
);
fillInInfoFields(deviceConfigVars, appNameOrSlug, printedUUID);
fillInInfoFields(deviceConfigVars, fleetSlug, printedUUID);
deviceVars.push(...deviceConfigVars);
} else {
const pineOpts: SDK.PineOptions<SDK.DeviceServiceEnvironmentVariable> = {
@ -345,13 +299,13 @@ async function getDeviceVars(
fullUUID,
pineOpts,
);
fillInInfoFields(deviceServiceVars, appNameOrSlug, printedUUID);
fillInInfoFields(deviceServiceVars, fleetSlug, printedUUID);
deviceVars.push(...deviceServiceVars);
const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
fullUUID,
);
fillInInfoFields(deviceEnvVars, appNameOrSlug, printedUUID);
fillInInfoFields(deviceEnvVars, fleetSlug, printedUUID);
deviceVars.push(...deviceEnvVars);
}
return deviceVars;
@ -367,7 +321,7 @@ function fillInInfoFields(
| EnvironmentVariableInfo[]
| DeviceServiceEnvironmentVariableInfo[]
| ServiceEnvironmentVariableInfo[],
appNameOrSlug?: string,
fleetSlug?: string,
deviceUUID?: string,
) {
for (const envVar of varArray) {
@ -381,7 +335,7 @@ function fillInInfoFields(
?.installs__service as SDK.Service[]
)[0]?.service_name;
}
envVar.appName = appNameOrSlug;
envVar.fleet = fleetSlug;
envVar.serviceName = envVar.serviceName || '*';
envVar.deviceUUID = deviceUUID || '*';
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,6 +15,135 @@
* limitations under the License.
*/
import { FleetCreateCmd } from '../app/create';
import { flags } from '@oclif/command';
import type { Application } from 'balena-sdk';
export default FleetCreateCmd;
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
organization?: string;
type?: string; // application device type
help: void;
}
interface ArgsDef {
name: string;
}
export default class FleetCreateCmd extends Command {
public static description = stripIndent`
Create a fleet.
Create a new balena fleet.
You can specify the organization the fleet should belong to using
the \`--organization\` option. The organization's handle, not its name,
should be provided. Organization handles can be listed with the
\`balena orgs\` command.
The fleet's default device type is specified with the \`--type\` option.
The \`balena devices supported\` command can be used to list the available
device types.
Interactive dropdowns will be shown for selection if no device type or
organization is specified and there are multiple options to choose from.
If there is a single option to choose from, it will be chosen automatically.
This interactive behavior can be disabled by explicitly specifying a device
type and organization.
`;
public static examples = [
'$ balena fleet create MyFleet',
'$ balena fleet create MyFleet --organization mmyorg',
'$ balena fleet create MyFleet -o myorg --type raspberry-pi',
];
public static args = [
{
name: 'name',
description: 'fleet name',
required: true,
},
];
public static usage = 'fleet create <name>';
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description: 'handle of the organization the fleet should belong to',
}),
type: flags.string({
char: 't',
description:
'fleet device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
FleetCreateCmd,
);
// Ascertain device type
const deviceType =
options.type ||
(await (await import('../../utils/patterns')).selectDeviceType());
// Ascertain organization
const organization =
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
let application: Application;
try {
application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
});
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: fleet "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
throw new ExpectedError(
`Error: You are not authorized to create fleets in organization "${organization}".`,
);
}
throw err;
}
// Output
console.log(
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
);
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk());
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
const { selectOrganization } = await import('../../utils/patterns');
return selectOrganization(organizations);
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,6 +15,89 @@
* limitations under the License.
*/
import { FleetCmd } from '../app';
import type { flags } from '@oclif/command';
import type { Release } from 'balena-sdk';
export default FleetCmd;
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
import { isV14 } from '../../utils/version';
import type { DataOutputOptions } from '../../framework';
interface FlagsDef extends DataOutputOptions {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetCmd extends Command {
public static description = stripIndent`
Display information about a single fleet.
Display detailed information about a single fleet.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet MyFleet',
'$ balena fleet myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
...(isV14() ? cf.dataOutputFlags : {}),
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
FleetCmd,
);
const { getApplication } = await import('../../utils/sdk');
const application = (await getApplication(getBalenaSdk(), params.fleet, {
$expand: {
is_for__device_type: { $select: 'slug' },
should_be_running__release: { $select: 'commit' },
},
})) as ApplicationWithDeviceType & {
should_be_running__release: [Release?];
// For display purposes:
device_type: string;
commit?: string;
};
application.device_type = application.is_for__device_type[0].slug;
application.commit = application.should_be_running__release[0]?.commit;
if (isV14()) {
await this.outputData(
application,
['app_name', 'id', 'device_type', 'slug', 'commit'],
options,
);
} else {
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
console.log(`== ${application.slug}`);
console.log(
getVisuals().table.vertical(application, [
'id',
'device_type',
'slug',
'commit',
]),
);
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 Balena Ltd.
* 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.
@ -15,6 +15,67 @@
* limitations under the License.
*/
import { FleetPurgeCmd } from '../app/purge';
import type { flags } from '@oclif/command';
export default FleetPurgeCmd;
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetPurgeCmd extends Command {
public static description = stripIndent`
Purge data from a fleet.
Purge data from all devices belonging to a fleet.
This will clear the fleet's '/data' directory.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet purge MyFleet',
'$ balena fleet purge myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet purge <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// balena.models.application.purge only accepts a numeric id
// so we must first fetch the app to get it's id,
const application = await getApplication(balena, params.fleet);
try {
await balena.models.application.purge(application.id);
} catch (e) {
if (e.message.toLowerCase().includes('no online device(s) found')) {
// application.purge throws an error if no devices are online
// ignore in this case.
} else {
throw e;
}
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 Balena Ltd.
* 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.
@ -15,6 +15,135 @@
* limitations under the License.
*/
import { FleetRenameCmd } from '../app/rename';
import type { flags } from '@oclif/command';
import type { ApplicationType } from 'balena-sdk';
export default FleetRenameCmd;
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
newName?: string;
}
export default class FleetRenameCmd extends Command {
public static description = stripIndent`
Rename a fleet.
Rename a fleet.
Note, if the \`newName\` parameter is omitted, it will be
prompted for interactively.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet rename OldName',
'$ balena fleet rename OldName NewName',
'$ balena fleet rename myorg/oldname NewName',
];
public static args = [
ca.fleetRequired,
{
name: 'newName',
description: 'the new name for the fleet',
},
];
public static usage = 'fleet rename <fleet> [newName]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
const { validateApplicationName } = await import('../../utils/validation');
const { ExpectedError } = await import('../../errors');
const balena = getBalenaSdk();
// Disambiguate target application (if params.params is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, params.fleet, {
$expand: {
application_type: {
$select: ['is_legacy'],
},
},
});
// Check app exists
if (!application) {
throw new ExpectedError(`Error: fleet ${params.fleet} not found.`);
}
// Check app supports renaming
const appType = (application.application_type as ApplicationType[])?.[0];
if (appType.is_legacy) {
throw new ExpectedError(
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
);
}
// Ascertain new name
const newName =
params.newName ||
(await getCliForm().ask({
message: 'Please enter the new name for this fleet:',
type: 'input',
validate: validateApplicationName,
})) ||
'';
// Check they haven't used slug in new name
if (newName.includes('/')) {
throw new ExpectedError(
`New fleet name cannot include '/', please check that you are not specifying fleet slug.`,
);
}
// Rename
try {
await balena.models.application.rename(application.id, newName);
} catch (e) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
if ((e.message || '').toLowerCase().includes('unique')) {
throw new ExpectedError(`Error: fleet ${newName} already exists.`);
}
// BalenaRequestError: Request error: App name may only contain [a-zA-Z0-9_-].
if ((e.message || '').toLowerCase().includes('name may only contain')) {
throw new ExpectedError(
`Error: new fleet name may only include characters [a-zA-Z0-9_-].`,
);
}
throw e;
}
// Get application again, to be sure of results
const renamedApplication = await balena.models.application.get(
application.id,
);
// Output result
console.log(`Fleet renamed`);
console.log('From:');
console.log(`\tname: ${application.app_name}`);
console.log(`\tslug: ${application.slug}`);
console.log('To:');
console.log(`\tname: ${renamedApplication.app_name}`);
console.log(`\tslug: ${renamedApplication.slug}`);
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 Balena Ltd.
* 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.
@ -15,6 +15,56 @@
* limitations under the License.
*/
import { FleetRestartCmd } from '../app/restart';
import type { flags } from '@oclif/command';
export default FleetRestartCmd;
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetRestartCmd extends Command {
public static description = stripIndent`
Restart a fleet.
Restart all devices belonging to a fleet.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet restart MyFleet',
'$ balena fleet restart myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet restart <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetRestartCmd);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const application = await getApplication(balena, params.fleet);
await balena.models.application.restart(application.id);
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2021 Balena Ltd.
* 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.
@ -15,6 +15,70 @@
* limitations under the License.
*/
import { FleetRmCmd } from '../app/rm';
import type { flags } from '@oclif/command';
export default FleetRmCmd;
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetRmCmd extends Command {
public static description = stripIndent`
Remove a fleet.
Permanently remove a fleet.
The --yes option may be used to avoid interactive confirmation.
${applicationIdInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena fleet rm MyFleet',
'$ balena fleet rm MyFleet --yes',
'$ balena fleet rm myorg/myfleet',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet rm <fleet>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
FleetRmCmd,
);
const { confirm } = await import('../../utils/patterns');
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// Confirm
await confirm(
options.yes ?? false,
`Are you sure you want to delete fleet ${params.fleet}?`,
);
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const application = await getApplication(balena, params.fleet);
// Remove
await balena.models.application.remove(application.id);
}
}

View File

@ -15,6 +15,94 @@
* limitations under the License.
*/
import { FleetsCmd } from './apps';
import { flags } from '@oclif/command';
export default FleetsCmd;
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { isV14 } from '../utils/version';
import type { DataSetOutputOptions } from '../framework';
interface ExtendedApplication extends ApplicationWithDeviceType {
device_count: number;
online_devices: number;
device_type?: string;
}
interface FlagsDef extends DataSetOutputOptions {
help: void;
verbose?: boolean;
}
export default class FleetsCmd extends Command {
public static description = stripIndent`
List all fleets.
List all your balena fleets.
For detailed information on a particular fleet, use
\`balena fleet <fleet>\`
`;
public static examples = ['$ balena fleets'];
public static usage = 'fleets';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public static primary = true;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(FleetsCmd);
const balena = getBalenaSdk();
// Get applications
const applications =
(await balena.models.application.getAllDirectlyAccessible({
$select: ['id', 'app_name', 'slug'],
$expand: {
is_for__device_type: { $select: 'slug' },
owns__device: { $select: 'is_online' },
},
})) as ExtendedApplication[];
// Add extended properties
applications.forEach((application) => {
application.device_count = application.owns__device?.length ?? 0;
application.online_devices =
application.owns__device?.filter((d) => d.is_online).length || 0;
application.device_type = application.is_for__device_type[0].slug;
});
if (isV14()) {
await this.outputData(
applications,
[
'id',
'app_name',
'slug',
'device_type',
'device_count',
'online_devices',
],
options,
);
} else {
console.log(
getVisuals().table.horizontal(applications, [
'id',
'app_name => NAME',
'slug',
'device_type',
'online_devices',
'device_count',
]),
);
}
}
}

View File

@ -21,10 +21,8 @@ import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import { parseAsLocalHostnameOrIp } from '../utils/validation';
import { isV13 } from '../utils/version';
interface FlagsDef {
application?: string;
fleet?: string;
pollInterval?: number;
help?: void;
@ -77,7 +75,6 @@ export default class JoinCmd extends Command {
public static usage = 'join [deviceIpOrHostname]';
public static flags: flags.Input<FlagsDef> = {
...(isV13() ? {} : { application: cf.application }),
fleet: cf.fleet,
pollInterval: flags.integer({
description: 'the interval in minutes to check for updates',
@ -101,7 +98,7 @@ export default class JoinCmd extends Command {
logger,
sdk,
params.deviceIpOrHostname,
options.application || options.fleet,
options.fleet,
options.pollInterval,
);
}

View File

@ -23,19 +23,12 @@ import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo } from '../../utils/messages';
const CONNECTIONS_FOLDER = '/system-connections';
interface FlagsDef {
advanced?: boolean;
application?: string;
app?: string;
fleet?: string;
config?: string;
'config-app-update-poll-interval'?: number;
@ -43,7 +36,6 @@ interface FlagsDef {
'config-wifi-key'?: string;
'config-wifi-ssid'?: string;
device?: string; // device UUID
'device-api-key'?: string;
'device-type'?: string;
help?: void;
version?: string;
@ -66,10 +58,6 @@ interface Answers {
provisioningKeyName?: string;
}
const deviceApiKeyDeprecationMsg = stripIndent`
The --device-api-key option is deprecated and will be removed in a future release.
A suitable key is automatically generated or fetched if this option is omitted.`;
export default class OsConfigureCmd extends Command {
public static description = stripIndent`
Configure a previously downloaded balenaOS image.
@ -93,8 +81,6 @@ export default class OsConfigureCmd extends Command {
https://www.balena.io/docs/reference/OS/network/2.x/
https://developer.gnome.org/NetworkManager/stable/ref-settings.html
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
${applicationIdInfo.split('\n').join('\n\t\t')}
Note: This command is currently not supported on Windows natively. Windows users
@ -105,7 +91,6 @@ export default class OsConfigureCmd extends Command {
public static examples = [
'$ balena os configure ../path/rpi3.img --device 7cf02a6',
'$ balena os configure ../path/rpi3.img --device 7cf02a6 --device-api-key <existingDeviceKey>',
'$ balena os configure ../path/rpi3.img --fleet myorg/myfleet',
'$ balena os configure ../path/rpi3.img --fleet MyFleet --version 2.12.7',
'$ balena os configure ../path/rpi3.img -f MyFinFleet --device-type raspberrypi3',
@ -128,22 +113,7 @@ export default class OsConfigureCmd extends Command {
description:
'ask advanced configuration questions (when in interactive mode)',
}),
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', 'application', 'device'],
},
fleet: { ...cf.fleet, exclusive: ['device'] },
config: flags.string({
description:
'path to a pre-generated config.json file to be injected in the OS image',
@ -163,15 +133,7 @@ export default class OsConfigureCmd extends Command {
'config-wifi-ssid': flags.string({
description: 'WiFi SSID (network name) (non-interactive configuration)',
}),
device: {
exclusive: ['app', 'application', 'fleet', 'provisioning-key-name'],
...cf.device,
},
'device-api-key': flags.string({
char: 'k',
description:
'custom device API key (DEPRECATED and only supported with balenaOS 2.0.3+)',
}),
device: { ...cf.device, exclusive: ['fleet', 'provisioning-key-name'] },
'device-type': flags.string({
description:
'device type slug (e.g. "raspberrypi3") to override the fleet device type',
@ -203,10 +165,6 @@ export default class OsConfigureCmd extends Command {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
OsConfigureCmd,
);
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
await validateOptions(options);
@ -233,7 +191,7 @@ export default class OsConfigureCmd extends Command {
};
deviceTypeSlug = device.is_of__device_type[0].slug;
} else {
app = (await getApplication(balena, options.application!, {
app = (await getApplication(balena, options.fleet!, {
$expand: {
is_for__device_type: { $select: 'slug' },
},
@ -259,7 +217,7 @@ export default class OsConfigureCmd extends Command {
options,
configJson,
);
if (options.application) {
if (options.fleet) {
answers.deviceType = deviceTypeSlug;
}
answers.version =
@ -270,11 +228,7 @@ export default class OsConfigureCmd extends Command {
if (_.isEmpty(configJson)) {
if (device) {
configJson = await generateDeviceConfig(
device,
options['device-api-key'],
answers,
);
configJson = await generateDeviceConfig(device, undefined, answers);
} else {
configJson = await generateApplicationConfig(app!, answers);
}
@ -335,23 +289,16 @@ export default class OsConfigureCmd extends Command {
async function validateOptions(options: FlagsDef) {
// The 'device' and 'application' options are declared "exclusive" in the oclif
// flag definitions above, so oclif will enforce that they are not both used together.
if (!options.device && !options.application) {
if (!options.device && !options.fleet) {
throw new ExpectedError(
"Either the '--device' or the '--fleet' option must be provided",
);
}
if (!options.application && options['device-type']) {
if (!options.fleet && options['device-type']) {
throw new ExpectedError(
"The '--device-type' option can only be used in conjunction with the '--fleet' option",
);
}
if (options['device-api-key']) {
console.error(stripIndent`
-------------------------------------------------------------------------------------------
Warning: ${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t\t')}
-------------------------------------------------------------------------------------------
`);
}
await Command.checkLoggedIn();
}
@ -401,7 +348,7 @@ async function checkDeviceTypeCompatibility(
const helpers = await import('../../utils/helpers');
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
throw new ExpectedError(
`Device type ${options['device-type']} is incompatible with fleet ${options.application}`,
`Device type ${options['device-type']} is incompatible with fleet ${options.fleet}`,
);
}
}
@ -426,7 +373,13 @@ async function askQuestionsForDeviceType(
options: FlagsDef,
configJson?: import('../../utils/config').ImgConfig,
): Promise<Answers> {
const answerSources: any[] = [camelifyConfigOptions(options)];
const answerSources: any[] = [
{
...camelifyConfigOptions(options),
app: options.fleet,
application: options.fleet,
},
];
const defaultAnswers: Partial<Answers> = {};
const questions: any = deviceType.options;
let extraOpts: { override: object } | undefined;

View File

@ -24,15 +24,10 @@ import {
getVisuals,
stripIndent,
} from '../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../utils/messages';
import { applicationIdInfo } from '../utils/messages';
import type { DockerConnectionCliFlags } from '../utils/docker';
import { dockerConnectionCliFlags } from '../utils/docker';
import { parseAsInteger } from '../utils/validation';
import { isV13 } from '../utils/version';
import { flags } from '@oclif/command';
import * as _ from 'lodash';
@ -40,7 +35,6 @@ import type { Application, BalenaSDK, PineExpand, Release } from 'balena-sdk';
import type { Preloader } from 'balena-preload';
interface FlagsDef extends DockerConnectionCliFlags {
app?: string;
fleet?: string;
commit?: string;
'splash-image'?: string;
@ -99,7 +93,6 @@ export default class PreloadCmd extends Command {
public static usage = 'preload <image>';
public static flags: flags.Input<FlagsDef> = {
...(isV13() ? {} : { app: cf.application }),
fleet: cf.fleet,
commit: flags.string({
description: `\
@ -163,11 +156,6 @@ Can be repeated to add multiple certificates.\
PreloadCmd,
);
if (options.app && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.app ||= options.fleet;
const balena = getBalenaSdk();
const balenaPreload = await import('balena-preload');
const visuals = getVisuals();
@ -194,15 +182,9 @@ Can be repeated to add multiple certificates.\
// balena-preload currently does not work with numerical app IDs
// Load app here, and use app slug from hereon
if (options.app && !options.app.includes('/')) {
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../utils/sdk');
const application = await getApplication(balena, options.app);
if (!application) {
throw new ExpectedError(`Fleet not found: ${options.app}`);
}
options.app = application.slug;
}
const fleetSlug: string | undefined = options.fleet
? await (await import('../utils/sdk')).getFleetSlug(balena, options.fleet)
: undefined;
const progressBars: {
[key: string]: ReturnType<typeof getVisuals>['Progress'];
@ -238,15 +220,12 @@ Can be repeated to add multiple certificates.\
? 'latest'
: options.commit;
const image = params.image;
const appId = options.app;
const splashImage = options['splash-image'];
const additionalSpace = options['additional-space'];
const dontCheckArch = options['dont-check-arch'] || false;
const pinDevice = options['pin-device-to-release'] || false;
if (dontCheckArch && !appId) {
if (dontCheckArch && !fleetSlug) {
throw new ExpectedError(
'You need to specify a fleet if you disable the architecture check.',
);
@ -265,7 +244,7 @@ Can be repeated to add multiple certificates.\
const preloader = new balenaPreload.Preloader(
null,
docker,
appId,
fleetSlug,
commit,
image,
splashImage,
@ -309,7 +288,7 @@ Can be repeated to add multiple certificates.\
preloader.on('error', reject);
resolve(
this.prepareAndPreload(preloader, balena, {
appId,
appId: fleetSlug,
commit,
pinDevice,
}),
@ -364,8 +343,8 @@ Can be repeated to add multiple certificates.\
} catch {
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
}
return (await balena.models.application.getAll({
$select: ['id', 'app_name', 'should_track_latest_release'],
return (await balena.models.application.getAllDirectlyAccessible({
$select: ['id', 'slug', 'should_track_latest_release'],
$expand: this.applicationExpandOptions,
$filter: {
// get the apps that are of the same arch as the device type of the image
@ -408,7 +387,7 @@ Can be repeated to add multiple certificates.\
},
},
},
$orderby: 'app_name asc',
$orderby: 'slug asc',
})) as Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
@ -437,7 +416,7 @@ Can be repeated to add multiple certificates.\
message: 'Select a fleet',
type: 'list',
choices: applications.map((app) => ({
name: app.app_name,
name: app.slug,
value: app,
})),
});
@ -512,7 +491,7 @@ Would you like to disable automatic updates for this fleet now?\
});
}
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string | number) {
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string) {
const { getApplication } = await import('../utils/sdk');
return (await getApplication(balenaSdk, appId, {

View File

@ -22,7 +22,6 @@ import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
import type { BalenaSDK } from 'balena-sdk';
import { ExpectedError, instanceOf } from '../errors';
import { isV13 } from '../utils/version';
import { RegistrySecrets } from 'resin-multibuild';
import { lowercaseIfSlug } from '../utils/normalization';
import {
@ -47,14 +46,11 @@ interface FlagsDef {
pull: boolean;
'noparent-check': boolean;
'registry-secrets'?: string;
gitignore?: boolean; // v13: delete this flag
nogitignore?: boolean; // v13: delete this flag
nolive: boolean;
detached: boolean;
service?: string[];
system: boolean;
env?: string[];
'convert-eol'?: boolean;
'noconvert-eol': boolean;
'multi-dockerignore': boolean;
'release-tag'?: string[];
@ -218,16 +214,6 @@ export default class PushCmd extends Command {
`,
multiple: true,
}),
...(isV13()
? {}
: {
'convert-eol': flags.boolean({
description: 'No-op and deprecated since balena CLI v12.0.0',
char: 'l',
hidden: true,
default: false,
}),
}),
'noconvert-eol': flags.boolean({
description: `Don't convert line endings from CRLF (Windows format) to LF (Unix format).`,
default: false,
@ -237,28 +223,7 @@ export default class PushCmd extends Command {
'Have each service use its own .dockerignore file. See "balena help push".',
char: 'm',
default: false,
exclusive: ['gitignore'], // v13: delete this line
}),
...(isV13()
? {}
: {
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted.`,
char: 'g',
default: false,
exclusive: ['multi-dockerignore'],
}),
nogitignore: flags.boolean({
description:
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
char: 'G',
hidden: true,
default: false,
}),
}),
'release-tag': flags.string({
description: stripIndent`
Set release tags if the image build is successful (balenaCloud only). Multiple
@ -378,7 +343,6 @@ export default class PushCmd extends Command {
source: options.source,
auth: token,
baseUrl,
nogitignore: !options.gitignore, // v13: delete this line
sdk,
opts,
};
@ -422,7 +386,6 @@ export default class PushCmd extends Command {
multiDockerignore: options['multi-dockerignore'],
nocache: options.nocache,
pull: options.pull,
nogitignore: !options.gitignore, // v13: delete this line
noParentCheck: options['noparent-check'],
nolive: options.nolive,
detached: options.detached,

View File

@ -49,7 +49,7 @@ export default class ReleasesCmd extends Command {
public static args = [
{
name: 'fleet',
description: 'fleet name or slug',
description: 'fleet name or slug (preferred)',
required: true,
},
];
@ -69,9 +69,10 @@ export default class ReleasesCmd extends Command {
];
const balena = getBalenaSdk();
const { getFleetSlug } = await import('../utils/sdk');
const releases = await balena.models.release.getAllByApplication(
params.fleet,
await getFleetSlug(balena, params.fleet),
{ $select: fields },
);

View File

@ -20,15 +20,9 @@ import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getCliUx, stripIndent } from '../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../utils/messages';
import { isV13 } from '../utils/version';
import { applicationIdInfo } from '../utils/messages';
interface FlagsDef {
application?: string;
fleet?: string;
device?: string;
duration?: string;
@ -77,11 +71,10 @@ export default class SupportCmd extends Command {
description: 'comma-separated list (no spaces) of device UUIDs',
char: 'd',
}),
...(isV13() ? {} : { application: cf.application }),
fleet: {
...cf.fleet,
description:
'comma-separated list (no spaces) of fleet names or org/name slugs',
'comma-separated list (no spaces) of fleet names or slugs (preferred)',
},
duration: flags.string({
description:
@ -98,18 +91,13 @@ export default class SupportCmd extends Command {
SupportCmd,
);
if (options.application && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.fleet;
const balena = getBalenaSdk();
const ux = getCliUx();
const enabling = params.action === 'enable';
// Validation
if (!options.device && !options.application) {
if (!options.device && !options.fleet) {
throw new ExpectedError('At least one device or fleet must be specified');
}
@ -125,7 +113,7 @@ export default class SupportCmd extends Command {
const expiryTs = Date.now() + this.parseDuration(duration);
const deviceUuids = options.device?.split(',') || [];
const appNames = options.application?.split(',') || [];
const appNames = options.fleet?.split(',') || [];
const enablingMessage = 'Enabling support access for';
const disablingMessage = 'Disabling support access for';
@ -142,14 +130,17 @@ export default class SupportCmd extends Command {
ux.action.stop();
}
const { getFleetSlug } = await import('../utils/sdk');
// Process applications
for (const appName of appNames) {
const slug = await getFleetSlug(balena, appName);
if (enabling) {
ux.action.start(`${enablingMessage} fleet ${appName}`);
await balena.models.application.grantSupportAccess(appName, expiryTs);
ux.action.start(`${enablingMessage} fleet ${slug}`);
await balena.models.application.grantSupportAccess(slug, expiryTs);
} else if (params.action === 'disable') {
ux.action.start(`${disablingMessage} fleet ${appName}`);
await balena.models.application.revokeSupportAccess(appName);
ux.action.start(`${disablingMessage} fleet ${slug}`);
await balena.models.application.revokeSupportAccess(slug);
}
ux.action.stop();
}

View File

@ -19,16 +19,9 @@ import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
app?: string;
application?: string;
fleet?: string;
device?: string;
release?: string;
@ -67,29 +60,17 @@ export default class TagRmCmd extends Command {
public static usage = 'tag rm <tagKey>';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device', 'release'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', 'application', 'device', 'release'],
exclusive: ['device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'fleet', 'release'],
exclusive: ['fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'fleet', 'device'],
exclusive: ['fleet', 'device'],
},
help: cf.help,
};
@ -101,25 +82,20 @@ export default class TagRmCmd extends Command {
TagRmCmd,
);
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
if (!options.fleet && !options.device && !options.release) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(TagRmCmd.missingResourceMessage);
}
const { tryAsInteger } = await import('../../utils/validation');
if (options.application) {
const { getTypedApplicationIdentifier } = await import('../../utils/sdk');
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.remove(
await getTypedApplicationIdentifier(balena, options.application),
await getFleetSlug(balena, options.fleet),
params.tagKey,
);
}

View File

@ -19,16 +19,9 @@ import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
app?: string;
application?: string;
fleet?: string;
device?: string;
release?: string;
@ -80,29 +73,17 @@ export default class TagSetCmd extends Command {
public static usage = 'tag set <tagKey> [value]';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device', 'release'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', 'application', 'device', 'release'],
exclusive: ['device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'fleet', 'release'],
exclusive: ['fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'fleet', 'device'],
exclusive: ['fleet', 'device'],
},
help: cf.help,
};
@ -114,15 +95,10 @@ export default class TagSetCmd extends Command {
TagSetCmd,
);
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
if (!options.fleet && !options.device && !options.release) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(TagSetCmd.missingResourceMessage);
}
@ -131,10 +107,10 @@ export default class TagSetCmd extends Command {
const { tryAsInteger } = await import('../../utils/validation');
if (options.application) {
const { getTypedApplicationIdentifier } = await import('../../utils/sdk');
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.set(
await getTypedApplicationIdentifier(balena, options.application),
await getFleetSlug(balena, options.fleet),
params.tagKey,
params.value,
);

View File

@ -20,16 +20,9 @@ import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import {
applicationIdInfo,
appToFleetFlagMsg,
warnify,
} from '../utils/messages';
import { isV13 } from '../utils/version';
import { applicationIdInfo } from '../utils/messages';
interface FlagsDef {
app?: string;
application?: string;
fleet?: string;
device?: string;
release?: string;
@ -56,29 +49,17 @@ export default class TagsCmd extends Command {
public static usage = 'tags';
public static flags: flags.Input<FlagsDef> = {
...(isV13()
? {}
: {
application: {
...cf.application,
exclusive: ['app', 'fleet', 'device', 'release'],
},
app: {
...cf.app,
exclusive: ['application', 'fleet', 'device', 'release'],
},
}),
fleet: {
...cf.fleet,
exclusive: ['app', 'application', 'device', 'release'],
exclusive: ['device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'fleet', 'release'],
exclusive: ['fleet', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'fleet', 'device'],
exclusive: ['fleet', 'device'],
},
help: cf.help,
};
@ -88,15 +69,10 @@ export default class TagsCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(TagsCmd);
if ((options.application || options.app) && process.stderr.isTTY) {
console.error(warnify(appToFleetFlagMsg));
}
options.application ||= options.app || options.fleet;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
if (!options.fleet && !options.device && !options.release) {
throw new ExpectedError(this.missingResourceMessage);
}
@ -104,10 +80,10 @@ export default class TagsCmd extends Command {
let tags;
if (options.application) {
const { getTypedApplicationIdentifier } = await import('../utils/sdk');
if (options.fleet) {
const { getFleetSlug } = await import('../utils/sdk');
tags = await balena.models.application.tags.getAllByApplication(
await getTypedApplicationIdentifier(balena, options.application),
await getFleetSlug(balena, options.fleet),
);
}
if (options.device) {

View File

@ -131,6 +131,13 @@ Please use "balena ${alternative}" instead.`);
'local scan': [replaced, 'scan', 'v11.0.0'],
'local ssh': [replaced, 'ssh', 'v11.0.0'],
'local stop': [removed, stopAlternative, 'v11.0.0'],
app: [replaced, 'fleet', 'v13.0.0'],
apps: [replaced, 'fleets', 'v13.0.0'],
'app create': [replaced, 'fleet create', 'v13.0.0'],
'app purge': [replaced, 'fleet purge', 'v13.0.0'],
'app rename': [replaced, 'fleet rename', 'v13.0.0'],
'app restart': [replaced, 'fleet restart', 'v13.0.0'],
'app rm': [replaced, 'fleet rm', 'v13.0.0'],
};
let cmd: string | undefined;
if (argvSlice.length > 1) {

View File

@ -253,13 +253,13 @@ export async function getFormattedOsVersions(
const sdk = getBalenaSdk();
let slug = deviceType;
let versionsByDT: SDK.OsVersionsByDeviceType =
await sdk.models.hostapp.getAvailableOsVersions([slug]);
await sdk.models.os.getAvailableOsVersions([slug]);
// if slug is an alias, fetch the real slug
if (!versionsByDT[slug]?.length) {
// unaliasDeviceType() produces a nice error msg if slug is invalid
slug = await unaliasDeviceType(sdk, slug);
if (slug !== deviceType) {
versionsByDT = await sdk.models.hostapp.getAvailableOsVersions([slug]);
versionsByDT = await sdk.models.os.getAvailableOsVersions([slug]);
}
}
const versions: SDK.OsVersion[] = (versionsByDT[slug] || [])

View File

@ -19,35 +19,13 @@ import { flags } from '@oclif/command';
import { stripIndent } from './lazy';
import { lowercaseIfSlug } from './normalization';
import { isV13 } from './version';
import { isV14 } from './version';
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
import type { DataOutputOptions, DataSetOutputOptions } from '../framework';
export const v13: IBooleanFlag<boolean> = flags.boolean({
description: stripIndent`\
enable selected balena CLI v13 pre-release features, like the renaming
from "application" to "fleet" in command output`,
default: false,
});
export const application = flags.string({
char: 'a',
description: 'DEPRECATED alias for -f, --fleet',
parse: lowercaseIfSlug,
});
// TODO: Consider remove second alias 'app' when we can, to simplify.
export const app = flags.string({
description: 'DEPRECATED alias for -f, --fleet',
parse: lowercaseIfSlug,
});
export const fleet = flags.string({
char: 'f',
description: isV13()
? 'fleet name, slug (preferred), or numeric ID (deprecated)'
: // avoid the '(deprecated)' remark in v12 while cf.application and
// cf.app are also described as deprecated, to avoid the impression
// that cf.fleet is deprecated as well.
'fleet name, slug (preferred), or numeric ID',
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
parse: lowercaseIfSlug,
});
@ -114,12 +92,14 @@ export const deviceType = flags.string({
required: true,
});
export const deviceTypeIgnored = flags.string({
description: 'ignored - no longer required',
char: 't',
required: false,
hidden: isV13(),
});
export const deviceTypeIgnored = isV14()
? undefined
: flags.string({
description: 'ignored - no longer required',
char: 't',
required: false,
hidden: true,
});
export const json: IBooleanFlag<boolean> = flags.boolean({
char: 'j',

View File

@ -51,7 +51,6 @@ export interface ComposeOpts {
dockerfilePath?: string;
inlineLogs?: boolean;
multiDockerignore: boolean;
nogitignore: boolean; // v13: delete this line
noParentCheck: boolean;
projectName: string;
projectPath: string;
@ -63,12 +62,9 @@ export interface ComposeCliFlags {
dockerfile?: string;
logs: boolean;
nologs: boolean;
gitignore?: boolean; // v13: delete this line
nogitignore?: boolean; // v13: delete this line
'multi-dockerignore': boolean;
'noparent-check': boolean;
'registry-secrets'?: RegistrySecrets;
'convert-eol': boolean;
'noconvert-eol': boolean;
projectName?: string;
}
@ -102,6 +98,5 @@ interface TarDirectoryOptions {
composition?: Composition;
convertEol?: boolean;
multiDockerignore?: boolean;
nogitignore: boolean; // v13: delete this line
preFinalizeCallback?: (pack: Pack) => void | Promise<void>;
}

View File

@ -16,21 +16,13 @@
*/
import * as path from 'path';
import { ExpectedError } from '../errors';
import { getChalk } from './lazy';
import { isV13 } from './version';
/**
* @returns Promise<{import('./compose-types').ComposeOpts}>
*/
export function generateOpts(options) {
const { promises: fs } = require('fs');
if (!isV13() && options.gitignore && options['multi-dockerignore']) {
throw new ExpectedError(
'The --gitignore and --multi-dockerignore options cannot be used together',
);
}
return fs.realpath(options.source || '.').then((projectPath) => ({
projectName: options.projectName,
projectPath,
@ -38,7 +30,6 @@ export function generateOpts(options) {
convertEol: !options['noconvert-eol'],
dockerfilePath: options.dockerfile,
multiDockerignore: !!options['multi-dockerignore'],
nogitignore: !options.gitignore, // v13: delete this line
noParentCheck: options['noparent-check'],
}));
}
@ -89,92 +80,6 @@ export function createProject(
};
}
/**
* This is the CLI v10 / v11 "original" tarDirectory function. It is still
* around for the benefit of the `--gitignore` option, but is expected to be
* deleted in CLI v13.
* @param {string} dir Source directory
* @param {import('./compose-types').TarDirectoryOptions} param
* @returns {Promise<import('stream').Readable>}
*
* v13: delete this function
*/
export async function originalTarDirectory(dir, param) {
let {
preFinalizeCallback = null,
convertEol = false,
nogitignore = false,
} = param;
if (convertEol == null) {
convertEol = false;
}
const Bluebird = require('bluebird');
const tar = require('tar-stream');
const klaw = require('klaw');
const { promises: fs } = require('fs');
const streamToPromise = require('stream-to-promise');
const { printGitignoreWarn } = require('./compose_ts');
const { FileIgnorer, IgnoreFileType } = require('./ignore');
const { toPosixPath } = require('resin-multibuild').PathUtils;
let readFile;
if (process.platform === 'win32') {
const { readFileWithEolConversion } = require('./eol-conversion');
readFile = (file) => readFileWithEolConversion(file, convertEol);
} else {
({ readFile } = fs);
}
const getFiles = () =>
Bluebird.resolve(streamToPromise(klaw(dir)))
// @ts-ignore
.filter((item) => !item.stats.isDirectory())
// @ts-ignore
.map((item) => item.path);
const ignore = new FileIgnorer(dir);
const pack = tar.pack();
const ignoreFiles = {};
return getFiles()
.each(function (file) {
const type = ignore.getIgnoreFileType(path.relative(dir, file));
if (type != null) {
ignoreFiles[type] = ignoreFiles[type] || [];
ignoreFiles[type].push(path.resolve(dir, file));
return ignore.addIgnoreFile(file, type);
}
})
.tap(() => {
if (!nogitignore) {
printGitignoreWarn(
(ignoreFiles[IgnoreFileType.DockerIgnore] || [])[0] || '',
ignoreFiles[IgnoreFileType.GitIgnore] || [],
);
}
})
.filter(ignore.filter)
.map(function (file) {
const relPath = path.relative(path.resolve(dir), file);
return Promise.all([relPath, fs.stat(file), readFile(file)]).then(
([filename, stats, data]) =>
pack.entry(
{
name: toPosixPath(filename),
mtime: stats.mtime,
size: stats.size,
mode: stats.mode,
},
data,
),
);
})
.then(() => preFinalizeCallback?.(pack))
.then(function () {
pack.finalize();
return pack;
});
}
/**
* @param {string} apiEndpoint
* @param {string} auth

View File

@ -43,7 +43,6 @@ import {
import type { DeviceInfo } from './device/api';
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
import Logger = require('./logger');
import { isV13 } from './version';
import { exists } from './which';
const allowedContractTypes = ['sw.application', 'sw.block'];
@ -105,8 +104,6 @@ export async function applyReleaseTagKeysAndValues(
const LOG_LENGTH_MAX = 512 * 1024; // 512KB
const compositionFileNames = ['docker-compose.yml', 'docker-compose.yaml'];
const hr =
'----------------------------------------------------------------------';
/**
* high-level function resolving a project and creating a composition out
@ -257,7 +254,6 @@ export interface BuildProjectOpts {
inlineLogs?: boolean;
convertEol: boolean;
dockerfilePath?: string;
nogitignore: boolean; // v13: delete this line
multiDockerignore: boolean;
}
@ -748,43 +744,19 @@ export function isBuildConfig(
* Create a tar stream out of the local filesystem at the given directory,
* while optionally applying file filters such as '.dockerignore' and
* optionally converting text file line endings (CRLF to LF).
* @param dir Source directory
* @param param Options
* @returns Readable stream
* @param dir Project directory (the '--source' command line option)
* @param param TarDirectoryOptions
* @returns Readable stream (to be sent to the Docker Engine)
*/
export async function tarDirectory(
dir: string,
param: TarDirectoryOptions,
): Promise<import('stream').Readable> {
const { nogitignore = false } = param; // v13: delete this line
if (isV13() || nogitignore) {
return newTarDirectory(dir, param);
} else {
return (await import('./compose')).originalTarDirectory(dir, param);
}
}
/**
* Create a tar stream out of the local filesystem at the given directory,
* while optionally applying file filters such as '.dockerignore' and
* optionally converting text file line endings (CRLF to LF).
* @param dir Source directory
* @param param Options
* @returns Readable stream
*/
async function newTarDirectory(
dir: string,
{
composition,
convertEol = false,
multiDockerignore = false,
nogitignore = false, // v13: delete this line
preFinalizeCallback,
}: TarDirectoryOptions,
): Promise<import('stream').Readable> {
if (!isV13()) {
require('assert').strict.equal(nogitignore, true);
}
const { filterFilesWithDockerignore } = await import('./ignore');
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
@ -905,48 +877,6 @@ function printDockerignoreWarn(
}
}
/**
* Print a deprecation warning if any '.gitignore' or '.dockerignore' file is
* found and the --gitignore (-g) option has been provided (v11 compatibility).
* @param dockerignoreFile Absolute path to a .dockerignore file
* @param gitignoreFiles Array of absolute paths to .gitginore files
*
* v13: delete this function
*/
export function printGitignoreWarn(
dockerignoreFile: string,
gitignoreFiles: string[],
) {
if (isV13()) {
return;
}
const ignoreFiles = [dockerignoreFile, ...gitignoreFiles].filter((e) => e);
if (ignoreFiles.length === 0) {
return;
}
const msg = [' ', hr, 'Using file ignore patterns from:'];
msg.push(...ignoreFiles.map((e) => `* ${e}`));
if (gitignoreFiles.length) {
msg.push(stripIndent`
.gitignore files are being considered because the --gitignore option was used.
This option is deprecated and will be removed in the next major version release.
For more information, see 'balena help ${Logger.command}'.
`);
msg.push(hr);
Logger.getLogger().logWarn(msg.join('\n'));
} else if (dockerignoreFile && process.platform === 'win32') {
msg.push(stripIndent`
The --gitignore option was used, but no .gitignore files were found.
The --gitignore option is deprecated and will be removed in the next major
version release. It prevents the use of a better dockerignore parser and
filter library that fixes several issues on Windows and improves compatibility
with 'docker build'. For more information, see 'balena help ${Logger.command}'.
`);
msg.push(hr);
Logger.getLogger().logWarn(msg.join('\n'));
}
}
/**
* Check whether the "build secrets" feature is being used and, if so,
* verify that the target docker daemon is balenaEngine. If the
@ -1729,21 +1659,6 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
description:
'Hide the image build log output (produce less verbose output)',
}),
...(isV13()
? {}
: {
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
until your project can be adapted.`,
char: 'g',
}),
nogitignore: flags.boolean({
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
char: 'G',
}),
}),
'multi-dockerignore': flags.boolean({
description:
'Have each service use its own .dockerignore file. See "balena help build".',
@ -1758,10 +1673,6 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
'Path to a YAML or JSON file with passwords for a private Docker registry',
char: 'R',
}),
'convert-eol': flags.boolean({
description: 'No-op and deprecated since balena CLI v12.0.0',
char: 'l',
}),
'noconvert-eol': flags.boolean({
description:
"Don't convert line endings from CRLF (Windows format) to LF (Unix format).",

View File

@ -59,7 +59,6 @@ export interface DeviceDeployOptions {
registrySecrets: RegistrySecrets;
multiDockerignore: boolean;
nocache: boolean;
nogitignore: boolean; // v13: delete this line
noParentCheck: boolean;
nolive: boolean;
pull: boolean;
@ -184,7 +183,6 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
convertEol: opts.convertEol,
dockerfilePath: opts.dockerfilePath,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore, // v13: delete this line
noParentCheck: opts.noParentCheck,
projectName: 'local',
projectPath: opts.source,
@ -204,7 +202,6 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
composition: project.composition,
convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore, // v13: delete this line
});
globalLogger.logDebug(`Tarring complete in ${Date.now() - tarStartTime} ms`);
@ -435,7 +432,6 @@ export async function rebuildSingleTask(
composition,
convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore, // v13: delete this line
});
const task = _.find(

View File

@ -151,26 +151,8 @@ export async function osProgressHandler(step: InitializeEmitter) {
export async function getAppWithArch(
applicationName: string,
): Promise<ApplicationWithDeviceType & { arch: string }> {
const app = await getApplication(applicationName);
const { getExpanded } = await import('./pine');
return {
...app,
arch: getExpanded(
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
)!.slug,
};
}
// TODO: Drop this. The sdk now has this baked in application.get().
function getApplication(
applicationName: string,
): Promise<ApplicationWithDeviceType> {
// Check for an app of the form `user/application`, and send
// that off to a special handler (before importing any modules)
const match = applicationName.split('/');
const extraOptions: BalenaSdk.PineOptions<BalenaSdk.Application> = {
const { getApplication } = await import('./sdk');
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
$expand: {
application_type: {
$select: ['name', 'slug', 'supports_multicontainer', 'is_legacy'],
@ -185,20 +167,20 @@ function getApplication(
},
},
};
const balena = getBalenaSdk();
if (match.length > 1) {
return balena.models.application.getAppByOwner(
match[1],
match[0],
extraOptions,
) as Promise<ApplicationWithDeviceType>;
}
return balena.models.application.get(
const app = (await getApplication(
balena,
applicationName,
extraOptions,
) as Promise<ApplicationWithDeviceType>;
options,
)) as ApplicationWithDeviceType;
const { getExpanded } = await import('./pine');
return {
...app,
arch: getExpanded(
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
)!.slug,
};
}
const second = 1000; // 1000 milliseconds
@ -428,7 +410,7 @@ export function getProxyConfig(): ProxyConfig | undefined {
export const expandForAppName = {
$expand: {
belongs_to__application: { $select: 'app_name' },
belongs_to__application: { $select: ['app_name', 'slug'] as any },
is_of__device_type: { $select: 'slug' },
is_running__release: { $select: 'commit' },
},

View File

@ -17,183 +17,11 @@
import * as _ from 'lodash';
import { promises as fs, Stats } from 'fs';
import * as path from 'path';
import * as MultiBuild from 'resin-multibuild';
import dockerIgnore = require('@zeit/dockerignore');
import ignore from 'ignore';
import type { Ignore } from '@balena/dockerignore';
import { ExpectedError } from '../errors';
const { toPosixPath } = MultiBuild.PathUtils;
// v13: delete this enum
export enum IgnoreFileType {
DockerIgnore,
GitIgnore,
}
interface IgnoreEntry {
pattern: string;
// The relative file path from the base path of the build context
filePath: string;
}
/**
* This class is used by the CLI v10 / v11 "original" tarDirectory function
* in `compose.js`. It is still around for the benefit of the `--gitignore`
* option, but is expected to be deleted in CLI v13.
*
* v13: delete this class
*/
export class FileIgnorer {
private dockerIgnoreEntries: IgnoreEntry[];
private gitIgnoreEntries: IgnoreEntry[];
private static ignoreFiles: Array<{
pattern: string;
type: IgnoreFileType;
allowSubdirs: boolean;
}> = [
{
pattern: '.gitignore',
type: IgnoreFileType.GitIgnore,
allowSubdirs: true,
},
{
pattern: '.dockerignore',
type: IgnoreFileType.DockerIgnore,
allowSubdirs: false,
},
];
public constructor(public basePath: string) {
this.dockerIgnoreEntries = [];
this.gitIgnoreEntries = [];
}
/**
* @param {string} relativePath
* The relative pathname from the build context, for example a root level .gitignore should be
* ./.gitignore
* @returns IgnoreFileType
* The type of ignore file, or null
*/
public getIgnoreFileType(relativePath: string): IgnoreFileType | null {
for (const { pattern, type, allowSubdirs } of FileIgnorer.ignoreFiles) {
if (
path.basename(relativePath) === pattern &&
(allowSubdirs || path.dirname(relativePath) === '.')
) {
return type;
}
}
return null;
}
/**
* @param {string} fullPath
* The full path on disk of the ignore file
* @param {IgnoreFileType} type
* @returns Promise
*/
public async addIgnoreFile(
fullPath: string,
type: IgnoreFileType,
): Promise<void> {
const contents = await fs.readFile(fullPath, 'utf8');
contents.split('\n').forEach((line) => {
// ignore empty lines and comments
if (/\s*#/.test(line) || _.isEmpty(line)) {
return;
}
this.addEntry(line, fullPath, type);
});
return;
}
// Pass this function as a predicate to a filter function, and it will filter
// any ignored files
public filter = (filename: string): boolean => {
const relFile = path.relative(this.basePath, filename);
// Don't ignore any metadata files
// The regex below matches `.balena/qemu` and `myservice/.balena/qemu`
// but not `some.dir.for.balena/qemu`.
if (/(^|\/)\.(balena|resin)\//.test(toPosixPath(relFile))) {
return true;
}
// Don't ignore Dockerfile (with or without extension) or docker-compose.yml
if (
/^Dockerfile$|^Dockerfile\.\S+/.test(path.basename(relFile)) ||
path.basename(relFile) === 'docker-compose.yml'
) {
return true;
}
const dockerIgnoreHandle = dockerIgnore();
const gitIgnoreHandle = ignore();
interface IgnoreHandle {
add: (pattern: string) => void;
ignores: (file: string) => boolean;
}
const ignoreTypes: Array<{
handle: IgnoreHandle;
entries: IgnoreEntry[];
}> = [
{ handle: dockerIgnoreHandle, entries: this.dockerIgnoreEntries },
{ handle: gitIgnoreHandle, entries: this.gitIgnoreEntries },
];
_.each(ignoreTypes, ({ handle, entries }) => {
_.each(entries, ({ pattern, filePath }) => {
if (FileIgnorer.contains(path.posix.dirname(filePath), filename)) {
handle.add(pattern);
}
});
});
return !_.some(ignoreTypes, ({ handle }) => handle.ignores(relFile));
}; // tslint:disable-line:semicolon
private addEntry(
pattern: string,
filePath: string,
type: IgnoreFileType,
): void {
const entry: IgnoreEntry = { pattern, filePath };
switch (type) {
case IgnoreFileType.DockerIgnore:
this.dockerIgnoreEntries.push(entry);
break;
case IgnoreFileType.GitIgnore:
this.gitIgnoreEntries.push(entry);
break;
}
}
/**
* Given two paths, check whether the first contains the second
* @param path1 The potentially containing path
* @param path2 The potentially contained path
* @return A boolean indicating whether `path1` contains `path2`
*/
private static contains(path1: string, path2: string): boolean {
// First normalise the input, to remove any path weirdness
path1 = path.posix.normalize(path1);
path2 = path.posix.normalize(path2);
// Now test if the start of the relative path contains ../ ,
// which would tell us that path1 is not part of path2
return !/^\.\.\//.test(path.posix.relative(path1, path2));
}
}
export interface FileStats {
filePath: string;
relPath: string;

View File

@ -15,8 +15,6 @@
* limitations under the License.
*/
import { isV13 } from './version';
export const reachingOut = `\
For further help or support, visit:
https://www.balena.io/docs/reference/balena-cli/#support-faq-and-troubleshooting
@ -88,60 +86,7 @@ If the --registry-secrets option is not specified, and a secrets.yml or
secrets.json file exists in the balena directory (usually $HOME/.balena),
this file will be used instead.`;
const dockerignoreHelpV12 =
'DOCKERIGNORE AND GITIGNORE FILES \n' +
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
the project root (--source directory) in order to decide which source files to
exclude from the "build context" (tar stream) sent to balenaCloud, Docker
daemon or balenaEngine. In a microservices (multicontainer) fleet, the
source directory is the directory that contains the "docker-compose.yml" file.
The --multi-dockerignore (-m) option may be used with microservices
(multicontainer) fleets that define a docker-compose.yml file. When this
option is used, each service subdirectory (defined by the \`build\` or
\`build.context\` service properties in the docker-compose.yml file) is
filtered separately according to a .dockerignore file defined in the service
subdirectory. If no .dockerignore file exists in a service subdirectory, then
only the default .dockerignore patterns (see below) apply for that service
subdirectory.
When the --multi-dockerignore (-m) option is used, the .dockerignore file (if
any) defined at the overall project root will be used to filter files and
subdirectories other than service subdirectories. It will not have any effect
on service subdirectories, whether or not a service subdirectory defines its
own .dockerignore file. Multiple .dockerignore files are not merged or added
together, and cannot override or extend other files. This behavior maximizes
compatibility with the standard docker-compose tool, while still allowing a
root .dockerignore file (at the overall project root) to filter files and
folders that are outside service subdirectories.
balena CLI releases older than v12.0.0 also took .gitignore files into account.
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
option if compatibility is required. This option is mutually exclusive with
--multi-dockerignore (-m) and will be removed in the CLI's next major version
release (v13).
Default .dockerignore patterns \n` +
`When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
patterns found in the applicable .dockerignore files, in the following order:
\`\`\`
**/.git
< user's patterns from the applicable '.dockerignore' file, if any >
!**/.balena
!**/.resin
!**/Dockerfile
!**/Dockerfile.*
!**/docker-compose.yml
\`\`\`
These patterns always apply, whether or not .dockerignore files exist in the
project. If necessary, the effect of the \`**/.git\` pattern may be modified by
adding counter patterns to the applicable .dockerignore file(s), for example
\`!mysubmodule/.git\`. For documentation on pattern format, see:
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore`;
const dockerignoreHelpV13 =
export const dockerignoreHelp =
'DOCKERIGNORE AND GITIGNORE FILES \n' +
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
the project root (--source directory) in order to decide which source files to
@ -191,10 +136,6 @@ adding exception patterns to the applicable .dockerignore file(s), for example
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore`;
export const dockerignoreHelp = isV13()
? dockerignoreHelpV13
: dockerignoreHelpV12;
export const applicationIdInfo = `\
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
@ -234,30 +175,6 @@ If you have a particular use for buildArg, which is not satisfied by build-time
secrets, please contact us via support or the forums: https://forums.balena.io/
\n`;
// Note: if you edit this message, check that the regex replace
// logic in lib/commands/apps.ts still works.
export const appToFleetCmdMsg = `\
Renaming notice: The 'app' command was renamed to 'fleet', and 'app'
is now an alias. THE ALIAS WILL BE REMOVED in the next major version
of the balena CLI (so that a different 'app' command can be implemented
in the future). Use 'fleet' instead of 'app' to avoid this warning.
Find out more at: https://git.io/JRuZr`;
export const appToFleetFlagMsg = `\
Renaming notice: The '-a', '--app' or '--application' options are now
aliases for the '-f' or '--fleet' options. THE ALIASES WILL BE REMOVED
in the next major version of the balena CLI (so that a different '--app'
option can be implemented in the future). Use '-f' or '--fleet' instead.
Find out more at: https://git.io/JRuZr`;
export const appToFleetOutputMsg = `\
Renaming notice: The 'app' or 'application' words in table headers
or in JSON object keys/properties will be replaced with 'fleet' in
the next major version of the CLI (v13). The --v13 option may be used
to enable the new names already now, and suppress a warning message.
(The --v13 option will be silently ignored in CLI v13.)
Find out more at: https://git.io/JRuZr`;
export function getNodeEngineVersionWarn(
version: string,
validVersions: string,

View File

@ -13,15 +13,14 @@ 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 type * as BalenaSdk from 'balena-sdk';
import type { Application, BalenaSDK, Device, Organization } from 'balena-sdk';
import _ = require('lodash');
import { instanceOf, NotLoggedInError, ExpectedError } from '../errors';
import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
import validation = require('./validation');
import { delay } from './helpers';
import { isV13 } from './version';
import type { Application, Device, Organization } from 'balena-sdk';
export function authenticate(options: {}): Promise<void> {
const balena = getBalenaSdk();
@ -142,11 +141,7 @@ export async function confirm(
) {
if (yesOption) {
if (yesMessage) {
if (isV13()) {
console.error(yesMessage);
} else {
console.log(yesMessage);
}
console.log(yesMessage);
}
return;
}
@ -174,7 +169,7 @@ export function selectApplication(
throw new ExpectedError('No fleets found');
}
const apps = (await balena.models.application.getAll({
const apps = (await balena.models.application.getAllDirectlyAccessible({
$expand: {
is_for__device_type: {
$select: 'slug',
@ -326,7 +321,7 @@ export function inferOrSelectDevice(preferredUuid: string) {
* TODO: Modify this when app IDs dropped.
*/
export async function getOnlineTargetDeviceUuid(
sdk: BalenaSdk.BalenaSDK,
sdk: BalenaSDK,
applicationOrDevice: string,
) {
const logger = (await import('../utils/logger')).getLogger();

View File

@ -235,7 +235,7 @@ async function getOrSelectApplication(
name = appName;
}
const applications = (await sdk.models.application.getAll(
const applications = (await sdk.models.application.getAllDirectlyAccessible(
options,
)) as ApplicationWithDeviceType[];
@ -273,7 +273,7 @@ async function createOrSelectApp(
deviceType: string,
): Promise<ApplicationWithDeviceType> {
// No fleet specified, show a list to select one.
const applications = (await sdk.models.application.getAll({
const applications = (await sdk.models.application.getAllDirectlyAccessible({
$expand: { is_for__device_type: { $select: 'slug' } },
$filter: {
is_for__device_type: {
@ -322,10 +322,13 @@ async function createApplication(
});
try {
await sdk.models.application.get(appName, {
await sdk.models.application.getDirectlyAccessible(appName, {
$filter: {
$or: [
{ slug: { $startswith: `${username!.toLowerCase()}/` } },
// TODO: do we still need the following filter? Is it for
// old openBalena instances where slugs were equal to the
// app name and did not contain the slash character?
{ $not: { slug: { $contains: '/' } } },
],
},

View File

@ -50,7 +50,6 @@ export interface RemoteBuild {
source: string;
auth: string;
baseUrl: string;
nogitignore: boolean; // v13: delete this line
opts: BuildOpts;
sdk: BalenaSDK;
// For internal use
@ -323,7 +322,6 @@ async function getTarStream(build: RemoteBuild): Promise<Stream.Readable> {
preFinalizeCallback: preFinalizeCb,
convertEol: build.opts.convertEol,
multiDockerignore: build.opts.multiDockerignore,
nogitignore: build.nogitignore, // v13: delete this line
});
globalLogger.logDebug(
`Tarring complete in ${Date.now() - tarStartTime} ms`,

View File

@ -23,21 +23,36 @@ import type {
} from 'balena-sdk';
/**
* Wraps the sdk application.get method,
* adding disambiguation in cases where the provided
* identifier could be interpreted in multiple valid ways.
* Get a fleet object, disambiguating the fleet identifier which may be a
* a fleet slug, name or numeric database ID (as a string).
* TODO: add support for fleet UUIDs.
*/
export async function getApplication(
sdk: BalenaSDK,
nameOrSlugOrId: string | number,
options?: PineOptions<Application>,
): Promise<Application> {
const { looksLikeInteger } = await import('./validation');
if (looksLikeInteger(nameOrSlugOrId as string)) {
const { looksLikeFleetSlug, looksLikeInteger } = await import('./validation');
if (
typeof nameOrSlugOrId === 'string' &&
looksLikeFleetSlug(nameOrSlugOrId)
) {
return await sdk.models.application.getDirectlyAccessible(
nameOrSlugOrId,
options,
);
}
if (typeof nameOrSlugOrId === 'number' || looksLikeInteger(nameOrSlugOrId)) {
try {
// Test for existence of app with this numerical ID
return await sdk.models.application.get(Number(nameOrSlugOrId), options);
return await sdk.models.application.getDirectlyAccessible(
Number(nameOrSlugOrId),
options,
);
} catch (e) {
if (typeof nameOrSlugOrId === 'number') {
throw e;
}
const { instanceOf } = await import('../errors');
const { BalenaApplicationNotFound } = await import('balena-errors');
if (!instanceOf(e, BalenaApplicationNotFound)) {
@ -46,47 +61,34 @@ export async function getApplication(
// App with this numerical ID not found, but there may be an app with this numerical name.
}
}
return sdk.models.application.get(nameOrSlugOrId, options);
// Not a slug and not a numeric database ID: must be an app name.
// TODO: revisit this logic when we add support for fleet UUIDs.
return await sdk.models.application.getAppByName(
nameOrSlugOrId,
options,
'directly_accessible',
);
}
/**
* Given an string representation of an application identifier,
* which could be one of:
* - name (including numeric names)
* - slug
* - numerical id
* disambiguate and return a properly typed identifier.
*
* Attempts to minimise the number of API calls required.
* TODO: Remove this once support for numeric App IDs is removed.
* Given a fleet name, slug or numeric database ID, return its slug.
* This function conditionally makes an async SDK/API call to retrieve the
* application object, which can be wasteful if the application object is
* required before or after the call to this function. If this is the case,
* consider calling `getApplication()` and reusing the application object.
*/
export async function getTypedApplicationIdentifier(
export async function getFleetSlug(
sdk: BalenaSDK,
nameOrSlugOrId: string,
) {
const { looksLikeInteger } = await import('./validation');
// If there's no possible ambiguity,
// return the passed identifier unchanged
if (!looksLikeInteger(nameOrSlugOrId)) {
return nameOrSlugOrId;
nameOrSlugOrId: string | number,
): Promise<string> {
const { looksLikeFleetSlug } = await import('./validation');
if (
typeof nameOrSlugOrId === 'string' &&
looksLikeFleetSlug(nameOrSlugOrId)
) {
return nameOrSlugOrId.toLowerCase();
}
// Resolve ambiguity
try {
// Test for existence of app with this numerical ID,
// and return typed id if found
return (await sdk.models.application.get(Number(nameOrSlugOrId))).id;
} catch (e) {
const { instanceOf } = await import('../errors');
const { BalenaApplicationNotFound } = await import('balena-errors');
if (!instanceOf(e, BalenaApplicationNotFound)) {
throw e;
}
}
// App with this numerical id not found
// return the passed identifier unchanged
return nameOrSlugOrId;
return (await getApplication(sdk, nameOrSlugOrId)).slug;
}
/**

View File

@ -118,7 +118,6 @@ export function parseAsLocalHostnameOrIp(input: string, paramName?: string) {
return input;
}
export function looksLikeAppSlug(input: string) {
// One or more non whitespace chars, /, 4 or more non whitespace chars
return /[\S]+\/[\S]{4,}/.test(input);
export function looksLikeFleetSlug(input: string) {
return input.includes('/');
}

View File

@ -22,12 +22,12 @@ export function isVersionGTE(v: string): boolean {
return semver.gte(process.env.BALENA_CLI_VERSION_OVERRIDE || version, v);
}
let v13: boolean;
let v14: boolean;
/** Feature switch for the next major version of the CLI */
export function isV13(): boolean {
if (v13 === undefined) {
v13 = isVersionGTE('13.0.0');
export function isV14(): boolean {
if (v14 === undefined) {
v14 = isVersionGTE('14.0.0');
}
return v13;
return v14;
}

180
npm-shrinkwrap.json generated
View File

@ -2445,6 +2445,11 @@
"resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
"integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="
},
"@types/json-schema": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
},
"@types/jsonstream": {
"version": "0.8.30",
"resolved": "https://registry.npmjs.org/@types/jsonstream/-/jsonstream-0.8.30.tgz",
@ -2807,9 +2812,9 @@
}
},
"@types/uuid": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz",
"integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg=="
"version": "8.3.3",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.3.tgz",
"integrity": "sha512-0LbEEx1zxrYB3pgpd1M5lEhLcXjKJnYghvhTRgaBeUivLHMDM1TzF3IJ6hXU2+8uA4Xz+5BA63mtZo5DjVT8iA=="
},
"@types/which": {
"version": "2.0.1",
@ -2921,11 +2926,6 @@
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="
},
"@zeit/dockerignore": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@zeit/dockerignore/-/dockerignore-0.0.3.tgz",
"integrity": "sha512-tO000RCJxEbRBhQpWEaTF+aAIkMRSuQVnC1g7Y1l/duhxjp6CotepHcUerQBFa5lgjVesh/zlblq2t1GvfAEbQ=="
},
"JSONStream": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
@ -3675,10 +3675,54 @@
"rimraf": "^3.0.2"
},
"dependencies": {
"balena-sdk": {
"version": "15.59.2",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.59.2.tgz",
"integrity": "sha512-pMNwqqnqtets6PoYxRQhOHBnk7Say4gNhInAvS69UlEhraCR0Xau8rJk9G2IthWDA4tpR2In3u4SQJMIHYj84w==",
"requires": {
"@balena/es-version": "^1.0.0",
"@types/lodash": "^4.14.168",
"@types/memoizee": "^0.4.5",
"@types/node": "^10.17.55",
"abortcontroller-polyfill": "^1.7.1",
"balena-auth": "^4.1.0",
"balena-errors": "^4.7.1",
"balena-hup-action-utils": "~4.0.2",
"balena-pine": "^12.4.0",
"balena-register-device": "^7.1.0",
"balena-request": "^11.5.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6",
"lodash": "^4.17.21",
"memoizee": "^0.4.15",
"moment": "^2.29.1",
"ndjson": "^2.0.0",
"semver": "^7.3.4",
"tslib": "^2.1.0"
}
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
@ -3730,6 +3774,32 @@
"zip-stream": "^2.1.2"
}
},
"balena-sdk": {
"version": "15.59.2",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.59.2.tgz",
"integrity": "sha512-pMNwqqnqtets6PoYxRQhOHBnk7Say4gNhInAvS69UlEhraCR0Xau8rJk9G2IthWDA4tpR2In3u4SQJMIHYj84w==",
"requires": {
"@balena/es-version": "^1.0.0",
"@types/lodash": "^4.14.168",
"@types/memoizee": "^0.4.5",
"@types/node": "^10.17.55",
"abortcontroller-polyfill": "^1.7.1",
"balena-auth": "^4.1.0",
"balena-errors": "^4.7.1",
"balena-hup-action-utils": "~4.0.2",
"balena-pine": "^12.4.0",
"balena-register-device": "^7.1.0",
"balena-request": "^11.5.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6",
"lodash": "^4.17.21",
"memoizee": "^0.4.15",
"moment": "^2.29.1",
"ndjson": "^2.0.0",
"semver": "^7.3.4",
"tslib": "^2.1.0"
}
},
"compress-commons": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz",
@ -3766,6 +3836,11 @@
"readable-stream": "^3.4.0"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@ -3776,6 +3851,14 @@
"util-deprecate": "^1.0.1"
}
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"tar-fs": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
@ -3801,6 +3884,11 @@
}
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"zip-stream": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz",
@ -3853,9 +3941,9 @@
}
},
"balena-request": {
"version": "11.4.2",
"resolved": "https://registry.npmjs.org/balena-request/-/balena-request-11.4.2.tgz",
"integrity": "sha512-J4SrFBUR4AB2Y3afsX2QAMZ7H/zjysXjOyEhEqvjNTsBfe5ReCmf17vzRQ8q2URCm00nUhQgfQtlJUq6miB1/g==",
"version": "11.5.0",
"resolved": "https://registry.npmjs.org/balena-request/-/balena-request-11.5.0.tgz",
"integrity": "sha512-mLf5NQU2qqGKyjZAmz8fFOzyUjwF9uptcDPUe56ADAQfLvQYML8izW7/9Q7DBRGpvlZrBZ/OnjWLK3vUs1tNdA==",
"requires": {
"@balena/node-web-streams": "^0.2.3",
"balena-errors": "^4.7.1",
@ -3867,11 +3955,12 @@
}
},
"balena-sdk": {
"version": "15.51.1",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.51.1.tgz",
"integrity": "sha512-EMCQruytqyvpfxvjq9Zd/wWnnOAIl/Wd1majqv6hqa+z104UUTjnRCQaUji4mo8YtrFLn7aUUZFFHIKiv/3sTg==",
"version": "16.8.0",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-16.8.0.tgz",
"integrity": "sha512-kch7hhB9PksFsyVOAFyylsQMlv976V11DOwhb41w2JykVKjR9KSNFBfy1wYwYY1Wcp8PWXvj9WxtB0Sd/SW9bQ==",
"requires": {
"@balena/es-version": "^1.0.0",
"@types/json-schema": "^7.0.9",
"@types/lodash": "^4.14.168",
"@types/memoizee": "^0.4.5",
"@types/node": "^10.17.55",
@ -3881,14 +3970,13 @@
"balena-hup-action-utils": "~4.0.2",
"balena-pine": "^12.4.0",
"balena-register-device": "^7.1.0",
"balena-request": "^11.4.2",
"balena-request": "^11.5.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6",
"lodash": "^4.17.21",
"memoizee": "^0.4.15",
"moment": "^2.29.1",
"ndjson": "^2.0.0",
"semver": "^7.3.4",
"tslib": "^2.1.0"
},
"dependencies": {
@ -3897,14 +3985,6 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@ -3990,6 +4070,39 @@
"typed-error": "^2.0.0"
},
"dependencies": {
"balena-sdk": {
"version": "15.59.2",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.59.2.tgz",
"integrity": "sha512-pMNwqqnqtets6PoYxRQhOHBnk7Say4gNhInAvS69UlEhraCR0Xau8rJk9G2IthWDA4tpR2In3u4SQJMIHYj84w==",
"requires": {
"@balena/es-version": "^1.0.0",
"@types/lodash": "^4.14.168",
"@types/memoizee": "^0.4.5",
"@types/node": "^10.17.55",
"abortcontroller-polyfill": "^1.7.1",
"balena-auth": "^4.1.0",
"balena-errors": "^4.7.1",
"balena-hup-action-utils": "~4.0.2",
"balena-pine": "^12.4.0",
"balena-register-device": "^7.1.0",
"balena-request": "^11.5.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6",
"lodash": "^4.17.21",
"memoizee": "^0.4.15",
"moment": "^2.29.1",
"ndjson": "^2.0.0",
"semver": "^7.3.4",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
@ -4013,6 +4126,19 @@
"esprima": "^4.0.0"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"supports-color": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
@ -18212,9 +18338,9 @@
}
},
"web-streams-polyfill": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz",
"integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q=="
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
"integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA=="
},
"which": {
"version": "2.0.2",

View File

@ -90,7 +90,7 @@
"author": "Balena Inc. (https://balena.io/)",
"license": "Apache-2.0",
"engines": {
"node": ">=10.20.0 <13.0.0",
"node": ">=12.0.0 <13.0.0",
"npm": "<7.0.0"
},
"husky": {
@ -200,7 +200,6 @@
"@sentry/node": "^6.13.2",
"@types/fast-levenshtein": "0.0.1",
"@types/update-notifier": "^4.1.1",
"@zeit/dockerignore": "0.0.3",
"JSONStream": "^1.0.3",
"balena-config-json": "^4.2.0",
"balena-device-init": "^6.0.0",
@ -209,7 +208,7 @@
"balena-image-manager": "^7.1.1",
"balena-preload": "^11.0.0",
"balena-release": "^3.2.0",
"balena-sdk": "^15.51.1",
"balena-sdk": "^16.8.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.7",
"balena-settings-storage": "^7.0.0",
@ -241,7 +240,6 @@
"global-tunnel-ng": "^2.1.1",
"got": "^11.8.2",
"humanize": "0.0.9",
"ignore": "^5.1.8",
"inquirer": "^7.3.3",
"is-elevated": "^3.0.0",
"is-root": "^2.1.0",

View File

@ -22,7 +22,6 @@ import { promises as fs } from 'fs';
import * as path from 'path';
import { stripIndent } from '../../build/utils/lazy';
import { isV13 } from '../../build/utils/version';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
@ -127,23 +126,14 @@ describe('balena build', function () {
];
if (isWindows) {
const fname = path.join(projectPath, 'src', 'windows-crlf.sh');
if (isWindows) {
expectedResponseLines.push(
`[Info] Converting line endings CRLF -> LF for file: ${fname}`,
);
} else {
expectedResponseLines.push(
`[Warn] CRLF (Windows) line endings detected in file: ${fname}`,
'[Warn] Windows-format line endings were detected in some files. Consider using the `--convert-eol` option.',
);
}
expectedResponseLines.push(
`[Info] Converting line endings CRLF -> LF for file: ${fname}`,
);
}
docker.expectGetInfo({});
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 ${
isV13() ? '' : '-g'
}`,
commandLine: `build ${projectPath} --deviceType nuc --arch amd64`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: {
@ -192,16 +182,9 @@ describe('balena build', function () {
];
if (isWindows) {
const fname = path.join(projectPath, 'src', 'windows-crlf.sh');
if (isWindows) {
expectedResponseLines.push(
`[Info] Converting line endings CRLF -> LF for file: ${fname}`,
);
} else {
expectedResponseLines.push(
`[Warn] CRLF (Windows) line endings detected in file: ${fname}`,
'[Warn] Windows-format line endings were detected in some files. Consider using the `--convert-eol` option.',
);
}
expectedResponseLines.push(
`[Info] Converting line endings CRLF -> LF for file: ${fname}`,
);
}
docker.expectGetInfo({});
docker.expectGetManifestBusybox();
@ -297,9 +280,7 @@ describe('balena build', function () {
docker.expectGetInfo({ OperatingSystem: 'balenaOS 2.44.0+rev1' });
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `build ${projectPath} --emulated --deviceType ${deviceType} --arch ${arch} ${
isV13() ? '' : '--nogitignore'
}`,
commandLine: `build ${projectPath} --emulated --deviceType ${deviceType} --arch ${arch}`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: {
@ -317,7 +298,7 @@ describe('balena build', function () {
}
});
it('should create the expected tar stream (single container, --[no]convert-eol, --multi-dockerignore)', async () => {
it('should create the expected tar stream (single container, --noconvert-eol, --multi-dockerignore)', async () => {
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
const expectedFiles: ExpectedTarStreamFiles = {
'src/.dockerignore': { fileSize: 16, type: 'file' },
@ -448,9 +429,7 @@ describe('balena build', function () {
docker.expectGetManifestNucAlpine();
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol ${
isV13() ? '' : '-G'
} -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2`,
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2`,
dockerMock: docker,
expectedFilesByService,
expectedQueryParamsByService,
@ -533,7 +512,7 @@ describe('balena build', function () {
docker.expectGetManifestNucAlpine();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m`,
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -m`,
dockerMock: docker,
expectedFilesByService,
expectedQueryParamsByService,
@ -618,7 +597,7 @@ describe('balena build', function () {
docker.expectGetManifestNucAlpine();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m --tag ${tag} --projectName ${projectName}`,
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -m --tag ${tag} --projectName ${projectName}`,
dockerMock: docker,
expectedFilesByService,
expectedQueryParamsByService,

View File

@ -23,7 +23,6 @@ import * as nock from 'nock';
import * as path from 'path';
import * as sinon from 'sinon';
import { isV13 } from '../../build/utils/version';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
@ -149,9 +148,7 @@ describe('balena deploy', function () {
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `deploy testApp --build --source ${projectPath} ${
isV13() ? '' : '-G'
}`,
commandLine: `deploy testApp --build --source ${projectPath}`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams },
@ -315,9 +312,7 @@ describe('balena deploy', function () {
sinon.stub(process, 'exit');
await testDockerBuildStream({
commandLine: `deploy testApp --build --source ${projectPath} --noconvert-eol ${
isV13() ? '' : '-G'
}`,
commandLine: `deploy testApp --build --source ${projectPath} --noconvert-eol`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams },

View File

@ -21,9 +21,6 @@ import * as path from 'path';
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
import { isV13 } from '../../../build/utils/version';
describe('balena device', function () {
let api: BalenaAPIMock;
@ -38,13 +35,6 @@ describe('balena device', function () {
api.done();
});
const expectedWarn =
!isV13() &&
process.stderr.isTTY &&
process.env.BALENA_CLI_TEST_TYPE !== 'standalone'
? warnify(appToFleetOutputMsg) + '\n'
: '';
it('should error if uuid not provided', async () => {
const { out, err } = await runCommand('device');
const errLines = cleanOutput(err);
@ -57,27 +47,25 @@ describe('balena device', function () {
it('should list device details for provided uuid', async () => {
api.scope
.get(
/^\/v6\/device\?.+&\$expand=belongs_to__application\(\$select=app_name\)/,
/^\/v6\/device\?.+&\$expand=belongs_to__application\(\$select=app_name,slug\)/,
)
.replyWithFile(200, path.join(apiResponsePath, 'device.json'), {
'Content-Type': 'application/json',
});
const { out, err } = await runCommand('device 27fda508c');
const { out } = await runCommand('device 27fda508c');
const lines = cleanOutput(out);
expect(lines).to.have.lengthOf(25);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('test app');
expect(err.join('')).to.eql(expectedWarn);
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
});
it.skip('correctly handles devices with missing fields', async () => {
api.scope
.get(
/^\/v6\/device\?.+&\$expand=belongs_to__application\(\$select=app_name\)/,
/^\/v6\/device\?.+&\$expand=belongs_to__application\(\$select=app_name,slug\)/,
)
.replyWithFile(
200,
@ -87,15 +75,13 @@ describe('balena device', function () {
},
);
const { out, err } = await runCommand('device 27fda508c');
const { out } = await runCommand('device 27fda508c');
const lines = cleanOutput(out);
expect(lines).to.have.lengthOf(14);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('test app');
expect(err.join('')).to.eql(expectedWarn);
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
});
it('correctly handles devices with missing application', async () => {
@ -103,7 +89,7 @@ describe('balena device', function () {
// e.g. When user has a device associated with app that user is no longer a collaborator of.
api.scope
.get(
/^\/v6\/device\?.+&\$expand=belongs_to__application\(\$select=app_name\)/,
/^\/v6\/device\?.+&\$expand=belongs_to__application\(\$select=app_name,slug\)/,
)
.replyWithFile(
200,
@ -113,14 +99,12 @@ describe('balena device', function () {
},
);
const { out, err } = await runCommand('device 27fda508c');
const { out } = await runCommand('device 27fda508c');
const lines = cleanOutput(out);
expect(lines).to.have.lengthOf(25);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('N/a');
expect(err.join('')).to.eql(expectedWarn);
});
});

View File

@ -21,9 +21,6 @@ import * as path from 'path';
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
import { isV13 } from '../../../build/utils/version';
describe('balena devices', function () {
let api: BalenaAPIMock;
@ -38,39 +35,28 @@ describe('balena devices', function () {
api.done();
});
const expectedWarn =
!isV13() &&
process.stderr.isTTY &&
process.env.BALENA_CLI_TEST_TYPE !== 'standalone'
? warnify(appToFleetOutputMsg) + '\n'
: '';
it('should list devices from own and collaborator apps', async () => {
api.scope
.get(
'/v6/device?$orderby=device_name%20asc&$expand=belongs_to__application($select=app_name),is_of__device_type($select=slug),is_running__release($select=commit)',
'/v6/device?$orderby=device_name%20asc&$expand=belongs_to__application($select=app_name,slug),is_of__device_type($select=slug),is_running__release($select=commit)',
)
.replyWithFile(200, path.join(apiResponsePath, 'devices.json'), {
'Content-Type': 'application/json',
});
const { out, err } = await runCommand('devices');
const { out } = await runCommand('devices');
const lines = cleanOutput(out);
expect(lines[0].replace(/ +/g, ' ')).to.equal(
isV13()
? 'ID UUID DEVICE NAME DEVICE TYPE FLEET STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL'
: 'ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
'ID UUID DEVICE NAME DEVICE TYPE FLEET STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
);
expect(lines).to.have.lengthOf.at.least(2);
expect(lines.some((l) => l.includes('test app'))).to.be.true;
expect(lines.some((l) => l.includes('org/test app'))).to.be.true;
// Devices with missing applications will have application name set to `N/a`.
// e.g. When user has a device associated with app that user is no longer a collaborator of.
expect(lines.some((l) => l.includes('N/a'))).to.be.true;
expect(err.join('')).to.eql(expectedWarn);
});
});

View File

@ -20,8 +20,6 @@ import { expect } from 'chai';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { isV13 } from '../../../build/utils/version';
describe('balena devices supported', function () {
let api: BalenaAPIMock;
@ -48,34 +46,16 @@ describe('balena devices supported', function () {
api.expectGetDeviceTypes();
api.expectGetConfigDeviceTypes();
const { out, err } = await runCommand('devices supported -v');
const { out, err } = await runCommand('devices supported');
const lines = cleanOutput(out, true);
expect(lines[0]).to.equal(
isV13() ? 'SLUG ALIASES ARCH NAME' : 'SLUG ALIASES ARCH STATE NAME',
);
expect(lines[0]).to.equal('SLUG ALIASES ARCH NAME');
expect(lines).to.have.lengthOf.at.least(2);
expect(lines).to.contain('intel-nuc nuc amd64 Intel NUC');
expect(lines).to.contain(
isV13()
? 'intel-nuc nuc amd64 Intel NUC'
: 'intel-nuc nuc amd64 RELEASED Intel NUC',
'odroid-xu4 odroid-ux3, odroid-u3+ armv7hf ODROID-XU4',
);
expect(lines).to.contain(
isV13()
? 'odroid-xu4 odroid-ux3, odroid-u3+ armv7hf ODROID-XU4'
: 'odroid-xu4 odroid-ux3, odroid-u3+ armv7hf RELEASED ODROID-XU4',
);
// Discontinued devices should be filtered out from results
expect(lines.some((l) => l.includes('DISCONTINUED'))).to.be.false;
// Beta devices should be listed as new
expect(lines.some((l) => l.includes('BETA'))).to.be.false;
expect(lines.some((l) => l.includes('NEW'))).to.equal(
isV13() ? false : true,
);
expect(err).to.eql([]);
});
});

View File

@ -21,9 +21,6 @@ import { stripIndent } from '../../../build/utils/lazy';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { runCommand } from '../../helpers';
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
import { isV13 } from '../../../build/utils/version';
describe('balena envs', function () {
const appName = 'test';
let fullUUID: string;
@ -44,13 +41,6 @@ describe('balena envs', function () {
api.done();
});
const appToFleetOutputWarn =
!isV13() &&
process.stderr.isTTY &&
process.env.BALENA_CLI_TEST_TYPE !== 'standalone'
? warnify(appToFleetOutputMsg) + '\n'
: '';
it('should successfully list env vars for a test fleet', async () => {
api.expectGetApplication();
api.expectGetAppEnvVars();
@ -60,11 +50,11 @@ describe('balena envs', function () {
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE FLEET SERVICE
120110 svar1 svar1-value test service1
120111 svar2 svar2-value test service2
120101 var1 var1-val test *
120102 var2 22 test *
ID NAME VALUE FLEET SERVICE
120110 svar1 svar1-value gh_user/testApp service1
120111 svar2 svar2-value gh_user/testApp service2
120101 var1 var1-val gh_user/testApp *
120102 var2 22 gh_user/testApp *
` + '\n',
);
expect(err.join('')).to.equal('');
@ -79,7 +69,7 @@ describe('balena envs', function () {
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE FLEET
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false gh_user/testApp
` + '\n',
);
@ -94,7 +84,7 @@ describe('balena envs', function () {
expect(JSON.parse(out.join(''))).to.deep.equal([
{
fleetName: 'test',
fleet: 'gh_user/testApp',
id: 120300,
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
value: 'false',
@ -116,10 +106,10 @@ describe('balena envs', function () {
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE FLEET SERVICE
120111 svar2 svar2-value test service2
120101 var1 var1-val test *
120102 var2 22 test *
ID NAME VALUE FLEET SERVICE
120111 svar2 svar2-value gh_user/testApp service2
120101 var1 var1-val gh_user/testApp *
120102 var2 22 gh_user/testApp *
` + '\n',
);
expect(err.join('')).to.equal('');
@ -138,10 +128,10 @@ describe('balena envs', function () {
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE FLEET SERVICE
120110 svar1 svar1-value test ${serviceName}
120101 var1 var1-val test *
120102 var2 22 test *
ID NAME VALUE FLEET SERVICE
120110 svar1 svar1-value gh_user/testApp ${serviceName}
120101 var1 var1-val gh_user/testApp *
120102 var2 22 gh_user/testApp *
` + '\n',
);
expect(err.join('')).to.equal('');
@ -157,29 +147,24 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid}`);
const { err } = result;
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE
120110 svar1 svar1-value test * service1
120111 svar2 svar2-value test * service2
120120 svar3 svar3-value test ${uuid} service1
120121 svar4 svar4-value test ${uuid} service2
120101 var1 var1-val test * *
120102 var2 22 test * *
120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} *
ID NAME VALUE FLEET DEVICE SERVICE
120110 svar1 svar1-value org/test * service1
120111 svar2 svar2-value org/test * service2
120120 svar3 svar3-value org/test ${uuid} service1
120121 svar4 svar4-value org/test ${uuid} service2
120101 var1 var1-val org/test * *
120102 var2 22 org/test * *
120203 var3 var3-val org/test ${uuid} *
120204 var4 44 org/test ${uuid} *
` + '\n';
if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn);
});
it('should successfully list env variables for a test device (JSON output)', async () => {
@ -191,22 +176,17 @@ describe('balena envs', function () {
api.expectGetDeviceServiceVars();
const { out, err } = await runCommand(`envs -jd ${shortUUID}`);
let expected = `[
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "service1" },
{ "id": 120111, "appName": "test", "deviceUUID": "*", "name": "svar2", "value": "svar2-value", "serviceName": "service2" },
{ "id": 120120, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "service1" },
{ "id": 120121, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar4", "value": "svar4-value", "serviceName": "service2" },
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
const expected = `[
{ "id": 120101, "fleet": "org/test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
{ "id": 120102, "fleet": "org/test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120110, "fleet": "org/test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "service1" },
{ "id": 120111, "fleet": "org/test", "deviceUUID": "*", "name": "svar2", "value": "svar2-value", "serviceName": "service2" },
{ "id": 120120, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "service1" },
{ "id": 120121, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "svar4", "value": "svar4-value", "serviceName": "service2" },
{ "id": 120203, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
{ "id": 120204, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
]`;
if (isV13()) {
expected = expected.replace(
/"appName": "test"/g,
'"fleetName": "org/test"',
);
}
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(err.join('')).to.equal('');
});
@ -218,23 +198,18 @@ describe('balena envs', function () {
api.expectGetAppConfigVars();
const result = await runCommand(`envs -d ${shortUUID} --config`);
const { err } = result;
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE APPLICATION DEVICE
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test *
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 test ${shortUUID}
ID NAME VALUE FLEET DEVICE
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false org/test *
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 org/test ${shortUUID}
` + '\n';
if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn);
});
it('should successfully list service variables for a test device (-s flag)', async () => {
@ -249,27 +224,22 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
const { err } = result;
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE
120111 svar2 svar2-value test * service2
120121 svar4 svar4-value test ${uuid} service2
120101 var1 var1-val test * *
120102 var2 22 test * *
120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} *
ID NAME VALUE FLEET DEVICE SERVICE
120111 svar2 svar2-value org/test * service2
120121 svar4 svar4-value org/test ${uuid} service2
120101 var1 var1-val org/test * *
120102 var2 22 org/test * *
120203 var3 var3-val org/test ${uuid} *
120204 var4 44 org/test ${uuid} *
` + '\n';
if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn);
});
it('should successfully list env and service variables for a test device (unknown fleet)', async () => {
@ -279,24 +249,20 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid}`);
const { err } = result;
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE
120120 svar3 svar3-value N/A ${uuid} service1
120121 svar4 svar4-value N/A ${uuid} service2
120203 var3 var3-val N/A ${uuid} *
120204 var4 44 N/A ${uuid} *
ID NAME VALUE FLEET DEVICE SERVICE
120120 svar3 svar3-value N/A ${uuid} service1
120121 svar4 svar4-value N/A ${uuid} service2
120203 var3 var3-val N/A ${uuid} *
120204 var4 44 N/A ${uuid} *
` + '\n';
if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ');
}
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn);
});
it('should successfully list env and service vars for a test device (-s flags)', async () => {
@ -311,27 +277,22 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
const { err } = result;
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE
120110 svar1 svar1-value test * ${serviceName}
120120 svar3 svar3-value test ${uuid} ${serviceName}
120101 var1 var1-val test * *
120102 var2 22 test * *
120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} *
ID NAME VALUE FLEET DEVICE SERVICE
120110 svar1 svar1-value org/test * ${serviceName}
120120 svar3 svar3-value org/test ${uuid} ${serviceName}
120101 var1 var1-val org/test * *
120102 var2 22 org/test * *
120203 var3 var3-val org/test ${uuid} *
120204 var4 44 org/test ${uuid} *
` + '\n';
if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn);
});
it('should successfully list env and service vars for a test device (-js flags)', async () => {
@ -347,20 +308,15 @@ describe('balena envs', function () {
const { out, err } = await runCommand(
`envs -d ${shortUUID} -js ${serviceName}`,
);
let expected = `[
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" },
{ "id": 120120, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "${serviceName}" },
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
const expected = `[
{ "id": 120101, "fleet": "org/test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
{ "id": 120102, "fleet": "org/test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120110, "fleet": "org/test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" },
{ "id": 120120, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "${serviceName}" },
{ "id": 120203, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
{ "id": 120204, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
]`;
if (isV13()) {
expected = expected.replace(
/"appName": "test"/g,
'"fleetName": "org/test"',
);
}
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(err.join('')).to.equal('');
});

View File

@ -19,7 +19,6 @@ import { expect } from 'chai';
import { promises as fs } from 'fs';
import * as path from 'path';
import { isV13 } from '../../build/utils/version';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { BuilderMock, builderResponsePath } from '../nock/builder-mock';
import { expectStreamNoCRLF, testPushBuildStream } from '../docker-build';
@ -37,7 +36,6 @@ import {
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
const itNoV13 = isV13() ? it.skip : it;
const itNoWin = process.platform === 'win32' ? it.skip : it;
const commonResponseLines = {
@ -82,9 +80,6 @@ const commonQueryParams = [
['isdraft', 'false'],
];
const hr =
'----------------------------------------------------------------------';
describe('balena push', function () {
let api: BalenaAPIMock;
let builder: BuilderMock;
@ -195,7 +190,7 @@ describe('balena push', function () {
});
});
it('should create the expected tar stream (single container, --[no]convert-eol)', async () => {
it('should create the expected tar stream (single container, --noconvert-eol)', async () => {
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
const expectedFiles: ExpectedTarStreamFiles = {
'src/.dockerignore': { fileSize: 16, type: 'file' },
@ -240,76 +235,7 @@ describe('balena push', function () {
});
});
itNoV13(
'should create the expected tar stream (single container, --gitignore)',
async () => {
const projectPath = path.join(
projectsPath,
'no-docker-compose',
'dockerignore1',
);
const expectedFiles: ExpectedTarStreamFiles = {
'.balena/balena.yml': { fileSize: 12, type: 'file' },
'.dockerignore': { fileSize: 438, type: 'file' },
'.gitignore': { fileSize: 20, type: 'file' },
'.git/bar.txt': { fileSize: 4, type: 'file' },
'.git/foo.txt': { fileSize: 4, type: 'file' },
'c.txt': { fileSize: 1, type: 'file' },
Dockerfile: { fileSize: 13, type: 'file' },
'src/.balena/balena.yml': { fileSize: 16, type: 'file' },
'src/.gitignore': { fileSize: 10, type: 'file' },
'vendor/.git/vendor-git-contents': { fileSize: 20, type: 'file' },
// When --gitignore (-g) is provided for v11 compatibility, the old
// `zeit/dockerignore` npm package is still used but it is broken on
// Windows (reason why we created `@balena/dockerignore`).
...(isWindows
? {
'src/src-b.txt': { fileSize: 5, type: 'file' },
'dot.git/bar.txt': { fileSize: 4, type: 'file' },
'dot.git/foo.txt': { fileSize: 4, type: 'file' },
'vendor/dot.git/vendor-git-contents': {
fileSize: 20,
type: 'file',
},
}
: {}),
};
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
const responseFilename = 'build-POST-v3.json';
const responseBody = await fs.readFile(
path.join(builderResponsePath, responseFilename),
'utf8',
);
const expectedResponseLines = [
...[
`[Warn] ${hr}`,
'[Warn] Using file ignore patterns from:',
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
`[Warn] * ${path.join(projectPath, '.gitignore')}`,
`[Warn] * ${path.join(projectPath, 'src', '.gitignore')}`,
'[Warn] .gitignore files are being considered because the --gitignore option was used.',
'[Warn] This option is deprecated and will be removed in the next major version release.',
"[Warn] For more information, see 'balena help push'.",
`[Warn] ${hr}`,
],
...commonResponseLines[responseFilename],
];
await testPushBuildStream({
builderMock: builder,
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -g`,
expectedFiles,
expectedQueryParams: commonQueryParams,
expectedResponseLines,
projectPath,
responseBody,
responseCode: 200,
});
},
);
it('should create the expected tar stream (single container, --nogitignore)', async () => {
it('should create the expected tar stream (single container, dockerignore1)', async () => {
const projectPath = path.join(
projectsPath,
'no-docker-compose',
@ -352,7 +278,7 @@ describe('balena push', function () {
// (with a mismatched fileSize 13 vs 5 for 'symlink-a.txt'), ensure that the
// `core.symlinks` property is set to `true` in the `.git/config` file. Ref:
// https://git-scm.com/docs/git-config#Documentation/git-config.txt-coresymlinks
it('should create the expected tar stream (single container, symbolic links, --gitignore)', async () => {
it('should create the expected tar stream (single container, symbolic links)', async () => {
const projectPath = path.join(
projectsPath,
'no-docker-compose',
@ -366,12 +292,6 @@ describe('balena push', function () {
'lib/src-b.txt': { fileSize: 5, type: 'file' },
'src/src-b.txt': { fileSize: 5, type: 'file' },
'symlink-a.txt': { fileSize: 5, type: 'file' },
...(isWindows
? {
'lib/src-a.txt': { fileSize: 5, type: 'file' },
'src/src-a.txt': { fileSize: 5, type: 'file' },
}
: {}),
};
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
const responseFilename = 'build-POST-v3.json';
@ -379,27 +299,11 @@ describe('balena push', function () {
path.join(builderResponsePath, responseFilename),
'utf8',
);
const expectedResponseLines =
!isV13() && isWindows
? [
`[Warn] ${hr}`,
'[Warn] Using file ignore patterns from:',
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
'[Warn] The --gitignore option was used, but no .gitignore files were found.',
'[Warn] The --gitignore option is deprecated and will be removed in the next major',
'[Warn] version release. It prevents the use of a better dockerignore parser and',
'[Warn] filter library that fixes several issues on Windows and improves compatibility',
"[Warn] with 'docker build'. For more information, see 'balena help push'.",
`[Warn] ${hr}`,
...commonResponseLines[responseFilename],
]
: commonResponseLines[responseFilename];
const expectedResponseLines = commonResponseLines[responseFilename];
await testPushBuildStream({
builderMock: builder,
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} ${
isV13() ? '' : '--gitignore'
}`,
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath}`,
expectedFiles,
expectedQueryParams: commonQueryParams,
expectedResponseLines,

View File

@ -4,6 +4,7 @@
"belongs_to__application": [
{
"app_name": "test app",
"slug": "org/test app",
"__metadata": {}
}
],

View File

@ -4,6 +4,7 @@
"belongs_to__application": [
{
"app_name": "test app",
"slug": "org/test app",
"__metadata": {}
}
],

View File

@ -4,6 +4,7 @@
"belongs_to__application": [
{
"app_name": "test app",
"slug": "org/test app",
"__metadata": {}
}
],

View File

@ -1,101 +0,0 @@
import { expect } from 'chai';
import * as _ from 'lodash';
import * as path from 'path';
import { FileIgnorer, IgnoreFileType } from '../../build/utils/ignore';
// Note that brack notation is used intentionally when accessing private members
// of the FileIgnorer class to prevent a Typescript compilation error (this
// behavior is by design: see
// https://github.com/microsoft/TypeScript/issues/19335 )
//
// v13: delete this file
//
describe('File ignorer', function () {
it('should detect ignore files', function () {
const f = new FileIgnorer(`.${path.sep}`);
expect(f.getIgnoreFileType('.gitignore')).to.equal(
IgnoreFileType.GitIgnore,
);
expect(f.getIgnoreFileType('.dockerignore')).to.equal(
IgnoreFileType.DockerIgnore,
);
expect(f.getIgnoreFileType('./.gitignore')).to.equal(
IgnoreFileType.GitIgnore,
);
expect(f.getIgnoreFileType('./.dockerignore')).to.equal(
IgnoreFileType.DockerIgnore,
);
// gitignore files can appear in subdirectories, but dockerignore files cannot
expect(f.getIgnoreFileType('./subdir/.gitignore')).to.equal(
IgnoreFileType.GitIgnore,
);
expect(f.getIgnoreFileType('./subdir/.dockerignore')).to.equal(null);
expect(f.getIgnoreFileType('./subdir/subdir2/.gitignore')).to.equal(
IgnoreFileType.GitIgnore,
);
expect(f.getIgnoreFileType('file')).to.equal(null);
return expect(f.getIgnoreFileType('./file')).to.equal(null);
});
it('should filter files from the root directory', function () {
const ignore = new FileIgnorer(`.${path.sep}`);
ignore['gitIgnoreEntries'] = [
{ pattern: '*.ignore', filePath: '.gitignore' },
];
ignore['dockerIgnoreEntries'] = [
{ pattern: '*.ignore2', filePath: '.dockerignore' },
];
const files = [
'a',
'a/b',
'a/b/c',
'file.ignore',
'file2.ignore',
'file.ignore2',
'file2.ignore',
];
return expect(_.filter(files, ignore.filter.bind(ignore))).to.deep.equal([
'a',
'a/b',
'a/b/c',
]);
});
return it('should filter files from subdirectories', function () {
const ignore = new FileIgnorer(`.${path.sep}`);
ignore['gitIgnoreEntries'] = [
{ pattern: '*.ignore', filePath: 'lib/.gitignore' },
];
let files = [
'test.ignore',
'root.ignore',
'lib/normal-file',
'lib/should.ignore',
'lib/thistoo.ignore',
];
expect(_.filter(files, ignore.filter.bind(ignore))).to.deep.equal([
'test.ignore',
'root.ignore',
'lib/normal-file',
]);
ignore['gitIgnoreEntries'] = [
{ pattern: '*.ignore', filePath: './lib/.gitignore' },
];
files = [
'test.ignore',
'root.ignore',
'lib/normal-file',
'lib/should.ignore',
'lib/thistoo.ignore',
];
return expect(_.filter(files, ignore.filter.bind(ignore))).to.deep.equal([
'test.ignore',
'root.ignore',
'lib/normal-file',
]);
});
});

View File

@ -33,8 +33,6 @@ interface TarFiles {
};
}
const itSkipWindows = process.platform === 'win32' ? it.skip : it;
describe('compare new and old tarDirectory implementations', function () {
const extraContent = 'extra';
const extraEntry: tar.Headers = {
@ -82,7 +80,6 @@ describe('compare new and old tarDirectory implementations', function () {
const tarPack = await tarDirectory(dockerignoreProjDir, {
preFinalizeCallback,
nogitignore: true,
});
const fileList = await getTarPackFiles(tarPack);
@ -105,67 +102,11 @@ describe('compare new and old tarDirectory implementations', function () {
'symlink-a.txt': { fileSize: 5, type: 'file' },
};
const tarPack = await tarDirectory(projectPath, { nogitignore: true });
const tarPack = await tarDirectory(projectPath, {});
const fileList = await getTarPackFiles(tarPack);
expect(fileList).to.deep.equal(expectedFiles);
});
// Skip Windows because the old tarDirectory() implementation (still used when
// '--gitignore' is provided) uses the old `zeit/dockerignore` npm package
// that is broken on Windows (reason why we created `@balena/dockerignore`).
itSkipWindows('should produce a compatible tar stream', async function () {
const dockerignoreProjDir = path.join(
projectsPath,
'no-docker-compose',
'dockerignore1',
);
const oldTarPack = await tarDirectory(dockerignoreProjDir, {
preFinalizeCallback,
nogitignore: false,
});
const oldFileList = await getTarPackFiles(oldTarPack);
const newTarPack = await tarDirectory(dockerignoreProjDir, {
preFinalizeCallback,
nogitignore: true,
});
const newFileList = await getTarPackFiles(newTarPack);
const gitIgnored = ['a.txt', 'src/src-a.txt', 'src/src-c.txt'];
expect({
...newFileList,
..._.pick(oldFileList, ['.git/bar.txt']),
}).to.deep.equal({
...oldFileList,
..._.pick(newFileList, gitIgnored),
});
});
itSkipWindows(
'should produce a compatible tar stream (symbolic links)',
async function () {
const dockerignoreProjDir = path.join(
projectsPath,
'no-docker-compose',
'dockerignore2',
);
const oldTarPack = await tarDirectory(dockerignoreProjDir, {
preFinalizeCallback,
nogitignore: false,
});
const oldFileList = await getTarPackFiles(oldTarPack);
const newTarPack = await tarDirectory(dockerignoreProjDir, {
preFinalizeCallback,
nogitignore: true,
});
const newFileList = await getTarPackFiles(newTarPack);
expect(newFileList).to.deep.equal(oldFileList);
},
);
});
async function getTarPackFiles(