mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
88 Commits
Author | SHA1 | Date | |
---|---|---|---|
ba4b9bd447 | |||
02c0ea5b59 | |||
bc3558dd8e | |||
aad62d1ccd | |||
ecc6f80164 | |||
c0fd1e3886 | |||
9d3120b144 | |||
ed0e03ddb2 | |||
8fe6d6c026 | |||
727033ae14 | |||
c19ce6a905 | |||
1a33029738 | |||
043bc48a1c | |||
a10156a441 | |||
4f665f43d2 | |||
9f097a96f5 | |||
64d1943804 | |||
666ce876e6 | |||
e01184080f | |||
93039b010d | |||
795259bf30 | |||
fa134d2d39 | |||
bef5221ed8 | |||
72d6db796c | |||
e848eb63ee | |||
6f0f7350cf | |||
07a88c700e | |||
9cae66bd92 | |||
cddea24cef | |||
b1c246c0b4 | |||
00b4d57a03 | |||
2cba82e914 | |||
1352c5c823 | |||
c86eb97010 | |||
53be743b9d | |||
d9f21b4c3f | |||
261ab398dd | |||
f28a9992e4 | |||
29e7827eb1 | |||
1d77cf3665 | |||
017c767f61 | |||
7d79c4e24b | |||
60bc5092e0 | |||
a33a794931 | |||
f0ede6fca2 | |||
dbe177e766 | |||
09f80730a8 | |||
327d28c103 | |||
56ab785a82 | |||
305d65d5ed | |||
c4d3686a34 | |||
ce06854b55 | |||
8db05cc8a7 | |||
7a22c987d2 | |||
45efbcdfe3 | |||
d6a9b78b3e | |||
e8ac3ea960 | |||
0ffa0f85a2 | |||
5e7479f60e | |||
07365c45f2 | |||
e5076434c6 | |||
5d687f5a55 | |||
e192767156 | |||
5a8d2fad5f | |||
45f482fad1 | |||
c0e7ae9c91 | |||
36077cacda | |||
82b9983450 | |||
703dbd01c9 | |||
602e63c8a9 | |||
2ab635f49a | |||
322736a145 | |||
c347b67b25 | |||
4022beeb56 | |||
ccf97cfc9f | |||
9c5fe14f2e | |||
38e29251e7 | |||
bfc7a14646 | |||
610db81fcb | |||
d1f7d6d07f | |||
694eb78aaa | |||
1caccafbcd | |||
61d4d1f1e7 | |||
a01c85bc15 | |||
5d7b7cfc6f | |||
92fd9e0883 | |||
24273b5ac0 | |||
6155509f4c |
@ -1,2 +0,0 @@
|
||||
/completion/*
|
||||
/bin/*
|
21
.eslintrc.js
21
.eslintrc.js
@ -1,21 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@balena/lint/config/.eslintrc.js'],
|
||||
parserOptions: {
|
||||
project: 'tsconfig.dev.json',
|
||||
},
|
||||
root: true,
|
||||
rules: {
|
||||
ignoreDefinitionFiles: 0,
|
||||
// to avoid the `warning Forbidden non-null assertion @typescript-eslint/no-non-null-assertion`
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
},
|
||||
};
|
4
.github/actions/publish/action.yml
vendored
4
.github/actions/publish/action.yml
vendored
@ -39,7 +39,7 @@ runs:
|
||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
@ -135,7 +135,7 @@ runs:
|
||||
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
with:
|
||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
|
||||
path: dist
|
||||
|
4
.github/actions/test/action.yml
vendored
4
.github/actions/test/action.yml
vendored
@ -26,7 +26,7 @@ runs:
|
||||
steps:
|
||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
@ -58,7 +58,7 @@ runs:
|
||||
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
|
4
.github/workflows/flowzone.yml
vendored
4
.github/workflows/flowzone.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
"os": [
|
||||
["self-hosted", "X64"],
|
||||
["self-hosted", "ARM64"],
|
||||
["macos-12"],
|
||||
["macos-13"],
|
||||
["windows-2019"],
|
||||
["macos-latest-xlarge"]
|
||||
]
|
||||
@ -36,7 +36,7 @@ jobs:
|
||||
"os": [
|
||||
["self-hosted", "X64"],
|
||||
["self-hosted", "ARM64"],
|
||||
["macos-12"],
|
||||
["macos-13"],
|
||||
["windows-2019"],
|
||||
["macos-latest-xlarge"]
|
||||
]
|
||||
|
File diff suppressed because it is too large
Load Diff
226
CHANGELOG.md
226
CHANGELOG.md
@ -4,6 +4,232 @@ All notable changes to this project will be documented in this file
|
||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 21.0.0 - 2025-03-11
|
||||
|
||||
* Drop support for OS versions <2.14.0 [myarmolinsky]
|
||||
* api-key generate: Add required argument `expiryDate` [myarmolinsky]
|
||||
* Update `balena-preload` to 18.0.1 [myarmolinsky]
|
||||
* Add dependency `date-fns` [myarmolinsky]
|
||||
* Update `balena-sdk` to 21.2.1 [myarmolinsky]
|
||||
|
||||
## 20.2.10 - 2025-03-10
|
||||
|
||||
* Update TypeScript to 5.8.2 [Thodoris Greasidis]
|
||||
|
||||
## 20.2.9 - 2025-02-26
|
||||
|
||||
* Fix CORS issue with X-Balena-Client header [Thodoris Greasidis]
|
||||
|
||||
## 20.2.8 - 2025-02-26
|
||||
|
||||
* Update balena-config-json dependency and fix test [Ken Bannister]
|
||||
|
||||
## 20.2.7 - 2025-02-25
|
||||
|
||||
* Use the CLI version in the X-Balena-Client header [Thodoris Greasidis]
|
||||
|
||||
## 20.2.6 - 2025-02-25
|
||||
|
||||
* Update actions/upload-artifact digest to 4cec3d8 [balena-renovate[bot]]
|
||||
|
||||
## 20.2.5 - 2025-02-25
|
||||
|
||||
* Update actions/setup-node digest to 1d0ff46 [balena-renovate[bot]]
|
||||
|
||||
## 20.2.4 - 2025-02-25
|
||||
|
||||
* Pin docker-modem and dockerode to avoid regression [Ken Bannister]
|
||||
|
||||
## 20.2.3 - 2025-01-15
|
||||
|
||||
* Remove unused old eslint version files [Otavio Jacobi]
|
||||
|
||||
## 20.2.2 - 2025-01-12
|
||||
|
||||
* Use the promises namespace of balena-image-fs [Thodoris Greasidis]
|
||||
|
||||
<details>
|
||||
<summary> Update balena-device-init to 8.1.3 & balena-image-fs to 7.3.0 [Thodoris Greasidis] </summary>
|
||||
|
||||
> ### balena-image-fs-7.3.0 - 2025-01-06
|
||||
>
|
||||
> * Drop Bluebird from devDependencies [Thodoris Greasidis]
|
||||
> * flowzone: Remove empty with clause [Thodoris Greasidis]
|
||||
> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-image-fs-7.2.2 - 2024-01-02
|
||||
>
|
||||
> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
|
||||
>
|
||||
> ### balena-image-fs-7.2.1 - 2023-12-19
|
||||
>
|
||||
> * Remove repo config from flowzone.yml [Kyle Harding]
|
||||
>
|
||||
> ### balena-image-fs-7.2.0 - 2023-01-20
|
||||
>
|
||||
> * Add support for Node 18 [Akis Kesoglou]
|
||||
>
|
||||
> ### balena-image-fs-7.1.2 - 2023-01-05
|
||||
>
|
||||
> * Update dependencies [ab77]
|
||||
>
|
||||
> ### balena-image-fs-7.1.1 - 2022-12-20
|
||||
>
|
||||
> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
|
||||
>
|
||||
> ### balena-image-fs-7.1.0 - 2022-12-13
|
||||
>
|
||||
> * update dependencies [Zane Hitchcox]
|
||||
>
|
||||
> ### balena-device-init-8.1.3 - 2025-01-09
|
||||
>
|
||||
> * README: Remove the travisci badge [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-device-init-8.1.2 - 2025-01-09
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Use the promises namespace of balena-image-fs [Thodoris Greasidis] </summary>
|
||||
>
|
||||
>> #### balena-image-fs-7.3.0 - 2025-01-06
|
||||
>>
|
||||
>> * Drop Bluebird from devDependencies [Thodoris Greasidis]
|
||||
>> * flowzone: Remove empty with clause [Thodoris Greasidis]
|
||||
>> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
|
||||
>>
|
||||
>> #### balena-image-fs-7.2.2 - 2024-01-02
|
||||
>>
|
||||
>> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
|
||||
>>
|
||||
>> #### balena-image-fs-7.2.1 - 2023-12-19
|
||||
>>
|
||||
>> * Remove repo config from flowzone.yml [Kyle Harding]
|
||||
>>
|
||||
>> #### balena-image-fs-7.2.0 - 2023-01-20
|
||||
>>
|
||||
>> * Add support for Node 18 [Akis Kesoglou]
|
||||
>>
|
||||
>> #### balena-image-fs-7.1.2 - 2023-01-05
|
||||
>>
|
||||
>> * Update dependencies [ab77]
|
||||
>>
|
||||
>> #### balena-image-fs-7.1.1 - 2022-12-20
|
||||
>>
|
||||
>> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
|
||||
>>
|
||||
>> #### balena-image-fs-7.1.0 - 2022-12-13
|
||||
>>
|
||||
>> * update dependencies [Zane Hitchcox]
|
||||
>>
|
||||
>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ### balena-device-init-8.1.1 - 2025-01-06
|
||||
>
|
||||
> * Convert some parts to async await and simplify [Thodoris Greasidis]
|
||||
> * Avoid unnecessary destructuring [Thodoris Greasidis]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
## 20.2.1 - 2025-01-01
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update balena-preload to 17.0.0 [Thodoris Greasidis] </summary>
|
||||
|
||||
> ### balena-preload-17.0.0 - 2024-10-21
|
||||
>
|
||||
> * Improve typings [Thodoris Greasidis]
|
||||
> * Stop returning Bluebird promises & drop it from the dependencies [Thodoris Greasidis]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
## 20.2.0 - 2024-12-31
|
||||
|
||||
|
||||
<details>
|
||||
<summary> os configure: Give precedence to the boot partition located in the image over the device-type.json contents [Thodoris Greasidis] </summary>
|
||||
|
||||
> ### balena-device-init-8.1.0 - Invalid date
|
||||
>
|
||||
> * Try to find the boot partition by inspecting the image [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-device-init-8.0.1 - 2024-12-19
|
||||
>
|
||||
> * Drop the unnecessary eslint.config.js [Thodoris Greasidis]
|
||||
> * packacke.json: Explicitly set type commonjs [Thodoris Greasidis]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
## 20.1.6 - 2024-12-30
|
||||
|
||||
* Add more realistic os configure tests [Thodoris Greasidis]
|
||||
|
||||
## 20.1.5 - 2024-12-20
|
||||
|
||||
* Update shrinkwrapped express to v4.21.2 [Oskar Williams]
|
||||
|
||||
## 20.1.4 - 2024-12-20
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update balena-device-init to 8.0.0 [Thodoris Greasidis] </summary>
|
||||
|
||||
> ### balena-device-init-8.0.0 - 2024-12-18
|
||||
>
|
||||
> * Avoid running linting in the custom tests [Thodoris Greasidis]
|
||||
> * Stop returning Bluebird promises [Thodoris Greasidis]
|
||||
> * package: Publish only the build & typings folders [Thodoris Greasidis]
|
||||
> * Convert to TypeScript with es6 module notation [Thodoris Greasidis]
|
||||
> * Replace gulp, coffeelint & mochainon with tsc, @balena/lint, mocha, chai & sinon [Thodoris Greasidis]
|
||||
> * Drop support for node <20.6.0 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-device-init-7.0.2 - 2024-12-17
|
||||
>
|
||||
> * flowzone: Update runner versions [Thodoris Greasidis]
|
||||
> * Pin etcher-sdk to 9.0.8 to match resin-device-operations and fix tests [Thodoris Greasidis]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
## 20.1.3 - 2024-12-20
|
||||
|
||||
* Update oclif to 4.17.0 and @oclif/core 4.1.0 [Otavio Jacobi]
|
||||
|
||||
## 20.1.2 - 2024-12-17
|
||||
|
||||
* Remove unnecessary `Promise.resolve` and `Promise.reject` [Pagan Gazzard]
|
||||
|
||||
## 20.1.1 - 2024-12-16
|
||||
|
||||
* Update @balena/lint to v9.1.3 [Otavio Jacobi]
|
||||
|
||||
## 20.1.0 - 2024-12-12
|
||||
|
||||
* `device os-update`: Add handling for updates that require takeover [myarmolinsky]
|
||||
* Update `balena-sdk` [myarmolinsky]
|
||||
* Update `@balena/compose` [myarmolinsky]
|
||||
|
||||
## 20.0.9 - 2024-12-05
|
||||
|
||||
* Update shrinkwrapped express to v4.21.1 [Oskar Williams]
|
||||
|
||||
## 20.0.8 - 2024-12-04
|
||||
|
||||
* Run test and publish with macos-13 [Otavio Jacobi]
|
||||
|
||||
## 20.0.7 - 2024-11-23
|
||||
|
||||
* Update TypeScript to 5.7.2 [Thodoris Greasidis]
|
||||
|
||||
## 20.0.6 - 2024-11-08
|
||||
|
||||
* Refactor balena build for clarity [Thodoris Greasidis]
|
||||
|
||||
## 20.0.5 - 2024-11-05
|
||||
|
||||
* Update actions/upload-artifact digest to b4b15b8 [balena-renovate[bot]]
|
||||
|
@ -305,7 +305,7 @@ async function zipPkg() {
|
||||
);
|
||||
}
|
||||
await fs.mkdirp(path.dirname(outputFile));
|
||||
await new Promise((resolve, reject) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
console.log(`Zipping standalone package to "${outputFile}"...`);
|
||||
|
||||
const archive = archiver('zip', {
|
||||
|
@ -145,7 +145,7 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
|
||||
throw new Error(`Error parsing section title`);
|
||||
}
|
||||
// match[1] has the title, match[2] has the rest
|
||||
return match && match[2];
|
||||
return match?.[2];
|
||||
}),
|
||||
mdParser.getSectionOfTitle('Installation'),
|
||||
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),
|
||||
|
@ -3,7 +3,7 @@ import * as semver from 'semver';
|
||||
|
||||
const changeTypes = ['major', 'minor', 'patch'] as const;
|
||||
|
||||
const validateChangeType = (maybeChangeType: string = 'minor') => {
|
||||
const validateChangeType = (maybeChangeType = 'minor') => {
|
||||
maybeChangeType = maybeChangeType.toLowerCase();
|
||||
switch (maybeChangeType) {
|
||||
case 'patch':
|
||||
@ -136,5 +136,4 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
main();
|
||||
void main();
|
||||
|
@ -19,6 +19,7 @@ import { spawn } from 'child_process';
|
||||
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, '..');
|
||||
|
||||
@ -101,7 +102,6 @@ export function loadPackageJson() {
|
||||
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
|
||||
*/
|
||||
export async function which(program: string): Promise<string> {
|
||||
const whichMod = await import('which');
|
||||
let programPath: string;
|
||||
try {
|
||||
programPath = await whichMod(program);
|
||||
@ -132,7 +132,7 @@ export async function whichSpawn(
|
||||
.on('error', reject)
|
||||
.on('close', resolve);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
reject(err as Error);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
@ -57,7 +57,10 @@ require('ts-node').register({
|
||||
project: path.join(rootDir, 'tsconfig.json'),
|
||||
transpileOnly: true,
|
||||
});
|
||||
require('../src/app').run(undefined, { dir: __dirname, development: 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) {
|
||||
|
@ -5,7 +5,7 @@
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
// Disable oclif registering ts-node
|
||||
process.env.OCLIF_TS_NODE = 0;
|
||||
process.env.OCLIF_TS_NODE = '0';
|
||||
|
||||
async function run() {
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
@ -18,4 +18,4 @@ async function run() {
|
||||
await require('../build/app').run(undefined, { dir: __dirname });
|
||||
}
|
||||
|
||||
run();
|
||||
void run();
|
||||
|
@ -326,6 +326,8 @@ or to authenticate requests to the API with an 'Authorization: Bearer <key>' hea
|
||||
Examples:
|
||||
|
||||
$ balena api-key generate "Jenkins Key"
|
||||
$ balena api-key generate "Jenkins Key" 2025-10-30
|
||||
$ balena api-key generate "Jenkins Key" never
|
||||
|
||||
### Arguments
|
||||
|
||||
@ -333,6 +335,10 @@ Examples:
|
||||
|
||||
the API key name
|
||||
|
||||
#### EXPIRYDATE
|
||||
|
||||
the expiry date of the API key as an ISO date string, or "never" for no expiry
|
||||
|
||||
## api-key list
|
||||
|
||||
### Aliases
|
||||
|
32
eslint.config.js
Normal file
32
eslint.config.js
Normal file
@ -0,0 +1,32 @@
|
||||
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: '^_',
|
||||
}],
|
||||
},
|
||||
}),
|
||||
];
|
5728
npm-shrinkwrap.json
generated
5728
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "20.0.5",
|
||||
"version": "21.0.0",
|
||||
"description": "The official balena Command Line Interface",
|
||||
"main": "./build/app.js",
|
||||
"homepage": "https://github.com/balena-io/balena-cli",
|
||||
@ -58,6 +58,7 @@
|
||||
"build:completion": "node completion/generate-completion.js",
|
||||
"build:standalone": "ts-node --transpile-only automation/run.ts build:standalone",
|
||||
"build:installer": "ts-node --transpile-only automation/run.ts build:installer",
|
||||
"deduplicate-dependencies": "npm dd && git add npm-shrinkwrap.json && git commit --message \"Deduplicate dependencies\"",
|
||||
"package": "npm run build:fast && npm run build:standalone && npm run build:installer",
|
||||
"pretest": "npm run build",
|
||||
"test": "npm run test:shrinkwrap && npm run test:core",
|
||||
@ -111,7 +112,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@balena/lint": "^8.0.0",
|
||||
"@balena/lint": "^9.1.3",
|
||||
"@electron/notarize": "^2.0.0",
|
||||
"@types/archiver": "^6.0.2",
|
||||
"@types/bluebird": "^3.5.36",
|
||||
@ -180,27 +181,27 @@
|
||||
"mock-fs": "^5.2.0",
|
||||
"mock-require": "^3.0.3",
|
||||
"nock": "^13.2.1",
|
||||
"oclif": "^4.14.0",
|
||||
"oclif": "^4.17.0",
|
||||
"rewire": "^7.0.0",
|
||||
"simple-git": "^3.14.1",
|
||||
"sinon": "^19.0.0",
|
||||
"string-to-stream": "^3.0.1",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^5.6.2"
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@balena/compose": "^5.0.0",
|
||||
"@balena/compose": "^6.0.0",
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"@balena/env-parsing": "^1.1.8",
|
||||
"@balena/es-version": "^1.0.1",
|
||||
"@oclif/core": "^4.0.31",
|
||||
"@oclif/core": "^4.1.0",
|
||||
"@sentry/node": "^6.16.1",
|
||||
"balena-config-json": "^4.2.0",
|
||||
"balena-device-init": "^7.0.1",
|
||||
"balena-config-json": "^4.2.2",
|
||||
"balena-device-init": "^8.1.3",
|
||||
"balena-errors": "^4.7.3",
|
||||
"balena-image-fs": "^7.0.6",
|
||||
"balena-preload": "^16.0.0",
|
||||
"balena-sdk": "^20.3.0",
|
||||
"balena-image-fs": "^7.3.0",
|
||||
"balena-preload": "^18.0.1",
|
||||
"balena-sdk": "^21.2.1",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^5.0.2",
|
||||
"balena-settings-storage": "^8.1.0",
|
||||
@ -211,10 +212,11 @@
|
||||
"cli-truncate": "^2.1.0",
|
||||
"color-hash": "^1.1.1",
|
||||
"common-tags": "^1.7.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"denymount": "^2.3.0",
|
||||
"docker-modem": "^5.0.3",
|
||||
"docker-modem": "5.0.5",
|
||||
"docker-progress": "^5.1.3",
|
||||
"dockerode": "^4.0.2",
|
||||
"dockerode": "4.0.3",
|
||||
"ejs": "^3.1.6",
|
||||
"etcher-sdk": "9.1.0",
|
||||
"express": "^4.17.2",
|
||||
@ -274,6 +276,6 @@
|
||||
}
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2024-11-05T10:12:23.858Z"
|
||||
"publishedAt": "2025-03-11T14:42:29.446Z"
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
|
||||
index ef7f90e..8264b7c 100644
|
||||
index bfe9205..482519e 100644
|
||||
--- a/node_modules/oclif/lib/commands/pack/win.js
|
||||
+++ b/node_modules/oclif/lib/commands/pack/win.js
|
||||
@@ -76,6 +76,12 @@ InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
|
||||
@@ -86,6 +86,12 @@ InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
|
||||
${customization}
|
||||
|
||||
Section "${config.name} CLI \${VERSION}"
|
||||
@ -16,20 +16,18 @@ index ef7f90e..8264b7c 100644
|
||||
File /r bin
|
||||
File /r client
|
||||
diff --git a/node_modules/oclif/lib/tarballs/build.js b/node_modules/oclif/lib/tarballs/build.js
|
||||
index 14d5a6e..7b42a6f 100644
|
||||
index f0c8d95..a72400e 100644
|
||||
--- a/node_modules/oclif/lib/tarballs/build.js
|
||||
+++ b/node_modules/oclif/lib/tarballs/build.js
|
||||
@@ -200,6 +200,13 @@ const extractCLI = async (tarball, c) => {
|
||||
@@ -218,6 +218,11 @@ const extractCLI = async (tarball, c) => {
|
||||
(0, promises_1.rm)(path.join(workspace, path.basename(tarball)), { recursive: true }),
|
||||
(0, fs_extra_1.remove)(path.join(workspace, 'bin', 'run.cmd')),
|
||||
]);
|
||||
+
|
||||
+ // The oclif installers are a production installation, while the source
|
||||
+ // `bin` folder may contain a `.fast-boot.json` file of a dev installation.
|
||||
+ // This has previously led to issues preventing the CLI from starting, so
|
||||
+ // delete `.fast-boot.json` (if any) from the destination folder.
|
||||
+ await (0, fs_extra_1.remove)(path.join(workspace, 'bin', '.fast-boot.json'));
|
||||
+
|
||||
};
|
||||
const buildTarget = async (target, c, options) => {
|
||||
const workspace = c.workspace(target);
|
||||
if (target.platform === 'win32' && target.arch === 'arm64' && (0, semver_1.lt)(c.nodeVersion, '20.0.0')) {
|
4
repo.yml
4
repo.yml
@ -6,6 +6,10 @@ upstream:
|
||||
url: 'https://github.com/balena-io/balena-sdk'
|
||||
- repo: 'balena-config-json'
|
||||
url: 'https://github.com/balena-io-modules/balena-config-json'
|
||||
- repo: 'balena-image-fs'
|
||||
url: 'https://github.com/balena-io-modules/balena-image-fs'
|
||||
- repo: 'balena-device-init'
|
||||
url: 'https://github.com/balena-io-modules/balena-device-init'
|
||||
- repo: 'balena-image-manager'
|
||||
url: 'https://github.com/balena-io-modules/balena-image-manager'
|
||||
- repo: 'balena-preload'
|
||||
|
@ -101,11 +101,9 @@ async function init() {
|
||||
|
||||
/** Execute the oclif parser and the CLI command. */
|
||||
async function oclifRun(command: string[], options: AppOptions) {
|
||||
let deprecationPromise: Promise<void>;
|
||||
let deprecationPromise: Promise<void> | undefined;
|
||||
// check and enforce the CLI's deprecation policy
|
||||
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
|
||||
deprecationPromise = Promise.resolve();
|
||||
} else {
|
||||
if (!(unsupportedFlag || process.env.BALENARC_UNSUPPORTED)) {
|
||||
const { DeprecationChecker } = await import('./deprecation');
|
||||
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
||||
// warnAndAbortIfDeprecated uses previously cached data only
|
||||
|
@ -17,7 +17,16 @@
|
||||
|
||||
import { Args, Command } from '@oclif/core';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
formatDuration,
|
||||
intervalToDuration,
|
||||
isValid,
|
||||
parseISO,
|
||||
} from 'date-fns';
|
||||
|
||||
// In days
|
||||
const durations = [1, 7, 30, 90];
|
||||
|
||||
async function isLoggedInWithJwt() {
|
||||
const balena = getBalenaSdk();
|
||||
@ -41,13 +50,21 @@ export default class GenerateCmd extends Command {
|
||||
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 examples = [
|
||||
'$ balena api-key generate "Jenkins Key"',
|
||||
'$ balena api-key generate "Jenkins Key" 2025-10-30',
|
||||
'$ balena api-key generate "Jenkins Key" never',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
name: Args.string({
|
||||
description: 'the API key name',
|
||||
required: true,
|
||||
}),
|
||||
expiryDate: Args.string({
|
||||
description:
|
||||
'the expiry date of the API key as an ISO date string, or "never" for no expiry',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
@ -55,9 +72,61 @@ export default class GenerateCmd extends Command {
|
||||
public async run() {
|
||||
const { args: params } = await this.parse(GenerateCmd);
|
||||
|
||||
let expiryDateResponse: string | number | undefined = params.expiryDate;
|
||||
let key;
|
||||
try {
|
||||
key = await getBalenaSdk().models.apiKey.create(params.name);
|
||||
if (!expiryDateResponse) {
|
||||
expiryDateResponse = await getCliForm().ask({
|
||||
message: 'Please pick an expiry date for the API key',
|
||||
type: 'list',
|
||||
choices: [...durations, 'custom', 'never'].map((duration) => ({
|
||||
name:
|
||||
duration === 'never'
|
||||
? 'No expiration'
|
||||
: typeof duration === 'number'
|
||||
? formatDuration(
|
||||
intervalToDuration({
|
||||
start: 0,
|
||||
end: duration * 24 * 60 * 60 * 1000,
|
||||
}),
|
||||
)
|
||||
: 'Custom expiration',
|
||||
value: duration,
|
||||
})),
|
||||
});
|
||||
}
|
||||
let expiryDate: Date | null;
|
||||
if (expiryDateResponse === 'never') {
|
||||
expiryDate = null;
|
||||
} else if (expiryDateResponse === 'custom') {
|
||||
do {
|
||||
expiryDate = parseISO(
|
||||
await getCliForm().ask({
|
||||
message:
|
||||
'Please enter an expiry date for the API key as an ISO date string',
|
||||
type: 'input',
|
||||
}),
|
||||
);
|
||||
if (!isValid(expiryDate)) {
|
||||
console.error('Invalid date format');
|
||||
}
|
||||
} while (!isValid(expiryDate));
|
||||
} else if (typeof expiryDateResponse === 'string') {
|
||||
expiryDate = parseISO(expiryDateResponse);
|
||||
if (!isValid(expiryDate)) {
|
||||
throw new Error(
|
||||
'Invalid date format, please use a valid ISO date string',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
expiryDate = new Date(
|
||||
Date.now() + expiryDateResponse * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
key = await getBalenaSdk().models.apiKey.create({
|
||||
name: params.name,
|
||||
expiryDate: expiryDate === null ? null : expiryDate.toISOString(),
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name === 'BalenaNotLoggedIn') {
|
||||
if (await isLoggedInWithJwt()) {
|
||||
|
@ -50,9 +50,9 @@ export default class RevokeCmd extends Command {
|
||||
return;
|
||||
}
|
||||
await Promise.all(
|
||||
apiKeyIds.map(
|
||||
async (id) => await getBalenaSdk().models.apiKey.revoke(Number(id)),
|
||||
),
|
||||
apiKeyIds.map(async (id) => {
|
||||
await getBalenaSdk().models.apiKey.revoke(Number(id));
|
||||
}),
|
||||
);
|
||||
console.log('Successfully revoked the given API keys');
|
||||
}
|
||||
|
@ -36,14 +36,16 @@ import { buildProject, composeCliFlags } from '../../utils/compose_ts';
|
||||
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
||||
import { dockerCliFlags } from '../../utils/docker';
|
||||
|
||||
// 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 {
|
||||
type ComposeGenerateOptsParam = Parameters<typeof compose.generateOpts>[0];
|
||||
|
||||
interface PrepareBuildOpts
|
||||
extends ComposeCliFlags,
|
||||
DockerCliFlags,
|
||||
ComposeGenerateOptsParam {
|
||||
arch?: string;
|
||||
deviceType?: string;
|
||||
fleet?: string;
|
||||
source?: string; // Not part of command profile - source param copied here.
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export default class BuildCmd extends Command {
|
||||
@ -113,29 +115,31 @@ ${dockerignoreHelp}
|
||||
const logger = Logger.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
// `build` accepts `source` as a parameter, but compose expects it as an option
|
||||
options.source = params.source;
|
||||
delete params.source;
|
||||
const prepareBuildOpts = {
|
||||
...options,
|
||||
source: params.source,
|
||||
};
|
||||
|
||||
await this.resolveArchFromDeviceType(sdk, options);
|
||||
await this.resolveArchFromDeviceType(sdk, prepareBuildOpts);
|
||||
|
||||
await this.validateOptions(options, sdk);
|
||||
await this.validateOptions(prepareBuildOpts, sdk);
|
||||
|
||||
// Build args are under consideration for removal - warn user
|
||||
if (options.buildArg) {
|
||||
if (prepareBuildOpts.buildArg) {
|
||||
console.log(buildArgDeprecation);
|
||||
}
|
||||
|
||||
const app = await this.getAppAndResolveArch(options);
|
||||
const app = await this.getAppAndResolveArch(prepareBuildOpts);
|
||||
|
||||
const { docker, buildOpts, composeOpts } = await this.prepareBuild(options);
|
||||
const { docker, buildOpts, composeOpts } =
|
||||
await this.prepareBuild(prepareBuildOpts);
|
||||
|
||||
try {
|
||||
await this.buildProject(docker, logger, composeOpts, {
|
||||
appType: app?.application_type?.[0],
|
||||
arch: options.arch!,
|
||||
deviceType: options.deviceType!,
|
||||
buildEmulated: options.emulated,
|
||||
arch: prepareBuildOpts.arch!,
|
||||
deviceType: prepareBuildOpts.deviceType!,
|
||||
buildEmulated: prepareBuildOpts.emulated,
|
||||
buildOpts,
|
||||
});
|
||||
} catch (err) {
|
||||
@ -147,7 +151,7 @@ ${dockerignoreHelp}
|
||||
logger.logSuccess('Build succeeded!');
|
||||
}
|
||||
|
||||
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
|
||||
protected async validateOptions(opts: PrepareBuildOpts, sdk: BalenaSDK) {
|
||||
// Validate option combinations
|
||||
if (
|
||||
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
||||
@ -175,7 +179,10 @@ ${dockerignoreHelp}
|
||||
opts['registry-secrets'] = registrySecrets;
|
||||
}
|
||||
|
||||
protected async resolveArchFromDeviceType(sdk: BalenaSDK, opts: FlagsDef) {
|
||||
protected async resolveArchFromDeviceType(
|
||||
sdk: BalenaSDK,
|
||||
opts: PrepareBuildOpts,
|
||||
) {
|
||||
if (opts.deviceType != null && opts.arch == null) {
|
||||
try {
|
||||
const deviceTypeOpts = {
|
||||
@ -208,7 +215,7 @@ ${dockerignoreHelp}
|
||||
}
|
||||
}
|
||||
|
||||
protected async getAppAndResolveArch(opts: FlagsDef) {
|
||||
protected async getAppAndResolveArch(opts: PrepareBuildOpts) {
|
||||
if (opts.fleet) {
|
||||
const { getAppWithArch } = await import('../../utils/helpers');
|
||||
const app = await getAppWithArch(opts.fleet);
|
||||
@ -218,7 +225,7 @@ ${dockerignoreHelp}
|
||||
}
|
||||
}
|
||||
|
||||
protected async prepareBuild(options: FlagsDef) {
|
||||
protected async prepareBuild(options: PrepareBuildOpts) {
|
||||
const { getDocker, generateBuildOpts } = await import('../../utils/docker');
|
||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||
getDocker(options),
|
||||
|
@ -91,7 +91,7 @@ export default class DeviceDetectCmd extends Command {
|
||||
try {
|
||||
await docker.ping();
|
||||
return true;
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
|
@ -155,7 +155,7 @@ export default class DeviceInitCmd extends Command {
|
||||
try {
|
||||
logger.logDebug(`Process failed, removing device ${device.uuid}`);
|
||||
await balena.models.device.remove(device.uuid);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore removal failures, and throw original error
|
||||
}
|
||||
throw e;
|
||||
|
@ -135,7 +135,7 @@ export default class DeviceLogsCmd extends Command {
|
||||
logger.logDebug('Checking we can access device');
|
||||
try {
|
||||
await deviceApi.ping();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(
|
||||
`Cannot access device at address ${params.device}. Device may not be in local mode.`,
|
||||
|
@ -20,6 +20,7 @@ import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import type { Device } from 'balena-sdk';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import { getExpandedProp } from '../../utils/pine';
|
||||
|
||||
export default class DeviceOsUpdateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
@ -126,20 +127,46 @@ export default class DeviceOsUpdateCmd extends Command {
|
||||
);
|
||||
}
|
||||
} 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({
|
||||
message: 'Target OS version',
|
||||
type: 'list',
|
||||
choices: hupVersionInfo.versions.map((version) => ({
|
||||
name:
|
||||
hupVersionInfo.recommended === version
|
||||
? `${version} (recommended)`
|
||||
: version,
|
||||
value: version,
|
||||
})),
|
||||
choices,
|
||||
});
|
||||
}
|
||||
|
||||
const takeoverRequired =
|
||||
(await sdk.models.os.getOsUpdateType(
|
||||
getExpandedProp(is_of__device_type, 'slug')!,
|
||||
currentOsVersion,
|
||||
targetOsVersion,
|
||||
)) === 'takeover';
|
||||
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
|
||||
await patterns.confirm(
|
||||
options.yes || false,
|
||||
|
@ -76,6 +76,6 @@ export default class DeviceRegisterCmd extends Command {
|
||||
options.deviceType,
|
||||
);
|
||||
|
||||
return result && result.uuid;
|
||||
return result.uuid;
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ export default class DeviceSSHCmd extends Command {
|
||||
SSH server port number (default 22222) if the target is an IP address or .local
|
||||
hostname. Otherwise, port number for the balenaCloud gateway (default 22).`,
|
||||
char: 'p',
|
||||
parse: async (p) => parseAsInteger(p, 'port'),
|
||||
parse: (p) => parseAsInteger(p, 'port'),
|
||||
}),
|
||||
tty: Flags.boolean({
|
||||
default: false,
|
||||
@ -110,13 +110,14 @@ export default class DeviceSSHCmd extends Command {
|
||||
// Local connection
|
||||
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
|
||||
const { performLocalDeviceSSH } = await import('../../utils/device/ssh');
|
||||
return await performLocalDeviceSSH({
|
||||
await performLocalDeviceSSH({
|
||||
hostname: params.fleetOrDevice,
|
||||
port: options.port || 'local',
|
||||
forceTTY: options.tty,
|
||||
verbose: options.verbose,
|
||||
service: params.service,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Remote connection
|
||||
@ -132,7 +133,7 @@ export default class DeviceSSHCmd extends Command {
|
||||
const useProxy = !!proxyConfig && !options.noproxy;
|
||||
|
||||
// this will be a tunnelled SSH connection...
|
||||
await checkNotUsingOfflineMode();
|
||||
checkNotUsingOfflineMode();
|
||||
await checkLoggedIn();
|
||||
const deviceUuid = await getOnlineTargetDeviceUuid(
|
||||
sdk,
|
||||
|
2
src/commands/env/rename.ts
vendored
2
src/commands/env/rename.ts
vendored
@ -41,7 +41,7 @@ export default class EnvRenameCmd extends Command {
|
||||
id: Args.integer({
|
||||
required: true,
|
||||
description: "variable's numeric database ID",
|
||||
parse: async (input) => parseAsInteger(input, 'id'),
|
||||
parse: (input) => parseAsInteger(input, 'id'),
|
||||
}),
|
||||
value: Args.string({
|
||||
required: true,
|
||||
|
2
src/commands/env/rm.ts
vendored
2
src/commands/env/rm.ts
vendored
@ -46,7 +46,7 @@ export default class EnvRmCmd extends Command {
|
||||
id: Args.integer({
|
||||
required: true,
|
||||
description: "variable's numeric database ID",
|
||||
parse: async (input) => parseAsInteger(input, 'id'),
|
||||
parse: (input) => parseAsInteger(input, 'id'),
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -16,7 +16,6 @@
|
||||
*/
|
||||
|
||||
import { Args, Command } from '@oclif/core';
|
||||
import { promisify } from 'util';
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class LocalConfigureCmd extends Command {
|
||||
@ -237,7 +236,7 @@ export default class LocalConfigureCmd extends Command {
|
||||
const bootPartition = await getBootPartition(target);
|
||||
|
||||
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
|
||||
return await _fs.promises.readdir(this.CONNECTIONS_FOLDER);
|
||||
});
|
||||
|
||||
let connectionFileName;
|
||||
@ -246,13 +245,11 @@ export default class LocalConfigureCmd extends Command {
|
||||
} else if (_.includes(files, 'resin-sample.ignore')) {
|
||||
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
||||
await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||
const readFileAsync = promisify(_fs.readFile);
|
||||
const writeFileAsync = promisify(_fs.writeFile);
|
||||
const contents = await readFileAsync(
|
||||
const contents = await _fs.promises.readFile(
|
||||
`${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
return await writeFileAsync(
|
||||
await _fs.promises.writeFile(
|
||||
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||
contents,
|
||||
);
|
||||
@ -269,13 +266,13 @@ export default class LocalConfigureCmd extends Command {
|
||||
} else {
|
||||
// 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) => {
|
||||
return await promisify(_fs.writeFile)(
|
||||
await _fs.promises.writeFile(
|
||||
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||
this.CONNECTION_FILE,
|
||||
);
|
||||
});
|
||||
}
|
||||
return await this.getConfigurationSchema(bootPartition, connectionFileName);
|
||||
return this.getConfigurationSchema(bootPartition, connectionFileName);
|
||||
}
|
||||
|
||||
async removeHostname(schema: any) {
|
||||
|
@ -132,7 +132,7 @@ export default class LoginCmd extends Command {
|
||||
// We can safely assume this won't be undefined as doLogin will throw if this call fails
|
||||
// We also don't need to worry too much about the amount of calls to whoami
|
||||
// as these are cached by the SDK
|
||||
const whoamiResult = (await balena.auth.whoami()) as WhoamiResult;
|
||||
const whoamiResult = (await balena.auth.whoami())!;
|
||||
|
||||
if (whoamiResult.actorType !== 'user' && !options.hideExperimentalWarning) {
|
||||
console.info(stripIndent`
|
||||
@ -168,7 +168,7 @@ ${messages.reachingOut}`);
|
||||
|
||||
async doLogin(
|
||||
loginOptions: FlagsDef,
|
||||
balenaUrl: string = 'balena-cloud.com',
|
||||
balenaUrl = 'balena-cloud.com',
|
||||
token?: string,
|
||||
): Promise<void> {
|
||||
// Token
|
||||
|
@ -18,7 +18,6 @@
|
||||
import { Flags, Args, Command } from '@oclif/core';
|
||||
import type { Interfaces } from '@oclif/core';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import { promisify } from 'util';
|
||||
import * as _ from 'lodash';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
@ -292,7 +291,7 @@ export default class OsConfigureCmd extends Command {
|
||||
|
||||
for (const { name, content } of files) {
|
||||
await imagefs.interact(image, bootPartition, async (_fs) => {
|
||||
return await promisify(_fs.writeFile)(
|
||||
await _fs.promises.writeFile(
|
||||
path.join(CONNECTIONS_FOLDER, name),
|
||||
content,
|
||||
);
|
||||
|
@ -109,7 +109,7 @@ https://github.com/balena-io-examples/staged-releases\
|
||||
'additional-space': Flags.integer({
|
||||
description:
|
||||
'expand the image by this amount of bytes instead of automatically estimating the required amount',
|
||||
parse: async (x) => parseAsInteger(x, 'additional-space'),
|
||||
parse: (x) => parseAsInteger(x, 'additional-space'),
|
||||
}),
|
||||
'add-certificate': Flags.string({
|
||||
description: `\
|
||||
@ -126,7 +126,7 @@ Can be repeated to add multiple certificates.\
|
||||
dockerPort: Flags.integer({
|
||||
description:
|
||||
'Docker daemon TCP port number (hint: 2375 for balena devices)',
|
||||
parse: async (p) => parseAsInteger(p, 'dockerPort'),
|
||||
parse: (p) => parseAsInteger(p, 'dockerPort'),
|
||||
}),
|
||||
};
|
||||
|
||||
@ -155,7 +155,7 @@ Can be repeated to add multiple certificates.\
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
throw new ExpectedError(
|
||||
`The provided image path does not exist: ${params.image}`,
|
||||
);
|
||||
@ -192,11 +192,11 @@ Can be repeated to add multiple certificates.\
|
||||
event.name,
|
||||
));
|
||||
if (event.action === 'start') {
|
||||
return spinner.start();
|
||||
} else {
|
||||
console.log();
|
||||
return spinner.stop();
|
||||
spinner.start();
|
||||
return;
|
||||
}
|
||||
console.log();
|
||||
spinner.stop();
|
||||
};
|
||||
|
||||
const commit = this.isCurrentCommit(options.commit || '')
|
||||
@ -295,7 +295,7 @@ Can be repeated to add multiple certificates.\
|
||||
owns__release: {
|
||||
$select: ['id', 'commit', 'end_timestamp', 'composition'],
|
||||
$expand: {
|
||||
contains__image: {
|
||||
release_image: {
|
||||
$select: ['image'],
|
||||
$expand: {
|
||||
image: {
|
||||
|
@ -24,7 +24,7 @@ import { tryAsInteger } from '../../utils/validation';
|
||||
import { jsonInfo } from '../../utils/messages';
|
||||
|
||||
export const commitOrIdArg = Args.custom({
|
||||
parse: async (commitOrId: string) => tryAsInteger(commitOrId),
|
||||
parse: tryAsInteger,
|
||||
});
|
||||
|
||||
type FlagsDef = Interfaces.InferredFlags<typeof ReleaseCmd.flags>;
|
||||
@ -86,7 +86,7 @@ export default class ReleaseCmd extends Command {
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
options: FlagsDef,
|
||||
) {
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
const fields = [
|
||||
'id',
|
||||
'commit',
|
||||
'created_at',
|
||||
@ -96,7 +96,7 @@ export default class ReleaseCmd extends Command {
|
||||
'build_log',
|
||||
'start_timestamp',
|
||||
'end_timestamp',
|
||||
];
|
||||
] satisfies BalenaSdk.PineOptions<BalenaSdk.Release>['$select'];
|
||||
|
||||
const release = await balena.models.release.get(commitOrId, {
|
||||
...(!options.json && { $select: fields }),
|
||||
|
@ -56,14 +56,14 @@ export default class ReleaseListCmd extends Command {
|
||||
public async run() {
|
||||
const { args: params, flags: options } = await this.parse(ReleaseListCmd);
|
||||
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
const fields = [
|
||||
'id',
|
||||
'commit',
|
||||
'created_at',
|
||||
'status',
|
||||
'semver',
|
||||
'is_final',
|
||||
];
|
||||
] satisfies BalenaSdk.PineOptions<BalenaSdk.Release>['$select'];
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const { getFleetSlug } = await import('../../utils/sdk');
|
||||
|
@ -34,7 +34,7 @@ export default class SSHKeyCmd extends Command {
|
||||
public static args = {
|
||||
id: Args.integer({
|
||||
description: 'balenaCloud ID for the SSH key',
|
||||
parse: async (x) => parseAsInteger(x, 'id'),
|
||||
parse: (x) => parseAsInteger(x, 'id'),
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
@ -40,7 +40,7 @@ export default class SSHKeyRmCmd extends Command {
|
||||
public static args = {
|
||||
id: Args.integer({
|
||||
description: 'balenaCloud ID for the SSH key',
|
||||
parse: async (x) => parseAsInteger(x, 'id'),
|
||||
parse: (x) => parseAsInteger(x, 'id'),
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
@ -69,10 +69,9 @@ export default class VersionCmd extends Command {
|
||||
const { flags: options } = await this.parse(VersionCmd);
|
||||
const versions: JsonVersions = {
|
||||
'balena-cli': (await import('../../../package.json')).version,
|
||||
'Node.js':
|
||||
process.version && process.version.startsWith('v')
|
||||
? process.version.slice(1)
|
||||
: process.version,
|
||||
'Node.js': process.version.startsWith('v')
|
||||
? process.version.slice(1)
|
||||
: process.version,
|
||||
};
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(versions, null, 4));
|
||||
|
@ -93,7 +93,7 @@ function interpret(error: Error): string {
|
||||
|
||||
if (hasCode(error)) {
|
||||
const errorCodeHandler = messages[error.code];
|
||||
const message = errorCodeHandler && errorCodeHandler(error);
|
||||
const message = errorCodeHandler?.(error);
|
||||
|
||||
if (message) {
|
||||
return message;
|
||||
@ -229,7 +229,7 @@ async function sentryCaptureException(error: Error) {
|
||||
Sentry.captureException(error);
|
||||
try {
|
||||
await Sentry.close(1000);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
if (process.env.DEBUG) {
|
||||
console.error('[debug] Timeout reporting error to sentry.io');
|
||||
}
|
||||
|
@ -209,12 +209,12 @@ See: https://git.io/JRHUW#deprecation-policy`,
|
||||
return indent(body, 2);
|
||||
}
|
||||
|
||||
protected formatDescription(desc: string = '') {
|
||||
protected formatDescription(desc = '') {
|
||||
const chalk = getChalk();
|
||||
|
||||
desc = desc.split('\n')[0];
|
||||
// Remove any ending .
|
||||
if (desc[desc.length - 1] === '.') {
|
||||
if (desc.endsWith('.')) {
|
||||
desc = desc.substring(0, desc.length - 1);
|
||||
}
|
||||
// Lowercase first letter if second char is lowercase, to preserve e.g. 'SSH ...')
|
||||
|
@ -103,7 +103,7 @@ const hook: Hook<'prerun'> = async function (options) {
|
||||
.offlineCompatible ?? DEFAULT_OFFLINE_COMPATIBLE
|
||||
)
|
||||
) {
|
||||
await checkNotUsingOfflineMode();
|
||||
checkNotUsingOfflineMode();
|
||||
}
|
||||
} catch (error) {
|
||||
this.error(error);
|
||||
|
@ -48,7 +48,7 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
||||
if (
|
||||
cmdSlice.length > 1 &&
|
||||
cmdSlice[0] === 'help' &&
|
||||
cmdSlice[1][0] !== '-'
|
||||
!cmdSlice[1].startsWith('-')
|
||||
) {
|
||||
cmdSlice.shift();
|
||||
cmdSlice.push('--help');
|
||||
|
@ -164,9 +164,8 @@ export async function downloadOSImage(
|
||||
stream.on('progress', (state: any) => {
|
||||
if (state != null) {
|
||||
return bar.update(state);
|
||||
} else {
|
||||
return spinner.start();
|
||||
}
|
||||
spinner.start();
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
|
@ -240,7 +240,7 @@ export const getPreviousRepos = (
|
||||
status: 'success',
|
||||
},
|
||||
$expand: {
|
||||
contains__image: {
|
||||
release_image: {
|
||||
$select: 'image',
|
||||
$expand: { image: { $select: 'is_stored_at__image_location' } },
|
||||
},
|
||||
@ -252,7 +252,7 @@ export const getPreviousRepos = (
|
||||
.then(function (release) {
|
||||
// grab all images from the latest release, return all image locations in the registry
|
||||
if (release.length > 0) {
|
||||
const images = release[0].contains__image as Array<{
|
||||
const images = release[0].release_image as Array<{
|
||||
image: [SDK.Image];
|
||||
}>;
|
||||
const { getRegistryAndName } =
|
||||
@ -386,7 +386,7 @@ export class BuildProgressUI implements Renderer {
|
||||
.map(function (service) {
|
||||
const stream = through.obj(function (event, _enc, cb) {
|
||||
eventHandler(service, event);
|
||||
return cb();
|
||||
cb();
|
||||
});
|
||||
stream.pipe(tty.stream, { end: false });
|
||||
return [service, stream];
|
||||
@ -471,17 +471,20 @@ export class BuildProgressUI implements Renderer {
|
||||
const { status, progress, error } = serviceToDataMap[service] ?? {};
|
||||
if (error) {
|
||||
return `${error}`;
|
||||
} else if (progress) {
|
||||
}
|
||||
|
||||
if (progress) {
|
||||
const bar = renderProgressBar(progress, 20);
|
||||
if (status) {
|
||||
return `${bar} ${status}`;
|
||||
}
|
||||
return `${bar}`;
|
||||
} else if (status) {
|
||||
return `${status}`;
|
||||
} else {
|
||||
return 'Waiting...';
|
||||
return bar;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
return status;
|
||||
}
|
||||
return 'Waiting...';
|
||||
})
|
||||
.map((data, index) => [services[index], data])
|
||||
.fromPairs()
|
||||
@ -552,7 +555,7 @@ export class BuildProgressInline implements Renderer {
|
||||
.map(function (service) {
|
||||
const stream = through.obj(function (event, _enc, cb) {
|
||||
eventHandler(service, event);
|
||||
return cb();
|
||||
cb();
|
||||
});
|
||||
stream.pipe(outStream, { end: false });
|
||||
return [service, stream];
|
||||
@ -606,11 +609,11 @@ export class BuildProgressInline implements Renderer {
|
||||
const { status, error } = event;
|
||||
if (error) {
|
||||
return `${error}`;
|
||||
} else if (status) {
|
||||
return `${status}`;
|
||||
} else {
|
||||
return 'Waiting...';
|
||||
}
|
||||
if (status) {
|
||||
return status;
|
||||
}
|
||||
return 'Waiting...';
|
||||
})();
|
||||
|
||||
const prefix = _.padEnd(getChalk().bold(service), this._prefixWidth);
|
||||
|
@ -966,7 +966,7 @@ export async function makeBuildTasks(
|
||||
deviceInfo: DeviceInfo,
|
||||
logger: Logger,
|
||||
projectName: string,
|
||||
releaseHash: string = 'unavailable',
|
||||
releaseHash = 'unavailable',
|
||||
preprocessHook?: (dockerfile: string) => string,
|
||||
): Promise<MultiBuild.BuildTask[]> {
|
||||
const multiBuild = await import('@balena/compose/dist/multibuild');
|
||||
@ -1492,7 +1492,7 @@ export function createRunLoop(tick: (...args: any[]) => void) {
|
||||
},
|
||||
end() {
|
||||
clearInterval(timerId);
|
||||
return runloop.onEnd();
|
||||
runloop.onEnd();
|
||||
},
|
||||
};
|
||||
return runloop;
|
||||
@ -1549,7 +1549,7 @@ function dropEmptyLinesStream() {
|
||||
if (str.trim()) {
|
||||
this.push(str);
|
||||
}
|
||||
return cb();
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
@ -1570,7 +1570,7 @@ function buildLogCapture(objectMode: boolean, buffer: string[]) {
|
||||
buffer.push(data);
|
||||
}
|
||||
|
||||
return cb(null, data);
|
||||
cb(null, data);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1585,14 +1585,16 @@ function buildProgressAdapter(inline: boolean) {
|
||||
|
||||
return through({ objectMode: true }, function (str, _enc, cb) {
|
||||
if (str == null) {
|
||||
return cb(null, str);
|
||||
cb(null, str);
|
||||
return;
|
||||
}
|
||||
|
||||
if (inline) {
|
||||
return cb(null, { status: str });
|
||||
cb(null, { status: str });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^Successfully tagged /.test(str)) {
|
||||
if (!str.startsWith('Successfully tagged ')) {
|
||||
const match = stepRegex.exec(str);
|
||||
if (match) {
|
||||
step = match[1];
|
||||
@ -1607,7 +1609,7 @@ function buildProgressAdapter(inline: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null, { status: str, progress });
|
||||
cb(null, { status: str, progress });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import * as semver from 'balena-semver';
|
||||
import { getBalenaSdk, stripIndent } from './lazy';
|
||||
|
||||
export interface ImgConfig {
|
||||
@ -81,7 +80,7 @@ export async function generateApplicationConfig(
|
||||
)) as ImgConfig;
|
||||
|
||||
// merge sshKeys to config, when they have been specified
|
||||
if (options.os && options.os.sshKeys) {
|
||||
if (options.os?.sshKeys) {
|
||||
// Create config.os object if it does not exist
|
||||
config.os = config.os ? config.os : {};
|
||||
config.os.sshKeys = config.os.sshKeys
|
||||
@ -122,16 +121,10 @@ export function generateDeviceConfig(
|
||||
// os.getConfig always returns a config for an app
|
||||
delete config.apiKey;
|
||||
|
||||
if (deviceApiKey == null && semver.satisfies(options.version, '<2.0.3')) {
|
||||
config.apiKey = await sdk.models.application.generateApiKey(
|
||||
application.id,
|
||||
);
|
||||
} else {
|
||||
config.deviceApiKey =
|
||||
typeof deviceApiKey === 'string' && deviceApiKey
|
||||
? deviceApiKey
|
||||
: await sdk.models.device.generateDeviceKey(device.uuid);
|
||||
}
|
||||
config.deviceApiKey =
|
||||
typeof deviceApiKey === 'string' && deviceApiKey
|
||||
? deviceApiKey
|
||||
: await sdk.models.device.generateDeviceKey(device.uuid);
|
||||
|
||||
return config;
|
||||
})
|
||||
|
@ -86,7 +86,7 @@ const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
|
||||
obj = JSON.parse(data);
|
||||
} catch (e) {
|
||||
logger.logError('Error parsing reply from remote side');
|
||||
reject(e);
|
||||
reject(e as Error);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -232,7 +232,7 @@ export const deployLegacy = async function (
|
||||
username,
|
||||
appName,
|
||||
]);
|
||||
await uploadLogs(...args);
|
||||
uploadLogs(...args);
|
||||
}
|
||||
|
||||
return buildId;
|
||||
|
@ -74,7 +74,7 @@ export class DeviceAPI {
|
||||
public constructor(
|
||||
private logger: Logger,
|
||||
addr: string,
|
||||
port: number = 48484,
|
||||
port = 48484,
|
||||
) {
|
||||
this.deviceAddress = `http://${addr}:${port}/`;
|
||||
}
|
||||
@ -201,7 +201,7 @@ export class DeviceAPI {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = request.get(url);
|
||||
|
||||
req.on('error', reject).on('response', async (res) => {
|
||||
req.on('error', reject).on('response', (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
reject(
|
||||
new ApiErrors.DeviceAPIError(
|
||||
@ -213,7 +213,7 @@ export class DeviceAPI {
|
||||
try {
|
||||
res.socket.setKeepAlive(true, 1000);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
reject(error as Error);
|
||||
}
|
||||
resolve(res);
|
||||
});
|
||||
@ -238,7 +238,7 @@ export class DeviceAPI {
|
||||
if (_.isObject(opts) && (opts as ObjectWithUrl).url != null) {
|
||||
// the `as string` shouldn't be necessary, but the type system
|
||||
// is getting a little confused
|
||||
url = (opts as ObjectWithUrl).url as string;
|
||||
url = (opts as ObjectWithUrl).url!;
|
||||
} else if (typeof opts === 'string') {
|
||||
url = opts;
|
||||
}
|
||||
@ -252,21 +252,26 @@ export class DeviceAPI {
|
||||
return await new Promise((resolve, reject) => {
|
||||
return request(opts, (err, response, body) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
reject(err as Error);
|
||||
return;
|
||||
}
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
return resolve(body);
|
||||
case 400:
|
||||
return reject(
|
||||
new ApiErrors.BadRequestDeviceAPIError(body.message),
|
||||
);
|
||||
case 503:
|
||||
return reject(
|
||||
new ApiErrors.ServiceUnavailableAPIError(body.message),
|
||||
);
|
||||
default:
|
||||
return reject(new ApiErrors.DeviceAPIError(body.message));
|
||||
case 200: {
|
||||
resolve(body);
|
||||
return;
|
||||
}
|
||||
case 400: {
|
||||
reject(new ApiErrors.BadRequestDeviceAPIError(body.message));
|
||||
return;
|
||||
}
|
||||
case 503: {
|
||||
reject(new ApiErrors.ServiceUnavailableAPIError(body.message));
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
reject(new ApiErrors.DeviceAPIError(body.message));
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -74,11 +74,11 @@ interface ParsedEnvironment {
|
||||
[serviceName: string]: { [key: string]: string };
|
||||
}
|
||||
|
||||
async function environmentFromInput(
|
||||
function environmentFromInput(
|
||||
envs: string[],
|
||||
serviceNames: string[],
|
||||
logger: Logger,
|
||||
): Promise<ParsedEnvironment> {
|
||||
): ParsedEnvironment {
|
||||
// A normal environment variable regex, with an added part
|
||||
// to find a colon followed servicename at the start
|
||||
const varRegex = /^(?:([^\s:]+):)?([^\s]+?)=(.*)$/;
|
||||
@ -143,7 +143,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
try {
|
||||
globalLogger.logDebug('Checking we can access device');
|
||||
await api.ping();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Could not communicate with device supervisor at address ${opts.deviceHost}:${port}.
|
||||
Device may not have local mode enabled. Check with:
|
||||
@ -191,10 +191,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
});
|
||||
|
||||
// Attempt to attach to the device's docker daemon
|
||||
const docker = connectToDocker(
|
||||
opts.deviceHost,
|
||||
opts.devicePort != null ? opts.devicePort : 2375,
|
||||
);
|
||||
const docker = connectToDocker(opts.deviceHost, opts.devicePort ?? 2375);
|
||||
|
||||
await checkBuildSecretsRequirements(docker, opts.source);
|
||||
globalLogger.logDebug('Tarring all non-ignored files...');
|
||||
@ -231,7 +228,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
// Print a newline to clearly separate build time and runtime
|
||||
console.log();
|
||||
|
||||
const envs = await environmentFromInput(
|
||||
const envs = environmentFromInput(
|
||||
opts.env,
|
||||
Object.getOwnPropertyNames(project.composition.services),
|
||||
globalLogger,
|
||||
@ -388,7 +385,7 @@ async function performBuilds(
|
||||
);
|
||||
|
||||
// Check for failures
|
||||
await inspectBuildResults(localImages);
|
||||
inspectBuildResults(localImages);
|
||||
|
||||
const imagesToRemove: string[] = [];
|
||||
|
||||
@ -497,7 +494,7 @@ export async function rebuildSingleTask(
|
||||
}
|
||||
|
||||
await assignDockerBuildOpts(docker, [task], opts);
|
||||
await assignOutputHandlers([task], logger, logHandler);
|
||||
assignOutputHandlers([task], logger, logHandler);
|
||||
|
||||
const [localImage] = await multibuild.performBuilds(
|
||||
[task],
|
||||
@ -568,7 +565,7 @@ async function assignDockerBuildOpts(
|
||||
globalLogger.logDebug(`Using ${images.length} on-device images for cache...`);
|
||||
|
||||
await Promise.all(
|
||||
buildTasks.map(async (task: BuildTask) => {
|
||||
buildTasks.map((task: BuildTask) => {
|
||||
task.dockerOpts = {
|
||||
...(task.dockerOpts || {}),
|
||||
...{
|
||||
@ -666,7 +663,7 @@ export function generateTargetState(
|
||||
return targetState;
|
||||
}
|
||||
|
||||
async function inspectBuildResults(images: LocalImage[]): Promise<void> {
|
||||
function inspectBuildResults(images: LocalImage[]): void {
|
||||
const failures: LocalPushErrors.BuildFailure[] = [];
|
||||
|
||||
_.each(images, (image) => {
|
||||
@ -679,6 +676,6 @@ async function inspectBuildResults(images: LocalImage[]): Promise<void> {
|
||||
});
|
||||
|
||||
if (failures.length > 0) {
|
||||
throw new LocalPushErrors.BuildError(failures).toString();
|
||||
throw new LocalPushErrors.BuildError(failures);
|
||||
}
|
||||
}
|
||||
|
@ -191,8 +191,8 @@ export class LivepushManager {
|
||||
);
|
||||
const eventQueue = this.updateEventsWaiting[$serviceName];
|
||||
eventQueue.push(changedPath);
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.getDebouncedEventHandler($serviceName)();
|
||||
|
||||
void this.getDebouncedEventHandler($serviceName)();
|
||||
};
|
||||
|
||||
const monitor = this.setupFilesystemWatcher(
|
||||
@ -252,7 +252,7 @@ export class LivepushManager {
|
||||
try {
|
||||
// sync because chokidar defines a sync interface
|
||||
stats = fs.lstatSync(filePath);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// OK: the file may have been deleted. See also:
|
||||
// https://github.com/paulmillr/chokidar/blob/3.4.3/lib/fsevents-handler.js#L326-L328
|
||||
// https://github.com/paulmillr/chokidar/blob/3.4.3/lib/nodefs-handler.js#L364
|
||||
@ -267,15 +267,15 @@ export class LivepushManager {
|
||||
return dockerignore.ignores(relPath);
|
||||
},
|
||||
});
|
||||
monitor.on('add', (changedPath: string) =>
|
||||
changedPathHandler(serviceName, changedPath),
|
||||
);
|
||||
monitor.on('change', (changedPath: string) =>
|
||||
changedPathHandler(serviceName, changedPath),
|
||||
);
|
||||
monitor.on('unlink', (changedPath: string) =>
|
||||
changedPathHandler(serviceName, changedPath),
|
||||
);
|
||||
monitor.on('add', (changedPath: string) => {
|
||||
changedPathHandler(serviceName, changedPath);
|
||||
});
|
||||
monitor.on('change', (changedPath: string) => {
|
||||
changedPathHandler(serviceName, changedPath);
|
||||
});
|
||||
monitor.on('unlink', (changedPath: string) => {
|
||||
changedPathHandler(serviceName, changedPath);
|
||||
});
|
||||
return monitor;
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,7 @@ export const dockerConnectionCliFlags = {
|
||||
description:
|
||||
'Docker daemon TCP port number (hint: 2375 for balena devices)',
|
||||
char: 'p',
|
||||
parse: async (p) => parseAsInteger(p, 'dockerPort'),
|
||||
parse: (p) => parseAsInteger(p, 'dockerPort'),
|
||||
}),
|
||||
ca: Flags.string({
|
||||
description: 'Docker host TLS certificate authority file',
|
||||
@ -169,9 +169,7 @@ export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
|
||||
// version of balenaEngine, but it was at one point (mis)spelt 'balaena':
|
||||
// https://github.com/balena-os/balena-engine/pull/32/files
|
||||
const dockerVersion = (await docker.version()) as BalenaEngineVersion;
|
||||
return !!(
|
||||
dockerVersion.Engine && dockerVersion.Engine.match(/balena|balaena/)
|
||||
);
|
||||
return !!dockerVersion.Engine?.match(/balena|balaena/);
|
||||
}
|
||||
|
||||
export async function getDocker(
|
||||
|
@ -84,7 +84,7 @@ export async function readFileWithEolConversion(
|
||||
}
|
||||
|
||||
// Analyse encoding
|
||||
const encoding = await detectEncoding(fileBuffer);
|
||||
const encoding = detectEncoding(fileBuffer);
|
||||
|
||||
// Skip further processing of non-convertible encodings
|
||||
if (!CONVERTIBLE_ENCODINGS.includes(encoding)) {
|
||||
@ -132,10 +132,10 @@ export async function readFileWithEolConversion(
|
||||
* @param fileBuffer File contents whose encoding should be detected
|
||||
* @param bytesRead Optional "file size" if smaller than the buffer size
|
||||
*/
|
||||
export async function detectEncoding(
|
||||
export function detectEncoding(
|
||||
fileBuffer: Buffer,
|
||||
bytesRead = fileBuffer.length,
|
||||
): Promise<string> {
|
||||
): string {
|
||||
// empty file
|
||||
if (bytesRead === 0) {
|
||||
return '';
|
||||
|
@ -110,6 +110,27 @@ export async function getManifest(
|
||||
const init = await import('balena-device-init');
|
||||
const sdk = getBalenaSdk();
|
||||
const manifest = await init.getImageManifest(image);
|
||||
if (manifest != null) {
|
||||
const config = manifest.configuration?.config;
|
||||
if (config?.partition != null) {
|
||||
const { getBootPartition } = await import('balena-config-json');
|
||||
// Find the device-type.json property that holds the boot partition number for
|
||||
// this device type (config.partition or config.partition.primary) and overwrite it
|
||||
// with the boot partition number that was found by inspecting the image.
|
||||
// since it's deprecated & no longer updated for newer releases.
|
||||
if (typeof config.partition === 'number') {
|
||||
config.partition = await getBootPartition(image);
|
||||
} else if (config.partition.primary != null) {
|
||||
config.partition.primary = await getBootPartition(image);
|
||||
}
|
||||
// TODO: Add handling for when we no longer include a `config.partition` at all.
|
||||
}
|
||||
} else {
|
||||
// TODO: Change this in the next major to throw, after confirming that this works for all supported OS versions.
|
||||
console.error(
|
||||
`[warn] Error while finding a device-type.json on the provided image path. Attempting to fetch from the API.`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
manifest != null &&
|
||||
manifest.slug !== deviceType &&
|
||||
@ -281,8 +302,7 @@ export function isWindowsComExeShell() {
|
||||
// neither bash nor sh (e.g. not MSYS, MSYS2, Cygwin, WSL)
|
||||
process.env.SHELL == null &&
|
||||
// Windows cmd.exe or PowerShell
|
||||
process.env.ComSpec != null &&
|
||||
process.env.ComSpec.endsWith('cmd.exe')
|
||||
process.env.ComSpec?.endsWith('cmd.exe')
|
||||
);
|
||||
}
|
||||
|
||||
@ -366,7 +386,7 @@ export function getProxyConfig(): ProxyConfig | undefined {
|
||||
let url: InstanceType<typeof URL>;
|
||||
try {
|
||||
url = new URL(proxyUrl);
|
||||
} catch (_e) {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
@ -469,7 +489,7 @@ export function pickAndRename<T extends Dictionary<any>>(
|
||||
let renameFrom = f;
|
||||
let renameTo = f;
|
||||
const match = f.match(/(?<from>\S+)\s+=>\s+(?<to>\S+)/);
|
||||
if (match && match.groups) {
|
||||
if (match?.groups) {
|
||||
renameFrom = match.groups.from;
|
||||
renameTo = match.groups.to;
|
||||
}
|
||||
|
@ -76,38 +76,28 @@ export const getImagePath = async (deviceType: string, version?: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Determine if a device image is fresh
|
||||
* @summary Determine if a device image is cached
|
||||
*
|
||||
* @description
|
||||
* If the device image does not exist, return false.
|
||||
*
|
||||
* @param {String} deviceType - device type slug or alias
|
||||
* @param {String} version - the exact balenaOS version number
|
||||
* @returns {Promise<Boolean>} is image fresh
|
||||
* @returns {Promise<Boolean>} is image cached
|
||||
*
|
||||
* @example
|
||||
* isImageFresh('raspberry-pi', '1.2.3').then (isFresh) ->
|
||||
* if isFresh
|
||||
* console.log('The Raspberry Pi image v1.2.3 is fresh!')
|
||||
* isImageCached ('raspberry-pi', '1.2.3').then (isCached) ->
|
||||
* if isCached
|
||||
* console.log('The Raspberry Pi image v1.2.3 is cached!')
|
||||
*/
|
||||
export const isImageFresh = async (deviceType: string, version: string) => {
|
||||
export const isImageCached = async (deviceType: string, version: string) => {
|
||||
const imagePath = await getImagePath(deviceType, version);
|
||||
let createdDate;
|
||||
try {
|
||||
createdDate = await getFileCreatedDate(imagePath);
|
||||
const createdDate = await getFileCreatedDate(imagePath);
|
||||
return createdDate != null;
|
||||
} catch {
|
||||
// Swallow errors from getFileCreatedTime.
|
||||
}
|
||||
if (createdDate == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const lastModifiedDate = await balena.models.os.getLastModified(
|
||||
deviceType,
|
||||
version,
|
||||
);
|
||||
return lastModifiedDate < createdDate;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -118,7 +108,7 @@ export const isImageFresh = async (deviceType: string, version: string) => {
|
||||
*/
|
||||
export const isESR = (version: string) => {
|
||||
const match = version.match(/^v?(\d+)\.\d+\.\d+/);
|
||||
const major = parseInt((match && match[1]) || '', 10);
|
||||
const major = parseInt(match?.[1] || '', 10);
|
||||
return major >= 2018; // note: (NaN >= 2018) is false
|
||||
};
|
||||
|
||||
@ -286,7 +276,7 @@ export const getStream = async (
|
||||
versionOrRange = 'latest';
|
||||
}
|
||||
const version = await resolveVersion(deviceType, versionOrRange);
|
||||
const isFresh = await isImageFresh(deviceType, version);
|
||||
const isFresh = await isImageCached(deviceType, version);
|
||||
const $stream = isFresh
|
||||
? await getImage(deviceType, version)
|
||||
: await doDownload({ ...options, deviceType, version });
|
||||
|
@ -21,6 +21,7 @@ import type { Chalk } from 'chalk';
|
||||
import type * as visuals from 'resin-cli-visuals';
|
||||
import type * as CliForm from 'resin-cli-form';
|
||||
import type { ux } from '@oclif/core';
|
||||
import { version } from '../../package.json';
|
||||
|
||||
// Equivalent of _.once but avoiding the need to import lodash for lazy deps
|
||||
const once = <T>(fn: () => T) => {
|
||||
@ -43,9 +44,26 @@ export const onceAsync = <T>(fn: () => Promise<T>) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getBalenaSdk = once(() =>
|
||||
(require('balena-sdk') as typeof BalenaSdk).fromSharedOptions(),
|
||||
);
|
||||
const cliXBalenaClientHeaderInterceptor: BalenaSdk.Interceptor = {
|
||||
request($request) {
|
||||
if ($request.headers['X-Balena-Client']) {
|
||||
// We intentionally overwrite the sdk version string from the header
|
||||
// to conserve bandwidth. We only do that when the SDK already has specified
|
||||
// the X-Balena-Client header, since that signals that this is a safe url to
|
||||
// include the extra header and will not cause CORS errors.
|
||||
$request.headers['X-Balena-Client'] = `balena-cli/${version}`;
|
||||
}
|
||||
return $request;
|
||||
},
|
||||
};
|
||||
|
||||
export const getBalenaSdk = once(() => {
|
||||
const sdk = (require('balena-sdk') as typeof BalenaSdk).fromSharedOptions();
|
||||
if (!sdk.interceptors.includes(cliXBalenaClientHeaderInterceptor)) {
|
||||
sdk.interceptors.push(cliXBalenaClientHeaderInterceptor);
|
||||
}
|
||||
return sdk;
|
||||
});
|
||||
|
||||
export const getVisuals = once(
|
||||
() => require('resin-cli-visuals') as typeof visuals,
|
||||
|
@ -39,7 +39,7 @@ export async function disambiguateReleaseParam(
|
||||
|
||||
// Accepting short hashes of 7,8,9 chars.
|
||||
const possibleUuidHashLength = [7, 8, 9, 32, 40, 62].includes(release.length);
|
||||
const hasLeadingZero = release[0] === '0';
|
||||
const hasLeadingZero = release.startsWith('0');
|
||||
const isOnlyNumerical = /^[0-9]+$/.test(release);
|
||||
|
||||
// Reject non-numerical values with invalid uuid/hash lengths
|
||||
@ -75,17 +75,19 @@ export async function disambiguateReleaseParam(
|
||||
return (await balena.models.release.get(release, { $select: 'id' })).id;
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/require-await -- oclif parse functions require a Promise return */
|
||||
/**
|
||||
* Convert to lowercase if looks like slug
|
||||
*/
|
||||
export async function lowercaseIfSlug(s: string) {
|
||||
/* eslint-enable @typescript-eslint/require-await */
|
||||
return s.includes('/') ? s.toLowerCase() : s;
|
||||
}
|
||||
|
||||
export function normalizeOsVersion(version: string) {
|
||||
// Note that `version` may also be 'latest', 'recommended', 'default'
|
||||
if (/^v?\d+\.\d+\.\d+/.test(version)) {
|
||||
if (version[0] === 'v') {
|
||||
if (version.startsWith('v')) {
|
||||
version = version.slice(1);
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ export function capitanoizeOclifUsage(
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export async function getCommandsFromManifest() {
|
||||
export function getCommandsFromManifest() {
|
||||
const manifest = require('../../oclif.manifest.json');
|
||||
|
||||
if (manifest.commands == null) {
|
||||
|
@ -119,7 +119,7 @@ export const checkLoggedInIf = async (doCheck: boolean) => {
|
||||
*
|
||||
* @throws {NotAvailableInOfflineModeError}
|
||||
*/
|
||||
export const checkNotUsingOfflineMode = async () => {
|
||||
export const checkNotUsingOfflineMode = () => {
|
||||
if (process.env.BALENARC_OFFLINE_MODE) {
|
||||
throw new NotAvailableInOfflineModeError(stripIndent`
|
||||
This command requires an internet connection, and cannot be used in offline mode.
|
||||
|
@ -390,13 +390,12 @@ async function createApplication(
|
||||
try {
|
||||
const userInfo = await sdk.auth.getUserInfo();
|
||||
username = userInfo.username;
|
||||
} catch (err) {
|
||||
} catch {
|
||||
throw new sdk.errors.BalenaNotLoggedIn();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
const applicationName = await new Promise<string>(async (resolve, reject) => {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
const appName = await getCliForm().ask({
|
||||
@ -418,11 +417,13 @@ async function createApplication(
|
||||
'You already have a fleet with that name; please choose another.',
|
||||
);
|
||||
continue;
|
||||
} catch (err) {
|
||||
return resolve(appName);
|
||||
} catch {
|
||||
resolve(appName);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
reject(err as Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -452,9 +453,7 @@ async function generateApplicationConfig(
|
||||
const manifest = await sdk.models.config.getDeviceTypeManifestBySlug(
|
||||
app.is_for__device_type[0].slug,
|
||||
);
|
||||
const opts =
|
||||
manifest.options &&
|
||||
manifest.options.filter((opt) => opt.name !== 'network');
|
||||
const opts = manifest.options?.filter((opt) => opt.name !== 'network');
|
||||
|
||||
const override = {
|
||||
appUpdatePollInterval: options.appUpdatePollInterval,
|
||||
|
@ -50,7 +50,7 @@ export function copyQemu(context: string, arch: string) {
|
||||
.then(() => getQemuPath(arch))
|
||||
.then(
|
||||
(qemu) =>
|
||||
new Promise(function (resolve, reject) {
|
||||
new Promise<void>(function (resolve, reject) {
|
||||
const read = fs.createReadStream(qemu);
|
||||
const write = fs.createWriteStream(binPath);
|
||||
|
||||
@ -114,7 +114,7 @@ async function installQemu(arch: string, qemuPath: string) {
|
||||
stream.resume();
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
reject(err as Error);
|
||||
}
|
||||
});
|
||||
request(qemuUrl)
|
||||
|
@ -110,7 +110,7 @@ export async function startRemoteBuild(
|
||||
const [buildRequest, stream] = await getRemoteBuildStream(build);
|
||||
|
||||
// Setup CTRL-C handler so the user can interrupt the build
|
||||
let cancellationPromise = Promise.resolve();
|
||||
let cancellationPromise: Promise<void> | undefined;
|
||||
const sigintHandler = () => {
|
||||
process.exitCode = 130;
|
||||
console.error('\nReceived SIGINT, cleaning up. Please wait.');
|
||||
@ -246,7 +246,8 @@ function getBuilderMessageHandler(
|
||||
console.error(`[debug] handling message: ${JSON.stringify(obj)}`);
|
||||
}
|
||||
if (obj.type != null && obj.type === 'metadata') {
|
||||
return handleBuilderMetadata(obj, build);
|
||||
handleBuilderMetadata(obj, build);
|
||||
return;
|
||||
}
|
||||
if (obj.message) {
|
||||
readline.clearLine(process.stdout, 0);
|
||||
@ -423,10 +424,20 @@ async function getRemoteBuildStream(
|
||||
stream = buildRequest.pipe(JSONStream.parse('*')) as NodeJS.ReadStream;
|
||||
}
|
||||
stream = stream
|
||||
.once('error', () => uploadSpinner.stop())
|
||||
.once('close', () => uploadSpinner.stop())
|
||||
.once('data', () => uploadSpinner.stop())
|
||||
.once('end', () => uploadSpinner.stop())
|
||||
.once('finish', () => uploadSpinner.stop());
|
||||
.once('error', () => {
|
||||
uploadSpinner.stop();
|
||||
})
|
||||
.once('close', () => {
|
||||
uploadSpinner.stop();
|
||||
})
|
||||
.once('data', () => {
|
||||
uploadSpinner.stop();
|
||||
})
|
||||
.once('end', () => {
|
||||
uploadSpinner.stop();
|
||||
})
|
||||
.once('finish', () => {
|
||||
uploadSpinner.stop();
|
||||
});
|
||||
return [buildRequest, stream];
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ export function sshArgsForRemoteCommand({
|
||||
...['-o', 'LogLevel=ERROR'],
|
||||
...['-o', 'StrictHostKeyChecking=no'],
|
||||
...['-o', 'UserKnownHostsFile=/dev/null'],
|
||||
...(proxyCommand && proxyCommand.length
|
||||
...(proxyCommand?.length
|
||||
? ['-o', `ProxyCommand=${proxyCommand.join(' ')}`]
|
||||
: []),
|
||||
`${username}@${hostname}`,
|
||||
@ -155,9 +155,9 @@ export async function runRemoteCommand({
|
||||
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
|
||||
const ps = spawn(program, args, { stdio })
|
||||
.on('error', reject)
|
||||
.on('close', (code, signal) =>
|
||||
resolve([code ?? undefined, signal ?? undefined]),
|
||||
);
|
||||
.on('close', (code, signal) => {
|
||||
resolve([code ?? undefined, signal ?? undefined]);
|
||||
});
|
||||
|
||||
if (ps.stdin && stdin && typeof stdin !== 'string') {
|
||||
stdin.pipe(ps.stdin);
|
||||
@ -272,7 +272,7 @@ export async function getLocalDeviceCmdStdout(
|
||||
export const isRootUserGood = _.memoize(async (hostname: string, port) => {
|
||||
try {
|
||||
await runRemoteCommand({ cmd: 'exit 0', hostname, port, ...stdioIgnore });
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -29,7 +29,11 @@ export function buffer(
|
||||
new Promise(function (resolve, reject) {
|
||||
const fstream = fs.createReadStream(bufferFile);
|
||||
|
||||
fstream.on('open', () => resolve(fstream)).on('error', reject);
|
||||
fstream
|
||||
.on('open', () => {
|
||||
resolve(fstream);
|
||||
})
|
||||
.on('error', reject);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ async function spawnAndPipe(
|
||||
});
|
||||
}
|
||||
|
||||
async function windosuExec(
|
||||
function windosuExec(
|
||||
escapedArgs: string[],
|
||||
stderr?: NodeJS.WritableStream,
|
||||
): Promise<void> {
|
||||
|
@ -42,9 +42,9 @@ export = (stream: NodeJS.WriteStream = process.stdout) => {
|
||||
|
||||
const showCursor = () => stream.write('\u001B[?25h');
|
||||
|
||||
const cursorUp = (rows: number = 0) => stream.write(`\u001B[${rows}A`);
|
||||
const cursorUp = (rows = 0) => stream.write(`\u001B[${rows}A`);
|
||||
|
||||
const cursorDown = (rows: number = 0) => stream.write(`\u001B[${rows}B`);
|
||||
const cursorDown = (rows = 0) => stream.write(`\u001B[${rows}B`);
|
||||
|
||||
const write = (str: string) => stream.write(str);
|
||||
|
||||
|
@ -61,11 +61,11 @@ export const tunnelConnectionToDevice = (
|
||||
client.pipe(remote);
|
||||
remote.pipe(client);
|
||||
remote.on('error', (err) => {
|
||||
console.error('Remote: ' + err);
|
||||
console.error(`Remote: ${err}`);
|
||||
client.end();
|
||||
});
|
||||
client.on('error', (err) => {
|
||||
console.error('Client: ' + err);
|
||||
console.error(`Client: ${err}`);
|
||||
remote.end();
|
||||
});
|
||||
remote.on('close', () => {
|
||||
|
@ -87,7 +87,8 @@ export function looksLikeInteger(input: string) {
|
||||
return /^(?:0|[1-9][0-9]*)$/.test(input);
|
||||
}
|
||||
|
||||
export function parseAsInteger(input: string, paramName?: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/require-await -- oclif parse functions require a Promise return
|
||||
export async function parseAsInteger(input: string, paramName?: string) {
|
||||
if (!looksLikeInteger(input)) {
|
||||
const message =
|
||||
paramName == null
|
||||
@ -100,14 +101,15 @@ export function parseAsInteger(input: string, paramName?: string) {
|
||||
return Number(input);
|
||||
}
|
||||
|
||||
export function tryAsInteger(input: string): number | string {
|
||||
export async function tryAsInteger(input: string): Promise<number | string> {
|
||||
try {
|
||||
return parseAsInteger(input);
|
||||
return await parseAsInteger(input);
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await -- oclif parse functions require a Promise return
|
||||
export async function parseAsLocalHostnameOrIp(input: string) {
|
||||
if (input && !validateLocalHostnameOrIp(input)) {
|
||||
throw new ExpectedError(
|
||||
|
@ -31,7 +31,7 @@ chai.use(chaiAsPromised);
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
async function getPage(name: string): Promise<string> {
|
||||
function getPage(name: string): string {
|
||||
const pagePath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
@ -88,7 +88,7 @@ describe('Login server:', function () {
|
||||
expect(body).to.equal(opt.expectedBody);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
reject(err as Error);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -134,7 +134,7 @@ describe('Login server:', function () {
|
||||
describe('given the token authenticates with the server', function () {
|
||||
beforeEach(function () {
|
||||
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
|
||||
this.loginIfTokenValidStub.returns(Promise.resolve(true));
|
||||
this.loginIfTokenValidStub.resolves(true);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -143,7 +143,7 @@ describe('Login server:', function () {
|
||||
|
||||
it('should eventually be the token', async () => {
|
||||
await testLogin({
|
||||
expectedBody: await getPage('success'),
|
||||
expectedBody: getPage('success'),
|
||||
expectedStatusCode: 200,
|
||||
expectedToken: tokens.johndoe.token,
|
||||
});
|
||||
@ -153,7 +153,7 @@ describe('Login server:', function () {
|
||||
describe('given the token does not authenticate with the server', function () {
|
||||
beforeEach(function () {
|
||||
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
|
||||
return this.loginIfTokenValidStub.returns(Promise.resolve(false));
|
||||
return this.loginIfTokenValidStub.resolves(false);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -162,7 +162,7 @@ describe('Login server:', function () {
|
||||
|
||||
it('should be rejected', async () => {
|
||||
await testLogin({
|
||||
expectedBody: await getPage('error'),
|
||||
expectedBody: getPage('error'),
|
||||
expectedStatusCode: 401,
|
||||
expectedToken: tokens.johndoe.token,
|
||||
expectedErrorMsg: 'Invalid token',
|
||||
@ -171,7 +171,7 @@ describe('Login server:', function () {
|
||||
|
||||
it('should be rejected if no token', async () => {
|
||||
await testLogin({
|
||||
expectedBody: await getPage('error'),
|
||||
expectedBody: getPage('error'),
|
||||
expectedStatusCode: 401,
|
||||
expectedToken: '',
|
||||
expectedErrorMsg: 'No token',
|
||||
@ -180,7 +180,7 @@ describe('Login server:', function () {
|
||||
|
||||
it('should be rejected if token is malformed', async () => {
|
||||
await testLogin({
|
||||
expectedBody: await getPage('error'),
|
||||
expectedBody: getPage('error'),
|
||||
expectedStatusCode: 401,
|
||||
expectedToken: 'asdf',
|
||||
expectedErrorMsg: 'Invalid token',
|
||||
|
@ -267,15 +267,15 @@ describe('balena build', function () {
|
||||
...fsMod,
|
||||
promises: {
|
||||
...fsMod.promises,
|
||||
access: async (p: string) =>
|
||||
access: (p: string) =>
|
||||
p === qemuBinPath ? undefined : fsMod.promises.access(p),
|
||||
stat: async (p: string) =>
|
||||
stat: (p: string) =>
|
||||
p === qemuBinPath ? { size: 1 } : fsMod.promises.stat(p),
|
||||
},
|
||||
});
|
||||
mock(qemuModPath, {
|
||||
...qemuMod,
|
||||
copyQemu: async () => '',
|
||||
copyQemu: () => '',
|
||||
});
|
||||
mock.reRequire('../../build/utils/qemu');
|
||||
docker.expectGetInfo({ OperatingSystem: 'balenaOS 2.44.0+rev1' });
|
||||
|
@ -169,8 +169,12 @@ async function startMockSshServer(): Promise<[Server, number]> {
|
||||
console.error(`[debug] mock ssh server: ${msg}`);
|
||||
}
|
||||
};
|
||||
c.on('error', (err) => handler(err.message));
|
||||
c.on('end', () => handler('client disconnected'));
|
||||
c.on('error', (err) => {
|
||||
handler(err.message);
|
||||
});
|
||||
c.on('end', () => {
|
||||
handler('client disconnected');
|
||||
});
|
||||
c.end();
|
||||
});
|
||||
server.on('error', (err) => {
|
||||
|
@ -21,6 +21,8 @@ import * as process from 'process';
|
||||
import { runCommand } from '../../helpers';
|
||||
import { promisify } from 'util';
|
||||
import * as tmp from 'tmp';
|
||||
import type * as $imagefs from 'balena-image-fs';
|
||||
import * as stripIndent from 'common-tags/lib/stripIndent';
|
||||
|
||||
tmp.setGracefulCleanup();
|
||||
const tmpNameAsync = promisify(tmp.tmpName);
|
||||
@ -29,30 +31,94 @@ import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
describe('balena os configure', function () {
|
||||
let imagefs: typeof $imagefs;
|
||||
let api: BalenaAPIMock;
|
||||
let tmpPath: string;
|
||||
let tmpDummyPath: string;
|
||||
let tmpMatchingDtJsonPartitionPath: string;
|
||||
let tmpNonMatchingDtJsonPartitionPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
before(async function () {
|
||||
// We conditionally import balena-image-fs, since when imported on top level then unrelated tests on win32 failed with:
|
||||
// EPERM: operation not permitted, rename 'C:\Users\RUNNER~1\AppData\Local\Temp\tmp-<...>.inprogress' -> 'C:\Users\RUNNER~1\AppData\Local\Temp\tmp-<...>'
|
||||
// at async Object.rename (node:internal/fs/promises:782:10) {
|
||||
imagefs = await import('balena-image-fs');
|
||||
tmpDummyPath = (await tmpNameAsync()) as string;
|
||||
await fs.copyFile('./tests/test-data/dummy.img', tmpDummyPath);
|
||||
tmpMatchingDtJsonPartitionPath = (await tmpNameAsync()) as string;
|
||||
await fs.copyFile(
|
||||
'./tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img',
|
||||
tmpMatchingDtJsonPartitionPath,
|
||||
);
|
||||
|
||||
tmpNonMatchingDtJsonPartitionPath = (await tmpNameAsync()) as string;
|
||||
// Create an image with a device-type.json that mentions a non matching boot partition.
|
||||
// We copy the pre-existing image and modify it, since including a separate one
|
||||
// would add 18MB more to the repository.
|
||||
await fs.copyFile(
|
||||
'./tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img',
|
||||
tmpNonMatchingDtJsonPartitionPath,
|
||||
);
|
||||
await imagefs.interact(
|
||||
tmpNonMatchingDtJsonPartitionPath,
|
||||
12,
|
||||
async (_fs) => {
|
||||
const dtJson = JSON.parse(
|
||||
await _fs.promises.readFile('/device-type.json', {
|
||||
encoding: 'utf8',
|
||||
}),
|
||||
);
|
||||
expect(dtJson).to.have.nested.property(
|
||||
'configuration.config.partition',
|
||||
12,
|
||||
);
|
||||
dtJson.configuration.config.partition = 999;
|
||||
await _fs.promises.writeFile(
|
||||
'/device-type.json',
|
||||
JSON.stringify(dtJson),
|
||||
);
|
||||
|
||||
await _fs.promises.writeFile(
|
||||
'/os-release',
|
||||
stripIndent`
|
||||
ID="balena-os"
|
||||
NAME="balenaOS"
|
||||
VERSION="6.1.25"
|
||||
VERSION_ID="6.1.25"
|
||||
PRETTY_NAME="balenaOS 6.1.25"
|
||||
DISTRO_CODENAME="kirkstone"
|
||||
MACHINE="jetson-nano"
|
||||
META_BALENA_VERSION="6.1.25"`,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
tmpPath = (await tmpNameAsync()) as string;
|
||||
await fs.copyFile('./tests/test-data/dummy.img', tmpPath);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
afterEach(() => {
|
||||
api.done();
|
||||
await fs.unlink(tmpPath);
|
||||
});
|
||||
|
||||
it('should inject a valid config.json file', async () => {
|
||||
after(async () => {
|
||||
await fs.unlink(tmpDummyPath);
|
||||
await fs.unlink(tmpMatchingDtJsonPartitionPath);
|
||||
await fs.unlink(tmpNonMatchingDtJsonPartitionPath);
|
||||
});
|
||||
|
||||
it('should detect the OS version and inject a valid config.json file to a 6.0.13 image with partition 12 as boot & matching device-type.json', async () => {
|
||||
api.expectGetApplication();
|
||||
api.expectGetConfigDeviceTypes();
|
||||
api.expectGetDeviceTypes();
|
||||
// It should not reach to /config or /device-types/v1 but instead find
|
||||
// everything required from the device-type.json in the image.
|
||||
// api.expectGetConfigDeviceTypes();
|
||||
api.expectDownloadConfig();
|
||||
|
||||
const command: string[] = [
|
||||
`os configure ${tmpPath}`,
|
||||
'--device-type raspberrypi3',
|
||||
'--version 2.47.0+rev1',
|
||||
`os configure ${tmpMatchingDtJsonPartitionPath}`,
|
||||
'--device-type jetson-nano',
|
||||
'--fleet testApp',
|
||||
'--config-app-update-poll-interval 10',
|
||||
'--config-network ethernet',
|
||||
@ -65,9 +131,124 @@ if (process.platform !== 'win32') {
|
||||
expect(err.join('')).to.equal('');
|
||||
|
||||
// confirm the image contains a config.json...
|
||||
const imagefs = await import('balena-image-fs');
|
||||
const config = await imagefs.interact(tmpPath, 1, async (_fs) => {
|
||||
return await promisify(_fs.readFile)('/config.json');
|
||||
const config = await imagefs.interact(
|
||||
tmpMatchingDtJsonPartitionPath,
|
||||
12,
|
||||
async (_fs) => {
|
||||
const dtJson = JSON.parse(
|
||||
await _fs.promises.readFile('/device-type.json', {
|
||||
encoding: 'utf8',
|
||||
}),
|
||||
);
|
||||
// confirm that the device-type.json mentions the expected partition
|
||||
expect(dtJson).to.have.nested.property(
|
||||
'configuration.config.partition',
|
||||
12,
|
||||
);
|
||||
return await _fs.promises.readFile('/config.json');
|
||||
},
|
||||
);
|
||||
expect(config).to.not.be.empty;
|
||||
|
||||
// confirm the image has the correct config.json values...
|
||||
const configObj = JSON.parse(config.toString('utf8'));
|
||||
expect(configObj).to.have.property('deviceType', 'jetson-nano');
|
||||
expect(configObj).to.have.property('initialDeviceName', 'testDeviceName');
|
||||
});
|
||||
|
||||
it('should detect the OS version and inject a valid config.json file to a 6.1.25 image with partition 12 as boot & a non-matching device-type.json', async () => {
|
||||
api.expectGetApplication();
|
||||
api.expectGetDeviceTypes();
|
||||
// It should not reach to /config or /device-types/v1 but instead find
|
||||
// everything required from the device-type.json in the image.
|
||||
// api.expectGetConfigDeviceTypes();
|
||||
api.expectDownloadConfig();
|
||||
|
||||
const command: string[] = [
|
||||
`os configure ${tmpNonMatchingDtJsonPartitionPath}`,
|
||||
'--device-type jetson-nano',
|
||||
'--fleet testApp',
|
||||
'--config-app-update-poll-interval 10',
|
||||
'--config-network ethernet',
|
||||
'--initial-device-name testDeviceName',
|
||||
'--provisioning-key-name testKey',
|
||||
'--provisioning-key-expiry-date 2050-12-12',
|
||||
];
|
||||
|
||||
const { err } = await runCommand(command.join(' '));
|
||||
expect(err.join('')).to.equal('');
|
||||
|
||||
// confirm the image contains a config.json...
|
||||
const config = await imagefs.interact(
|
||||
tmpNonMatchingDtJsonPartitionPath,
|
||||
12,
|
||||
async (_fs) => {
|
||||
const dtJson = JSON.parse(
|
||||
await _fs.promises.readFile('/device-type.json', {
|
||||
encoding: 'utf8',
|
||||
}),
|
||||
);
|
||||
// confirm that the device-type.json mentions the expected partition
|
||||
expect(dtJson).to.have.nested.property(
|
||||
'configuration.config.partition',
|
||||
999,
|
||||
);
|
||||
return await _fs.promises.readFile('/config.json');
|
||||
},
|
||||
);
|
||||
expect(config).to.not.be.empty;
|
||||
|
||||
// confirm the image has the correct config.json values...
|
||||
const configObj = JSON.parse(config.toString('utf8'));
|
||||
expect(configObj).to.have.property('deviceType', 'jetson-nano');
|
||||
expect(configObj).to.have.property('initialDeviceName', 'testDeviceName');
|
||||
});
|
||||
|
||||
// TODO: In the next major consider just failing when we can't find a device-types.json in the image.
|
||||
it('should inject a valid config.json file to a dummy image', async () => {
|
||||
api.expectGetApplication();
|
||||
// Since the dummy image doesn't include a device-type.json
|
||||
// we have to reach to the API to fetch the manifest of the device type.
|
||||
api.expectGetConfigDeviceTypes();
|
||||
api.expectDownloadConfig();
|
||||
|
||||
const command: string[] = [
|
||||
`os configure ${tmpDummyPath}`,
|
||||
'--device-type raspberrypi3',
|
||||
'--version 2.47.0+rev1',
|
||||
'--fleet testApp',
|
||||
'--config-app-update-poll-interval 10',
|
||||
'--config-network ethernet',
|
||||
'--initial-device-name testDeviceName',
|
||||
'--provisioning-key-name testKey',
|
||||
'--provisioning-key-expiry-date 2050-12-12',
|
||||
];
|
||||
|
||||
const { err } = await runCommand(command.join(' '));
|
||||
// Once we replace the dummy.img with one that includes a os-release & device-type.json
|
||||
// then we should be able to change this to expect no errors.
|
||||
expect(
|
||||
err.flatMap((line) => line.split('\n')).filter((line) => line !== ''),
|
||||
).to.deep.equal(
|
||||
stripIndent`
|
||||
[warn] "${tmpDummyPath}":
|
||||
[warn] Found partition table with 1 partitions,
|
||||
[warn] but none with a name/label in ['resin-boot', 'flash-boot', 'balena-boot'].
|
||||
[warn] Will scan all partitions for contents.
|
||||
[warn] "${tmpDummyPath}":
|
||||
[warn] 1 partition(s) found, but none containing file "/device-type.json".
|
||||
[warn] Assuming default boot partition number '1'.
|
||||
[warn] "${tmpDummyPath}":
|
||||
[warn] Could not find a previous "/config.json" file in partition '1'.
|
||||
[warn] Proceeding anyway, but this is unexpected.
|
||||
[warn] Error while finding a device-type.json on the provided image path. Attempting to fetch from the API.`.split(
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
|
||||
// confirm the image contains a config.json...
|
||||
const config = await imagefs.interact(tmpDummyPath, 1, async (_fs) => {
|
||||
return await _fs.promises.readFile('/config.json');
|
||||
});
|
||||
expect(config).to.not.be.empty;
|
||||
|
||||
|
@ -462,7 +462,13 @@ describe('balena push', function () {
|
||||
tmp.setGracefulCleanup();
|
||||
const projectPath = await new Promise<string>((resolve, reject) => {
|
||||
const opts = { template: 'tmp-XXXXXX', unsafeCleanup: true };
|
||||
tmp.dir(opts, (e, p) => (e ? reject(e) : resolve(p)));
|
||||
tmp.dir(opts, (e, p) => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
} else {
|
||||
resolve(p);
|
||||
}
|
||||
});
|
||||
});
|
||||
console.error(`[debug] Temp project dir: ${projectPath}`);
|
||||
|
||||
@ -475,7 +481,7 @@ describe('balena push', function () {
|
||||
try {
|
||||
server.listen(socketPath, resolve);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
reject(e as Error);
|
||||
}
|
||||
});
|
||||
console.error(`[debug] Checking existence of socket at '${socketPath}'`);
|
||||
@ -505,7 +511,13 @@ describe('balena push', function () {
|
||||
|
||||
// Terminate Unix Domain Socket server
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((e) => (e ? reject(e) : resolve()));
|
||||
server.close((e) => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
expect(await exists(socketPath), 'Socket existence').to.be.false;
|
||||
|
@ -82,7 +82,7 @@ describe('balena release', function () {
|
||||
expect(err).to.be.empty;
|
||||
const json = JSON.parse(out.join(''));
|
||||
expect(json[0].commit).to.equal('90247b54de4fa7a0a3cbc85e73c68039');
|
||||
expect(json[0].contains__image[0].image[0].start_timestamp).to.equal(
|
||||
expect(json[0].release_image[0].image[0].start_timestamp).to.equal(
|
||||
'2020-01-04T01:13:08.583Z',
|
||||
);
|
||||
});
|
||||
|
@ -26,7 +26,7 @@ describe('balena whoami', function () {
|
||||
api = new BalenaAPIMock();
|
||||
});
|
||||
|
||||
this.afterEach(async () => {
|
||||
this.afterEach(() => {
|
||||
// Check all expected api calls have been made and clean up.
|
||||
api.done();
|
||||
});
|
||||
|
@ -26,7 +26,6 @@ import * as tar from 'tar-stream';
|
||||
import { streamToBuffer } from 'tar-utils';
|
||||
import { URL } from 'url';
|
||||
import { diff } from 'deep-object-diff';
|
||||
|
||||
import { makeImageName } from '../build/utils/compose_ts';
|
||||
import { stripIndent } from '../build/utils/lazy';
|
||||
import type { BuilderMock } from './nock/builder-mock';
|
||||
@ -77,13 +76,13 @@ export async function inspectTarStream(
|
||||
type: header.type,
|
||||
};
|
||||
const expected = expectedFiles[header.name];
|
||||
if (expected && expected.testStream) {
|
||||
if (expected?.testStream) {
|
||||
await expected.testStream(header, stream, expected);
|
||||
} else {
|
||||
await defaultTestStream(header, stream, expected, projectPath);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
reject(err as Error);
|
||||
}
|
||||
next();
|
||||
},
|
||||
@ -144,9 +143,8 @@ export async function expectStreamNoCRLF(
|
||||
_header: tar.Headers,
|
||||
stream: Readable,
|
||||
): Promise<void> {
|
||||
const chai = await import('chai');
|
||||
const buf = await streamToBuffer(stream);
|
||||
await chai.expect(buf.includes('\r\n')).to.be.false;
|
||||
expect(buf.includes('\r\n')).to.be.false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -184,7 +182,7 @@ export async function testDockerBuildStream(o: {
|
||||
|
||||
o.dockerMock.expectPostBuild({
|
||||
...o,
|
||||
checkURI: async (uri: string) => {
|
||||
checkURI: (uri: string) => {
|
||||
const url = new URL(uri, 'http://test.net/');
|
||||
const queryParams = Array.from(url.searchParams.entries());
|
||||
expect(deepJsonParse(queryParams)).to.have.deep.members(
|
||||
@ -241,7 +239,7 @@ export async function testPushBuildStream(o: {
|
||||
|
||||
o.builderMock.expectPostBuild({
|
||||
...o,
|
||||
checkURI: async (uri: string) => {
|
||||
checkURI: (uri: string) => {
|
||||
const url = new URL(uri, 'http://test.net/');
|
||||
const queryParams = Array.from(url.searchParams.entries());
|
||||
expect(deepJsonParse(queryParams)).to.have.deep.members(
|
||||
|
@ -287,15 +287,14 @@ export function monochrome(text: string): string {
|
||||
*/
|
||||
export function fillTemplate(
|
||||
templateString: string,
|
||||
templateVars: object,
|
||||
templateVars: Record<string, unknown>,
|
||||
): string {
|
||||
const escaped = templateString.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
|
||||
const resolved = new Function(
|
||||
...Object.keys(templateVars),
|
||||
`return \`${escaped}\`;`,
|
||||
).call(null, ...Object.values(templateVars));
|
||||
const unescaped = resolved.replace(/\\`/g, '`').replace(/\\\\/g, '\\');
|
||||
return unescaped;
|
||||
return templateString.replace(/\$\{(\w+)\}/g, (_, key) => {
|
||||
if (key in templateVars) {
|
||||
return String(templateVars[key]);
|
||||
}
|
||||
throw new Error(`Missing template variable: ${key}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,22 +61,25 @@ export class BalenaAPIMock extends NockMock {
|
||||
}
|
||||
|
||||
public expectDownloadConfig(opts: ScopeOpts = {}) {
|
||||
this.optPost('/download-config', opts).reply(
|
||||
200,
|
||||
JSON.parse(`{
|
||||
"applicationId":1301645,
|
||||
"deviceType":"raspberrypi3",
|
||||
"userId":43699,
|
||||
"appUpdatePollInterval":600000,
|
||||
"listenPort":48484,
|
||||
"vpnPort":443,
|
||||
"apiEndpoint":"https://api.balena-cloud.com",
|
||||
"vpnEndpoint":"vpn.balena-cloud.com",
|
||||
"registryEndpoint":"registry2.balena-cloud.com",
|
||||
"deltaEndpoint":"https://delta.balena-cloud.com",
|
||||
"apiKey":"nothingtoseehere"
|
||||
}`),
|
||||
);
|
||||
this.optPost('/download-config', opts).reply(200, (_uri, body) => {
|
||||
let deviceType = 'raspberrypi3';
|
||||
if (typeof body === 'object' && 'deviceType' in body) {
|
||||
deviceType = body.deviceType;
|
||||
}
|
||||
return JSON.parse(`{
|
||||
"applicationId":1301645,
|
||||
"deviceType":"${deviceType}",
|
||||
"userId":43699,
|
||||
"appUpdatePollInterval":600000,
|
||||
"listenPort":48484,
|
||||
"vpnPort":443,
|
||||
"apiEndpoint":"https://api.balena-cloud.com",
|
||||
"vpnEndpoint":"vpn.balena-cloud.com",
|
||||
"registryEndpoint":"registry2.balena-cloud.com",
|
||||
"deltaEndpoint":"https://delta.balena-cloud.com",
|
||||
"apiKey":"nothingtoseehere"
|
||||
}`);
|
||||
});
|
||||
}
|
||||
|
||||
public expectApplicationProvisioning(opts: ScopeOpts = {}) {
|
||||
@ -283,7 +286,7 @@ export class BalenaAPIMock extends NockMock {
|
||||
this.optGet(/^\/v\d+\/service_environment_variable($|\?)/, opts).reply(
|
||||
function (uri, _requestBody) {
|
||||
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
|
||||
const serviceName = (match && match[1]) || undefined;
|
||||
const serviceName = match?.[1] || undefined;
|
||||
let varArray: any[];
|
||||
if (serviceName) {
|
||||
const varObj = appServiceVarsByService[serviceName];
|
||||
@ -331,7 +334,7 @@ export class BalenaAPIMock extends NockMock {
|
||||
opts,
|
||||
).reply(function (uri, _requestBody) {
|
||||
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
|
||||
const serviceName = (match && match[1]) || undefined;
|
||||
const serviceName = match?.[1] || undefined;
|
||||
let varArray: any[];
|
||||
if (serviceName) {
|
||||
const varObj = deviceServiceVarsByService[serviceName];
|
||||
|
@ -37,7 +37,7 @@ export class BuilderMock extends NockMock {
|
||||
persist?: boolean;
|
||||
responseBody: any;
|
||||
responseCode: number;
|
||||
checkURI: (uri: string) => Promise<void>;
|
||||
checkURI: (uri: string) => Promise<void> | void;
|
||||
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
|
||||
}) {
|
||||
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(
|
||||
|
@ -75,7 +75,7 @@ export class DockerMock extends NockMock {
|
||||
responseBody: any;
|
||||
responseCode: number;
|
||||
tag: string;
|
||||
checkURI: (uri: string) => Promise<void>;
|
||||
checkURI: (uri: string) => Promise<void> | void;
|
||||
checkBuildRequestBody: (requestBody: string) => Promise<void>;
|
||||
}) {
|
||||
this.optPost(
|
||||
|
@ -37,7 +37,7 @@ export class NockMock {
|
||||
|
||||
constructor(
|
||||
public basePathPattern: string | RegExp,
|
||||
public allowUnmocked: boolean = false,
|
||||
public allowUnmocked = false,
|
||||
) {
|
||||
if (NockMock.instanceCount === 0) {
|
||||
if (!nock.isActive()) {
|
||||
|
@ -50,7 +50,7 @@ export async function exists(fPath: string) {
|
||||
try {
|
||||
await fs.stat(fPath);
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
"build_log": null,
|
||||
"start_timestamp": "2021-08-25T22:18:33.624Z",
|
||||
"end_timestamp": "2021-08-25T22:18:48.820Z",
|
||||
"contains__image": [
|
||||
"release_image": [
|
||||
{
|
||||
"image": [
|
||||
{
|
||||
|
Binary file not shown.
@ -373,6 +373,8 @@
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/index.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/FormData.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/form-data-encoder/lib/index.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/formdata-node/lib/form-data.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js
|
||||
|
@ -373,6 +373,8 @@
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/FormData.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/form-data-encoder/lib/index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/formdata-node/lib/form-data.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js
|
||||
|
@ -373,6 +373,8 @@
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/index.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/FormData.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/form-data-encoder/lib/index.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/formdata-node/lib/form-data.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
|
||||
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js
|
||||
|
@ -373,6 +373,8 @@
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/FormData.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/form-data-encoder/lib/index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/formdata-node/lib/form-data.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js
|
||||
|
@ -373,6 +373,8 @@
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules\axios\lib\platform\node\index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules\axios\lib\platform\node\classes\FormData.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules\axios\lib\platform\node\classes\URLSearchParams.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules\form-data-encoder\lib\index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules\formdata-node\lib\form-data.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules\@isaacs\cliui\node_modules\string-width\index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules\@isaacs\cliui\node_modules\strip-ansi\index.js
|
||||
> Warning Failed to make bytecode node20-x64 for file node_modules\@isaacs\cliui\node_modules\wrap-ansi\index.js
|
||||
|
@ -75,12 +75,12 @@ describe('detectEncoding() function', function () {
|
||||
|
||||
for (const fname of sampleBinary) {
|
||||
const buf = await fs.readFile(path.join('node_modules', fname));
|
||||
const encoding = await detectEncoding(buf);
|
||||
const encoding = detectEncoding(buf);
|
||||
expect(encoding).to.equal('binary');
|
||||
}
|
||||
for (const fname of sampleText) {
|
||||
const buf = await fs.readFile(fname);
|
||||
const encoding = await detectEncoding(buf);
|
||||
const encoding = detectEncoding(buf);
|
||||
expect(encoding).to.equal('utf-8');
|
||||
}
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as stream from 'stream';
|
||||
import { AssertionError, expect } from 'chai';
|
||||
import { expect } from 'chai';
|
||||
import { stub } from 'sinon';
|
||||
import * as tmp from 'tmp';
|
||||
import { delay } from '../../utils';
|
||||
@ -32,9 +32,7 @@ describe('image-manager', function () {
|
||||
fs.writeSync(this.image.fd, 'Cache image', 0, 'utf8');
|
||||
|
||||
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
|
||||
return this.cacheGetImagePathStub.returns(
|
||||
Promise.resolve(this.image.name),
|
||||
);
|
||||
return this.cacheGetImagePathStub.resolves(this.image.name);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -44,8 +42,8 @@ describe('image-manager', function () {
|
||||
|
||||
describe('given the image is fresh', function () {
|
||||
beforeEach(function () {
|
||||
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
|
||||
return this.cacheIsImageFresh.returns(Promise.resolve(true));
|
||||
this.cacheIsImageFresh = stub(imageManager, 'isImageCached');
|
||||
return this.cacheIsImageFresh.resolves(true);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -58,11 +56,11 @@ describe('image-manager', function () {
|
||||
void imageManager.getStream('raspberry-pi').then(function (stream) {
|
||||
let result = '';
|
||||
|
||||
stream.on('data', (chunk) => (result += chunk.toString()));
|
||||
stream.on('data', (chunk: string) => (result += chunk.toString()));
|
||||
|
||||
return stream.on('end', function () {
|
||||
expect(result).to.equal('Cache image');
|
||||
return done();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -70,8 +68,8 @@ describe('image-manager', function () {
|
||||
|
||||
describe('given the image is not fresh', function () {
|
||||
beforeEach(function () {
|
||||
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
|
||||
return this.cacheIsImageFresh.returns(Promise.resolve(false));
|
||||
this.cacheIsImageFresh = stub(imageManager, 'isImageCached');
|
||||
return this.cacheIsImageFresh.resolves(false);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -82,9 +80,7 @@ describe('image-manager', function () {
|
||||
describe.skip('given a valid download endpoint', function () {
|
||||
beforeEach(function () {
|
||||
this.osDownloadStub = stub(balena.models.os, 'download');
|
||||
this.osDownloadStub.returns(
|
||||
Promise.resolve(stringToStream('Download image')),
|
||||
);
|
||||
this.osDownloadStub.resolves(stringToStream('Download image'));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -95,7 +91,7 @@ describe('image-manager', function () {
|
||||
void imageManager.getStream('raspberry-pi').then((stream) => {
|
||||
let result = '';
|
||||
|
||||
stream.on('data', (chunk) => (result += chunk));
|
||||
stream.on('data', (chunk: string) => (result += chunk));
|
||||
|
||||
stream.on('end', async () => {
|
||||
expect(result).to.equal('Download image');
|
||||
@ -131,7 +127,7 @@ describe('image-manager', function () {
|
||||
beforeEach(function () {
|
||||
this.osDownloadStream = new stream.PassThrough();
|
||||
this.osDownloadStub = stub(balena.models.os, 'download');
|
||||
this.osDownloadStub.returns(Promise.resolve(this.osDownloadStream));
|
||||
this.osDownloadStub.resolves(this.osDownloadStream);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -153,9 +149,7 @@ describe('image-manager', function () {
|
||||
const contents = await fsAsync
|
||||
.stat(this.image.name + '.inprogress')
|
||||
.then(function () {
|
||||
throw new AssertionError(
|
||||
'Image cache should be deleted on failure',
|
||||
);
|
||||
throw new Error('Image cache should be deleted on failure');
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.code !== 'ENOENT') {
|
||||
@ -174,7 +168,7 @@ describe('image-manager', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a stream with the mime property', async function () {
|
||||
describe('given a stream with the mime property', function () {
|
||||
beforeEach(function () {
|
||||
this.osDownloadStub = stub(balena.models.os, 'download');
|
||||
const message = 'Lorem ipsum dolor sit amet';
|
||||
@ -184,7 +178,7 @@ describe('image-manager', function () {
|
||||
mime?: string;
|
||||
};
|
||||
mockResultStream.mime = 'application/zip';
|
||||
this.osDownloadStub.returns(Promise.resolve(mockResultStream));
|
||||
this.osDownloadStub.resolves(mockResultStream);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -209,12 +203,10 @@ describe('image-manager', function () {
|
||||
|
||||
this.balenaSettingsGetStub
|
||||
.withArgs('cacheDirectory')
|
||||
.returns(
|
||||
Promise.resolve(
|
||||
os.platform() === 'win32'
|
||||
? 'C:\\Users\\johndoe\\_balena\\cache'
|
||||
: '/Users/johndoe/.balena/cache',
|
||||
),
|
||||
.resolves(
|
||||
os.platform() === 'win32'
|
||||
? 'C:\\Users\\johndoe\\_balena\\cache'
|
||||
: '/Users/johndoe/.balena/cache',
|
||||
);
|
||||
});
|
||||
|
||||
@ -228,21 +220,21 @@ describe('image-manager', function () {
|
||||
balena.models.config,
|
||||
'getDeviceTypeManifestBySlug',
|
||||
);
|
||||
this.getDeviceTypeManifestBySlugStub.withArgs('raspberry-pi').returns(
|
||||
Promise.resolve({
|
||||
this.getDeviceTypeManifestBySlugStub
|
||||
.withArgs('raspberry-pi')
|
||||
.resolves({
|
||||
yocto: {
|
||||
fstype: 'resin-sdcard',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
this.getDeviceTypeManifestBySlugStub.withArgs('intel-edison').returns(
|
||||
Promise.resolve({
|
||||
this.getDeviceTypeManifestBySlugStub
|
||||
.withArgs('intel-edison')
|
||||
.resolves({
|
||||
yocto: {
|
||||
fstype: 'zip',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -288,20 +280,18 @@ describe('image-manager', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isImageFresh()', () => {
|
||||
describe('.isImageCached()', () => {
|
||||
describe('given the raspberry-pi manifest', function () {
|
||||
beforeEach(function () {
|
||||
this.getDeviceTypeManifestBySlugStub = stub(
|
||||
balena.models.config,
|
||||
'getDeviceTypeManifestBySlug',
|
||||
);
|
||||
this.getDeviceTypeManifestBySlugStub.returns(
|
||||
Promise.resolve({
|
||||
yocto: {
|
||||
fstype: 'balena-sdcard',
|
||||
},
|
||||
}),
|
||||
);
|
||||
this.getDeviceTypeManifestBySlugStub.resolves({
|
||||
yocto: {
|
||||
fstype: 'balena-sdcard',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -314,8 +304,8 @@ describe('image-manager', function () {
|
||||
imageManager,
|
||||
'getFileCreatedDate',
|
||||
);
|
||||
this.utilsGetFileCreatedDate.returns(
|
||||
Promise.reject(new Error("ENOENT, stat 'raspberry-pi'")),
|
||||
this.utilsGetFileCreatedDate.rejects(
|
||||
new Error("ENOENT, stat 'raspberry-pi'"),
|
||||
);
|
||||
});
|
||||
|
||||
@ -324,78 +314,8 @@ describe('image-manager', function () {
|
||||
});
|
||||
|
||||
it('should return false', async function () {
|
||||
expect(await imageManager.isImageFresh('raspberry-pi', '1.2.3')).to.be
|
||||
.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a fixed created time', function () {
|
||||
beforeEach(function () {
|
||||
this.utilsGetFileCreatedDate = stub(
|
||||
imageManager,
|
||||
'getFileCreatedDate',
|
||||
);
|
||||
this.utilsGetFileCreatedDate.returns(
|
||||
Promise.resolve(new Date('2014-01-01T00:00:00.000Z')),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.utilsGetFileCreatedDate.restore();
|
||||
});
|
||||
|
||||
describe('given the file was created before the os last modified time', function () {
|
||||
beforeEach(function () {
|
||||
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
|
||||
this.osGetLastModified.returns(
|
||||
Promise.resolve(new Date('2014-02-01T00:00:00.000Z')),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.osGetLastModified.restore();
|
||||
});
|
||||
|
||||
it('should return false', function () {
|
||||
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
|
||||
return expect(promise).to.eventually.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the file was created after the os last modified time', function () {
|
||||
beforeEach(function () {
|
||||
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
|
||||
this.osGetLastModified.returns(
|
||||
Promise.resolve(new Date('2013-01-01T00:00:00.000Z')),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.osGetLastModified.restore();
|
||||
});
|
||||
|
||||
it('should return true', function () {
|
||||
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
|
||||
return expect(promise).to.eventually.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the file was created just at the os last modified time', function () {
|
||||
beforeEach(function () {
|
||||
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
|
||||
this.osGetLastModified.returns(
|
||||
Promise.resolve(new Date('2014-00-01T00:00:00.000Z')),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.osGetLastModified.restore();
|
||||
});
|
||||
|
||||
it('should return false', function () {
|
||||
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
|
||||
return expect(promise).to.eventually.be.false;
|
||||
});
|
||||
expect(await imageManager.isImageCached('raspberry-pi', '1.2.3')).to
|
||||
.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -408,7 +328,7 @@ describe('image-manager', function () {
|
||||
fs.writeSync(this.image.fd, 'Lorem ipsum dolor sit amet', 0, 'utf8');
|
||||
|
||||
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
|
||||
this.cacheGetImagePathStub.returns(Promise.resolve(this.image.name));
|
||||
this.cacheGetImagePathStub.resolves(this.image.name);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
@ -422,7 +342,7 @@ describe('image-manager', function () {
|
||||
.then(function (stream) {
|
||||
let result = '';
|
||||
|
||||
stream.on('data', (chunk) => (result += chunk));
|
||||
stream.on('data', (chunk) => (result += chunk as string));
|
||||
|
||||
stream.on('end', function () {
|
||||
expect(result).to.equal('Lorem ipsum dolor sit amet');
|
||||
@ -445,7 +365,7 @@ describe('image-manager', function () {
|
||||
beforeEach(function () {
|
||||
this.image = tmp.fileSync();
|
||||
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
|
||||
this.cacheGetImagePathStub.returns(Promise.resolve(this.image.name));
|
||||
this.cacheGetImagePathStub.resolves(this.image.name);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
@ -488,9 +408,7 @@ describe('image-manager', function () {
|
||||
beforeEach(function () {
|
||||
this.date = new Date(2014, 1, 1);
|
||||
this.fsStatStub = stub(fs.promises, 'stat');
|
||||
this.fsStatStub
|
||||
.withArgs('foo')
|
||||
.returns(Promise.resolve({ ctime: this.date }));
|
||||
this.fsStatStub.withArgs('foo').resolves({ ctime: this.date });
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -508,7 +426,7 @@ describe('image-manager', function () {
|
||||
this.fsStatStub = stub(fs.promises, 'stat');
|
||||
this.fsStatStub
|
||||
.withArgs('foo')
|
||||
.returns(Promise.reject(new Error("ENOENT, stat 'foo'")));
|
||||
.rejects(new Error("ENOENT, stat 'foo'"));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -554,7 +472,9 @@ describe('image-manager', function () {
|
||||
mockFs({});
|
||||
});
|
||||
|
||||
afterEach(() => mockFs.restore());
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should keep the cache directory removed', async function () {
|
||||
const exists = await fsExistsAsync(this.cacheDirectory);
|
||||
|
@ -117,7 +117,7 @@ describe('disambiguateReleaseParam() function', () => {
|
||||
it('should return id from SDK on first call, if match is found', async () => {
|
||||
const input = '1234';
|
||||
const output = 1234;
|
||||
const getRelease = sinon.stub().returns(Promise.resolve({ id: output }));
|
||||
const getRelease = sinon.stub().resolves({ id: output });
|
||||
const sdk: any = {
|
||||
models: {
|
||||
release: {
|
||||
@ -139,9 +139,9 @@ describe('disambiguateReleaseParam() function', () => {
|
||||
const getRelease = sinon
|
||||
.stub()
|
||||
.onCall(0)
|
||||
.returns(Promise.reject(new BalenaReleaseNotFound(input)))
|
||||
.rejects(new BalenaReleaseNotFound(input))
|
||||
.onCall(1)
|
||||
.returns(Promise.resolve({ id: output }));
|
||||
.resolves({ id: output });
|
||||
|
||||
const sdk: any = {
|
||||
models: {
|
||||
@ -161,9 +161,7 @@ describe('disambiguateReleaseParam() function', () => {
|
||||
|
||||
it('should throw error if no match found', async () => {
|
||||
const input = '1234';
|
||||
const getRelease = sinon
|
||||
.stub()
|
||||
.returns(Promise.reject(new BalenaReleaseNotFound(input)));
|
||||
const getRelease = sinon.stub().rejects(new BalenaReleaseNotFound(input));
|
||||
|
||||
const sdk: any = {
|
||||
models: {
|
||||
@ -185,9 +183,7 @@ describe('disambiguateReleaseParam() function', () => {
|
||||
it('should throw error if unknown error returned from SDK', async () => {
|
||||
const input = '1234';
|
||||
|
||||
const getRelease = sinon
|
||||
.stub()
|
||||
.returns(Promise.reject(new Error('some error')));
|
||||
const getRelease = sinon.stub().rejects(new Error('some error'));
|
||||
|
||||
const sdk: any = {
|
||||
models: {
|
||||
|
@ -15,10 +15,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import * as chai from 'chai';
|
||||
import { ExpectedError } from '../../build/errors';
|
||||
import * as v from '../../build/utils/validation';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
const { expect } = chai;
|
||||
|
||||
describe('validateEmail() function', () => {
|
||||
it('should reject invalid email addresses with a message', () => {
|
||||
const errorMessage = 'Email is not valid';
|
||||
@ -186,55 +190,55 @@ describe('validateUuid() function', () => {
|
||||
|
||||
describe('parseAsInteger() function', () => {
|
||||
it('should reject non-numeric characters', () => {
|
||||
expect(() => v.parseAsInteger('abc')).to.throw(ExpectedError);
|
||||
expect(() => v.parseAsInteger('1a')).to.throw(ExpectedError);
|
||||
expect(() => v.parseAsInteger('a1')).to.throw(ExpectedError);
|
||||
expect(() => v.parseAsInteger('a')).to.throw(ExpectedError);
|
||||
expect(() => v.parseAsInteger('1.0')).to.throw(ExpectedError);
|
||||
expect(v.parseAsInteger('abc')).to.be.rejectedWith(ExpectedError);
|
||||
expect(v.parseAsInteger('1a')).to.be.rejectedWith(ExpectedError);
|
||||
expect(v.parseAsInteger('a1')).to.be.rejectedWith(ExpectedError);
|
||||
expect(v.parseAsInteger('a')).to.be.rejectedWith(ExpectedError);
|
||||
expect(v.parseAsInteger('1.0')).to.be.rejectedWith(ExpectedError);
|
||||
});
|
||||
|
||||
it('should reject leading zeros', () => {
|
||||
expect(() => v.parseAsInteger('01')).to.throw(ExpectedError);
|
||||
expect(() => v.parseAsInteger('001')).to.throw(ExpectedError);
|
||||
expect(v.parseAsInteger('01')).to.be.rejectedWith(ExpectedError);
|
||||
expect(v.parseAsInteger('001')).to.be.rejectedWith(ExpectedError);
|
||||
});
|
||||
|
||||
it('should throw with specific message when param name passed', () => {
|
||||
expect(() => v.parseAsInteger('abc')).to.throw(
|
||||
expect(v.parseAsInteger('abc')).to.be.rejectedWith(
|
||||
'The parameter must be an integer.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with general message when no param name passed', () => {
|
||||
expect(() => v.parseAsInteger('abc', 'foo')).to.throw(
|
||||
expect(v.parseAsInteger('abc', 'foo')).to.be.rejectedWith(
|
||||
"The parameter 'foo' must be an integer.",
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse integers to number type', () => {
|
||||
expect(v.parseAsInteger('100')).to.equal(100);
|
||||
expect(v.parseAsInteger('100')).to.be.a('number');
|
||||
expect(v.parseAsInteger('0')).to.equal(0);
|
||||
expect(v.parseAsInteger('0')).to.be.a('number');
|
||||
it('should parse integers to number type', async () => {
|
||||
expect(await v.parseAsInteger('100')).to.equal(100);
|
||||
expect(await v.parseAsInteger('100')).to.be.a('number');
|
||||
expect(await v.parseAsInteger('0')).to.equal(0);
|
||||
expect(await v.parseAsInteger('0')).to.be.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tryAsInteger() function', () => {
|
||||
it('should return string with non-numeric characters as string', () => {
|
||||
expect(v.tryAsInteger('abc')).to.be.a('string');
|
||||
expect(v.tryAsInteger('1a')).to.be.a('string');
|
||||
expect(v.tryAsInteger('a1')).to.be.a('string');
|
||||
expect(v.tryAsInteger('a')).to.be.a('string');
|
||||
expect(v.tryAsInteger('1.0')).to.be.a('string');
|
||||
it('should return string with non-numeric characters as string', async () => {
|
||||
expect(await v.tryAsInteger('abc')).to.be.a('string');
|
||||
expect(await v.tryAsInteger('1a')).to.be.a('string');
|
||||
expect(await v.tryAsInteger('a1')).to.be.a('string');
|
||||
expect(await v.tryAsInteger('a')).to.be.a('string');
|
||||
expect(await v.tryAsInteger('1.0')).to.be.a('string');
|
||||
});
|
||||
|
||||
it('should return numerical strings with leading zeros as string', () => {
|
||||
expect(v.tryAsInteger('01')).to.be.a('string');
|
||||
expect(v.tryAsInteger('001')).to.be.a('string');
|
||||
it('should return numerical strings with leading zeros as string', async () => {
|
||||
expect(await v.tryAsInteger('01')).to.be.a('string');
|
||||
expect(await v.tryAsInteger('001')).to.be.a('string');
|
||||
});
|
||||
|
||||
it('should return numerical strings without leading zeros as number', () => {
|
||||
expect(v.tryAsInteger('100')).to.be.a('number');
|
||||
expect(v.tryAsInteger('256')).to.be.a('number');
|
||||
expect(v.tryAsInteger('0')).to.be.a('number');
|
||||
it('should return numerical strings without leading zeros as number', async () => {
|
||||
expect(await v.tryAsInteger('100')).to.be.a('number');
|
||||
expect(await v.tryAsInteger('256')).to.be.a('number');
|
||||
expect(await v.tryAsInteger('0')).to.be.a('number');
|
||||
});
|
||||
});
|
||||
|
@ -9,6 +9,7 @@
|
||||
"./src/**/*",
|
||||
"./patches/*",
|
||||
"./tests/**/*",
|
||||
"./typings/**/*"
|
||||
"./typings/**/*",
|
||||
".mocharc-standalone.js",
|
||||
]
|
||||
}
|
||||
|
2
typings/JSONStream/index.d.ts
vendored
2
typings/JSONStream/index.d.ts
vendored
@ -32,7 +32,7 @@ declare module 'JSONStream' {
|
||||
recurse: boolean;
|
||||
}
|
||||
|
||||
export function parse(pattern: any | any[]): NodeJS.ReadWriteStream;
|
||||
export function parse(pattern: any): NodeJS.ReadWriteStream;
|
||||
|
||||
type NewlineOnlyIndicator = false;
|
||||
|
||||
|
105
typings/balena-device-init/index.d.ts
vendored
105
typings/balena-device-init/index.d.ts
vendored
@ -1,105 +0,0 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
declare module 'balena-device-init' {
|
||||
import { DeviceTypeJson } from 'balena-sdk';
|
||||
|
||||
interface OperationState {
|
||||
operation:
|
||||
| CopyOperation
|
||||
| ReplaceOperation
|
||||
| RunScriptOperation
|
||||
| BurnOperation;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
interface Operation {
|
||||
command: string;
|
||||
}
|
||||
|
||||
interface CopyOperation extends Operation {
|
||||
command: 'copy';
|
||||
from: { path: string };
|
||||
to: { path: string };
|
||||
}
|
||||
|
||||
interface ReplaceOperation extends Operation {
|
||||
command: 'replace';
|
||||
copy: string;
|
||||
replace: string;
|
||||
file: {
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RunScriptOperation extends Operation {
|
||||
command: 'run-script';
|
||||
script: string;
|
||||
arguments?: string[];
|
||||
}
|
||||
|
||||
interface BurnOperation extends Operation {
|
||||
command: 'burn';
|
||||
image?: string;
|
||||
}
|
||||
|
||||
interface BurnProgress {
|
||||
type: 'write' | 'check';
|
||||
percentage: number;
|
||||
transferred: number;
|
||||
length: number;
|
||||
remaining: number;
|
||||
eta: number;
|
||||
runtime: number;
|
||||
delta: number;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
interface InitializeEmitter {
|
||||
on(event: 'stdout' | 'stderr', callback: (msg: string) => void): void;
|
||||
on(event: 'state', callback: (state: OperationState) => void): void;
|
||||
on(event: 'burn', callback: (state: BurnProgress) => void): void;
|
||||
on(event: 'end', callback: () => void): void;
|
||||
on(event: 'error', callback: (error: Error) => void): void;
|
||||
}
|
||||
|
||||
// As of writing this, these are Bluebird promises, but we are typing then
|
||||
// as normal Promises so that we do not rely on Bluebird specific methods,
|
||||
// so that the CLI will not require any change once the package drops Bluebird.
|
||||
|
||||
export function configure(
|
||||
image: string,
|
||||
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
|
||||
config: object,
|
||||
options?: object,
|
||||
): Promise<InitializeEmitter>;
|
||||
|
||||
export function initialize(
|
||||
image: string,
|
||||
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
|
||||
config: object,
|
||||
): Promise<InitializeEmitter>;
|
||||
|
||||
export function getImageOsVersion(
|
||||
image: string,
|
||||
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
|
||||
): Promise<string | null>;
|
||||
|
||||
export function getImageManifest(
|
||||
image: string,
|
||||
): Promise<BalenaSdk.DeviceTypeJson.DeviceType.DeviceType | null>;
|
||||
}
|
Reference in New Issue
Block a user