Compare commits

..

1 Commits

Author SHA1 Message Date
ab1d8aa6ba (v14) Migrate tabular commands to new output framework
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2022-03-25 18:14:57 +01:00
113 changed files with 3238 additions and 6175 deletions

View File

@ -1,38 +0,0 @@
---
name: cleanup
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
# --- custom environment
VERBOSE:
type: string
default: "true"
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
# delete draft releases if the pull request is closed without merging
- name: Delete draft release
if: |
runner.os == 'Linux' &&
github.event_name == 'pull_request' &&
github.event.pull_request.merged == false &&
github.event.action == 'closed'
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
gh release delete --yes '${{ github.event.pull_request.head.ref }}' || true
env:
GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}

View File

@ -1,54 +0,0 @@
---
name: publish GitHub release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
- name: Get release version
if: runner.os == 'Linux'
id: get_release
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
# https://docs.github.com/en/rest/releases
- name: Finalize GitHub release
if: runner.os == 'Linux'
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
previous_tag="$(git tag --sort=-version:refname | head -n 2 | tail -n 1)"
release_notes="$(git log ${previous_tag}..HEAD --pretty=reference)"
gh release edit '${{ github.event.pull_request.head.ref }}' \
--notes "${release_notes}" \
--title 'v${{ steps.get_release.outputs.version }}' \
--tag 'v${{ steps.get_release.outputs.version }}' \
--prerelease=false \
--draft=false
release_id="$(gh api "/repos/${{ github.repository }}/releases/tags/v${{ steps.get_release.outputs.version }}" \
-H 'Accept: application/vnd.github+json' | jq -r .id)"
gh api --method PATCH "/repos/${{ github.repository }}/releases/${release_id}" \
-H 'Accept: application/vnd.github+json' \
-F make_latest="true"
env:
GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}

View File

