mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-04-24 04:55:44 +00:00
Compare commits
No commits in common. "master" and "v14.4.1" have entirely different histories.
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -4,10 +4,6 @@
|
|||||||
*.* -eol
|
*.* -eol
|
||||||
|
|
||||||
*.sh text eol=lf
|
*.sh text eol=lf
|
||||||
.dockerignore eol=lf
|
|
||||||
Dockerfile eol=lf
|
|
||||||
Dockerfile.* eol=lf
|
|
||||||
* text=auto eol=lf
|
|
||||||
|
|
||||||
# lf for the docs as it's auto-generated and will otherwise trigger an uncommited error on windows
|
# lf for the docs as it's auto-generated and will otherwise trigger an uncommited error on windows
|
||||||
docs/balena-cli.md text eol=lf
|
docs/balena-cli.md text eol=lf
|
||||||
|
143
.github/actions/publish/action.yml
vendored
143
.github/actions/publish/action.yml
vendored
@ -1,143 +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
|
|
||||||
variables:
|
|
||||||
description: 'JSON stringified object containing all the variables from the calling workflow'
|
|
||||||
required: true
|
|
||||||
|
|
||||||
# --- custom environment
|
|
||||||
XCODE_APP_LOADER_EMAIL:
|
|
||||||
type: string
|
|
||||||
default: 'accounts+apple@balena.io'
|
|
||||||
NODE_VERSION:
|
|
||||||
type: string
|
|
||||||
default: '20.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@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
|
|
||||||
with:
|
|
||||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
|
||||||
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@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
|
||||||
with:
|
|
||||||
node-version: ${{ inputs.NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- name: Set up Python 3.11
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
|
|
||||||
with:
|
|
||||||
python-version: "3.11"
|
|
||||||
|
|
||||||
- name: Install additional tools
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
choco install yq
|
|
||||||
|
|
||||||
- name: Install additional tools
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
shell: bash
|
|
||||||
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@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2
|
|
||||||
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:SM_CLIENT_CERT_FILE_B64
|
|
||||||
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/Certificate_pkcs12.p12
|
|
||||||
Remove-Item -path ${{ runner.temp }} -include certificate.base64
|
|
||||||
env:
|
|
||||||
SM_CLIENT_CERT_FILE_B64: ${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_FILE_B64 }}
|
|
||||||
|
|
||||||
# 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
|
|
||||||
shell: bash
|
|
||||||
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
|
|
||||||
SM_HOST=${{ fromJSON(inputs.secrets).SM_HOST }}
|
|
||||||
SM_API_KEY=${{ fromJSON(inputs.secrets).SM_API_KEY }}
|
|
||||||
SM_CLIENT_CERT_FILE='${{ runner.temp }}\Certificate_pkcs12.p12'
|
|
||||||
SM_CLIENT_CERT_PASSWORD=${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_PASSWORD }}
|
|
||||||
SM_CODE_SIGNING_CERT_SHA1_HASH=${{ fromJSON(inputs.secrets).SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
|
||||||
|
|
||||||
curl --silent --retry 3 --fail https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download \
|
|
||||||
-H "x-api-key:$SM_API_KEY" \
|
|
||||||
-o smtools-windows-x64.msi
|
|
||||||
msiexec -i smtools-windows-x64.msi -qn
|
|
||||||
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
|
|
||||||
smksp_registrar.exe list
|
|
||||||
smctl.exe keypair ls
|
|
||||||
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
|
|
||||||
smksp_cert_sync.exe
|
|
||||||
|
|
||||||
# (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
|
|
||||||
|
|
||||||
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://docs.digicert.com/es/software-trust-manager/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html
|
|
||||||
TIMESTAMP_SERVER: http://timestamp.digicert.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 }}
|
|
||||||
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
|
|
||||||
path: dist
|
|
||||||
retention-days: 1
|
|
||||||
if-no-files-found: error
|
|
65
.github/actions/test/action.yml
vendored
65
.github/actions/test/action.yml
vendored
@ -1,65 +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
|
|
||||||
variables:
|
|
||||||
description: "JSON stringified object containing all the variables from the calling workflow"
|
|
||||||
required: true
|
|
||||||
|
|
||||||
# --- custom environment
|
|
||||||
NODE_VERSION:
|
|
||||||
type: string
|
|
||||||
default: '20.x'
|
|
||||||
VERBOSE:
|
|
||||||
type: string
|
|
||||||
default: "true"
|
|
||||||
|
|
||||||
runs:
|
|
||||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
|
||||||
with:
|
|
||||||
node-version: ${{ inputs.NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- name: Set up Python 3.11
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
|
|
||||||
with:
|
|
||||||
python-version: "3.11"
|
|
||||||
|
|
||||||
- name: Test release
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -ea
|
|
||||||
|
|
||||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
|
||||||
|
|
||||||
if [[ -e package-lock.json ]] || [[ -e npm-shrinkwrap.json ]]; then
|
|
||||||
npm ci
|
|
||||||
else
|
|
||||||
npm i
|
|
||||||
fi
|
|
||||||
|
|
||||||
npm run build
|
|
||||||
npm run test:core
|
|
||||||
|
|
||||||
- name: Compress custom source
|
|
||||||
shell: pwsh
|
|
||||||
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
|
|
||||||
|
|
||||||
- name: Upload custom artifact
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
|
||||||
path: ${{ runner.temp }}/custom.tgz
|
|
||||||
retention-days: 1
|
|
4
.github/renovate.json
vendored
4
.github/renovate.json
vendored
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["github>balena-io/renovate-config"],
|
|
||||||
"postUpdateOptions": ["npmDedupe"]
|
|
||||||
}
|
|
45
.github/workflows/flowzone.yml
vendored
45
.github/workflows/flowzone.yml
vendored
@ -1,45 +0,0 @@
|
|||||||
name: Flowzone
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, closed]
|
|
||||||
branches: [main, master]
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, synchronize, closed]
|
|
||||||
branches: [main, master]
|
|
||||||
jobs:
|
|
||||||
flowzone:
|
|
||||||
name: Flowzone
|
|
||||||
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
|
|
||||||
# prevent duplicate workflow executions for pull_request and pull_request_target
|
|
||||||
if: |
|
|
||||||
(
|
|
||||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
||||||
github.event_name == 'pull_request'
|
|
||||||
) || (
|
|
||||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
|
||||||
github.event_name == 'pull_request_target'
|
|
||||||
)
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
custom_test_matrix: >
|
|
||||||
{
|
|
||||||
"os": [
|
|
||||||
["self-hosted", "X64"],
|
|
||||||
["self-hosted", "ARM64"],
|
|
||||||
["macos-13"],
|
|
||||||
["windows-2019"],
|
|
||||||
["macos-latest-xlarge"]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
custom_publish_matrix: >
|
|
||||||
{
|
|
||||||
"os": [
|
|
||||||
["self-hosted", "X64"],
|
|
||||||
["self-hosted", "ARM64"],
|
|
||||||
["macos-13"],
|
|
||||||
["windows-2019"],
|
|
||||||
["macos-latest-xlarge"]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
github_prerelease: false
|
|
||||||
restrict_custom_actions: false
|
|
@ -1 +0,0 @@
|
|||||||
node automation/check-npm-version.js && ts-node automation/check-doc.ts
|
|
20
.resinci.yml
Normal file
20
.resinci.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
npm:
|
||||||
|
platforms:
|
||||||
|
- name: linux
|
||||||
|
os: ubuntu
|
||||||
|
architecture: x86_64
|
||||||
|
node_versions:
|
||||||
|
- "12"
|
||||||
|
- "14"
|
||||||
|
##
|
||||||
|
## Temporarily skip Alpine tests until the following issues are resolved:
|
||||||
|
## * https://github.com/concourse/concourse/issues/7905
|
||||||
|
## * https://github.com/product-os/balena-concourse/issues/631
|
||||||
|
##
|
||||||
|
# - name: linux
|
||||||
|
# os: alpine
|
||||||
|
# architecture: x86_64
|
||||||
|
# node_versions:
|
||||||
|
# - "12"
|
||||||
|
# - "14"
|
File diff suppressed because it is too large
Load Diff
3451
CHANGELOG.md
3451
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -115,27 +115,14 @@ The content sources for the auto generation of `docs/balena-cli.md` are:
|
|||||||
* [Selected
|
* [Selected
|
||||||
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
|
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
|
||||||
of the README file.
|
of the README file.
|
||||||
* The CLI's command documentation in source code (`src/commands/` folder), for example:
|
* The CLI's command documentation in source code (`lib/commands/` folder), for example:
|
||||||
* `src/commands/push.ts`
|
* `lib/commands/push.ts`
|
||||||
* `src/commands/env/add.ts`
|
* `lib/commands/env/add.ts`
|
||||||
|
|
||||||
The README file is manually edited, but subsections are automatically extracted for inclusion in
|
The README file is manually edited, but subsections are automatically extracted for inclusion in
|
||||||
`docs/balena-cli.md` by the `getCapitanoDoc()` function in
|
`docs/balena-cli.md` by the `getCapitanoDoc()` function in
|
||||||
[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
|
[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
|
||||||
|
|
||||||
**IMPORTANT**
|
|
||||||
|
|
||||||
The file [`capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts) lists
|
|
||||||
commands to generate documentation from. At the moment, it's manually updated and maintained alphabetically.
|
|
||||||
|
|
||||||
To add a new command to be documented,
|
|
||||||
|
|
||||||
1. Find the resource which it is part of or create a new one.
|
|
||||||
2. List the location of the build file
|
|
||||||
3. Make sure to add your files in alphabetical order
|
|
||||||
|
|
||||||
Once added, run the command `npm run build` to generate the documentation
|
|
||||||
|
|
||||||
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||||
|
|
||||||
## Patches folder
|
## Patches folder
|
||||||
@ -223,7 +210,7 @@ command, or by manually editing or copying files to the `node_modules` folder.
|
|||||||
|
|
||||||
Unexpected behavior may then be observed because of the CLI's use of the
|
Unexpected behavior may then be observed because of the CLI's use of the
|
||||||
[fast-boot2](https://www.npmjs.com/package/fast-boot2) package that caches module resolution.
|
[fast-boot2](https://www.npmjs.com/package/fast-boot2) package that caches module resolution.
|
||||||
`fast-boot2` is configured in `src/fast-boot.ts` to automatically invalidate the cache if
|
`fast-boot2` is configured in `lib/fast-boot.ts` to automatically invalidate the cache if
|
||||||
changes are made to the `package.json` or `npm-shrinkwrap.json` files, but the cache won't
|
changes are made to the `package.json` or `npm-shrinkwrap.json` files, but the cache won't
|
||||||
be automatically invalidated if `npm link` is used or if manual modifications are made to the
|
be automatically invalidated if `npm link` is used or if manual modifications are made to the
|
||||||
`node_modules` folder. In this situation:
|
`node_modules` folder. In this situation:
|
||||||
|
@ -40,7 +40,7 @@ By default, the CLI is installed to the following folders:
|
|||||||
OS | Folders
|
OS | Folders
|
||||||
--- | ---
|
--- | ---
|
||||||
Windows: | `C:\Program Files\balena-cli\`
|
Windows: | `C:\Program Files\balena-cli\`
|
||||||
macOS: | `/usr/local/src/balena-cli/` <br> `/usr/local/bin/balena`
|
macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
|
||||||
|
|
||||||
## Standalone Zip Package
|
## Standalone Zip Package
|
||||||
|
|
||||||
@ -78,8 +78,8 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
|
|||||||
The npm installation involves building native (platform-specific) binary modules, which require
|
The npm installation involves building native (platform-specific) binary modules, which require
|
||||||
some development tools to be installed first, as follows.
|
some development tools to be installed first, as follows.
|
||||||
|
|
||||||
> **The balena CLI currently requires Node.js version ^20.6.0**
|
> **The balena CLI currently requires Node.js version 12 (min 12.8.0).**
|
||||||
> **Versions 21 and later are not yet fully supported.**
|
> **Versions 13 and later are not yet fully supported.**
|
||||||
|
|
||||||
### Install development tools
|
### Install development tools
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ some development tools to be installed first, as follows.
|
|||||||
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
|
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
|
||||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||||
$ . ~/.bashrc
|
$ . ~/.bashrc
|
||||||
$ nvm install 20
|
$ nvm install 12
|
||||||
```
|
```
|
||||||
|
|
||||||
The `curl` command line above uses
|
The `curl` command line above uses
|
||||||
@ -106,15 +106,15 @@ recommended.
|
|||||||
```sh
|
```sh
|
||||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||||
$ . ~/.bashrc
|
$ . ~/.bashrc
|
||||||
$ nvm install 20
|
$ nvm install 12
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **Windows** (not WSL)
|
#### **Windows** (not WSL)
|
||||||
|
|
||||||
Install:
|
Install:
|
||||||
|
|
||||||
|
* Node.js v12 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
||||||
* If you'd like the ability to switch between Node.js versions, install
|
* If you'd like the ability to switch between Node.js versions, install
|
||||||
- Node.js v20 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
|
||||||
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
|
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
|
||||||
instead.
|
instead.
|
||||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
||||||
@ -145,7 +145,7 @@ container) in order to allow npm scripts like `postinstall` to be executed.
|
|||||||
|
|
||||||
## Additional Dependencies
|
## Additional Dependencies
|
||||||
|
|
||||||
The `balena device ssh`, `device detect`, `build`, `deploy` and `preload` commands may require
|
The `balena ssh`, `scan`, `build`, `deploy` and `preload` commands may require
|
||||||
additional software to be installed. Check the Additional Dependencies sections for each operating
|
additional software to be installed. Check the Additional Dependencies sections for each operating
|
||||||
system:
|
system:
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ as described above.
|
|||||||
|
|
||||||
## sudo configuration
|
## sudo configuration
|
||||||
|
|
||||||
A few CLI commands require execution through sudo, e.g. `sudo balena device detect`.
|
A few CLI commands require execution through sudo, e.g. `sudo balena scan`.
|
||||||
If your Linux distribution has an `/etc/sudoers` file that defines a `secure_path`
|
If your Linux distribution has an `/etc/sudoers` file that defines a `secure_path`
|
||||||
setting, run `sudo visudo` to edit it and add the balena CLI's installation folder to
|
setting, run `sudo visudo` to edit it and add the balena CLI's installation folder to
|
||||||
the ***pre-existing*** `secure_path` setting, for example:
|
the ***pre-existing*** `secure_path` setting, for example:
|
||||||
@ -61,19 +61,19 @@ instructions](https://docs.docker.com/install/overview/) to install Docker on th
|
|||||||
workstation as the balena CLI. The [advanced installation
|
workstation as the balena CLI. The [advanced installation
|
||||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||||
|
|
||||||
### balena device ssh
|
### balena ssh
|
||||||
|
|
||||||
The `balena device ssh` command requires the `ssh` command-line tool to be available. Most Linux
|
The `balena ssh` command requires the `ssh` command-line tool to be available. Most Linux
|
||||||
distributions will already have it installed. Otherwise, `sudo apt-get install openssh-client`
|
distributions will already have it installed. Otherwise, `sudo apt-get install openssh-client`
|
||||||
should do the trick on Debian or Ubuntu.
|
should do the trick on Debian or Ubuntu.
|
||||||
|
|
||||||
The `balena device ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||||
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||||
|
|
||||||
### balena device detect
|
### balena scan
|
||||||
|
|
||||||
The `balena device detect` command requires a multicast DNS (mDNS) service like
|
The `balena scan` command requires a multicast DNS (mDNS) service like
|
||||||
[Avahi](https://en.wikipedia.org/wiki/Avahi_(software)), which is installed by default on most
|
[Avahi](https://en.wikipedia.org/wiki/Avahi_(software)), which is installed by default on most
|
||||||
desktop Linux distributions. Otherwise, on Debian or Ubuntu, the installation command would be
|
desktop Linux distributions. Otherwise, on Debian or Ubuntu, the installation command would be
|
||||||
`sudo apt-get install avahi-daemon`.
|
`sudo apt-get install avahi-daemon`.
|
||||||
|
@ -19,7 +19,7 @@ Selected operating system: **macOS**
|
|||||||
- On the terminal prompt, type `balena version` and hit Enter. It should display
|
- On the terminal prompt, type `balena version` and hit Enter. It should display
|
||||||
the version of the balena CLI that you have installed.
|
the version of the balena CLI that you have installed.
|
||||||
|
|
||||||
No further steps are required to run most CLI commands. The `balena device ssh`, `build`, `deploy`
|
No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy`
|
||||||
and `preload` commands may require additional software to be installed, as described
|
and `preload` commands may require additional software to be installed, as described
|
||||||
in the next section.
|
in the next section.
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ To update the balena CLI, repeat the steps above for the new version.
|
|||||||
To uninstall it, run the following command on a terminal prompt:
|
To uninstall it, run the following command on a terminal prompt:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
sudo /usr/local/src/balena-cli/bin/uninstall
|
sudo /usr/local/lib/balena-cli/bin/uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
## Additional Dependencies
|
## Additional Dependencies
|
||||||
@ -41,9 +41,9 @@ instructions](https://docs.docker.com/install/overview/) to install Docker on th
|
|||||||
workstation as the balena CLI. The [advanced installation
|
workstation as the balena CLI. The [advanced installation
|
||||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||||
|
|
||||||
### balena device ssh
|
### balena ssh
|
||||||
|
|
||||||
The `balena device ssh` command requires the `ssh` command-line tool to be available. To check whether
|
The `balena ssh` command requires the `ssh` command-line tool to be available. To check whether
|
||||||
it is already installed, run `ssh` on a Terminal window. If it is not yet installed, the options
|
it is already installed, run `ssh` on a Terminal window. If it is not yet installed, the options
|
||||||
include:
|
include:
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ include:
|
|||||||
Components → Command Line Tools → Install.
|
Components → Command Line Tools → Install.
|
||||||
* Or, install [Homebrew](https://brew.sh/), then `brew install openssh`
|
* Or, install [Homebrew](https://brew.sh/), then `brew install openssh`
|
||||||
|
|
||||||
The `balena device ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||||
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ Selected operating system: **Windows**
|
|||||||
- On the command prompt, type `balena version` and hit Enter. It should display
|
- On the command prompt, type `balena version` and hit Enter. It should display
|
||||||
the version of the balena CLI that you have installed.
|
the version of the balena CLI that you have installed.
|
||||||
|
|
||||||
No further steps are required to run most CLI commands. The `balena device ssh`, `device detect`, `build`,
|
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
|
||||||
`deploy` and `preload` commands may require additional software to be installed, as
|
`deploy` and `preload` commands may require additional software to be installed, as
|
||||||
described below.
|
described below.
|
||||||
|
|
||||||
@ -34,9 +34,9 @@ instructions](https://docs.docker.com/install/overview/) to install Docker on th
|
|||||||
workstation as the balena CLI. The [advanced installation
|
workstation as the balena CLI. The [advanced installation
|
||||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||||
|
|
||||||
### balena device ssh
|
### balena ssh
|
||||||
|
|
||||||
The `balena device ssh` command requires the `ssh` command-line tool to be available. Microsoft started
|
The `balena ssh` command requires the `ssh` command-line tool to be available. Microsoft started
|
||||||
distributing an SSH client with Windows 10, which is automatically installed through Windows
|
distributing an SSH client with Windows 10, which is automatically installed through Windows
|
||||||
Update. To check whether it is installed, run `ssh` on a Windows Command Prompt or PowerShell. It
|
Update. To check whether it is installed, run `ssh` on a Windows Command Prompt or PowerShell. It
|
||||||
can also be [manually
|
can also be [manually
|
||||||
@ -44,13 +44,13 @@ installed](https://docs.microsoft.com/en-us/windows-server/administration/openss
|
|||||||
if needed. For older versions of Windows, there are several ssh/OpenSSH clients provided by 3rd
|
if needed. For older versions of Windows, there are several ssh/OpenSSH clients provided by 3rd
|
||||||
parties.
|
parties.
|
||||||
|
|
||||||
The `balena device ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||||
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||||
|
|
||||||
### balena device detect
|
### balena scan
|
||||||
|
|
||||||
The `balena device detect` command requires a multicast DNS (mDNS) service like Apple's Bonjour.
|
The `balena scan` command requires a multicast DNS (mDNS) service like Apple's Bonjour.
|
||||||
Many Windows machines will already have this service installed, as it is bundled in popular
|
Many Windows machines will already have this service installed, as it is bundled in popular
|
||||||
applications such as Skype (Wikipedia lists [several others](https://en.wikipedia.org/wiki/Bonjour_(software))).
|
applications such as Skype (Wikipedia lists [several others](https://en.wikipedia.org/wiki/Bonjour_(software))).
|
||||||
Otherwise, Bonjour for Windows can be downloaded and installed from: https://support.apple.com/kb/DL999
|
Otherwise, Bonjour for Windows can be downloaded and installed from: https://support.apple.com/kb/DL999
|
||||||
|
@ -88,9 +88,9 @@ HTTP(S) proxies can be configured through any of the following methods, in prece
|
|||||||
* The `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables, in the same URL format as
|
* The `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables, in the same URL format as
|
||||||
`BALENARC_PROXY`.
|
`BALENARC_PROXY`.
|
||||||
|
|
||||||
### Proxy setup for balena device ssh
|
### Proxy setup for balena ssh
|
||||||
|
|
||||||
In order to work behind a proxy server, the `balena device ssh` command requires the
|
In order to work behind a proxy server, the `balena ssh` command requires the
|
||||||
[`proxytunnel`](http://proxytunnel.sourceforge.net/) package (command-line tool) to be installed.
|
[`proxytunnel`](http://proxytunnel.sourceforge.net/) package (command-line tool) to be installed.
|
||||||
`proxytunnel` is available for Linux distributions like Ubuntu/Debian (`apt install proxytunnel`),
|
`proxytunnel` is available for Linux distributions like Ubuntu/Debian (`apt install proxytunnel`),
|
||||||
and for macOS through [Homebrew](https://brew.sh/). Windows support is limited to the [Windows
|
and for macOS through [Homebrew](https://brew.sh/). Windows support is limited to the [Windows
|
||||||
@ -110,7 +110,7 @@ The `BALENARC_NO_PROXY` variable may be used to exclude specified destinations f
|
|||||||
> * This feature requires CLI version 11.30.8 or later. In the case of the npm [installation
|
> * This feature requires CLI version 11.30.8 or later. In the case of the npm [installation
|
||||||
> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires
|
> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires
|
||||||
> Node.js version 10.16.0 or later.
|
> Node.js version 10.16.0 or later.
|
||||||
> * To exclude a `balena device ssh` target from proxying (IP address or `.local` hostname), the
|
> * To exclude a `balena ssh` target from proxying (IP address or `.local` hostname), the
|
||||||
> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable.
|
> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable.
|
||||||
|
|
||||||
By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4
|
By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4
|
||||||
|
@ -31,7 +31,7 @@ command again.
|
|||||||
|
|
||||||
Check whether the SD card is locked (a physical switch on the side of the card).
|
Check whether the SD card is locked (a physical switch on the side of the card).
|
||||||
|
|
||||||
## I get `connect ETIMEDOUT` with `balena device tunnel`
|
## I get `connect ETIMEDOUT` with `balena tunnel`
|
||||||
|
|
||||||
Please update the CLI to the latest version. This issue was fixed in v12.38.5.
|
Please update the CLI to the latest version. This issue was fixed in v12.38.5.
|
||||||
For more details, see: https://github.com/balena-io/balena-cli/issues/2172
|
For more details, see: https://github.com/balena-io/balena-cli/issues/2172
|
||||||
@ -79,10 +79,10 @@ Try resetting the ownership by running:
|
|||||||
$ sudo chown -R <user> $HOME/.balena
|
$ sudo chown -R <user> $HOME/.balena
|
||||||
```
|
```
|
||||||
|
|
||||||
## Broken line wrapping / cursor behavior with `balena device ssh`
|
## Broken line wrapping / cursor behavior with `balena ssh`
|
||||||
|
|
||||||
Users sometimes come across broken line wrapping or cursor behavior in text terminals, for example
|
Users sometimes come across broken line wrapping or cursor behavior in text terminals, for example
|
||||||
when long command lines are typed in a `balena device ssh` session, or when using text editors like `vim`
|
when long command lines are typed in a `balena ssh` session, or when using text editors like `vim`
|
||||||
or `nano`. This is not something specific to the balena CLI, being also a commonly reported issue
|
or `nano`. This is not something specific to the balena CLI, being also a commonly reported issue
|
||||||
with standard remote terminal tools like `ssh` or `telnet`. It is often a remote shell
|
with standard remote terminal tools like `ssh` or `telnet`. It is often a remote shell
|
||||||
configuration issue (files like `/etc/profile`, `~/.bash_profile`, `~/.bash_login`, `~/.profile`
|
configuration issue (files like `/etc/profile`, `~/.bash_profile`, `~/.bash_login`, `~/.profile`
|
||||||
|
@ -15,20 +15,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { JsonVersions } from '../src/commands/version/index';
|
import type { JsonVersions } from '../lib/commands/version';
|
||||||
|
|
||||||
import { run as oclifRun } from '@oclif/core';
|
import { run as oclifRun } from 'oclif';
|
||||||
import * as archiver from 'archiver';
|
import * as archiver from 'archiver';
|
||||||
import { exec, execFile } from 'child_process';
|
import * as Bluebird from 'bluebird';
|
||||||
|
import { execFile } from 'child_process';
|
||||||
import * as filehound from 'filehound';
|
import * as filehound from 'filehound';
|
||||||
import type { Stats } from 'fs';
|
import { Stats } from 'fs';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import * as klaw from 'klaw';
|
import * as klaw from 'klaw';
|
||||||
|
import * as _ from 'lodash';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as rimraf from 'rimraf';
|
import * as rimraf from 'rimraf';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { notarize } from '@electron/notarize';
|
|
||||||
|
|
||||||
import { stripIndent } from '../build/utils/lazy';
|
import { stripIndent } from '../build/utils/lazy';
|
||||||
import {
|
import {
|
||||||
@ -40,12 +41,12 @@ import {
|
|||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
const execAsync = promisify(exec);
|
|
||||||
const rimrafAsync = promisify(rimraf);
|
|
||||||
|
|
||||||
export const packageJSON = loadPackageJson();
|
export const packageJSON = loadPackageJson();
|
||||||
export const version = 'v' + packageJSON.version;
|
export const version = 'v' + packageJSON.version;
|
||||||
const arch = process.arch;
|
const arch = process.arch;
|
||||||
|
const MSYS2_BASH =
|
||||||
|
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||||
|
|
||||||
function dPath(...paths: string[]) {
|
function dPath(...paths: string[]) {
|
||||||
return path.join(ROOT, 'dist', ...paths);
|
return path.join(ROOT, 'dist', ...paths);
|
||||||
@ -61,13 +62,9 @@ const standaloneZips: PathByPlatform = {
|
|||||||
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
|
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOclifInstallersOriginalNames = async (): Promise<PathByPlatform> => {
|
const oclifInstallers: PathByPlatform = {
|
||||||
const { stdout } = await execAsync('git rev-parse --short HEAD');
|
darwin: dPath('macos', `balena-${version}.pkg`),
|
||||||
const sha = stdout.trim();
|
win32: dPath('win32', `balena-${version}-${arch}.exe`),
|
||||||
return {
|
|
||||||
darwin: dPath('macos', `balena-${version}-${sha}-${arch}.pkg`),
|
|
||||||
win32: dPath('win32', `balena-${version}-${sha}-${arch}.exe`),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renamedOclifInstallers: PathByPlatform = {
|
const renamedOclifInstallers: PathByPlatform = {
|
||||||
@ -92,7 +89,7 @@ async function diffPkgOutput(pkgOut: string) {
|
|||||||
'tests',
|
'tests',
|
||||||
'test-data',
|
'test-data',
|
||||||
'pkg',
|
'pkg',
|
||||||
`expected-warnings-${process.platform}-${arch}.txt`,
|
`expected-warnings-${process.platform}.txt`,
|
||||||
);
|
);
|
||||||
const absSavedPath = path.join(ROOT, relSavedPath);
|
const absSavedPath = path.join(ROOT, relSavedPath);
|
||||||
const ignoreStartsWith = [
|
const ignoreStartsWith = [
|
||||||
@ -160,7 +157,7 @@ ${sep}
|
|||||||
* messages (stdout and stderr) in order to call diffPkgOutput().
|
* messages (stdout and stderr) in order to call diffPkgOutput().
|
||||||
*/
|
*/
|
||||||
async function execPkg(...args: any[]) {
|
async function execPkg(...args: any[]) {
|
||||||
const { exec: pkgExec } = await import('@yao-pkg/pkg');
|
const { exec: pkgExec } = await import('pkg');
|
||||||
const outTap = new StdOutTap(true);
|
const outTap = new StdOutTap(true);
|
||||||
try {
|
try {
|
||||||
outTap.tap();
|
outTap.tap();
|
||||||
@ -185,18 +182,9 @@ async function execPkg(...args: any[]) {
|
|||||||
* to be directly executed from inside another binary executable.)
|
* to be directly executed from inside another binary executable.)
|
||||||
*/
|
*/
|
||||||
async function buildPkg() {
|
async function buildPkg() {
|
||||||
// https://github.com/vercel/pkg#targets
|
|
||||||
let targets = `linux-${arch}`;
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
targets = `macos-${arch}`;
|
|
||||||
}
|
|
||||||
// TBC: not yet possible to build for Windows arm64 on x64 nodes
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
targets = `win-x64`;
|
|
||||||
}
|
|
||||||
const args = [
|
const args = [
|
||||||
'--targets',
|
'--target',
|
||||||
targets,
|
'host',
|
||||||
'--output',
|
'--output',
|
||||||
'build-bin/balena',
|
'build-bin/balena',
|
||||||
'package.json',
|
'package.json',
|
||||||
@ -211,6 +199,7 @@ async function buildPkg() {
|
|||||||
const paths: Array<[string, string[], string[]]> = [
|
const paths: Array<[string, string[], string[]]> = [
|
||||||
// [platform, [source path], [destination path]]
|
// [platform, [source path], [destination path]]
|
||||||
['*', ['open', 'xdg-open'], ['xdg-open']],
|
['*', ['open', 'xdg-open'], ['xdg-open']],
|
||||||
|
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
|
||||||
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
||||||
];
|
];
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@ -305,7 +294,7 @@ async function zipPkg() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
await fs.mkdirp(path.dirname(outputFile));
|
await fs.mkdirp(path.dirname(outputFile));
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
console.log(`Zipping standalone package to "${outputFile}"...`);
|
console.log(`Zipping standalone package to "${outputFile}"...`);
|
||||||
|
|
||||||
const archive = archiver('zip', {
|
const archive = archiver('zip', {
|
||||||
@ -326,11 +315,7 @@ async function zipPkg() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function signFilesForNotarization() {
|
async function signFilesForNotarization() {
|
||||||
console.log('Signing files for notarization');
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('Deleting unneeded zip files...');
|
console.log('Deleting unneeded zip files...');
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
klaw('node_modules/')
|
klaw('node_modules/')
|
||||||
@ -430,7 +415,6 @@ export async function buildStandaloneZip() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renameInstallerFiles() {
|
async function renameInstallerFiles() {
|
||||||
const oclifInstallers = await getOclifInstallersOriginalNames();
|
|
||||||
if (await fs.pathExists(oclifInstallers[process.platform])) {
|
if (await fs.pathExists(oclifInstallers[process.platform])) {
|
||||||
await fs.rename(
|
await fs.rename(
|
||||||
oclifInstallers[process.platform],
|
oclifInstallers[process.platform],
|
||||||
@ -441,30 +425,20 @@ async function renameInstallerFiles() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
||||||
* executable installer using Microsoft SignTool.exe (Sign Tool)
|
* executable installer by running the balena-io/scripts/shared/sign-exe.sh
|
||||||
* https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
|
* script (which must be in the PATH) using a MSYS2 bash shell.
|
||||||
*/
|
*/
|
||||||
async function signWindowsInstaller() {
|
async function signWindowsInstaller() {
|
||||||
if (process.env.SM_CODE_SIGNING_CERT_SHA1_HASH) {
|
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
||||||
const exeName = renamedOclifInstallers[process.platform];
|
const exeName = renamedOclifInstallers[process.platform];
|
||||||
console.log(`Signing installer "${exeName}"`);
|
console.log(`Signing installer "${exeName}"`);
|
||||||
// trust ...
|
await execFileAsync(MSYS2_BASH, [
|
||||||
await execFileAsync('signtool.exe', [
|
'sign-exe.sh',
|
||||||
'sign',
|
'-f',
|
||||||
'-sha1',
|
exeName,
|
||||||
process.env.SM_CODE_SIGNING_CERT_SHA1_HASH,
|
|
||||||
'-tr',
|
|
||||||
process.env.TIMESTAMP_SERVER || 'http://timestamp.comodoca.com',
|
|
||||||
'-td',
|
|
||||||
'SHA256',
|
|
||||||
'-fd',
|
|
||||||
'SHA256',
|
|
||||||
'-d',
|
'-d',
|
||||||
`balena-cli ${version}`,
|
`balena-cli ${version}`,
|
||||||
exeName,
|
|
||||||
]);
|
]);
|
||||||
// ... but verify
|
|
||||||
await execFileAsync('signtool.exe', ['verify', '-pa', '-v', exeName]);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
'Skipping installer signing step because CSC_* env vars are not set',
|
'Skipping installer signing step because CSC_* env vars are not set',
|
||||||
@ -476,20 +450,14 @@ async function signWindowsInstaller() {
|
|||||||
* Wait for Apple Installer Notarization to continue
|
* Wait for Apple Installer Notarization to continue
|
||||||
*/
|
*/
|
||||||
async function notarizeMacInstaller(): Promise<void> {
|
async function notarizeMacInstaller(): Promise<void> {
|
||||||
const teamId = process.env.XCODE_APP_LOADER_TEAM_ID || '66H43P8FRG';
|
const appleId = 'accounts+apple@balena.io';
|
||||||
const appleId =
|
const { notarize } = await import('electron-notarize');
|
||||||
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
|
await notarize({
|
||||||
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
|
appBundleId: 'io.balena.etcher',
|
||||||
|
appPath: renamedOclifInstallers.darwin,
|
||||||
if (appleIdPassword && teamId) {
|
appleId,
|
||||||
await notarize({
|
appleIdPassword: '@keychain:CLI_PASSWORD',
|
||||||
tool: 'notarytool',
|
});
|
||||||
teamId,
|
|
||||||
appPath: renamedOclifInstallers.darwin,
|
|
||||||
appleId,
|
|
||||||
appleIdPassword,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -503,10 +471,9 @@ export async function buildOclifInstaller() {
|
|||||||
let packOpts = ['-r', ROOT];
|
let packOpts = ['-r', ROOT];
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
packOS = 'macos';
|
packOS = 'macos';
|
||||||
packOpts = packOpts.concat('--targets', `darwin-${arch}`);
|
|
||||||
} else if (process.platform === 'win32') {
|
} else if (process.platform === 'win32') {
|
||||||
packOS = 'win';
|
packOS = 'win';
|
||||||
packOpts = packOpts.concat('--targets', 'win32-x64');
|
packOpts = packOpts.concat('-t', 'win32-x64');
|
||||||
}
|
}
|
||||||
if (packOS) {
|
if (packOS) {
|
||||||
console.log(`Building oclif installer for CLI ${version}`);
|
console.log(`Building oclif installer for CLI ${version}`);
|
||||||
@ -517,14 +484,17 @@ export async function buildOclifInstaller() {
|
|||||||
}
|
}
|
||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
console.log(`rimraf(${dir})`);
|
console.log(`rimraf(${dir})`);
|
||||||
await rimrafAsync(dir);
|
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
|
||||||
|
}
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
console.log('Signing files for notarization...');
|
||||||
|
await signFilesForNotarization();
|
||||||
}
|
}
|
||||||
console.log('=======================================================');
|
console.log('=======================================================');
|
||||||
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
|
console.log(`oclif "${packCmd}" "${packOpts.join('" "')}"`);
|
||||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||||
console.log('=======================================================');
|
console.log('=======================================================');
|
||||||
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
|
await oclifRun([packCmd].concat(...packOpts));
|
||||||
await oclifRun([packCmd].concat(...packOpts), oclifPath);
|
|
||||||
await renameInstallerFiles();
|
await renameInstallerFiles();
|
||||||
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
||||||
// The macOS installer is automatically signed by oclif (which runs the
|
// The macOS installer is automatically signed by oclif (which runs the
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { MarkdownFileParser } from './utils';
|
import { MarkdownFileParser } from './utils';
|
||||||
import { GlobSync } from 'glob';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the skeleton of CLI documentation/reference web page at:
|
* This is the skeleton of CLI documentation/reference web page at:
|
||||||
@ -25,111 +24,173 @@ import { GlobSync } from 'glob';
|
|||||||
*
|
*
|
||||||
* The `getCapitanoDoc` function in this module parses README.md and adds
|
* The `getCapitanoDoc` function in this module parses README.md and adds
|
||||||
* some content to this object.
|
* some content to this object.
|
||||||
*
|
|
||||||
* IMPORTANT
|
|
||||||
*
|
|
||||||
* All commands need to be stored under a folder in src/commands to maintain uniformity
|
|
||||||
* Generating docs will error out if directive not followed
|
|
||||||
* To add a custom heading for command docs, add the heading next to the folder name
|
|
||||||
* in the `commandHeadings` dictionary.
|
|
||||||
*
|
|
||||||
* This dictionary is the source of truth that creates the docs config which is used
|
|
||||||
* to generate the CLI documentation. By default, the folder name will be used.
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
const capitanoDoc = {
|
||||||
interface Category {
|
|
||||||
title: string;
|
|
||||||
files: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Documentation {
|
|
||||||
title: string;
|
|
||||||
introduction: string;
|
|
||||||
categories: Category[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mapping folders names to custom headings in the docs
|
|
||||||
const commandHeadings: { [key: string]: string } = {
|
|
||||||
'api-key': 'API Keys',
|
|
||||||
login: 'Authentication',
|
|
||||||
whoami: 'Authentication',
|
|
||||||
logout: 'Authentication',
|
|
||||||
env: 'Environment Variables',
|
|
||||||
help: 'Help and Version',
|
|
||||||
'ssh-key': 'SSH Keys',
|
|
||||||
organization: 'Organizations',
|
|
||||||
os: 'OS',
|
|
||||||
util: 'Utilities',
|
|
||||||
build: 'Deploy',
|
|
||||||
join: 'Platform',
|
|
||||||
leave: 'Platform',
|
|
||||||
app: 'Apps',
|
|
||||||
block: 'Blocks',
|
|
||||||
device: 'Devices',
|
|
||||||
fleet: 'Fleets',
|
|
||||||
release: 'Releases',
|
|
||||||
tag: 'Tags',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch all available commands
|
|
||||||
const allCommandsPaths = new GlobSync('build/commands/**/*.js', {
|
|
||||||
ignore: 'build/commands/internal/**',
|
|
||||||
}).found;
|
|
||||||
|
|
||||||
// Throw error if any commands found outside of command directories
|
|
||||||
const illegalCommandPaths = allCommandsPaths.filter((commandPath: string) =>
|
|
||||||
/^build\/commands\/[^/]+\.js$/.test(commandPath),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (illegalCommandPaths.length !== 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Found the following commands without a command directory: ${illegalCommandPaths}\n
|
|
||||||
To resolve this error, move the respective commands to their resource directories or create new ones.\n
|
|
||||||
Refer to the automation/capitanodoc/capitanodoc.ts file for more information.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Docs config template
|
|
||||||
const capitanoDoc: Documentation = {
|
|
||||||
title: 'balena CLI Documentation',
|
title: 'balena CLI Documentation',
|
||||||
introduction: '',
|
introduction: '',
|
||||||
categories: [],
|
categories: [
|
||||||
|
{
|
||||||
|
title: 'API keys',
|
||||||
|
files: ['build/commands/api-key/generate.js'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Fleet',
|
||||||
|
files: [
|
||||||
|
'build/commands/fleets.js',
|
||||||
|
'build/commands/fleet/index.js',
|
||||||
|
'build/commands/fleet/create.js',
|
||||||
|
'build/commands/fleet/purge.js',
|
||||||
|
'build/commands/fleet/rename.js',
|
||||||
|
'build/commands/fleet/restart.js',
|
||||||
|
'build/commands/fleet/rm.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Authentication',
|
||||||
|
files: [
|
||||||
|
'build/commands/login.js',
|
||||||
|
'build/commands/logout.js',
|
||||||
|
'build/commands/whoami.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Device',
|
||||||
|
files: [
|
||||||
|
'build/commands/devices/index.js',
|
||||||
|
'build/commands/devices/supported.js',
|
||||||
|
'build/commands/device/index.js',
|
||||||
|
'build/commands/device/deactivate.js',
|
||||||
|
'build/commands/device/identify.js',
|
||||||
|
'build/commands/device/init.js',
|
||||||
|
'build/commands/device/local-mode.js',
|
||||||
|
'build/commands/device/move.js',
|
||||||
|
'build/commands/device/os-update.js',
|
||||||
|
'build/commands/device/public-url.js',
|
||||||
|
'build/commands/device/purge.js',
|
||||||
|
'build/commands/device/reboot.js',
|
||||||
|
'build/commands/device/register.js',
|
||||||
|
'build/commands/device/rename.js',
|
||||||
|
'build/commands/device/restart.js',
|
||||||
|
'build/commands/device/rm.js',
|
||||||
|
'build/commands/device/shutdown.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Releases',
|
||||||
|
files: [
|
||||||
|
'build/commands/releases.js',
|
||||||
|
'build/commands/release/index.js',
|
||||||
|
'build/commands/release/finalize.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Environment Variables',
|
||||||
|
files: [
|
||||||
|
'build/commands/envs.js',
|
||||||
|
'build/commands/env/add.js',
|
||||||
|
'build/commands/env/rename.js',
|
||||||
|
'build/commands/env/rm.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tags',
|
||||||
|
files: [
|
||||||
|
'build/commands/tags.js',
|
||||||
|
'build/commands/tag/rm.js',
|
||||||
|
'build/commands/tag/set.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Help and Version',
|
||||||
|
files: ['help', 'build/commands/version.js'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Keys',
|
||||||
|
files: [
|
||||||
|
'build/commands/keys.js',
|
||||||
|
'build/commands/key/index.js',
|
||||||
|
'build/commands/key/add.js',
|
||||||
|
'build/commands/key/rm.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Logs',
|
||||||
|
files: ['build/commands/logs.js'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Network',
|
||||||
|
files: [
|
||||||
|
'build/commands/scan.js',
|
||||||
|
'build/commands/ssh.js',
|
||||||
|
'build/commands/tunnel.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Notes',
|
||||||
|
files: ['build/commands/note.js'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'OS',
|
||||||
|
files: [
|
||||||
|
'build/commands/os/build-config.js',
|
||||||
|
'build/commands/os/configure.js',
|
||||||
|
'build/commands/os/versions.js',
|
||||||
|
'build/commands/os/download.js',
|
||||||
|
'build/commands/os/initialize.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Config',
|
||||||
|
files: [
|
||||||
|
'build/commands/config/generate.js',
|
||||||
|
'build/commands/config/inject.js',
|
||||||
|
'build/commands/config/read.js',
|
||||||
|
'build/commands/config/reconfigure.js',
|
||||||
|
'build/commands/config/write.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Preload',
|
||||||
|
files: ['build/commands/preload.js'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Push',
|
||||||
|
files: ['build/commands/push.js'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Settings',
|
||||||
|
files: ['build/commands/settings.js'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Local',
|
||||||
|
files: [
|
||||||
|
'build/commands/local/configure.js',
|
||||||
|
'build/commands/local/flash.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deploy',
|
||||||
|
files: ['build/commands/build.js', 'build/commands/deploy.js'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Platform',
|
||||||
|
files: ['build/commands/join.js', 'build/commands/leave.js'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Utilities',
|
||||||
|
files: ['build/commands/util/available-drives.js'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Support',
|
||||||
|
files: ['build/commands/support.js'],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to capitalize each word of directory name
|
|
||||||
function formatTitle(dir: string): string {
|
|
||||||
return dir.replace(/(^\w|\s\w)/g, (word) => word.toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a map to track the categories for faster lookup
|
|
||||||
const categoriesMap: { [key: string]: Category } = {};
|
|
||||||
|
|
||||||
for (const commandPath of allCommandsPaths) {
|
|
||||||
const commandDir = path.basename(path.dirname(commandPath));
|
|
||||||
const heading = commandHeadings[commandDir] || formatTitle(commandDir);
|
|
||||||
|
|
||||||
if (!categoriesMap[heading]) {
|
|
||||||
categoriesMap[heading] = { title: heading, files: [] };
|
|
||||||
capitanoDoc.categories.push(categoriesMap[heading]);
|
|
||||||
}
|
|
||||||
|
|
||||||
categoriesMap[heading].files.push(commandPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort Category titles alphabetically
|
|
||||||
capitanoDoc.categories = capitanoDoc.categories.sort((a, b) =>
|
|
||||||
a.title.localeCompare(b.title),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort Category file paths alphabetically
|
|
||||||
capitanoDoc.categories.forEach((category) => {
|
|
||||||
category.files.sort((a, b) => a.localeCompare(b));
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modify and return the `capitanoDoc` object above in order to generate the
|
* Modify and return the `capitanoDoc` object above in order to render the
|
||||||
* CLI documentation at docs/balena-cli.md
|
* CLI documentation/reference web page at:
|
||||||
|
* https://www.balena.io/docs/reference/cli/
|
||||||
*
|
*
|
||||||
* This function parses the README.md file to extract relevant sections
|
* This function parses the README.md file to extract relevant sections
|
||||||
* for the documentation web page.
|
* for the documentation web page.
|
||||||
@ -145,7 +206,7 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
|
|||||||
throw new Error(`Error parsing section title`);
|
throw new Error(`Error parsing section title`);
|
||||||
}
|
}
|
||||||
// match[1] has the title, match[2] has the rest
|
// match[1] has the title, match[2] has the rest
|
||||||
return match?.[2];
|
return match && match[2];
|
||||||
}),
|
}),
|
||||||
mdParser.getSectionOfTitle('Installation'),
|
mdParser.getSectionOfTitle('Installation'),
|
||||||
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),
|
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),
|
||||||
|
4
automation/capitanodoc/doc-types.d.ts
vendored
4
automation/capitanodoc/doc-types.d.ts
vendored
@ -14,7 +14,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import type { Command as OclifCommandClass } from '@oclif/core';
|
import { Command as OclifCommandClass } from '@oclif/command';
|
||||||
|
|
||||||
type OclifCommand = typeof OclifCommandClass;
|
type OclifCommand = typeof OclifCommandClass;
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export interface Document {
|
|||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
title: string;
|
title: string;
|
||||||
commands: Array<OclifCommand & { name: string }>;
|
commands: OclifCommand[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export { OclifCommand };
|
export { OclifCommand };
|
||||||
|
@ -16,8 +16,9 @@
|
|||||||
*/
|
*/
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getCapitanoDoc } from './capitanodoc';
|
import { getCapitanoDoc } from './capitanodoc';
|
||||||
import type { Category, Document, OclifCommand } from './doc-types';
|
import { Category, Document, OclifCommand } from './doc-types';
|
||||||
import * as markdown from './markdown';
|
import * as markdown from './markdown';
|
||||||
|
import { stripIndent } from '../../lib/utils/lazy';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the markdown document (as a string) for the CLI documentation
|
* Generates the markdown document (as a string) for the CLI documentation
|
||||||
@ -38,7 +39,7 @@ export async function renderMarkdown(): Promise<string> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const jsFilename of commandCategory.files) {
|
for (const jsFilename of commandCategory.files) {
|
||||||
category.commands.push(await importOclifCommands(jsFilename));
|
category.commands.push(...importOclifCommands(jsFilename));
|
||||||
}
|
}
|
||||||
result.categories.push(category);
|
result.categories.push(category);
|
||||||
}
|
}
|
||||||
@ -46,23 +47,49 @@ export async function renderMarkdown(): Promise<string> {
|
|||||||
return markdown.render(result);
|
return markdown.render(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importOclifCommands(jsFilename: string) {
|
// Help is now managed via a plugin
|
||||||
const command = (await import(path.join(process.cwd(), jsFilename)))
|
// This fake command allows capitanodoc to include help in docs
|
||||||
.default as OclifCommand;
|
class FakeHelpCommand {
|
||||||
|
description = stripIndent`
|
||||||
|
List balena commands, or get detailed help for a specific command.
|
||||||
|
|
||||||
return {
|
List balena commands, or get detailed help for a specific command.
|
||||||
...command,
|
`;
|
||||||
// build/commands/device/index.js -> device
|
|
||||||
// build/commands/device/list.js -> device list
|
examples = [
|
||||||
name: jsFilename
|
'$ balena help',
|
||||||
.split('/')
|
'$ balena help login',
|
||||||
.slice(2)
|
'$ balena help os download',
|
||||||
.join(' ')
|
];
|
||||||
.split('.')
|
|
||||||
.slice(0, 1)
|
args = [
|
||||||
.join(' ')
|
{
|
||||||
.split(' index')[0],
|
name: 'command',
|
||||||
} as Category['commands'][0];
|
description: 'command to show help for',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
usage = 'help [command]';
|
||||||
|
|
||||||
|
flags = {
|
||||||
|
verbose: {
|
||||||
|
description: 'show additional commands',
|
||||||
|
char: '-v',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function importOclifCommands(jsFilename: string): OclifCommand[] {
|
||||||
|
// TODO: Currently oclif commands with no `usage` overridden will cause
|
||||||
|
// an error when parsed. This should be improved so that `usage` does not have
|
||||||
|
// to be overridden if not necessary.
|
||||||
|
|
||||||
|
const command: OclifCommand =
|
||||||
|
jsFilename === 'help'
|
||||||
|
? (new FakeHelpCommand() as unknown as OclifCommand)
|
||||||
|
: (require(path.join(process.cwd(), jsFilename)).default as OclifCommand);
|
||||||
|
|
||||||
|
return [command];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,5 +105,5 @@ async function printMarkdown() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// tslint:disable-next-line:no-floating-promises
|
||||||
printMarkdown();
|
printMarkdown();
|
||||||
|
@ -14,31 +14,16 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Parser } from '@oclif/core';
|
import { flagUsages } from '@oclif/parser';
|
||||||
import * as ent from 'ent';
|
import * as ent from 'ent';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { capitanoizeOclifUsage } from '../../src/utils/oclif-utils';
|
import { getManualSortCompareFunction } from '../../lib/utils/helpers';
|
||||||
import type { Category, Document } from './doc-types';
|
import { capitanoizeOclifUsage } from '../../lib/utils/oclif-utils';
|
||||||
|
import { Category, Document, OclifCommand } from './doc-types';
|
||||||
|
|
||||||
function renderOclifCommand(command: Category['commands'][0]): string[] {
|
function renderOclifCommand(command: OclifCommand): string[] {
|
||||||
const result = [`## ${ent.encode(command.name || '')}`];
|
const result = [`## ${ent.encode(command.usage || '')}`];
|
||||||
if (command.aliases?.length) {
|
|
||||||
result.push('### Aliases');
|
|
||||||
result.push(
|
|
||||||
command.aliases
|
|
||||||
.map(
|
|
||||||
(alias) =>
|
|
||||||
`- \`${alias}\`${command.deprecateAliases ? ' *(deprecated)*' : ''}`,
|
|
||||||
)
|
|
||||||
.join('\n'),
|
|
||||||
);
|
|
||||||
result.push(
|
|
||||||
`\nTo use one of the aliases, replace \`${command.name}\` with the alias.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push('### Description');
|
|
||||||
const description = (command.description || '')
|
const description = (command.description || '')
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.slice(1) // remove the first line, which oclif uses as help header
|
.slice(1) // remove the first line, which oclif uses as help header
|
||||||
@ -52,8 +37,8 @@ function renderOclifCommand(command: Category['commands'][0]): string[] {
|
|||||||
|
|
||||||
if (!_.isEmpty(command.args)) {
|
if (!_.isEmpty(command.args)) {
|
||||||
result.push('### Arguments');
|
result.push('### Arguments');
|
||||||
for (const [name, arg] of Object.entries(command.args!)) {
|
for (const arg of command.args!) {
|
||||||
result.push(`#### ${name.toUpperCase()}`, arg.description || '');
|
result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +49,7 @@ function renderOclifCommand(command: Category['commands'][0]): string[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
flag.name = name;
|
flag.name = name;
|
||||||
const flagUsage = Parser.flagUsages([flag])
|
const flagUsage = flagUsages([flag])
|
||||||
.map(([usage, _description]) => usage)
|
.map(([usage, _description]) => usage)
|
||||||
.join()
|
.join()
|
||||||
.trim();
|
.trim();
|
||||||
@ -95,7 +80,7 @@ function renderToc(categories: Category[]): string[] {
|
|||||||
result.push(
|
result.push(
|
||||||
category.commands
|
category.commands
|
||||||
.map((command) => {
|
.map((command) => {
|
||||||
const signature = capitanoizeOclifUsage(command.name);
|
const signature = capitanoizeOclifUsage(command.usage);
|
||||||
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
|
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
|
||||||
})
|
})
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
@ -104,7 +89,33 @@ function renderToc(categories: Category[]): string[] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const manualCategorySorting: { [category: string]: string[] } = {
|
||||||
|
'Environment Variables': ['envs', 'env rm', 'env add', 'env rename'],
|
||||||
|
OS: [
|
||||||
|
'os versions',
|
||||||
|
'os download',
|
||||||
|
'os build config',
|
||||||
|
'os configure',
|
||||||
|
'os initialize',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function sortCommands(doc: Document): void {
|
||||||
|
for (const category of doc.categories) {
|
||||||
|
if (category.title in manualCategorySorting) {
|
||||||
|
category.commands = category.commands.sort(
|
||||||
|
getManualSortCompareFunction<OclifCommand, string>(
|
||||||
|
manualCategorySorting[category.title],
|
||||||
|
(cmd: OclifCommand, x: string) =>
|
||||||
|
(cmd.usage || '').toString().replace(/\W+/g, ' ').includes(x),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function render(doc: Document) {
|
export function render(doc: Document) {
|
||||||
|
sortCommands(doc);
|
||||||
const result = [
|
const result = [
|
||||||
`# ${doc.title}`,
|
`# ${doc.title}`,
|
||||||
doc.introduction,
|
doc.introduction,
|
||||||
|
@ -15,9 +15,41 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { OptionDefinition } from 'capitano';
|
||||||
|
import * as ent from 'ent';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
|
|
||||||
|
export function getOptionPrefix(signature: string) {
|
||||||
|
if (signature.length > 1) {
|
||||||
|
return '--';
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOptionSignature(signature: string) {
|
||||||
|
return `${getOptionPrefix(signature)}${signature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCapitanoOption(option: OptionDefinition): string {
|
||||||
|
let result = getOptionSignature(option.signature);
|
||||||
|
|
||||||
|
if (Array.isArray(option.alias)) {
|
||||||
|
for (const alias of option.alias) {
|
||||||
|
result += `, ${getOptionSignature(alias)}`;
|
||||||
|
}
|
||||||
|
} else if (typeof option.alias === 'string') {
|
||||||
|
result += `, ${getOptionSignature(option.alias)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.parameter) {
|
||||||
|
result += ` <${option.parameter}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ent.encode(result);
|
||||||
|
}
|
||||||
|
|
||||||
export class MarkdownFileParser {
|
export class MarkdownFileParser {
|
||||||
constructor(public mdFilePath: string) {}
|
constructor(public mdFilePath: string) {}
|
||||||
|
|
||||||
|
@ -15,12 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-imports
|
const stripIndent = require('common-tags/lib/stripIndent');
|
||||||
import { stripIndent } from 'common-tags';
|
const _ = require('lodash');
|
||||||
import * as _ from 'lodash';
|
const { promises: fs } = require('fs');
|
||||||
import { promises as fs } from 'fs';
|
const path = require('path');
|
||||||
import * as path from 'path';
|
const simplegit = require('simple-git/promise');
|
||||||
import { simpleGit } from 'simple-git';
|
|
||||||
|
|
||||||
const ROOT = path.normalize(path.join(__dirname, '..'));
|
const ROOT = path.normalize(path.join(__dirname, '..'));
|
||||||
|
|
||||||
@ -32,7 +31,7 @@ const ROOT = path.normalize(path.join(__dirname, '..'));
|
|||||||
* using `touch`.
|
* using `touch`.
|
||||||
*/
|
*/
|
||||||
async function checkBuildTimestamps() {
|
async function checkBuildTimestamps() {
|
||||||
const git = simpleGit(ROOT);
|
const git = simplegit(ROOT);
|
||||||
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
|
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
|
||||||
const [docStat, gitStatus] = await Promise.all([
|
const [docStat, gitStatus] = await Promise.all([
|
||||||
fs.stat(docFile),
|
fs.stat(docFile),
|
||||||
@ -43,8 +42,8 @@ async function checkBuildTimestamps() {
|
|||||||
...gitStatus.staged,
|
...gitStatus.staged,
|
||||||
...gitStatus.renamed.map((o) => o.to),
|
...gitStatus.renamed.map((o) => o.to),
|
||||||
])
|
])
|
||||||
// select only staged files that start with src/ or typings/
|
// select only staged files that start with lib/ or typings/
|
||||||
.filter((f) => f.match(/^(src|typings)[/\\]/))
|
.filter((f) => f.match(/^(lib|typings)[/\\]/))
|
||||||
.map((f) => path.join(ROOT, f));
|
.map((f) => path.join(ROOT, f));
|
||||||
|
|
||||||
const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));
|
const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));
|
||||||
@ -82,5 +81,4 @@ async function run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
run();
|
run();
|
@ -23,8 +23,8 @@ function parseSemver(version) {
|
|||||||
* @param {string} v2
|
* @param {string} v2
|
||||||
*/
|
*/
|
||||||
function semverGte(v1, v2) {
|
function semverGte(v1, v2) {
|
||||||
const v1Array = parseSemver(v1);
|
let v1Array = parseSemver(v1);
|
||||||
const v2Array = parseSemver(v2);
|
let v2Array = parseSemver(v2);
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
if (v1Array[i] < v2Array[i]) {
|
if (v1Array[i] < v2Array[i]) {
|
||||||
return false;
|
return false;
|
||||||
|
257
automation/deploy-bin.ts
Normal file
257
automation/deploy-bin.ts
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* 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.
|
||||||
|
* 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 * as Bluebird from 'bluebird';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as semver from 'semver';
|
||||||
|
|
||||||
|
import { finalReleaseAssets, version } from './build-bin';
|
||||||
|
|
||||||
|
const { GITHUB_TOKEN } = process.env;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update a release in GitHub's releases page, uploading the
|
||||||
|
* installer files (standalone zip + native oclif installers).
|
||||||
|
*/
|
||||||
|
export async function createGitHubRelease() {
|
||||||
|
console.log(`Publishing release ${version} to GitHub`);
|
||||||
|
const publishRelease = await import('publish-release');
|
||||||
|
const ghRelease = await Bluebird.fromCallback(
|
||||||
|
publishRelease.bind(null, {
|
||||||
|
token: GITHUB_TOKEN || '',
|
||||||
|
owner: 'balena-io',
|
||||||
|
repo: 'balena-cli',
|
||||||
|
tag: version,
|
||||||
|
name: `balena-CLI ${version}`,
|
||||||
|
reuseRelease: true,
|
||||||
|
assets: finalReleaseAssets[process.platform],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
console.log(`Release ${version} successful: ${ghRelease.html_url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top-level function to create a CLI release in GitHub's releases page:
|
||||||
|
* call zipStandaloneInstaller(), rename the files as we'd like them to
|
||||||
|
* display on the releases page, and call createGitHubRelease() to upload
|
||||||
|
* the files.
|
||||||
|
*/
|
||||||
|
export async function release() {
|
||||||
|
try {
|
||||||
|
await createGitHubRelease();
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error creating GitHub release:\n${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return a cached Octokit instance, creating a new one as needed. */
|
||||||
|
const getOctokit = _.once(function () {
|
||||||
|
const Octokit = (
|
||||||
|
require('@octokit/rest') as typeof import('@octokit/rest')
|
||||||
|
).Octokit.plugin(
|
||||||
|
(
|
||||||
|
require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling')
|
||||||
|
).throttling,
|
||||||
|
);
|
||||||
|
return new Octokit({
|
||||||
|
auth: GITHUB_TOKEN,
|
||||||
|
throttle: {
|
||||||
|
onRateLimit: (retryAfter: number, options: any) => {
|
||||||
|
console.warn(
|
||||||
|
`Request quota exhausted for request ${options.method} ${options.url}`,
|
||||||
|
);
|
||||||
|
// retries 3 times
|
||||||
|
if (options.request.retryCount < 3) {
|
||||||
|
console.log(`Retrying after ${retryAfter} seconds!`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAbuseLimit: (_retryAfter: number, options: any) => {
|
||||||
|
// does not retry, only logs a warning
|
||||||
|
console.warn(
|
||||||
|
`Abuse detected for request ${options.method} ${options.url}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract pagination information (current page, total pages, ordinal number)
|
||||||
|
* from the 'link' response header (example below), using the parse-link-header
|
||||||
|
* npm package:
|
||||||
|
* "link": "<https://api.github.com/repositories/187370853/releases?per_page=2&page=2>; rel=\"next\",
|
||||||
|
* <https://api.github.com/repositories/187370853/releases?per_page=2&page=3>; rel=\"last\""
|
||||||
|
*
|
||||||
|
* @param response Octokit response object (including response.headers.link)
|
||||||
|
* @param perPageDefault Default per_page pagination value if missing in URL
|
||||||
|
* @return Object where 'page' is the current page number (1-based),
|
||||||
|
* 'pages' is the total number of pages, and 'ordinal' is the ordinal number
|
||||||
|
* (3rd, 4th, 5th...) of the first item in the current page.
|
||||||
|
*/
|
||||||
|
function getPageNumbers(
|
||||||
|
response: any,
|
||||||
|
perPageDefault: number,
|
||||||
|
): { page: number; pages: number; ordinal: number } {
|
||||||
|
const res = { page: 1, pages: 1, ordinal: 1 };
|
||||||
|
if (!response.headers.link) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
const parse =
|
||||||
|
require('parse-link-header') as typeof import('parse-link-header');
|
||||||
|
const parsed = parse(response.headers.link);
|
||||||
|
if (parsed == null) {
|
||||||
|
throw new Error(`Failed to parse link header: '${response.headers.link}'`);
|
||||||
|
}
|
||||||
|
let perPage = perPageDefault;
|
||||||
|
if (parsed.next) {
|
||||||
|
if (parsed.next.per_page) {
|
||||||
|
perPage = parseInt(parsed.next.per_page, 10);
|
||||||
|
}
|
||||||
|
res.page = parseInt(parsed.next.page, 10) - 1;
|
||||||
|
res.pages = parseInt(parsed.last.page, 10);
|
||||||
|
} else {
|
||||||
|
if (parsed.prev.per_page) {
|
||||||
|
perPage = parseInt(parsed.prev.per_page, 10);
|
||||||
|
}
|
||||||
|
res.page = res.pages = parseInt(parsed.prev.page, 10) + 1;
|
||||||
|
}
|
||||||
|
res.ordinal = (res.page - 1) * perPage + 1;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over every GitHub release in the given owner/repo, check whether
|
||||||
|
* its tag_name matches against the affectedVersions semver spec, and if so
|
||||||
|
* replace its release description (body) with the given newDescription value.
|
||||||
|
* @param owner GitHub repo owner, e.g. 'balena-io' or 'pdcastro'
|
||||||
|
* @param repo GitHub repo, e.g. 'balena-cli'
|
||||||
|
* @param affectedVersions Semver spec, e.g. '2.6.1 - 7.10.9 || 8.0.0'
|
||||||
|
* @param newDescription New release description (body)
|
||||||
|
* @param editID Short string present in newDescription, e.g. '[AA101]', that
|
||||||
|
* can be searched to determine whether that release has already been updated.
|
||||||
|
*/
|
||||||
|
async function updateGitHubReleaseDescriptions(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
affectedVersions: string,
|
||||||
|
newDescription: string,
|
||||||
|
editID: string,
|
||||||
|
) {
|
||||||
|
const perPage = 30;
|
||||||
|
const octokit = getOctokit();
|
||||||
|
const options = await octokit.repos.listReleases.endpoint.merge({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
per_page: perPage,
|
||||||
|
});
|
||||||
|
let errCount = 0;
|
||||||
|
type Release =
|
||||||
|
import('@octokit/rest').RestEndpointMethodTypes['repos']['listReleases']['response']['data'][0];
|
||||||
|
for await (const response of octokit.paginate.iterator<Release>(options)) {
|
||||||
|
const {
|
||||||
|
page: thisPage,
|
||||||
|
pages: totalPages,
|
||||||
|
ordinal,
|
||||||
|
} = getPageNumbers(response, perPage);
|
||||||
|
let i = 0;
|
||||||
|
for (const cliRelease of response.data) {
|
||||||
|
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;
|
||||||
|
if (!cliRelease.id) {
|
||||||
|
console.error(
|
||||||
|
`${prefix} Error: missing release ID (errCount=${++errCount})`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${cliRelease.id})`;
|
||||||
|
if (cliRelease.draft === true) {
|
||||||
|
console.info(`${skipMsg}: draft release`);
|
||||||
|
continue;
|
||||||
|
} else if (cliRelease.body && cliRelease.body.includes(editID)) {
|
||||||
|
console.info(`${skipMsg}: already updated`);
|
||||||
|
continue;
|
||||||
|
} else if (!semver.satisfies(cliRelease.tag_name, affectedVersions)) {
|
||||||
|
console.info(`${skipMsg}: outside version range`);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
const updatedRelease = {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
release_id: cliRelease.id,
|
||||||
|
body: newDescription,
|
||||||
|
};
|
||||||
|
let oldBodyPreview = cliRelease.body;
|
||||||
|
if (oldBodyPreview) {
|
||||||
|
oldBodyPreview = oldBodyPreview.replace(/\s+/g, ' ').trim();
|
||||||
|
if (oldBodyPreview.length > 12) {
|
||||||
|
oldBodyPreview = oldBodyPreview.substring(0, 9) + '...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.info(
|
||||||
|
`${prefix} updating release "${cliRelease.tag_name}" (${cliRelease.id}) old body="${oldBodyPreview}"`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await octokit.repos.updateRelease(updatedRelease);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`${skipMsg}: Error: ${err.message} (count=${++errCount})`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a warning description to CLI releases affected by a mixpanel tracking
|
||||||
|
* security issue (#1359). This function can be executed "manually" with the
|
||||||
|
* following command line:
|
||||||
|
*
|
||||||
|
* npx ts-node --type-check -P automation/tsconfig.json automation/run.ts fix1359
|
||||||
|
*/
|
||||||
|
export async function updateDescriptionOfReleasesAffectedByIssue1359() {
|
||||||
|
// Run only on Linux/Node10, instead of all platform/Node combinations.
|
||||||
|
// (It could have been any other platform, as long as it only runs once.)
|
||||||
|
if (process.platform !== 'linux' || semver.major(process.version) !== 10) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const owner = 'balena-io';
|
||||||
|
const repo = 'balena-cli';
|
||||||
|
const affectedVersions =
|
||||||
|
'2.6.1 - 7.10.9 || 8.0.0 - 8.1.0 || 9.0.0 - 9.15.6 || 10.0.0 - 10.17.5 || 11.0.0 - 11.7.2';
|
||||||
|
const editID = '[AA100]';
|
||||||
|
let newDescription = `
|
||||||
|
Please note: the "login" command in this release is affected by a
|
||||||
|
security issue fixed in versions
|
||||||
|
[7.10.10](https://github.com/balena-io/balena-cli/releases/tag/v7.10.10),
|
||||||
|
[8.1.1](https://github.com/balena-io/balena-cli/releases/tag/v8.1.1),
|
||||||
|
[9.15.7](https://github.com/balena-io/balena-cli/releases/tag/v9.15.7),
|
||||||
|
[10.17.6](https://github.com/balena-io/balena-cli/releases/tag/v10.17.6),
|
||||||
|
[11.7.3](https://github.com/balena-io/balena-cli/releases/tag/v11.7.3)
|
||||||
|
and later. If you need to use this version, avoid passing your password,
|
||||||
|
keys or tokens as command-line arguments. ${editID}`;
|
||||||
|
// remove line breaks and collapse white space
|
||||||
|
newDescription = newDescription.replace(/\s+/g, ' ').trim();
|
||||||
|
await updateGitHubReleaseDescriptions(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
affectedVersions,
|
||||||
|
newDescription,
|
||||||
|
editID,
|
||||||
|
);
|
||||||
|
}
|
@ -21,9 +21,12 @@ import {
|
|||||||
buildOclifInstaller,
|
buildOclifInstaller,
|
||||||
buildStandaloneZip,
|
buildStandaloneZip,
|
||||||
catchUncommitted,
|
catchUncommitted,
|
||||||
signFilesForNotarization,
|
|
||||||
testShrinkwrap,
|
testShrinkwrap,
|
||||||
} from './build-bin';
|
} from './build-bin';
|
||||||
|
import {
|
||||||
|
release,
|
||||||
|
updateDescriptionOfReleasesAffectedByIssue1359,
|
||||||
|
} from './deploy-bin';
|
||||||
|
|
||||||
// DEBUG set to falsy for negative values else is truthy
|
// DEBUG set to falsy for negative values else is truthy
|
||||||
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||||
@ -37,6 +40,7 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
|||||||
* of the following strings, then call the appropriate functions:
|
* of the following strings, then call the appropriate functions:
|
||||||
* 'build:installer' (to build a native oclif installer)
|
* 'build:installer' (to build a native oclif installer)
|
||||||
* 'build:standalone' (to build a standalone pkg package)
|
* 'build:standalone' (to build a standalone pkg package)
|
||||||
|
* 'release' (to create/update a GitHub release)
|
||||||
*
|
*
|
||||||
* @param args Arguments to parse (default is process.argv.slice(2))
|
* @param args Arguments to parse (default is process.argv.slice(2))
|
||||||
*/
|
*/
|
||||||
@ -50,16 +54,32 @@ async function parse(args?: string[]) {
|
|||||||
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
||||||
'build:installer': buildOclifInstaller,
|
'build:installer': buildOclifInstaller,
|
||||||
'build:standalone': buildStandaloneZip,
|
'build:standalone': buildStandaloneZip,
|
||||||
'sign:binaries': signFilesForNotarization,
|
|
||||||
'catch-uncommitted': catchUncommitted,
|
'catch-uncommitted': catchUncommitted,
|
||||||
'test-shrinkwrap': testShrinkwrap,
|
'test-shrinkwrap': testShrinkwrap,
|
||||||
|
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
|
||||||
|
release,
|
||||||
};
|
};
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
if (!Object.hasOwn(commands, arg)) {
|
if (!commands.hasOwnProperty(arg)) {
|
||||||
throw new Error(`command unknown: ${arg}`);
|
throw new Error(`command unknown: ${arg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The BUILD_TMP env var is used as an alternative location for oclif
|
||||||
|
// (patched) to copy/extract the CLI files, run npm install and then
|
||||||
|
// create the NSIS executable installer for Windows. This was necessary
|
||||||
|
// to avoid issues with a 260-char limit on Windows paths (possibly a
|
||||||
|
// limitation of some library used by NSIS), as the "current working dir"
|
||||||
|
// provided by balena CI is a rather long path to start with.
|
||||||
|
if (process.platform === 'win32' && !process.env.BUILD_TMP) {
|
||||||
|
const randID = (await import('crypto'))
|
||||||
|
.randomBytes(6)
|
||||||
|
.toString('base64')
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_'); // base64url (RFC 4648)
|
||||||
|
process.env.BUILD_TMP = `C:\\tmp\\${randID}`;
|
||||||
|
}
|
||||||
|
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
try {
|
try {
|
||||||
const cmdFunc = commands[arg];
|
const cmdFunc = commands[arg];
|
||||||
@ -83,5 +103,5 @@ export async function run(args?: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// tslint:disable-next-line:no-floating-promises
|
||||||
run();
|
run();
|
||||||
|
@ -3,7 +3,7 @@ import * as semver from 'semver';
|
|||||||
|
|
||||||
const changeTypes = ['major', 'minor', 'patch'] as const;
|
const changeTypes = ['major', 'minor', 'patch'] as const;
|
||||||
|
|
||||||
const validateChangeType = (maybeChangeType = 'minor') => {
|
const validateChangeType = (maybeChangeType: string = 'minor') => {
|
||||||
maybeChangeType = maybeChangeType.toLowerCase();
|
maybeChangeType = maybeChangeType.toLowerCase();
|
||||||
switch (maybeChangeType) {
|
switch (maybeChangeType) {
|
||||||
case 'patch':
|
case 'patch':
|
||||||
@ -107,11 +107,11 @@ async function $main() {
|
|||||||
|
|
||||||
const changeType = process.argv[4]
|
const changeType = process.argv[4]
|
||||||
? // if the caller specified a change type, use that one
|
? // if the caller specified a change type, use that one
|
||||||
validateChangeType(process.argv[4])
|
validateChangeType(process.argv[4])
|
||||||
: // use the same change type as in the dependency, but avoid major bumps
|
: // use the same change type as in the dependency, but avoid major bumps
|
||||||
semverChangeType && semverChangeType !== 'major'
|
semverChangeType && semverChangeType !== 'major'
|
||||||
? semverChangeType
|
? semverChangeType
|
||||||
: 'minor';
|
: 'minor';
|
||||||
console.log(`Using Change-type: ${changeType}`);
|
console.log(`Using Change-type: ${changeType}`);
|
||||||
|
|
||||||
let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD');
|
let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD');
|
||||||
@ -136,4 +136,5 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void main();
|
// tslint:disable-next-line:no-floating-promises
|
||||||
|
main();
|
||||||
|
@ -16,10 +16,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
|
import * as _ from 'lodash';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
|
||||||
import { diffTrimmedLines } from 'diff';
|
|
||||||
import * as whichMod from 'which';
|
|
||||||
|
|
||||||
export const ROOT = path.join(__dirname, '..');
|
export const ROOT = path.join(__dirname, '..');
|
||||||
|
|
||||||
@ -67,6 +65,7 @@ export class StdOutTap {
|
|||||||
* https://www.npmjs.com/package/diff
|
* https://www.npmjs.com/package/diff
|
||||||
*/
|
*/
|
||||||
export function diffLines(str1: string, str2: string): string {
|
export function diffLines(str1: string, str2: string): string {
|
||||||
|
const { diffTrimmedLines } = require('diff');
|
||||||
const diffObjs = diffTrimmedLines(str1, str2);
|
const diffObjs = diffTrimmedLines(str1, str2);
|
||||||
const prefix = (chunk: string, char: string) =>
|
const prefix = (chunk: string, char: string) =>
|
||||||
chunk
|
chunk
|
||||||
@ -78,18 +77,15 @@ export function diffLines(str1: string, str2: string): string {
|
|||||||
return part.added
|
return part.added
|
||||||
? prefix(part.value, '+')
|
? prefix(part.value, '+')
|
||||||
: part.removed
|
: part.removed
|
||||||
? prefix(part.value, '-')
|
? prefix(part.value, '-')
|
||||||
: prefix(part.value, ' ');
|
: prefix(part.value, ' ');
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
return diffStr;
|
return diffStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadPackageJson() {
|
export function loadPackageJson() {
|
||||||
const packageJsonPath = path.join(ROOT, 'package.json');
|
return require(path.join(ROOT, 'package.json'));
|
||||||
|
|
||||||
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
|
|
||||||
return JSON.parse(packageJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -102,6 +98,7 @@ export function loadPackageJson() {
|
|||||||
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
|
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
|
||||||
*/
|
*/
|
||||||
export async function which(program: string): Promise<string> {
|
export async function which(program: string): Promise<string> {
|
||||||
|
const whichMod = await import('which');
|
||||||
let programPath: string;
|
let programPath: string;
|
||||||
try {
|
try {
|
||||||
programPath = await whichMod(program);
|
programPath = await whichMod(program);
|
||||||
@ -132,7 +129,7 @@ export async function whichSpawn(
|
|||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
.on('close', resolve);
|
.on('close', resolve);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err as Error);
|
reject(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -1 +0,0 @@
|
|||||||
run.js
|
|
23
bin/balena
Executable file
23
bin/balena
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// tslint:disable:no-var-requires
|
||||||
|
|
||||||
|
// We boost the threadpool size as ext2fs can deadlock with some
|
||||||
|
// operations otherwise, if the pool runs out.
|
||||||
|
process.env.UV_THREADPOOL_SIZE = '64';
|
||||||
|
|
||||||
|
// Disable oclif registering ts-node
|
||||||
|
process.env.OCLIF_TS_NODE = 0;
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
// Use fast-boot to cache require lookups, speeding up startup
|
||||||
|
await require('../build/fast-boot').start();
|
||||||
|
|
||||||
|
// Set the desired es version for downstream modules that support it
|
||||||
|
require('@balena/es-version').set('es2018');
|
||||||
|
|
||||||
|
// Run the CLI
|
||||||
|
await require('../build/app').run();
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
@ -1 +0,0 @@
|
|||||||
dev.js
|
|
89
bin/balena-dev
Executable file
89
bin/balena-dev
Executable file
@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// ****************************************************************************
|
||||||
|
// THIS IS FOR DEV PURPOSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
|
||||||
|
// Before opening a PR you should build and test your changes using bin/balena
|
||||||
|
// ****************************************************************************
|
||||||
|
|
||||||
|
// tslint:disable:no-var-requires
|
||||||
|
|
||||||
|
// We boost the threadpool size as ext2fs can deadlock with some
|
||||||
|
// operations otherwise, if the pool runs out.
|
||||||
|
process.env.UV_THREADPOOL_SIZE = '64';
|
||||||
|
|
||||||
|
// Note on `fast-boot2`: We do not use `fast-boot2` with `balena-dev` because:
|
||||||
|
// * fast-boot2's cacheKiller option is configured to include the timestamps of
|
||||||
|
// the package.json and npm-shrinkwrap.json files, to avoid unexpected CLI
|
||||||
|
// behavior when changes are made to dependencies during development. This is
|
||||||
|
// generally a good thing, however, `balena-dev` (a few lines below) edits
|
||||||
|
// `package.json` to modify oclif paths, and this results in cache
|
||||||
|
// invalidation and a performance hit rather than speedup.
|
||||||
|
// * Even if the timestamps are removed from cacheKiller, so that there is no
|
||||||
|
// cache invalidation, fast-boot's speedup is barely noticeable when ts-node
|
||||||
|
// is used, e.g. 1.43s vs 1.4s when running `balena version`.
|
||||||
|
// * `fast-boot` causes unexpected behavior when used with `npm link` or
|
||||||
|
// when the `node_modules` folder is manually modified (affecting transitive
|
||||||
|
// dependencies) during development (e.g. bug investigations). A workaround
|
||||||
|
// is to use `balena-dev` without `fast-boot`. See also notes in
|
||||||
|
// `CONTRIBUTING.md`.
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const rootDir = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
// Allow balena-dev to work with oclif by temporarily
|
||||||
|
// pointing oclif config options to lib/ instead of build/
|
||||||
|
modifyOclifPaths();
|
||||||
|
// Undo changes on exit
|
||||||
|
process.on('exit', function () {
|
||||||
|
modifyOclifPaths(true);
|
||||||
|
});
|
||||||
|
// Undo changes in case of ctrl-c
|
||||||
|
process.on('SIGINT', function () {
|
||||||
|
modifyOclifPaths(true);
|
||||||
|
// Note process exit here will interfere with commands that do their own SIGINT handling,
|
||||||
|
// but without it commands can not be exited.
|
||||||
|
// So currently using balena-dev does not guarantee proper exit behaviour when using ctrl-c.
|
||||||
|
// Ideally a better solution is needed.
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the desired es version for downstream modules that support it
|
||||||
|
require('@balena/es-version').set('es2018');
|
||||||
|
|
||||||
|
// Note: before ts-node v6.0.0, 'transpile-only' (no type checking) was the
|
||||||
|
// default option. We upgraded ts-node and found that adding 'transpile-only'
|
||||||
|
// was necessary to avoid a mysterious 'null' error message. On the plus side,
|
||||||
|
// it is supposed to run faster. We still benefit from type checking when
|
||||||
|
// running 'npm run build'.
|
||||||
|
require('ts-node').register({
|
||||||
|
project: path.join(rootDir, 'tsconfig.json'),
|
||||||
|
transpileOnly: true,
|
||||||
|
});
|
||||||
|
require('../lib/app').run();
|
||||||
|
|
||||||
|
// Modify package.json oclif paths from build/ -> lib/, or vice versa
|
||||||
|
function modifyOclifPaths(revert) {
|
||||||
|
const fs = require('fs');
|
||||||
|
const packageJsonPath = path.join(rootDir, 'package.json');
|
||||||
|
|
||||||
|
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
|
||||||
|
const packageObj = JSON.parse(packageJson);
|
||||||
|
|
||||||
|
if (!packageObj.oclif) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let oclifSectionText = JSON.stringify(packageObj.oclif);
|
||||||
|
if (!revert) {
|
||||||
|
oclifSectionText = oclifSectionText.replace(/\/build\//g, '/lib/');
|
||||||
|
} else {
|
||||||
|
oclifSectionText = oclifSectionText.replace(/\/lib\//g, '/build/');
|
||||||
|
}
|
||||||
|
|
||||||
|
packageObj.oclif = JSON.parse(oclifSectionText);
|
||||||
|
fs.writeFileSync(
|
||||||
|
packageJsonPath,
|
||||||
|
`${JSON.stringify(packageObj, null, 2)}\n`,
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
@echo off
|
|
||||||
|
|
||||||
node "%~dp0\run" %*
|
|
90
bin/dev.js
90
bin/dev.js
@ -1,90 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// ****************************************************************************
|
|
||||||
// THIS IS FOR DEV PURPOSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
|
|
||||||
// Before opening a PR you should build and test your changes using bin/balena
|
|
||||||
// ****************************************************************************
|
|
||||||
|
|
||||||
// We boost the threadpool size as ext2fs can deadlock with some
|
|
||||||
// operations otherwise, if the pool runs out.
|
|
||||||
process.env.UV_THREADPOOL_SIZE = '64';
|
|
||||||
|
|
||||||
// Note on `fast-boot2`: We do not use `fast-boot2` with `balena-dev` because:
|
|
||||||
// * fast-boot2's cacheKiller option is configured to include the timestamps of
|
|
||||||
// the package.json and npm-shrinkwrap.json files, to avoid unexpected CLI
|
|
||||||
// behavior when changes are made to dependencies during development. This is
|
|
||||||
// generally a good thing, however, `balena-dev` (a few lines below) edits
|
|
||||||
// `package.json` to modify oclif paths, and this results in cache
|
|
||||||
// invalidation and a performance hit rather than speedup.
|
|
||||||
// * Even if the timestamps are removed from cacheKiller, so that there is no
|
|
||||||
// cache invalidation, fast-boot's speedup is barely noticeable when ts-node
|
|
||||||
// is used, e.g. 1.43s vs 1.4s when running `balena version`.
|
|
||||||
// * `fast-boot` causes unexpected behavior when used with `npm link` or
|
|
||||||
// when the `node_modules` folder is manually modified (affecting transitive
|
|
||||||
// dependencies) during development (e.g. bug investigations). A workaround
|
|
||||||
// is to use `balena-dev` without `fast-boot`. See also notes in
|
|
||||||
// `CONTRIBUTING.md`.
|
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
const rootDir = path.join(__dirname, '..');
|
|
||||||
|
|
||||||
// Allow balena-dev to work with oclif by temporarily
|
|
||||||
// pointing oclif config options to src/ instead of build/
|
|
||||||
modifyOclifPaths();
|
|
||||||
// Undo changes on exit
|
|
||||||
process.on('exit', function () {
|
|
||||||
modifyOclifPaths(true);
|
|
||||||
});
|
|
||||||
// Undo changes in case of ctrl-c
|
|
||||||
process.on('SIGINT', function () {
|
|
||||||
modifyOclifPaths(true);
|
|
||||||
// Note process exit here will interfere with commands that do their own SIGINT handling,
|
|
||||||
// but without it commands can not be exited.
|
|
||||||
// So currently using balena-dev does not guarantee proper exit behaviour when using ctrl-c.
|
|
||||||
// Ideally a better solution is needed.
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set the desired es version for downstream modules that support it
|
|
||||||
require('@balena/es-version').set('es2018');
|
|
||||||
|
|
||||||
// Note: before ts-node v6.0.0, 'transpile-only' (no type checking) was the
|
|
||||||
// default option. We upgraded ts-node and found that adding 'transpile-only'
|
|
||||||
// was necessary to avoid a mysterious 'null' error message. On the plus side,
|
|
||||||
// it is supposed to run faster. We still benefit from type checking when
|
|
||||||
// running 'npm run build'.
|
|
||||||
require('ts-node').register({
|
|
||||||
project: path.join(rootDir, 'tsconfig.json'),
|
|
||||||
transpileOnly: true,
|
|
||||||
});
|
|
||||||
void require('../src/app').run(undefined, {
|
|
||||||
dir: __dirname,
|
|
||||||
development: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Modify package.json oclif paths from build/ -> src/, or vice versa
|
|
||||||
function modifyOclifPaths(revert) {
|
|
||||||
const fs = require('fs');
|
|
||||||
const packageJsonPath = path.join(rootDir, 'package.json');
|
|
||||||
|
|
||||||
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
|
|
||||||
const packageObj = JSON.parse(packageJson);
|
|
||||||
|
|
||||||
if (!packageObj.oclif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let oclifSectionText = JSON.stringify(packageObj.oclif);
|
|
||||||
if (!revert) {
|
|
||||||
oclifSectionText = oclifSectionText.replace(/\/build\//g, '/src/');
|
|
||||||
} else {
|
|
||||||
oclifSectionText = oclifSectionText.replace(/\/src\//g, '/build/');
|
|
||||||
}
|
|
||||||
|
|
||||||
packageObj.oclif = JSON.parse(oclifSectionText);
|
|
||||||
fs.writeFileSync(
|
|
||||||
packageJsonPath,
|
|
||||||
`${JSON.stringify(packageObj, null, 2)}\n`,
|
|
||||||
'utf8',
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
@echo off
|
|
||||||
|
|
||||||
node "%~dp0\run" %*
|
|
21
bin/run.js
21
bin/run.js
@ -1,21 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// We boost the threadpool size as ext2fs can deadlock with some
|
|
||||||
// operations otherwise, if the pool runs out.
|
|
||||||
process.env.UV_THREADPOOL_SIZE = '64';
|
|
||||||
|
|
||||||
// Disable oclif registering ts-node
|
|
||||||
process.env.OCLIF_TS_NODE = '0';
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
// Use fast-boot to cache require lookups, speeding up startup
|
|
||||||
await require('../build/fast-boot').start();
|
|
||||||
|
|
||||||
// Set the desired es version for downstream modules that support it
|
|
||||||
require('@balena/es-version').set('es2018');
|
|
||||||
|
|
||||||
// Run the CLI
|
|
||||||
await require('../build/app').run(undefined, { dir: __dirname });
|
|
||||||
}
|
|
||||||
|
|
||||||
void run();
|
|
@ -8,28 +8,25 @@ _balena() {
|
|||||||
local context state line curcontext="$curcontext"
|
local context state line curcontext="$curcontext"
|
||||||
|
|
||||||
# Valid top-level completions
|
# Valid top-level completions
|
||||||
main_commands=( api-key app block build config deploy device device-type env fleet internal join leave local login logout organization os preload push release settings ssh-key support tag util version whoami )
|
main_commands=( build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util )
|
||||||
# Sub-completions
|
# Sub-completions
|
||||||
api_key_cmds=( generate list revoke )
|
api_key_cmds=( generate )
|
||||||
app_cmds=( create )
|
|
||||||
block_cmds=( create )
|
|
||||||
config_cmds=( generate inject read reconfigure write )
|
config_cmds=( generate inject read reconfigure write )
|
||||||
device_type_cmds=( list )
|
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 detect identify init list local-mode logs move note os-update pin public-url purge reboot register rename restart rm shutdown ssh start-service stop-service track-fleet tunnel )
|
devices_cmds=( supported )
|
||||||
env_cmds=( list rename rm set )
|
env_cmds=( add rename rm )
|
||||||
fleet_cmds=( create list pin purge rename restart rm track-latest )
|
fleet_cmds=( create pin purge rename restart rm track-latest )
|
||||||
internal_cmds=( osinit )
|
internal_cmds=( osinit )
|
||||||
|
key_cmds=( add rm )
|
||||||
local_cmds=( configure flash )
|
local_cmds=( configure flash )
|
||||||
organization_cmds=( list )
|
|
||||||
os_cmds=( build-config configure download initialize versions )
|
os_cmds=( build-config configure download initialize versions )
|
||||||
release_cmds=( finalize invalidate list validate )
|
release_cmds=( finalize invalidate validate )
|
||||||
ssh_key_cmds=( add list rm )
|
tag_cmds=( rm set )
|
||||||
tag_cmds=( list rm set )
|
|
||||||
|
|
||||||
|
|
||||||
_arguments -C \
|
_arguments -C \
|
||||||
'(- 1 *)--version[show version and exit]' \
|
'(- 1 *)--version[show version and exit]' \
|
||||||
'(- 1 *)--help[show help options and exit]' \
|
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||||
'1:first command:_balena_main_cmds' \
|
'1:first command:_balena_main_cmds' \
|
||||||
'2:second command:_balena_sec_cmds' \
|
'2:second command:_balena_sec_cmds' \
|
||||||
&& ret=0
|
&& ret=0
|
||||||
@ -46,21 +43,15 @@ _balena_sec_cmds() {
|
|||||||
"api-key")
|
"api-key")
|
||||||
_describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0
|
_describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
"app")
|
|
||||||
_describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0
|
|
||||||
;;
|
|
||||||
"block")
|
|
||||||
_describe -t block_cmds 'block_cmd' block_cmds "$@" && ret=0
|
|
||||||
;;
|
|
||||||
"config")
|
"config")
|
||||||
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
|
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
"device-type")
|
|
||||||
_describe -t device_type_cmds 'device-type_cmd' device_type_cmds "$@" && ret=0
|
|
||||||
;;
|
|
||||||
"device")
|
"device")
|
||||||
_describe -t device_cmds 'device_cmd' device_cmds "$@" && ret=0
|
_describe -t device_cmds 'device_cmd' device_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
|
"devices")
|
||||||
|
_describe -t devices_cmds 'devices_cmd' devices_cmds "$@" && ret=0
|
||||||
|
;;
|
||||||
"env")
|
"env")
|
||||||
_describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0
|
_describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
@ -70,21 +61,18 @@ _balena_sec_cmds() {
|
|||||||
"internal")
|
"internal")
|
||||||
_describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0
|
_describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
|
"key")
|
||||||
|
_describe -t key_cmds 'key_cmd' key_cmds "$@" && ret=0
|
||||||
|
;;
|
||||||
"local")
|
"local")
|
||||||
_describe -t local_cmds 'local_cmd' local_cmds "$@" && ret=0
|
_describe -t local_cmds 'local_cmd' local_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
"organization")
|
|
||||||
_describe -t organization_cmds 'organization_cmd' organization_cmds "$@" && ret=0
|
|
||||||
;;
|
|
||||||
"os")
|
"os")
|
||||||
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
|
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
"release")
|
"release")
|
||||||
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
|
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
"ssh-key")
|
|
||||||
_describe -t ssh_key_cmds 'ssh-key_cmd' ssh_key_cmds "$@" && ret=0
|
|
||||||
;;
|
|
||||||
"tag")
|
"tag")
|
||||||
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
|
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
|
@ -7,23 +7,20 @@ _balena_complete()
|
|||||||
local cur prev
|
local cur prev
|
||||||
|
|
||||||
# Valid top-level completions
|
# Valid top-level completions
|
||||||
main_commands="api-key app block build config deploy device device-type env fleet internal join leave local login logout organization os preload push release settings ssh-key support tag util version whoami"
|
main_commands="build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util"
|
||||||
# Sub-completions
|
# Sub-completions
|
||||||
api_key_cmds="generate list revoke"
|
api_key_cmds="generate"
|
||||||
app_cmds="create"
|
|
||||||
block_cmds="create"
|
|
||||||
config_cmds="generate inject read reconfigure write"
|
config_cmds="generate inject read reconfigure write"
|
||||||
device_type_cmds="list"
|
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 detect identify init list local-mode logs move note os-update pin public-url purge reboot register rename restart rm shutdown ssh start-service stop-service track-fleet tunnel"
|
devices_cmds="supported"
|
||||||
env_cmds="list rename rm set"
|
env_cmds="add rename rm"
|
||||||
fleet_cmds="create list pin purge rename restart rm track-latest"
|
fleet_cmds="create pin purge rename restart rm track-latest"
|
||||||
internal_cmds="osinit"
|
internal_cmds="osinit"
|
||||||
|
key_cmds="add rm"
|
||||||
local_cmds="configure flash"
|
local_cmds="configure flash"
|
||||||
organization_cmds="list"
|
|
||||||
os_cmds="build-config configure download initialize versions"
|
os_cmds="build-config configure download initialize versions"
|
||||||
release_cmds="finalize invalidate list validate"
|
release_cmds="finalize invalidate validate"
|
||||||
ssh_key_cmds="add list rm"
|
tag_cmds="rm set"
|
||||||
tag_cmds="list rm set"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -40,21 +37,15 @@ _balena_complete()
|
|||||||
api-key)
|
api-key)
|
||||||
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
app)
|
|
||||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
|
||||||
;;
|
|
||||||
block)
|
|
||||||
COMPREPLY=( $(compgen -W "$block_cmds" -- $cur) )
|
|
||||||
;;
|
|
||||||
config)
|
config)
|
||||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
device-type)
|
|
||||||
COMPREPLY=( $(compgen -W "$device_type_cmds" -- $cur) )
|
|
||||||
;;
|
|
||||||
device)
|
device)
|
||||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
|
devices)
|
||||||
|
COMPREPLY=( $(compgen -W "$devices_cmds" -- $cur) )
|
||||||
|
;;
|
||||||
env)
|
env)
|
||||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
@ -64,21 +55,18 @@ _balena_complete()
|
|||||||
internal)
|
internal)
|
||||||
COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
|
key)
|
||||||
|
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||||
|
;;
|
||||||
local)
|
local)
|
||||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
organization)
|
|
||||||
COMPREPLY=( $(compgen -W "$organization_cmds" -- $cur) )
|
|
||||||
;;
|
|
||||||
os)
|
os)
|
||||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
release)
|
release)
|
||||||
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
ssh-key)
|
|
||||||
COMPREPLY=( $(compgen -W "$ssh_key_cmds" -- $cur) )
|
|
||||||
;;
|
|
||||||
tag)
|
tag)
|
||||||
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
|
@ -31,9 +31,9 @@ if (fs.existsSync(commandsFilePath)) {
|
|||||||
|
|
||||||
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
|
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
|
||||||
|
|
||||||
const mainCommands = [];
|
var mainCommands = [];
|
||||||
const additionalCommands = [];
|
var additionalCommands = [];
|
||||||
for (const key of Object.keys(commandsJson.commands).sort()) {
|
for (const key of Object.keys(commandsJson.commands)) {
|
||||||
const cmd = key.split(':');
|
const cmd = key.split(':');
|
||||||
if (cmd.length > 1) {
|
if (cmd.length > 1) {
|
||||||
additionalCommands.push(cmd);
|
additionalCommands.push(cmd);
|
||||||
@ -72,8 +72,8 @@ fs.readFile(bashFilePathIn, 'utf8', function (err, data) {
|
|||||||
/\$main_commands\$/g,
|
/\$main_commands\$/g,
|
||||||
'main_commands="' + mainCommandsStr + '"',
|
'main_commands="' + mainCommandsStr + '"',
|
||||||
);
|
);
|
||||||
let subCommands = [];
|
var subCommands = [];
|
||||||
let prevElement = additionalCommands[0][0];
|
var prevElement = additionalCommands[0][0];
|
||||||
additionalCommands.forEach(function (element) {
|
additionalCommands.forEach(function (element) {
|
||||||
if (element[0] === prevElement) {
|
if (element[0] === prevElement) {
|
||||||
subCommands.push(element[1]);
|
subCommands.push(element[1]);
|
||||||
@ -134,8 +134,8 @@ fs.readFile(zshFilePathIn, 'utf8', function (err, data) {
|
|||||||
/\$main_commands\$/g,
|
/\$main_commands\$/g,
|
||||||
'main_commands=( ' + mainCommandsStr + ' )',
|
'main_commands=( ' + mainCommandsStr + ' )',
|
||||||
);
|
);
|
||||||
let subCommands = [];
|
var subCommands = [];
|
||||||
let prevElement = additionalCommands[0][0];
|
var prevElement = additionalCommands[0][0];
|
||||||
additionalCommands.forEach(function (element) {
|
additionalCommands.forEach(function (element) {
|
||||||
if (element[0] === prevElement) {
|
if (element[0] === prevElement) {
|
||||||
subCommands.push(element[1]);
|
subCommands.push(element[1]);
|
||||||
|
@ -14,7 +14,7 @@ $sub_cmds$
|
|||||||
|
|
||||||
_arguments -C \
|
_arguments -C \
|
||||||
'(- 1 *)--version[show version and exit]' \
|
'(- 1 *)--version[show version and exit]' \
|
||||||
'(- 1 *)--help[show help options and exit]' \
|
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||||
'1:first command:_balena_main_cmds' \
|
'1:first command:_balena_main_cmds' \
|
||||||
'2:second command:_balena_sec_cmds' \
|
'2:second command:_balena_sec_cmds' \
|
||||||
&& ret=0
|
&& ret=0
|
||||||
|
5263
docs/balena-cli.md
5263
docs/balena-cli.md
File diff suppressed because it is too large
Load Diff
@ -1,32 +0,0 @@
|
|||||||
const { FlatCompat } = require('@eslint/eslintrc');
|
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: __dirname,
|
|
||||||
});
|
|
||||||
module.exports = [
|
|
||||||
...require('@balena/lint/config/eslint.config'),
|
|
||||||
...compat.config({
|
|
||||||
parserOptions: {
|
|
||||||
project: 'tsconfig.dev.json',
|
|
||||||
},
|
|
||||||
ignorePatterns: ['**/generate-completion.js', '**/bin/**/*'],
|
|
||||||
rules: {
|
|
||||||
ignoreDefinitionFiles: 0,
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
||||||
'@typescript-eslint/no-shadow': 'off',
|
|
||||||
'@typescript-eslint/no-var-requires': 'off',
|
|
||||||
'@typescript-eslint/no-require-imports': 'off',
|
|
||||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
|
||||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
|
||||||
|
|
||||||
'no-restricted-imports': ['error', {
|
|
||||||
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
|
|
||||||
}],
|
|
||||||
|
|
||||||
'@typescript-eslint/no-unused-vars': ['error', {
|
|
||||||
argsIgnorePattern: '^_',
|
|
||||||
caughtErrorsIgnorePattern: '^_',
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
@ -16,15 +16,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as packageJSON from '../package.json';
|
import * as packageJSON from '../package.json';
|
||||||
import type { AppOptions } from './preparser';
|
|
||||||
import {
|
import {
|
||||||
|
AppOptions,
|
||||||
checkDeletedCommand,
|
checkDeletedCommand,
|
||||||
preparseArgs,
|
preparseArgs,
|
||||||
unsupportedFlag,
|
unsupportedFlag,
|
||||||
} from './preparser';
|
} from './preparser';
|
||||||
import { CliSettings } from './utils/bootstrap';
|
import { CliSettings } from './utils/bootstrap';
|
||||||
import { onceAsync } from './utils/lazy';
|
import { onceAsync } from './utils/lazy';
|
||||||
import { run as mainRun, settings } from '@oclif/core';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sentry.io setup
|
* Sentry.io setup
|
||||||
@ -101,9 +100,11 @@ async function init() {
|
|||||||
|
|
||||||
/** Execute the oclif parser and the CLI command. */
|
/** Execute the oclif parser and the CLI command. */
|
||||||
async function oclifRun(command: string[], options: AppOptions) {
|
async function oclifRun(command: string[], options: AppOptions) {
|
||||||
let deprecationPromise: Promise<void> | undefined;
|
let deprecationPromise: Promise<void>;
|
||||||
// check and enforce the CLI's deprecation policy
|
// check and enforce the CLI's deprecation policy
|
||||||
if (!(unsupportedFlag || process.env.BALENARC_UNSUPPORTED)) {
|
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
|
||||||
|
deprecationPromise = Promise.resolve();
|
||||||
|
} else {
|
||||||
const { DeprecationChecker } = await import('./deprecation');
|
const { DeprecationChecker } = await import('./deprecation');
|
||||||
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
||||||
// warnAndAbortIfDeprecated uses previously cached data only
|
// warnAndAbortIfDeprecated uses previously cached data only
|
||||||
@ -113,16 +114,10 @@ async function oclifRun(command: string[], options: AppOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const runPromise = (async function (shouldFlush: boolean) {
|
const runPromise = (async function (shouldFlush: boolean) {
|
||||||
|
const { CustomMain } = await import('./utils/oclif-utils');
|
||||||
let isEEXIT = false;
|
let isEEXIT = false;
|
||||||
try {
|
try {
|
||||||
if (options.development) {
|
await CustomMain.run(command);
|
||||||
// In dev mode -> use ts-node and dev plugins
|
|
||||||
process.env.NODE_ENV = 'development';
|
|
||||||
settings.debug = true;
|
|
||||||
}
|
|
||||||
// For posteriority: We can't use default oclif 'execute' as
|
|
||||||
// We customize error handling and flushing
|
|
||||||
await mainRun(command, options.loadOptions ?? options.dir);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
|
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
|
||||||
// for example the `balena help` command.
|
// for example the `balena help` command.
|
||||||
@ -135,8 +130,7 @@ async function oclifRun(command: string[], options: AppOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldFlush) {
|
if (shouldFlush) {
|
||||||
const { flush } = await import('@oclif/core');
|
await import('@oclif/command/flush');
|
||||||
await flush();
|
|
||||||
}
|
}
|
||||||
// TODO: figure out why we need to call fast-boot stop() here, in
|
// TODO: figure out why we need to call fast-boot stop() here, in
|
||||||
// addition to calling it in the main `run()` function in this file.
|
// addition to calling it in the main `run()` function in this file.
|
||||||
@ -151,13 +145,13 @@ async function oclifRun(command: string[], options: AppOptions) {
|
|||||||
}
|
}
|
||||||
})(!options.noFlush);
|
})(!options.noFlush);
|
||||||
|
|
||||||
const { trackPromise } = await import('./hooks/prerun');
|
const { trackPromise } = await import('./hooks/prerun/track');
|
||||||
|
|
||||||
await Promise.all([trackPromise, deprecationPromise, runPromise]);
|
await Promise.all([trackPromise, deprecationPromise, runPromise]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CLI entrypoint. Called by the `bin/run.js` and `bin/dev.js` scripts. */
|
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
||||||
export async function run(cliArgs = process.argv, options: AppOptions) {
|
export async function run(cliArgs = process.argv, options: AppOptions = {}) {
|
||||||
try {
|
try {
|
||||||
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
|
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
|
||||||
'./utils/bootstrap'
|
'./utils/bootstrap'
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
174
lib/command.ts
Normal file
174
lib/command.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* @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 Command from '@oclif/command';
|
||||||
|
import {
|
||||||
|
InsufficientPrivilegesError,
|
||||||
|
NotAvailableInOfflineModeError,
|
||||||
|
} from './errors';
|
||||||
|
import { stripIndent } from './utils/lazy';
|
||||||
|
import * as output from './framework/output';
|
||||||
|
|
||||||
|
export default abstract class BalenaCommand extends Command {
|
||||||
|
/**
|
||||||
|
* When set to true, command will be listed in `help`,
|
||||||
|
* otherwise listed in `help --verbose` with secondary commands.
|
||||||
|
*/
|
||||||
|
public static primary = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require elevated privileges to run.
|
||||||
|
* When set to true, command will exit with an error
|
||||||
|
* if executed without root on Mac/Linux
|
||||||
|
* or if executed by non-Administrator on Windows.
|
||||||
|
*/
|
||||||
|
public static root = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require authentication to run.
|
||||||
|
* When set to true, command will exit with an error
|
||||||
|
* if user is not already logged in.
|
||||||
|
*/
|
||||||
|
public static authenticated = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require an internet connection to run.
|
||||||
|
* When set to true, command will exit with an error
|
||||||
|
* if user is running in offline mode (BALENARC_OFFLINE_MODE).
|
||||||
|
*/
|
||||||
|
public static offlineCompatible = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept piped input.
|
||||||
|
* When set to true, command will read from stdin during init
|
||||||
|
* and make contents available on member `stdin`.
|
||||||
|
*/
|
||||||
|
public static readStdin = false;
|
||||||
|
|
||||||
|
public stdin: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw InsufficientPrivilegesError if not root on Mac/Linux
|
||||||
|
* or non-Administrator on Windows.
|
||||||
|
*
|
||||||
|
* Called automatically if `root=true`.
|
||||||
|
* Can be called explicitly by command implementation, if e.g.:
|
||||||
|
* - check should only be done conditionally
|
||||||
|
* - other code needs to execute before check
|
||||||
|
*/
|
||||||
|
protected static async checkElevatedPrivileges() {
|
||||||
|
const isElevated = await (await import('is-elevated'))();
|
||||||
|
if (!isElevated) {
|
||||||
|
throw new InsufficientPrivilegesError(
|
||||||
|
'You need root/admin privileges to run this command',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw NotLoggedInError if not logged in.
|
||||||
|
*
|
||||||
|
* Called automatically if `authenticated=true`.
|
||||||
|
* Can be called explicitly by command implementation, if e.g.:
|
||||||
|
* - check should only be done conditionally
|
||||||
|
* - other code needs to execute before check
|
||||||
|
*
|
||||||
|
* Note, currently public to allow use outside of derived commands
|
||||||
|
* (as some command implementations require this. Can be made protected
|
||||||
|
* if this changes).
|
||||||
|
*
|
||||||
|
* @throws {NotLoggedInError}
|
||||||
|
*/
|
||||||
|
public static async checkLoggedIn() {
|
||||||
|
await (await import('./utils/patterns')).checkLoggedIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw NotLoggedInError if not logged in when condition true.
|
||||||
|
*
|
||||||
|
* @param {boolean} doCheck - will check if true.
|
||||||
|
* @throws {NotLoggedInError}
|
||||||
|
*/
|
||||||
|
public static async checkLoggedInIf(doCheck: boolean) {
|
||||||
|
if (doCheck) {
|
||||||
|
await this.checkLoggedIn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw NotAvailableInOfflineModeError if in offline mode.
|
||||||
|
*
|
||||||
|
* Called automatically if `onlineOnly=true`.
|
||||||
|
* Can be called explicitly by command implementation, if e.g.:
|
||||||
|
* - check should only be done conditionally
|
||||||
|
* - other code needs to execute before check
|
||||||
|
*
|
||||||
|
* Note, currently public to allow use outside of derived commands
|
||||||
|
* (as some command implementations require this. Can be made protected
|
||||||
|
* if this changes).
|
||||||
|
*
|
||||||
|
* @throws {NotAvailableInOfflineModeError}
|
||||||
|
*/
|
||||||
|
public static checkNotUsingOfflineMode() {
|
||||||
|
if (process.env.BALENARC_OFFLINE_MODE) {
|
||||||
|
throw new NotAvailableInOfflineModeError(stripIndent`
|
||||||
|
This command requires an internet connection, and cannot be used in offline mode.
|
||||||
|
To leave offline mode, unset the BALENARC_OFFLINE_MODE environment variable.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read stdin contents and make available to command.
|
||||||
|
*
|
||||||
|
* This approach could be improved in the future to automatically set argument
|
||||||
|
* values from stdin based in configuration, minimising command implementation.
|
||||||
|
*/
|
||||||
|
protected async getStdin() {
|
||||||
|
this.stdin = await (await import('get-stdin'))();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a logger instance.
|
||||||
|
*/
|
||||||
|
protected static async getLogger() {
|
||||||
|
return (await import('./utils/logger')).getLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async init() {
|
||||||
|
const ctr = this.constructor as typeof BalenaCommand;
|
||||||
|
|
||||||
|
if (ctr.root) {
|
||||||
|
await BalenaCommand.checkElevatedPrivileges();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctr.authenticated) {
|
||||||
|
await BalenaCommand.checkLoggedIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctr.offlineCompatible) {
|
||||||
|
BalenaCommand.checkNotUsingOfflineMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctr.readStdin) {
|
||||||
|
await this.getStdin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected outputMessage = output.outputMessage;
|
||||||
|
protected outputData = output.outputData;
|
||||||
|
}
|
85
lib/commands/api-key/generate.ts
Normal file
85
lib/commands/api-key/generate.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* @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 { ExpectedError } from '../../errors';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class GenerateCmd extends Command {
|
||||||
|
public static description = stripIndent`
|
||||||
|
Generate a new balenaCloud API key.
|
||||||
|
|
||||||
|
Generate a new balenaCloud API key for the current user, with the given
|
||||||
|
name. The key will be logged to the console.
|
||||||
|
|
||||||
|
This key can be used to log into the CLI using 'balena login --token <key>',
|
||||||
|
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||||
|
`;
|
||||||
|
public static examples = ['$ balena api-key generate "Jenkins Key"'];
|
||||||
|
|
||||||
|
public static args = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
description: 'the API key name',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'api-key generate <name>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static authenticated = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(GenerateCmd);
|
||||||
|
|
||||||
|
let key;
|
||||||
|
try {
|
||||||
|
key = await getBalenaSdk().models.apiKey.create(params.name);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name === 'BalenaNotLoggedIn') {
|
||||||
|
throw new ExpectedError(stripIndent`
|
||||||
|
This command cannot be run when logged in with an API key.
|
||||||
|
Please login again with 'balena login' and select an alternative method.
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(stripIndent`
|
||||||
|
Registered api key '${params.name}':
|
||||||
|
|
||||||
|
${key}
|
||||||
|
|
||||||
|
This key will not be shown again, so please save it now.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
@ -15,36 +15,31 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Flags, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
import { getBalenaSdk } from '../../utils/lazy';
|
import Command from '../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import { getBalenaSdk } from '../utils/lazy';
|
||||||
import * as compose from '../../utils/compose';
|
import * as cf from '../utils/common-flags';
|
||||||
import type {
|
import * as compose from '../utils/compose';
|
||||||
ApplicationType,
|
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
||||||
BalenaSDK,
|
|
||||||
DeviceType,
|
|
||||||
PineOptions,
|
|
||||||
PineTypedResult,
|
|
||||||
} from 'balena-sdk';
|
|
||||||
import {
|
import {
|
||||||
buildArgDeprecation,
|
buildArgDeprecation,
|
||||||
dockerignoreHelp,
|
dockerignoreHelp,
|
||||||
registrySecretsHelp,
|
registrySecretsHelp,
|
||||||
} from '../../utils/messages';
|
} from '../utils/messages';
|
||||||
import type { ComposeCliFlags, ComposeOpts } from '../../utils/compose-types';
|
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
|
||||||
import { buildProject, composeCliFlags } from '../../utils/compose_ts';
|
import { buildProject, composeCliFlags } from '../utils/compose_ts';
|
||||||
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
||||||
import { dockerCliFlags } from '../../utils/docker';
|
import { dockerCliFlags } from '../utils/docker';
|
||||||
|
|
||||||
type ComposeGenerateOptsParam = Parameters<typeof compose.generateOpts>[0];
|
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||||
|
|
||||||
interface PrepareBuildOpts
|
|
||||||
extends ComposeCliFlags,
|
|
||||||
DockerCliFlags,
|
|
||||||
ComposeGenerateOptsParam {
|
|
||||||
arch?: string;
|
arch?: string;
|
||||||
deviceType?: string;
|
deviceType?: string;
|
||||||
fleet?: string;
|
fleet?: string;
|
||||||
|
source?: string; // Not part of command profile - source param copied here.
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
source?: string;
|
source?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,73 +68,75 @@ ${dockerignoreHelp}
|
|||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena build --fleet myFleet',
|
'$ balena build --fleet myFleet',
|
||||||
'$ balena build ./source/ --fleet myorg/myfleet',
|
'$ balena build ./source/ --fleet myorg/myfleet',
|
||||||
'$ balena build --deviceType raspberrypi3 --emulated',
|
|
||||||
'$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated',
|
'$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated',
|
||||||
'$ balena build --docker /var/run/docker.sock --fleet myFleet # Linux, Mac',
|
'$ balena build --docker /var/run/docker.sock --fleet myFleet # Linux, Mac',
|
||||||
'$ balena build --docker //./pipe/docker_engine --fleet myFleet # Windows',
|
'$ balena build --docker //./pipe/docker_engine --fleet myFleet # Windows',
|
||||||
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
|
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [
|
||||||
source: Args.string({ description: 'path of project source directory' }),
|
{
|
||||||
};
|
name: 'source',
|
||||||
|
description: 'path of project source directory',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'build [source]';
|
||||||
arch: Flags.string({
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
arch: flags.string({
|
||||||
description: 'the architecture to build for',
|
description: 'the architecture to build for',
|
||||||
char: 'A',
|
char: 'A',
|
||||||
}),
|
}),
|
||||||
deviceType: Flags.string({
|
deviceType: flags.string({
|
||||||
description: 'the type of device this build is for',
|
description: 'the type of device this build is for',
|
||||||
char: 'd',
|
char: 'd',
|
||||||
}),
|
}),
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
...composeCliFlags,
|
...composeCliFlags,
|
||||||
...dockerCliFlags,
|
...dockerCliFlags,
|
||||||
|
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||||
|
// Revisit this in future release.
|
||||||
|
help: flags.help({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(BuildCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
BuildCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const Logger = await import('../../utils/logger');
|
await Command.checkLoggedInIf(!!options.fleet);
|
||||||
const { checkLoggedInIf } = await import('../../utils/patterns');
|
|
||||||
|
|
||||||
await checkLoggedInIf(!!options.fleet);
|
|
||||||
|
|
||||||
(await import('events')).defaultMaxListeners = 1000;
|
(await import('events')).defaultMaxListeners = 1000;
|
||||||
|
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
|
|
||||||
const logger = Logger.getLogger();
|
const logger = await Command.getLogger();
|
||||||
logger.logDebug('Parsing input...');
|
logger.logDebug('Parsing input...');
|
||||||
|
|
||||||
const prepareBuildOpts = {
|
// `build` accepts `source` as a parameter, but compose expects it as an option
|
||||||
...options,
|
options.source = params.source;
|
||||||
source: params.source,
|
delete params.source;
|
||||||
};
|
|
||||||
|
|
||||||
await this.resolveArchFromDeviceType(sdk, prepareBuildOpts);
|
await this.validateOptions(options, sdk);
|
||||||
|
|
||||||
await this.validateOptions(prepareBuildOpts, sdk);
|
|
||||||
|
|
||||||
// Build args are under consideration for removal - warn user
|
// Build args are under consideration for removal - warn user
|
||||||
if (prepareBuildOpts.buildArg) {
|
if (options.buildArg) {
|
||||||
console.log(buildArgDeprecation);
|
console.log(buildArgDeprecation);
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = await this.getAppAndResolveArch(prepareBuildOpts);
|
const app = await this.getAppAndResolveArch(options);
|
||||||
|
|
||||||
const { docker, buildOpts, composeOpts } =
|
const { docker, buildOpts, composeOpts } = await this.prepareBuild(options);
|
||||||
await this.prepareBuild(prepareBuildOpts);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.buildProject(docker, logger, composeOpts, {
|
await this.buildProject(docker, logger, composeOpts, {
|
||||||
appType: app?.application_type?.[0],
|
app,
|
||||||
arch: prepareBuildOpts.arch!,
|
arch: options.arch!,
|
||||||
deviceType: prepareBuildOpts.deviceType!,
|
deviceType: options.deviceType!,
|
||||||
buildEmulated: prepareBuildOpts.emulated,
|
buildEmulated: options.emulated,
|
||||||
buildOpts,
|
buildOpts,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -151,20 +148,20 @@ ${dockerignoreHelp}
|
|||||||
logger.logSuccess('Build succeeded!');
|
logger.logSuccess('Build succeeded!');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async validateOptions(opts: PrepareBuildOpts, sdk: BalenaSDK) {
|
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
|
||||||
// Validate option combinations
|
// Validate option combinations
|
||||||
if (
|
if (
|
||||||
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
||||||
(opts.fleet != null && (opts.arch != null || opts.deviceType != null))
|
(opts.fleet != null && (opts.arch != null || opts.deviceType != null))
|
||||||
) {
|
) {
|
||||||
const { ExpectedError } = await import('../../errors');
|
const { ExpectedError } = await import('../errors');
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
'You must specify either a fleet (-f), or the device type (-d) and optionally the architecture (-A)',
|
'You must specify either a fleet (-f), or the device type (-d) and architecture (-A)',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate project directory
|
// Validate project directory
|
||||||
const { validateProjectDirectory } = await import('../../utils/compose_ts');
|
const { validateProjectDirectory } = await import('../utils/compose_ts');
|
||||||
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
|
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
|
||||||
sdk,
|
sdk,
|
||||||
{
|
{
|
||||||
@ -179,45 +176,9 @@ ${dockerignoreHelp}
|
|||||||
opts['registry-secrets'] = registrySecrets;
|
opts['registry-secrets'] = registrySecrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async resolveArchFromDeviceType(
|
protected async getAppAndResolveArch(opts: FlagsDef) {
|
||||||
sdk: BalenaSDK,
|
|
||||||
opts: PrepareBuildOpts,
|
|
||||||
) {
|
|
||||||
if (opts.deviceType != null && opts.arch == null) {
|
|
||||||
try {
|
|
||||||
const deviceTypeOpts = {
|
|
||||||
$select: 'is_of__cpu_architecture',
|
|
||||||
$expand: {
|
|
||||||
is_of__cpu_architecture: {
|
|
||||||
$select: 'slug',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies PineOptions<DeviceType>;
|
|
||||||
opts.arch = (
|
|
||||||
(await sdk.models.deviceType.get(
|
|
||||||
opts.deviceType,
|
|
||||||
deviceTypeOpts,
|
|
||||||
)) as PineTypedResult<DeviceType, typeof deviceTypeOpts>
|
|
||||||
).is_of__cpu_architecture[0].slug;
|
|
||||||
} catch (err) {
|
|
||||||
const { ExpectedError } = await import('../../errors');
|
|
||||||
if (err instanceof sdk.errors.BalenaInvalidDeviceType) {
|
|
||||||
let message = err.message;
|
|
||||||
if (!(await sdk.auth.isLoggedIn())) {
|
|
||||||
message = `${message}. In case you are trying to use a private device type, please try to log in first.`;
|
|
||||||
}
|
|
||||||
throw new ExpectedError(message);
|
|
||||||
}
|
|
||||||
throw new ExpectedError(
|
|
||||||
'Failed to resolve the architecture of the provided device type. If you are in an air-gapped environment please also define the architecture (-A) parameter.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getAppAndResolveArch(opts: PrepareBuildOpts) {
|
|
||||||
if (opts.fleet) {
|
if (opts.fleet) {
|
||||||
const { getAppWithArch } = await import('../../utils/helpers');
|
const { getAppWithArch } = await import('../utils/helpers');
|
||||||
const app = await getAppWithArch(opts.fleet);
|
const app = await getAppWithArch(opts.fleet);
|
||||||
opts.arch = app.arch;
|
opts.arch = app.arch;
|
||||||
opts.deviceType = app.is_for__device_type[0].slug;
|
opts.deviceType = app.is_for__device_type[0].slug;
|
||||||
@ -225,8 +186,8 @@ ${dockerignoreHelp}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async prepareBuild(options: PrepareBuildOpts) {
|
protected async prepareBuild(options: FlagsDef) {
|
||||||
const { getDocker, generateBuildOpts } = await import('../../utils/docker');
|
const { getDocker, generateBuildOpts } = await import('../utils/docker');
|
||||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||||
getDocker(options),
|
getDocker(options),
|
||||||
generateBuildOpts(options),
|
generateBuildOpts(options),
|
||||||
@ -247,24 +208,24 @@ ${dockerignoreHelp}
|
|||||||
* buildEmulated
|
* buildEmulated
|
||||||
* buildOpts: arguments to forward to docker build command
|
* buildOpts: arguments to forward to docker build command
|
||||||
*
|
*
|
||||||
* @param {Dockerode} docker
|
* @param {DockerToolbelt} docker
|
||||||
* @param {Logger} logger
|
* @param {Logger} logger
|
||||||
* @param {ComposeOpts} composeOpts
|
* @param {ComposeOpts} composeOpts
|
||||||
* @param opts
|
* @param opts
|
||||||
*/
|
*/
|
||||||
protected async buildProject(
|
protected async buildProject(
|
||||||
docker: import('dockerode'),
|
docker: import('dockerode'),
|
||||||
logger: import('../../utils/logger'),
|
logger: import('../utils/logger'),
|
||||||
composeOpts: ComposeOpts,
|
composeOpts: ComposeOpts,
|
||||||
opts: {
|
opts: {
|
||||||
appType?: Pick<ApplicationType, 'supports_multicontainer'>;
|
app?: Application;
|
||||||
arch: string;
|
arch: string;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
buildEmulated: boolean;
|
buildEmulated: boolean;
|
||||||
buildOpts: BuildOpts;
|
buildOpts: BuildOpts;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { loadProject } = await import('../../utils/compose_ts');
|
const { loadProject } = await import('../utils/compose_ts');
|
||||||
|
|
||||||
const project = await loadProject(
|
const project = await loadProject(
|
||||||
logger,
|
logger,
|
||||||
@ -273,10 +234,11 @@ ${dockerignoreHelp}
|
|||||||
opts.buildOpts.t,
|
opts.buildOpts.t,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||||
if (
|
if (
|
||||||
opts.appType != null &&
|
appType != null &&
|
||||||
project.descriptors.length > 1 &&
|
project.descriptors.length > 1 &&
|
||||||
!opts.appType.supports_multicontainer
|
!appType.supports_multicontainer
|
||||||
) {
|
) {
|
||||||
logger.logWarn(
|
logger.logWarn(
|
||||||
'Target fleet does not support multiple containers.\n' +
|
'Target fleet does not support multiple containers.\n' +
|
@ -15,16 +15,31 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flags, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
import type { Interfaces } from '@oclif/core';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||||
import {
|
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
|
||||||
applicationIdInfo,
|
import type { PineDeferred } from 'balena-sdk';
|
||||||
devModeInfo,
|
|
||||||
secureBootInfo,
|
interface FlagsDef {
|
||||||
} from '../../utils/messages';
|
version: string; // OS version
|
||||||
import type { BalenaSDK, PineDeferred } from 'balena-sdk';
|
fleet?: string;
|
||||||
|
dev?: boolean; // balenaOS development variant
|
||||||
|
device?: string;
|
||||||
|
deviceApiKey?: string;
|
||||||
|
deviceType?: string;
|
||||||
|
'generate-device-api-key': boolean;
|
||||||
|
output?: string;
|
||||||
|
// Options for non-interactive configuration
|
||||||
|
network?: string;
|
||||||
|
wifiSsid?: string;
|
||||||
|
wifiKey?: string;
|
||||||
|
appUpdatePollInterval?: string;
|
||||||
|
'provisioning-key-name'?: string;
|
||||||
|
'provisioning-key-expiry-date'?: string;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
export default class ConfigGenerateCmd extends Command {
|
export default class ConfigGenerateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -36,8 +51,6 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
|
|
||||||
${devModeInfo.split('\n').join('\n\t\t')}
|
${devModeInfo.split('\n').join('\n\t\t')}
|
||||||
|
|
||||||
${secureBootInfo.split('\n').join('\n\t\t')}
|
|
||||||
|
|
||||||
To configure an image for a fleet of mixed device types, use the --fleet option
|
To configure an image for a fleet of mixed device types, use the --fleet option
|
||||||
alongside the --deviceType option to specify the target device type.
|
alongside the --deviceType option to specify the target device type.
|
||||||
|
|
||||||
@ -53,20 +66,20 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>',
|
'$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>',
|
||||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
|
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
|
||||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev',
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev',
|
||||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot',
|
|
||||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
|
||||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
|
||||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'config generate';
|
||||||
version: Flags.string({
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
version: flags.string({
|
||||||
description: 'a balenaOS version',
|
description: 'a balenaOS version',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
}),
|
||||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||||
dev: cf.dev,
|
dev: cf.dev,
|
||||||
secureBoot: cf.secureBoot,
|
|
||||||
device: {
|
device: {
|
||||||
...cf.device,
|
...cf.device,
|
||||||
exclusive: [
|
exclusive: [
|
||||||
@ -75,71 +88,64 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
'provisioning-key-expiry-date',
|
'provisioning-key-expiry-date',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
deviceApiKey: Flags.string({
|
deviceApiKey: flags.string({
|
||||||
description:
|
description:
|
||||||
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
||||||
char: 'k',
|
char: 'k',
|
||||||
}),
|
}),
|
||||||
deviceType: Flags.string({
|
deviceType: flags.string({
|
||||||
description:
|
description:
|
||||||
"device type slug (run 'balena device-type list' for possible values)",
|
"device type slug (run 'balena devices supported' for possible values)",
|
||||||
}),
|
}),
|
||||||
'generate-device-api-key': Flags.boolean({
|
'generate-device-api-key': flags.boolean({
|
||||||
description: 'generate a fresh device key for the device',
|
description: 'generate a fresh device key for the device',
|
||||||
}),
|
}),
|
||||||
output: Flags.string({
|
output: flags.string({
|
||||||
description: 'path of output file',
|
description: 'path of output file',
|
||||||
char: 'o',
|
char: 'o',
|
||||||
}),
|
}),
|
||||||
// Options for non-interactive configuration
|
// Options for non-interactive configuration
|
||||||
network: Flags.string({
|
network: flags.string({
|
||||||
description: 'the network type to use: ethernet or wifi',
|
description: 'the network type to use: ethernet or wifi',
|
||||||
options: ['ethernet', 'wifi'],
|
options: ['ethernet', 'wifi'],
|
||||||
}),
|
}),
|
||||||
wifiSsid: Flags.string({
|
wifiSsid: flags.string({
|
||||||
description:
|
description:
|
||||||
'the wifi ssid to use (used only if --network is set to wifi)',
|
'the wifi ssid to use (used only if --network is set to wifi)',
|
||||||
}),
|
}),
|
||||||
wifiKey: Flags.string({
|
wifiKey: flags.string({
|
||||||
description:
|
description:
|
||||||
'the wifi key to use (used only if --network is set to wifi)',
|
'the wifi key to use (used only if --network is set to wifi)',
|
||||||
}),
|
}),
|
||||||
appUpdatePollInterval: Flags.string({
|
appUpdatePollInterval: flags.string({
|
||||||
description:
|
description:
|
||||||
'supervisor cloud polling interval in minutes (e.g. for device variables)',
|
'supervisor cloud polling interval in minutes (e.g. for device variables)',
|
||||||
}),
|
}),
|
||||||
'provisioning-key-name': Flags.string({
|
'provisioning-key-name': flags.string({
|
||||||
description: 'custom key name assigned to generated provisioning api key',
|
description: 'custom key name assigned to generated provisioning api key',
|
||||||
exclusive: ['device'],
|
exclusive: ['device'],
|
||||||
}),
|
}),
|
||||||
'provisioning-key-expiry-date': Flags.string({
|
'provisioning-key-expiry-date': flags.string({
|
||||||
description:
|
description:
|
||||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||||
exclusive: ['device'],
|
exclusive: ['device'],
|
||||||
}),
|
}),
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async getApplication(balena: BalenaSDK, fleet: string) {
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
|
||||||
return await getApplication(balena, fleet, {
|
|
||||||
$select: 'slug',
|
|
||||||
$expand: {
|
|
||||||
is_for__device_type: { $select: 'slug' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = await this.parse(ConfigGenerateCmd);
|
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
|
||||||
|
|
||||||
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
await this.validateOptions(options);
|
await this.validateOptions(options);
|
||||||
|
|
||||||
let resourceDeviceType: string;
|
let resourceDeviceType: string;
|
||||||
let application: Awaited<ReturnType<typeof this.getApplication>> | null =
|
let application: ApplicationWithDeviceType | null = null;
|
||||||
null;
|
|
||||||
let device:
|
let device:
|
||||||
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
||||||
| null = null;
|
| null = null;
|
||||||
@ -159,40 +165,36 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
resourceDeviceType = device.is_of__device_type[0].slug;
|
resourceDeviceType = device.is_of__device_type[0].slug;
|
||||||
} else {
|
} else {
|
||||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||||
application = await this.getApplication(balena, options.fleet!);
|
application = (await getApplication(balena, options.fleet!, {
|
||||||
|
$expand: {
|
||||||
|
is_for__device_type: { $select: 'slug' },
|
||||||
|
},
|
||||||
|
})) as ApplicationWithDeviceType;
|
||||||
resourceDeviceType = application.is_for__device_type[0].slug;
|
resourceDeviceType = application.is_for__device_type[0].slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceType = options.deviceType || resourceDeviceType;
|
const deviceType = options.deviceType || resourceDeviceType;
|
||||||
|
|
||||||
|
const deviceManifest = await balena.models.device.getManifestBySlug(
|
||||||
|
deviceType,
|
||||||
|
);
|
||||||
|
|
||||||
// Check compatibility if application and deviceType provided
|
// Check compatibility if application and deviceType provided
|
||||||
if (options.fleet && options.deviceType) {
|
if (options.fleet && options.deviceType) {
|
||||||
|
const appDeviceManifest = await balena.models.device.getManifestBySlug(
|
||||||
|
resourceDeviceType,
|
||||||
|
);
|
||||||
|
|
||||||
const helpers = await import('../../utils/helpers');
|
const helpers = await import('../../utils/helpers');
|
||||||
if (
|
if (
|
||||||
!(await helpers.areDeviceTypesCompatible(
|
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
|
||||||
resourceDeviceType,
|
|
||||||
deviceType,
|
|
||||||
))
|
|
||||||
) {
|
) {
|
||||||
const { ExpectedError } = await import('../../errors');
|
throw new balena.errors.BalenaInvalidDeviceType(
|
||||||
throw new ExpectedError(
|
|
||||||
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
|
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceManifest =
|
|
||||||
await balena.models.config.getDeviceTypeManifestBySlug(deviceType);
|
|
||||||
|
|
||||||
const { validateSecureBootOptionAndWarn } = await import(
|
|
||||||
'../../utils/config'
|
|
||||||
);
|
|
||||||
await validateSecureBootOptionAndWarn(
|
|
||||||
options.secureBoot,
|
|
||||||
deviceType,
|
|
||||||
options.version,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prompt for values
|
// Prompt for values
|
||||||
// Pass params as an override: if there is any param with exactly the same name as a
|
// 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)
|
// required option, that value is used (and the corresponding question is not asked)
|
||||||
@ -201,7 +203,6 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
});
|
});
|
||||||
answers.version = options.version;
|
answers.version = options.version;
|
||||||
answers.developmentMode = options.dev;
|
answers.developmentMode = options.dev;
|
||||||
answers.secureBoot = options.secureBoot;
|
|
||||||
answers.provisioningKeyName = options['provisioning-key-name'];
|
answers.provisioningKeyName = options['provisioning-key-name'];
|
||||||
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
|
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
|
||||||
|
|
||||||
@ -243,9 +244,7 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
protected readonly deviceTypeNotAllowedMessage =
|
protected readonly deviceTypeNotAllowedMessage =
|
||||||
'The --deviceType option can only be used alongside the --fleet option';
|
'The --deviceType option can only be used alongside the --fleet option';
|
||||||
|
|
||||||
protected async validateOptions(
|
protected async validateOptions(options: FlagsDef) {
|
||||||
options: Interfaces.InferredFlags<typeof ConfigGenerateCmd.flags>,
|
|
||||||
) {
|
|
||||||
const { ExpectedError } = await import('../../errors');
|
const { ExpectedError } = await import('../../errors');
|
||||||
|
|
||||||
if (options.device == null && options.fleet == null) {
|
if (options.device == null && options.fleet == null) {
|
||||||
@ -255,8 +254,6 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
if (!options.fleet && options.deviceType) {
|
if (!options.fleet && options.deviceType) {
|
||||||
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
|
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
|
||||||
}
|
}
|
||||||
const { normalizeOsVersion } = await import('../../utils/normalization');
|
|
||||||
options.version = normalizeOsVersion(options.version);
|
|
||||||
const { validateDevOptionAndWarn } = await import('../../utils/config');
|
const { validateDevOptionAndWarn } = await import('../../utils/config');
|
||||||
await validateDevOptionAndWarn(options.dev, options.version);
|
await validateDevOptionAndWarn(options.dev, options.version);
|
||||||
}
|
}
|
@ -15,10 +15,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
type?: string;
|
||||||
|
drive?: string;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
file: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class ConfigInjectCmd extends Command {
|
export default class ConfigInjectCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Inject a config.json file to a balenaOS image or attached media.
|
Inject a config.json file to a balenaOS image or attached media.
|
||||||
@ -35,22 +46,28 @@ export default class ConfigInjectCmd extends Command {
|
|||||||
'$ balena config inject my/config.json --drive /dev/disk2',
|
'$ balena config inject my/config.json --drive /dev/disk2',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [
|
||||||
file: Args.string({
|
{
|
||||||
|
name: 'file',
|
||||||
description: 'the path to the config.json file to inject',
|
description: 'the path to the config.json file to inject',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'config inject <file>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static root = true;
|
public static root = true;
|
||||||
public static offlineCompatible = true;
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(ConfigInjectCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
ConfigInjectCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const { safeUmount } = await import('../../utils/umount');
|
const { safeUmount } = await import('../../utils/umount');
|
||||||
|
|
||||||
@ -64,12 +81,7 @@ export default class ConfigInjectCmd extends Command {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
const config = await import('balena-config-json');
|
||||||
await config.write(
|
await config.write(drive, '', configJSON);
|
||||||
drive,
|
|
||||||
// Will be removed in the next major of balena-config-json
|
|
||||||
undefined,
|
|
||||||
configJSON,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.info('Done');
|
console.info('Done');
|
||||||
}
|
}
|
@ -15,10 +15,18 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
type?: string;
|
||||||
|
drive?: string;
|
||||||
|
help: void;
|
||||||
|
json: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default class ConfigReadCmd extends Command {
|
export default class ConfigReadCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Read the config.json file of a balenaOS image or attached media.
|
Read the config.json file of a balenaOS image or attached media.
|
||||||
@ -36,8 +44,11 @@ export default class ConfigReadCmd extends Command {
|
|||||||
'$ balena config read --drive balena.img',
|
'$ balena config read --drive balena.img',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'config read';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
|
help: cf.help,
|
||||||
json: cf.json,
|
json: cf.json,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,7 +56,7 @@ export default class ConfigReadCmd extends Command {
|
|||||||
public static offlineCompatible = true;
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = await this.parse(ConfigReadCmd);
|
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
|
||||||
|
|
||||||
const { safeUmount } = await import('../../utils/umount');
|
const { safeUmount } = await import('../../utils/umount');
|
||||||
|
|
||||||
@ -54,7 +65,7 @@ export default class ConfigReadCmd extends Command {
|
|||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
const config = await import('balena-config-json');
|
||||||
const configJSON = await config.read(drive);
|
const configJSON = await config.read(drive, '');
|
||||||
|
|
||||||
if (options.json) {
|
if (options.json) {
|
||||||
console.log(JSON.stringify(configJSON, null, 4));
|
console.log(JSON.stringify(configJSON, null, 4));
|
@ -15,10 +15,19 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flags, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
type?: string;
|
||||||
|
drive?: string;
|
||||||
|
advanced: boolean;
|
||||||
|
help: void;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class ConfigReconfigureCmd extends Command {
|
export default class ConfigReconfigureCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Interactively reconfigure a balenaOS image file or attached media.
|
Interactively reconfigure a balenaOS image file or attached media.
|
||||||
@ -38,13 +47,16 @@ export default class ConfigReconfigureCmd extends Command {
|
|||||||
'$ balena config reconfigure --drive balena.img --advanced',
|
'$ balena config reconfigure --drive balena.img --advanced',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'config reconfigure';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
advanced: Flags.boolean({
|
advanced: flags.boolean({
|
||||||
description: 'show advanced commands',
|
description: 'show advanced commands',
|
||||||
char: 'v',
|
char: 'v',
|
||||||
}),
|
}),
|
||||||
version: Flags.string({
|
help: cf.help,
|
||||||
|
version: flags.string({
|
||||||
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@ -53,7 +65,7 @@ export default class ConfigReconfigureCmd extends Command {
|
|||||||
public static root = true;
|
public static root = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = await this.parse(ConfigReconfigureCmd);
|
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
|
||||||
|
|
||||||
const { safeUmount } = await import('../../utils/umount');
|
const { safeUmount } = await import('../../utils/umount');
|
||||||
|
|
||||||
@ -62,7 +74,7 @@ export default class ConfigReconfigureCmd extends Command {
|
|||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
const config = await import('balena-config-json');
|
||||||
const { uuid } = await config.read(drive);
|
const { uuid } = await config.read(drive, '');
|
||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
|
|
||||||
if (!uuid) {
|
if (!uuid) {
|
@ -15,10 +15,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
type?: string;
|
||||||
|
drive?: string;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class ConfigWriteCmd extends Command {
|
export default class ConfigWriteCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Write a key-value pair to the config.json file of an OS image or attached media.
|
Write a key-value pair to the config.json file of an OS image or attached media.
|
||||||
@ -36,26 +48,33 @@ export default class ConfigWriteCmd extends Command {
|
|||||||
'$ balena config write --drive balena.img os.network.connectivity.interval 300',
|
'$ balena config write --drive balena.img os.network.connectivity.interval 300',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [
|
||||||
key: Args.string({
|
{
|
||||||
|
name: 'key',
|
||||||
description: 'the key of the config parameter to write',
|
description: 'the key of the config parameter to write',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
value: Args.string({
|
{
|
||||||
|
name: 'value',
|
||||||
description: 'the value of the config parameter to write',
|
description: 'the value of the config parameter to write',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'config write <key> <value>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static root = true;
|
public static root = true;
|
||||||
public static offlineCompatible = true;
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(ConfigWriteCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
ConfigWriteCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const { denyMount, safeUmount } = await import('../../utils/umount');
|
const { denyMount, safeUmount } = await import('../../utils/umount');
|
||||||
|
|
||||||
@ -64,19 +83,14 @@ export default class ConfigWriteCmd extends Command {
|
|||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
const config = await import('balena-config-json');
|
||||||
const configJSON = await config.read(drive);
|
const configJSON = await config.read(drive, '');
|
||||||
|
|
||||||
console.info(`Setting ${params.key} to ${params.value}`);
|
console.info(`Setting ${params.key} to ${params.value}`);
|
||||||
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
|
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
|
||||||
|
|
||||||
await denyMount(drive, async () => {
|
await denyMount(drive, async () => {
|
||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
await config.write(
|
await config.write(drive, '', configJSON);
|
||||||
drive,
|
|
||||||
// Will be removed in the next major of balena-config-json
|
|
||||||
undefined,
|
|
||||||
configJSON,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.info('Done');
|
console.info('Done');
|
@ -15,44 +15,45 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Flags, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
import type { ImageDescriptor } from '@balena/compose/dist/parse';
|
import type { ImageDescriptor } from '@balena/compose/dist/parse';
|
||||||
import { ExpectedError } from '../../errors';
|
|
||||||
import { getBalenaSdk, getChalk, stripIndent } from '../../utils/lazy';
|
import Command from '../command';
|
||||||
|
import { ExpectedError } from '../errors';
|
||||||
|
import { getBalenaSdk, getChalk, stripIndent } from '../utils/lazy';
|
||||||
import {
|
import {
|
||||||
dockerignoreHelp,
|
dockerignoreHelp,
|
||||||
registrySecretsHelp,
|
registrySecretsHelp,
|
||||||
buildArgDeprecation,
|
buildArgDeprecation,
|
||||||
} from '../../utils/messages';
|
} from '../utils/messages';
|
||||||
import * as ca from '../../utils/common-args';
|
import * as ca from '../utils/common-args';
|
||||||
import * as compose from '../../utils/compose';
|
import * as compose from '../utils/compose';
|
||||||
import type {
|
import type {
|
||||||
BuiltImage,
|
BuiltImage,
|
||||||
ComposeCliFlags,
|
ComposeCliFlags,
|
||||||
ComposeOpts,
|
ComposeOpts,
|
||||||
Release as ComposeReleaseInfo,
|
Release as ComposeReleaseInfo,
|
||||||
} from '../../utils/compose-types';
|
} from '../utils/compose-types';
|
||||||
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
||||||
import {
|
import {
|
||||||
applyReleaseTagKeysAndValues,
|
applyReleaseTagKeysAndValues,
|
||||||
buildProject,
|
buildProject,
|
||||||
composeCliFlags,
|
composeCliFlags,
|
||||||
isBuildConfig,
|
isBuildConfig,
|
||||||
parseReleaseTagKeysAndValues,
|
parseReleaseTagKeysAndValues,
|
||||||
} from '../../utils/compose_ts';
|
} from '../utils/compose_ts';
|
||||||
import { dockerCliFlags } from '../../utils/docker';
|
import { dockerCliFlags } from '../utils/docker';
|
||||||
import type { ApplicationType, DeviceType, Release } from 'balena-sdk';
|
import type {
|
||||||
|
Application,
|
||||||
|
ApplicationType,
|
||||||
|
DeviceType,
|
||||||
|
Release,
|
||||||
|
} from 'balena-sdk';
|
||||||
|
|
||||||
interface ApplicationWithArch {
|
interface ApplicationWithArch extends Application {
|
||||||
id: number;
|
|
||||||
arch: string;
|
arch: string;
|
||||||
is_for__device_type: [Pick<DeviceType, 'slug'>];
|
|
||||||
application_type: [Pick<ApplicationType, 'slug' | 'supports_multicontainer'>];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: For this special one we can't use Interfaces.InferredFlags/InferredArgs
|
|
||||||
// because of the 'registry-secrets' type which is defined in the actual code
|
|
||||||
// as a path (string | undefined) but then the cli turns it into an object
|
|
||||||
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||||
source?: string;
|
source?: string;
|
||||||
build: boolean;
|
build: boolean;
|
||||||
@ -60,6 +61,12 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
|||||||
'release-tag'?: string[];
|
'release-tag'?: string[];
|
||||||
draft: boolean;
|
draft: boolean;
|
||||||
note?: string;
|
note?: string;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
fleet: string;
|
||||||
|
image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DeployCmd extends Command {
|
export default class DeployCmd extends Command {
|
||||||
@ -100,26 +107,31 @@ ${dockerignoreHelp}
|
|||||||
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [
|
||||||
fleet: ca.fleetRequired,
|
ca.fleetRequired,
|
||||||
image: Args.string({ description: 'the image to deploy' }),
|
{
|
||||||
};
|
name: 'image',
|
||||||
|
description: 'the image to deploy',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'deploy <fleet> [image]';
|
||||||
source: Flags.string({
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
source: flags.string({
|
||||||
description:
|
description:
|
||||||
'specify an alternate source directory; default is the working directory',
|
'specify an alternate source directory; default is the working directory',
|
||||||
char: 's',
|
char: 's',
|
||||||
}),
|
}),
|
||||||
build: Flags.boolean({
|
build: flags.boolean({
|
||||||
description: 'force a rebuild before deploy',
|
description: 'force a rebuild before deploy',
|
||||||
char: 'b',
|
char: 'b',
|
||||||
}),
|
}),
|
||||||
nologupload: Flags.boolean({
|
nologupload: flags.boolean({
|
||||||
description:
|
description:
|
||||||
"don't upload build logs to the dashboard with image (if building)",
|
"don't upload build logs to the dashboard with image (if building)",
|
||||||
}),
|
}),
|
||||||
'release-tag': Flags.string({
|
'release-tag': flags.string({
|
||||||
description: stripIndent`
|
description: stripIndent`
|
||||||
Set release tags if the image deployment is successful. Multiple
|
Set release tags if the image deployment is successful. Multiple
|
||||||
arguments may be provided, alternating tag keys and values (see examples).
|
arguments may be provided, alternating tag keys and values (see examples).
|
||||||
@ -127,7 +139,7 @@ ${dockerignoreHelp}
|
|||||||
`,
|
`,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
}),
|
}),
|
||||||
draft: Flags.boolean({
|
draft: flags.boolean({
|
||||||
description: stripIndent`
|
description: stripIndent`
|
||||||
Deploy the release as a draft. Draft releases are ignored
|
Deploy the release as a draft. Draft releases are ignored
|
||||||
by the 'track latest' release policy but can be used through release pinning.
|
by the 'track latest' release policy but can be used through release pinning.
|
||||||
@ -135,9 +147,12 @@ ${dockerignoreHelp}
|
|||||||
as final by default unless this option is given.`,
|
as final by default unless this option is given.`,
|
||||||
default: false,
|
default: false,
|
||||||
}),
|
}),
|
||||||
note: Flags.string({ description: 'The notes for this release' }),
|
note: flags.string({ description: 'The notes for this release' }),
|
||||||
...composeCliFlags,
|
...composeCliFlags,
|
||||||
...dockerCliFlags,
|
...dockerCliFlags,
|
||||||
|
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||||
|
// Revisit this in future release.
|
||||||
|
help: flags.help({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
@ -145,13 +160,13 @@ ${dockerignoreHelp}
|
|||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(DeployCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
DeployCmd,
|
||||||
|
);
|
||||||
|
|
||||||
(await import('events')).defaultMaxListeners = 1000;
|
(await import('events')).defaultMaxListeners = 1000;
|
||||||
|
|
||||||
const Logger = await import('../../utils/logger');
|
const logger = await Command.getLogger();
|
||||||
|
|
||||||
const logger = Logger.getLogger();
|
|
||||||
logger.logDebug('Parsing input...');
|
logger.logDebug('Parsing input...');
|
||||||
|
|
||||||
const { fleet, image } = params;
|
const { fleet, image } = params;
|
||||||
@ -169,7 +184,7 @@ ${dockerignoreHelp}
|
|||||||
|
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
const { getRegistrySecrets, validateProjectDirectory } = await import(
|
const { getRegistrySecrets, validateProjectDirectory } = await import(
|
||||||
'../../utils/compose_ts'
|
'../utils/compose_ts'
|
||||||
);
|
);
|
||||||
|
|
||||||
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
||||||
@ -177,7 +192,7 @@ ${dockerignoreHelp}
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
(options as FlagsDef)['registry-secrets'] = await getRegistrySecrets(
|
options['registry-secrets'] = await getRegistrySecrets(
|
||||||
sdk,
|
sdk,
|
||||||
options['registry-secrets'],
|
options['registry-secrets'],
|
||||||
);
|
);
|
||||||
@ -190,16 +205,16 @@ ${dockerignoreHelp}
|
|||||||
registrySecretsPath: options['registry-secrets'],
|
registrySecretsPath: options['registry-secrets'],
|
||||||
});
|
});
|
||||||
options.dockerfile = dockerfilePath;
|
options.dockerfile = dockerfilePath;
|
||||||
(options as FlagsDef)['registry-secrets'] = registrySecrets;
|
options['registry-secrets'] = registrySecrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
const helpers = await import('../../utils/helpers');
|
const helpers = await import('../utils/helpers');
|
||||||
const app = await helpers.getAppWithArch(fleet);
|
const app = await helpers.getAppWithArch(fleet);
|
||||||
|
|
||||||
const dockerUtils = await import('../../utils/docker');
|
const dockerUtils = await import('../utils/docker');
|
||||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||||
dockerUtils.getDocker(options),
|
dockerUtils.getDocker(options),
|
||||||
dockerUtils.generateBuildOpts(options as FlagsDef),
|
dockerUtils.generateBuildOpts(options),
|
||||||
compose.generateOpts(options),
|
compose.generateOpts(options),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -220,13 +235,13 @@ ${dockerignoreHelp}
|
|||||||
releaseTagValues,
|
releaseTagValues,
|
||||||
);
|
);
|
||||||
if (options.note) {
|
if (options.note) {
|
||||||
await sdk.models.release.setNote(release.id, options.note);
|
await sdk.models.release.note(release.id, options.note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deployProject(
|
async deployProject(
|
||||||
docker: import('dockerode'),
|
docker: import('dockerode'),
|
||||||
logger: import('../../utils/logger'),
|
logger: import('../utils/logger'),
|
||||||
composeOpts: ComposeOpts,
|
composeOpts: ComposeOpts,
|
||||||
opts: {
|
opts: {
|
||||||
app: ApplicationWithArch; // the application instance to deploy to
|
app: ApplicationWithArch; // the application instance to deploy to
|
||||||
@ -244,10 +259,10 @@ ${dockerignoreHelp}
|
|||||||
const doodles = await import('resin-doodles');
|
const doodles = await import('resin-doodles');
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
const { deployProject: $deployProject, loadProject } = await import(
|
const { deployProject: $deployProject, loadProject } = await import(
|
||||||
'../../utils/compose_ts'
|
'../utils/compose_ts'
|
||||||
);
|
);
|
||||||
|
|
||||||
const appType = opts.app.application_type[0];
|
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const project = await loadProject(
|
const project = await loadProject(
|
||||||
@ -304,7 +319,7 @@ ${dockerignoreHelp}
|
|||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
composition: compositionToBuild,
|
composition: compositionToBuild,
|
||||||
arch: opts.app.arch,
|
arch: opts.app.arch,
|
||||||
deviceType: opts.app.is_for__device_type[0].slug,
|
deviceType: (opts.app?.is_for__device_type as DeviceType[])?.[0].slug,
|
||||||
emulated: opts.buildEmulated,
|
emulated: opts.buildEmulated,
|
||||||
buildOpts: opts.buildOpts,
|
buildOpts: opts.buildOpts,
|
||||||
inlineLogs: composeOpts.inlineLogs,
|
inlineLogs: composeOpts.inlineLogs,
|
||||||
@ -325,17 +340,17 @@ ${dockerignoreHelp}
|
|||||||
);
|
);
|
||||||
|
|
||||||
let release: Release | ComposeReleaseInfo['release'];
|
let release: Release | ComposeReleaseInfo['release'];
|
||||||
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
|
if (appType?.is_legacy) {
|
||||||
const { deployLegacy } = require('../../utils/deploy-legacy');
|
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||||
|
|
||||||
const msg = getChalk().yellow(
|
const msg = getChalk().yellow(
|
||||||
'Target fleet requires legacy deploy method.',
|
'Target fleet requires legacy deploy method.',
|
||||||
);
|
);
|
||||||
logger.logWarn(msg);
|
logger.logWarn(msg);
|
||||||
|
|
||||||
const [token, { username }, url, options] = await Promise.all([
|
const [token, username, url, options] = await Promise.all([
|
||||||
sdk.auth.getToken(),
|
sdk.auth.getToken(),
|
||||||
sdk.auth.getUserInfo(),
|
sdk.auth.whoami(),
|
||||||
sdk.settings.get('balenaUrl'),
|
sdk.settings.get('balenaUrl'),
|
||||||
{
|
{
|
||||||
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||||
@ -358,17 +373,23 @@ ${dockerignoreHelp}
|
|||||||
$select: ['commit'],
|
$select: ['commit'],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const [userId, auth, apiEndpoint] = await Promise.all([
|
||||||
|
sdk.auth.getUserId(),
|
||||||
|
sdk.auth.getToken(),
|
||||||
|
sdk.settings.get('apiUrl'),
|
||||||
|
]);
|
||||||
release = await $deployProject(
|
release = await $deployProject(
|
||||||
docker,
|
docker,
|
||||||
sdk,
|
|
||||||
logger,
|
logger,
|
||||||
project.composition,
|
project.composition,
|
||||||
images,
|
images,
|
||||||
opts.app.id,
|
opts.app.id,
|
||||||
|
userId,
|
||||||
|
`Bearer ${auth}`,
|
||||||
|
apiEndpoint,
|
||||||
!opts.shouldUploadLogs,
|
!opts.shouldUploadLogs,
|
||||||
composeOpts.projectPath,
|
composeOpts.projectPath,
|
||||||
opts.createAsDraft,
|
opts.createAsDraft,
|
||||||
project.descriptors,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -15,10 +15,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
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 * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
yes: boolean;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceDeactivateCmd extends Command {
|
export default class DeviceDeactivateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Deactivate a device.
|
Deactivate a device.
|
||||||
@ -33,22 +44,27 @@ export default class DeviceDeactivateCmd extends Command {
|
|||||||
'$ balena device deactivate 7cf02a6 --yes',
|
'$ balena device deactivate 7cf02a6 --yes',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: 'the UUID of the device to be deactivated',
|
description: 'the UUID of the device to be deactivated',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device deactivate <uuid>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } =
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
await this.parse(DeviceDeactivateCmd);
|
DeviceDeactivateCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
@ -15,10 +15,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceIdentifyCmd extends Command {
|
export default class DeviceIdentifyCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Identify a device.
|
Identify a device.
|
||||||
@ -27,17 +38,24 @@ export default class DeviceIdentifyCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = ['$ balena device identify 23c73a1'];
|
public static examples = ['$ balena device identify 23c73a1'];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: 'the uuid of the device to identify',
|
description: 'the uuid of the device to identify',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'device identify <uuid>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(DeviceIdentifyCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceIdentifyCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,11 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flags, Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import { IArg } from '@oclif/parser/lib/args';
|
||||||
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { expandForAppName } from '../../utils/helpers';
|
import { expandForAppName } from '../../utils/helpers';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import { jsonInfo } from '../../utils/messages';
|
|
||||||
|
|
||||||
import type { Application, Release } from 'balena-sdk';
|
import type { Application, Release } from 'balena-sdk';
|
||||||
|
|
||||||
@ -40,30 +41,39 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
|||||||
undervoltage_detected?: boolean;
|
undervoltage_detected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
view: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceCmd extends Command {
|
export default class DeviceCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Show info about a single device.
|
Show info about a single device.
|
||||||
|
|
||||||
Show information about a single device.
|
Show information about a single device.
|
||||||
|
|
||||||
${jsonInfo.split('\n').join('\n\t\t')}
|
|
||||||
`;
|
`;
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena device 7cf02a6',
|
'$ balena device 7cf02a6',
|
||||||
'$ balena device 7cf02a6 --view',
|
'$ balena device 7cf02a6 --view',
|
||||||
'$ balena device 7cf02a6 --json',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: 'the device uuid',
|
description: 'the device uuid',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device <uuid>';
|
||||||
json: cf.json,
|
|
||||||
view: Flags.boolean({
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
|
view: flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
description: 'open device dashboard page',
|
description: 'open device dashboard page',
|
||||||
}),
|
}),
|
||||||
@ -73,63 +83,39 @@ export default class DeviceCmd extends Command {
|
|||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(DeviceCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
DeviceCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
let device: ExtendedDevice;
|
const device = (await balena.models.device.get(params.uuid, {
|
||||||
if (options.json) {
|
$select: [
|
||||||
const [deviceBase, deviceComputed] = await Promise.all([
|
'device_name',
|
||||||
balena.models.device.get(params.uuid, {
|
'id',
|
||||||
$expand: {
|
'overall_status',
|
||||||
device_tag: {
|
'is_online',
|
||||||
$select: ['tag_key', 'value'],
|
'ip_address',
|
||||||
},
|
'mac_address',
|
||||||
...expandForAppName.$expand,
|
'last_connectivity_event',
|
||||||
},
|
'uuid',
|
||||||
}),
|
'supervisor_version',
|
||||||
balena.models.device.get(params.uuid, {
|
'is_web_accessible',
|
||||||
$select: [
|
'note',
|
||||||
'overall_status',
|
'os_version',
|
||||||
'overall_progress',
|
'memory_usage',
|
||||||
'should_be_running__release',
|
'memory_total',
|
||||||
],
|
'public_address',
|
||||||
}),
|
'storage_block_device',
|
||||||
]);
|
'storage_usage',
|
||||||
|
'storage_total',
|
||||||
device = {
|
'cpu_usage',
|
||||||
...deviceBase,
|
'cpu_temp',
|
||||||
...deviceComputed,
|
'cpu_id',
|
||||||
} as ExtendedDevice;
|
'is_undervolted',
|
||||||
} else {
|
],
|
||||||
device = (await balena.models.device.get(params.uuid, {
|
...expandForAppName,
|
||||||
$select: [
|
})) as ExtendedDevice;
|
||||||
'device_name',
|
|
||||||
'id',
|
|
||||||
'overall_status',
|
|
||||||
'is_online',
|
|
||||||
'ip_address',
|
|
||||||
'mac_address',
|
|
||||||
'last_connectivity_event',
|
|
||||||
'uuid',
|
|
||||||
'supervisor_version',
|
|
||||||
'is_web_accessible',
|
|
||||||
'note',
|
|
||||||
'os_version',
|
|
||||||
'memory_usage',
|
|
||||||
'memory_total',
|
|
||||||
'public_address',
|
|
||||||
'storage_block_device',
|
|
||||||
'storage_usage',
|
|
||||||
'storage_total',
|
|
||||||
'cpu_usage',
|
|
||||||
'cpu_temp',
|
|
||||||
'cpu_id',
|
|
||||||
'is_undervolted',
|
|
||||||
],
|
|
||||||
...expandForAppName,
|
|
||||||
})) as ExtendedDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.view) {
|
if (options.view) {
|
||||||
const open = await import('open');
|
const open = await import('open');
|
||||||
@ -193,11 +179,6 @@ export default class DeviceCmd extends Command {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.json) {
|
|
||||||
console.log(JSON.stringify(device, null, 4));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
getVisuals().table.vertical(device, [
|
getVisuals().table.vertical(device, [
|
||||||
`$${device.device_name}$`,
|
`$${device.device_name}$`,
|
@ -15,7 +15,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flags, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
@ -28,6 +29,7 @@ interface FlagsDef {
|
|||||||
'os-version'?: string;
|
'os-version'?: string;
|
||||||
drive?: string;
|
drive?: string;
|
||||||
config?: string;
|
config?: string;
|
||||||
|
help: void;
|
||||||
'provisioning-key-name'?: string;
|
'provisioning-key-name'?: string;
|
||||||
'provisioning-key-expiry-date'?: string;
|
'provisioning-key-expiry-date'?: string;
|
||||||
}
|
}
|
||||||
@ -50,7 +52,7 @@ export default class DeviceInitCmd extends Command {
|
|||||||
Possible arguments for the '--fleet', '--os-version' and '--drive' options can
|
Possible arguments for the '--fleet', '--os-version' and '--drive' options can
|
||||||
be listed respectively with the commands:
|
be listed respectively with the commands:
|
||||||
|
|
||||||
'balena fleet list'
|
'balena fleets'
|
||||||
'balena os versions'
|
'balena os versions'
|
||||||
'balena util available-drives'
|
'balena util available-drives'
|
||||||
|
|
||||||
@ -72,14 +74,16 @@ export default class DeviceInitCmd extends Command {
|
|||||||
'$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --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',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device init';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
advanced: Flags.boolean({
|
advanced: flags.boolean({
|
||||||
char: 'v',
|
char: 'v',
|
||||||
description: 'show advanced configuration options',
|
description: 'show advanced configuration options',
|
||||||
}),
|
}),
|
||||||
'os-version': Flags.string({
|
'os-version': flags.string({
|
||||||
description: stripIndent`
|
description: stripIndent`
|
||||||
exact version number, or a valid semver range,
|
exact version number, or a valid semver range,
|
||||||
or 'latest' (includes pre-releases),
|
or 'latest' (includes pre-releases),
|
||||||
@ -89,22 +93,23 @@ export default class DeviceInitCmd extends Command {
|
|||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
drive: cf.drive,
|
drive: cf.drive,
|
||||||
config: Flags.string({
|
config: flags.string({
|
||||||
description: 'path to the config JSON file, see `balena os build-config`',
|
description: 'path to the config JSON file, see `balena os build-config`',
|
||||||
}),
|
}),
|
||||||
'provisioning-key-name': Flags.string({
|
'provisioning-key-name': flags.string({
|
||||||
description: 'custom key name assigned to generated provisioning api key',
|
description: 'custom key name assigned to generated provisioning api key',
|
||||||
}),
|
}),
|
||||||
'provisioning-key-expiry-date': Flags.string({
|
'provisioning-key-expiry-date': flags.string({
|
||||||
description:
|
description:
|
||||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||||
}),
|
}),
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = await this.parse(DeviceInitCmd);
|
const { flags: options } = this.parse<FlagsDef, {}>(DeviceInitCmd);
|
||||||
|
|
||||||
// Imports
|
// Imports
|
||||||
const { promisify } = await import('util');
|
const { promisify } = await import('util');
|
||||||
@ -114,22 +119,25 @@ export default class DeviceInitCmd extends Command {
|
|||||||
tmp.setGracefulCleanup();
|
tmp.setGracefulCleanup();
|
||||||
const { downloadOSImage } = await import('../../utils/cloud');
|
const { downloadOSImage } = await import('../../utils/cloud');
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
const Logger = await import('../../utils/logger');
|
|
||||||
|
|
||||||
const logger = Logger.getLogger();
|
const logger = await Command.getLogger();
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
// Get application and
|
// Get application and
|
||||||
const application = options.fleet
|
const application = (await getApplication(
|
||||||
? await getApplication(balena, options.fleet, {
|
balena,
|
||||||
$select: ['id', 'slug'],
|
options.fleet ||
|
||||||
$expand: {
|
(
|
||||||
is_for__device_type: {
|
await (await import('../../utils/patterns')).selectApplication()
|
||||||
$select: 'slug',
|
).slug,
|
||||||
},
|
{
|
||||||
|
$expand: {
|
||||||
|
is_for__device_type: {
|
||||||
|
$select: 'slug',
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
: await (await import('../../utils/patterns')).selectApplication();
|
},
|
||||||
|
)) as ApplicationWithDeviceType;
|
||||||
|
|
||||||
// Register new device
|
// Register new device
|
||||||
const deviceUuid = balena.models.device.generateUniqueKey();
|
const deviceUuid = balena.models.device.generateUniqueKey();
|
||||||
@ -155,7 +163,7 @@ export default class DeviceInitCmd extends Command {
|
|||||||
try {
|
try {
|
||||||
logger.logDebug(`Process failed, removing device ${device.uuid}`);
|
logger.logDebug(`Process failed, removing device ${device.uuid}`);
|
||||||
await balena.models.device.remove(device.uuid);
|
await balena.models.device.remove(device.uuid);
|
||||||
} catch {
|
} catch (e) {
|
||||||
// Ignore removal failures, and throw original error
|
// Ignore removal failures, and throw original error
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
@ -15,9 +15,23 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flags, Args, Command } from '@oclif/core';
|
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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
enable: boolean;
|
||||||
|
disable: boolean;
|
||||||
|
status: boolean;
|
||||||
|
help?: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceLocalModeCmd extends Command {
|
export default class DeviceLocalModeCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Get or manage the local mode status for a device.
|
Get or manage the local mode status for a device.
|
||||||
@ -33,33 +47,38 @@ export default class DeviceLocalModeCmd extends Command {
|
|||||||
'$ balena device local-mode 23c73a1 --status',
|
'$ balena device local-mode 23c73a1 --status',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: 'the uuid of the device to manage',
|
description: 'the uuid of the device to manage',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device local-mode <uuid>';
|
||||||
enable: Flags.boolean({
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
enable: flags.boolean({
|
||||||
description: 'enable local mode',
|
description: 'enable local mode',
|
||||||
exclusive: ['disable', 'status'],
|
exclusive: ['disable', 'status'],
|
||||||
}),
|
}),
|
||||||
disable: Flags.boolean({
|
disable: flags.boolean({
|
||||||
description: 'disable local mode',
|
description: 'disable local mode',
|
||||||
exclusive: ['enable', 'status'],
|
exclusive: ['enable', 'status'],
|
||||||
}),
|
}),
|
||||||
status: Flags.boolean({
|
status: flags.boolean({
|
||||||
description: 'output boolean indicating local mode status',
|
description: 'output boolean indicating local mode status',
|
||||||
exclusive: ['enable', 'disable'],
|
exclusive: ['enable', 'disable'],
|
||||||
}),
|
}),
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } =
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
await this.parse(DeviceLocalModeCmd);
|
DeviceLocalModeCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,18 +15,36 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import type { flags } from '@oclif/command';
|
||||||
|
import type { IArg } from '@oclif/parser/lib/args';
|
||||||
import type {
|
import type {
|
||||||
BalenaSDK,
|
BalenaSDK,
|
||||||
Device,
|
Device,
|
||||||
PineOptions,
|
DeviceType,
|
||||||
PineTypedResult,
|
PineTypedResult,
|
||||||
} from 'balena-sdk';
|
} from 'balena-sdk';
|
||||||
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
|
|
||||||
|
type ExtendedDevice = PineTypedResult<
|
||||||
|
Device,
|
||||||
|
typeof import('../../utils/helpers').expandForAppNameAndCpuArch
|
||||||
|
> & {
|
||||||
|
application_name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
fleet?: string;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceMoveCmd extends Command {
|
export default class DeviceMoveCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Move one or more devices to another fleet.
|
Move one or more devices to another fleet.
|
||||||
@ -45,63 +63,61 @@ export default class DeviceMoveCmd extends Command {
|
|||||||
'$ balena device move 7cf02a6 -f myorg/mynewfleet',
|
'$ balena device move 7cf02a6 -f myorg/mynewfleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of device UUIDs to be moved',
|
'comma-separated list (no blank spaces) of device UUIDs to be moved',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device move <uuid(s)>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
private async getDevices(balena: BalenaSDK, deviceUuids: string[]) {
|
|
||||||
const deviceOptions = {
|
|
||||||
$select: 'belongs_to__application',
|
|
||||||
$expand: {
|
|
||||||
is_of__device_type: {
|
|
||||||
$select: 'is_of__cpu_architecture',
|
|
||||||
$expand: {
|
|
||||||
is_of__cpu_architecture: {
|
|
||||||
$select: 'slug',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies PineOptions<Device>;
|
|
||||||
|
|
||||||
// TODO: Refacor once `device.get()` accepts an array of uuids`
|
|
||||||
const devices = await Promise.all(
|
|
||||||
deviceUuids.map(
|
|
||||||
(uuid) =>
|
|
||||||
balena.models.device.get(uuid, deviceOptions) as Promise<
|
|
||||||
PineTypedResult<Device, typeof deviceOptions>
|
|
||||||
>,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return devices;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(DeviceMoveCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
DeviceMoveCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
|
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
|
||||||
|
|
||||||
// Split uuids string into array of uuids
|
// Split uuids string into array of uuids
|
||||||
const deviceUuids = params.uuid.split(',');
|
const deviceUuids = params.uuid.split(',');
|
||||||
|
|
||||||
const devices = await this.getDevices(balena, deviceUuids);
|
// Get devices
|
||||||
|
const devices = await Promise.all(
|
||||||
|
deviceUuids.map(
|
||||||
|
(uuid) =>
|
||||||
|
balena.models.device.get(
|
||||||
|
uuid,
|
||||||
|
expandForAppNameAndCpuArch,
|
||||||
|
) as Promise<ExtendedDevice>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Map application name for each device
|
||||||
|
for (const device of devices) {
|
||||||
|
const belongsToApplication = device.belongs_to__application;
|
||||||
|
device.application_name = belongsToApplication?.[0]
|
||||||
|
? belongsToApplication[0].app_name
|
||||||
|
: 'N/a';
|
||||||
|
}
|
||||||
|
|
||||||
// Disambiguate application
|
// Disambiguate application
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
|
||||||
// Get destination application
|
// Get destination application
|
||||||
const application = options.fleet
|
const application = options.fleet
|
||||||
? await getApplication(balena, options.fleet, { $select: ['id', 'slug'] })
|
? await getApplication(balena, options.fleet)
|
||||||
: await this.interactivelySelectApplication(balena, devices);
|
: await this.interactivelySelectApplication(balena, devices);
|
||||||
|
|
||||||
// Move each device
|
// Move each device
|
||||||
@ -118,8 +134,9 @@ export default class DeviceMoveCmd extends Command {
|
|||||||
|
|
||||||
async interactivelySelectApplication(
|
async interactivelySelectApplication(
|
||||||
balena: BalenaSDK,
|
balena: BalenaSDK,
|
||||||
devices: Awaited<ReturnType<typeof this.getDevices>>,
|
devices: ExtendedDevice[],
|
||||||
) {
|
) {
|
||||||
|
const { getExpandedProp } = await import('../../utils/pine');
|
||||||
// deduplicate the slugs
|
// deduplicate the slugs
|
||||||
const deviceCpuArchs = Array.from(
|
const deviceCpuArchs = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
@ -129,44 +146,46 @@ export default class DeviceMoveCmd extends Command {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const allCpuArches = await balena.pine.get({
|
const deviceTypeOptions = {
|
||||||
resource: 'cpu_architecture',
|
$select: 'slug',
|
||||||
options: {
|
$expand: {
|
||||||
$select: ['id', 'slug'],
|
is_of__cpu_architecture: {
|
||||||
|
$select: 'slug',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
} as const;
|
||||||
|
const deviceTypes = (await balena.models.deviceType.getAllSupported(
|
||||||
|
deviceTypeOptions,
|
||||||
|
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
|
||||||
|
|
||||||
const compatibleCpuArchIds = allCpuArches
|
const compatibleDeviceTypeSlugs = new Set(
|
||||||
.filter((cpuArch) => {
|
deviceTypes
|
||||||
return deviceCpuArchs.every((deviceCpuArch) =>
|
.filter((deviceType) => {
|
||||||
balena.models.os.isArchitectureCompatibleWith(
|
const deviceTypeArch = getExpandedProp(
|
||||||
deviceCpuArch,
|
deviceType.is_of__cpu_architecture,
|
||||||
cpuArch.slug,
|
'slug',
|
||||||
),
|
)!;
|
||||||
);
|
return deviceCpuArchs.every((deviceCpuArch) =>
|
||||||
})
|
balena.models.os.isArchitectureCompatibleWith(
|
||||||
.map((deviceType) => deviceType.id);
|
deviceCpuArch,
|
||||||
|
deviceTypeArch,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((deviceType) => deviceType.slug),
|
||||||
|
);
|
||||||
|
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
try {
|
try {
|
||||||
const application = await patterns.selectApplication(
|
const application = await patterns.selectApplication(
|
||||||
{
|
(app) =>
|
||||||
is_for__device_type: {
|
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
|
||||||
$any: {
|
devices.some((device) => device.application_name !== app.app_name),
|
||||||
$alias: 'dt',
|
|
||||||
$expr: {
|
|
||||||
dt: {
|
|
||||||
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
return application;
|
return application;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!compatibleCpuArchIds.length) {
|
if (!compatibleDeviceTypeSlugs.size) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`${err.message}\nDo all devices have a compatible architecture?`,
|
`${err.message}\nDo all devices have a compatible architecture?`,
|
||||||
);
|
);
|
@ -15,12 +15,23 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flags, Args, Command } from '@oclif/core';
|
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 * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||||
import type { Device } from 'balena-sdk';
|
import type { Device } from 'balena-sdk';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import { getExpandedProp } from '../../utils/pine';
|
|
||||||
|
interface FlagsDef {
|
||||||
|
version?: string;
|
||||||
|
yes: boolean;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceOsUpdateCmd extends Command {
|
export default class DeviceOsUpdateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -37,34 +48,32 @@ export default class DeviceOsUpdateCmd extends Command {
|
|||||||
'$ balena device os-update 23c73a1',
|
'$ balena device os-update 23c73a1',
|
||||||
'$ balena device os-update 23c73a1 --version 2.101.7',
|
'$ balena device os-update 23c73a1 --version 2.101.7',
|
||||||
'$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod',
|
'$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod',
|
||||||
'$ balena device os-update 23c73a1 --include-draft',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: 'the uuid of the device to update',
|
description: 'the uuid of the device to update',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device os-update <uuid>';
|
||||||
version: Flags.string({
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
version: flags.string({
|
||||||
description: 'a balenaOS version',
|
description: 'a balenaOS version',
|
||||||
exclusive: ['include-draft'],
|
|
||||||
}),
|
|
||||||
'include-draft': Flags.boolean({
|
|
||||||
description: 'include pre-release balenaOS versions',
|
|
||||||
default: false,
|
|
||||||
exclusive: ['version'],
|
|
||||||
}),
|
}),
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } =
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
await this.parse(DeviceOsUpdateCmd);
|
DeviceOsUpdateCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
|
|
||||||
@ -90,25 +99,10 @@ export default class DeviceOsUpdateCmd extends Command {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let includeDraft = options['include-draft'];
|
|
||||||
if (!includeDraft && options.version != null) {
|
|
||||||
const bSemver = await import('balena-semver');
|
|
||||||
const parsedVersion = bSemver.parse(options.version);
|
|
||||||
// When the user provides a draft version, we need to pass `includeDraft`
|
|
||||||
// to the os.getSupportedOsUpdateVersions() since w/o it the results
|
|
||||||
// will for sure not include the user provided version and the command
|
|
||||||
// would return a "not in the Host OS update targets" error.
|
|
||||||
includeDraft =
|
|
||||||
parsedVersion != null && parsedVersion.prerelease.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get supported OS update versions
|
// Get supported OS update versions
|
||||||
const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions(
|
const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions(
|
||||||
is_of__device_type[0].slug,
|
is_of__device_type[0].slug,
|
||||||
currentOsVersion,
|
currentOsVersion,
|
||||||
{
|
|
||||||
includeDraft,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
if (hupVersionInfo.versions.length === 0) {
|
if (hupVersionInfo.versions.length === 0) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
@ -119,72 +113,33 @@ export default class DeviceOsUpdateCmd extends Command {
|
|||||||
// Get target OS version
|
// Get target OS version
|
||||||
let targetOsVersion = options.version;
|
let targetOsVersion = options.version;
|
||||||
if (targetOsVersion != null) {
|
if (targetOsVersion != null) {
|
||||||
const { normalizeOsVersion } = await import('../../utils/normalization');
|
|
||||||
targetOsVersion = normalizeOsVersion(targetOsVersion);
|
|
||||||
if (!hupVersionInfo.versions.includes(targetOsVersion)) {
|
if (!hupVersionInfo.versions.includes(targetOsVersion)) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
|
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const choices = await Promise.all(
|
|
||||||
hupVersionInfo.versions.map(async (version) => {
|
|
||||||
const takeoverRequired =
|
|
||||||
(await sdk.models.os.getOsUpdateType(
|
|
||||||
getExpandedProp(is_of__device_type, 'slug')!,
|
|
||||||
currentOsVersion,
|
|
||||||
version,
|
|
||||||
)) === 'takeover';
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: `${version}${hupVersionInfo.recommended === version ? ' (recommended)' : ''}${takeoverRequired ? ' ADVANCED UPDATE: Requires disk re-partitioning with no rollback option' : ''}`,
|
|
||||||
value: version,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
targetOsVersion = await getCliForm().ask({
|
targetOsVersion = await getCliForm().ask({
|
||||||
message: 'Target OS version',
|
message: 'Target OS version',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
choices,
|
choices: hupVersionInfo.versions.map((version) => ({
|
||||||
|
name:
|
||||||
|
hupVersionInfo.recommended === version
|
||||||
|
? `${version} (recommended)`
|
||||||
|
: version,
|
||||||
|
value: version,
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const takeoverRequired =
|
|
||||||
(await sdk.models.os.getOsUpdateType(
|
|
||||||
getExpandedProp(is_of__device_type, 'slug')!,
|
|
||||||
currentOsVersion,
|
|
||||||
targetOsVersion,
|
|
||||||
)) === 'takeover';
|
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
// Warn the user if the update requires a takeover
|
|
||||||
if (takeoverRequired) {
|
|
||||||
await patterns.confirm(
|
|
||||||
options.yes || false,
|
|
||||||
stripIndent`Before you proceed, note that this update process is different from a regular HostOS Update:
|
|
||||||
DATA LOSS: This update requires disk re-partitioning, which will erase all data stored on the device.
|
|
||||||
NO ROLLBACK: Unlike our HostOS update mechanism, this process does not allow reverting to a previous version in case of failure.
|
|
||||||
Make sure to back up all important data before continuing. For more details, check our documentation: https://docs.balena.io/reference/OS/updates/update-process/
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Confirm and start update
|
// Confirm and start update
|
||||||
await patterns.confirm(
|
await patterns.confirm(
|
||||||
options.yes || false,
|
options.yes || false,
|
||||||
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
|
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
|
||||||
);
|
);
|
||||||
|
|
||||||
await sdk.models.device
|
await sdk.models.device.startOsUpdate(uuid, targetOsVersion);
|
||||||
.startOsUpdate(uuid, targetOsVersion, {
|
await patterns.awaitDeviceOsUpdate(uuid, targetOsVersion);
|
||||||
runDetached: true,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
console.log(
|
|
||||||
`The balena OS update has started. You can keep track of the progress via the dashboard.\n` +
|
|
||||||
`To open the dashboard page related to a device via the CLI, you can use \`balena device UUID --view\``,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(`Failed to start OS update for device ${uuid}:`, error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,10 +15,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { getExpandedProp } from '../../utils/pine';
|
import { getExpandedProp } from '../../utils/pine';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
releaseToPinTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DevicePinCmd extends Command {
|
export default class DevicePinCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Pin a device to a release.
|
Pin a device to a release.
|
||||||
@ -32,26 +44,34 @@ export default class DevicePinCmd extends Command {
|
|||||||
'$ balena device pin 7cf02a6 91165e5',
|
'$ balena device pin 7cf02a6 91165e5',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: 'the uuid of the device to pin to a release',
|
description: 'the uuid of the device to pin to a release',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
releaseToPinTo: Args.string({
|
{
|
||||||
|
name: 'releaseToPinTo',
|
||||||
description: 'the commit of the release for the device to get pinned to',
|
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 static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(DevicePinCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePinCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
const device = await balena.models.device.get(params.uuid, {
|
const device = await balena.models.device.get(params.uuid, {
|
||||||
$expand: {
|
$expand: {
|
||||||
is_pinned_on__release: {
|
should_be_running__release: {
|
||||||
$select: 'commit',
|
$select: 'commit',
|
||||||
},
|
},
|
||||||
belongs_to__application: {
|
belongs_to__application: {
|
||||||
@ -61,7 +81,7 @@ export default class DevicePinCmd extends Command {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pinnedRelease = getExpandedProp(
|
const pinnedRelease = getExpandedProp(
|
||||||
device.is_pinned_on__release,
|
device.should_be_running__release,
|
||||||
'commit',
|
'commit',
|
||||||
);
|
);
|
||||||
const appSlug = getExpandedProp(device.belongs_to__application, 'slug');
|
const appSlug = getExpandedProp(device.belongs_to__application, 'slug');
|
||||||
@ -74,7 +94,7 @@ export default class DevicePinCmd extends Command {
|
|||||||
pinnedRelease
|
pinnedRelease
|
||||||
? `This device is currently pinned to ${pinnedRelease}.`
|
? `This device is currently pinned to ${pinnedRelease}.`
|
||||||
: 'This device is not currently pinned to any release.'
|
: '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 release list ${appSlug}\`.`,
|
} \n\nTo see a list of all releases this device can be pinned to, run \`balena releases ${appSlug}\`.`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await balena.models.device.pinToRelease(params.uuid, releaseToPinTo);
|
await balena.models.device.pinToRelease(params.uuid, releaseToPinTo);
|
@ -15,10 +15,24 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flags, Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import type { IArg } from '@oclif/parser/lib/args';
|
||||||
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
enable: boolean;
|
||||||
|
disable: boolean;
|
||||||
|
status: boolean;
|
||||||
|
help?: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DevicePublicUrlCmd extends Command {
|
export default class DevicePublicUrlCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Get or manage the public URL for a device.
|
Get or manage the public URL for a device.
|
||||||
@ -35,33 +49,38 @@ export default class DevicePublicUrlCmd extends Command {
|
|||||||
'$ balena device public-url 23c73a1 --status',
|
'$ balena device public-url 23c73a1 --status',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: 'the uuid of the device to manage',
|
description: 'the uuid of the device to manage',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device public-url <uuid>';
|
||||||
enable: Flags.boolean({
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
enable: flags.boolean({
|
||||||
description: 'enable the public URL',
|
description: 'enable the public URL',
|
||||||
exclusive: ['disable', 'status'],
|
exclusive: ['disable', 'status'],
|
||||||
}),
|
}),
|
||||||
disable: Flags.boolean({
|
disable: flags.boolean({
|
||||||
description: 'disable the public URL',
|
description: 'disable the public URL',
|
||||||
exclusive: ['enable', 'status'],
|
exclusive: ['enable', 'status'],
|
||||||
}),
|
}),
|
||||||
status: Flags.boolean({
|
status: flags.boolean({
|
||||||
description: 'determine if public URL is enabled',
|
description: 'determine if public URL is enabled',
|
||||||
exclusive: ['enable', 'disable'],
|
exclusive: ['enable', 'disable'],
|
||||||
}),
|
}),
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } =
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
await this.parse(DevicePublicUrlCmd);
|
DevicePublicUrlCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,9 +15,20 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
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, getCliUx, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DevicePurgeCmd extends Command {
|
export default class DevicePurgeCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Purge data from a device.
|
Purge data from a device.
|
||||||
@ -33,17 +44,24 @@ export default class DevicePurgeCmd extends Command {
|
|||||||
'$ balena device purge 55d43b3,23c73a1',
|
'$ balena device purge 55d43b3,23c73a1',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static usage = 'device purge <uuid>';
|
||||||
uuid: Args.string({
|
|
||||||
|
public static args: Array<IArg<any>> = [
|
||||||
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(DevicePurgeCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePurgeCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const ux = getCliUx();
|
const ux = getCliUx();
|
@ -15,10 +15,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
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 * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
force: boolean;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceRebootCmd extends Command {
|
export default class DeviceRebootCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Restart a device.
|
Restart a device.
|
||||||
@ -27,21 +38,27 @@ export default class DeviceRebootCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = ['$ balena device reboot 23c73a1'];
|
public static examples = ['$ balena device reboot 23c73a1'];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: 'the uuid of the device to reboot',
|
description: 'the uuid of the device to reboot',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device reboot <uuid>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
force: cf.force,
|
force: cf.force,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(DeviceRebootCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
DeviceRebootCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,11 +15,24 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flags, Command } from '@oclif/core';
|
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 * as ca from '../../utils/common-args';
|
import * as ca from '../../utils/common-args';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
uuid?: string;
|
||||||
|
deviceType?: string;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
fleet: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceRegisterCmd extends Command {
|
export default class DeviceRegisterCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Register a new device.
|
Register a new device.
|
||||||
@ -38,34 +51,34 @@ export default class DeviceRegisterCmd extends Command {
|
|||||||
'$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>',
|
'$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [ca.fleetRequired];
|
||||||
fleet: ca.fleetRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device register <fleet>';
|
||||||
uuid: Flags.string({
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
uuid: flags.string({
|
||||||
description: 'custom uuid',
|
description: 'custom uuid',
|
||||||
char: 'u',
|
char: 'u',
|
||||||
}),
|
}),
|
||||||
deviceType: Flags.string({
|
deviceType: flags.string({
|
||||||
description:
|
description:
|
||||||
"device type slug (run 'balena device-type list' for possible values)",
|
"device type slug (run 'balena devices supported' for possible values)",
|
||||||
}),
|
}),
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } =
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
await this.parse(DeviceRegisterCmd);
|
DeviceRegisterCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
const application = await getApplication(balena, params.fleet, {
|
const application = await getApplication(balena, params.fleet);
|
||||||
$select: ['id', 'slug'],
|
|
||||||
});
|
|
||||||
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
|
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
|
||||||
|
|
||||||
console.info(`Registering to ${application.slug}: ${uuid}`);
|
console.info(`Registering to ${application.slug}: ${uuid}`);
|
||||||
@ -76,6 +89,6 @@ export default class DeviceRegisterCmd extends Command {
|
|||||||
options.deviceType,
|
options.deviceType,
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.uuid;
|
return result && result.uuid;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,9 +15,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
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, getCliForm } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
newName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceRenameCmd extends Command {
|
export default class DeviceRenameCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Rename a device.
|
Rename a device.
|
||||||
@ -31,20 +43,28 @@ export default class DeviceRenameCmd extends Command {
|
|||||||
'$ balena device rename 7cf02a6 MyPi',
|
'$ balena device rename 7cf02a6 MyPi',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: 'the uuid of the device to rename',
|
description: 'the uuid of the device to rename',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
newName: Args.string({
|
{
|
||||||
|
name: 'newName',
|
||||||
description: 'the new name for the device',
|
description: 'the new name for the device',
|
||||||
}),
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'device rename <uuid> [newName]';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(DeviceRenameCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceRenameCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,7 +15,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flags, Args, Command } from '@oclif/core';
|
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, getCliUx, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||||
import type {
|
import type {
|
||||||
BalenaSDK,
|
BalenaSDK,
|
||||||
@ -23,6 +26,15 @@ import type {
|
|||||||
CurrentServiceWithCommit,
|
CurrentServiceWithCommit,
|
||||||
} from 'balena-sdk';
|
} from 'balena-sdk';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
service?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceRestartCmd extends Command {
|
export default class DeviceRestartCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Restart containers on a device.
|
Restart containers on a device.
|
||||||
@ -43,26 +55,32 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
'$ balena device restart 23c73a1 -s myService1,myService2',
|
'$ balena device restart 23c73a1 -s myService1,myService2',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of device UUIDs to restart',
|
'comma-separated list (no blank spaces) of device UUIDs to restart',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device restart <uuid>';
|
||||||
service: Flags.string({
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
service: flags.string({
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of service names to restart',
|
'comma-separated list (no blank spaces) of service names to restart',
|
||||||
char: 's',
|
char: 's',
|
||||||
}),
|
}),
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(DeviceRestartCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
DeviceRestartCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const ux = getCliUx();
|
const ux = getCliUx();
|
||||||
@ -154,7 +172,7 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
|
|
||||||
async restartAllServices(balena: BalenaSDK, deviceUuid: string) {
|
async restartAllServices(balena: BalenaSDK, deviceUuid: string) {
|
||||||
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
|
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
|
||||||
// Need to use device.get first to distinguish between non-existant and disconnected devices.
|
// 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
|
// Remove this workaround when SDK issue resolved: https://github.com/balena-io/balena-sdk/issues/649
|
||||||
const { instanceOf, ExpectedError } = await import('../../errors');
|
const { instanceOf, ExpectedError } = await import('../../errors');
|
||||||
try {
|
try {
|
@ -15,10 +15,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
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 * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
yes: boolean;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceRmCmd extends Command {
|
export default class DeviceRmCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Remove one or more devices.
|
Remove one or more devices.
|
||||||
@ -34,22 +45,28 @@ export default class DeviceRmCmd extends Command {
|
|||||||
'$ balena device rm 7cf02a6 --yes',
|
'$ balena device rm 7cf02a6 --yes',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of device UUIDs to be removed',
|
'comma-separated list (no blank spaces) of device UUIDs to be removed',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device rm <uuid(s)>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(DeviceRmCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
DeviceRmCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
@ -15,11 +15,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
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 * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
force: boolean;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceShutdownCmd extends Command {
|
export default class DeviceShutdownCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Shutdown a device.
|
Shutdown a device.
|
||||||
@ -28,22 +39,27 @@ export default class DeviceShutdownCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = ['$ balena device shutdown 23c73a1'];
|
public static examples = ['$ balena device shutdown 23c73a1'];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: 'the uuid of the device to shutdown',
|
description: 'the uuid of the device to shutdown',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'device shutdown <uuid>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
force: cf.force,
|
force: cf.force,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } =
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
await this.parse(DeviceShutdownCmd);
|
DeviceShutdownCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,9 +15,20 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DeviceTrackFleetCmd extends Command {
|
export default class DeviceTrackFleetCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Make a device track the fleet's pinned release.
|
Make a device track the fleet's pinned release.
|
||||||
@ -26,17 +37,24 @@ export default class DeviceTrackFleetCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = ['$ balena device track-fleet 7cf02a6'];
|
public static examples = ['$ balena device track-fleet 7cf02a6'];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
uuid: Args.string({
|
{
|
||||||
|
name: 'uuid',
|
||||||
description: "the uuid of the device to make track the fleet's release",
|
description: "the uuid of the device to make track the fleet's release",
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'device track-fleet <uuid>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(DeviceTrackFleetCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceTrackFleetCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
@ -15,30 +15,28 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { expandForAppName } from '../../utils/helpers';
|
import { expandForAppName } from '../../utils/helpers';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
||||||
|
|
||||||
import type { Device, PineOptions } from 'balena-sdk';
|
import type { Application } from 'balena-sdk';
|
||||||
|
|
||||||
const devicesSelectFields = {
|
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||||
$select: [
|
dashboard_url?: string;
|
||||||
'id',
|
fleet?: string | null; // 'org/name' slug
|
||||||
'uuid',
|
device_type?: string | null;
|
||||||
'device_name',
|
}
|
||||||
'status',
|
|
||||||
'is_online',
|
|
||||||
'supervisor_version',
|
|
||||||
'os_version',
|
|
||||||
],
|
|
||||||
} satisfies PineOptions<Device>;
|
|
||||||
|
|
||||||
export default class DeviceListCmd extends Command {
|
interface FlagsDef {
|
||||||
public static aliases = ['devices'];
|
fleet?: string;
|
||||||
public static deprecateAliases = true;
|
help: void;
|
||||||
|
json: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DevicesCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
List all devices.
|
List all devices.
|
||||||
|
|
||||||
@ -51,14 +49,17 @@ export default class DeviceListCmd extends Command {
|
|||||||
${jsonInfo.split('\n').join('\n\t\t')}
|
${jsonInfo.split('\n').join('\n\t\t')}
|
||||||
`;
|
`;
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena device list',
|
'$ balena devices',
|
||||||
'$ balena device list --fleet MyFleet',
|
'$ balena devices --fleet MyFleet',
|
||||||
'$ balena device list -f myorg/myfleet',
|
'$ balena devices -f myorg/myfleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'devices';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
json: cf.json,
|
json: cf.json,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
@ -66,42 +67,39 @@ export default class DeviceListCmd extends Command {
|
|||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = await this.parse(DeviceListCmd);
|
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const devicesOptions = {
|
|
||||||
...devicesSelectFields,
|
|
||||||
...expandForAppName,
|
|
||||||
$orderby: { device_name: 'asc' },
|
|
||||||
} satisfies PineOptions<Device>;
|
|
||||||
|
|
||||||
const devices = (
|
let devices;
|
||||||
await (async () => {
|
|
||||||
if (options.fleet != null) {
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
|
||||||
const application = await getApplication(balena, options.fleet, {
|
|
||||||
$select: 'slug',
|
|
||||||
$expand: {
|
|
||||||
owns__device: devicesOptions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return application.owns__device;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await balena.pine.get({
|
if (options.fleet != null) {
|
||||||
resource: 'device',
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
options: devicesOptions,
|
const application = await getApplication(balena, options.fleet);
|
||||||
});
|
devices = (await balena.models.device.getAllByApplication(
|
||||||
})()
|
application.id,
|
||||||
).map((device) => ({
|
expandForAppName,
|
||||||
...device,
|
)) as ExtendedDevice[];
|
||||||
dashboard_url: balena.models.device.getDashboardUrl(device.uuid),
|
} else {
|
||||||
fleet: device.belongs_to__application?.[0]?.slug || null,
|
devices = (await balena.models.device.getAll(
|
||||||
uuid: options.json ? device.uuid : device.uuid.slice(0, 7),
|
expandForAppName,
|
||||||
device_type: device.is_of__device_type?.[0]?.slug || null,
|
)) as ExtendedDevice[];
|
||||||
}));
|
}
|
||||||
|
|
||||||
const fields: Array<keyof (typeof devices)[number]> = [
|
devices = devices.map(function (device) {
|
||||||
|
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||||
|
|
||||||
|
const belongsToApplication =
|
||||||
|
device.belongs_to__application as Application[];
|
||||||
|
device.fleet = belongsToApplication?.[0]?.slug || null;
|
||||||
|
|
||||||
|
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
|
||||||
|
|
||||||
|
device.device_type = device.is_of__device_type?.[0]?.slug || null;
|
||||||
|
return device;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fields = [
|
||||||
'id',
|
'id',
|
||||||
'uuid',
|
'uuid',
|
||||||
'device_name',
|
'device_name',
|
@ -14,23 +14,24 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Flags, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
import type * as BalenaSdk from 'balena-sdk';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
import Command from '../../command';
|
||||||
|
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
import { CommandHelp } from '../../utils/oclif-utils';
|
||||||
|
|
||||||
export default class DeviceTypeListCmd extends Command {
|
interface FlagsDef {
|
||||||
public static aliases = ['devices supported'];
|
help: void;
|
||||||
public static deprecateAliases = true;
|
json?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DevicesSupportedCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
List the device types supported by balena (like 'raspberrypi3' or 'intel-nuc').
|
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||||
|
|
||||||
List the device types supported by balena (like 'raspberrypi3' or 'intel-nuc').
|
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||||
|
|
||||||
By default, only actively supported device types are listed.
|
|
||||||
The --all option can be used to list all device types, including those that are
|
|
||||||
no longer supported by balena.
|
|
||||||
|
|
||||||
The --json option is recommended when scripting the output of this command,
|
The --json option is recommended when scripting the output of this command,
|
||||||
because the JSON format is less likely to change and it better represents data
|
because the JSON format is less likely to change and it better represents data
|
||||||
@ -39,58 +40,55 @@ export default class DeviceTypeListCmd extends Command {
|
|||||||
(https://stedolan.github.io/jq/manual/).
|
(https://stedolan.github.io/jq/manual/).
|
||||||
`;
|
`;
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena device-type list',
|
'$ balena devices supported',
|
||||||
'$ balena device-type list --all',
|
'$ balena devices supported --json',
|
||||||
'$ balena device-type list --json',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = (
|
||||||
json: Flags.boolean({
|
'devices supported ' +
|
||||||
|
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
|
json: flags.boolean({
|
||||||
char: 'j',
|
char: 'j',
|
||||||
description: 'produce JSON output instead of tabular output',
|
description: 'produce JSON output instead of tabular output',
|
||||||
}),
|
}),
|
||||||
all: Flags.boolean({
|
|
||||||
description: 'include device types no longer supported by balena',
|
|
||||||
default: false,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = await this.parse(DeviceTypeListCmd);
|
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
|
||||||
const pineOptions = {
|
const [dts, configDTs] = await Promise.all([
|
||||||
$select: ['slug', 'name'],
|
getBalenaSdk().models.deviceType.getAllSupported({
|
||||||
$expand: {
|
$expand: { is_of__cpu_architecture: { $select: 'slug' } },
|
||||||
is_of__cpu_architecture: { $select: 'slug' },
|
$select: ['slug', 'name'],
|
||||||
device_type_alias: {
|
}),
|
||||||
$select: 'is_referenced_by__alias',
|
getBalenaSdk().models.config.getDeviceTypes(),
|
||||||
$orderby: { is_referenced_by__alias: 'asc' },
|
]);
|
||||||
},
|
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
|
||||||
},
|
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
|
||||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
|
|
||||||
const dts = (
|
|
||||||
options.all
|
|
||||||
? await getBalenaSdk().models.deviceType.getAll(pineOptions)
|
|
||||||
: await getBalenaSdk().models.deviceType.getAllSupported(pineOptions)
|
|
||||||
) as Array<
|
|
||||||
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
|
|
||||||
>;
|
|
||||||
interface DT {
|
interface DT {
|
||||||
slug: string;
|
slug: string;
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
arch: string;
|
arch: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
let deviceTypes = dts.map((dt): DT => {
|
let deviceTypes: DT[] = [];
|
||||||
const aliases = dt.device_type_alias
|
for (const slug of Object.keys(dtsBySlug)) {
|
||||||
.map((dta) => dta.is_referenced_by__alias)
|
const configDT: Partial<typeof configDTs[0]> =
|
||||||
.filter((alias) => alias !== dt.slug);
|
configDTsBySlug[slug] || {};
|
||||||
return {
|
const aliases = (configDT.aliases || []).filter(
|
||||||
slug: dt.slug,
|
(alias) => alias !== slug,
|
||||||
|
);
|
||||||
|
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
|
||||||
|
deviceTypes.push({
|
||||||
|
slug,
|
||||||
aliases: options.json ? aliases : [aliases.join(', ')],
|
aliases: options.json ? aliases : [aliases.join(', ')],
|
||||||
arch: dt.is_of__cpu_architecture[0]?.slug || 'n/a',
|
arch: (dt.is_of__cpu_architecture as any)?.[0]?.slug || 'n/a',
|
||||||
name: dt.name || 'N/A',
|
name: dt.name || 'N/A',
|
||||||
};
|
});
|
||||||
});
|
}
|
||||||
const fields = ['slug', 'aliases', 'arch', 'name'];
|
const fields = ['slug', 'aliases', 'arch', 'name'];
|
||||||
deviceTypes = _.sortBy(deviceTypes, fields);
|
deviceTypes = _.sortBy(deviceTypes, fields);
|
||||||
if (options.json) {
|
if (options.json) {
|
@ -15,8 +15,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
import type * as BalenaSdk from 'balena-sdk';
|
import type * as BalenaSdk from 'balena-sdk';
|
||||||
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
@ -25,6 +26,7 @@ import { applicationIdInfo } from '../../utils/messages';
|
|||||||
interface FlagsDef {
|
interface FlagsDef {
|
||||||
fleet?: string;
|
fleet?: string;
|
||||||
device?: string; // device UUID
|
device?: string; // device UUID
|
||||||
|
help: void;
|
||||||
quiet: boolean;
|
quiet: boolean;
|
||||||
service?: string; // service name
|
service?: string; // service name
|
||||||
}
|
}
|
||||||
@ -34,14 +36,11 @@ interface ArgsDef {
|
|||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class EnvSetCmd extends Command {
|
export default class EnvAddCmd extends Command {
|
||||||
public static aliases = ['env add'];
|
|
||||||
public static deprecateAliases = true;
|
|
||||||
|
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Add or update env or config variable to fleets, devices or services.
|
Add env or config variable to fleets, devices or services.
|
||||||
|
|
||||||
Add or update an environment or config variable to one or more fleets, devices or
|
Add an environment or config variable to one or more fleets, devices or
|
||||||
services, as selected by the respective command-line options. Either the
|
services, as selected by the respective command-line options. Either the
|
||||||
--fleet or the --device option must be provided, and either may be be
|
--fleet or the --device option must be provided, and either may be be
|
||||||
used alongside the --service option to define a service-specific variable.
|
used alongside the --service option to define a service-specific variable.
|
||||||
@ -68,41 +67,45 @@ export default class EnvSetCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena env set TERM --fleet MyFleet',
|
'$ balena env add TERM --fleet MyFleet',
|
||||||
'$ balena env set EDITOR vim -f myorg/myfleet',
|
'$ balena env add EDITOR vim -f myorg/myfleet',
|
||||||
'$ balena env set EDITOR vim --fleet MyFleet,MyFleet2',
|
'$ balena env add EDITOR vim --fleet MyFleet,MyFleet2',
|
||||||
'$ balena env set EDITOR vim --fleet MyFleet --service MyService',
|
'$ balena env add EDITOR vim --fleet MyFleet --service MyService',
|
||||||
'$ balena env set EDITOR vim --fleet MyFleet,MyFleet2 --service MyService,MyService2',
|
'$ balena env add EDITOR vim --fleet MyFleet,MyFleet2 --service MyService,MyService2',
|
||||||
'$ balena env set EDITOR vim --device 7cf02a6',
|
'$ balena env add EDITOR vim --device 7cf02a6',
|
||||||
'$ balena env set EDITOR vim --device 7cf02a6,d6f1433',
|
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433',
|
||||||
'$ balena env set EDITOR vim --device 7cf02a6 --service MyService',
|
'$ balena env add EDITOR vim --device 7cf02a6 --service MyService',
|
||||||
'$ balena env set EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
|
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [
|
||||||
name: Args.string({
|
{
|
||||||
|
name: 'name',
|
||||||
required: true,
|
required: true,
|
||||||
description: 'environment or config variable name',
|
description: 'environment or config variable name',
|
||||||
}),
|
},
|
||||||
value: Args.string({
|
{
|
||||||
|
name: 'value',
|
||||||
required: false,
|
required: false,
|
||||||
description:
|
description:
|
||||||
"variable value; if omitted, use value from this process' environment",
|
"variable value; if omitted, use value from this process' environment",
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
// Required for supporting empty string ('') `value` args.
|
public static usage = 'env add <name> [value]';
|
||||||
public static strict = false;
|
|
||||||
|
|
||||||
public static flags = {
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||||
device: { ...cf.device, exclusive: ['fleet'] },
|
device: { ...cf.device, exclusive: ['fleet'] },
|
||||||
|
help: cf.help,
|
||||||
quiet: cf.quiet,
|
quiet: cf.quiet,
|
||||||
service: cf.service,
|
service: cf.service,
|
||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(EnvSetCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
EnvAddCmd,
|
||||||
|
);
|
||||||
const cmd = this;
|
const cmd = this;
|
||||||
|
|
||||||
if (!options.fleet && !options.device) {
|
if (!options.fleet && !options.device) {
|
||||||
@ -111,9 +114,7 @@ export default class EnvSetCmd extends Command {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { checkLoggedIn } = await import('../../utils/patterns');
|
await Command.checkLoggedIn();
|
||||||
|
|
||||||
await checkLoggedIn();
|
|
||||||
|
|
||||||
if (params.value == null) {
|
if (params.value == null) {
|
||||||
params.value = process.env[params.name];
|
params.value = process.env[params.name];
|
||||||
@ -150,15 +151,16 @@ export default class EnvSetCmd extends Command {
|
|||||||
|
|
||||||
const varType = isConfigVar ? 'configVar' : 'envVar';
|
const varType = isConfigVar ? 'configVar' : 'envVar';
|
||||||
if (options.fleet) {
|
if (options.fleet) {
|
||||||
for (const appSlug of await resolveFleetSlugs(balena, options.fleet)) {
|
const { getFleetSlug } = await import('../../utils/sdk');
|
||||||
|
for (const app of options.fleet.split(',')) {
|
||||||
try {
|
try {
|
||||||
await balena.models.application[varType].set(
|
await balena.models.application[varType].set(
|
||||||
appSlug,
|
await getFleetSlug(balena, app),
|
||||||
params.name,
|
params.name,
|
||||||
params.value,
|
params.value,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`${err.message}, fleet: ${appSlug}`);
|
console.error(`${err.message}, fleet: ${app}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,25 +181,6 @@ export default class EnvSetCmd extends Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Stop accepting application names in the next major
|
|
||||||
// and just drop this in favor of doing the .split(',') directly.
|
|
||||||
async function resolveFleetSlugs(
|
|
||||||
balena: BalenaSdk.BalenaSDK,
|
|
||||||
fleetOption: string,
|
|
||||||
) {
|
|
||||||
const fleetSlugs: string[] = [];
|
|
||||||
const { getFleetSlug } = await import('../../utils/sdk');
|
|
||||||
for (const appNameOrSlug of fleetOption.split(',')) {
|
|
||||||
try {
|
|
||||||
fleetSlugs.push(await getFleetSlug(balena, appNameOrSlug));
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`${err.message}, fleet: ${appNameOrSlug}`);
|
|
||||||
process.exitCode = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fleetSlugs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add service variables for a device or fleet.
|
* Add service variables for a device or fleet.
|
||||||
*/
|
*/
|
||||||
@ -207,17 +190,17 @@ async function setServiceVars(
|
|||||||
options: FlagsDef,
|
options: FlagsDef,
|
||||||
) {
|
) {
|
||||||
if (options.fleet) {
|
if (options.fleet) {
|
||||||
for (const appSlug of await resolveFleetSlugs(sdk, options.fleet)) {
|
for (const app of options.fleet.split(',')) {
|
||||||
for (const service of options.service!.split(',')) {
|
for (const service of options.service!.split(',')) {
|
||||||
try {
|
try {
|
||||||
const serviceId = await getServiceIdForApp(sdk, appSlug, service);
|
const serviceId = await getServiceIdForApp(sdk, app, service);
|
||||||
await sdk.models.service.var.set(
|
await sdk.models.service.var.set(
|
||||||
serviceId,
|
serviceId,
|
||||||
params.name,
|
params.name,
|
||||||
params.value!,
|
params.value!,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`${err.message}, fleet: ${appSlug}`);
|
console.error(`${err.message}, fleet: ${app}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,12 +245,11 @@ async function setServiceVars(
|
|||||||
*/
|
*/
|
||||||
async function getServiceIdForApp(
|
async function getServiceIdForApp(
|
||||||
sdk: BalenaSdk.BalenaSDK,
|
sdk: BalenaSdk.BalenaSDK,
|
||||||
appSlug: string,
|
appName: string,
|
||||||
serviceName: string,
|
serviceName: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
let serviceId: number | undefined;
|
let serviceId: number | undefined;
|
||||||
const services = await sdk.models.service.getAllByApplication(appSlug, {
|
const services = await sdk.models.service.getAllByApplication(appName, {
|
||||||
$select: 'id',
|
|
||||||
$filter: { service_name: serviceName },
|
$filter: { service_name: serviceName },
|
||||||
});
|
});
|
||||||
if (services.length > 0) {
|
if (services.length > 0) {
|
||||||
@ -275,7 +257,7 @@ async function getServiceIdForApp(
|
|||||||
}
|
}
|
||||||
if (serviceId === undefined) {
|
if (serviceId === undefined) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`Cannot find service ${serviceName} for fleet ${appSlug}`,
|
`Cannot find service ${serviceName} for fleet ${appName}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return serviceId;
|
return serviceId;
|
@ -14,11 +14,28 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import Command from '../../command';
|
||||||
|
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
import * as ec from '../../utils/env-common';
|
import * as ec from '../../utils/env-common';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { parseAsInteger } from '../../utils/validation';
|
import { parseAsInteger } from '../../utils/validation';
|
||||||
|
|
||||||
|
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
config: boolean;
|
||||||
|
device: boolean;
|
||||||
|
service: boolean;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
id: number;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class EnvRenameCmd extends Command {
|
export default class EnvRenameCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Change the value of a config or env var for a fleet, device or service.
|
Change the value of a config or env var for a fleet, device or service.
|
||||||
@ -37,31 +54,36 @@ export default class EnvRenameCmd extends Command {
|
|||||||
'$ balena env rename 678678 1 --device --config',
|
'$ balena env rename 678678 1 --device --config',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
id: Args.integer({
|
{
|
||||||
|
name: 'id',
|
||||||
required: true,
|
required: true,
|
||||||
description: "variable's numeric database ID",
|
description: "variable's numeric database ID",
|
||||||
parse: (input) => parseAsInteger(input, 'id'),
|
parse: (input) => parseAsInteger(input, 'id'),
|
||||||
}),
|
},
|
||||||
value: Args.string({
|
{
|
||||||
|
name: 'value',
|
||||||
required: true,
|
required: true,
|
||||||
description:
|
description:
|
||||||
"variable value; if omitted, use value from this process' environment",
|
"variable value; if omitted, use value from this process' environment",
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'env rename <id> <value>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
config: ec.booleanConfig,
|
config: ec.booleanConfig,
|
||||||
device: ec.booleanDevice,
|
device: ec.booleanDevice,
|
||||||
service: ec.booleanService,
|
service: ec.booleanService,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: opt } = await this.parse(EnvRenameCmd);
|
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
EnvRenameCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const { checkLoggedIn } = await import('../../utils/patterns');
|
await Command.checkLoggedIn();
|
||||||
|
|
||||||
await checkLoggedIn();
|
|
||||||
|
|
||||||
await getBalenaSdk().pine.patch({
|
await getBalenaSdk().pine.patch({
|
||||||
resource: ec.getVarResourceName(opt.config, opt.device, opt.service),
|
resource: ec.getVarResourceName(opt.config, opt.device, opt.service),
|
40
src/commands/env/rm.ts → lib/commands/env/rm.ts
vendored
40
src/commands/env/rm.ts → lib/commands/env/rm.ts
vendored
@ -15,11 +15,26 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flags, Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import Command from '../../command';
|
||||||
|
|
||||||
import * as ec from '../../utils/env-common';
|
import * as ec from '../../utils/env-common';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { parseAsInteger } from '../../utils/validation';
|
import { parseAsInteger } from '../../utils/validation';
|
||||||
|
|
||||||
|
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
config: boolean;
|
||||||
|
device: boolean;
|
||||||
|
service: boolean;
|
||||||
|
yes: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default class EnvRmCmd extends Command {
|
export default class EnvRmCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Remove a config or env var from a fleet, device or service.
|
Remove a config or env var from a fleet, device or service.
|
||||||
@ -42,19 +57,22 @@ export default class EnvRmCmd extends Command {
|
|||||||
'$ balena env rm 789789 --device --service --yes',
|
'$ balena env rm 789789 --device --service --yes',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
id: Args.integer({
|
{
|
||||||
|
name: 'id',
|
||||||
required: true,
|
required: true,
|
||||||
description: "variable's numeric database ID",
|
description: "variable's numeric database ID",
|
||||||
parse: (input) => parseAsInteger(input, 'id'),
|
parse: (input) => parseAsInteger(input, 'id'),
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'env rm <id>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
config: ec.booleanConfig,
|
config: ec.booleanConfig,
|
||||||
device: ec.booleanDevice,
|
device: ec.booleanDevice,
|
||||||
service: ec.booleanService,
|
service: ec.booleanService,
|
||||||
yes: Flags.boolean({
|
yes: flags.boolean({
|
||||||
char: 'y',
|
char: 'y',
|
||||||
description:
|
description:
|
||||||
'do not prompt for confirmation before deleting the variable',
|
'do not prompt for confirmation before deleting the variable',
|
||||||
@ -63,11 +81,11 @@ export default class EnvRmCmd extends Command {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: opt } = await this.parse(EnvRmCmd);
|
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
EnvRmCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const { checkLoggedIn } = await import('../../utils/patterns');
|
await Command.checkLoggedIn();
|
||||||
|
|
||||||
await checkLoggedIn();
|
|
||||||
|
|
||||||
const { confirm } = await import('../../utils/patterns');
|
const { confirm } = await import('../../utils/patterns');
|
||||||
await confirm(
|
await confirm(
|
@ -14,16 +14,23 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Flags, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
import type { Interfaces } from '@oclif/core';
|
|
||||||
import type * as SDK from 'balena-sdk';
|
import type * as SDK from 'balena-sdk';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { ExpectedError } from '../../errors';
|
import Command from '../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import { ExpectedError } from '../errors';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import * as cf from '../utils/common-flags';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||||
|
import { applicationIdInfo } from '../utils/messages';
|
||||||
|
|
||||||
type FlagsDef = Interfaces.InferredFlags<typeof EnvListCmd.flags>;
|
interface FlagsDef {
|
||||||
|
fleet?: string;
|
||||||
|
config: boolean;
|
||||||
|
device?: string; // device UUID
|
||||||
|
json: boolean;
|
||||||
|
help: void;
|
||||||
|
service?: string; // service name
|
||||||
|
}
|
||||||
|
|
||||||
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
|
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
|
||||||
fleet?: string | null; // fleet slug
|
fleet?: string | null; // fleet slug
|
||||||
@ -45,10 +52,7 @@ interface ServiceEnvironmentVariableInfo
|
|||||||
serviceName?: string; // service name
|
serviceName?: string; // service name
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class EnvListCmd extends Command {
|
export default class EnvsCmd extends Command {
|
||||||
public static aliases = ['envs'];
|
|
||||||
public static deprecateAliases = true;
|
|
||||||
|
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
List the environment or config variables of a fleet, device or service.
|
List the environment or config variables of a fleet, device or service.
|
||||||
|
|
||||||
@ -86,37 +90,39 @@ export default class EnvListCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena env list --fleet myorg/myfleet',
|
'$ balena envs --fleet myorg/myfleet',
|
||||||
'$ balena env list --fleet MyFleet --json',
|
'$ balena envs --fleet MyFleet --json',
|
||||||
'$ balena env list --fleet MyFleet --service MyService',
|
'$ balena envs --fleet MyFleet --service MyService',
|
||||||
'$ balena env list --fleet MyFleet --config',
|
'$ balena envs --fleet MyFleet --service MyService',
|
||||||
'$ balena env list --device 7cf02a6',
|
'$ balena envs --fleet MyFleet --config',
|
||||||
'$ balena env list --device 7cf02a6 --json',
|
'$ balena envs --device 7cf02a6',
|
||||||
'$ balena env list --device 7cf02a6 --config --json',
|
'$ balena envs --device 7cf02a6 --json',
|
||||||
'$ balena env list --device 7cf02a6 --service MyService',
|
'$ balena envs --device 7cf02a6 --config --json',
|
||||||
|
'$ balena envs --device 7cf02a6 --service MyService',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'envs';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||||
config: Flags.boolean({
|
config: flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
char: 'c',
|
char: 'c',
|
||||||
description: 'show configuration variables only',
|
description: 'show configuration variables only',
|
||||||
exclusive: ['service'],
|
exclusive: ['service'],
|
||||||
}),
|
}),
|
||||||
device: { ...cf.device, exclusive: ['fleet'] },
|
device: { ...cf.device, exclusive: ['fleet'] },
|
||||||
|
help: cf.help,
|
||||||
json: cf.json,
|
json: cf.json,
|
||||||
service: { ...cf.service, exclusive: ['config'] },
|
service: { ...cf.service, exclusive: ['config'] },
|
||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = await this.parse(EnvListCmd);
|
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
|
||||||
|
|
||||||
const variables: EnvironmentVariableInfo[] = [];
|
const variables: EnvironmentVariableInfo[] = [];
|
||||||
|
|
||||||
const { checkLoggedIn } = await import('../../utils/patterns');
|
await Command.checkLoggedIn();
|
||||||
|
|
||||||
await checkLoggedIn();
|
|
||||||
|
|
||||||
if (!options.fleet && !options.device) {
|
if (!options.fleet && !options.device) {
|
||||||
throw new ExpectedError('Missing --fleet or --device option');
|
throw new ExpectedError('Missing --fleet or --device option');
|
||||||
@ -125,16 +131,12 @@ export default class EnvListCmd extends Command {
|
|||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
let fleetSlug: string | undefined = options.fleet
|
let fleetSlug: string | undefined = options.fleet
|
||||||
? await (
|
? await (await import('../utils/sdk')).getFleetSlug(balena, options.fleet)
|
||||||
await import('../../utils/sdk')
|
|
||||||
).getFleetSlug(balena, options.fleet)
|
|
||||||
: undefined;
|
: undefined;
|
||||||
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
|
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
|
||||||
|
|
||||||
if (options.device) {
|
if (options.device) {
|
||||||
const { getDeviceAndMaybeAppFromUUID } = await import(
|
const { getDeviceAndMaybeAppFromUUID } = await import('../utils/cloud');
|
||||||
'../../utils/cloud'
|
|
||||||
);
|
|
||||||
const [device, app] = await getDeviceAndMaybeAppFromUUID(
|
const [device, app] = await getDeviceAndMaybeAppFromUUID(
|
||||||
balena,
|
balena,
|
||||||
options.device,
|
options.device,
|
||||||
@ -187,7 +189,7 @@ export default class EnvListCmd extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.json) {
|
if (options.json) {
|
||||||
const { pickAndRename } = await import('../../utils/helpers');
|
const { pickAndRename } = await import('../utils/helpers');
|
||||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||||
this.log(JSON.stringify(mapped, null, 4));
|
this.log(JSON.stringify(mapped, null, 4));
|
||||||
} else {
|
} else {
|
||||||
@ -207,7 +209,6 @@ async function validateServiceName(
|
|||||||
fleetSlug: string,
|
fleetSlug: string,
|
||||||
) {
|
) {
|
||||||
const services = await sdk.models.service.getAllByApplication(fleetSlug, {
|
const services = await sdk.models.service.getAllByApplication(fleetSlug, {
|
||||||
$select: 'id',
|
|
||||||
$filter: { service_name: serviceName },
|
$filter: { service_name: serviceName },
|
||||||
});
|
});
|
||||||
if (services.length === 0) {
|
if (services.length === 0) {
|
||||||
@ -233,10 +234,9 @@ async function getAppVars(
|
|||||||
if (!fleetSlug) {
|
if (!fleetSlug) {
|
||||||
return appVars;
|
return appVars;
|
||||||
}
|
}
|
||||||
const vars =
|
const vars = await sdk.models.application[
|
||||||
await sdk.models.application[
|
options.config ? 'configVar' : 'envVar'
|
||||||
options.config ? 'configVar' : 'envVar'
|
].getAllByApplication(fleetSlug);
|
||||||
].getAllByApplication(fleetSlug);
|
|
||||||
fillInInfoFields(vars, fleetSlug);
|
fillInInfoFields(vars, fleetSlug);
|
||||||
appVars.push(...vars);
|
appVars.push(...vars);
|
||||||
if (!options.config) {
|
if (!options.config) {
|
||||||
@ -275,8 +275,9 @@ async function getDeviceVars(
|
|||||||
const printedUUID = options.json ? fullUUID : options.device!;
|
const printedUUID = options.json ? fullUUID : options.device!;
|
||||||
const deviceVars: EnvironmentVariableInfo[] = [];
|
const deviceVars: EnvironmentVariableInfo[] = [];
|
||||||
if (options.config) {
|
if (options.config) {
|
||||||
const deviceConfigVars =
|
const deviceConfigVars = await sdk.models.device.configVar.getAllByDevice(
|
||||||
await sdk.models.device.configVar.getAllByDevice(fullUUID);
|
fullUUID,
|
||||||
|
);
|
||||||
fillInInfoFields(deviceConfigVars, fleetSlug, printedUUID);
|
fillInInfoFields(deviceConfigVars, fleetSlug, printedUUID);
|
||||||
deviceVars.push(...deviceConfigVars);
|
deviceVars.push(...deviceConfigVars);
|
||||||
} else {
|
} else {
|
||||||
@ -301,8 +302,9 @@ async function getDeviceVars(
|
|||||||
fillInInfoFields(deviceServiceVars, fleetSlug, printedUUID);
|
fillInInfoFields(deviceServiceVars, fleetSlug, printedUUID);
|
||||||
deviceVars.push(...deviceServiceVars);
|
deviceVars.push(...deviceServiceVars);
|
||||||
|
|
||||||
const deviceEnvVars =
|
const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
|
||||||
await sdk.models.device.envVar.getAllByDevice(fullUUID);
|
fullUUID,
|
||||||
|
);
|
||||||
fillInInfoFields(deviceEnvVars, fleetSlug, printedUUID);
|
fillInInfoFields(deviceEnvVars, fleetSlug, printedUUID);
|
||||||
deviceVars.push(...deviceEnvVars);
|
deviceVars.push(...deviceEnvVars);
|
||||||
}
|
}
|
149
lib/commands/fleet/create.ts
Normal file
149
lib/commands/fleet/create.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2016-2021 Balena Ltd.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { flags } from '@oclif/command';
|
||||||
|
import type { Application } from 'balena-sdk';
|
||||||
|
|
||||||
|
import Command from '../../command';
|
||||||
|
import { ExpectedError } from '../../errors';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
organization?: string;
|
||||||
|
type?: string; // application device type
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class FleetCreateCmd extends Command {
|
||||||
|
public static description = stripIndent`
|
||||||
|
Create a fleet.
|
||||||
|
|
||||||
|
Create a new balena fleet.
|
||||||
|
|
||||||
|
You can specify the organization the fleet should belong to using
|
||||||
|
the \`--organization\` option. The organization's handle, not its name,
|
||||||
|
should be provided. Organization handles can be listed with the
|
||||||
|
\`balena orgs\` command.
|
||||||
|
|
||||||
|
The fleet's default device type is specified with the \`--type\` option.
|
||||||
|
The \`balena devices supported\` command can be used to list the available
|
||||||
|
device types.
|
||||||
|
|
||||||
|
Interactive dropdowns will be shown for selection if no device type or
|
||||||
|
organization is specified and there are multiple options to choose from.
|
||||||
|
If there is a single option to choose from, it will be chosen automatically.
|
||||||
|
This interactive behavior can be disabled by explicitly specifying a device
|
||||||
|
type and organization.
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static examples = [
|
||||||
|
'$ balena fleet create MyFleet',
|
||||||
|
'$ balena fleet create MyFleet --organization mmyorg',
|
||||||
|
'$ balena fleet create MyFleet -o myorg --type raspberry-pi',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static args = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
description: 'fleet name',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'fleet create <name>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
organization: flags.string({
|
||||||
|
char: 'o',
|
||||||
|
description: 'handle of the organization the fleet should belong to',
|
||||||
|
}),
|
||||||
|
type: flags.string({
|
||||||
|
char: 't',
|
||||||
|
description:
|
||||||
|
'fleet device type (Check available types with `balena devices supported`)',
|
||||||
|
}),
|
||||||
|
help: cf.help,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static authenticated = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
FleetCreateCmd,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ascertain device type
|
||||||
|
const deviceType =
|
||||||
|
options.type ||
|
||||||
|
(await (await import('../../utils/patterns')).selectDeviceType());
|
||||||
|
|
||||||
|
// Ascertain organization
|
||||||
|
const organization =
|
||||||
|
options.organization?.toLowerCase() || (await this.getOrganization());
|
||||||
|
|
||||||
|
// Create application
|
||||||
|
let application: Application;
|
||||||
|
try {
|
||||||
|
application = await getBalenaSdk().models.application.create({
|
||||||
|
name: params.name,
|
||||||
|
deviceType,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||||
|
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||||
|
throw new ExpectedError(
|
||||||
|
`Error: fleet "${params.name}" already exists in organization "${organization}".`,
|
||||||
|
);
|
||||||
|
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
|
||||||
|
// BalenaRequestError: Request error: Unauthorized
|
||||||
|
throw new ExpectedError(
|
||||||
|
`Error: You are not authorized to create fleets in organization "${organization}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output
|
||||||
|
console.log(
|
||||||
|
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganization() {
|
||||||
|
const { getOwnOrganizations } = await import('../../utils/sdk');
|
||||||
|
const organizations = await getOwnOrganizations(getBalenaSdk());
|
||||||
|
|
||||||
|
if (organizations.length === 0) {
|
||||||
|
// User is not a member of any organizations (should not happen).
|
||||||
|
throw new Error('This account is not a member of any organizations');
|
||||||
|
} else if (organizations.length === 1) {
|
||||||
|
// User is a member of only one organization - use this.
|
||||||
|
return organizations[0].handle;
|
||||||
|
} else {
|
||||||
|
// User is a member of multiple organizations -
|
||||||
|
const { selectOrganization } = await import('../../utils/patterns');
|
||||||
|
return selectOrganization(organizations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,11 +15,25 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flags, Command } from '@oclif/core';
|
import type { flags as flagsType } from '@oclif/command';
|
||||||
|
import { flags } from '@oclif/command';
|
||||||
|
import type { Release } from 'balena-sdk';
|
||||||
|
|
||||||
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import * as ca from '../../utils/common-args';
|
import * as ca from '../../utils/common-args';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
|
import type { DataOutputOptions } from '../../framework';
|
||||||
|
|
||||||
|
interface FlagsDef extends DataOutputOptions {
|
||||||
|
help: void;
|
||||||
|
view: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
fleet: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class FleetCmd extends Command {
|
export default class FleetCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -35,34 +49,42 @@ export default class FleetCmd extends Command {
|
|||||||
'$ balena fleet myorg/myfleet --view',
|
'$ balena fleet myorg/myfleet --view',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [ca.fleetRequired];
|
||||||
fleet: ca.fleetRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'fleet <fleet>';
|
||||||
view: Flags.boolean({
|
|
||||||
|
public static flags: flagsType.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
|
view: flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
description: 'open fleet dashboard page',
|
description: 'open fleet dashboard page',
|
||||||
}),
|
}),
|
||||||
json: cf.json,
|
...cf.dataOutputFlags,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(FleetCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
FleetCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
const application = await getApplication(balena, params.fleet, {
|
const application = (await getApplication(balena, params.fleet, {
|
||||||
$expand: {
|
$expand: {
|
||||||
is_for__device_type: { $select: 'slug' },
|
is_for__device_type: { $select: 'slug' },
|
||||||
should_be_running__release: { $select: 'commit' },
|
should_be_running__release: { $select: 'commit' },
|
||||||
},
|
},
|
||||||
});
|
})) as ApplicationWithDeviceType & {
|
||||||
|
should_be_running__release: [Release?];
|
||||||
|
// For display purposes:
|
||||||
|
device_type: string;
|
||||||
|
commit?: string;
|
||||||
|
};
|
||||||
|
|
||||||
if (options.view) {
|
if (options.view) {
|
||||||
const open = await import('open');
|
const open = await import('open');
|
||||||
@ -73,28 +95,13 @@ export default class FleetCmd extends Command {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationToDisplay = {
|
application.device_type = application.is_for__device_type[0].slug;
|
||||||
id: application.id,
|
application.commit = application.should_be_running__release[0]?.commit;
|
||||||
app_name: application.app_name,
|
|
||||||
slug: application.slug,
|
|
||||||
device_type: application.is_for__device_type[0].slug,
|
|
||||||
commit: application.should_be_running__release[0]?.commit,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options.json) {
|
await this.outputData(
|
||||||
console.log(JSON.stringify(applicationToDisplay, null, 4));
|
application,
|
||||||
return;
|
['app_name', 'id', 'device_type', 'slug', 'commit'],
|
||||||
}
|
options,
|
||||||
|
|
||||||
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
|
|
||||||
console.log(`== ${applicationToDisplay.app_name}`);
|
|
||||||
console.log(
|
|
||||||
getVisuals().table.vertical(applicationToDisplay, [
|
|
||||||
'id',
|
|
||||||
'device_type',
|
|
||||||
'slug',
|
|
||||||
'commit',
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,10 +15,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { getExpandedProp } from '../../utils/pine';
|
import { getExpandedProp } from '../../utils/pine';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
slug: string;
|
||||||
|
releaseToPinTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class FleetPinCmd extends Command {
|
export default class FleetPinCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Pin a fleet to a release.
|
Pin a fleet to a release.
|
||||||
@ -32,20 +44,28 @@ export default class FleetPinCmd extends Command {
|
|||||||
'$ balena fleet pin myorg/myfleet 91165e5',
|
'$ balena fleet pin myorg/myfleet 91165e5',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
slug: Args.string({
|
{
|
||||||
|
name: 'slug',
|
||||||
description: 'the slug of the fleet to pin to a release',
|
description: 'the slug of the fleet to pin to a release',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
releaseToPinTo: Args.string({
|
{
|
||||||
|
name: 'releaseToPinTo',
|
||||||
description: 'the commit of the release for the fleet to get pinned to',
|
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 static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(FleetPinCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPinCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
@ -71,7 +91,7 @@ export default class FleetPinCmd extends Command {
|
|||||||
pinnedRelease
|
pinnedRelease
|
||||||
? `This fleet is currently pinned to ${pinnedRelease}.`
|
? `This fleet is currently pinned to ${pinnedRelease}.`
|
||||||
: 'This fleet is not currently pinned to any release.'
|
: '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 release list ${slug}\`.`,
|
} \n\nTo see a list of all releases this fleet can be pinned to, run \`balena releases ${slug}\`.`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await balena.models.application.pinToRelease(slug, releaseToPinTo);
|
await balena.models.application.pinToRelease(slug, releaseToPinTo);
|
@ -15,11 +15,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Command } from '@oclif/core';
|
import type { flags } from '@oclif/command';
|
||||||
|
|
||||||
|
import Command from '../../command';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
import * as ca from '../../utils/common-args';
|
import * as ca from '../../utils/common-args';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
fleet: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class FleetPurgeCmd extends Command {
|
export default class FleetPurgeCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Purge data from a fleet.
|
Purge data from a fleet.
|
||||||
@ -35,14 +46,18 @@ export default class FleetPurgeCmd extends Command {
|
|||||||
'$ balena fleet purge myorg/myfleet',
|
'$ balena fleet purge myorg/myfleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [ca.fleetRequired];
|
||||||
fleet: ca.fleetRequired,
|
|
||||||
|
public static usage = 'fleet purge <fleet>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(FleetPurgeCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
|
||||||
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
|
||||||
@ -50,9 +65,7 @@ export default class FleetPurgeCmd extends Command {
|
|||||||
|
|
||||||
// balena.models.application.purge only accepts a numeric id
|
// balena.models.application.purge only accepts a numeric id
|
||||||
// so we must first fetch the app to get it's id,
|
// so we must first fetch the app to get it's id,
|
||||||
const application = await getApplication(balena, params.fleet, {
|
const application = await getApplication(balena, params.fleet);
|
||||||
$select: 'id',
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await balena.models.application.purge(application.id);
|
await balena.models.application.purge(application.id);
|
@ -15,11 +15,24 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import type { flags } from '@oclif/command';
|
||||||
|
import type { ApplicationType } from 'balena-sdk';
|
||||||
|
|
||||||
|
import Command from '../../command';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
import * as ca from '../../utils/common-args';
|
import * as ca from '../../utils/common-args';
|
||||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
fleet: string;
|
||||||
|
newName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class FleetRenameCmd extends Command {
|
export default class FleetRenameCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Rename a fleet.
|
Rename a fleet.
|
||||||
@ -38,17 +51,24 @@ export default class FleetRenameCmd extends Command {
|
|||||||
'$ balena fleet rename myorg/oldname NewName',
|
'$ balena fleet rename myorg/oldname NewName',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [
|
||||||
fleet: ca.fleetRequired,
|
ca.fleetRequired,
|
||||||
newName: Args.string({
|
{
|
||||||
|
name: 'newName',
|
||||||
description: 'the new name for the fleet',
|
description: 'the new name for the fleet',
|
||||||
}),
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'fleet rename <fleet> [newName]';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(FleetRenameCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
|
||||||
|
|
||||||
const { validateApplicationName } = await import('../../utils/validation');
|
const { validateApplicationName } = await import('../../utils/validation');
|
||||||
const { ExpectedError } = await import('../../errors');
|
const { ExpectedError } = await import('../../errors');
|
||||||
@ -58,10 +78,9 @@ export default class FleetRenameCmd extends Command {
|
|||||||
// Disambiguate target application (if params.params is a number, it could either be an ID or a numerical name)
|
// Disambiguate target application (if params.params is a number, it could either be an ID or a numerical name)
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
const application = await getApplication(balena, params.fleet, {
|
const application = await getApplication(balena, params.fleet, {
|
||||||
$select: ['id', 'app_name', 'slug'],
|
|
||||||
$expand: {
|
$expand: {
|
||||||
application_type: {
|
application_type: {
|
||||||
$select: 'slug',
|
$select: ['is_legacy'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -72,8 +91,8 @@ export default class FleetRenameCmd extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check app supports renaming
|
// Check app supports renaming
|
||||||
const appType = application.application_type[0];
|
const appType = (application.application_type as ApplicationType[])?.[0];
|
||||||
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
|
if (appType.is_legacy) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
|
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
|
||||||
);
|
);
|
||||||
@ -114,9 +133,9 @@ export default class FleetRenameCmd extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get application again, to be sure of results
|
// Get application again, to be sure of results
|
||||||
const renamedApplication = await getApplication(balena, application.id, {
|
const renamedApplication = await balena.models.application.get(
|
||||||
$select: ['app_name', 'slug'],
|
application.id,
|
||||||
});
|
);
|
||||||
|
|
||||||
// Output result
|
// Output result
|
||||||
console.log(`Fleet renamed`);
|
console.log(`Fleet renamed`);
|
@ -15,11 +15,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Command } from '@oclif/core';
|
import type { flags } from '@oclif/command';
|
||||||
|
|
||||||
|
import Command from '../../command';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
import * as ca from '../../utils/common-args';
|
import * as ca from '../../utils/common-args';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
fleet: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class FleetRestartCmd extends Command {
|
export default class FleetRestartCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Restart a fleet.
|
Restart a fleet.
|
||||||
@ -34,23 +45,25 @@ export default class FleetRestartCmd extends Command {
|
|||||||
'$ balena fleet restart myorg/myfleet',
|
'$ balena fleet restart myorg/myfleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [ca.fleetRequired];
|
||||||
fleet: ca.fleetRequired,
|
|
||||||
|
public static usage = 'fleet restart <fleet>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(FleetRestartCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetRestartCmd);
|
||||||
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
// Disambiguate application
|
// Disambiguate application
|
||||||
const application = await getApplication(balena, params.fleet, {
|
const application = await getApplication(balena, params.fleet);
|
||||||
$select: 'slug',
|
|
||||||
});
|
|
||||||
|
|
||||||
await balena.models.application.restart(application.slug);
|
await balena.models.application.restart(application.slug);
|
||||||
}
|
}
|
@ -15,11 +15,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { flags } from '@oclif/command';
|
||||||
|
|
||||||
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import * as ca from '../../utils/common-args';
|
import * as ca from '../../utils/common-args';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
import { Command } from '@oclif/core';
|
|
||||||
|
interface FlagsDef {
|
||||||
|
yes: boolean;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
fleet: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class FleetRmCmd extends Command {
|
export default class FleetRmCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -38,18 +49,21 @@ export default class FleetRmCmd extends Command {
|
|||||||
'$ balena fleet rm myorg/myfleet',
|
'$ balena fleet rm myorg/myfleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [ca.fleetRequired];
|
||||||
fleet: ca.fleetRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'fleet rm <fleet>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(FleetRmCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
FleetRmCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const { confirm } = await import('../../utils/patterns');
|
const { confirm } = await import('../../utils/patterns');
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
@ -62,9 +76,7 @@ export default class FleetRmCmd extends Command {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||||
const application = await getApplication(balena, params.fleet, {
|
const application = await getApplication(balena, params.fleet);
|
||||||
$select: 'slug',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove
|
// Remove
|
||||||
await balena.models.application.remove(application.slug);
|
await balena.models.application.remove(application.slug);
|
@ -15,9 +15,20 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class FleetTrackLatestCmd extends Command {
|
export default class FleetTrackLatestCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Make this fleet track the latest release.
|
Make this fleet track the latest release.
|
||||||
@ -29,17 +40,24 @@ export default class FleetTrackLatestCmd extends Command {
|
|||||||
'$ balena fleet track-latest myfleet',
|
'$ balena fleet track-latest myfleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
slug: Args.string({
|
{
|
||||||
|
name: 'slug',
|
||||||
description: 'the slug of the fleet to make track the latest release',
|
description: 'the slug of the fleet to make track the latest release',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'fleet track-latest <slug>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(FleetTrackLatestCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetTrackLatestCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
94
lib/commands/fleets.ts
Normal file
94
lib/commands/fleets.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2016-2021 Balena Ltd.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { flags } from '@oclif/command';
|
||||||
|
|
||||||
|
import Command from '../command';
|
||||||
|
import * as cf from '../utils/common-flags';
|
||||||
|
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||||
|
import type { DataSetOutputOptions } from '../framework';
|
||||||
|
|
||||||
|
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||||
|
device_count: number;
|
||||||
|
online_devices: number;
|
||||||
|
device_type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlagsDef extends DataSetOutputOptions {
|
||||||
|
help: void;
|
||||||
|
verbose?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class FleetsCmd extends Command {
|
||||||
|
public static description = stripIndent`
|
||||||
|
List all fleets.
|
||||||
|
|
||||||
|
List all your balena fleets.
|
||||||
|
|
||||||
|
For detailed information on a particular fleet, use
|
||||||
|
\`balena fleet <fleet>\`
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static examples = ['$ balena fleets'];
|
||||||
|
|
||||||
|
public static usage = 'fleets';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
...cf.dataSetOutputFlags,
|
||||||
|
help: cf.help,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static authenticated = true;
|
||||||
|
public static primary = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { flags: options } = this.parse<FlagsDef, {}>(FleetsCmd);
|
||||||
|
|
||||||
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
|
// Get applications
|
||||||
|
const applications =
|
||||||
|
(await balena.models.application.getAllDirectlyAccessible({
|
||||||
|
$select: ['id', 'app_name', 'slug'],
|
||||||
|
$expand: {
|
||||||
|
is_for__device_type: { $select: 'slug' },
|
||||||
|
owns__device: { $select: 'is_online' },
|
||||||
|
},
|
||||||
|
})) as ExtendedApplication[];
|
||||||
|
|
||||||
|
// Add extended properties
|
||||||
|
applications.forEach((application) => {
|
||||||
|
application.device_count = application.owns__device?.length ?? 0;
|
||||||
|
application.online_devices =
|
||||||
|
application.owns__device?.filter((d) => d.is_online).length || 0;
|
||||||
|
application.device_type = application.is_for__device_type[0].slug;
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.outputData(
|
||||||
|
applications,
|
||||||
|
[
|
||||||
|
'id',
|
||||||
|
'app_name',
|
||||||
|
'slug',
|
||||||
|
'device_type',
|
||||||
|
'device_count',
|
||||||
|
'online_devices',
|
||||||
|
],
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -15,8 +15,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import Command from '../../command';
|
||||||
import { stripIndent } from '../../utils/lazy';
|
import { stripIndent } from '../../utils/lazy';
|
||||||
|
import { CommandHelp } from '../../utils/oclif-utils';
|
||||||
|
|
||||||
// 'Internal' commands are called during the execution of other commands.
|
// 'Internal' commands are called during the execution of other commands.
|
||||||
// `osinit` is called during `os initialize`
|
// `osinit` is called during `os initialize`
|
||||||
@ -26,6 +27,12 @@ import { stripIndent } from '../../utils/lazy';
|
|||||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
|
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
|
||||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
|
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
image: string;
|
||||||
|
type: string;
|
||||||
|
config: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class OsinitCmd extends Command {
|
export default class OsinitCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Do actual init of the device with the preconfigured os image.
|
Do actual init of the device with the preconfigured os image.
|
||||||
@ -34,24 +41,32 @@ export default class OsinitCmd extends Command {
|
|||||||
Use \`balena os initialize <image>\` instead.
|
Use \`balena os initialize <image>\` instead.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
public static args = {
|
public static args = [
|
||||||
image: Args.string({
|
{
|
||||||
|
name: 'image',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
type: Args.string({
|
{
|
||||||
|
name: 'type',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
config: Args.string({
|
{
|
||||||
|
name: 'config',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
|
public static usage = (
|
||||||
|
'internal osinit ' +
|
||||||
|
new CommandHelp({ args: OsinitCmd.args }).defaultUsage()
|
||||||
|
).trim();
|
||||||
|
|
||||||
public static hidden = true;
|
public static hidden = true;
|
||||||
public static root = true;
|
public static root = true;
|
||||||
public static offlineCompatible = true;
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(OsinitCmd);
|
const { args: params } = this.parse<{}, ArgsDef>(OsinitCmd);
|
||||||
|
|
||||||
const config = JSON.parse(params.config);
|
const config = JSON.parse(params.config);
|
||||||
|
|
@ -15,11 +15,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Flags, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import Command from '../command';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import * as cf from '../utils/common-flags';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||||
import { parseAsLocalHostnameOrIp } from '../../utils/validation';
|
import { applicationIdInfo } from '../utils/messages';
|
||||||
|
import { parseAsLocalHostnameOrIp } from '../utils/validation';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
fleet?: string;
|
||||||
|
pollInterval?: number;
|
||||||
|
help?: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
deviceIpOrHostname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class JoinCmd extends Command {
|
export default class JoinCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -52,31 +63,37 @@ export default class JoinCmd extends Command {
|
|||||||
'$ balena join 192.168.1.25 --fleet MyFleet',
|
'$ balena join 192.168.1.25 --fleet MyFleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [
|
||||||
deviceIpOrHostname: Args.string({
|
{
|
||||||
|
name: 'deviceIpOrHostname',
|
||||||
description: 'the IP or hostname of device',
|
description: 'the IP or hostname of device',
|
||||||
parse: parseAsLocalHostnameOrIp,
|
parse: parseAsLocalHostnameOrIp,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
// Hardcoded to preserve camelcase
|
||||||
|
public static usage = 'join [deviceIpOrHostname]';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
pollInterval: Flags.integer({
|
pollInterval: flags.integer({
|
||||||
description: 'the interval in minutes to check for updates',
|
description: 'the interval in minutes to check for updates',
|
||||||
char: 'i',
|
char: 'i',
|
||||||
}),
|
}),
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(JoinCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
JoinCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const promote = await import('../../utils/promote');
|
const promote = await import('../utils/promote');
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
const Logger = await import('../../utils/logger');
|
const logger = await Command.getLogger();
|
||||||
const logger = Logger.getLogger();
|
|
||||||
return promote.join(
|
return promote.join(
|
||||||
logger,
|
logger,
|
||||||
sdk,
|
sdk,
|
@ -15,13 +15,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import Command from '../../command';
|
||||||
|
import { ExpectedError } from '../../errors';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
export default class SSHKeyAddCmd extends Command {
|
interface FlagsDef {
|
||||||
public static aliases = ['key add'];
|
help: void;
|
||||||
public static deprecateAliases = true;
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class KeyAddCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Add an SSH key to balenaCloud.
|
Add an SSH key to balenaCloud.
|
||||||
|
|
||||||
@ -45,34 +54,45 @@ export default class SSHKeyAddCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena ssh-key add Main ~/.ssh/id_rsa.pub',
|
'$ balena key add Main ~/.ssh/id_rsa.pub',
|
||||||
'$ cat ~/.ssh/id_rsa.pub | balena ssh-key add Main',
|
'$ cat ~/.ssh/id_rsa.pub | balena key add Main',
|
||||||
'# Windows 10 (cmd.exe prompt) example',
|
'# Windows 10 (cmd.exe prompt) example',
|
||||||
'$ balena ssh-key add Main %userprofile%.sshid_rsa.pub',
|
'$ balena key add Main %userprofile%.sshid_rsa.pub',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [
|
||||||
name: Args.string({
|
{
|
||||||
|
name: 'name',
|
||||||
description: 'the SSH key name',
|
description: 'the SSH key name',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
path: Args.string({
|
{
|
||||||
|
name: `path`,
|
||||||
description: `the path to the public key file`,
|
description: `the path to the public key file`,
|
||||||
required: true,
|
},
|
||||||
}),
|
];
|
||||||
|
|
||||||
|
public static usage = 'key add <name> [path]';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public static readStdin = true;
|
||||||
const { args: params } = await this.parse(SSHKeyAddCmd);
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(KeyAddCmd);
|
||||||
|
|
||||||
const { readFile } = (await import('fs')).promises;
|
|
||||||
let key: string;
|
let key: string;
|
||||||
try {
|
if (params.path != null) {
|
||||||
|
const { readFile } = (await import('fs')).promises;
|
||||||
key = await readFile(params.path, 'utf8');
|
key = await readFile(params.path, 'utf8');
|
||||||
} catch {
|
} else if (this.stdin.length > 0) {
|
||||||
key = params.path;
|
key = this.stdin;
|
||||||
|
} else {
|
||||||
|
throw new ExpectedError('No public key file or path provided.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await getBalenaSdk().models.key.create(params.name, key);
|
await getBalenaSdk().models.key.create(params.name, key);
|
@ -15,34 +15,50 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import Command from '../../command';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import { parseAsInteger } from '../../utils/validation';
|
import { parseAsInteger } from '../../utils/validation';
|
||||||
|
|
||||||
export default class SSHKeyCmd extends Command {
|
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||||
public static aliases = ['key'];
|
|
||||||
public static deprecateAliases = true;
|
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class KeyCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Display an SSH key.
|
Display an SSH key.
|
||||||
|
|
||||||
Display a single SSH key registered in balenaCloud for the logged in user.
|
Display a single SSH key registered in balenaCloud for the logged in user.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
public static examples = ['$ balena ssh-key 17'];
|
public static examples = ['$ balena key 17'];
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
id: Args.integer({
|
{
|
||||||
|
name: 'id',
|
||||||
description: 'balenaCloud ID for the SSH key',
|
description: 'balenaCloud ID for the SSH key',
|
||||||
parse: (x) => parseAsInteger(x, 'id'),
|
parse: (x) => parseAsInteger(x, 'id'),
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'key <id>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(SSHKeyCmd);
|
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
|
||||||
|
|
||||||
const key = await getBalenaSdk().models.key.get(params.id);
|
const key = await getBalenaSdk().models.key.get(params.id);
|
||||||
|
|
@ -15,15 +15,24 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { parseAsInteger } from '../../utils/validation';
|
import { parseAsInteger } from '../../utils/validation';
|
||||||
|
|
||||||
export default class SSHKeyRmCmd extends Command {
|
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||||
public static aliases = ['key rm'];
|
|
||||||
public static deprecateAliases = true;
|
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
yes: boolean;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class KeyRmCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Remove an SSH key from balenaCloud.
|
Remove an SSH key from balenaCloud.
|
||||||
|
|
||||||
@ -32,27 +41,30 @@ export default class SSHKeyRmCmd extends Command {
|
|||||||
The --yes option may be used to avoid interactive confirmation.
|
The --yes option may be used to avoid interactive confirmation.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
public static examples = [
|
public static examples = ['$ balena key rm 17', '$ balena key rm 17 --yes'];
|
||||||
'$ balena ssh-key rm 17',
|
|
||||||
'$ balena ssh-key rm 17 --yes',
|
|
||||||
];
|
|
||||||
|
|
||||||
public static args = {
|
public static args: Array<IArg<any>> = [
|
||||||
id: Args.integer({
|
{
|
||||||
|
name: 'id',
|
||||||
description: 'balenaCloud ID for the SSH key',
|
description: 'balenaCloud ID for the SSH key',
|
||||||
parse: (x) => parseAsInteger(x, 'id'),
|
parse: (x) => parseAsInteger(x, 'id'),
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
public static flags = {
|
public static usage = 'key rm <id>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = await this.parse(SSHKeyRmCmd);
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
KeyRmCmd,
|
||||||
|
);
|
||||||
|
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
|
|
@ -15,24 +15,33 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import Command from '../command';
|
||||||
|
import * as cf from '../utils/common-flags';
|
||||||
|
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||||
|
|
||||||
export default class SSHKeyListCmd extends Command {
|
interface FlagsDef {
|
||||||
public static aliases = ['keys', 'key list'];
|
help: void;
|
||||||
public static deprecateAliases = true;
|
}
|
||||||
|
|
||||||
|
export default class KeysCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
List the SSH keys in balenaCloud.
|
List the SSH keys in balenaCloud.
|
||||||
|
|
||||||
List all SSH keys registered in balenaCloud for the logged in user.
|
List all SSH keys registered in balenaCloud for the logged in user.
|
||||||
`;
|
`;
|
||||||
public static examples = ['$ balena ssh-key list'];
|
public static examples = ['$ balena keys'];
|
||||||
|
|
||||||
|
public static usage = 'keys';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
await this.parse(SSHKeyListCmd);
|
this.parse<FlagsDef, {}>(KeysCmd);
|
||||||
|
|
||||||
const keys = await getBalenaSdk().models.key.getAll();
|
const keys = await getBalenaSdk().models.key.getAll();
|
||||||
|
|
@ -15,9 +15,19 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
import { stripIndent } from '../../utils/lazy';
|
import Command from '../command';
|
||||||
import { parseAsLocalHostnameOrIp } from '../../utils/validation';
|
import * as cf from '../utils/common-flags';
|
||||||
|
import { stripIndent } from '../utils/lazy';
|
||||||
|
import { parseAsLocalHostnameOrIp } from '../utils/validation';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help?: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
deviceIpOrHostname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class LeaveCmd extends Command {
|
export default class LeaveCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -41,22 +51,28 @@ export default class LeaveCmd extends Command {
|
|||||||
'$ balena leave 192.168.1.25',
|
'$ balena leave 192.168.1.25',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [
|
||||||
deviceIpOrHostname: Args.string({
|
{
|
||||||
|
name: 'deviceIpOrHostname',
|
||||||
description: 'the device IP or hostname',
|
description: 'the device IP or hostname',
|
||||||
parse: parseAsLocalHostnameOrIp,
|
parse: parseAsLocalHostnameOrIp,
|
||||||
}),
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'leave [deviceIpOrHostname]';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(LeaveCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(LeaveCmd);
|
||||||
|
|
||||||
const promote = await import('../../utils/promote');
|
const promote = await import('../utils/promote');
|
||||||
const Logger = await import('../../utils/logger');
|
const logger = await Command.getLogger();
|
||||||
const logger = Logger.getLogger();
|
|
||||||
return promote.leave(logger, params.deviceIpOrHostname);
|
return promote.leave(logger, params.deviceIpOrHostname);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,9 +15,20 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Args, Command } from '@oclif/core';
|
import { flags } from '@oclif/command';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import Command from '../../command';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
import { stripIndent } from '../../utils/lazy';
|
import { stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class LocalConfigureCmd extends Command {
|
export default class LocalConfigureCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
(Re)configure a balenaOS drive or image.
|
(Re)configure a balenaOS drive or image.
|
||||||
@ -30,18 +41,25 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
'$ balena local configure path/to/image.img',
|
'$ balena local configure path/to/image.img',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = {
|
public static args = [
|
||||||
target: Args.string({
|
{
|
||||||
|
name: 'target',
|
||||||
description: 'path of drive or image to configure',
|
description: 'path of drive or image to configure',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'local configure <target>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static root = true;
|
public static root = true;
|
||||||
public static offlineCompatible = true;
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = await this.parse(LocalConfigureCmd);
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);
|
||||||
|
|
||||||
const reconfix = await import('reconfix');
|
const reconfix = await import('reconfix');
|
||||||
const { denyMount, safeUmount } = await import('../../utils/umount');
|
const { denyMount, safeUmount } = await import('../../utils/umount');
|
||||||
@ -236,7 +254,7 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
const bootPartition = await getBootPartition(target);
|
const bootPartition = await getBootPartition(target);
|
||||||
|
|
||||||
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
|
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||||
return await _fs.promises.readdir(this.CONNECTIONS_FOLDER);
|
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
|
||||||
});
|
});
|
||||||
|
|
||||||
let connectionFileName;
|
let connectionFileName;
|
||||||
@ -245,11 +263,13 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
} else if (_.includes(files, 'resin-sample.ignore')) {
|
} else if (_.includes(files, 'resin-sample.ignore')) {
|
||||||
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
||||||
await imagefs.interact(target, bootPartition, async (_fs) => {
|
await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||||
const contents = await _fs.promises.readFile(
|
const readFileAsync = promisify(_fs.readFile);
|
||||||
|
const writeFileAsync = promisify(_fs.writeFile);
|
||||||
|
const contents = await readFileAsync(
|
||||||
`${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
|
`${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
|
||||||
{ encoding: 'utf8' },
|
{ encoding: 'utf8' },
|
||||||
);
|
);
|
||||||
await _fs.promises.writeFile(
|
return await writeFileAsync(
|
||||||
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||||
contents,
|
contents,
|
||||||
);
|
);
|
||||||
@ -266,13 +286,13 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
} else {
|
} else {
|
||||||
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
|
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
|
||||||
await imagefs.interact(target, bootPartition, async (_fs) => {
|
await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||||
await _fs.promises.writeFile(
|
return await promisify(_fs.writeFile)(
|
||||||
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||||
this.CONNECTION_FILE,
|
this.CONNECTION_FILE,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this.getConfigurationSchema(bootPartition, connectionFileName);
|
return await this.getConfigurationSchema(bootPartition, connectionFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeHostname(schema: any) {
|
async removeHostname(schema: any) {
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user