@ -1,153 +0,0 @@
---
name: package and draft GitHub release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
# --- custom environment
XCODE_APP_LOADER_EMAIL:
type: string
default: "accounts+apple@balena.io"
NODE_VERSION:
type: string
# FIXME: (please) https://github.com/balena-io/balena-cli/issues/2165
default: "12.x"
VERBOSE:
type: string
default: "true"
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
- name: Download custom source artifact
uses: actions/download-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
path: ${{ runner.temp }}
- name: Extract custom source artifact
shell: pwsh
working-directory: .
run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Install additional tools
if: runner.os == 'Windows'
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
choco install yq
- name: Install additional tools
if: runner.os == 'macOS'
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
brew install coreutils
# https://www.electron.build/code-signing.html
# https://github.com/Apple-Actions/import-codesign-certs
- name: Import Apple code signing certificate
if: runner.os == 'macOS'
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
- name: Import Windows code signing certificate
if: runner.os == 'Windows'
shell: powershell
run: |
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
Remove-Item -path ${{ runner.temp }} -include certificate.base64
Import-PfxCertificate `
-FilePath ${{ runner.temp }}/certificate.pfx `
-CertStoreLocation Cert:\CurrentUser\My `
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
env:
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
# https://github.com/product-os/scripts/tree/master/shared
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
- name: Package release
id: package_release
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
if [[ $runner_os =~ darwin|macos|osx ]]; then
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
CSC_KEYCHAIN=signing_temp
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
elif [[ $runner_os =~ windows|win ]]; then
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
CSC_LINK='${{ runner.temp }}\certificate.pfx'
# patches/all/oclif.patch
MSYSSHELLPATH="$(which bash)"
MSYSTEM=MSYS
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
fi
npm run package
find dist -type f -maxdepth 1
echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
env:
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
CSC_FOR_PULL_REQUEST: true
# https://sectigo.com/resource-library/time-stamping-server
TIMESTAMP_SERVER: http://timestamp.sectigo.com
# Apple notarization (automation/build-bin.ts)
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
# https://github.com/softprops/action-gh-release#-customizing
- name: Create draft GitHub (pre)release
uses: softprops/action-gh-release@v1
with:
# use PR branch name for draft releases
name: ${{ github.event.pull_request.head.ref }}
tag_name: ${{ github.event.pull_request.head.ref }}
draft: true
prerelease: true
token: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
files: |
dist/*.pkg
dist/*.exe
dist/*.zip
- name: Compress custom source
shell: pwsh
run: tar -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
path: ${{ runner.temp }}/custom.tgz
retention-days: 1

View File

@ -1,70 +0,0 @@
---
name: test release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
# --- custom environment
NODE_VERSION:
type: string
# FIXME: (please) https://github.com/balena-io/balena-cli/issues/2165
default: "12.x"
VERBOSE:
type: string
default: "true"
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
- name: Delete previous draft release
if: runner.os == 'Linux'
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
gh release delete --yes '${{ github.event.pull_request.head.ref }}' || true
env:
GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
# https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Test release
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
if [[ -e package-lock.json ]]; then
npm ci
else
npm i
fi
npm run build
npm run test
- name: Compress custom source
shell: pwsh
run: tar -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
path: ${{ runner.temp }}/custom.tgz
retention-days: 1

View File

@ -1,16 +0,0 @@
name: Flowzone
on:
pull_request:
types: [opened, synchronize, closed]
branches:
- "main"
- "master"
jobs:
flowzone:
name: Flowzone
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
secrets: inherit
with:
tests_run_on: '["ubuntu-latest","macos-latest","windows-2019"]'

15
.resinci.yml Normal file
View File

@ -0,0 +1,15 @@
---
npm:
platforms:
- name: linux
os: ubuntu
architecture: x86_64
node_versions:
- "12"
- "14"
- name: linux
os: alpine
architecture: x86_64
node_versions:
- "12"
- "14"

File diff suppressed because it is too large Load Diff

View File

@ -4,521 +4,6 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/).
## 14.5.4 - 2022-11-10
* Error when the device type and image parameters do not match [Thodoris Greasidis]
## 14.5.3 - 2022-11-10
* Switch to Flowzone [ab77]
## 14.5.2 - 2022-10-21
* Stop waiting for the analytics response [Thodoris Greasidis]
## 14.5.1 - 2022-10-20
* Bump parse-link-header from 1.0.1 to 2.0.0 [dependabot[bot]]
## 14.5.0 - 2022-10-18
* keeps events loggiging with default message [Otávio Jacobi]
* uses amplitude data events format [Otávio Jacobi]
* changes analytics endpoint to analytics-backend [Otávio Jacobi]
## 14.4.4 - 2022-10-18
* Update simple-git to 3.14.1 [Thodoris Greasidis]
## 14.4.3 - 2022-10-17
* config generate: Fix the incompatible arch errors showing as not found [Thodoris Greasidis]
## 14.4.2 - 2022-10-17
* Stop relying on device-type.json for resolving the device type aliases [Thodoris Greasidis]
* Stop relying on device-type.json for resolving the cpu architecture [Thodoris Greasidis]
## 14.4.1 - 2022-10-12
* balena os initialize: Clarify that the process includes flashing [Heath Raftery]
## 14.4.0 - 2022-10-12
* device register: Add support for the `--deviceType` option [Thodoris Greasidis]
<details>
<summary> Update balena-sdk to 16.28.0 [Thodoris Greasidis] </summary>
> ### balena-sdk-16.28.0 - 2022-10-12
>
> * device.register: Allow providing a device type for the registered device [Thodoris Greasidis]
>
> ### balena-sdk-16.27.0 - 2022-10-07
>
> * Add support for batch operations for more device modifying methods [Thodoris Greasidis]
>
> ### balena-sdk-16.26.7 - 2022-10-07
>
> * Fix request batching chunking when there is no grouping navigation prop [Thodoris Greasidis]
> * request-batching: Increase the batch size to 200 items [Thodoris Greasidis]
>
> ### balena-sdk-16.26.6 - 2022-10-06
>
> * Fix request batching not chunking the items of the operation [Thodoris Greasidis]
>
> ### balena-sdk-16.26.5 - 2022-09-26
>
> * Delete redundant .resinci.yml [Pagan Gazzard]
>
> ### balena-sdk-16.26.4 - 2022-09-23
>
> * Remove moment in favor of date-fns [Matthew Yarmolinsky]
>
> ### balena-sdk-16.26.3 - 2022-09-21
>
> * Skip running tests in flowzone till we can inject env vars [Thodoris Greasidis]
> * Switch from balenaCI to flowzone [Pagan Gazzard]
>
> ### balena-sdk-16.26.2 - 2022-09-06
>
>
> <details>
> <summary> Update balena-register-device to 8.0.0 [Thodoris Greasidis] </summary>
>
>> #### balena-register-device-8.0.0 - 2022-09-06
>>
>> * Remove the travis & appveyor configurations [Thodoris Greasidis]
>> * tsconfig: Enable strict type checking [Thodoris Greasidis]
>> * Update devDependencies [Thodoris Greasidis]
>> * Update the uuid package to v9 [Thodoris Greasidis]
>> * Prevent creating a package-lock.json [Thodoris Greasidis]
>> * Drop support for node 10 in favor of 14 & 16 [Thodoris Greasidis]
>>
>> #### balena-register-device-7.2.0 - 2021-04-29
>>
>> * Support `supervisorVersion`/`osVersion`/`osVariant`/`macAddress` fields [Pagan Gazzard]
>>
>> #### balena-register-device-7.1.1 - 2021-04-29
>>
>> * Update dependencies [Pagan Gazzard]
>>
> </details>
>
>
> ### balena-sdk-16.26.1 - 2022-08-29
>
> * Update TypeScript to v4.8 [Thodoris Greasidis]
>
> ### balena-sdk-16.26.0 - 2022-08-26
>
> * Pin TypeScript to 4.7 until upstream dependencies are updated [Thodoris Greasidis]
> * types: Add the InvitationTokenDecodedPayload type [Thodoris Greasidis]
>
> ### balena-sdk-16.25.1 - 2022-08-05
>
> * Deprecate the public_key from the user JWT [Thodoris Greasidis]
>
> ### balena-sdk-16.25.0 - 2022-08-04
>
> * application.remove: Support batch deletions by providing multiple IDs [Thodoris Greasidis]
> * Refactor the request batching implementation to be generic [Thodoris Greasidis]
> * Change pine options merging to extend the default `$select`ed properties [Thodoris Greasidis]
>
> ### balena-sdk-16.24.2 - 2022-08-02
>
> * Refactor the internal mergePineOptions utility [Thodoris Greasidis]
>
> ### balena-sdk-16.24.1 - 2022-07-21
>
> * Update Husky to v7 [Thodoris Greasidis]
>
> ### balena-sdk-16.24.0 - 2022-07-08
>
> * types: Add missing Application to Service relation [Thodoris Greasidis]
>
> ### balena-sdk-16.23.0 - 2022-07-07
>
> * Add expiry-date for generation of user and device keys [Nitish Agarwal]
>
</details>
## 14.3.1 - 2022-09-06
* Add unified OS versions in the examples of the device & os commands [Thodoris Greasidis]
## 14.3.0 - 2022-08-17
* release: Add `validate` command for validating releases [Matthew Yarmolinsky]
* release: Add `invalidate` command for invalidating releases [Matthew Yarmolinsky]
## 14.2.0 - 2022-08-16
* fleet: Add `track-latest` command for tracking the latest release [Matthew Yarmolinsky]
* fleet: Add `pin` command for pinning fleets to a specific release [Matthew Yarmolinsky]
## 14.1.0 - 2022-08-03
* Add device track command for pinning a device to the latest release or a specific release [Matthew Yarmolinsky]
## 14.0.0 - 2022-08-01
* Drop undocumented support for numeric ids in balena device commands [Matthew Yarmolinsky]
* Drop support for the deprecated `balena device public-url <enable|disable|status> <uuid>` and related format [Matthew Yarmolinsky]
* Drop support for numeric fleet id parameters from all commands [Matthew Yarmolinsky]
* fleet: Add `--filter`, `--no-header`, `--no-truncate`, and `--sort` options [Matthew Yarmolinsky]
* fleet: Add `--fields` and `--json` options [Matthew Yarmolinsky]
* fleet: Use the oclif output formatter [Matthew Yarmolinsky]
* config: Drop optional and ignored `--type` flag [Matthew Yarmolinsky]
* Drop deprecated `--logs` flag [Matthew Yarmolinsky]
* Drop support for open-balena-api < v0.131.0 [Matthew Yarmolinsky]
## 13.10.1 - 2022-08-01
* Fix balena deploy missing dependency error [Thodoris Greasidis]
## 13.10.0 - 2022-07-20
* Add `--view` flag to `device` command for opening a device's dashboard page [Matthew Yarmolinsky]
## 13.9.0 - 2022-07-19
* Switch to balena-compose [Akis Kesoglou]
## 13.8.0 - 2022-07-18
* Add `--note` option for `push` and `deploy` [Matthew Yarmolinsky]
## 13.7.1 - 2022-07-13
* os download: Fix resolving to draft releases [Thodoris Greasidis]
## 13.7.0 - 2022-07-07
* Add `--view` flag to `fleet` command for opening a fleet's dashboard page [Matthew Yarmolinsky]
## 13.6.1 - 2022-06-09
<details>
<summary> Update balena-sdk to use the native OS release phase & variant fields [Thodoris Greasidis] </summary>
> ### balena-sdk-16.22.0 - 2022-06-06
>
> * os: Start using the release.phase field in the available versions [Thodoris Greasidis]
>
> ### balena-sdk-16.21.1 - 2022-06-02
>
> * Add provisioning key expiry date to generateDeviceProvisioningKey [Nitish Agarwal]
>
> ### balena-sdk-16.21.0 - 2022-06-01
>
> * os: Refactor the computation of OS releases [Thodoris Greasidis]
> * os: Use the model's release variant when the native fields are used [Thodoris Greasidis]
>
> ### balena-sdk-16.20.6 - 2022-06-01
>
> * Deprecate the needsPasswordReset field of the JWTUser [Thodoris Greasidis]
>
> ### balena-sdk-16.20.5 - 2022-05-25
>
> * Update TypeScript to v4.7 [Thodoris Greasidis]
>
</details>
## 13.6.0 - 2022-06-06
* Update QEMU to v7.0.0 [Kyle Harding]
## 13.5.3 - 2022-05-31
* Drop the needsPasswordReset property from the tests [Thodoris Greasidis]
## 13.5.2 - 2022-05-31
* Deduplicate npm-shrinkwrap.json [Thodoris Greasidis]
## 13.5.1 - 2022-05-26
* preload: Fix issue where balenaOS v2.98.3+ required an Internet connection to start apps [pipex]
## 13.5.0 - 2022-05-24
<details>
<summary> Update balena-sdk to 16.20.4 [Nitish Agarwal] </summary>
> ### balena-sdk-16.20.4 - 2022-05-09
>
> * bump @types/node from 10.17.60 to 12.20.500 [Thodoris Greasidis]
>
> ### balena-sdk-16.20.3 - 2022-05-06
>
> * patch: bump browserify from 14.5.0 to 17.0.0 [dependabot[bot]]
>
> ### balena-sdk-16.20.2 - 2022-05-05
>
> * patch: bump tmp from 0.0.31 to 0.2.1 [dependabot[bot]]
>
> ### balena-sdk-16.20.1 - 2022-05-05
>
> * Drop the non-populated apiUrl & actionsUrl properties from Config type [Thodoris Greasidis]
>
> ### balena-sdk-16.20.0 - 2022-05-04
>
> * models.apiKey: Update apiKeyInfo with expiryDate option [Nitish Agarwal]
> * os.getConfig: Add typings for the provisioningKeyExpiryDate option [Balena CI]
>
> ### balena-sdk-16.19.14 - 2022-05-04
>
> * config.getAll: Mark the deviceTypes property as optional [Thodoris Greasidis]
>
> ### balena-sdk-16.19.13 - 2022-05-03
>
> * patch: bump mocha from 3.5.3 to 10.0.0 [dependabot[bot]]
>
> ### balena-sdk-16.19.12 - 2022-05-03
>
> * config.getAll: Deprecate the pubnub property and mark as optional [Thodoris Greasidis]
>
> ### balena-sdk-16.19.11 - 2022-05-03
>
> * patch: bump mockttp from 0.9.1 to 2.7.0 [Thodoris Greasidis]
>
> ### balena-sdk-16.19.10 - 2022-04-27
>
> * Reduce the prod typing dependencies [Thodoris Greasidis]
>
> ### balena-sdk-16.19.9 - 2022-04-26
>
> * patch: Remove documentation.md from the NPM package [Vipul Gupta]
>
> ### balena-sdk-16.19.8 - 2022-04-20
>
> * patch: Remove additional quotes [Vipul Gupta (@vipulgupta2048)]
>
> ### balena-sdk-16.19.7 - 2022-04-12
>
> * tests: Update to work with latest major of superagent [Thodoris Greasidis]
> * patch: bump superagent from 3.8.3 to 7.1.2 [dependabot[bot]]
>
> ### balena-sdk-16.19.6 - 2022-04-11
>
> * patch: bump dotenv from 4.0.0 to 16.0.0 [dependabot[bot]]
>
> ### balena-sdk-16.19.5 - 2022-04-09
>
> * Bump karma to v6 [Thodoris Greasidis]
>
> ### balena-sdk-16.19.4 - 2022-04-09
>
> * Add dependabot configuration [Thodoris Greasidis]
>
> ### balena-sdk-16.19.3 - 2022-04-06
>
> * tests: Update v5 model endpoint prefix references [Thodoris Greasidis]
>
> ### balena-sdk-16.19.2 - 2022-04-06
>
>
> <details>
> <summary> Fix extracting a meaningful error message instead of "[object Object]" [Thodoris Greasidis] </summary>
>
>> #### balena-request-11.5.5 - 2022-04-06
>>
>> * Fix extracting the response error from object response bodies [Thodoris Greasidis]
>>
>> #### balena-request-11.5.4 - 2022-04-06
>>
>> * Drop explicit karma-chrome-launcher devDependency [Thodoris Greasidis]
>>
> </details>
>
>
> ### balena-sdk-16.19.1 - 2022-04-05
>
> * Update balena-request dependency to v11.5.3 [Matthew Yarmolinsky]
>
> ### balena-sdk-16.19.0 - 2022-03-16
>
> * Add release.setKnownIssueList function for setting a release's known issue list [Matthew Yarmolinsky]
>
> ### balena-sdk-16.18.0 - 2022-03-14
>
> * minor: Add trying SDK in the browser [Vipul Gupta (@vipulgupta2048)]
>
> ### balena-sdk-16.17.0 - 2022-03-11
>
> * device.getWithServiceDetails: Add the release id in the service info [Matthew Yarmolinsky]
>
> ### balena-sdk-16.16.1 - 2022-03-08
>
> * Replace internal use of deprecated OsVersion.rawVersion with raw_version [Thodoris Greasidis]
>
> ### balena-sdk-16.16.0 - 2022-03-03
>
> * Add support for named imports from .mjs files [Thodoris Greasidis]
> * Update npx command to fix ts-compatibility tests [Thodoris Greasidis]
> * Regenerate Documentation [Thodoris Greasidis]
> * Update typescript to 4.6.2 [Thodoris Greasidis]
>
> ### balena-sdk-16.15.1 - 2022-02-24
>
> * Remove unnecessary vpn address filtering when fetching local addresses [Pagan Gazzard]
>
> ### balena-sdk-16.15.0 - 2022-02-16
>
> * Add applicationClass parameter to application create function for setting is_of__class property [Matthew Yarmolinsky]
>
> ### balena-sdk-16.14.0 - 2022-02-15
>
> * Add name and description field to generateDeviceKey for device. [Nitish Agarwal]
>
> ### balena-sdk-16.13.4 - 2022-01-27
>
> * typings: Fix conditional $or/$and/$not $filters [Thodoris Greasidis]
>
> ### balena-sdk-16.13.3 - 2022-01-27
>
> * Deprecate the supportsBlink field of the DeviceTypeJson.DeviceType type [Thodoris Greasidis]
>
> ### balena-sdk-16.13.2 - 2022-01-25
>
> * Deprecate the logoUrl field of the DeviceTypeJson.DeviceType type [Thodoris Greasidis]
>
> ### balena-sdk-16.13.1 - 2022-01-21
>
> * Replace internal use of release.contains__image with release_image [Thodoris Greasidis]
>
> ### balena-sdk-16.13.0 - 2022-01-21
>
> * models: Deprecate the release.contains__image in favor of the term form [Thodoris Greasidis]
> * models: Add the release_image term form property in the Release typings [Thodoris Greasidis]
>
> ### balena-sdk-16.12.1 - 2022-01-17
>
> * config.getConfigVarSchema: Send the token only when using a device type [Thodoris Greasidis]
>
> ### balena-sdk-16.12.0 - 2022-01-10
>
> * Replace DeviceTypeJson usage for alias resolution with model queries [Thodoris Greasidis]
> * models/device-type: Support aliases as argument of the get() method [Thodoris Greasidis]
>
> ### balena-sdk-16.11.3 - 2022-01-09
>
> * Fix jsdoc example for balena.errors [Ken Bannister]
>
> ### balena-sdk-16.11.2 - Invalid date
>
> * tests: Convert auth spec to async await [Thodoris Greasidis]
>
> ### balena-sdk-16.11.1 - Invalid date
>
> * Fix buggy tests causing flakiness on node 16 [Thodoris Greasidis]
>
> ### balena-sdk-16.11.0 - Invalid date
>
> * Alias device.getManifestBySlug as config.getDeviceTypeManifestBySlug [Thodoris Greasidis]
> * Deprecate device.getManifestByApplication [Thodoris Greasidis]
>
> ### balena-sdk-16.10.0 - Invalid date
>
> * application.get: Add support for retrieving applications by uuid [Thodoris Greasidis]
> * package.json: Rename the lint-fix npm script to lint:fix [Thodoris Greasidis]
>
> ### balena-sdk-16.9.4 - 2021-12-29
>
> * os: Avoid mutating the args in getAvailableOsVersions & getAllOsVersion [Thodoris Greasidis]
>
> ### balena-sdk-16.9.3 - 2021-12-28
>
> * os: Replace semver normalization with balena-semver [Thodoris Greasidis]
>
> ### balena-sdk-16.9.2 - 2021-12-28
>
> * Stop relying on the balena-pine module [Thodoris Greasidis]
>
> ### balena-sdk-16.9.1 - 2021-12-28
>
> * Enable nested changelogs for balena-hup-action-utils [Thodoris Greasidis]
>
</details>
* Add provisioning key expiry date option to config generate options [Balena CI]
## 13.4.3 - 2022-05-19
<details>
<summary> Update docker-progress to 5.1.3 [Pagan Gazzard] </summary>
> ### docker-progress-5.1.3 - 2022-05-11
>
> * Reject on the stream closing if it has not already ended successfully [Pagan Gazzard]
>
> ### docker-progress-5.1.2 - 2022-05-10
>
> * Update dependencies [Pagan Gazzard]
>
> ### docker-progress-5.1.1 - 2022-05-10
>
> * Avoid breaking changes to PushPullOptions required properties [Kyle Harding]
>
> ### docker-progress-5.1.0 - 2022-03-10
>
> * Add support for building images with progress [Felipe Lalanne]
>
</details>
## 13.4.2 - 2022-05-10
<details>
<summary> preload: Fix detection of supervisor version for balenaOS v2.93.0 [Kyle Harding] </summary>
> ### balena-preload-12.0.1 - 2022-05-10
>
> * Update supervisor image regex to include tagged images [Kyle Harding]
>
</details>
## 13.4.1 - 2022-04-11
* leave: Update log message to advise that device still needs deleting [Taro Murao]
## 13.4.0 - 2022-04-08
* deploy: Support all valid semver versions in balena.yml [Thodoris Greasidis]
## 13.3.3 - 2022-04-08
* Document the 'patches' folder in CONTRIBUTING.md [Paulo Castro]
## 13.3.2 - 2022-04-07
* Skip Alpine tests until Concourse + Alpine v3.14 issues are resolved [Paulo Castro]
* build: Ensure HTTPS is used with dockerPort 2376 or with ca/cert/key [Paulo Castro]
## 13.3.1 - 2022-03-08
* Include link to Wiki release notes in version update notifications [Paulo Castro]
## 13.3.0 - 2022-02-28
* ssh: Allow ssh to service with IP address and production balenaOS image [Paulo Castro]
* ssh: Advise use of 'balena login' if root authentication fails [Paulo Castro]
## 13.2.1 - 2022-02-24
* Remove unnecessary fetch of device info in `balena tunnel` [Pagan Gazzard]
* Correctly use the device uuid when logging the tunnel target [Pagan Gazzard]
## 13.2.0 - 2022-02-12
* ssh: Attempt cloud username if 'root' authentication fails [Paulo Castro]
* Replace occurrence of through2 dependency with standard stream module [Paulo Castro]
* Refactor cached username logic from events.ts to bootstrap.ts for reuse [Paulo Castro]
## 13.1.13 - 2022-02-10
* Drop unused awaitDevice utility function [Lucian Buzzo]

View File

@ -125,39 +125,6 @@ The README file is manually edited, but subsections are automatically extracted
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
## Patches folder
The `patches` folder contains patch files created with the
[patch-package](https://www.npmjs.com/package/patch-package) tool. Small code changes to
third-party modules can be made by directly editing Javascript files under the `node_modules`
folder and then running `patch-package` to create the patch files. The patch files are then
applied immediately after `npm install`, through the `postinstall` script defined in
`package.json`.
The subfolders of the `patches` folder are documented in the
[apply-patches.js](https://github.com/balena-io/balena-cli/blob/master/patches/apply-patches.js)
script.
To make changes to the patch files under the `patches` folder, **do not edit them directly,**
not even for a "single character change" because the hash values in the patch files also need
to be recomputed by `patch-packages`. Instead, edit the relevant files under `node_modules`
directly, and then run `patch-packages` with the `--patch-dir` option to specify the subfolder
where the patch should be saved. For example, edit `node_modules/exit-hook/index.js` and then
run:
```sh
$ npx patch-package --patch-dir patches/all exit-hook
```
That said, these kinds of patches should be avoided in favour of creating pull requests
upstream. Patch files create additional maintenance work over time as the patches need to be
updated when the dependencies are updated, and they prevent the compounding community benefit
that sharing fixes upstream have on open source projects like the balena CLI. The typical
scenario where these patches are used is when the upstream maintainers are unresponsive or
unwilling to merge the required fixes, the fixes are very small and specific to the balena CLI,
and creating a fork of the upstream repo is likely to be more long-term effort than maintaining
the patches.
## Windows
Besides the regular npm installation dependencies, the `npm run build:installer` script

View File

@ -45,6 +45,8 @@ const execFileAsync = promisify(execFile);
export const packageJSON = loadPackageJson();
export const version = 'v' + packageJSON.version;
const arch = process.arch;
const MSYS2_BASH =
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
function dPath(...paths: string[]) {
return path.join(ROOT, 'dist', ...paths);
@ -423,28 +425,20 @@ async function renameInstallerFiles() {
/**
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
* executable installer using Microsoft SignTool.exe (Sign Tool)
* https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
* executable installer by running the balena-io/scripts/shared/sign-exe.sh
* script (which must be in the PATH) using a MSYS2 bash shell.
*/
async function signWindowsInstaller() {
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
const exeName = renamedOclifInstallers[process.platform];
console.log(`Signing installer "${exeName}"`);
// trust ...
await execFileAsync('signtool.exe', [
'sign',
'-t',
process.env.TIMESTAMP_SERVER || 'http://timestamp.comodoca.com',
await execFileAsync(MSYS2_BASH, [
'sign-exe.sh',
'-f',
process.env.CSC_LINK,
'-p',
process.env.CSC_KEY_PASSWORD,
exeName,
'-d',
`balena-cli ${version}`,
exeName,
]);
// ... but verify
await execFileAsync('signtool.exe', ['verify', '-pa', '-v', exeName]);
} else {
console.log(
'Skipping installer signing step because CSC_* env vars are not set',
@ -456,21 +450,14 @@ async function signWindowsInstaller() {
* Wait for Apple Installer Notarization to continue
*/
async function notarizeMacInstaller(): Promise<void> {
const appleId =
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
const appBundleId = packageJSON.oclif.macos.identifier || 'io.balena.cli';
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
if (appleIdPassword) {
const { notarize } = await import('electron-notarize');
// https://github.com/electron/notarize/blob/main/README.md
await notarize({
appBundleId,
appPath: renamedOclifInstallers.darwin,
appleId,
appleIdPassword,
});
}
const appleId = 'accounts+apple@balena.io';
const { notarize } = await import('electron-notarize');
await notarize({
appBundleId: 'io.balena.etcher',
appPath: renamedOclifInstallers.darwin,
appleId,
appleIdPassword: '@keychain:CLI_PASSWORD',
});
}
/**

View File

@ -15,12 +15,11 @@
* limitations under the License.
*/
// tslint:disable-next-line:import-blacklist
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import { promises as fs } from 'fs';
import * as path from 'path';
import { simpleGit } from 'simple-git';
const stripIndent = require('common-tags/lib/stripIndent');
const _ = require('lodash');
const { promises: fs } = require('fs');
const path = require('path');
const simplegit = require('simple-git/promise');
const ROOT = path.normalize(path.join(__dirname, '..'));
@ -32,7 +31,7 @@ const ROOT = path.normalize(path.join(__dirname, '..'));
* using `touch`.
*/
async function checkBuildTimestamps() {
const git = simpleGit(ROOT);
const git = simplegit(ROOT);
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
const [docStat, gitStatus] = await Promise.all([
fs.stat(docFile),
@ -82,5 +81,4 @@ async function run() {
}
}
// tslint:disable-next-line:no-floating-promises
run();

View File

@ -12,15 +12,15 @@ _balena() {
# Sub-completions
api_key_cmds=( generate )
config_cmds=( generate inject read reconfigure write )
device_cmds=( deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet )
device_cmds=( deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown )
devices_cmds=( supported )
env_cmds=( add rename rm )
fleet_cmds=( create pin purge rename restart rm track-latest )
fleet_cmds=( create purge rename restart rm )
internal_cmds=( osinit )
key_cmds=( add rm )
local_cmds=( configure flash )
os_cmds=( build-config configure download initialize versions )
release_cmds=( finalize invalidate validate )
release_cmds=( finalize )
tag_cmds=( rm set )

View File

@ -11,15 +11,15 @@ _balena_complete()
# Sub-completions
api_key_cmds="generate"
config_cmds="generate inject read reconfigure write"
device_cmds="deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet"
device_cmds="deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown"
devices_cmds="supported"
env_cmds="add rename rm"
fleet_cmds="create pin purge rename restart rm track-latest"
fleet_cmds="create purge rename restart rm"
internal_cmds="osinit"
key_cmds="add rm"
local_cmds="configure flash"
os_cmds="build-config configure download initialize versions"
release_cmds="finalize invalidate validate"
release_cmds="finalize"
tag_cmds="rm set"

View File

@ -361,7 +361,7 @@ field to sort by (prepend '-' for descending order)
Display detailed information about a single fleet.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -369,26 +369,23 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
$ balena fleet MyFleet
$ balena fleet myorg/myfleet
$ balena fleet myorg/myfleet --view
### Arguments
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
#### --view
open fleet dashboard page
#### --fields FIELDS
only show provided fields (comma-separated)
@ -443,7 +440,7 @@ fleet device type (Check available types with `balena devices supported`)
Purge data from all devices belonging to a fleet.
This will clear the fleet's '/data' directory.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -451,7 +448,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -462,7 +461,7 @@ Examples:
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
@ -473,7 +472,7 @@ Rename a fleet.
Note, if the `newName` parameter is omitted, it will be
prompted for interactively.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -481,7 +480,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -493,7 +494,7 @@ Examples:
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### NEWNAME
@ -505,7 +506,7 @@ the new name for the fleet
Restart all devices belonging to a fleet.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -513,7 +514,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -524,7 +527,7 @@ Examples:
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
@ -534,7 +537,7 @@ Permanently remove a fleet.
The --yes option may be used to avoid interactive confirmation.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -542,7 +545,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -554,7 +559,7 @@ Examples:
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
@ -645,7 +650,7 @@ List all of your devices.
Devices can be filtered by fleet with the `--fleet` option.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -653,7 +658,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
The --json option is recommended when scripting the output of this command,
because field names are less likely to change in JSON format and because it
@ -671,11 +678,31 @@ Examples:
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
produce JSON output instead of tabular output
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
## devices supported
@ -694,9 +721,29 @@ Examples:
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
produce JSON output instead of tabular output
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
## device &#60;uuid&#62;
@ -705,7 +752,6 @@ Show information about a single device.
Examples:
$ balena device 7cf02a6
$ balena device 7cf02a6 --view
### Arguments
@ -715,9 +761,13 @@ the device uuid
### Options
#### --view
#### --fields FIELDS
open device dashboard page
only show provided fields (comma-separated)
#### -j, --json
output in json format
## device deactivate &#60;uuid&#62;
@ -783,7 +833,7 @@ If the '--fleet' or '--drive' options are omitted, interactive menus will be
presented with values to choose from. If the '--os-version' option is omitted,
the latest released OS version for the fleet's default device type will be used.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -791,7 +841,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Image configuration questions will be asked interactively unless a pre-configured
'config.json' file is provided with the '--config' option. The file can be
@ -801,14 +853,13 @@ Examples:
$ balena device init
$ balena device init -f myorg/myfleet
$ balena device init --fleet myFleet --os-version 2.101.7 --drive /dev/disk5 --config config.json --yes
$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes
### Options
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -y, --yes
@ -840,10 +891,6 @@ path to the config JSON file, see `balena os build-config`
custom key name assigned to generated provisioning api key
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
## device local-mode &#60;uuid&#62;
Output current local mode status, or enable/disable local mode
@ -882,7 +929,7 @@ Move one or more devices to another fleet.
If --fleet is omitted, the fleet will be prompted for interactively.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -890,7 +937,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -909,7 +958,7 @@ comma-separated list (no blank spaces) of device UUIDs to be moved
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
## device os-update &#60;uuid&#62;
@ -923,7 +972,6 @@ Requires balenaCloud; will not work with openBalena or standalone balenaOS.
Examples:
$ balena device os-update 23c73a1
$ balena device os-update 23c73a1 --version 2.101.7
$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod
### Arguments
@ -948,6 +996,9 @@ This command will output the current public URL for the
specified device. It can also enable or disable the URL,
or output the enabled status, using the respective options.
The old command style 'balena device public-url enable <uuid>'
is deprecated, but still supported.
Examples:
$ balena device public-url 23c73a1
@ -961,6 +1012,10 @@ Examples:
the uuid of the device to manage
#### LEGACYUUID
### Options
#### --enable
@ -1022,7 +1077,7 @@ Register a new device with a balena fleet.
If --uuid is not provided, a new UUID will be automatically assigned.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1030,20 +1085,21 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
$ balena device register MyFleet
$ balena device register MyFleet --uuid <uuid>
$ balena device register myorg/myfleet --uuid <uuid>
$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>
### Arguments
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
@ -1051,10 +1107,6 @@ fleet name or slug (preferred)
custom uuid
#### --deviceType DEVICETYPE
device type slug (run 'balena devices supported' for possible values)
## device rename &#60;uuid&#62; [newName]
Rename a device.
@ -1180,6 +1232,14 @@ fleet name or slug (preferred)
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
## release &#60;commitOrId&#62;
@ -1201,6 +1261,14 @@ the commit or ID of the release to get information
Return the release composition
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
## release finalize &#60;commitOrId&#62;
Finalize a release. Releases can be "draft" or "final", and this command
@ -1261,7 +1329,7 @@ name may be null in JSON output (or 'N/A' in tabular output) if the fleet that
the device belonged to is no longer accessible by the current user (for example,
in case the current user was removed from the fleet by the fleet's owner).
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1269,7 +1337,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -1287,7 +1357,7 @@ Examples:
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -c, --config
@ -1297,9 +1367,29 @@ show configuration variables only
device UUID
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
produce JSON output instead of tabular output
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
#### -s, --service SERVICE
@ -1397,7 +1487,7 @@ therefore the --service option cannot be used when the variable name starts
with a reserved prefix. When defining custom fleet variables, please avoid
these reserved prefixes.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1405,7 +1495,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -1433,7 +1525,7 @@ variable value; if omitted, use value from this process' environment
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -1516,7 +1608,7 @@ select a service variable (may be used together with the --device option)
List all tags and their values for the specified fleet, device or release.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1524,7 +1616,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -1538,7 +1632,7 @@ Examples:
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -1548,11 +1642,35 @@ device UUID
release id
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
## tag rm &#60;tagKey&#62;
Remove a tag from a fleet, device or release.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1560,7 +1678,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -1580,7 +1700,7 @@ the key string of the tag
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -1598,7 +1718,7 @@ You can optionally provide a value to be associated with the created
tag, as an extra argument after the tag key. If a value isn't
provided, a tag with an empty value is created.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1606,7 +1726,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -1633,7 +1755,7 @@ the optional value associated with the tag
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -1712,6 +1834,30 @@ Examples:
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
## key &#60;id&#62;
Display a single SSH key registered in balenaCloud for the logged in user.
@ -1728,6 +1874,14 @@ balenaCloud ID for the SSH key
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
## key add &#60;name&#62; [path]
Add an SSH key to the balenaCloud account of the logged in user.
@ -1918,7 +2072,7 @@ Examples:
#### FLEETORDEVICE
fleet name/slug, device uuid, or address of local device
fleet name/slug/id, device uuid, or address of local device
#### SERVICE
@ -1986,7 +2140,7 @@ Examples:
#### DEVICEORFLEET
device UUID or fleet name/slug
device UUID or fleet name/slug/ID
### Options
@ -2072,11 +2226,9 @@ Development images can be selected by appending `.dev` to the version.
Examples:
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.101.7
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2022.7.0
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.90.0
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1.dev
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.60.0
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2021.10.2.prod
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default
@ -2167,7 +2319,7 @@ 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
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -2175,7 +2327,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Note: This command is currently not supported on Windows natively. Windows users
are advised to install the Windows Subsystem for Linux (WSL) with Ubuntu, and use
@ -2204,7 +2358,7 @@ ask advanced configuration questions (when in interactive mode)
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### --config CONFIG
@ -2254,16 +2408,10 @@ paths to local files to place into the 'system-connections' directory
custom key name assigned to generated provisioning api key
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
## os initialize &#60;image&#62;
Initialize an os image for a device with a previously
configured operating system image and flash the
an external storage drive or the device's storage
medium depending on the device type.
configured operating system image.
Note: Initializing the device may ask for administrative permissions
@ -2320,7 +2468,7 @@ alongside the --deviceType option to specify the target device type.
To avoid interactive questions, specify a command line option for each question that
would otherwise be asked.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -2328,7 +2476,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -2349,7 +2499,7 @@ a balenaOS version
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### --dev
@ -2395,10 +2545,6 @@ supervisor cloud polling interval in minutes (e.g. for device variables)
custom key name assigned to generated provisioning api key
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
## config inject &#60;file&#62;
Inject a 'config.json' file to a balenaOS image file or attached SD card or
@ -2525,7 +2671,7 @@ Check also the Preloading and Preregistering section of the balena CLI's advance
masterclass document:
https://www.balena.io/docs/learn/more/masterclasses/advanced-cli/#5-preloading-and-preregistering
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -2533,7 +2679,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Note that the this command requires Docker to be installed, as further detailed
in the balena CLI's installation instructions:
@ -2559,7 +2707,7 @@ the image file path
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -c, --commit COMMIT
@ -2723,7 +2871,6 @@ Examples:
$ balena push myFleet
$ balena push myFleet --source <source directory>
$ balena push myFleet -s <source directory>
$ balena push myFleet --source <source directory> --note "this is the note for this release"
$ balena push myFleet --release-tag key1 "" key2 "value2 with spaces"
$ balena push myorg/myfleet
@ -2840,10 +2987,6 @@ by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.
#### --note NOTE
The notes for this release
# Settings
## settings
@ -2856,6 +2999,14 @@ Examples:
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
# Local
## local configure &#60;target&#62;
@ -3023,7 +3174,7 @@ the type of device this build is for
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -e, --emulated
@ -3033,6 +3184,10 @@ Use QEMU for ARM architecture emulation during the image build
Alternative Dockerfile name/path, relative to the source folder
#### --logs
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
#### --nologs
Hide the image build log output (produce less verbose output)
@ -3205,7 +3360,6 @@ Examples:
$ balena deploy myFleet
$ balena deploy myorg/myfleet --build --source myBuildDir/
$ balena deploy myorg/myfleet --build --source myBuildDir/ --note "this is the note for this release"
$ balena deploy myorg/myfleet myRepo/myImage
$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"
@ -3213,7 +3367,7 @@ Examples:
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### IMAGE
@ -3246,10 +3400,6 @@ by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.
#### --note NOTE
The notes for this release
#### -e, --emulated
Use QEMU for ARM architecture emulation during the image build
@ -3258,6 +3408,10 @@ Use QEMU for ARM architecture emulation during the image build
Alternative Dockerfile name/path, relative to the source folder
#### --logs
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
#### --nologs
Hide the image build log output (produce less verbose output)
@ -3351,7 +3505,7 @@ scan the local network for balenaOS devices and prompt you to select one
from an interactive picker. This may require administrator/root privileges.
Likewise, if the fleet option is not provided then a picker will be shown.
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -3359,7 +3513,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:
@ -3380,7 +3536,7 @@ the IP or hostname of device
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -i, --pollInterval POLLINTERVAL
@ -3436,7 +3592,7 @@ or hours, e.g. '12h', '2d'.
Both --device and --fleet flags accept multiple values, specified as
a comma-separated list (with no spaces).
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -3444,7 +3600,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
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.
Examples:

View File

@ -171,4 +171,5 @@ export default abstract class BalenaCommand extends Command {
protected outputMessage = output.outputMessage;
protected outputData = output.outputData;
protected printTitle = output.printTitle;
}

View File

@ -37,7 +37,6 @@ interface FlagsDef {
wifiKey?: string;
appUpdatePollInterval?: string;
'provisioning-key-name'?: string;
'provisioning-key-expiry-date'?: string;
help: void;
}
@ -82,11 +81,7 @@ export default class ConfigGenerateCmd extends Command {
dev: cf.dev,
device: {
...cf.device,
exclusive: [
'fleet',
'provisioning-key-name',
'provisioning-key-expiry-date',
],
exclusive: ['fleet', 'provisioning-key-name'],
},
deviceApiKey: flags.string({
description:
@ -125,11 +120,6 @@ export default class ConfigGenerateCmd extends Command {
description: 'custom key name assigned to generated provisioning api key',
exclusive: ['device'],
}),
'provisioning-key-expiry-date': flags.string({
description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
exclusive: ['device'],
}),
help: cf.help,
};
@ -150,9 +140,11 @@ export default class ConfigGenerateCmd extends Command {
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
| null = null;
if (options.device != null) {
const rawDevice = await balena.models.device.get(options.device, {
$expand: { is_of__device_type: { $select: 'slug' } },
});
const { tryAsInteger } = await import('../../utils/validation');
const rawDevice = await balena.models.device.get(
tryAsInteger(options.device),
{ $expand: { is_of__device_type: { $select: 'slug' } } },
);
if (!rawDevice.belongs_to__application) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(stripIndent`
@ -175,26 +167,26 @@ export default class ConfigGenerateCmd extends Command {
const deviceType = options.deviceType || resourceDeviceType;
const deviceManifest = await balena.models.device.getManifestBySlug(
deviceType,
);
// Check compatibility if application and deviceType provided
if (options.fleet && options.deviceType) {
const appDeviceManifest = await balena.models.device.getManifestBySlug(
resourceDeviceType,
);
const helpers = await import('../../utils/helpers');
if (
!(await helpers.areDeviceTypesCompatible(
resourceDeviceType,
deviceType,
))
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(
throw new balena.errors.BalenaInvalidDeviceType(
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
);
}
}
const deviceManifest = await balena.models.device.getManifestBySlug(
deviceType,
);
// Prompt for values
// 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)
@ -204,7 +196,6 @@ export default class ConfigGenerateCmd extends Command {
answers.version = options.version;
answers.developmentMode = options.dev;
answers.provisioningKeyName = options['provisioning-key-name'];
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
// Generate config
const { generateDeviceConfig, generateApplicationConfig } = await import(

View File

@ -57,6 +57,7 @@ export default class ConfigInjectCmd extends Command {
public static usage = 'config inject <file>';
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
};

View File

@ -47,6 +47,7 @@ export default class ConfigReadCmd extends Command {
public static usage = 'config read';
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
json: cf.json,

View File

@ -50,6 +50,7 @@ export default class ConfigReconfigureCmd extends Command {
public static usage = 'config reconfigure';
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
advanced: flags.boolean({
description: 'show advanced commands',

View File

@ -64,6 +64,7 @@ export default class ConfigWriteCmd extends Command {
public static usage = 'config write <key> <value>';
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
};

View File

@ -16,7 +16,7 @@
*/
import { flags } from '@oclif/command';
import type { ImageDescriptor } from '@balena/compose/dist/parse';
import type { ImageDescriptor } from 'resin-compose-parse';
import Command from '../command';
import { ExpectedError } from '../errors';
@ -60,7 +60,6 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
nologupload: boolean;
'release-tag'?: string[];
draft: boolean;
note?: string;
help: void;
}
@ -102,7 +101,6 @@ ${dockerignoreHelp}
public static examples = [
'$ balena deploy myFleet',
'$ balena deploy myorg/myfleet --build --source myBuildDir/',
'$ balena deploy myorg/myfleet --build --source myBuildDir/ --note "this is the note for this release"',
'$ balena deploy myorg/myfleet myRepo/myImage',
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
];
@ -147,7 +145,6 @@ ${dockerignoreHelp}
as final by default unless this option is given.`,
default: false,
}),
note: flags.string({ description: 'The notes for this release' }),
...composeCliFlags,
...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
@ -234,9 +231,6 @@ ${dockerignoreHelp}
releaseTagKeys,
releaseTagValues,
);
if (options.note) {
await sdk.models.release.note(release.id, options.note);
}
}
async deployProject(

View File

@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { ExpectedError } from '../../errors';
interface FlagsDef {
@ -42,6 +43,7 @@ export default class DeviceIdentifyCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to identify',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,8 +21,11 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import type { Application, Release } from 'balena-sdk';
import type { DataOutputOptions } from '../../framework';
import { isV14 } from '../../utils/version';
interface ExtendedDevice extends DeviceWithDeviceType {
dashboard_url?: string;
@ -41,9 +44,8 @@ interface ExtendedDevice extends DeviceWithDeviceType {
undervoltage_detected?: boolean;
}
interface FlagsDef {
interface FlagsDef extends DataOutputOptions {
help: void;
view: boolean;
}
interface ArgsDef {
@ -56,15 +58,13 @@ export default class DeviceCmd extends Command {
Show information about a single device.
`;
public static examples = [
'$ balena device 7cf02a6',
'$ balena device 7cf02a6 --view',
];
public static examples = ['$ balena device 7cf02a6'];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the device uuid',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];
@ -73,10 +73,7 @@ export default class DeviceCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
view: flags.boolean({
default: false,
description: 'open device dashboard page',
}),
...(isV14() ? cf.dataOutputFlags : {}),
};
public static authenticated = true;
@ -116,14 +113,6 @@ export default class DeviceCmd extends Command {
],
...expandForAppName,
})) as ExtendedDevice;
if (options.view) {
const open = await import('open');
const dashboardUrl = balena.models.device.getDashboardUrl(device.uuid);
await open(dashboardUrl, { wait: false });
return;
}
device.status = device.overall_status;
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
@ -179,37 +168,52 @@ export default class DeviceCmd extends Command {
);
}
console.log(
getVisuals().table.vertical(device, [
`$${device.device_name}$`,
'id',
'device_type',
'status',
'is_online',
'ip_address',
'public_address',
'mac_address',
'fleet',
'last_seen',
'uuid',
'commit',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'dashboard_url',
'cpu_usage_percent',
'cpu_temp_c',
'cpu_id',
'memory_usage_mb',
'memory_total_mb',
'memory_usage_percent',
'storage_block_device',
'storage_usage_mb',
'storage_total_mb',
'storage_usage_percent',
'undervoltage_detected',
]),
);
const outputFields = [
'device_name',
'id',
'device_type',
'status',
'is_online',
'ip_address',
'public_address',
'mac_address',
'fleet',
'last_seen',
'uuid',
'commit',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'dashboard_url',
'cpu_usage_percent',
'cpu_temp_c',
'cpu_id',
'memory_usage_mb',
'memory_total_mb',
'memory_usage_percent',
'storage_block_device',
'storage_usage_mb',
'storage_total_mb',
'storage_usage_percent',
'undervoltage_detected',
];
if (isV14()) {
await this.outputData(device, outputFields, {
...options,
hideNullOrUndefinedValues: true,
titleField: 'device_name',
});
} else {
// Old output implementation
outputFields.unshift(`$${device.device_name}$`);
console.log(
getVisuals().table.vertical(
device,
outputFields.filter((f) => f !== 'device_name'),
),
);
}
}
}

View File

@ -31,7 +31,6 @@ interface FlagsDef {
config?: string;
help: void;
'provisioning-key-name'?: string;
'provisioning-key-expiry-date'?: string;
}
export default class DeviceInitCmd extends Command {
@ -70,7 +69,6 @@ export default class DeviceInitCmd extends Command {
public static examples = [
'$ balena device init',
'$ balena device init -f myorg/myfleet',
'$ balena device init --fleet myFleet --os-version 2.101.7 --drive /dev/disk5 --config config.json --yes',
'$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes',
];
@ -99,10 +97,6 @@ export default class DeviceInitCmd extends Command {
'provisioning-key-name': flags.string({
description: 'custom key name assigned to generated provisioning api key',
}),
'provisioning-key-expiry-date': flags.string({
description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
}),
help: cf.help,
};
@ -129,7 +123,7 @@ export default class DeviceInitCmd extends Command {
options.fleet ||
(
await (await import('../../utils/patterns')).selectApplication()
).slug,
).id,
{
$expand: {
is_for__device_type: {
@ -191,14 +185,6 @@ export default class DeviceInitCmd extends Command {
options['provisioning-key-name'],
);
}
if (options['provisioning-key-expiry-date']) {
configureCommand.push(
'--provisioning-key-expiry-date',
options['provisioning-key-expiry-date'],
);
}
await runCommand(configureCommand);
}

View File

@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
enable: boolean;
@ -51,6 +52,7 @@ export default class DeviceLocalModeCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to manage',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];

View File

@ -88,14 +88,17 @@ export default class DeviceMoveCmd extends Command {
const balena = getBalenaSdk();
const { tryAsInteger } = await import('../../utils/validation');
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
// Split uuids string into array of uuids
const deviceUuids = params.uuid.split(',');
// Parse ids string into array of correct types
const deviceIds: Array<string | number> = params.uuid
.split(',')
.map((id) => tryAsInteger(id));
// Get devices
const devices = await Promise.all(
deviceUuids.map(
deviceIds.map(
(uuid) =>
balena.models.device.get(
uuid,
@ -112,7 +115,7 @@ export default class DeviceMoveCmd extends Command {
: 'N/a';
}
// Disambiguate application
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../../utils/sdk');
// Get destination application
@ -121,7 +124,7 @@ export default class DeviceMoveCmd extends Command {
: await this.interactivelySelectApplication(balena, devices);
// Move each device
for (const uuid of deviceUuids) {
for (const uuid of deviceIds) {
try {
await balena.models.device.move(uuid, application.id);
console.info(`Device ${uuid} was moved to fleet ${application.slug}`);

View File

@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import type { Device } from 'balena-sdk';
import { ExpectedError } from '../../errors';
@ -46,7 +47,6 @@ export default class DeviceOsUpdateCmd extends Command {
`;
public static examples = [
'$ balena device os-update 23c73a1',
'$ balena device os-update 23c73a1 --version 2.101.7',
'$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod',
];
@ -54,6 +54,7 @@ export default class DeviceOsUpdateCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to update',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];

View File

@ -1,103 +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 { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getExpandedProp } from '../../utils/pine';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
releaseToPinTo?: string;
}
export default class DevicePinCmd extends Command {
public static description = stripIndent`
Pin a device to a release.
Pin a device to a release.
Note, if the commit is omitted, the currently pinned release will be printed, with instructions for how to see a list of releases
`;
public static examples = [
'$ balena device pin 7cf02a6',
'$ balena device pin 7cf02a6 91165e5',
];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to pin to a release',
required: true,
},
{
name: 'releaseToPinTo',
description: 'the commit of the release for the device to get pinned to',
},
];
public static usage = 'device pin <uuid> [releaseToPinTo]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePinCmd);
const balena = getBalenaSdk();
const device = await balena.models.device.get(params.uuid, {
$expand: {
should_be_running__release: {
$select: 'commit',
},
belongs_to__application: {
$select: 'slug',
},
},
});
const pinnedRelease = getExpandedProp(
device.should_be_running__release,
'commit',
);
const appSlug = getExpandedProp(device.belongs_to__application, 'slug');
const releaseToPinTo = params.releaseToPinTo;
if (!releaseToPinTo) {
console.log(
`${
pinnedRelease
? `This device is currently pinned to ${pinnedRelease}.`
: 'This device is not currently pinned to any release.'
} \n\nTo see a list of all releases this device can be pinned to, run \`balena releases ${appSlug}\`.`,
);
} else {
await balena.models.device.pinToRelease(params.uuid, releaseToPinTo);
}
}
}

View File

@ -21,6 +21,7 @@ import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
enable: boolean;
@ -31,6 +32,8 @@ interface FlagsDef {
interface ArgsDef {
uuid: string;
// Optional hidden arg to support old command format
legacyUuid?: string;
}
export default class DevicePublicUrlCmd extends Command {
@ -40,6 +43,9 @@ export default class DevicePublicUrlCmd extends Command {
This command will output the current public URL for the
specified device. It can also enable or disable the URL,
or output the enabled status, using the respective options.
The old command style 'balena device public-url enable <uuid>'
is deprecated, but still supported.
`;
public static examples = [
@ -53,8 +59,15 @@ export default class DevicePublicUrlCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to manage',
parse: (dev) => tryAsInteger(dev),
required: true,
},
{
// Optional hidden arg to support old command format
name: 'legacyUuid',
parse: (dev) => tryAsInteger(dev),
hidden: true,
},
];
public static usage = 'device public-url <uuid>';
@ -82,6 +95,25 @@ export default class DevicePublicUrlCmd extends Command {
DevicePublicUrlCmd,
);
// Legacy command format support.
// Previously this command used the following format
// (changed due to oclif technicalities):
// `balena device public-url enable|disable|status <uuid>`
if (params.legacyUuid) {
const action = params.uuid;
if (!['enable', 'disable', 'status'].includes(action)) {
throw new ExpectedError(
`Unexpected arguments: ${params.uuid} ${params.legacyUuid}`,
);
}
options.enable = action === 'enable';
options.disable = action === 'disable';
options.status = action === 'status';
params.uuid = params.legacyUuid;
delete params.legacyUuid;
}
const balena = getBalenaSdk();
if (options.enable) {

View File

@ -63,14 +63,17 @@ export default class DevicePurgeCmd extends Command {
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePurgeCmd);
const { tryAsInteger } = await import('../../utils/validation');
const balena = getBalenaSdk();
const ux = getCliUx();
const deviceUuids = params.uuid.split(',');
const deviceIds = params.uuid.split(',').map((id) => {
return tryAsInteger(id);
});
for (const uuid of deviceUuids) {
ux.action.start(`Purging data from device ${uuid}`);
await balena.models.device.purge(uuid);
for (const deviceId of deviceIds) {
ux.action.start(`Purging data from device ${deviceId}`);
await balena.models.device.purge(deviceId);
ux.action.stop();
}
}

View File

@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
force: boolean;
@ -42,6 +43,7 @@ export default class DeviceRebootCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to reboot',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];

View File

@ -25,7 +25,6 @@ import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
uuid?: string;
deviceType?: string;
help: void;
}
@ -48,7 +47,6 @@ export default class DeviceRegisterCmd extends Command {
'$ balena device register MyFleet',
'$ balena device register MyFleet --uuid <uuid>',
'$ balena device register myorg/myfleet --uuid <uuid>',
'$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>',
];
public static args: Array<IArg<any>> = [ca.fleetRequired];
@ -60,10 +58,6 @@ export default class DeviceRegisterCmd extends Command {
description: 'custom uuid',
char: 'u',
}),
deviceType: flags.string({
description:
"device type slug (run 'balena devices supported' for possible values)",
}),
help: cf.help,
};
@ -83,11 +77,7 @@ export default class DeviceRegisterCmd extends Command {
console.info(`Registering to ${application.slug}: ${uuid}`);
const result = await balena.models.device.register(
application.id,
uuid,
options.deviceType,
);
const result = await balena.models.device.register(application.id, uuid);
return result && result.uuid;
}

View File

@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
@ -47,6 +48,7 @@ export default class DeviceRenameCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to rename',
parse: (dev) => tryAsInteger(dev),
required: true,
},
{

View File

@ -82,21 +82,24 @@ export default class DeviceRestartCmd extends Command {
DeviceRestartCmd,
);
const { tryAsInteger } = await import('../../utils/validation');
const balena = getBalenaSdk();
const ux = getCliUx();
const deviceUuids = params.uuid.split(',');
const deviceIds = params.uuid.split(',').map((id) => {
return tryAsInteger(id);
});
const serviceNames = options.service?.split(',');
// Iterate sequentially through deviceUuids.
// Iterate sequentially through deviceIds.
// We may later want to add a batching feature,
// so that n devices are processed in parallel
for (const uuid of deviceUuids) {
ux.action.start(`Restarting services on device ${uuid}`);
for (const deviceId of deviceIds) {
ux.action.start(`Restarting services on device ${deviceId}`);
if (serviceNames) {
await this.restartServices(balena, uuid, serviceNames);
await this.restartServices(balena, deviceId, serviceNames);
} else {
await this.restartAllServices(balena, uuid);
await this.restartAllServices(balena, deviceId);
}
ux.action.stop();
}
@ -104,7 +107,7 @@ export default class DeviceRestartCmd extends Command {
async restartServices(
balena: BalenaSDK,
deviceUuid: string,
deviceId: number | string,
serviceNames: string[],
) {
const { ExpectedError, instanceOf } = await import('../../errors');
@ -113,7 +116,7 @@ export default class DeviceRestartCmd extends Command {
// Get device
let device: DeviceWithServiceDetails<CurrentServiceWithCommit>;
try {
device = await balena.models.device.getWithServiceDetails(deviceUuid, {
device = await balena.models.device.getWithServiceDetails(deviceId, {
$expand: {
is_running__release: { $select: 'commit' },
},
@ -121,7 +124,7 @@ export default class DeviceRestartCmd extends Command {
} catch (e) {
const { BalenaDeviceNotFound } = await import('balena-errors');
if (instanceOf(e, BalenaDeviceNotFound)) {
throw new ExpectedError(`Device ${deviceUuid} not found.`);
throw new ExpectedError(`Device ${deviceId} not found.`);
} else {
throw e;
}
@ -133,7 +136,7 @@ export default class DeviceRestartCmd extends Command {
serviceNames.forEach((service) => {
if (!device.current_services[service]) {
throw new ExpectedError(
`Service ${service} not found on device ${deviceUuid}.`,
`Service ${service} not found on device ${deviceId}.`,
);
}
});
@ -152,7 +155,7 @@ export default class DeviceRestartCmd extends Command {
if (serviceContainer) {
restartPromises.push(
balena.models.device.restartService(
deviceUuid,
deviceId,
serviceContainer.image_id,
),
);
@ -163,32 +166,32 @@ export default class DeviceRestartCmd extends Command {
await Promise.all(restartPromises);
} catch (e) {
if (e.message.toLowerCase().includes('no online device')) {
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
throw new ExpectedError(`Device ${deviceId} is not online.`);
} else {
throw e;
}
}
}
async restartAllServices(balena: BalenaSDK, deviceUuid: string) {
async restartAllServices(balena: BalenaSDK, deviceId: number | string) {
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
// Need to use device.get first to distinguish between non-existant and offline devices.
// Remove this workaround when SDK issue resolved: https://github.com/balena-io/balena-sdk/issues/649
const { instanceOf, ExpectedError } = await import('../../errors');
try {
const device = await balena.models.device.get(deviceUuid);
const device = await balena.models.device.get(deviceId);
if (!device.is_online) {
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
throw new ExpectedError(`Device ${deviceId} is not online.`);
}
} catch (e) {
const { BalenaDeviceNotFound } = await import('balena-errors');
if (instanceOf(e, BalenaDeviceNotFound)) {
throw new ExpectedError(`Device ${deviceUuid} not found.`);
throw new ExpectedError(`Device ${deviceId} not found.`);
} else {
throw e;
}
}
await balena.models.device.restartApplication(deviceUuid);
await balena.models.device.restartApplication(deviceId);
}
}

View File

@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
yes: boolean;
@ -83,7 +84,7 @@ export default class DeviceRmCmd extends Command {
// Remove
for (const uuid of uuids) {
try {
await balena.models.device.remove(uuid);
await balena.models.device.remove(tryAsInteger(uuid));
} catch (err) {
console.info(`${err.message}, uuid: ${uuid}`);
process.exitCode = 1;

View File

@ -20,6 +20,7 @@ import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { ExpectedError } from '../../errors';
interface FlagsDef {
@ -43,6 +44,7 @@ export default class DeviceShutdownCmd extends Command {
{
name: 'uuid',
description: 'the uuid of the device to shutdown',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];

View File

@ -1,63 +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 { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceTrackFleetCmd extends Command {
public static description = stripIndent`
Make a device track the fleet's pinned release.
Make a device track the fleet's pinned release.
`;
public static examples = ['$ balena device track-fleet 7cf02a6'];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: "the uuid of the device to make track the fleet's release",
required: true,
},
];
public static usage = 'device track-fleet <uuid>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceTrackFleetCmd);
const balena = getBalenaSdk();
await balena.models.device.trackApplicationRelease(params.uuid);
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,8 +21,10 @@ import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
import type { Application } from 'balena-sdk';
import type { DataSetOutputOptions } from '../../framework';
import { isV14 } from '../../utils/version';
interface ExtendedDevice extends DeviceWithDeviceType {
dashboard_url?: string;
@ -30,10 +32,10 @@ interface ExtendedDevice extends DeviceWithDeviceType {
device_type?: string | null;
}
interface FlagsDef {
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
help: void;
json: boolean;
json?: boolean;
}
export default class DevicesCmd extends Command {
@ -58,12 +60,11 @@ export default class DevicesCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
fleet: cf.fleet,
json: cf.json,
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
help: cf.help,
};
public static primary = true;
public static authenticated = true;
public async run() {
@ -99,31 +100,52 @@ export default class DevicesCmd extends Command {
return device;
});
const fields = [
'id',
'uuid',
'device_name',
'device_type',
'fleet',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
];
if (isV14()) {
const outputFields = [
'id',
'uuid',
'device_name',
'device_type',
'fleet',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
];
if (options.json) {
const { pickAndRename } = await import('../../utils/helpers');
const mapped = devices.map((device) => pickAndRename(device, fields));
console.log(JSON.stringify(mapped, null, 4));
await this.outputData(devices, outputFields, {
...options,
displayNullValuesAs: 'N/a',
});
} else {
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
fields,
),
);
// Old output implementation
const fields = [
'id',
'uuid',
'device_name',
'device_type',
'fleet',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
];
if (options.json) {
const { pickAndRename } = await import('../../utils/helpers');
const mapped = devices.map((device) => pickAndRename(device, fields));
console.log(JSON.stringify(mapped, null, 4));
} else {
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
fields,
),
);
}
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2021 Balena Ltd.
* Copyright 2016 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,15 +15,16 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import type * as BalenaSdk from 'balena-sdk';
import * as _ from 'lodash';
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 type { DataSetOutputOptions } from '../../framework';
interface FlagsDef {
import { isV14 } from '../../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
json?: boolean;
}
@ -52,54 +53,55 @@ export default class DevicesSupportedCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
json: flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
}),
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
};
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
const pineOptions = {
$select: (['slug', 'name'] as const).slice(),
$expand: {
is_of__cpu_architecture: { $select: 'slug' },
device_type_alias: {
$select: 'is_referenced_by__alias',
$orderby: 'is_referenced_by__alias asc',
},
},
} as const;
const dts = (await getBalenaSdk().models.deviceType.getAllSupported(
pineOptions,
)) as Array<
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
>;
const [dts, configDTs] = await Promise.all([
getBalenaSdk().models.deviceType.getAllSupported({
$expand: { is_of__cpu_architecture: { $select: 'slug' } },
$select: ['slug', 'name'],
}),
getBalenaSdk().models.config.getDeviceTypes(),
]);
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
interface DT {
slug: string;
aliases: string[];
aliases: string[] | string;
arch: string;
name: string;
}
let deviceTypes = dts.map((dt): DT => {
const aliases = dt.device_type_alias
.map((dta) => dta.is_referenced_by__alias)
.filter((alias) => alias !== dt.slug);
return {
slug: dt.slug,
aliases: options.json ? aliases : [aliases.join(', ')],
arch: dt.is_of__cpu_architecture[0]?.slug || 'n/a',
let deviceTypes: DT[] = [];
for (const slug of Object.keys(dtsBySlug)) {
const configDT: Partial<typeof configDTs[0]> =
configDTsBySlug[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 || 'n/a',
name: dt.name || 'N/A',
};
});
});
}
const fields = ['slug', 'aliases', 'arch', 'name'];
deviceTypes = _.sortBy(deviceTypes, fields);
if (options.json) {
console.log(JSON.stringify(deviceTypes, null, 4));
if (isV14()) {
await this.outputData(deviceTypes, fields, options);
} else {
const visuals = getVisuals();
const output = await visuals.table.horizontal(deviceTypes, fields);
console.log(output);
// Old output implementation
if (options.json) {
console.log(JSON.stringify(deviceTypes, null, 4));
} else {
const visuals = getVisuals();
const output = await visuals.table.horizontal(deviceTypes, fields);
console.log(output);
}
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2021 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,12 +22,15 @@ import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
config: boolean;
device?: string; // device UUID
json: boolean;
json?: boolean;
help: void;
service?: string; // service name
}
@ -113,7 +116,7 @@ export default class EnvsCmd extends Command {
}),
device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
json: cf.json,
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
service: { ...cf.service, exclusive: ['config'] },
};
@ -181,24 +184,59 @@ export default class EnvsCmd extends Command {
return i;
});
if (options.device) {
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
}
if (!options.config) {
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
}
if (isV14()) {
const results = [...varArray] as any;
if (options.json) {
const { pickAndRename } = await import('../utils/helpers');
const mapped = varArray.map((o) => pickAndRename(o, fields));
this.log(JSON.stringify(mapped, null, 4));
// Rename fields
if (options.device) {
if (options.json) {
fields.push('deviceUUID');
} else {
results.forEach((r: any) => {
r.device = r.deviceUUID;
delete r.deviceUUID;
});
fields.push('device');
}
}
if (!options.config) {
if (options.json) {
fields.push('serviceName');
} else {
results.forEach((r: any) => {
r.service = r.serviceName;
delete r.serviceName;
});
fields.push('service');
}
}
await this.outputData(results, fields, {
...options,
sort: options.sort || 'name',
});
} else {
this.log(
getVisuals().table.horizontal(
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
fields,
),
);
// Old output implementation
if (options.device) {
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
}
if (!options.config) {
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
}
if (options.json) {
const { pickAndRename } = await import('../utils/helpers');
const mapped = varArray.map((o) => pickAndRename(o, fields));
this.log(JSON.stringify(mapped, null, 4));
} else {
this.log(
getVisuals().table.horizontal(
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
fields,
),
);
}
}
}
}

View File

@ -15,20 +15,19 @@
* limitations under the License.
*/
import type { flags as flagsType } from '@oclif/command';
import { flags } from '@oclif/command';
import type { flags } from '@oclif/command';
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, stripIndent } from '../../utils/lazy';
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;
view: boolean;
}
interface ArgsDef {
@ -46,20 +45,15 @@ export default class FleetCmd extends Command {
public static examples = [
'$ balena fleet MyFleet',
'$ balena fleet myorg/myfleet',
'$ balena fleet myorg/myfleet --view',
];
public static args = [ca.fleetRequired];
public static usage = 'fleet <fleet>';
public static flags: flagsType.Input<FlagsDef> = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
view: flags.boolean({
default: false,
description: 'open fleet dashboard page',
}),
...cf.dataOutputFlags,
...(isV14() ? cf.dataOutputFlags : {}),
};
public static authenticated = true;
@ -72,9 +66,7 @@ export default class FleetCmd extends Command {
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
const application = (await getApplication(balena, params.fleet, {
const application = (await getApplication(getBalenaSdk(), params.fleet, {
$expand: {
is_for__device_type: { $select: 'slug' },
should_be_running__release: { $select: 'commit' },
@ -86,22 +78,26 @@ export default class FleetCmd extends Command {
commit?: string;
};
if (options.view) {
const open = await import('open');
const dashboardUrl = balena.models.application.getDashboardUrl(
application.id,
);
await open(dashboardUrl, { wait: false });
return;
}
application.device_type = application.is_for__device_type[0].slug;
application.commit = application.should_be_running__release[0]?.commit;
await this.outputData(
application,
['app_name', 'id', 'device_type', 'slug', 'commit'],
options,
);
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,100 +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 { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getExpandedProp } from '../../utils/pine';
interface FlagsDef {
help: void;
}
interface ArgsDef {
slug: string;
releaseToPinTo?: string;
}
export default class FleetPinCmd extends Command {
public static description = stripIndent`
Pin a fleet to a release.
Pin a fleet to a release.
Note, if the commit is omitted, the currently pinned release will be printed, with instructions for how to see a list of releases
`;
public static examples = [
'$ balena fleet pin myfleet',
'$ balena fleet pin myorg/myfleet 91165e5',
];
public static args: Array<IArg<any>> = [
{
name: 'slug',
description: 'the slug of the fleet to pin to a release',
required: true,
},
{
name: 'releaseToPinTo',
description: 'the commit of the release for the fleet to get pinned to',
},
];
public static usage = 'fleet pin <slug> [releaseToPinTo]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPinCmd);
const balena = getBalenaSdk();
const fleet = await balena.models.application.get(params.slug, {
$expand: {
should_be_running__release: {
$select: 'commit',
},
},
});
const pinnedRelease = getExpandedProp(
fleet.should_be_running__release,
'commit',
);
const releaseToPinTo = params.releaseToPinTo;
const slug = params.slug;
if (!releaseToPinTo) {
console.log(
`${
pinnedRelease
? `This fleet is currently pinned to ${pinnedRelease}.`
: 'This fleet is not currently pinned to any release.'
} \n\nTo see a list of all releases this fleet can be pinned to, run \`balena releases ${slug}\`.`,
);
} else {
await balena.models.application.pinToRelease(slug, releaseToPinTo);
}
}
}

View File

@ -62,9 +62,9 @@ export default class FleetRestartCmd extends Command {
const balena = getBalenaSdk();
// Disambiguate application
// 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.slug);
await balena.models.application.restart(application.id);
}
}

View File

@ -79,6 +79,6 @@ export default class FleetRmCmd extends Command {
const application = await getApplication(balena, params.fleet);
// Remove
await balena.models.application.remove(application.slug);
await balena.models.application.remove(application.id);
}
}

View File

@ -1,66 +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 { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
slug: string;
}
export default class FleetTrackLatestCmd extends Command {
public static description = stripIndent`
Make this fleet track the latest release.
Make this fleet track the latest release.
`;
public static examples = [
'$ balena fleet track-latest myorg/myfleet',
'$ balena fleet track-latest myfleet',
];
public static args: Array<IArg<any>> = [
{
name: 'slug',
description: 'the slug of the fleet to make track the latest release',
required: true,
},
];
public static usage = 'fleet track-latest <slug>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetTrackLatestCmd);
const balena = getBalenaSdk();
await balena.models.application.trackLatestRelease(params.slug);
}
}

View File

@ -19,7 +19,8 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { isV14 } from '../utils/version';
import type { DataSetOutputOptions } from '../framework';
interface ExtendedApplication extends ApplicationWithDeviceType {
@ -48,7 +49,7 @@ export default class FleetsCmd extends Command {
public static usage = 'fleets';
public static flags: flags.Input<FlagsDef> = {
...cf.dataSetOutputFlags,
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
@ -78,17 +79,30 @@ export default class FleetsCmd extends Command {
application.device_type = application.is_for__device_type[0].slug;
});
await this.outputData(
applications,
[
'id',
'app_name',
'slug',
'device_type',
'device_count',
'online_devices',
],
options,
);
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

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,10 +20,13 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
import type { DataOutputOptions } from '../../framework';
import { isV14 } from '../../utils/version';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
interface FlagsDef extends DataOutputOptions {
help: void;
}
@ -52,27 +55,52 @@ export default class KeyCmd extends Command {
public static usage = 'key <id>';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
KeyCmd,
);
const key = await getBalenaSdk().models.key.get(params.id);
// Use 'name' instead of 'title' to match dashboard.
const displayKey = {
id: key.id,
name: key.title,
};
if (isV14()) {
// Use 'name' instead of 'title' to match dashboard.
const displayKey = {
id: key.id,
name: key.title,
public_key: key.public_key,
};
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
if (!options.json) {
// Id is redundant, since user must have provided it in command call
this.printTitle(displayKey.name);
this.outputMessage(displayKey.public_key);
} else {
await this.outputData(
displayKey,
['id', 'name', 'public_key'],
options,
);
}
} else {
// Old output implementation
// Use 'name' instead of 'title' to match dashboard.
const displayKey = {
id: key.id,
name: key.title,
};
// Since the public key string is long, it might
// wrap to lines below, causing the table layout to break.
// See https://github.com/balena-io/balena-cli/issues/151
console.log('\n' + key.public_key);
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
// Since the public key string is long, it might
// wrap to lines below, causing the table layout to break.
// See https://github.com/balena-io/balena-cli/issues/151
console.log('\n' + key.public_key);
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2022 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
@ -35,13 +38,14 @@ export default class KeysCmd extends Command {
public static usage = 'keys';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public async run() {
this.parse<FlagsDef, {}>(KeysCmd);
const { flags: options } = this.parse<FlagsDef, {}>(KeysCmd);
const keys = await getBalenaSdk().models.key.getAll();
@ -50,6 +54,12 @@ export default class KeysCmd extends Command {
return { id: k.id, name: k.title };
});
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
// Display
if (isV14()) {
await this.outputData(displayKeys, ['id', 'name'], options);
} else {
// Old output implementation
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2022 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
@ -36,12 +39,13 @@ export default class OrgsCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
...(isV14() ? cf.dataSetOutputFlags : {}),
};
public static authenticated = true;
public async run() {
this.parse<FlagsDef, {}>(OrgsCmd);
const { flags: options } = this.parse<FlagsDef, {}>(OrgsCmd);
const { getOwnOrganizations } = await import('../utils/sdk');
@ -49,8 +53,13 @@ export default class OrgsCmd extends Command {
const organizations = await getOwnOrganizations(getBalenaSdk());
// Display
console.log(
getVisuals().table.horizontal(organizations, ['name', 'handle']),
);
if (isV14()) {
await this.outputData(organizations, ['name', 'handle'], options);
} else {
// Old output implementation
console.log(
getVisuals().table.horizontal(organizations, ['name', 'handle']),
);
}
}
}

View File

@ -43,7 +43,6 @@ interface FlagsDef {
'system-connection': string[];
'initial-device-name'?: string;
'provisioning-key-name'?: string;
'provisioning-key-expiry-date'?: string;
}
interface ArgsDef {
@ -59,7 +58,6 @@ interface Answers {
wifiSsid?: string;
wifiKey?: string;
provisioningKeyName?: string;
provisioningKeyExpiryDate?: string;
}
export default class OsConfigureCmd extends Command {
@ -123,7 +121,7 @@ export default class OsConfigureCmd extends Command {
config: flags.string({
description:
'path to a pre-generated config.json file to be injected in the OS image',
exclusive: ['provisioning-key-name', 'provisioning-key-expiry-date'],
exclusive: ['provisioning-key-name'],
}),
'config-app-update-poll-interval': flags.integer({
description:
@ -140,14 +138,7 @@ export default class OsConfigureCmd extends Command {
description: 'WiFi SSID (network name) (non-interactive configuration)',
}),
dev: cf.dev,
device: {
...cf.device,
exclusive: [
'fleet',
'provisioning-key-name',
'provisioning-key-expiry-date',
],
},
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',
@ -170,11 +161,6 @@ export default class OsConfigureCmd extends Command {
description: 'custom key name assigned to generated provisioning api key',
exclusive: ['config', 'device'],
}),
'provisioning-key-expiry-date': flags.string({
description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
exclusive: ['config', 'device'],
}),
help: cf.help,
};
@ -215,7 +201,7 @@ export default class OsConfigureCmd extends Command {
is_for__device_type: { $select: 'slug' },
},
})) as ApplicationWithDeviceType;
await checkDeviceTypeCompatibility(options, app);
await checkDeviceTypeCompatibility(balena, options, app);
deviceTypeSlug =
options['device-type'] || app.is_for__device_type[0].slug;
}
@ -249,7 +235,6 @@ export default class OsConfigureCmd extends Command {
answers.version = osVersion;
answers.developmentMode = options.dev;
answers.provisioningKeyName = options['provisioning-key-name'];
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
if (_.isEmpty(configJson)) {
if (device) {
@ -361,17 +346,17 @@ async function getOsVersionFromImage(
* @param app Balena SDK Application model object
*/
async function checkDeviceTypeCompatibility(
sdk: BalenaSdk.BalenaSDK,
options: FlagsDef,
app: ApplicationWithDeviceType,
) {
if (options['device-type']) {
const [appDeviceType, optionDeviceType] = await Promise.all([
sdk.models.device.getManifestBySlug(app.is_for__device_type[0].slug),
sdk.models.device.getManifestBySlug(options['device-type']),
]);
const helpers = await import('../../utils/helpers');
if (
!(await helpers.areDeviceTypesCompatible(
app.is_for__device_type[0].slug,
options['device-type'],
))
) {
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
throw new ExpectedError(
`Device type ${options['device-type']} is incompatible with fleet ${options.fleet}`,
);

View File

@ -53,11 +53,9 @@ export default class OsDownloadCmd extends Command {
`;
public static examples = [
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.101.7',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2022.7.0',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.90.0',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1.dev',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.60.0',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2021.10.2.prod',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default',

View File

@ -42,9 +42,7 @@ export default class OsInitializeCmd extends Command {
Initialize an os image for a device.
Initialize an os image for a device with a previously
configured operating system image and flash the
an external storage drive or the device's storage
medium depending on the device type.
configured operating system image.
${INIT_WARNING_MESSAGE}
`;

View File

@ -288,7 +288,7 @@ Can be repeated to add multiple certificates.\
preloader.on('error', reject);
resolve(
this.prepareAndPreload(preloader, balena, {
slug: fleetSlug,
appId: fleetSlug,
commit,
pinDevice,
}),
@ -491,10 +491,10 @@ Would you like to disable automatic updates for this fleet now?\
});
}
async getAppWithReleases(balenaSdk: BalenaSDK, slug: string) {
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string) {
const { getApplication } = await import('../utils/sdk');
return (await getApplication(balenaSdk, slug, {
return (await getApplication(balenaSdk, appId, {
$expand: this.applicationExpandOptions,
})) as Application & { should_be_running__release: [Release?] };
}
@ -503,15 +503,15 @@ Would you like to disable automatic updates for this fleet now?\
preloader: Preloader,
balenaSdk: BalenaSDK,
options: {
slug?: string;
appId?: string;
commit?: string;
pinDevice: boolean;
},
) {
await preloader.prepare();
const application = options.slug
? await this.getAppWithReleases(balenaSdk, options.slug)
const application = options.appId
? await this.getAppWithReleases(balenaSdk, options.appId)
: await this.selectApplication(preloader.config.deviceType);
let commit: string; // commit hash or the strings 'latest' or 'current'
@ -523,7 +523,7 @@ Would you like to disable automatic updates for this fleet now?\
if (this.isCurrentCommit(options.commit)) {
if (!appCommit) {
throw new Error(
`Unexpected empty commit hash for fleet slug "${application.slug}"`,
`Unexpected empty commit hash for fleet ID "${application.id}"`,
);
}
// handle `--commit current` (and its `--commit latest` synonym)

View File

@ -22,7 +22,7 @@ import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
import type { BalenaSDK } from 'balena-sdk';
import { ExpectedError, instanceOf } from '../errors';
import { RegistrySecrets } from '@balena/compose/dist/multibuild';
import { RegistrySecrets } from 'resin-multibuild';
import { lowercaseIfSlug } from '../utils/normalization';
import {
applyReleaseTagKeysAndValues,
@ -55,7 +55,6 @@ interface FlagsDef {
'multi-dockerignore': boolean;
'release-tag'?: string[];
draft: boolean;
note?: string;
help: void;
}
@ -98,7 +97,6 @@ export default class PushCmd extends Command {
'$ balena push myFleet',
'$ balena push myFleet --source <source directory>',
'$ balena push myFleet -s <source directory>',
'$ balena push myFleet --source <source directory> --note "this is the note for this release"',
'$ balena push myFleet --release-tag key1 "" key2 "value2 with spaces"',
'$ balena push myorg/myfleet',
'',
@ -243,7 +241,6 @@ export default class PushCmd extends Command {
as final by default unless this option is given.`,
default: false,
}),
note: flags.string({ description: 'The notes for this release' }),
help: cf.help,
};
@ -357,9 +354,6 @@ export default class PushCmd extends Command {
releaseTagKeys,
releaseTagValues,
);
if (options.note) {
await sdk.models.release.note(releaseId, options.note);
}
} else if (releaseTagKeys.length > 0) {
throw new Error(stripIndent`
A release ID could not be parsed out of the builder's output.

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,8 +22,11 @@ import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import type * as BalenaSdk from 'balena-sdk';
import jsyaml = require('js-yaml');
import { tryAsInteger } from '../../utils/validation';
import type { DataOutputOptions } from '../../framework';
interface FlagsDef {
import { isV14 } from '../../utils/version';
interface FlagsDef extends DataOutputOptions {
help: void;
composition?: boolean;
}
@ -49,7 +52,9 @@ export default class ReleaseCmd extends Command {
default: false,
char: 'c',
description: 'Return the release composition',
exclusive: ['json', 'fields'],
}),
...(isV14() ? cf.dataOutputFlags : {}),
};
public static args = [
@ -68,29 +73,27 @@ export default class ReleaseCmd extends Command {
ReleaseCmd,
);
const balena = getBalenaSdk();
if (options.composition) {
await this.showComposition(params.commitOrId, balena);
await this.showComposition(params.commitOrId);
} else {
await this.showReleaseInfo(params.commitOrId, balena);
await this.showReleaseInfo(params.commitOrId, options);
}
}
async showComposition(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
const release = await balena.models.release.get(commitOrId, {
async showComposition(commitOrId: string | number) {
const release = await getBalenaSdk().models.release.get(commitOrId, {
$select: 'composition',
});
console.log(jsyaml.dump(release.composition));
if (isV14()) {
this.outputMessage(jsyaml.dump(release.composition));
} else {
// Old output implementation
console.log(jsyaml.dump(release.composition));
}
}
async showReleaseInfo(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
async showReleaseInfo(commitOrId: string | number, options: FlagsDef) {
const fields: Array<keyof BalenaSdk.Release> = [
'id',
'commit',
@ -103,7 +106,7 @@ export default class ReleaseCmd extends Command {
'end_timestamp',
];
const release = await balena.models.release.get(commitOrId, {
const release = await getBalenaSdk().models.release.get(commitOrId, {
$select: fields,
$expand: {
release_tag: {
@ -116,13 +119,28 @@ export default class ReleaseCmd extends Command {
.release_tag!.map((t) => `${t.tag_key}=${t.value}`)
.join('\n');
const _ = await import('lodash');
const values = _.mapValues(
release,
(val) => val ?? 'N/a',
) as Dictionary<string>;
values['tags'] = tagStr;
if (isV14()) {
await this.outputData(
{
tags: tagStr,
...release,
},
fields,
{
displayNullValuesAs: 'N/a',
...options,
},
);
} else {
// Old output implementation
const _ = await import('lodash');
const values = _.mapValues(
release,
(val) => val ?? 'N/a',
) as Dictionary<string>;
values['tags'] = tagStr;
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
}
}
}

View File

@ -1,83 +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 { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
}
interface ArgsDef {
commitOrId: string | number;
}
export default class ReleaseInvalidateCmd extends Command {
public static description = stripIndent`
Invalidate a release.
Invalidate a release.
Invalid releases are not automatically deployed to devices tracking the latest
release. For an invalid release to be deployed to a device, the device should be
explicity pinned to that release.
`;
public static examples = [
'$ balena release invalidate a777f7345fe3d655c1c981aa642e5555',
'$ balena release invalidate 1234567',
];
public static usage = 'release invalidate <commitOrId>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static args = [
{
name: 'commitOrId',
description: 'the commit or ID of the release to invalidate',
required: true,
parse: (commitOrId: string) => tryAsInteger(commitOrId),
},
];
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(
ReleaseInvalidateCmd,
);
const balena = getBalenaSdk();
const release = await balena.models.release.get(params.commitOrId, {
$select: ['id', 'is_invalidated'],
});
if (release.is_invalidated) {
console.log(`Release ${params.commitOrId} is already invalidated!`);
return;
}
await balena.models.release.setIsInvalidated(release.id, true);
console.log(`Release ${params.commitOrId} invalidated`);
}
}

View File

@ -1,80 +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 { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
}
interface ArgsDef {
commitOrId: string | number;
}
export default class ReleaseValidateCmd extends Command {
public static description = stripIndent`
Validate a release.
Validate a release.
Valid releases are automatically deployed to devices tracking the latest
release if they are finalized.
`;
public static examples = [
'$ balena release validate a777f7345fe3d655c1c981aa642e5555',
'$ balena release validate 1234567',
];
public static usage = 'release validate <commitOrId>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static args = [
{
name: 'commitOrId',
description: 'the commit or ID of the release to validate',
required: true,
parse: (commitOrId: string) => tryAsInteger(commitOrId),
},
];
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleaseValidateCmd);
const balena = getBalenaSdk();
const release = await balena.models.release.get(params.commitOrId, {
$select: ['id', 'is_invalidated'],
});
if (!release.is_invalidated) {
console.log(`Release ${params.commitOrId} is already validated!`);
return;
}
await balena.models.release.setIsInvalidated(release.id, false);
console.log(`Release ${params.commitOrId} validated`);
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,8 +21,11 @@ import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationNameNote } from '../utils/messages';
import type * as BalenaSdk from 'balena-sdk';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
@ -43,6 +46,7 @@ export default class ReleasesCmd extends Command {
public static usage = 'releases <fleet>';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
@ -57,7 +61,9 @@ export default class ReleasesCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleasesCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
ReleasesCmd,
);
const fields: Array<keyof BalenaSdk.Release> = [
'id',
@ -76,12 +82,20 @@ export default class ReleasesCmd extends Command {
{ $select: fields },
);
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
fields,
),
);
if (isV14()) {
await this.outputData(releases, fields, {
displayNullValuesAs: 'N/a',
...options,
});
} else {
// Old output implementation
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
fields,
),
);
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import type { DataOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataOutputOptions {
help: void;
}
@ -35,15 +38,27 @@ export default class SettingsCmd extends Command {
public static usage = 'settings';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
public async run() {
this.parse<FlagsDef, {}>(SettingsCmd);
const { flags: options } = this.parse<FlagsDef, {}>(SettingsCmd);
const settings = await getBalenaSdk().settings.getAll();
const prettyjson = await import('prettyjson');
console.log(prettyjson.render(settings));
if (isV14()) {
// Select all available fields for display
const fields = Object.keys(settings);
await this.outputData(settings, fields, {
noCapitalizeKeys: true,
...options,
});
} else {
// Old output implementation
const prettyjson = await import('prettyjson');
console.log(prettyjson.render(settings));
}
}
}

View File

@ -20,6 +20,7 @@ import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { parseAsInteger, validateLocalHostnameOrIp } from '../utils/validation';
import * as BalenaSdk from 'balena-sdk';
interface FlagsDef {
port?: number;
@ -76,7 +77,8 @@ export default class SshCmd extends Command {
public static args = [
{
name: 'fleetOrDevice',
description: 'fleet name/slug, device uuid, or address of local device',
description:
'fleet name/slug/id, device uuid, or address of local device',
required: true,
},
{
@ -126,8 +128,8 @@ export default class SshCmd extends Command {
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
return await performLocalDeviceSSH({
hostname: params.fleetOrDevice,
port: options.port || 'local',
address: params.fleetOrDevice,
port: options.port,
forceTTY: options.tty,
verbose: options.verbose,
service: params.service,
@ -150,6 +152,12 @@ export default class SshCmd extends Command {
params.fleetOrDevice,
);
const device = await sdk.models.device.get(deviceUuid, {
$select: ['id', 'supervisor_version', 'is_online'],
});
const deviceId = device.id;
const supervisorVersion = device.supervisor_version;
const { which } = await import('../utils/which');
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
@ -201,15 +209,19 @@ export default class SshCmd extends Command {
// that we know exists and is accessible
let containerId: string | undefined;
if (params.service != null) {
const { getContainerIdForService } = await import('../utils/device/ssh');
containerId = await getContainerIdForService({
containerId = await this.getContainerId(
sdk,
deviceUuid,
hostname: `ssh.${proxyUrl}`,
port: options.port || 'cloud',
proxyCommand,
service: params.service,
username: username!,
});
params.service,
{
port: options.port,
proxyCommand,
proxyUrl: proxyUrl || '',
username: username!,
},
supervisorVersion,
deviceId,
);
}
let accessCommand: string;
@ -218,14 +230,158 @@ export default class SshCmd extends Command {
} else {
accessCommand = `host ${deviceUuid}`;
}
const { runRemoteCommand } = await import('../utils/ssh');
await runRemoteCommand({
cmd: accessCommand,
hostname: `ssh.${proxyUrl}`,
port: options.port || 'cloud',
proxyCommand,
username,
const command = this.generateVpnSshCommand({
uuid: deviceUuid,
command: accessCommand,
verbose: options.verbose,
port: options.port,
proxyCommand,
proxyUrl: proxyUrl || '',
username: username!,
});
const { spawnSshAndThrowOnError } = await import('../utils/ssh');
return spawnSshAndThrowOnError(command);
}
async getContainerId(
sdk: BalenaSdk.BalenaSDK,
uuid: string,
serviceName: string,
sshOpts: {
port?: number;
proxyCommand?: string[];
proxyUrl: string;
username: string;
},
version?: string,
id?: number,
): Promise<string> {
const semver = await import('balena-semver');
if (version == null || id == null) {
const device = await sdk.models.device.get(uuid, {
$select: ['id', 'supervisor_version'],
});
version = device.supervisor_version;
id = device.id;
}
let containerId: string | undefined;
if (semver.gte(version, '8.6.0')) {
const apiUrl = await sdk.settings.get('apiUrl');
// TODO: Move this into the SDKs device model
const request = await sdk.request.send({
method: 'POST',
url: '/supervisor/v2/containerId',
baseUrl: apiUrl,
body: {
method: 'GET',
deviceId: id,
},
});
if (request.status !== 200) {
throw new Error(
`There was an error connecting to device ${uuid}, HTTP response code: ${request.status}.`,
);
}
const body = request.body;
if (body.status !== 'success') {
throw new Error(
`There was an error communicating with device ${uuid}.\n\tError: ${body.message}`,
);
}
containerId = body.services[serviceName];
} else {
console.error(stripIndent`
Using legacy method to detect container ID. This will be slow.
To speed up this process, please update your device to an OS
which has a supervisor version of at least v8.6.0.
`);
// We need to execute a balena ps command on the device,
// and parse the output, looking for a specific
// container
const childProcess = await import('child_process');
const { escapeRegExp } = await import('lodash');
const { which } = await import('../utils/which');
const { deviceContainerEngineBinary } = await import(
'../utils/device/ssh'
);
const sshBinary = await which('ssh');
const sshArgs = this.generateVpnSshCommand({
uuid,
verbose: false,
port: sshOpts.port,
command: `host ${uuid} "${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`,
proxyCommand: sshOpts.proxyCommand,
proxyUrl: sshOpts.proxyUrl,
username: sshOpts.username,
});
if (process.env.DEBUG) {
console.error(`[debug] [${sshBinary}, ${sshArgs.join(', ')}]`);
}
const subProcess = childProcess.spawn(sshBinary, sshArgs, {
stdio: [null, 'pipe', null],
});
const containers = await new Promise<string>((resolve, reject) => {
const output: string[] = [];
subProcess.stdout.on('data', (chunk) => output.push(chunk.toString()));
subProcess.on('close', (code: number) => {
if (code !== 0) {
reject(
new Error(
`Non-zero error code when looking for service container: ${code}`,
),
);
} else {
resolve(output.join(''));
}
});
});
const lines = containers.split('\n');
const regex = new RegExp(`\\/?${escapeRegExp(serviceName)}_\\d+_\\d+`);
for (const container of lines) {
const [cId, name] = container.split(' ');
if (regex.test(name)) {
containerId = cId;
break;
}
}
}
if (containerId == null) {
throw new Error(
`Could not find a service ${serviceName} on device ${uuid}.`,
);
}
return containerId;
}
generateVpnSshCommand(opts: {
uuid: string;
command: string;
verbose: boolean;
port?: number;
username: string;
proxyUrl: string;
proxyCommand?: string[];
}) {
return [
...(opts.verbose ? ['-vvv'] : []),
'-t',
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
...(opts.proxyCommand && opts.proxyCommand.length
? ['-o', `ProxyCommand=${opts.proxyCommand.join(' ')}`]
: []),
...(opts.port ? ['-p', opts.port.toString()] : []),
`${opts.username}@ssh.${opts.proxyUrl}`,
opts.command,
];
}
}

View File

@ -90,6 +90,8 @@ export default class TagRmCmd extends Command {
throw new ExpectedError(TagRmCmd.missingResourceMessage);
}
const { tryAsInteger } = await import('../../utils/validation');
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.remove(
@ -98,7 +100,10 @@ export default class TagRmCmd extends Command {
);
}
if (options.device) {
return balena.models.device.tags.remove(options.device, params.tagKey);
return balena.models.device.tags.remove(
tryAsInteger(options.device),
params.tagKey,
);
}
if (options.release) {
const { disambiguateReleaseParam } = await import(

View File

@ -105,6 +105,8 @@ export default class TagSetCmd extends Command {
params.value ??= '';
const { tryAsInteger } = await import('../../utils/validation');
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.set(
@ -115,7 +117,7 @@ export default class TagSetCmd extends Command {
}
if (options.device) {
return balena.models.device.tags.set(
options.device,
tryAsInteger(options.device),
params.tagKey,
params.value,
);

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,8 +21,12 @@ import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import type { ApplicationTag, DeviceTag, ReleaseTag } from 'balena-sdk';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
device?: string;
release?: string;
@ -61,6 +65,7 @@ export default class TagsCmd extends Command {
...cf.release,
exclusive: ['fleet', 'device'],
},
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
@ -76,7 +81,9 @@ export default class TagsCmd extends Command {
throw new ExpectedError(this.missingResourceMessage);
}
let tags;
const { tryAsInteger } = await import('../utils/validation');
let tags: ApplicationTag[] | DeviceTag[] | ReleaseTag[] = [];
if (options.fleet) {
const { getFleetSlug } = await import('../utils/sdk');
@ -85,7 +92,9 @@ export default class TagsCmd extends Command {
);
}
if (options.device) {
tags = await balena.models.device.tags.getAllByDevice(options.device);
tags = await balena.models.device.tags.getAllByDevice(
tryAsInteger(options.device),
);
}
if (options.release) {
const { disambiguateReleaseParam } = await import(
@ -99,11 +108,17 @@ export default class TagsCmd extends Command {
tags = await balena.models.release.tags.getAllByRelease(releaseParam);
}
if (!tags || tags.length === 0) {
if (tags.length === 0 && !options.json) {
// TODO: Later change to output message
throw new ExpectedError('No tags found');
}
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
if (isV14()) {
await this.outputData(tags, ['tag_key', 'value'], options);
} else {
// Old output implementation
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
}
}
protected missingResourceMessage = stripIndent`

View File

@ -82,7 +82,7 @@ export default class TunnelCmd extends Command {
public static args = [
{
name: 'deviceOrFleet',
description: 'device UUID or fleet name/slug',
description: 'device UUID or fleet name/slug/ID',
required: true,
parse: lowercaseIfSlug,
},
@ -136,7 +136,8 @@ export default class TunnelCmd extends Command {
// Ascertain device uuid
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
const uuid = await getOnlineTargetDeviceUuid(sdk, params.deviceOrFleet);
logger.logInfo(`Opening a tunnel to ${uuid}...`);
const device = await sdk.models.device.get(uuid);
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
const _ = await import('lodash');
const localListeners = _.chain(options.port)
@ -146,7 +147,11 @@ export default class TunnelCmd extends Command {
.map(async ({ localPort, localAddress, remotePort }) => {
try {
const { tunnelConnectionToDevice } = await import('../utils/tunnel');
const handler = await tunnelConnectionToDevice(uuid, remotePort, sdk);
const handler = await tunnelConnectionToDevice(
device.uuid,
remotePort,
sdk,
);
const { createServer } = await import('net');
const server = createServer(async (client: Socket) => {
@ -157,7 +162,7 @@ export default class TunnelCmd extends Command {
client.remotePort || 0,
client.localAddress,
client.localPort,
uuid,
device.vpn_address || '',
remotePort,
);
} catch (err) {
@ -166,7 +171,7 @@ export default class TunnelCmd extends Command {
client.remotePort || 0,
client.localAddress,
client.localPort,
uuid,
device.vpn_address || '',
remotePort,
err,
);
@ -181,15 +186,15 @@ export default class TunnelCmd extends Command {
});
logger.logInfo(
` - tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}`,
` - tunnelling ${localAddress}:${localPort} to ${device.uuid}:${remotePort}`,
);
return true;
} catch (err) {
logger.logWarn(
` - not tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}, failed ${JSON.stringify(
err.message,
)}`,
` - not tunnelling ${localAddress}:${localPort} to ${
device.uuid
}:${remotePort}, failed ${JSON.stringify(err.message)}`,
);
return false;

View File

@ -16,7 +16,12 @@
*/
import * as packageJSON from '../package.json';
import { stripIndent } from './utils/lazy';
import { getBalenaSdk, stripIndent } from './utils/lazy';
interface CachedUsername {
token: string;
username: string;
}
/**
* Track balena CLI usage events (product improvement analytics).
@ -24,7 +29,7 @@ import { stripIndent } from './utils/lazy';
* @param commandSignature A string like, for example:
* "push <fleetOrDevice>"
* That's literally so: "fleetOrDevice" is NOT replaced with the actual
* fleet slug or device uuid. The purpose is to find out the most / least
* fleet ID or device ID. The purpose is to find out the most / least
* used command verbs, so we can focus our development effort where it is most
* beneficial to end users.
*
@ -44,13 +49,40 @@ export async function trackCommand(commandSignature: string) {
scope.setExtra('command', commandSignature);
});
}
const { getCachedUsername } = await import('./utils/bootstrap');
let username: string | undefined;
try {
username = (await getCachedUsername())?.username;
} catch {
// ignore
}
const settings = await import('balena-settings-client');
const username = await (async () => {
const getStorage = await import('balena-settings-storage');
const dataDirectory = settings.get<string>('dataDirectory');
const storage = getStorage({ dataDirectory });
let token;
try {
token = await storage.get('token');
} catch {
// If we can't get a token then we can't get a username
return;
}
try {
const result = (await storage.get('cachedUsername')) as CachedUsername;
if (result.token === token) {
return result.username;
}
} catch {
// ignore
}
try {
const balena = getBalenaSdk();
const $username = await balena.auth.whoami();
await storage.set('cachedUsername', {
token,
username: $username,
} as CachedUsername);
return $username;
} catch {
return;
}
})();
if (!process.env.BALENARC_NO_SENTRY) {
Sentry!.configureScope((scope) => {
scope.setUser({
@ -64,7 +96,6 @@ export async function trackCommand(commandSignature: string) {
!process.env.BALENA_CLI_TEST_TYPE &&
!process.env.BALENARC_NO_ANALYTICS
) {
const settings = await import('balena-settings-client');
const balenaUrl = settings.get<string>('balenaUrl');
await sendEvent(balenaUrl, `[CLI] ${commandSignature}`, username);
}
@ -73,52 +104,38 @@ export async function trackCommand(commandSignature: string) {
}
}
const TIMEOUT = 4000;
/**
* Make the event tracking HTTPS request to balenaCloud's '/mixpanel' endpoint.
*/
async function sendEvent(balenaUrl: string, event: string, username?: string) {
const { default: got } = await import('got');
const trackData = {
api_key: 'balena-main',
events: [
{
event_type: event,
user_id: username,
version_name: packageJSON.version,
event_properties: {
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
arch: process.arch,
platform: process.platform,
node: process.version,
},
},
],
event,
properties: {
arch: process.arch,
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
distinct_id: username,
mp_lib: 'node',
node: process.version,
platform: process.platform,
token: 'balena-main',
version: packageJSON.version,
},
};
const url = `https://api.${balenaUrl}/mixpanel/track`;
const searchParams = {
ip: 0,
verbose: 0,
data: Buffer.from(JSON.stringify(trackData)).toString('base64'),
};
const url = `https://data.${balenaUrl}/amplitude/2/httpapi`;
try {
await got.post(url, {
json: trackData,
retry: 0,
timeout: {
// Starts when the request is initiated.
request: TIMEOUT,
// Starts when request has been flushed.
// Exits the request as soon as it's sent.
response: 0,
},
});
await got(url, { searchParams, retry: 0, timeout: 4000 });
} catch (e) {
if (process.env.DEBUG) {
console.error(`[debug] Event tracking error: ${e.message || e}`);
}
if (
e instanceof got.TimeoutError &&
TIMEOUT < (e.timings.phases.total ?? 0)
) {
if (e instanceof got.TimeoutError) {
console.error(stripIndent`
Timeout submitting analytics event to balenaCloud/openBalena.
If you are using the balena CLI in an air-gapped environment with a filtered

View File

@ -1,26 +1,35 @@
/*
Copyright 2020 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @license
* Copyright 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 { getCliUx, getChalk } from '../utils/lazy';
/**
* Used to extend FlagsDef for commands that output single-record data.
* Exposed to user in command options.
*/
export interface DataOutputOptions {
fields?: string;
json?: boolean;
}
/**
* Used to extend FlagsDef for commands that output multi-record data.
* Exposed to user in command options.
*/
export interface DataSetOutputOptions extends DataOutputOptions {
filter?: string;
'no-header'?: boolean;
@ -28,6 +37,14 @@ export interface DataSetOutputOptions extends DataOutputOptions {
sort?: string;
}
// Not exposed to user
export interface InternalOutputOptions {
displayNullValuesAs?: string;
hideNullOrUndefinedValues?: boolean;
titleField?: string;
noCapitalizeKeys?: boolean;
}
/**
* Output message to STDERR
*/
@ -49,7 +66,7 @@ export function outputMessage(msg: string) {
export async function outputData(
data: any[] | {},
fields: string[],
options: DataOutputOptions | DataSetOutputOptions,
options: (DataOutputOptions | DataSetOutputOptions) & InternalOutputOptions,
) {
if (Array.isArray(data)) {
await outputDataSet(data, fields, options as DataSetOutputOptions);
@ -68,7 +85,7 @@ export async function outputData(
async function outputDataSet(
data: any[],
fields: string[],
options: DataSetOutputOptions,
options: DataSetOutputOptions & InternalOutputOptions,
) {
// Oclif expects fields to be specified in the format used in table headers (though lowercase)
// By replacing underscores with spaces here, we can support both header format and actual field name
@ -77,6 +94,12 @@ async function outputDataSet(
options.filter = options.filter?.replace(/_/g, ' ');
options.sort = options.sort?.replace(/_/g, ' ');
if (!options.json) {
data = data.map((d) => {
return processNullValues(d, options);
});
}
getCliUx().table(
data,
// Convert fields array to column object keys
@ -97,7 +120,7 @@ async function outputDataSet(
}
/**
* Outputs a single data object (like `resin-cli-visuals table.vertical`),
* Outputs a single data object (similar to `resin-cli-visuals table.vertical`),
* but supporting a subset of options from `cli-ux table` (--json and --fields)
*
* @param data Array of data objects to output
@ -107,9 +130,9 @@ async function outputDataSet(
async function outputDataItem(
data: any,
fields: string[],
options: DataOutputOptions,
options: DataOutputOptions & InternalOutputOptions,
) {
const outData: typeof data = {};
let outData: typeof data = {};
// Convert comma separated list of fields in `options.fields` to array of correct format.
// Note, user may have specified the true field name (e.g. `some_field`),
@ -125,30 +148,83 @@ async function outputDataItem(
}
});
if (
(options.displayNullValuesAs || options.hideNullOrUndefinedValues) &&
!options.json
) {
outData = processNullValues(outData, options);
}
if (options.json) {
printLine(JSON.stringify(outData, undefined, 2));
} else {
const chalk = getChalk();
const { capitalize } = await import('lodash');
// Find longest key, so we can align results
const longestKeyLength = getLongestObjectKeyLength(outData);
if (options.titleField) {
printTitle(data[options.titleField as keyof any[]], options);
}
// Output one field per line
for (const [k, v] of Object.entries(outData)) {
for (let [k, v] of Object.entries(outData)) {
const shim = ' '.repeat(longestKeyLength - k.length);
const kDisplay = capitalize(k.replace(/_/g, ' '));
printLine(`${chalk.bold(kDisplay) + shim} : ${v}`);
let kDisplay = k.replace(/_/g, ' ');
// Start multiline values on the line below the field name
if (typeof v === 'string' && v.includes('\n')) {
v = `\n${v}`;
}
if (!options.noCapitalizeKeys) {
kDisplay = capitalize(kDisplay);
}
if (k !== options.titleField) {
printLine(` ${bold(kDisplay) + shim} : ${v}`);
}
}
}
}
function getLongestObjectKeyLength(o: any): number {
return Object.keys(o).length >= 1
? Object.keys(o).reduce((a, b) => {
return a.length > b.length ? a : b;
}).length
: 0;
/**
* Amend null/undefined values in data as per options:
* - options.displayNullValuesAs will replace the value with the specified string
* - options.hideNullOrUndefinedValues will remove the property from the data
*
* @param data The data object to process
* @param options Output options
*
* @returns a copy of the data with amended values.
*/
function processNullValues(data: any, options: InternalOutputOptions) {
const dataCopy = { ...data };
Object.entries(dataCopy).forEach(([k, v]) => {
if (v == null) {
if (options.displayNullValuesAs) {
dataCopy[k] = options.displayNullValuesAs;
} else if (options.hideNullOrUndefinedValues) {
delete dataCopy[k];
}
}
});
return dataCopy;
}
/**
* Print a title with underscore
*
* @param title The title string to print
* @param options Output options
*/
export function printTitle(
title: string,
options?: InternalOutputOptions & DataSetOutputOptions,
) {
if (!options?.['no-header']) {
printLine(` ${capitalize(bold(title))}`);
printLine(` ${bold('─'.repeat(title.length))}`);
}
}
function printLine(s: any) {
@ -156,3 +232,15 @@ function printLine(s: any) {
// but using this one explicitly for ease of testing
process.stdout.write(s + '\n');
}
function capitalize(s: string) {
return `${s[0].toUpperCase()}${s.slice(1)}`;
}
function bold(s: string) {
return getChalk().bold(s);
}
function getLongestObjectKeyLength(o: any): number {
return Math.max(0, ...Object.keys(o).map((k) => k.length));
}

View File

@ -53,6 +53,12 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
if (extractBooleanFlag(cmdSlice, '--debug')) {
process.env.DEBUG = '1';
}
// support global --v-next flag
if (extractBooleanFlag(cmdSlice, '--v-next')) {
const { version } = await import('../package.json');
const { inc } = await import('semver');
process.env.BALENA_CLI_VERSION_OVERRIDE = inc(version, 'major') || '';
}
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
}

View File

@ -119,61 +119,3 @@ export async function pkgExec(modFunc: string, args: string[]) {
console.error(err);
}
}
export interface CachedUsername {
token: string;
username: string;
}
let cachedUsername: CachedUsername | undefined;
/**
* Return the parsed contents of the `~/.balena/cachedUsername` file. If the file
* does not exist, create it with the details from the cloud. If not connected
* to the internet, return undefined. This function is used by `lib/events.ts`
* (event tracking) and `lib/utils/device/ssh.ts` and needs to gracefully handle
* the scenario of not being connected to the internet.
*/
export async function getCachedUsername(): Promise<CachedUsername | undefined> {
if (cachedUsername) {
return cachedUsername;
}
const [{ getBalenaSdk }, getStorage, settings] = await Promise.all([
import('./lazy'),
import('balena-settings-storage'),
import('balena-settings-client'),
]);
const dataDirectory = settings.get<string>('dataDirectory');
const storage = getStorage({ dataDirectory });
let token: string | undefined;
try {
token = (await storage.get('token')) as string | undefined;
} catch {
// ignore
}
if (!token) {
// If we can't get a token then we can't get a username
return;
}
try {
const result = (await storage.get('cachedUsername')) as
| CachedUsername
| undefined;
if (result && result.token === token && result.username) {
cachedUsername = result;
return cachedUsername;
}
} catch {
// ignore
}
try {
const username = await getBalenaSdk().auth.whoami();
if (username) {
cachedUsername = { token, username };
await storage.set('cachedUsername', cachedUsername);
}
} catch {
// ignore (not connected to the internet?)
}
return cachedUsername;
}

View File

@ -107,6 +107,16 @@ export const getDeviceAndMaybeAppFromUUID = _.memoize(
(_sdk, deviceUUID) => deviceUUID,
);
/** Given a device type alias like 'nuc', return the actual slug like 'intel-nuc'. */
export const unaliasDeviceType = _.memoize(async function (
sdk: SDK.BalenaSDK,
deviceType: string,
): Promise<string> {
return (
(await sdk.models.device.getManifestBySlug(deviceType)).slug || deviceType
);
});
/**
* Download balenaOS image for the specified `deviceType`.
* `OSVersion` may be one of:
@ -245,8 +255,8 @@ export async function getOsVersions(
);
// if slug is an alias, fetch the real slug
if (!versions.length) {
// unalias device type slug
slug = (await sdk.models.deviceType.get(slug, { $select: 'slug' })).slug;
// unaliasDeviceType() produces a nice error msg if slug is invalid
slug = await unaliasDeviceType(sdk, slug);
if (slug !== deviceType) {
versions = await sdk.models.os.getAvailableOsVersions(slug);
}

View File

@ -18,7 +18,7 @@ import { lowercaseIfSlug } from './normalization';
export const fleetRequired = {
name: 'fleet',
description: 'fleet name or slug (preferred)',
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
required: true,
parse: lowercaseIfSlug,
};

View File

@ -19,12 +19,13 @@ import { flags } from '@oclif/command';
import { stripIndent } from './lazy';
import { lowercaseIfSlug } from './normalization';
import { isV14 } from './version';
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
import type { DataOutputOptions, DataSetOutputOptions } from '../framework';
export const fleet = flags.string({
char: 'f',
description: 'fleet name or slug (preferred)',
description: 'fleet name, slug (preferred), or numeric ID (deprecated)',
parse: lowercaseIfSlug,
});
@ -96,6 +97,19 @@ export const deviceType = flags.string({
required: true,
});
export const deviceTypeIgnored = {
...(isV14()
? {}
: {
type: flags.string({
description: 'ignored - no longer required',
char: 't',
required: false,
hidden: true,
}),
}),
};
export const json: IBooleanFlag<boolean> = flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',

View File

@ -15,11 +15,8 @@
* limitations under the License.
*/
import type {
ImageModel,
ReleaseModel,
} from '@balena/compose/dist/release/models';
import type { Composition, ImageDescriptor } from '@balena/compose/dist/parse';
import type { ImageModel, ReleaseModel } from 'balena-release/build/models';
import type { Composition, ImageDescriptor } from 'resin-compose-parse';
import type { Pack } from 'tar-stream';
interface Image {
@ -42,7 +39,7 @@ export interface BuiltImage {
export interface TaggedImage {
localImage: import('dockerode').Image;
serviceImage: import('@balena/compose/dist/release/models').ImageModel;
serviceImage: import('balena-release/build/models').ImageModel;
serviceName: string;
logs: string;
props: BuiltImage.props;
@ -64,6 +61,7 @@ export interface ComposeOpts {
export interface ComposeCliFlags {
emulated: boolean;
dockerfile?: string;
logs: boolean;
nologs: boolean;
'multi-dockerignore': boolean;
'noparent-check': boolean;
@ -80,9 +78,7 @@ export interface ComposeProject {
}
export interface Release {
client: ReturnType<
typeof import('@balena/compose/dist/release').createClient
>;
client: ReturnType<typeof import('balena-release').createClient>;
release: Pick<
ReleaseModel,
| 'id'

View File

@ -19,7 +19,7 @@ import type { Renderer } from './compose_ts';
import type * as SDK from 'balena-sdk';
import type Dockerode = require('dockerode');
import * as path from 'path';
import type { Composition, ImageDescriptor } from '@balena/compose/dist/parse';
import type { Composition, ImageDescriptor } from 'resin-compose-parse';
import type {
BuiltImage,
ComposeOpts,
@ -64,7 +64,7 @@ export function createProject(
): ComposeProject {
const yml = require('js-yaml') as typeof import('js-yaml');
const compose =
require('@balena/compose/dist/parse') as typeof import('@balena/compose/dist/parse');
require('resin-compose-parse') as typeof import('resin-compose-parse');
// both methods below may throw.
const rawComposition = yml.load(composeStr);
@ -107,7 +107,7 @@ export const createRelease = async function (
const _ = require('lodash') as typeof import('lodash');
const crypto = require('crypto') as typeof import('crypto');
const releaseMod =
require('@balena/compose/dist/release') as typeof import('@balena/compose/dist/release');
require('balena-release') as typeof import('balena-release');
const client = releaseMod.createClient({ apiEndpoint, auth });
@ -214,7 +214,7 @@ export const getPreviousRepos = (
image: [SDK.Image];
}>;
const { getRegistryAndName } =
require('@balena/compose/dist/multibuild') as typeof import('@balena/compose/dist/multibuild');
require('resin-multibuild') as typeof import('resin-multibuild');
return Promise.all(
images.map(function (d) {
const imageName = d.image[0].is_stored_at__image_location || '';

View File

@ -16,7 +16,7 @@
*/
import { flags } from '@oclif/command';
import { BalenaSDK } from 'balena-sdk';
import type { TransposeOptions } from '@balena/compose/dist/emulate';
import type { TransposeOptions } from 'docker-qemu-transpose';
import type * as Dockerode from 'dockerode';
import { promises as fs } from 'fs';
import jsyaml = require('js-yaml');
@ -26,9 +26,8 @@ import type {
BuildConfig,
Composition,
ImageDescriptor,
} from '@balena/compose/dist/parse';
import type * as MultiBuild from '@balena/compose/dist/multibuild';
import * as semver from 'semver';
} from 'resin-compose-parse';
import type * as MultiBuild from 'resin-multibuild';
import type { Duplex, Readable } from 'stream';
import type { Pack } from 'tar-stream';
@ -118,7 +117,7 @@ export async function loadProject(
image?: string,
imageTag?: string,
): Promise<ComposeProject> {
const compose = await import('@balena/compose/dist/parse');
const compose = await import('resin-compose-parse');
const { createProject } = await import('./compose');
let composeName: string;
let composeStr: string;
@ -262,7 +261,7 @@ export async function buildProject(
opts: BuildProjectOpts,
): Promise<BuiltImage[]> {
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
const compose = await import('@balena/compose/dist/parse');
const compose = await import('resin-compose-parse');
const imageDescriptors = compose.parse(opts.composition);
const renderer = await startRenderer({ imageDescriptors, ...opts });
let buildSummaryByService: Dictionary<string> | undefined;
@ -333,7 +332,7 @@ async function $buildProject(
logger.logDebug('Prepared tasks; building...');
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
const builder = await import('@balena/compose/dist/multibuild');
const builder = await import('resin-multibuild');
const builtImages = await builder.performBuilds(
tasks,
@ -481,9 +480,8 @@ async function qemuTransposeBuildStream({
throw new Error(`No buildStream for task '${task.tag}'`);
}
const transpose = await import('@balena/compose/dist/emulate');
const { toPosixPath } = (await import('@balena/compose/dist/multibuild'))
.PathUtils;
const transpose = await import('docker-qemu-transpose');
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
const transposeOptions: TransposeOptions = {
hostQemuPath: toPosixPath(binPath),
@ -509,9 +507,9 @@ async function setTaskProgressHooks({
inlineLogs?: boolean;
renderer: Renderer;
task: BuildTaskPlus;
transposeOptions?: import('@balena/compose/dist/emulate').TransposeOptions;
transposeOptions?: import('docker-qemu-transpose').TransposeOptions;
}) {
const transpose = await import('@balena/compose/dist/emulate');
const transpose = await import('docker-qemu-transpose');
// Get the service-specific log stream
const logStream = renderer.streams[task.serviceName];
task.logBuffer = [];
@ -725,16 +723,16 @@ export async function getServiceDirsFromComposition(
*
* The `image` argument may therefore refer to either a `build` or `image` property
* of a service in a docker-compose.yml file, which is a bit confusing but it matches
* the `ImageDescriptor.image` property as defined by `@balena/compose/parse`.
* the `ImageDescriptor.image` property as defined by `resin-compose-parse`.
*
* Note that `@balena/compose/parse` "normalizes" the docker-compose.yml file such
* Note that `resin-compose-parse` "normalizes" the docker-compose.yml file such
* that, if `services.service.build` is a string, it is converted to a BuildConfig
* object with the string value assigned to `services.service.build.context`:
* https://github.com/balena-io-modules/balena-compose/blob/v0.1.0/lib/parse/compose.ts#L166-L167
* https://github.com/balena-io-modules/resin-compose-parse/blob/v2.1.3/src/compose.ts#L166-L167
* This is why this implementation works when `services.service.build` is defined
* as a string in the docker-compose.yml file.
*
* @param image The `ImageDescriptor.image` attribute parsed with `@balena/compose/parse`
* @param image The `ImageDescriptor.image` attribute parsed with `resin-compose-parse`
*/
export function isBuildConfig(
image: string | BuildConfig,
@ -760,8 +758,7 @@ export async function tarDirectory(
}: TarDirectoryOptions,
): Promise<import('stream').Readable> {
const { filterFilesWithDockerignore } = await import('./ignore');
const { toPosixPath } = (await import('@balena/compose/dist/multibuild'))
.PathUtils;
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
let readFile: (file: string) => Promise<Buffer>;
if (process.platform === 'win32') {
@ -943,7 +940,7 @@ async function parseRegistrySecrets(
throw new ExpectedError('Filename must end with .json, .yml or .yaml');
}
const raw = (await fs.readFile(secretsFilename)).toString();
const multiBuild = await import('@balena/compose/dist/multibuild');
const multiBuild = await import('resin-multibuild');
const registrySecrets =
new multiBuild.RegistrySecretValidator().validateRegistrySecrets(
isYaml ? require('js-yaml').load(raw) : JSON.parse(raw),
@ -972,7 +969,7 @@ export async function makeBuildTasks(
releaseHash: string = 'unavailable',
preprocessHook?: (dockerfile: string) => string,
): Promise<MultiBuild.BuildTask[]> {
const multiBuild = await import('@balena/compose/dist/multibuild');
const multiBuild = await import('resin-multibuild');
const buildTasks = await multiBuild.splitBuildStream(composition, tarStream);
logger.logDebug('Found build tasks:');
@ -1018,7 +1015,7 @@ async function performResolution(
releaseHash: string,
preprocessHook?: (dockerfile: string) => string,
): Promise<MultiBuild.BuildTask[]> {
const multiBuild = await import('@balena/compose/dist/multibuild');
const multiBuild = await import('resin-multibuild');
const resolveListeners: MultiBuild.ResolveListeners = {};
const resolvePromise = new Promise<never>((_resolve, reject) => {
resolveListeners.error = [reject];
@ -1083,7 +1080,7 @@ async function validateSpecifiedDockerfile(
dockerfilePath: string,
): Promise<string> {
const { contains, toNativePath, toPosixPath } = (
await import('@balena/compose/dist/multibuild')
await import('resin-multibuild')
).PathUtils;
const nativeProjectPath = path.normalize(projectPath);
@ -1243,7 +1240,7 @@ async function pushAndUpdateServiceImages(
token: string,
images: TaggedImage[],
afterEach: (
serviceImage: import('@balena/compose/dist/release/models').ImageModel,
serviceImage: import('balena-release/build/models').ImageModel,
props: object,
) => void,
) {
@ -1328,14 +1325,12 @@ async function pushAndUpdateServiceImages(
async function pushServiceImages(
docker: Dockerode,
logger: Logger,
pineClient: ReturnType<
typeof import('@balena/compose/dist/release').createClient
>,
pineClient: ReturnType<typeof import('balena-release').createClient>,
taggedImages: TaggedImage[],
token: string,
skipLogUpload: boolean,
): Promise<void> {
const releaseMod = await import('@balena/compose/dist/release');
const releaseMod = await import('balena-release');
logger.logInfo('Pushing images to registry...');
await pushAndUpdateServiceImages(
docker,
@ -1353,6 +1348,9 @@ async function pushServiceImages(
);
}
// TODO: This should be shared between the CLI & the Builder
const PLAIN_SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
export async function deployProject(
docker: Dockerode,
logger: Logger,
@ -1365,8 +1363,8 @@ export async function deployProject(
skipLogUpload: boolean,
projectPath: string,
isDraft: boolean,
): Promise<import('@balena/compose/dist/release/models').ReleaseModel> {
const releaseMod = await import('@balena/compose/dist/release');
): Promise<import('balena-release/build/models').ReleaseModel> {
const releaseMod = await import('balena-release');
const { createRelease, tagServiceImages } = await import('./compose');
const tty = (await import('./tty'))(process.stdout);
@ -1375,10 +1373,10 @@ export async function deployProject(
const contractPath = path.join(projectPath, 'balena.yml');
const contract = await getContractContent(contractPath);
if (contract?.version && !semver.valid(contract.version)) {
if (contract?.version && !PLAIN_SEMVER_REGEX.test(contract?.version)) {
throw new ExpectedError(stripIndent`\
Error: the version field in "${contractPath}"
is not a valid semver`);
Error: expected the version field in "${contractPath}"
to be a basic semver in the format '1.2.3'. Got '${contract.version}' instead`);
}
const $release = await runSpinner(
@ -1653,6 +1651,10 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
description:
'Alternative Dockerfile name/path, relative to the source folder',
}),
logs: flags.boolean({
description:
'No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.',
}),
nologs: flags.boolean({
description:
'Hide the image build log output (produce less verbose output)',

View File

@ -18,13 +18,13 @@
import * as semver from 'balena-semver';
import * as Docker from 'dockerode';
import * as _ from 'lodash';
import { Composition } from '@balena/compose/dist/parse';
import { Composition } from 'resin-compose-parse';
import {
BuildTask,
getAuthConfigObj,
LocalImage,
RegistrySecrets,
} from '@balena/compose/dist/multibuild';
} from 'resin-multibuild';
import type { Readable } from 'stream';
import { BALENA_ENGINE_TMP_PATH } from '../../config';
@ -321,7 +321,7 @@ async function performBuilds(
opts: DeviceDeployOptions,
buildLogs?: Dictionary<string>,
): Promise<BuildTask[]> {
const multibuild = await import('@balena/compose/dist/multibuild');
const multibuild = await import('resin-multibuild');
const buildTasks = await makeBuildTasks(
composition,
@ -370,7 +370,7 @@ async function performBuilds(
const imagesToRemove: string[] = [];
// Now tag any external images with the correct name that they should be,
// as this won't be done by @balena/compose/multibuild
// as this won't be done by resin-multibuild
await Promise.all(
localImages.map(async (localImage) => {
if (localImage.external) {
@ -414,7 +414,7 @@ export async function rebuildSingleTask(
// this should provide the following callback
containerIdCb?: (id: string) => void,
): Promise<string> {
const multibuild = await import('@balena/compose/dist/multibuild');
const multibuild = await import('resin-multibuild');
// First we run the build task, to get the new image id
let buildLogs = '';
const logHandler = (_s: string, line: string) => {
@ -533,17 +533,10 @@ async function assignDockerBuildOpts(
await Promise.all(
buildTasks.map(async (task: BuildTask) => {
task.dockerOpts = {
...(task.dockerOpts || {}),
...{
cachefrom: images,
labels: {
'io.resin.local.image': '1',
'io.resin.local.service': task.serviceName,
},
t: getImageNameFromTask(task),
nocache: opts.nocache,
forcerm: true,
pull: opts.pull,
cachefrom: images,
labels: {
'io.resin.local.image': '1',
'io.resin.local.service': task.serviceName,
},
t: getImageNameFromTask(task),
nocache: opts.nocache,

View File

@ -21,8 +21,8 @@ import * as fs from 'fs';
import Livepush, { ContainerNotRunningError } from 'livepush';
import * as _ from 'lodash';
import * as path from 'path';
import type { Composition } from '@balena/compose/dist/parse';
import type { BuildTask } from '@balena/compose/dist/multibuild';
import type { Composition } from 'resin-compose-parse';
import type { BuildTask } from 'resin-multibuild';
import { instanceOf } from '../../errors';
import Logger = require('../logger');

View File

@ -13,140 +13,89 @@ 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 { ExpectedError } from '../../errors';
import type { ContainerInfo } from 'dockerode';
import { stripIndent } from '../lazy';
import {
findBestUsernameForDevice,
getRemoteCommandOutput,
runRemoteCommand,
SshRemoteCommandOpts,
} from '../ssh';
export interface DeviceSSHOpts extends SshRemoteCommandOpts {
export interface DeviceSSHOpts {
address: string;
port?: number;
forceTTY?: boolean;
verbose: boolean;
service?: string;
}
const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then echo "balena"; else echo "docker"; fi)`;
/**
* List the running containers on the device over ssh, and return the full
* container name that matches the given service name.
*
* Note: In the past, two other approaches were implemented for this function:
*
* - Obtaining container IDs through a supervisor API call:
* '/supervisor/v2/containerId' endpoint, via cloud.
* - Obtaining container IDs using 'dockerode' connected directly to
* balenaEngine on a device, TCP port 2375.
*
* The problem with using the supervisor API is that it means that 'balena ssh'
* becomes dependent on the supervisor being up an running, but sometimes ssh
* is needed to investigate devices issues where the supervisor has got into
* trouble (e.g. supervisor in restart loop). This is the subject of CLI issue
* https://github.com/balena-io/balena-cli/issues/1560 .
*
* The problem with using dockerode to connect directly to port 2375 (balenaEngine)
* is that it only works with development variants of balenaOS. Production variants
* block access to port 2375 for security reasons. 'balena ssh' should support
* production variants as well, especially after balenaOS v2.44.0 that introduced
* support for using the cloud account username for ssh authentication.
*
* Overall, the most reliable approach is to run 'balena-engine ps' over ssh.
* It is OK to depend on balenaEngine because ssh to a container is implemented
* through 'balena-engine exec' anyway, and of course it is OK to depend on ssh
* itself.
*/
export async function getContainerIdForService(
opts: SshRemoteCommandOpts & { service: string; deviceUuid?: string },
): Promise<string> {
opts.cmd = `"${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`;
if (opts.deviceUuid) {
// If a device UUID is given, perform ssh via cloud proxy 'host' command
opts.cmd = `host ${opts.deviceUuid} ${opts.cmd}`;
}
const psLines: string[] = (
await getRemoteCommandOutput({ ...opts, stderr: 'inherit' })
).stdout
.toString()
.split('\n')
.filter((l) => l);
const { escapeRegExp } = await import('lodash');
const regex = new RegExp(`(?:^|\\/)${escapeRegExp(opts.service)}_\\d+_\\d+`);
// Old balenaOS container name pattern:
// main_1234567_2345678
// New balenaOS container name patterns:
// main_1234567_2345678_a000b111c222d333e444f555a666b777
// main_1_1_localrelease
const nameRegex = /(?:^|\/)([a-zA-Z0-9_-]+)_\d+_\d+(?:_.+)?$/;
const serviceNames: string[] = [];
const containerNames: string[] = [];
let containerId: string | undefined;
// sample psLine: 'b603c74e951e bar_4587562_2078151_3261c9d4c22f2c53a5267be459c89990'
for (const psLine of psLines) {
const [cId, name] = psLine.split(' ');
if (cId && name) {
if (regex.test(name)) {
containerNames.push(name);
containerId = cId;
}
const match = name.match(nameRegex);
if (match) {
serviceNames.push(match[1]);
}
}
}
if (containerNames.length > 1) {
const [s, d] = [opts.service, opts.deviceUuid || opts.hostname];
throw new ExpectedError(stripIndent`
Found more than one container matching service name "${s}" on device "${d}":
${containerNames.join(', ')}
Use different service names to avoid ambiguity.
`);
}
if (!containerId) {
const [s, d] = [opts.service, opts.deviceUuid || opts.hostname];
throw new ExpectedError(
`Could not find a container matching service name "${s}" on device "${d}".${
serviceNames.length > 0
? `\nAvailable services:\n\t${serviceNames.join('\n\t')}`
: ''
}`,
);
}
return containerId;
}
export const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then echo "balena"; else echo "docker"; fi)`;
export async function performLocalDeviceSSH(
opts: DeviceSSHOpts,
): Promise<void> {
// Before we started using `findBestUsernameForDevice`, we tried the approach
// of attempting ssh with the 'root' username first and, if that failed, then
// attempting ssh with a regular user (balenaCloud username). The problem with
// that approach was that it would print the following message to the console:
// "root@192.168.1.36: Permission denied (publickey)"
// ... right before having success as a regular user, which looked broken or
// confusing from users' point of view. Capturing stderr to prevent that
// message from being printed is tricky because the messages printed to stderr
// may include the stderr output of remote commands that are of interest to
// the user.
const username = await findBestUsernameForDevice(opts.hostname, opts.port);
let cmd = '';
const { escapeRegExp, reduce } = await import('lodash');
const { spawnSshAndThrowOnError } = await import('../ssh');
const { ExpectedError } = await import('../../errors');
if (opts.service) {
const containerId = await getContainerIdForService({
...opts,
service: opts.service,
username,
let command = '';
if (opts.service != null) {
// Get the containers which are on-device. Currently we
// are single application, which means we can assume any
// container which fulfills the form of
// $serviceName_$appId_$releaseId is what we want. Once
// we have multi-app, we should show a dialog which
// allows the user to choose the correct container
const Docker = await import('dockerode');
const docker = new Docker({
host: opts.address,
port: 2375,
});
const regex = new RegExp(`(^|\\/)${escapeRegExp(opts.service)}_\\d+_\\d+`);
const nameRegex = /\/?([a-zA-Z0-9_-]+)_\d+_\d+/;
let allContainers: ContainerInfo[];
try {
allContainers = await docker.listContainers();
} catch (_e) {
throw new ExpectedError(stripIndent`
Could not access docker daemon on device ${opts.address}.
Please ensure the device is in local mode.`);
}
const serviceNames: string[] = [];
const containers: Array<{ id: string; name: string }> = [];
for (const container of allContainers) {
for (const name of container.Names) {
if (regex.test(name)) {
containers.push({ id: container.Id, name });
break;
}
const match = name.match(nameRegex);
if (match) {
serviceNames.push(match[1]);
}
}
}
if (containers.length === 0) {
throw new ExpectedError(
`Could not find a service on device with name ${opts.service}. ${
serviceNames.length > 0
? `Available services:\n${reduce(
serviceNames,
(str, name) => `${str}\t${name}\n`,
'',
)}`
: ''
}`,
);
}
if (containers.length > 1) {
throw new ExpectedError(stripIndent`
Found more than one container matching service name "${opts.service}":
${containers.map((container) => container.name).join(', ')}
Use different service names to avoid ambiguity.
`);
}
const containerId = containers[0].id;
const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`;
// stdin (fd=0) is not a tty when data is piped in, for example
// echo 'ls -la; exit;' | balena ssh 192.168.0.20 service1
@ -154,8 +103,17 @@ export async function performLocalDeviceSSH(
// https://assets.balena.io/newsletter/2020-01/pipe.png
const isTTY = !!opts.forceTTY || (await import('tty')).isatty(0);
const ttyFlag = isTTY ? '-t' : '';
cmd = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
command = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
}
await runRemoteCommand({ ...opts, cmd, username });
return spawnSshAndThrowOnError([
...(opts.verbose ? ['-vvv'] : []),
'-t',
...['-p', opts.port ? opts.port.toString() : '22222'],
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
`root@${opts.address}`,
...(command ? [command] : []),
]);
}

View File

@ -105,7 +105,7 @@ export interface BuildOpts {
cachefrom?: string[];
nocache?: boolean;
pull?: boolean;
registryconfig?: import('@balena/compose/dist/multibuild').RegistrySecrets;
registryconfig?: import('resin-multibuild').RegistrySecrets;
squash?: boolean;
t?: string; // only the tag portion of the image name, e.g. 'abc' in 'myimg:abc'
}
@ -132,7 +132,7 @@ export function generateBuildOpts(options: {
'cache-from'?: string;
nocache: boolean;
pull?: boolean;
'registry-secrets'?: import('@balena/compose/dist/multibuild').RegistrySecrets;
'registry-secrets'?: import('resin-multibuild').RegistrySecrets;
squash: boolean;
tag?: string;
}): BuildOpts {
@ -174,8 +174,14 @@ export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
);
}
export interface ExtendedDockerOptions extends dockerode.DockerOptions {
docker?: string; // socket path, e.g. /var/run/docker.sock
dockerHost?: string; // host name or IP address
dockerPort?: number; // TCP port number, e.g. 2375
}
export async function getDocker(
options: DockerConnectionCliFlags,
options: ExtendedDockerOptions,
): Promise<dockerode> {
const connectOpts = await generateConnectOpts(options);
const client = await createClient(connectOpts);
@ -190,18 +196,14 @@ export async function createClient(
return new Docker(opts);
}
/**
* Initialize Docker connection options with the default values from the
* 'docker-modem' package, which takes several env vars into account,
* including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
* https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L15-L70
*
* @param opts Command line options like --dockerHost and --dockerPort
*/
export function getDefaultDockerModemOpts(
opts: DockerConnectionCliFlags,
): dockerode.DockerOptions {
const connectOpts: dockerode.DockerOptions = {};
async function generateConnectOpts(opts: ExtendedDockerOptions) {
let connectOpts: dockerode.DockerOptions = {};
// Start with docker-modem defaults which take several env vars into account,
// including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
// https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L15-L70
const Modem = require('docker-modem');
const defaultOpts = new Modem();
const optsOfInterest: Array<keyof dockerode.DockerOptions> = [
'ca',
'cert',
@ -213,33 +215,9 @@ export function getDefaultDockerModemOpts(
'username',
'timeout',
];
const Modem = require('docker-modem');
const originalDockerHost = process.env.DOCKER_HOST;
try {
if (opts.dockerHost) {
process.env.DOCKER_HOST ||= opts.dockerPort
? `${opts.dockerHost}:${opts.dockerPort}`
: opts.dockerHost;
}
const defaultOpts = new Modem();
for (const opt of optsOfInterest) {
connectOpts[opt] = defaultOpts[opt];
}
} finally {
// Did you know? Any value assigned to `process.env.XXX` becomes a string.
// For example, `process.env.DOCKER_HOST = undefined` results in
// value 'undefined' (a 9-character string) being assigned.
if (originalDockerHost) {
process.env.DOCKER_HOST = originalDockerHost;
} else {
delete process.env.DOCKER_HOST;
}
for (const opt of optsOfInterest) {
connectOpts[opt] = defaultOpts[opt];
}
return connectOpts;
}
export async function generateConnectOpts(opts: DockerConnectionCliFlags) {
let connectOpts = getDefaultDockerModemOpts(opts);
// Now override the default options with any explicit command line options
if (opts.docker != null && opts.dockerHost == null) {
@ -263,9 +241,9 @@ export async function generateConnectOpts(opts: DockerConnectionCliFlags) {
// These should be file paths (strings)
const tlsOpts = [opts.ca, opts.cert, opts.key];
// If any tlsOpts are set...
// If any are set...
if (tlsOpts.some((opt) => opt)) {
// but not all
// but not all ()
if (!tlsOpts.every((opt) => opt)) {
throw new ExpectedError(
'You must provide a CA, certificate and key in order to use TLS',
@ -280,11 +258,7 @@ export async function generateConnectOpts(opts: DockerConnectionCliFlags) {
const [ca, cert, key] = await Promise.all(
tlsOpts.map((opt: string) => fs.readFile(opt, 'utf8')),
);
// Also ensure that the protocol is 'https' like 'docker-modem' does:
// https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L101-L103
// TODO: delete redundant logic from this function now that similar logic
// exists in the 'docker-modem' package.
connectOpts = { ...connectOpts, ca, cert, key, protocol: 'https' };
connectOpts = { ...connectOpts, ca, cert, key };
}
return connectOpts;

View File

@ -107,50 +107,21 @@ export async function getManifest(
deviceType: string,
): Promise<BalenaSdk.DeviceTypeJson.DeviceType> {
const init = await import('balena-device-init');
const sdk = getBalenaSdk();
const manifest = await init.getImageManifest(image);
if (
manifest != null &&
manifest.slug !== deviceType &&
manifest.slug !== (await sdk.models.deviceType.get(deviceType)).slug
) {
const { ExpectedError } = await import('../errors');
throw new ExpectedError(
`The device type of the provided OS image ${manifest.slug}, does not match the expected device type ${deviceType}`,
);
if (manifest != null) {
return manifest;
}
return manifest ?? (await sdk.models.device.getManifestBySlug(deviceType));
return getBalenaSdk().models.device.getManifestBySlug(deviceType);
}
export const areDeviceTypesCompatible = async (
appDeviceTypeSlug: string,
osDeviceTypeSlug: string,
) => {
if (appDeviceTypeSlug === osDeviceTypeSlug) {
return true;
}
const sdk = getBalenaSdk();
const pineOptions = {
$select: 'is_of__cpu_architecture',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
} as const;
const [appDeviceType, osDeviceType] = await Promise.all(
[appDeviceTypeSlug, osDeviceTypeSlug].map(
(dtSlug) =>
sdk.models.deviceType.get(dtSlug, pineOptions) as Promise<
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
>,
),
);
return sdk.models.os.isArchitectureCompatibleWith(
osDeviceType.is_of__cpu_architecture[0].slug,
appDeviceType.is_of__cpu_architecture[0].slug,
);
};
export const areDeviceTypesCompatible = (
appDeviceType: BalenaSdk.DeviceTypeJson.DeviceType,
osDeviceType: BalenaSdk.DeviceTypeJson.DeviceType,
) =>
getBalenaSdk().models.os.isArchitectureCompatibleWith(
osDeviceType.arch,
appDeviceType.arch,
) && !!appDeviceType.isDependent === !!osDeviceType.isDependent;
export async function osProgressHandler(step: InitializeEmitter) {
step.on('stdout', process.stdout.write.bind(process.stdout));

View File

@ -137,7 +137,7 @@ adding exception patterns to the applicable .dockerignore file(s), for example
- https://www.npmjs.com/package/@balena/dockerignore`;
export const applicationIdInfo = `\
Fleets may be specified by fleet name or slug. Fleet slugs are
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
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -145,7 +145,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).`;
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.`;
export const applicationNameNote = `\
Fleets may be specified by fleet name or slug. Slugs are recommended because

View File

@ -280,64 +280,71 @@ export function inferOrSelectDevice(preferredUuid: string) {
}
/*
* Given fleetOrDevice, which may be
* - a fleet name
* - a fleet slug
* Given applicationOrDevice, which may be
* - an application name
* - an application slug
* - an application id (integer)
* - a device uuid
* Either:
* - in case of device uuid, return uuid of device after verifying that it exists and is online.
* - in case of fleet, return uuid of device user selects from list of online devices.
* - in case of application, return uuid of device user selects from list of online devices.
*
* TODO: Modify this when app IDs dropped.
*/
export async function getOnlineTargetDeviceUuid(
sdk: BalenaSDK,
fleetOrDevice: string,
applicationOrDevice: string,
) {
const logger = (await import('../utils/logger')).getLogger();
// If looks like UUID, probably device
if (validation.validateUuid(fleetOrDevice)) {
if (validation.validateUuid(applicationOrDevice)) {
let device: Device;
try {
logger.logDebug(
`Trying to fetch device by UUID ${fleetOrDevice} (${typeof fleetOrDevice})`,
`Trying to fetch device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
);
device = await sdk.models.device.get(fleetOrDevice, {
device = await sdk.models.device.get(applicationOrDevice, {
$select: ['uuid', 'is_online'],
});
if (!device.is_online) {
throw new ExpectedError(`Device with UUID ${fleetOrDevice} is offline`);
throw new ExpectedError(
`Device with UUID ${applicationOrDevice} is offline`,
);
}
return device.uuid;
} catch (err) {
const { BalenaDeviceNotFound } = await import('balena-errors');
if (instanceOf(err, BalenaDeviceNotFound)) {
logger.logDebug(`Device with UUID ${fleetOrDevice} not found`);
// Now try application
logger.logDebug(`Device with UUID ${applicationOrDevice} not found`);
// Now try app
} else {
throw err;
}
}
}
// Not a device UUID, try application
let application: Application;
// Not a device UUID, try app
let app: Application;
try {
logger.logDebug(`Fetching fleet ${fleetOrDevice}`);
logger.logDebug(`Fetching fleet ${applicationOrDevice}`);
const { getApplication } = await import('./sdk');
application = await getApplication(sdk, fleetOrDevice);
app = await getApplication(sdk, applicationOrDevice);
} catch (err) {
const { BalenaApplicationNotFound } = await import('balena-errors');
if (instanceOf(err, BalenaApplicationNotFound)) {
throw new ExpectedError(`Fleet or Device not found: ${fleetOrDevice}`);
throw new ExpectedError(
`Fleet or Device not found: ${applicationOrDevice}`,
);
} else {
throw err;
}
}
// App found, load its devices
const devices = await sdk.models.device.getAllByApplication(application.id, {
const devices = await sdk.models.device.getAllByApplication(app.id, {
$select: ['device_name', 'uuid'],
$filter: { is_online: true },
});
@ -345,13 +352,13 @@ export async function getOnlineTargetDeviceUuid(
// Throw if no devices online
if (_.isEmpty(devices)) {
throw new ExpectedError(
`Fleet ${application.slug} found, but has no devices online.`,
`Fleet ${app.slug} found, but has no devices online.`,
);
}
// Ask user to select from online devices for fleet
// Ask user to select from online devices for application
return getCliForm().ask({
message: `Select a device on fleet ${application.slug}`,
message: `Select a device on fleet ${app.slug}`,
type: 'list',
default: devices[0].uuid,
choices: _.map(devices, (device) => ({

View File

@ -20,7 +20,7 @@ import { ExpectedError, printErrorMessage } from '../errors';
import { getVisuals, stripIndent, getCliForm } from './lazy';
import Logger = require('./logger');
import { confirm } from './patterns';
import { getLocalDeviceCmdStdout, getDeviceOsRelease } from './ssh';
import { exec, execBuffered, getDeviceOsRelease } from './ssh';
const MIN_BALENAOS_VERSION = 'v2.14.0';
@ -80,12 +80,7 @@ export async function leave(
logger.logDebug('Deconfiguring...');
await deconfigure(deviceHostnameOrIp);
logger.logSuccess(stripIndent`
Device successfully left the platform. The device will still be listed as part
of the fleet, but changes to the fleet will no longer affect the device and its
status will eventually be reported as 'Offline'. To irrecoverably delete the
device from the fleet, use the 'balena device rm' command or delete it through
the balenaCloud web dashboard.`);
logger.logSuccess('Device successfully left the platform.');
}
async function execCommand(
@ -93,25 +88,20 @@ async function execCommand(
cmd: string,
msg: string,
): Promise<void> {
const { Writable } = await import('stream');
const through = await import('through2');
const visuals = getVisuals();
const spinner = new visuals.Spinner(`[${deviceIp}] Connecting...`);
const innerSpinner = spinner.spinner;
const stream = new Writable({
write(_chunk: Buffer, _enc, callback) {
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
callback();
},
const stream = through(function (data, _enc, cb) {
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
cb(null, data);
});
spinner.start();
try {
await getLocalDeviceCmdStdout(deviceIp, cmd, stream);
} finally {
spinner.stop();
}
await exec(deviceIp, cmd, stream);
spinner.stop();
}
async function configure(deviceIp: string, config: any): Promise<void> {
@ -131,7 +121,7 @@ async function deconfigure(deviceIp: string): Promise<void> {
async function assertDeviceIsCompatible(deviceIp: string): Promise<void> {
const cmd = 'os-config --version';
try {
await getLocalDeviceCmdStdout(deviceIp, cmd);
await execBuffered(deviceIp, cmd);
} catch (err) {
if (err instanceof ExpectedError) {
throw err;
@ -198,37 +188,31 @@ async function selectAppFromList(
async function getOrSelectApplication(
sdk: BalenaSdk.BalenaSDK,
deviceTypeSlug: string,
deviceType: string,
appName?: string,
): Promise<ApplicationWithDeviceType> {
const pineOptions = {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
} as const;
const [deviceType, allDeviceTypes] = await Promise.all([
sdk.models.deviceType.get(deviceTypeSlug, pineOptions) as Promise<
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
>,
sdk.models.deviceType.getAllSupported(pineOptions) as Promise<
Array<BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>>
>,
]);
const _ = await import('lodash');
const compatibleDeviceTypes = allDeviceTypes
.filter((dt) =>
sdk.models.os.isArchitectureCompatibleWith(
deviceType.is_of__cpu_architecture[0].slug,
dt.is_of__cpu_architecture[0].slug,
),
const allDeviceTypes = await sdk.models.config.getDeviceTypes();
const deviceTypeManifest = _.find(allDeviceTypes, { slug: deviceType });
if (!deviceTypeManifest) {
throw new ExpectedError(`"${deviceType}" is not a valid device type`);
}
const compatibleDeviceTypes = _(allDeviceTypes)
.filter(
(dt) =>
sdk.models.os.isArchitectureCompatibleWith(
deviceTypeManifest.arch,
dt.arch,
) &&
!!dt.isDependent === !!deviceTypeManifest.isDependent &&
dt.state !== 'DISCONTINUED',
)
.map((type) => type.slug);
.map((type) => type.slug)
.value();
if (!appName) {
return createOrSelectApp(sdk, compatibleDeviceTypes, deviceTypeSlug);
return createOrSelectApp(sdk, compatibleDeviceTypes, deviceType);
}
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
@ -263,13 +247,13 @@ async function getOrSelectApplication(
undefined,
true,
);
return await createApplication(sdk, deviceTypeSlug, name);
return await createApplication(sdk, deviceType, name);
}
// We've found at least one fleet with the given name.
// Filter out fleets for non-matching device types and see what we're left with.
const validApplications = applications.filter((app) =>
compatibleDeviceTypes.includes(app.is_for__device_type[0].slug),
_.includes(compatibleDeviceTypes, app.is_for__device_type[0].slug),
);
if (validApplications.length === 0) {
@ -340,7 +324,13 @@ async function createApplication(
try {
await sdk.models.application.getDirectlyAccessible(appName, {
$filter: {
slug: { $startswith: `${username!.toLowerCase()}/` },
$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: '/' } } },
],
},
});
// TODO: This is the only example in the codebase where `printErrorMessage()`

View File

@ -21,7 +21,7 @@ import { ExpectedError } from '../errors';
import { getBalenaSdk, stripIndent } from './lazy';
import Logger = require('./logger');
export const QEMU_VERSION = 'v7.0.0+balena1';
export const QEMU_VERSION = 'v6.0.0+balena1';
export const QEMU_BIN_NAME = 'qemu-execve';
export function qemuPathInContext(context: string) {

View File

@ -17,7 +17,7 @@ import type { BalenaSDK } from 'balena-sdk';
import * as JSONStream from 'JSONStream';
import * as readline from 'readline';
import * as request from 'request';
import { RegistrySecrets } from '@balena/compose/dist/multibuild';
import { RegistrySecrets } from 'resin-multibuild';
import type * as Stream from 'stream';
import streamToPromise = require('stream-to-promise');
import type { Pack } from 'tar-stream';

View File

@ -24,32 +24,54 @@ import type {
/**
* Get a fleet object, disambiguating the fleet identifier which may be a
* a fleet slug or name.
* a fleet slug, name or numeric database ID (as a string).
* TODO: add support for fleet UUIDs.
*/
export async function getApplication(
sdk: BalenaSDK,
nameOrSlug: string,
nameOrSlugOrId: string | number,
options?: PineOptions<Application>,
): Promise<Application> {
const { looksLikeFleetSlug } = await import('./validation');
if (!looksLikeFleetSlug(nameOrSlug)) {
// Not a slug: must be an app name.
// TODO: revisit this logic when we add support for fleet UUIDs.
return await sdk.models.application.getAppByName(
nameOrSlug,
const { looksLikeFleetSlug, looksLikeInteger } = await import('./validation');
if (
typeof nameOrSlugOrId === 'string' &&
looksLikeFleetSlug(nameOrSlugOrId)
) {
return await sdk.models.application.getDirectlyAccessible(
nameOrSlugOrId,
options,
'directly_accessible',
);
}
return await sdk.models.application.getDirectlyAccessible(
nameOrSlug,
if (typeof nameOrSlugOrId === 'number' || looksLikeInteger(nameOrSlugOrId)) {
try {
// Test for existence of app with this numerical ID
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)) {
throw e;
}
// App with this numerical ID not found, but there may be an app with this numerical name.
}
}
// 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 a fleet name or slug, return its slug.
* 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,
@ -57,15 +79,16 @@ export async function getApplication(
*/
export async function getFleetSlug(
sdk: BalenaSDK,
nameOrSlug: string,
nameOrSlugOrId: string | number,
): Promise<string> {
const { looksLikeFleetSlug } = await import('./validation');
if (!looksLikeFleetSlug(nameOrSlug)) {
// Not a slug: must be an app name.
// TODO: revisit this logic when we add support for fleet UUIDs.
return (await getApplication(sdk, nameOrSlug)).slug;
if (
typeof nameOrSlugOrId === 'string' &&
looksLikeFleetSlug(nameOrSlugOrId)
) {
return nameOrSlugOrId.toLowerCase();
}
return nameOrSlug.toLowerCase();
return (await getApplication(sdk, nameOrSlugOrId)).slug;
}
/**

View File

@ -16,314 +16,147 @@
*/
import { spawn, StdioOptions } from 'child_process';
import * as _ from 'lodash';
import { TypedError } from 'typed-error';
import { ExpectedError } from '../errors';
export class SshPermissionDeniedError extends ExpectedError {}
export class ExecError extends TypedError {
public cmd: string;
public exitCode: number;
export class RemoteCommandError extends ExpectedError {
cmd: string;
exitCode?: number;
exitSignal?: NodeJS.Signals;
constructor(cmd: string, exitCode?: number, exitSignal?: NodeJS.Signals) {
super(sshErrorMessage(cmd, exitSignal, exitCode));
constructor(cmd: string, exitCode: number) {
super(`Command '${cmd}' failed with error: ${exitCode}`);
this.cmd = cmd;
this.exitCode = exitCode;
this.exitSignal = exitSignal;
}
}
export interface SshRemoteCommandOpts {
cmd?: string;
hostname: string;
ignoreStdin?: boolean;
port?: number | 'cloud' | 'local';
proxyCommand?: string[];
username?: string;
verbose?: boolean;
}
export const stdioIgnore: {
stdin: 'ignore';
stdout: 'ignore';
stderr: 'ignore';
} = {
stdin: 'ignore',
stdout: 'ignore',
stderr: 'ignore',
};
export function sshArgsForRemoteCommand({
cmd = '',
hostname,
ignoreStdin = false,
port,
proxyCommand,
username = 'root',
verbose = false,
}: SshRemoteCommandOpts): string[] {
port = port === 'local' ? 22222 : port === 'cloud' ? 22 : port;
return [
...(verbose ? ['-vvv'] : []),
...(ignoreStdin ? ['-n'] : []),
'-t',
...(port ? ['-p', port.toString()] : []),
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
...(proxyCommand && proxyCommand.length
? ['-o', `ProxyCommand=${proxyCommand.join(' ')}`]
: []),
`${username}@${hostname}`,
...(cmd ? [cmd] : []),
];
}
/**
* Execute the given command on a local balenaOS device over ssh.
* @param cmd Shell command to execute on the device
* @param hostname Device's hostname or IP address
* @param port SSH server TCP port number or 'local' (22222) or 'cloud' (22)
* @param stdin Readable stream to pipe to the remote command stdin,
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
* @param stdout Writeable stream to pipe from the remote command stdout,
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
* @param stderr Writeable stream to pipe from the remote command stdout,
* or 'ignore' or 'inherit' as documented in the child_process.spawn function.
* @param username SSH username for authorization. With balenaOS 2.44.0 or
* later, it can be a balenaCloud username.
* @param verbose Produce debugging output
*/
export async function runRemoteCommand({
cmd = '',
hostname,
port,
proxyCommand,
stdin = 'inherit',
stdout = 'inherit',
stderr = 'inherit',
username = 'root',
verbose = false,
}: SshRemoteCommandOpts & {
stdin?: 'ignore' | 'inherit' | NodeJS.ReadableStream;
stdout?: 'ignore' | 'inherit' | NodeJS.WritableStream;
stderr?: 'ignore' | 'inherit' | NodeJS.WritableStream;
}): Promise<void> {
let ignoreStdin: boolean;
if (stdin === 'ignore') {
// Set ignoreStdin=true in order for the "ssh -n" option to be used to
// prevent the ssh client from using the CLI process stdin. In addition,
// stdin must be forced to 'inherit' (if it is not a readable stream) in
// order to work around a bug in older versions of the built-in Windows
// 10 ssh client that otherwise prints the following to stderr and
// hangs: "GetConsoleMode on STD_INPUT_HANDLE failed with 6"
// They actually fixed the bug in newer versions of the ssh client:
// https://github.com/PowerShell/Win32-OpenSSH/issues/856 but users
// have to manually download and install a new client.
ignoreStdin = true;
stdin = 'inherit';
} else {
ignoreStdin = false;
}
export async function exec(
deviceIp: string,
cmd: string,
stdout?: NodeJS.WritableStream,
): Promise<void> {
const { which } = await import('./which');
const program = await which('ssh');
const args = sshArgsForRemoteCommand({
const args = [
'-n',
'-t',
'-p',
'22222',
'-o',
'LogLevel=ERROR',
'-o',
'StrictHostKeyChecking=no',
'-o',
'UserKnownHostsFile=/dev/null',
`root@${deviceIp}`,
cmd,
hostname,
ignoreStdin,
port,
proxyCommand,
username,
verbose,
});
];
if (process.env.DEBUG) {
const logger = (await import('./logger')).getLogger();
logger.logDebug(`Executing [${program},${args}]`);
}
// Note: stdin must be 'inherit' to workaround a bug in older versions of
// the built-in Windows 10 ssh client that otherwise prints the following
// to stderr and hangs: "GetConsoleMode on STD_INPUT_HANDLE failed with 6"
// They fixed the bug in newer versions of the ssh client:
// https://github.com/PowerShell/Win32-OpenSSH/issues/856
// but users whould have to manually download and install a new client.
// Note that "ssh -n" does not solve the problem, but should in theory
// prevent the ssh client from using the CLI process stdin, even if it
// is connected with 'inherit'.
const stdio: StdioOptions = [
typeof stdin === 'string' ? stdin : 'pipe',
typeof stdout === 'string' ? stdout : 'pipe',
typeof stderr === 'string' ? stderr : 'pipe',
'inherit',
stdout ? 'pipe' : 'inherit',
'inherit',
];
let exitCode: number | undefined;
let exitSignal: NodeJS.Signals | undefined;
try {
[exitCode, exitSignal] = await new Promise<[number, NodeJS.Signals]>(
(resolve, reject) => {
const ps = spawn(program, args, { stdio })
.on('error', reject)
.on('close', (code, signal) => resolve([code, signal]));
if (ps.stdin && stdin && typeof stdin !== 'string') {
stdin.pipe(ps.stdin);
}
if (ps.stdout && stdout && typeof stdout !== 'string') {
ps.stdout.pipe(stdout);
}
if (ps.stderr && stderr && typeof stderr !== 'string') {
ps.stderr.pipe(stderr);
}
},
);
} catch (error) {
const msg = [
`ssh failed with exit code=${exitCode} signal=${exitSignal}:`,
`[${program}, ${args.join(', ')}]`,
...(error ? [`${error}`] : []),
];
throw new ExpectedError(msg.join('\n'));
}
if (exitCode || exitSignal) {
throw new RemoteCommandError(cmd, exitCode, exitSignal);
const exitCode = await new Promise<number>((resolve, reject) => {
const ps = spawn(program, args, { stdio })
.on('error', reject)
.on('close', resolve);
if (stdout && ps.stdout) {
ps.stdout.pipe(stdout);
}
});
if (exitCode !== 0) {
throw new ExecError(cmd, exitCode);
}
}
/**
* Execute the given command on a local balenaOS device over ssh.
* Capture stdout and/or stderr to Buffers and return them.
*
* @param deviceIp IP address of the local device
* @param cmd Shell command to execute on the device
* @param opts Options
* @param opts.username SSH username for authorization. With balenaOS 2.44.0 or
* later, it may be a balenaCloud username. Otherwise, 'root'.
* @param opts.stdin Passed through to the runRemoteCommand function
* @param opts.stdout If 'capture', capture stdout to a Buffer.
* @param opts.stderr If 'capture', capture stdout to a Buffer.
*/
export async function getRemoteCommandOutput({
cmd,
hostname,
port,
proxyCommand,
stdin = 'ignore',
stdout = 'capture',
stderr = 'capture',
username = 'root',
verbose = false,
}: SshRemoteCommandOpts & {
stdin?: 'ignore' | 'inherit' | NodeJS.ReadableStream;
stdout?: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream;
stderr?: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream;
}): Promise<{ stdout: Buffer; stderr: Buffer }> {
const { Writable } = await import('stream');
const stdoutChunks: Buffer[] = [];
const stderrChunks: Buffer[] = [];
const stdoutStream = new Writable({
write(chunk: Buffer, _enc, callback) {
stdoutChunks.push(chunk);
callback();
},
});
const stderrStream = new Writable({
write(chunk: Buffer, _enc, callback) {
stderrChunks.push(chunk);
callback();
},
});
await runRemoteCommand({
cmd,
hostname,
port,
proxyCommand,
stdin,
stdout: stdout === 'capture' ? stdoutStream : stdout,
stderr: stderr === 'capture' ? stderrStream : stderr,
username,
verbose,
});
return {
stdout: Buffer.concat(stdoutChunks),
stderr: Buffer.concat(stderrChunks),
};
}
/** Convenience wrapper for getRemoteCommandOutput */
export async function getLocalDeviceCmdStdout(
hostname: string,
export async function execBuffered(
deviceIp: string,
cmd: string,
stdout: 'capture' | 'ignore' | 'inherit' | NodeJS.WritableStream = 'capture',
): Promise<Buffer> {
const port = 'local';
return (
await getRemoteCommandOutput({
cmd,
hostname,
port,
stdout,
stderr: 'inherit',
username: await findBestUsernameForDevice(hostname, port),
})
).stdout;
enc?: string,
): Promise<string> {
const through = await import('through2');
const buffer: string[] = [];
await exec(
deviceIp,
cmd,
through(function (data, _enc, cb) {
buffer.push(data.toString(enc));
cb();
}),
);
return buffer.join('');
}
/**
* Run a trivial 'exit 0' command over ssh on the target hostname (typically the
* IP address of a local device) with the 'root' username, in order to determine
* whether root authentication suceeds. It should succeed with development
* variants of balenaOS and fail with production variants, unless a ssh key was
* added to the device's 'config.json' file.
* @return True if succesful, false on any errors.
*/
export const isRootUserGood = _.memoize(async (hostname: string, port) => {
try {
await runRemoteCommand({ cmd: 'exit 0', hostname, port, ...stdioIgnore });
} catch (e) {
return false;
}
return true;
});
/**
* Determine whether the given local device (hostname or IP address) should be
* accessed as the 'root' user or as a regular cloud user (balenaCloud or
* openBalena). Where possible, the root user is preferable because:
* - It allows ssh to be used in air-gapped scenarios (no internet access).
* Logging in as a regular user requires the device to fetch public keys from
* the cloud backend.
* - Root authentication is significantly faster for local devices (a fraction
* of a second versus 5+ seconds).
* - Non-root authentication requires balenaOS v2.44.0 or later, so not (yet)
* universally possible.
*/
export const findBestUsernameForDevice = _.memoize(
async (hostname: string, port): Promise<string> => {
let username: string | undefined;
if (await isRootUserGood(hostname, port)) {
username = 'root';
} else {
const { getCachedUsername } = await import('./bootstrap');
username = (await getCachedUsername())?.username;
}
if (!username) {
const { stripIndent } = await import('./lazy');
throw new ExpectedError(stripIndent`
SSH authentication failed for 'root@${hostname}'.
Please login with 'balena login' for alternative authentication.`);
}
return username;
},
);
/**
* Return a device's balenaOS release by executing 'cat /etc/os-release'
* over ssh to the given deviceIp address. The result is cached with
* lodash's memoize.
*/
export const getDeviceOsRelease = _.memoize(async (hostname: string) =>
(await getLocalDeviceCmdStdout(hostname, 'cat /etc/os-release')).toString(),
export const getDeviceOsRelease = _.memoize(async (deviceIp: string) =>
execBuffered(deviceIp, 'cat /etc/os-release'),
);
function sshErrorMessage(cmd: string, exitSignal?: string, exitCode?: number) {
// TODO: consolidate the various forms of executing ssh child processes
// in the CLI, like exec and spawn, starting with the files:
// lib/actions/ssh.ts
// lib/utils/ssh.ts
// lib/utils/device/ssh.ts
/**
* Obtain the full path for ssh using which, then spawn a child process.
* - If the child process returns error code 0, return the function normally
* (do not throw an error).
* - If the child process returns a non-zero error code, set process.exitCode
* to that error code, and throw ExpectedError with a warning message.
* - If the child process is terminated by a process signal, set
* process.exitCode = 1, and throw ExpectedError with a warning message.
*/
export async function spawnSshAndThrowOnError(
args: string[],
options?: import('child_process').SpawnOptions,
) {
const { whichSpawn } = await import('./which');
const [exitCode, exitSignal] = await whichSpawn(
'ssh',
args,
options,
true, // returnExitCodeOrSignal
);
if (exitCode || exitSignal) {
// ssh returns a wide range of exit codes, including return codes of
// interactive shells. For example, if the user types CTRL-C on an
// interactive shell and then `exit`, ssh returns error code 130.
// Another example, typing "exit 1" on an interactive shell causes ssh
// to return exit code 1. In these cases, print a short one-line warning
// message, and exits the CLI process with the same error code.
process.exitCode = exitCode;
throw new ExpectedError(sshErrorMessage(exitSignal, exitCode));
}
}
function sshErrorMessage(exitSignal?: string, exitCode?: number) {
const msg: string[] = [];
cmd = cmd ? `Remote command "${cmd}"` : 'Process';
if (exitSignal) {
msg.push(`SSH: ${cmd} terminated with signal "${exitSignal}"`);
msg.push(`Warning: ssh process was terminated with signal "${exitSignal}"`);
} else {
msg.push(`SSH: ${cmd} exited with non-zero status code "${exitCode}"`);
msg.push(`Warning: ssh process exited with non-zero code "${exitCode}"`);
switch (exitCode) {
case 255:
msg.push(`

View File

@ -1,5 +1,5 @@
/*
Copyright 2016-2022 Balena
Copyright 2016-2019 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -40,35 +40,14 @@ export function notify() {
}
}
const up = notifier.update;
const message = up && getNotifierMessage(up);
if (message) {
notifier.notify({ defer: false, message });
if (
up &&
(require('semver') as typeof import('semver')).lt(up.current, up.latest)
) {
notifier.notify({
defer: false,
message: `Update available ${up.current}${up.latest}\n
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md`,
});
}
}
export function getNotifierMessage(updateInfo: UpdateNotifier.UpdateInfo) {
const semver = require('semver') as typeof import('semver');
const message: string[] = [];
const [current, latest] = [updateInfo.current, updateInfo.latest];
if (semver.lt(current, latest)) {
message.push(
`Update available ${current}${latest}`,
'https://github.com/balena-io/balena-cli/blob/master/INSTALL.md',
);
const currentMajor = semver.major(current);
const latestMajor = semver.major(latest);
if (currentMajor !== latestMajor) {
message.push(
'',
`Check the v${latestMajor} release notes at:`,
getReleaseNotesUrl(latestMajor),
);
}
}
return message.join('\n');
}
function getReleaseNotesUrl(majorVersion: number) {
return `https://github.com/balena-io/balena-cli/wiki/CLI-v${majorVersion}-Release-Notes`;
}

View File

@ -21,3 +21,13 @@ import { version } from '../../package.json';
export function isVersionGTE(v: string): boolean {
return semver.gte(process.env.BALENA_CLI_VERSION_OVERRIDE || version, v);
}
let v14: boolean;
/** Feature switch for the next major version of the CLI */
export function isV14(): boolean {
if (v14 === undefined) {
v14 = isVersionGTE('14.0.0');
}
return v14;
}

View File

@ -95,3 +95,52 @@ export async function which(
}
return programPath;
}
/**
* Call which(programName) and spawn() with the given arguments.
*
* If returnExitCodeOrSignal is true, the returned promise will resolve to
* an array [code, signal] with the child process exit code number or exit
* signal string respectively (as provided by the spawn close event).
*
* If returnExitCodeOrSignal is false, the returned promise will reject with
* a custom error if the child process returns a non-zero exit code or a
* non-empty signal string (as reported by the spawn close event).
*
* In either case and if spawn itself emits an error event or fails synchronously,
* the returned promise will reject with a custom error that includes the error
* message of spawn's error.
*/
export async function whichSpawn(
programName: string,
args: string[],
options: import('child_process').SpawnOptions = { stdio: 'inherit' },
returnExitCodeOrSignal = false,
): Promise<[number | undefined, string | undefined]> {
const { spawn } = await import('child_process');
const program = await which(programName);
if (process.env.DEBUG) {
console.error(`[debug] [${program}, ${args.join(', ')}]`);
}
let error: Error | undefined;
let exitCode: number | undefined;
let exitSignal: string | undefined;
try {
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
spawn(program, args, options)
.on('error', reject)
.on('close', (code, signal) => resolve([code, signal]));
});
} catch (err) {
error = err;
}
if (error || (!returnExitCodeOrSignal && (exitCode || exitSignal))) {
const msg = [
`${programName} failed with exit code=${exitCode} signal=${exitSignal}:`,
`[${program}, ${args.join(', ')}]`,
...(error ? [`${error}`] : []),
];
throw new Error(msg.join('\n'));
}
return [exitCode, exitSignal];
}

1348
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "14.5.4",
"version": "13.1.13",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -29,7 +29,7 @@
"node_modules/balena-sdk/es2018/index.js",
"node_modules/balena-sync/build/**/*.js",
"node_modules/pinejs-client-request/node_modules/pinejs-client-core/es2018/index.js",
"node_modules/@balena/compose/dist/parse/schemas/*.json"
"node_modules/resin-compose-parse/build/schemas/*.json"
],
"assets": [
"build/auth/pages/*.ejs",
@ -90,11 +90,12 @@
"author": "Balena Inc. (https://balena.io/)",
"license": "Apache-2.0",
"engines": {
"node": ">=12 <16"
"node": ">=12.8.0 <13.0.0",
"npm": "<7.0.0"
},
"husky": {
"hooks": {
"pre-commit": "node automation/check-npm-version.js && ts-node automation/check-doc.ts"
"pre-commit": "node automation/check-npm-version.js && node automation/check-doc.js"
}
},
"oclif": {
@ -126,7 +127,7 @@
"@types/chai-as-promised": "^7.1.4",
"@types/cli-truncate": "^2.0.0",
"@types/common-tags": "^1.8.1",
"@types/dockerode": "^3.3.9",
"@types/dockerode": "^3.3.0",
"@types/ejs": "^3.1.0",
"@types/express": "^4.17.13",
"@types/fs-extra": "^9.0.13",
@ -154,7 +155,6 @@
"@types/request": "^2.48.7",
"@types/rewire": "^2.5.28",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.9",
"@types/shell-escape": "^0.2.0",
"@types/sinon": "^10.0.6",
"@types/split": "^1.0.0",
@ -183,17 +183,16 @@
"mocha": "^8.4.0",
"mock-require": "^3.0.3",
"nock": "^13.2.1",
"parse-link-header": "^2.0.0",
"parse-link-header": "^1.0.1",
"pkg": "^5.5.1",
"publish-release": "^1.6.1",
"rewire": "^5.0.0",
"simple-git": "^3.14.1",
"simple-git": "^2.48.0",
"sinon": "^11.1.2",
"ts-node": "^10.4.0",
"typescript": "^4.6.4"
"typescript": "^4.5.4"
},
"dependencies": {
"@balena/compose": "^2.1.1",
"@balena/dockerignore": "^1.0.2",
"@balena/es-version": "^1.0.1",
"@oclif/command": "^1.8.16",
@ -206,9 +205,10 @@
"balena-device-init": "^6.0.0",
"balena-errors": "^4.7.1",
"balena-image-fs": "^7.0.6",
"balena-image-manager": "^8.0.0",
"balena-preload": "^12.1.0",
"balena-sdk": "^16.28.0",
"balena-image-manager": "^7.1.1",
"balena-preload": "^12.0.0",
"balena-release": "^3.2.0",
"balena-sdk": "^16.9.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.7",
"balena-settings-storage": "^7.0.0",
@ -218,13 +218,14 @@
"chalk": "^3.0.0",
"chokidar": "^3.5.2",
"cli-truncate": "^2.1.0",
"cli-ux": "^5.5.1",
"color-hash": "^1.1.1",
"cli-ux": "^6.0.5",
"columnify": "^1.5.2",
"common-tags": "^1.7.2",
"denymount": "^2.3.0",
"docker-modem": "3.0.0",
"docker-progress": "^5.1.3",
"docker-progress": "^5.0.1",
"docker-qemu-transpose": "^1.1.1",
"dockerode": "^3.3.1",
"ejs": "^3.1.6",
"etcher-sdk": "^6.2.1",
@ -262,7 +263,9 @@
"request": "^2.88.2",
"resin-cli-form": "^2.0.2",
"resin-cli-visuals": "^1.8.0",
"resin-compose-parse": "^2.1.3",
"resin-doodles": "^0.2.0",
"resin-multibuild": "^4.12.2",
"resin-stream-logger": "^0.1.2",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
@ -284,6 +287,6 @@
"windosu": "^0.3.0"
},
"versionist": {
"publishedAt": "2022-11-10T18:31:19.865Z"
"publishedAt": "2022-02-10T11:50:34.458Z"
}
}

View File

@ -14,7 +14,7 @@ upstream:
url: 'https://github.com/balena-io-modules/balena-sync'
- repo: 'etcher-sdk'
url: 'https://github.com/balena-io-modules/etcher-sdk/'
- repo: 'balena-compose'
url: 'https://github.com/balena-io-modules/balena-compose'
- repo: 'resin-compose-parse'
url: 'https://github.com/balena-io-modules/resin-compose-parse'
- repo: 'docker-progress'
url: 'https://github.com/balena-io-modules/docker-progress'

View File

@ -6,6 +6,7 @@ const johnDoe = {
gitlab_id: 1325,
social_service_account: null,
hasPasswordSet: true,
needsPasswordReset: false,
public_key: false,
features: [],
id: 1344,
@ -20,6 +21,7 @@ const janeDoe = {
social_service_account: null,
has_disabled_newsletter: true,
hasPasswordSet: true,
needsPasswordReset: false,
public_key: false,
features: [],
intercomUserHash:

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import type { Request as ReleaseRequest } from '@balena/compose/dist/release';
import type { Request as ReleaseRequest } from 'balena-release';
import { expect } from 'chai';
import { promises as fs } from 'fs';
import * as _ from 'lodash';
@ -291,7 +291,7 @@ describe('balena deploy', function () {
statusCode: 500,
inspectRequest: (_uri, requestBody) => {
const imageBody = requestBody as Partial<
import('@balena/compose/dist/release/models').ImageModel
import('balena-release/build/models').ImageModel
>;
expect(imageBody.status).to.equal('success');
},
@ -300,7 +300,7 @@ describe('balena deploy', function () {
api.expectPatchRelease({
inspectRequest: (_uri, requestBody) => {
const releaseBody = requestBody as Partial<
import('@balena/compose/dist/release/models').ReleaseModel
import('balena-release/build/models').ReleaseModel
>;
expect(releaseBody.status).to.equal('failed');
},

View File

@ -21,6 +21,8 @@ import * as path from 'path';
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { isV14 } from '../../../lib/utils/version';
describe('balena device', function () {
let api: BalenaAPIMock;
@ -57,9 +59,16 @@ describe('balena device', function () {
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('org/test app');
if (isV14()) {
expect(lines).to.have.lengthOf(26);
expect(lines[0]).to.equal('sparkling-wood');
expect(lines[2].split(':')[0].trim()).to.equal('Id');
expect(lines[2].split(':')[1].trim()).to.equal('1747415');
} else {
expect(lines).to.have.lengthOf(25);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
}
});
it.skip('correctly handles devices with missing fields', async () => {
@ -79,14 +88,20 @@ describe('balena device', function () {
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('org/test app');
if (isV14()) {
expect(lines).to.have.lengthOf(15);
expect(lines[0]).to.equal('sparkling-wood');
expect(lines[7].split(':')[1].trim()).to.equal('org/test app');
} else {
expect(lines).to.have.lengthOf(14);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
}
});
it('correctly handles devices with missing application', async () => {
// 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.
it.skip('correctly handles devices with missing fleet', async () => {
// Devices with missing fleets will have fleet name set to `N/a`.
// e.g. When user has a device associated with fleet that user is no longer a collaborator of.
api.scope
.get(
/^\/v6\/device\?.+&\$expand=belongs_to__application\(\$select=app_name,slug\)/,
@ -103,8 +118,15 @@ describe('balena device', function () {
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');
if (isV14()) {
expect(lines).to.have.lengthOf(26);
expect(lines[0]).to.equal('sparkling-wood');
expect(lines[9].split(':')[0].trim()).to.equal('Fleet');
expect(lines[9].split(':')[1].trim()).to.equal('N/a');
} else {
expect(lines).to.have.lengthOf(25);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('N/a');
}
});
});

View File

@ -21,6 +21,8 @@ import * as path from 'path';
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { isV14 } from '../../../lib/utils/version';
describe('balena devices', function () {
let api: BalenaAPIMock;
@ -48,15 +50,24 @@ describe('balena devices', function () {
const lines = cleanOutput(out);
expect(lines[0].replace(/ +/g, ' ')).to.equal(
'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('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;
if (isV14()) {
expect(lines[0].replace(/ +/g, ' ')).to.equal(
' Id Uuid Device name Device type Fleet Status Is online Supervisor version Os version Dashboard url ',
);
expect(lines).to.have.lengthOf.at.least(3);
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;
} else {
expect(lines[0].replace(/ +/g, ' ')).to.equal(
'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('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;
}
});
});

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2019-2021 Balena Ltd.
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ import { expect } from 'chai';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { isV14 } from '../../../lib/utils/version';
describe('balena devices supported', function () {
let api: BalenaAPIMock;
@ -44,16 +45,20 @@ describe('balena devices supported', function () {
it('should list currently supported devices, with correct filtering', async () => {
api.expectGetDeviceTypes();
api.expectGetConfigDeviceTypes();
const { out, err } = await runCommand('devices supported');
const lines = cleanOutput(out, true);
expect(lines[0]).to.equal('SLUG ALIASES ARCH NAME');
expect(lines[0]).to.equal(
isV14() ? ' Slug Aliases Arch Name ' : '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(
'odroid-xu4 odroid-u3+, odroid-ux3 armv7hf ODROID-XU4',
'odroid-xu4 odroid-ux3, odroid-u3+ armv7hf ODROID-XU4',
);
expect(err).to.eql([]);
});

View File

@ -19,7 +19,9 @@ import { expect } from 'chai';
import { stripIndent } from '../../../build/utils/lazy';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { runCommand } from '../../helpers';
import { runCommand, removeFirstNLines, trimLines } from '../../helpers';
import { isV14 } from '../../../lib/utils/version';
describe('balena envs', function () {
const appName = 'test';
@ -48,15 +50,30 @@ describe('balena envs', function () {
const { out, err } = await runCommand(`envs -f ${appName}`);
expect(out.join('')).to.equal(
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
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(output).to.equal(expected);
} else {
expect(out.join('')).to.equal(
stripIndent`
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('');
});
@ -66,12 +83,24 @@ describe('balena envs', function () {
const { out, err } = await runCommand(`envs -f ${appName} --config`);
expect(out.join('')).to.equal(
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false gh_user/testApp
` + '\n';
expect(output).to.equal(expected);
} else {
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE FLEET
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false gh_user/testApp
` + '\n',
);
);
}
expect(err.join('')).to.equal('');
});
@ -82,15 +111,19 @@ describe('balena envs', function () {
const { out, err } = await runCommand(`envs -cjf ${appName}`);
expect(JSON.parse(out.join(''))).to.deep.equal([
{
fleet: 'gh_user/testApp',
id: 120300,
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
value: 'false',
},
]);
expect(err.join('')).to.equal('');
if (isV14()) {
// TODO: Add tests once oclif json issue resolved.
} else {
expect(JSON.parse(out.join(''))).to.deep.equal([
{
fleet: 'gh_user/testApp',
id: 120300,
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
value: 'false',
},
]);
expect(err.join('')).to.equal('');
}
});
it('should successfully list service variables for a test fleet (-s flag)', async () => {
@ -104,14 +137,28 @@ describe('balena envs', function () {
`envs -f ${appName} -s ${serviceName}`,
);
expect(out.join('')).to.equal(
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120111 svar2 svar2-value gh_user/testApp service2
120101 var1 var1-val gh_user/testApp *
120102 var2 22 gh_user/testApp *
` + '\n';
expect(output).to.equal(expected);
} else {
expect(out.join('')).to.equal(
stripIndent`
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('');
});
@ -126,14 +173,28 @@ describe('balena envs', function () {
`envs -f ${appName} -s ${serviceName}`,
);
expect(out.join('')).to.equal(
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120110 svar1 svar1-value gh_user/testApp ${serviceName}
120101 var1 var1-val gh_user/testApp *
120102 var2 22 gh_user/testApp *
` + '\n';
expect(output).to.equal(expected);
} else {
expect(out.join('')).to.equal(
stripIndent`
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('');
});
@ -148,8 +209,27 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid}`);
let { out } = result;
let expected =
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
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';
expect(output).to.equal(expected);
} else {
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120110 svar1 svar1-value org/test * service1
120111 svar2 svar2-value org/test * service2
@ -161,10 +241,10 @@ describe('balena envs', function () {
120204 var4 44 org/test ${uuid} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
}
});
it('should successfully list env variables for a test device (JSON output)', async () => {
@ -176,7 +256,11 @@ describe('balena envs', function () {
api.expectGetDeviceServiceVars();
const { out, err } = await runCommand(`envs -jd ${shortUUID}`);
const expected = `[
if (isV14()) {
// TODO: Add tests once oclif json issue resolved.
} else {
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" },
@ -187,7 +271,9 @@ describe('balena envs', function () {
{ "id": 120204, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
]`;
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
}
expect(err.join('')).to.equal('');
});
@ -199,17 +285,30 @@ describe('balena envs', function () {
const result = await runCommand(`envs -d ${shortUUID} --config`);
let { out } = result;
let expected =
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false org/test *
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 org/test ${shortUUID}
` + '\n';
expect(output).to.equal(expected);
} else {
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false org/test *
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 org/test ${shortUUID}
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(out.join('')).to.equal(expected);
}
});
it('should successfully list service variables for a test device (-s flag)', async () => {
@ -225,8 +324,25 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
let { out } = result;
let expected =
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
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';
expect(output).to.equal(expected);
} else {
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120111 svar2 svar2-value org/test * service2
120121 svar4 svar4-value org/test ${uuid} service2
@ -236,10 +352,11 @@ describe('balena envs', function () {
120204 var4 44 org/test ${uuid} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(out.join('')).to.equal(expected);
}
});
it('should successfully list env and service variables for a test device (unknown fleet)', async () => {
@ -250,8 +367,23 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid}`);
let { out } = result;
let expected =
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
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';
expect(output).to.equal(expected);
} else {
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120120 svar3 svar3-value N/A ${uuid} service1
120121 svar4 svar4-value N/A ${uuid} service2
@ -259,10 +391,11 @@ describe('balena envs', function () {
120204 var4 44 N/A ${uuid} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(out.join('')).to.equal(expected);
}
});
it('should successfully list env and service vars for a test device (-s flags)', async () => {
@ -278,8 +411,24 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
let { out } = result;
let expected =
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
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';
expect(output).to.equal(expected);
} else {
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120110 svar1 svar1-value org/test * ${serviceName}
120120 svar3 svar3-value org/test ${uuid} ${serviceName}
@ -289,10 +438,11 @@ describe('balena envs', function () {
120204 var4 44 org/test ${uuid} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(out.join('')).to.equal(expected);
}
});
it('should successfully list env and service vars for a test device (-js flags)', async () => {
@ -308,7 +458,11 @@ describe('balena envs', function () {
const { out, err } = await runCommand(
`envs -d ${shortUUID} -js ${serviceName}`,
);
const expected = `[
if (isV14()) {
// TODO: Add tests once oclif json issue resolved.
} else {
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}" },
@ -317,7 +471,8 @@ describe('balena envs', function () {
{ "id": 120204, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
]`;
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(err.join('')).to.equal('');
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(err.join('')).to.equal('');
}
});
});

View File

@ -59,7 +59,6 @@ if (process.platform !== 'win32') {
'--config-network ethernet',
'--initial-device-name testDeviceName',
'--provisioning-key-name testKey',
'--provisioning-key-expiry-date 2050-12-12',
];
const { err } = await runCommand(command.join(' '));

View File

@ -20,6 +20,8 @@ import { expect } from 'chai';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../helpers';
import { isV14 } from '../../lib/utils/version';
describe('balena release', function () {
let api: BalenaAPIMock;
@ -34,7 +36,7 @@ describe('balena release', function () {
api.done();
});
it('should show release details', async () => {
it.skip('should show release details', async () => {
api.expectGetRelease();
const { out } = await runCommand('release 27fda508c');
const lines = cleanOutput(out);
@ -44,7 +46,7 @@ describe('balena release', function () {
expect(lines[1]).to.contain(' 90247b54de4fa7a0a3cbc85e73c68039');
});
it('should return release composition', async () => {
it.skip('should return release composition', async () => {
api.expectGetRelease();
const { out } = await runCommand('release 27fda508c --composition');
const lines = cleanOutput(out);
@ -61,8 +63,14 @@ describe('balena release', function () {
api.expectGetApplication();
const { out } = await runCommand('releases someapp');
const lines = cleanOutput(out);
expect(lines.length).to.be.equal(2);
expect(lines[1]).to.contain('142334');
expect(lines[1]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
if (isV14()) {
expect(lines.length).to.be.equal(3);
expect(lines[2]).to.contain('142334');
expect(lines[2]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
} else {
expect(lines.length).to.be.equal(2);
expect(lines[1]).to.contain('142334');
expect(lines[1]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
}
});
});

Some files were not shown because too many files have changed in this diff Show More