Compare commits

...

90 Commits

Author SHA1 Message Date
9b6fb62e4f Update resin-compose-parse from 2.1.3 to 2.3.0
This adds support for Docker compose v2.4

Closes: #2364
Change-type: patch
Signed-off-by: Tomás Migone <tomas@balena.io>
2021-10-19 16:54:42 -03:00
cc60e86507 v12.50.2 2021-10-05 13:32:02 +03:00
bd774e8553 Merge pull request #2357 from balena-io/fix-fleet-rename-error-message
Error message when renaming a fleet now mentions the target name.
2021-10-05 10:27:51 +00:00
c493c33e38 Error message when renaming a fleet now mentions the target name.
Change-type: patch
Signed-off-by: Carlo Miguel F. Cruz <carloc@balena.io>
2021-10-05 17:01:07 +08:00
9487b33144 v12.50.1 2021-09-30 03:51:16 +03:00
befdae1b90 Merge pull request #2353 from balena-io/fix-help-release-finalize
Fix help output for 'release finalize' command
2021-09-30 00:49:35 +00:00
08dfc945f3 Update dependencies (@sentry/node error reporting)
Change-type: patch
2021-09-30 01:12:17 +01:00
8791c2f4e1 Replace mixpanel dependency with simple GET request
Change-type: patch
2021-09-30 01:12:17 +01:00
be306e6a20 Avoid NockMock warnings during standalone executable testing
Change-type: patch
2021-09-30 01:12:17 +01:00
6cfff72c59 Fix help output for 'release finalize' command
Change-type: patch
2021-09-30 01:12:17 +01:00
adae718c2e v12.50.0 2021-09-28 18:07:49 +03:00
132e1a63b2 Merge pull request #2345 from balena-io/add-release-handling
Add support for releases
2021-09-28 15:05:40 +00:00
a18e182ae4 Add support for releases
Signed-off-by: Paul Jonathan <pj@balena.io>
Change-type: minor
2021-09-28 14:28:43 +00:00
e098cdca17 v12.49.0 2021-09-23 23:56:47 +03:00
b42af74983 Merge pull request #2301 from balena-io/add-multiarch-handling
Add multiarch support
2021-09-23 20:54:51 +00:00
8bb211e441 build, deploy: Improve logging of image build messages
Change-type: patch
2021-09-23 16:37:45 +00:00
ffccbfba12 build, deploy: Add support for multiarch base images
Bump version of balena-multibuild to the one that supports multiarch
Remove previous hack to avoid sending platform information to multibuild

Change-type: minor
Signed-off-by: Paul Jonathan <pj@balena.io>
See: https://github.com/balena-io/balena-cli/issues/1508
2021-09-23 16:37:45 +00:00
56c1af50c0 v12.48.15 2021-09-22 17:52:15 +03:00
8b9e3ccdc8 Merge pull request #2346 from balena-io/update-sdk
Update balena-sdk to 15.51.1
2021-09-22 14:50:21 +00:00
de95262f93 Update balena-sdk to 15.51.1
Update balena-sdk from 15.48.0 to 15.51.1

Change-type: patch
2021-09-22 12:46:34 +05:30
ed49938504 v12.48.14 2021-09-22 00:37:13 +03:00
52ad0f6a57 Merge pull request #2347 from balena-io/klutchell/set-zlib-flush
os download: Avoid incomplete os downloads appearing as successful
2021-09-21 21:35:23 +00:00
7f6738c73c os download: Avoid incomplete os downloads appearing as successful
By forcing the zlib flush mode to Z_NO_FLUSH we are more likely to
see an error on image download pipelines vs silent failure and
incomplete files.

This is part of a larger investigation and may be removed in the
future when the root cause of the pipeline failures are identified.

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-09-20 13:28:14 -04:00
88fc3f7714 v12.48.13 2021-09-17 17:01:55 +03:00
1afb29b923 Merge pull request #2340 from balena-io/2339-config-inject-auth
config inject: Remove requirement of being logged in
2021-09-17 13:59:18 +00:00
09a4e8db2d config inject: Remove requirement of being logged in
Change-type: patch
2021-09-16 12:15:53 +01:00
6c81440428 v12.48.12 2021-09-13 18:28:39 +03:00
3eca65ce0d Merge pull request #2333 from balena-io/klutchell/qemu-6-0-0
build/deploy: Update QEMU to 6.0.0 for emulated builds
2021-09-13 15:26:41 +00:00
6319b9dc13 v12.48.11 2021-09-11 01:38:46 +03:00
290acaecbb Merge pull request #2328 from balena-io/825-1018-fix-build-deploy-tag
build, deploy: Fix processing of '--tag' option
2021-09-10 22:36:18 +00:00
305c9045f0 build, deploy: Fix processing of '--tag' option
Change-type: patch
Resolves: #825
Resolves: #1018
2021-09-10 23:11:20 +01:00
b701151769 build/deploy: Update QEMU to 6.0.0 for emulated builds
Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-09-10 13:32:12 +00:00
e03bbb7275 v12.48.10 2021-09-10 14:25:45 +03:00
3fd66c39ae Merge pull request #2331 from balena-io/2330-retry-supervisor-api-request
push: Await and retry supervisor API requests to a local device
2021-09-10 11:23:59 +00:00
b30075a18b push: Await and retry supervisor API requests to a local device
Change-type: patch
2021-09-10 01:44:26 +01:00
a4fc95e99b v12.48.9 2021-09-10 03:44:17 +03:00
63d8e5e6a3 Merge pull request #2332 from balena-io/bump-net-keepalive-v3.0.0
chore: Update net-keepalive dependency (fix CLI packaging errors)
2021-09-10 00:41:55 +00:00
6244af3464 chore: Update net-keepalive dependency (fix CLI packaging errors)
Change-type: patch
2021-09-10 00:20:26 +01:00
8773927b3f v12.48.8 2021-09-09 00:27:52 +03:00
29a3fd40a2 Merge pull request #2326 from balena-io/v13-gitignore-feature-switch
v13 preparations: Add feature switch for removal of '--gitignore' (push, build)
2021-09-08 21:24:42 +00:00
d6faf060e6 v13 preparations: Add feature switch for removal of '--gitignore' (push, build)
Change-type: patch
2021-09-08 18:10:22 +01:00
352fd197b7 v13 preparations: Adjust test cases for 'balena envs'
Change-type: patch
2021-09-08 17:48:16 +01:00
afb6f938b7 v13 preparations: Adjust test cases for 'balena devices'
Change-type: patch
2021-09-08 17:47:40 +01:00
d3adbcdba9 v12.48.7 2021-09-07 18:28:57 +03:00
33fce1f24f Merge pull request #2325 from balena-io/move-reduce-device-type-json
device move: Rely on the device type model to get the compatible apps
2021-09-07 15:26:20 +00:00
ab90a5f150 v12.48.6 2021-09-07 16:55:22 +03:00
a8b2212fed Merge pull request #2324 from balena-io/reduce-device-type-json
preload: Rely on the device type model to get the compatible apps
2021-09-07 13:53:31 +00:00
6bb8df30dd preload: Rely on the device type model to get the compatible apps
Connects-to: #2318
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 14:53:17 +03:00
0327ed766d device move: Improve types & reduce the number of API requests
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 11:43:35 +00:00
1009958340 v12.48.5 2021-09-07 14:12:24 +03:00
5ce17ea70f Merge pull request #2323 from balena-io/no-my_application
preload: Replace my_application query with the SDKs application.getAll()
2021-09-07 11:10:21 +00:00
9c821511b1 device move: Rely on the device type model to get the compatible apps
Connects-to: #2318
Change-type: patch
See: https://www.flowdock.com/app/rulemotion/i-cli/threads/s6x4Z_LoH8IG4PC_YeXMC0TP6v-
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 07:53:02 +00:00
d793335287 preload: Replace my_application query with the SDKs application.getAll()
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 02:09:39 +03:00
dc59b7e4b0 v12.48.4 2021-08-31 04:34:45 +03:00
370b844538 Merge pull request #2316 from balena-io/os-download-no-device-types-v1
os download: Use the hostApps instead of the device-types/v1 endpoint
2021-08-31 01:32:58 +00:00
a8c2724929 v12.48.3 2021-08-31 04:03:14 +03:00
09dd2dd354 Merge pull request #2317 from balena-io/balena-deploy-no-device-types-v1
balena deploy: Retrieve the cpu arch as part of the device type resource
2021-08-31 01:01:30 +00:00
f3ab41841a v12.48.2 2021-08-31 02:23:23 +03:00
3dee30a0fe Merge pull request #2313 from balena-io/install-docs-20210822
Clarify installation instructions
2021-08-30 23:21:40 +00:00
d34073f695 os download: Use the hostApps instead of the device-types/v1 endpoint
Connects-to: #2318
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-08-30 22:32:11 +00:00
24fe6666e4 balena deploy: Retrieve the cpu arch as part of the device type resource
Connects-to: #2318
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-08-30 22:30:16 +00:00
3fd5981085 Clarify installation instructions
Change-type: patch
2021-08-29 02:08:53 +01:00
08ee8643cb v12.48.1 2021-08-27 03:23:23 +03:00
8db36ccec9 Merge pull request #2312 from balena-io/remove-exitWithExpectedError
build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe)
2021-08-27 00:21:24 +00:00
deb3e4c4ac Improve error handling (remove most occurrences of process.exit())
Finally delete the deprecated exitWithExpectedError() function from
'lib/errors.ts'.

Change-type: patch
2021-08-27 00:53:21 +01:00
a8ff21af69 build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe)
Before this commit, `balena build` and `balena deploy` would almost
never respect CTRL-C on Windows (PowerShell, cmd.exe). Now CTRL-C
is respected over a large extent of runtime and, if CTRL-C is hit
while images are being uploaded (`balena deploy`), the release status
is correctly set to 'failed'.

Change-type: patch
2021-08-27 00:53:21 +01:00
4c54d6c171 v12.48.0 2021-08-26 18:38:44 +03:00
83f213c007 Merge pull request #2274 from balena-io/deploy-release-versioning
Add balena.yml handling to `balena deploy` release creation
2021-08-26 15:36:59 +00:00
d0cdc900a2 Add contract contents at release creation time
Change-type: patch
2021-08-26 16:11:23 +01:00
9937b91606 Documentation update with debugging notes
Signed-off-by: Paul Jonathan <pj@balena.io>
2021-08-26 00:34:54 +00:00
972c2470c5 Fix env variable to avoid test failures
Signed-off-by: Paul Jonathan <pj@balena.io>
Change-type: patch
2021-08-25 17:01:22 +00:00
7d568a928b Add balena.yml handling and --draft to balena deploy release creation
This change allows use of a contract and release semver when doing a push,
and is part of the larger feature to use the builder as part of a CI/CD pipeline.

Change-type: minor
Signed-off-by: Paul Jonathan <pj@balena.io>
2021-08-25 17:01:17 +00:00
2331e0a3e5 v12.47.0 2021-08-20 02:15:13 +03:00
cb9b6be24b Merge pull request #2309 from balena-io/warn-deprecation-policy
Add deprecation policy checker and --unsupported global flag
2021-08-19 23:13:25 +00:00
c2d3eee7cc Add deprecation policy checker and --unsupported global flag
Change-type: minor
2021-08-19 23:17:31 +01:00
d8b08f7272 v12.46.2 2021-08-17 02:27:23 +03:00
819bdac354 Merge pull request #2308 from balena-io/bump-sdk-15.48.0
Update dependencies (balena-sdk from v15.36.0 to v15.48.0)
2021-08-16 23:25:22 +00:00
318de8f017 Update dependencies (balena-sdk from v15.36.0 to v15.48.0)
Update balena-sdk from 15.36.0 to 15.48.0

Change-type: patch
2021-08-16 23:33:44 +01:00
2b0341e12a v12.46.1 2021-08-16 18:08:37 +03:00
21f7463607 Merge pull request #2303 from balena-io/preload-custom-dind
preload: Restore support for armv7 with custom preload image
2021-08-16 15:06:32 +00:00
19fd3094d1 preload: Restore support for armv7 with custom preload image
Update balena-preload from 10.4.20 to 10.5.0

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-08-16 09:29:14 -04:00
7c4974f4f5 v12.46.0 2021-08-15 20:09:40 +03:00
3b56ed278e Merge pull request #2270 from balena-io/allow-draft-releases
Add `--draft` option to `balena push`
2021-08-15 17:07:36 +00:00
254ef1c8cf Add --draft option to balena push
This change will allow to build releases as draft and have them being
set as final at a later stage. This change is part of a larger feature towards
using the builder as part of CI/CD pipelines.

Depends-on: https://github.com/balena-io/balena-builder/pull/868
Change-type: minor
2021-08-15 16:43:01 +00:00
d11f49e0f8 v12.45.2 2021-08-14 03:23:04 +03:00
48d7d0ef5e Merge pull request #2305 from balena-io/performResolution-error-handling
push, build: Improve error handling (identify which service failed)
2021-08-14 00:21:11 +00:00
c7bbbc4159 push, build: Improve error handling (identify which service failed)
Change-type: patch
2021-08-13 17:11:48 +01:00
d2fabcaf30 v12.45.1 2021-08-11 17:27:05 +03:00
e137c2aed2 Merge pull request #2307 from balena-io/2306-env-add-app-is-ambiguous
envs, env add: Fix "Application is ambiguous" when using device UUID
2021-08-11 14:25:21 +00:00
58704b08d3 envs, env add: Fix "Application is ambiguous" when using device UUID
Change-type: patch
2021-08-11 02:00:35 +01:00
99 changed files with 4732 additions and 1335 deletions

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,282 @@ 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/).
## 12.50.2 - 2021-10-05
* Error message when renaming a fleet now mentions the target name. [Carlo Miguel F. Cruz]
## 12.50.1 - 2021-09-30
* Update dependencies (@sentry/node error reporting) [Paulo Castro]
* Replace mixpanel dependency with simple GET request [Paulo Castro]
* Avoid NockMock warnings during standalone executable testing [Paulo Castro]
* Fix help output for 'release finalize' command [Paulo Castro]
## 12.50.0 - 2021-09-28
* Add support for releases [Paul Jonathan Zoulin]
## 12.49.0 - 2021-09-23
* build, deploy: Improve logging of image build messages [Paulo Castro]
* build, deploy: Add support for multiarch base images [toochevere]
## 12.48.15 - 2021-09-22
<details>
<summary> Update balena-sdk to 15.51.1 [Nitish Agarwal] </summary>
> ### balena-sdk-15.51.1 - 2021-09-20
>
>
> <details>
> <summary> Update balena-request to v11.4.2 [Kyle Harding] </summary>
>
>> #### balena-request-11.4.2 - 2021-09-20
>>
>> * Allow overriding the default zlib flush setting [Kyle Harding]
>>
> </details>
>
>
> ### balena-sdk-15.51.0 - 2021-09-16
>
> * os.getConfig: Add typings for the provisioningKeyName option [Nitish Agarwal]
>
> ### balena-sdk-15.50.1 - 2021-09-13
>
> * models/os: Always first normalize the device type slug [Thodoris Greasidis]
>
> ### balena-sdk-15.50.0 - 2021-09-10
>
> * Add release.finalize to promote draft releases to final [toochevere]
>
> ### balena-sdk-15.49.1 - 2021-09-10
>
> * typings: Drop the v5-model-only application_type.is_host_os [Thodoris Greasidis]
>
> ### balena-sdk-15.49.0 - 2021-09-06
>
> * os.getSupportedOsUpdateVersions: Use the hostApp releases [Thodoris Greasidis]
> * os.download: Use the hostApp for finding the latest release [Thodoris Greasidis]
>
> ### balena-sdk-15.48.3 - 2021-08-27
>
>
> <details>
> <summary> Update balena-request to 11.4.1 [Kyle Harding] </summary>
>
>> #### balena-request-11.4.1 - 2021-08-27
>>
>> * Allow more lenient gzip decompression [Kyle Harding]
>>
> </details>
>
>
> ### balena-sdk-15.48.2 - 2021-08-27
>
> * Improve hostapp.getAllOsVersions performance & reduce fetched data [Thodoris Greasidis]
>
> ### balena-sdk-15.48.1 - 2021-08-27
>
> * Update typescript to 4.4.2 [Thodoris Greasidis]
>
</details>
## 12.48.14 - 2021-09-20
* os download: Avoid incomplete os downloads appearing as successful [Kyle Harding]
## 12.48.13 - 2021-09-17
* config inject: Remove requirement of being logged in [Paulo Castro]
## 12.48.12 - 2021-09-10
* build/deploy: Update QEMU to 6.0.0 for emulated builds [Kyle Harding]
## 12.48.11 - 2021-09-10
* build, deploy: Fix processing of '--tag' option [Paulo Castro]
## 12.48.10 - 2021-09-10
* push: Await and retry supervisor API requests to a local device [Paulo Castro]
## 12.48.9 - 2021-09-09
* chore: Update net-keepalive dependency (fix CLI packaging errors) [Paulo Castro]
## 12.48.8 - 2021-09-08
* v13 preparations: Add feature switch for removal of '--gitignore' (push, build) [Paulo Castro]
* v13 preparations: Adjust test cases for 'balena envs' [Paulo Castro]
* v13 preparations: Adjust test cases for 'balena devices' [Paulo Castro]
## 12.48.7 - 2021-09-07
* device move: Improve types & reduce the number of API requests [Thodoris Greasidis]
* device move: Rely on the device type model to get the compatible apps [Thodoris Greasidis]
## 12.48.6 - 2021-09-07
* preload: Rely on the device type model to get the compatible apps [Thodoris Greasidis]
## 12.48.5 - 2021-09-07
* preload: Replace my_application query with the SDKs application.getAll() [Thodoris Greasidis]
## 12.48.4 - 2021-08-31
* os download: Use the hostApps instead of the device-types/v1 endpoint [Thodoris Greasidis]
## 12.48.3 - 2021-08-31
* balena deploy: Retrieve the cpu arch as part of the device type resource [Thodoris Greasidis]
## 12.48.2 - 2021-08-30
* Clarify installation instructions [Paulo Castro]
## 12.48.1 - 2021-08-27
* Improve error handling (remove most occurrences of process.exit()) [Paulo Castro]
* build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe) [Paulo Castro]
## 12.48.0 - 2021-08-26
* Add contract contents at release creation time [Paulo Castro]
* Fix env variable to avoid test failures [toochevere]
* Add balena.yml handling and `--draft` to `balena deploy` release creation [toochevere]
## 12.47.0 - 2021-08-19
* Add deprecation policy checker and --unsupported global flag [Paulo Castro]
## 12.46.2 - 2021-08-16
<details>
<summary> Update dependencies (balena-sdk from v15.36.0 to v15.48.0) [Paulo Castro] </summary>
> ### balena-sdk-15.48.0 - 2021-08-15
>
> * Deprecate the release.release_version property [Thodoris Greasidis]
> * typings: Add the release versioning properties [Thodoris Greasidis]
>
> ### balena-sdk-15.47.1 - 2021-08-10
>
> * Run browser tests using the minified browser bundle [Thodoris Greasidis]
> * Move to uglify-js to fix const assignment bug in minified build [Thodoris Greasidis]
>
> ### balena-sdk-15.47.0 - 2021-08-09
>
> * typings: Add the release.is_final & is_finalized_at__date properties [Thodoris Greasidis]
>
> ### balena-sdk-15.46.1 - 2021-07-28
>
> * apiKey.getAll: Return only NamedUserApiKeys for backwards compatibility [Thodoris Greasidis]
>
> ### balena-sdk-15.46.0 - 2021-07-27
>
> * Add email verification & email request methods [Nitish Agarwal]
>
> ### balena-sdk-15.45.0 - 2021-07-26
>
> * Update generateProvisioningKey to include keyName [Nitish Agarwal]
>
> ### balena-sdk-15.44.0 - 2021-07-15
>
> * typings: Add the subscription.is_active computed term [Thodoris Greasidis]
>
> ### balena-sdk-15.43.0 - 2021-07-14
>
> * typings: Add the organization_memebership.effective_seat_role field [Thodoris Greasidis]
>
> ### balena-sdk-15.42.2 - 2021-07-14
>
> * tests: Reduce the number of organizations created [Thodoris Greasidis]
>
> ### balena-sdk-15.42.1 - 2021-07-13
>
> * tests/api-key: Fix a race condition in the apiKey.create() tests [Thodoris Greasidis]
> * Convert the apiKey tests to async-await [Thodoris Greasidis]
>
> ### balena-sdk-15.42.0 - 2021-07-13
>
> * models/apiKey: Add getProvisioningApiKeysByApplication() method [Nitish Agarwal]
>
> ### balena-sdk-15.41.1 - 2021-06-30
>
> * Delete CODEOWNERS [Thodoris Greasidis]
>
> ### balena-sdk-15.41.0 - 2021-06-21
>
> * Add organization__has_private_access_to__device_type typings [Thodoris Greasidis]
> * typings: Add organization.has_past_due_invoice_since__date [Thodoris Greasidis]
>
> ### balena-sdk-15.40.0 - 2021-06-09
>
> * Add getAllNamedUserApiKeys() in the apiKey model [Thodoris Greasidis]
>
> ### balena-sdk-15.39.4 - 2021-06-08
>
> * Add missing modified_at in device type [JSReds]
>
> ### balena-sdk-15.39.3 - 2021-06-08
>
> * Fix lint with new linter version [JSReds]
>
> ### balena-sdk-15.39.2 - 2021-05-27
>
> * Update TypeScript to v4.3.2 [Thodoris Greasidis]
>
> ### balena-sdk-15.39.1 - 2021-05-24
>
> * Update balena-lint to v6 [Thodoris Greasidis]
>
> ### balena-sdk-15.39.0 - 2021-05-24
>
> * Add public device types [Tomás Migone]
>
> ### balena-sdk-15.38.0 - 2021-05-20
>
> * models/billing: Add changePlan method [Thodoris Greasidis]
> * Update DOCUMENTATION about getAllWithDeviceServiceDetails deprecation [Thodoris Greasidis]
>
> ### balena-sdk-15.37.0 - 2021-05-17
>
> * Add public organization types [JSReds]
>
</details>
## 12.46.1 - 2021-08-16
<details>
<summary> preload: Restore support for armv7 with custom preload image [Kyle Harding] </summary>
> ### balena-preload-10.5.0 - 2021-08-04
>
> * Remove mutually exclusive args from sfdisk [Kyle Harding]
> * Explicitly disable tls to avoid startup delays [Kyle Harding]
> * Use custom dind image based on alpine [Kyle Harding]
>
</details>
## 12.46.0 - 2021-08-15
* Add `--draft` option to `balena push` [Felipe Lalanne]
## 12.45.2 - 2021-08-13
* push, build: Improve error handling (identify which service failed) [Paulo Castro]
## 12.45.1 - 2021-08-11
* envs, env add: Fix "Application is ambiguous" when using device UUID [Paulo Castro]
## 12.45.0 - 2021-08-09
* Rename applications to fleets (stage 1). See: https://git.io/JRuZr [Paulo Castro]

View File

@ -259,3 +259,12 @@ gotchas to bear in mind:
`node_modules/balena-sdk/node_modules/balena-errors`
In the case of subclasses of `TypedError`, a string comparison may be used instead:
`error.name === 'BalenaApplicationNotFound'`
## Further debugging notes
* If you need to selectively run specific tests, `it.only` will not work in cases when authorization is required as part of the test cycle. In order to target specific tests, control execution via `.mocharc.js` instead. Here is an example of targeting the `deploy` tests.
replace: `spec: 'tests/**/*.spec.ts',`
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`

View File

@ -1,6 +1,10 @@
# balena CLI Installation Instructions for Linux
These instructions are suitable for most Linux distributions on Intel x86, except notably for **Linux Alpine** or **Busybox**. For these distros or for the ARM architecture, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation) method.
These instructions are suitable for most Linux distributions on Intel x86, such as
Ubuntu, Debian, Fedora, Arch Linux and other glibc-based distributions.
For the ARM architecture and for Linux distributions not based on glibc, such as
Alpine Linux, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation)
method.
Selected operating system: **Linux**

View File

@ -10,16 +10,14 @@ Selected operating system: **macOS**
Look for a file name that ends with "-installer.pkg":
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
2. Double click the downloaded file to run the installer. After the installation completes,
close and re-open any open [command
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
windows (so that the changes made by the installer to the PATH environment variable can take
effect).
2. Double click on the downloaded file to run the installer and follow the installer's
instructions.
3. Check that the installation was successful by running the following commands on a
command terminal:
* `balena version` - should print the CLI's version
* `balena help` - should print a list of available commands
3. Check that the installation was successful:
- [Open the Terminal
app](https://support.apple.com/en-gb/guide/terminal/apd5265185d-f365-44cb-8b09-71a064a42125/mac).
- On the terminal prompt, type `balena version` and hit Enter. It should display
the version of the balena CLI that you have installed.
No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy`
and `preload` commands may require additional software to be installed, as described

View File

@ -10,16 +10,14 @@ Selected operating system: **Windows**
Look for a file name that ends with "-installer.exe":
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
2. Double click the downloaded file to run the installer. After the installation completes,
close and re-open any open [command
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
windows (so that the changes made by the installer to the PATH environment variable can take
effect).
2. Double click on the downloaded file to run the installer and follow the installer's
instructions.
3. Check that the installation was successful by running the following commands on a
command terminal:
* `balena version` - should print the CLI's version
* `balena help` - should print a list of available commands
3. Check that the installation was successful:
- Click on the Windows Start Menu, type PowerShell, and then click
on Windows PowerShell.
- On the command prompt, type `balena version` and hit Enter. It should display
the version of the balena CLI that you have installed.
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
`deploy` and `preload` commands may require additional software to be installed, as

View File

@ -156,14 +156,18 @@ of major, minor and patch version releases.
The latest release of a major version of the balena CLI will remain compatible with
the balenaCloud backend services for at least one year from the date when the
following major version is released. For example, balena CLI v10.17.5, as the
latest v10 release, would remain compatible with the balenaCloud backend for one
year from the date when v11.0.0 is released.
following major version is released. For example, balena CLI v11.36.0, as the
latest v11 release, would remain compatible with the balenaCloud backend for one
year from the date when v12.0.0 was released.
At the end of this period, the older major version is considered deprecated and
some of the functionality that depends on balenaCloud services may stop working
at any time.
Users are encouraged to regularly update the balena CLI to the latest version.
Half way through to that period (6 months after the release of the next major
version), older major versions of the balena CLI will start printing a deprecation
warning message when it is used interactively (when `stderr` is attached to a TTY
device file). At the end of that period, older major versions will exit with an
error message unless the `--unsupported` flag is used. This behavior was
introduced in CLI version 12.47.0 and is also documented by `balena help`.
To take advantage of the latest backend features and ensure compatibility, users
are encouraged to regularly update the balena CLI to the latest version.
## Contributing (including editing documentation files)

View File

@ -22,25 +22,26 @@ import * as archiver from 'archiver';
import * as Bluebird from 'bluebird';
import { execFile } from 'child_process';
import * as filehound from 'filehound';
import { Stats } from 'fs';
import * as fs from 'fs-extra';
import * as klaw from 'klaw';
import * as _ from 'lodash';
import * as path from 'path';
import * as rimraf from 'rimraf';
import * as semver from 'semver';
import * as util from 'util';
import * as klaw from 'klaw';
import { Stats } from 'fs';
import { promisify } from 'util';
import { stripIndent } from '../lib/utils/lazy';
import { stripIndent } from '../build/utils/lazy';
import {
diffLines,
getSubprocessStdout,
loadPackageJson,
ROOT,
StdOutTap,
whichSpawn,
} from './utils';
const execFileAsync = promisify(execFile);
export const packageJSON = loadPackageJson();
export const version = 'v' + packageJSON.version;
const arch = process.arch;
@ -246,7 +247,17 @@ async function testPkg() {
console.log(`Testing standalone package "${pkgBalenaPath}"...`);
// Run `balena version -j`, parse its stdout as JSON, and check that the
// reported Node.js major version matches semver.major(process.version)
const stdout = await getSubprocessStdout(pkgBalenaPath, ['version', '-j']);
let { stdout, stderr } = await execFileAsync(pkgBalenaPath, [
'version',
'-j',
]);
const { filterCliOutputForTests } = await import('../tests/helpers');
const filtered = filterCliOutputForTests({
err: stderr.split(/\r?\n/),
out: stdout.split(/\r?\n/),
});
stdout = filtered.out.join('\n');
stderr = filtered.err.join('\n');
let pkgNodeVersion = '';
let pkgNodeMajorVersion = 0;
try {
@ -263,6 +274,10 @@ async function testPkg() {
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
);
}
if (filtered.err.length > 0) {
const err = filtered.err.join('\n');
throw new Error(`"${pkgBalenaPath}": non-empty stderr "${err}"`);
}
console.log('Success! (standalone package test successful)');
}
@ -411,8 +426,6 @@ async function renameInstallerFiles() {
async function signWindowsInstaller() {
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
const exeName = renamedOclifInstallers[process.platform];
const execFileAsync = util.promisify<string, string[], void>(execFile);
console.log(`Signing installer "${exeName}"`);
await execFileAsync(MSYS2_BASH, [
'sign-exe.sh',

View File

@ -82,6 +82,14 @@ const capitanoDoc = {
'build/commands/device/shutdown.js',
],
},
{
title: 'Releases',
files: [
'build/commands/releases.js',
'build/commands/release/index.js',
'build/commands/release/finalize.js',
],
},
{
title: 'Environment Variables',
files: [

View File

@ -101,7 +101,7 @@ async function printMarkdown() {
console.log(await renderMarkdown());
} catch (error) {
console.error(error);
process.exit(1);
process.exitCode = 1;
}
}

View File

@ -41,17 +41,25 @@ function checkNpmVersion() {
// the reason is that it would unnecessarily prevent end users from
// using npm v6.4.1 that ships with Node 8. (It is OK for the
// shrinkwrap file to get damaged if it is not going to be reused.)
console.error(`\
-------------------------------------------------------------------------------
throw new Error(`\
-----------------------------------------------------------------------------
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
At this point, however, your 'npm-shrinkwrap.json' file has already been
damaged. Please revert it to the master branch state with a command such as:
"git checkout master -- npm-shrinkwrap.json"
Then re-run "npm install" using npm version ${requiredVersion} or later.
-------------------------------------------------------------------------------`);
process.exit(1);
-----------------------------------------------------------------------------`);
}
}
checkNpmVersion();
function main() {
try {
checkNpmVersion();
} catch (e) {
console.error(e.message || e);
process.exitCode = 1;
}
}
main();

View File

@ -54,9 +54,7 @@ export async function release() {
try {
await createGitHubRelease();
} catch (err) {
console.error('Release failed');
console.error(err);
process.exit(1);
throw new Error(`Error creating GitHub release:\n${err}`);
}
}

View File

@ -35,11 +35,6 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
? ''
: '1';
function exitWithError(error: Error | string): never {
console.error(`Error: ${error}`);
process.exit(1);
}
/**
* Trivial command-line parser. Check whether the command-line argument is one
* of the following strings, then call the appropriate functions:
@ -49,12 +44,12 @@ function exitWithError(error: Error | string): never {
*
* @param args Arguments to parse (default is process.argv.slice(2))
*/
export async function run(args?: string[]) {
async function parse(args?: string[]) {
args = args || process.argv.slice(2);
console.log(`automation/run.ts process.argv=[${process.argv}]\n`);
console.log(`automation/run.ts args=[${args}]`);
console.error(`[debug] automation/run.ts process.argv=[${process.argv}]`);
console.error(`[debug] automation/run.ts args=[${args}]`);
if (_.isEmpty(args)) {
return exitWithError('missing command-line arguments');
throw new Error('missing command-line arguments');
}
const commands: { [cmd: string]: () => void | Promise<void> } = {
'build:installer': buildOclifInstaller,
@ -66,7 +61,7 @@ export async function run(args?: string[]) {
};
for (const arg of args) {
if (!commands.hasOwnProperty(arg)) {
return exitWithError(`command unknown: ${arg}`);
throw new Error(`command unknown: ${arg}`);
}
}
@ -90,9 +85,22 @@ export async function run(args?: string[]) {
const cmdFunc = commands[arg];
await cmdFunc();
} catch (err) {
return exitWithError(`"${arg}": ${err}`);
if (typeof err === 'object') {
err.message = `"${arg}": ${err.message}`;
}
throw err;
}
}
}
/** See jsdoc for parse() function above */
export async function run(args?: string[]) {
try {
await parse(args);
} catch (e) {
console.error(e.message ? `Error: ${e.message}` : e);
process.exitCode = 1;
}
}
run();

View File

@ -11,8 +11,7 @@ const validateChangeType = (maybeChangeType: string = 'minor') => {
case 'major':
return maybeChangeType;
default:
console.error(`Invalid change type: '${maybeChangeType}'`);
return process.exit(1);
throw new Error(`Invalid change type: '${maybeChangeType}'`);
}
};
@ -65,24 +64,17 @@ const getUpstreams = async () => {
return upstream;
};
const printUsage = (upstreams: Upstream[], upstreamName: string) => {
console.error(
`
const getUsage = (upstreams: Upstream[], upstreamName: string) => `
Usage: npm run update ${upstreamName} $version [$changeType=minor]
Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
`,
);
return process.exit(1);
};
`;
// TODO: Drop the wrapper function once we move to TS 3.8,
// which will support top level await.
async function main() {
async function $main() {
const upstreams = await getUpstreams();
if (process.argv.length < 3) {
return printUsage(upstreams, '$upstreamName');
throw new Error(getUsage(upstreams, '$upstreamName'));
}
const upstreamName = process.argv[2];
@ -90,16 +82,15 @@ async function main() {
const upstream = upstreams.find((v) => v.repo === upstreamName);
if (!upstream) {
console.error(
throw new Error(
`Invalid upstream name '${upstreamName}', valid options: ${upstreams
.map(({ repo }) => repo)
.join(', ')}`,
);
return process.exit(1);
}
if (process.argv.length < 4) {
printUsage(upstreams, upstreamName);
throw new Error(getUsage(upstreams, upstreamName));
}
const packageName = upstream.module || upstream.repo;
@ -108,8 +99,7 @@ async function main() {
await run(`npm install ${packageName}@${process.argv[3]}`);
const newVersion = await getVersion(packageName);
if (newVersion === oldVersion) {
console.error(`Already on version '${newVersion}'`);
return process.exit(1);
throw new Error(`Already on version '${newVersion}'`);
}
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
@ -137,4 +127,13 @@ async function main() {
);
}
async function main() {
try {
await $main();
} catch (e) {
console.error(e);
process.exitCode = 1;
}
}
main();

View File

@ -21,22 +21,6 @@ import * as path from 'path';
export const ROOT = path.join(__dirname, '..');
const nodeEngineWarn = `\
------------------------------------------------------------------------------
Warning: Node version "v14.x.x" does not match required versions ">=10.20.0 <13.0.0".
This may cause unexpected behavior. To upgrade Node, visit:
https://nodejs.org/en/download/
------------------------------------------------------------------------------
`;
const nodeEngineWarnArray = nodeEngineWarn.split('\n').filter((l) => l);
export function matchesNodeEngineVersionWarn(line: string) {
line = line.replace(/"v14\.\d{1,3}\.\d{1,3}"/, '"v14.x.x"');
return (
line === nodeEngineWarn || nodeEngineWarnArray.includes(line.trimEnd())
);
}
/** Tap and buffer this process' stdout and stderr */
export class StdOutTap {
public stdoutBuf: string[] = [];
@ -104,60 +88,6 @@ export function loadPackageJson() {
return require(path.join(ROOT, 'package.json'));
}
/**
* Run the executable at execPath as a child process, and resolve a promise
* to the executable's stdout output as a string. Reject the promise if
* anything is printed to stderr, or if the child process exits with a
* non-zero exit code.
* @param execPath Executable path
* @param args Command-line argument for the executable
*/
export async function getSubprocessStdout(
execPath: string,
args: string[],
): Promise<string> {
const child = spawn(execPath, args);
return new Promise((resolve, reject) => {
let stdout = '';
child.stdout.on('error', reject);
child.stderr.on('error', reject);
child.stdout.on('data', (data: Buffer) => {
try {
stdout = data.toString();
} catch (err) {
reject(err);
}
});
child.stderr.on('data', (data: Buffer) => {
try {
const stderr = data.toString();
// ignore any debug lines, but ensure that we parse
// every line provided to the stderr stream
const lines = _.filter(
stderr.trim().split(/\r?\n/),
(line) =>
!line.startsWith('[debug]') && !matchesNodeEngineVersionWarn(line),
);
if (lines.length > 0) {
reject(
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
);
}
} catch (err) {
reject(err);
}
});
child.on('exit', (code: number) => {
if (code) {
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
} else {
resolve(stdout);
}
});
});
}
/**
* Error handling wrapper around the npm `which` package:
* "Like the unix which utility. Finds the first instance of a specified

View File

@ -8,7 +8,7 @@ _balena() {
local context state line curcontext="$curcontext"
# Valid top-level completions
main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os tag util )
main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util )
# Sub-completions
api_key_cmds=( generate )
app_cmds=( create purge rename restart rm )
@ -21,6 +21,7 @@ _balena() {
key_cmds=( add rm )
local_cmds=( configure flash )
os_cmds=( build-config configure download initialize versions )
release_cmds=( finalize )
tag_cmds=( rm set )
@ -73,6 +74,9 @@ _balena_sec_cmds() {
"os")
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
;;
"release")
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
;;
"tag")
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
;;

View File

@ -7,7 +7,7 @@ _balena_complete()
local cur prev
# Valid top-level completions
main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os tag util"
main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util"
# Sub-completions
api_key_cmds="generate"
app_cmds="create purge rename restart rm"
@ -20,6 +20,7 @@ _balena_complete()
key_cmds="add rm"
local_cmds="configure flash"
os_cmds="build-config configure download initialize versions"
release_cmds="finalize"
tag_cmds="rm set"
@ -67,6 +68,9 @@ _balena_complete()
os)
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
;;
release)
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
;;
tag)
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
;;

View File

@ -144,14 +144,18 @@ of major, minor and patch version releases.
The latest release of a major version of the balena CLI will remain compatible with
the balenaCloud backend services for at least one year from the date when the
following major version is released. For example, balena CLI v10.17.5, as the
latest v10 release, would remain compatible with the balenaCloud backend for one
year from the date when v11.0.0 is released.
following major version is released. For example, balena CLI v11.36.0, as the
latest v11 release, would remain compatible with the balenaCloud backend for one
year from the date when v12.0.0 was released.
At the end of this period, the older major version is considered deprecated and
some of the functionality that depends on balenaCloud services may stop working
at any time.
Users are encouraged to regularly update the balena CLI to the latest version.
Half way through to that period (6 months after the release of the next major
version), older major versions of the balena CLI will start printing a deprecation
warning message when it is used interactively (when `stderr` is attached to a TTY
device file). At the end of that period, older major versions will exit with an
error message unless the `--unsupported` flag is used. This behavior was
introduced in CLI version 12.47.0 and is also documented by `balena help`.
To take advantage of the latest backend features and ensure compatibility, users
are encouraged to regularly update the balena CLI to the latest version.
# CLI Command Reference
@ -203,6 +207,12 @@ Users are encouraged to regularly update the balena CLI to the latest version.
- [device rm &#60;uuid(s)&#62;](#device-rm-uuid-s)
- [device shutdown &#60;uuid&#62;](#device-shutdown-uuid)
- Releases
- [releases &#60;fleet&#62;](#releases-fleet)
- [release &#60;commitorid&#62;](#release-commitorid)
- [release finalize &#60;commitorid&#62;](#release-finalize-commitorid)
- Environment Variables
- [envs](#envs)
@ -1290,6 +1300,80 @@ the uuid of the device to shutdown
force action if the update lock is set
# Releases
## releases &#60;fleet&#62;
List all releases of the given fleet.
Fleets may be specified by fleet name or slug. Slugs are recommended because
they are unique and unambiguous. Slugs can be listed with the `balena fleets`
command. Note that slugs may change if the fleet is renamed. Fleet names are
not unique and may result in "Fleet is ambiguous" errors at any time (even if
"it used to work in the past"), for example if the name clashes with a newly
created public/open fleet, or with fleets from other balena accounts that you
may be invited to join under any role. For this reason, fleet names are
especially discouraged in scripts (e.g. CI environments).
Examples:
$ balena releases myorg/myfleet
### Arguments
#### FLEET
fleet name or slug
### Options
## release &#60;commitOrId&#62;
Examples:
$ balena release a777f7345fe3d655c1c981aa642e5555
$ balena release 1234567
### Arguments
#### COMMITORID
the commit or ID of the release to get information
### Options
#### -c, --composition
Return the release composition
## release finalize &#60;commitOrId&#62;
Finalize a release. Releases can be "draft" or "final", and this command
changes a draft release into a final release. Draft releases can be created
with the `--draft` option of the `balena build` or `balena deploy`
commands.
Draft releases are not automatically deployed to devices tracking the latest
release. For a draft release to be deployed to a device, the device should be
explicity pinned to that release. Conversely, final releases may trigger immediate
deployment to unpinned devices (subject to a device's polling period) and, for
this reason, final releases cannot be changed back to draft status.
Examples:
$ balena release finalize a777f7345fe3d655c1c981aa642e5555
$ balena release finalize 1234567
### Arguments
#### COMMITORID
the commit or ID of the release to finalize
### Options
# Environment Variables
## envs
@ -2493,8 +2577,11 @@ the wifi key to use (used only if --network is set to wifi)
## config inject &#60;file&#62;
Inject a config.json file to the mounted filesystem,
e.g. the SD card of a provisioned device or balenaOS image.
Inject a config.json file to a mounted filesystem, e.g. the SD card of a
provisioned device or balenaOS image.
Note: if using a private/custom device type, please ensure you are logged in
('balena login' command). Public device types do not require logging in.
Examples:
@ -2922,22 +3009,29 @@ Don't convert line endings from CRLF (Windows format) to LF (Unix format).
Have each service use its own .dockerignore file. See "balena help push".
#### -G, --nogitignore
No-op (default behavior) since balena CLI v12.0.0. See "balena help push".
#### -g, --gitignore
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted.
#### -G, --nogitignore
No-op (default behavior) since balena CLI v12.0.0. See "balena help push".
#### --release-tag RELEASE-TAG
Set release tags if the image build is successful (balenaCloud only). Multiple
arguments may be provided, alternating tag keys and values (see examples).
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
#### --draft
Instruct the builder to create the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.
# Settings
## settings
@ -3148,14 +3242,14 @@ Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
until your project can be adapted.
#### -m, --multi-dockerignore
Have each service use its own .dockerignore file. See "balena help build".
#### -G, --nogitignore
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
#### -m, --multi-dockerignore
Have each service use its own .dockerignore file. See "balena help build".
#### --noparent-check
Disable project validation check of 'docker-compose.yml' file in parent folder
@ -3174,11 +3268,13 @@ Don't convert line endings from CRLF (Windows format) to LF (Unix format).
#### -n, --projectName PROJECTNAME
Specify an alternate project name; default is the directory name
Name prefix for locally built images. This is the 'projectName' portion
in 'projectName_serviceName:tag'. The default is the directory name.
#### -t, --tag TAG
The alias to the generated image
Tag locally built Docker images. This is the 'tag' portion
in 'projectName_serviceName:tag'. The default is 'latest'.
#### -B, --buildArg BUILDARG
@ -3358,6 +3454,13 @@ Set release tags if the image deployment is successful. Multiple
arguments may be provided, alternating tag keys and values (see examples).
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
#### --draft
Deploy the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.
#### -e, --emulated
Use QEMU for ARM architecture emulation during the image build
@ -3380,14 +3483,14 @@ Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
until your project can be adapted.
#### -m, --multi-dockerignore
Have each service use its own .dockerignore file. See "balena help build".
#### -G, --nogitignore
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
#### -m, --multi-dockerignore
Have each service use its own .dockerignore file. See "balena help build".
#### --noparent-check
Disable project validation check of 'docker-compose.yml' file in parent folder
@ -3406,11 +3509,13 @@ Don't convert line endings from CRLF (Windows format) to LF (Unix format).
#### -n, --projectName PROJECTNAME
Specify an alternate project name; default is the directory name
Name prefix for locally built images. This is the 'projectName' portion
in 'projectName_serviceName:tag'. The default is the directory name.
#### -t, --tag TAG
The alias to the generated image
Tag locally built Docker images. This is the 'tag' portion
in 'projectName_serviceName:tag'. The default is 'latest'.
#### -B, --buildArg BUILDARG

View File

@ -16,8 +16,14 @@
*/
import * as packageJSON from '../package.json';
import {
AppOptions,
checkDeletedCommand,
preparseArgs,
unsupportedFlag,
} from './preparser';
import { CliSettings } from './utils/bootstrap';
import { onceAsync, stripIndent } from './utils/lazy';
import { onceAsync } from './utils/lazy';
/**
* Sentry.io setup
@ -27,6 +33,7 @@ export const setupSentry = onceAsync(async () => {
const config = await import('./config');
const Sentry = await import('@sentry/node');
Sentry.init({
autoSessionTracking: false,
dsn: config.sentryDsn,
release: packageJSON.version,
});
@ -43,13 +50,8 @@ export const setupSentry = onceAsync(async () => {
async function checkNodeVersion() {
const validNodeVersions = packageJSON.engines.node;
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
console.warn(stripIndent`
------------------------------------------------------------------------------
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
This may cause unexpected behavior. To upgrade Node, visit:
https://nodejs.org/en/download/
------------------------------------------------------------------------------
`);
const { getNodeEngineVersionWarn } = await import('./utils/messages');
console.warn(getNodeEngineVersionWarn(process.version, validNodeVersions));
}
}
@ -93,10 +95,20 @@ async function init() {
}
/** Execute the oclif parser and the CLI command. */
async function oclifRun(
command: string[],
options: import('./preparser').AppOptions,
) {
async function oclifRun(command: string[], options: AppOptions) {
let deprecationPromise: Promise<void>;
// check and enforce the CLI's deprecation policy
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
deprecationPromise = Promise.resolve();
} else {
const { DeprecationChecker } = await import('./deprecation');
const deprecationChecker = new DeprecationChecker(packageJSON.version);
// warnAndAbortIfDeprecated uses previously cached data only
await deprecationChecker.warnAndAbortIfDeprecated();
// checkForNewReleasesIfNeeded may query the npm registry
deprecationPromise = deprecationChecker.checkForNewReleasesIfNeeded();
}
const runPromise = (async function (shouldFlush: boolean) {
const { CustomMain } = await import('./utils/oclif-utils');
let isEEXIT = false;
@ -130,14 +142,12 @@ async function oclifRun(
})(!options.noFlush);
const { trackPromise } = await import('./hooks/prerun/track');
await Promise.all([trackPromise, runPromise]);
await Promise.all([trackPromise, deprecationPromise, runPromise]);
}
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
export async function run(
cliArgs = process.argv,
options: import('./preparser').AppOptions = {},
) {
export async function run(cliArgs = process.argv, options: AppOptions = {}) {
try {
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
normalizeEnvVars();
@ -150,8 +160,6 @@ export async function run(
await init();
const { preparseArgs, checkDeletedCommand } = await import('./preparser');
// Look for commands that have been removed and if so, exit with a notice
checkDeletedCommand(cliArgs.slice(2));

View File

@ -120,7 +120,7 @@ export class FleetRenameCmd extends Command {
} catch (e) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
if ((e.message || '').toLowerCase().includes('unique')) {
throw new ExpectedError(`Error: fleet ${params.fleet} already exists.`);
throw new ExpectedError(`Error: fleet ${newName} already exists.`);
}
throw e;
}

View File

@ -239,7 +239,12 @@ ${dockerignoreHelp}
) {
const { loadProject } = await import('../utils/compose_ts');
const project = await loadProject(logger, composeOpts);
const project = await loadProject(
logger,
composeOpts,
undefined,
opts.buildOpts.t,
);
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
if (
@ -266,7 +271,7 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore,
nogitignore: composeOpts.nogitignore, // v13: delete this line
multiDockerignore: composeOpts.multiDockerignore,
});
}

View File

@ -34,8 +34,11 @@ export default class ConfigInjectCmd extends Command {
public static description = stripIndent`
Inject a configuration file into a device or OS image.
Inject a config.json file to the mounted filesystem,
e.g. the SD card of a provisioned device or balenaOS image.
Inject a config.json file to a mounted filesystem, e.g. the SD card of a
provisioned device or balenaOS image.
Note: if using a private/custom device type, please ensure you are logged in
('balena login' command). Public device types do not require logging in.
`;
public static examples = [
@ -59,8 +62,6 @@ export default class ConfigInjectCmd extends Command {
help: cf.help,
};
public static authenticated = true;
public static root = true;
public async run() {

View File

@ -34,7 +34,7 @@ import type {
ComposeOpts,
Release as ComposeReleaseInfo,
} from '../utils/compose-types';
import type { DockerCliFlags } from '../utils/docker';
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
import {
applyReleaseTagKeysAndValues,
buildProject,
@ -59,6 +59,7 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
build: boolean;
nologupload: boolean;
'release-tag'?: string[];
draft: boolean;
help: void;
}
@ -136,6 +137,14 @@ ${dockerignoreHelp}
`,
multiple: true,
}),
draft: flags.boolean({
description: stripIndent`
Deploy the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.`,
default: false,
}),
...composeCliFlags,
...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
@ -213,6 +222,7 @@ ${dockerignoreHelp}
shouldPerformBuild: !!options.build,
shouldUploadLogs: !options.nologupload,
buildEmulated: !!options.emulated,
createAsDraft: options.draft,
buildOpts,
});
await applyReleaseTagKeysAndValues(
@ -235,7 +245,8 @@ ${dockerignoreHelp}
shouldPerformBuild: boolean;
shouldUploadLogs: boolean;
buildEmulated: boolean;
buildOpts: any; // arguments to forward to docker build command
buildOpts: BuildOpts;
createAsDraft: boolean;
},
) {
const _ = await import('lodash');
@ -248,7 +259,12 @@ ${dockerignoreHelp}
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
try {
const project = await loadProject(logger, composeOpts, opts.image);
const project = await loadProject(
logger,
composeOpts,
opts.image,
opts.buildOpts.t,
);
if (project.descriptors.length > 1 && !appType?.supports_multicontainer) {
throw new ExpectedError(
'Target fleet does not support multiple containers. Aborting!',
@ -303,7 +319,7 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore,
nogitignore: composeOpts.nogitignore, // v13: delete this line
multiDockerignore: composeOpts.multiDockerignore,
});
builtImagesByService = _.keyBy(builtImages, 'serviceName');
@ -367,6 +383,8 @@ ${dockerignoreHelp}
`Bearer ${auth}`,
apiEndpoint,
!opts.shouldUploadLogs,
composeOpts.projectPath,
opts.createAsDraft,
);
}

View File

@ -17,7 +17,12 @@
import type { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import type { Application, BalenaSDK } from 'balena-sdk';
import type {
BalenaSDK,
Device,
DeviceType,
PineTypedResult,
} from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { ExpectedError } from '../../errors';
@ -29,9 +34,12 @@ import {
} from '../../utils/messages';
import { isV13 } from '../../utils/version';
interface ExtendedDevice extends DeviceWithDeviceType {
type ExtendedDevice = PineTypedResult<
Device,
typeof import('../../utils/helpers').expandForAppNameAndCpuArch
> & {
application_name?: string;
}
};
interface FlagsDef {
application?: string;
@ -94,7 +102,7 @@ export default class DeviceMoveCmd extends Command {
const balena = getBalenaSdk();
const { tryAsInteger } = await import('../../utils/validation');
const { expandForAppName } = await import('../../utils/helpers');
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
// Parse ids string into array of correct types
const deviceIds: Array<string | number> = params.uuid
@ -107,15 +115,14 @@ export default class DeviceMoveCmd extends Command {
(uuid) =>
balena.models.device.get(
uuid,
expandForAppName,
expandForAppNameAndCpuArch,
) as Promise<ExtendedDevice>,
),
);
// Map application name for each device
for (const device of devices) {
const belongsToApplication =
device.belongs_to__application as Application[];
const belongsToApplication = device.belongs_to__application;
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';
@ -145,42 +152,56 @@ export default class DeviceMoveCmd extends Command {
balena: BalenaSDK,
devices: ExtendedDevice[],
) {
const [deviceDeviceTypes, deviceTypes] = await Promise.all([
Promise.all(
devices.map((device) =>
balena.models.device.getManifestBySlug(
device.is_of__device_type[0].slug,
),
const { getExpandedProp } = await import('../../utils/pine');
// deduplicate the slugs
const deviceCpuArchs = Array.from(
new Set(
devices.map(
(d) => d.is_of__device_type[0].is_of__cpu_architecture[0].slug,
),
),
balena.models.config.getDeviceTypes(),
]);
);
const compatibleDeviceTypes = deviceTypes.filter((dt) =>
deviceDeviceTypes.every(
(deviceDeviceType) =>
balena.models.os.isArchitectureCompatibleWith(
deviceDeviceType.arch,
dt.arch,
) &&
!!dt.isDependent === !!deviceDeviceType.isDependent &&
dt.state !== 'DISCONTINUED',
),
const deviceTypeOptions = {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
} as const;
const deviceTypes = (await balena.models.deviceType.getAllSupported(
deviceTypeOptions,
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
const compatibleDeviceTypeSlugs = new Set(
deviceTypes
.filter((deviceType) => {
const deviceTypeArch = getExpandedProp(
deviceType.is_of__cpu_architecture,
'slug',
)!;
return deviceCpuArchs.every((deviceCpuArch) =>
balena.models.os.isArchitectureCompatibleWith(
deviceCpuArch,
deviceTypeArch,
),
);
})
.map((deviceType) => deviceType.slug),
);
const patterns = await import('../../utils/patterns');
try {
const application = await patterns.selectApplication(
(app) =>
compatibleDeviceTypes.some(
(dt) => dt.slug === app.is_for__device_type[0].slug,
) &&
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
devices.some((device) => device.application_name !== app.app_name),
true,
);
return application;
} catch (err) {
if (deviceDeviceTypes.length) {
if (!compatibleDeviceTypeSlugs.size) {
throw new ExpectedError(
`${err.message}\nDo all devices have a compatible architecture?`,
);

View File

@ -227,7 +227,7 @@ async function setServiceVars(
sdk,
uuid,
['id'],
['app_name'],
['slug'],
);
} catch (err) {
console.error(`${err.message}, device: ${uuid}`);
@ -236,11 +236,7 @@ async function setServiceVars(
}
for (const service of options.service!.split(',')) {
try {
const serviceId = await getServiceIdForApp(
sdk,
app.app_name,
service,
);
const serviceId = await getServiceIdForApp(sdk, app.slug, service);
await sdk.models.device.serviceVar.set(
device.id,
serviceId,

View File

@ -91,8 +91,6 @@ export default class EnvRmCmd extends Command {
await confirm(
opt.yes || false,
'Are you sure you want to delete the environment variable?',
undefined,
true,
);
const balena = getBalenaSdk();

View File

@ -174,11 +174,11 @@ export default class EnvsCmd extends Command {
balena,
options.device,
['uuid'],
['app_name'],
['slug'],
);
fullUUID = device.uuid;
if (app) {
appNameOrSlug = app.app_name;
appNameOrSlug = app.slug;
}
}
if (appNameOrSlug && options.service) {
@ -210,7 +210,14 @@ export default class EnvsCmd extends Command {
// Replace undefined app names with 'N/A' or null
varArray = varArray.map((i: EnvironmentVariableInfo) => {
i.appName = i.appName || (options.json ? null : 'N/A');
if (i.appName) {
// use slug in v13, app name in v12 for compatibility
i.appName = isV13()
? i.appName
: i.appName.substring(i.appName.indexOf('/') + 1);
} else {
i.appName = options.json ? null : 'N/A';
}
return i;
});

View File

@ -20,12 +20,7 @@ import type { BlockDevice } from 'etcher-sdk/build/source-destination';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import {
getChalk,
getCliForm,
getVisuals,
stripIndent,
} from '../../utils/lazy';
import { getChalk, getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
yes: boolean;
@ -93,24 +88,15 @@ export default class LocalFlashCmd extends Command {
}
}
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const drive = await this.getDrive(options);
const yes =
options.yes ||
(await getCliForm().ask({
message: 'This will erase the selected drive. Are you sure?',
type: 'confirm',
name: 'yes',
default: false,
}));
if (!yes) {
console.log(getChalk().red.bold('Aborted image flash'));
process.exit(0);
}
const { confirm } = await import('../../utils/patterns');
await confirm(
options.yes,
'This will erase the selected drive. Are you sure?',
);
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const file = new sourceDestination.File({
path: params.image,
});

View File

@ -92,7 +92,6 @@ export default class OsInitializeCmd extends Command {
options.yes,
`This will erase ${answers.drive}. Are you sure?`,
`Going to erase ${answers.drive}.`,
true,
);
const { safeUmount } = await import('../../utils/umount');
await safeUmount(answers.drive);

View File

@ -36,13 +36,7 @@ import { isV13 } from '../utils/version';
import { flags } from '@oclif/command';
import * as _ from 'lodash';
import type {
Application,
BalenaSDK,
DeviceTypeJson,
PineExpand,
Release,
} from 'balena-sdk';
import type { Application, BalenaSDK, PineExpand, Release } from 'balena-sdk';
import type { Preloader } from 'balena-preload';
interface FlagsDef extends DockerConnectionCliFlags {
@ -331,7 +325,6 @@ Can be repeated to add multiple certificates.\
readonly applicationExpandOptions: PineExpand<Application> = {
owns__release: {
$select: ['id', 'commit', 'end_timestamp', 'composition'],
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
$expand: {
contains__image: {
$select: ['image'],
@ -345,77 +338,75 @@ Can be repeated to add multiple certificates.\
$filter: {
status: 'success',
},
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
},
should_be_running__release: {
$select: 'commit',
},
};
allDeviceTypes: DeviceTypeJson.DeviceType[];
async getDeviceTypes() {
if (this.allDeviceTypes === undefined) {
const balena = getBalenaSdk();
const deviceTypes = await balena.models.config.getDeviceTypes();
this.allDeviceTypes = _.sortBy(deviceTypes, 'name');
}
return this.allDeviceTypes;
}
isCurrentCommit(commit: string) {
return commit === 'latest' || commit === 'current';
}
async getDeviceTypesWithSameArch(deviceTypeSlug: string) {
const deviceTypes = await this.getDeviceTypes();
const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug });
if (!deviceType) {
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
}
return _(deviceTypes).filter({ arch: deviceType.arch }).map('slug').value();
}
async getApplicationsWithSuccessfulBuilds(deviceTypeSlug: string) {
const balena = getBalenaSdk();
const deviceTypes = await this.getDeviceTypesWithSameArch(deviceTypeSlug);
// TODO: remove the explicit types once https://github.com/balena-io/balena-sdk/pull/889 gets merged
return balena.pine.get<
Application,
Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
}
>
>({
resource: 'my_application',
options: {
$filter: {
is_for__device_type: {
$any: {
$alias: 'dt',
$expr: {
dt: {
slug: { $in: deviceTypes },
},
},
},
},
owns__release: {
$any: {
$alias: 'r',
$expr: {
r: {
status: 'success',
try {
await balena.models.deviceType.get(deviceTypeSlug);
} catch {
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
}
return (await balena.models.application.getAll({
$select: ['id', 'app_name', 'should_track_latest_release'],
$expand: this.applicationExpandOptions,
$filter: {
// get the apps that are of the same arch as the device type of the image
is_for__device_type: {
$any: {
$alias: 'dt',
$expr: {
dt: {
is_of__cpu_architecture: {
$any: {
$alias: 'ioca',
$expr: {
ioca: {
is_supported_by__device_type: {
$any: {
$alias: 'isbdt',
$expr: {
isbdt: {
slug: deviceTypeSlug,
},
},
},
},
},
},
},
},
},
},
},
},
$expand: this.applicationExpandOptions,
$select: ['id', 'app_name', 'should_track_latest_release'],
$orderby: 'app_name asc',
owns__release: {
$any: {
$alias: 'r',
$expr: {
r: {
status: 'success',
},
},
},
},
},
});
$orderby: 'app_name asc',
})) as Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
}
>;
}
async selectApplication(deviceTypeSlug: string) {

View File

@ -47,8 +47,8 @@ interface FlagsDef {
pull: boolean;
'noparent-check': boolean;
'registry-secrets'?: string;
gitignore?: boolean;
nogitignore?: boolean;
gitignore?: boolean; // v13: delete this flag
nogitignore?: boolean; // v13: delete this flag
nolive: boolean;
detached: boolean;
service?: string[];
@ -58,6 +58,7 @@ interface FlagsDef {
'noconvert-eol': boolean;
'multi-dockerignore': boolean;
'release-tag'?: string[];
draft: boolean;
help: void;
}
@ -236,11 +237,20 @@ export default class PushCmd extends Command {
'Have each service use its own .dockerignore file. See "balena help push".',
char: 'm',
default: false,
exclusive: ['gitignore'],
exclusive: ['gitignore'], // v13: delete this line
}),
...(isV13()
? {}
: {
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted.`,
char: 'g',
default: false,
exclusive: ['multi-dockerignore'],
}),
nogitignore: flags.boolean({
description:
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
@ -249,15 +259,6 @@ export default class PushCmd extends Command {
default: false,
}),
}),
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted.`,
char: 'g',
default: false,
exclusive: ['multi-dockerignore'],
}),
'release-tag': flags.string({
description: stripIndent`
Set release tags if the image build is successful (balenaCloud only). Multiple
@ -267,6 +268,14 @@ export default class PushCmd extends Command {
multiple: true,
exclusive: ['detached'],
}),
draft: flags.boolean({
description: stripIndent`
Instruct the builder to create the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.`,
default: false,
}),
help: cf.help,
};
@ -362,13 +371,14 @@ export default class PushCmd extends Command {
registrySecrets,
headless: options.detached,
convertEol: !options['noconvert-eol'],
isDraft: options.draft,
};
const args = {
appSlug: application.slug,
source: options.source,
auth: token,
baseUrl,
nogitignore: !options.gitignore,
nogitignore: !options.gitignore, // v13: delete this line
sdk,
opts,
};
@ -394,7 +404,7 @@ export default class PushCmd extends Command {
registrySecrets: RegistrySecrets,
) {
// Check for invalid options
const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag'];
const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag', 'draft'];
this.checkInvalidOptions(
remoteOnlyOptions,
options,
@ -412,7 +422,7 @@ export default class PushCmd extends Command {
multiDockerignore: options['multi-dockerignore'],
nocache: options.nocache,
pull: options.pull,
nogitignore: !options.gitignore,
nogitignore: !options.gitignore, // v13: delete this line
noParentCheck: options['noparent-check'],
nolive: options.nolive,
detached: options.detached,

View File

@ -0,0 +1,86 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
}
interface ArgsDef {
commitOrId: string | number;
}
export default class ReleaseFinalizeCmd extends Command {
public static description = stripIndent`
Finalize a release.
Finalize a release. Releases can be "draft" or "final", and this command
changes a draft release into a final release. Draft releases can be created
with the \`--draft\` option of the \`balena build\` or \`balena deploy\`
commands.
Draft releases are not automatically deployed to devices tracking the latest
release. For a draft release to be deployed to a device, the device should be
explicity pinned to that release. Conversely, final releases may trigger immediate
deployment to unpinned devices (subject to a device's polling period) and, for
this reason, final releases cannot be changed back to draft status.
`;
public static examples = [
'$ balena release finalize a777f7345fe3d655c1c981aa642e5555',
'$ balena release finalize 1234567',
];
public static usage = 'release finalize <commitOrId>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static args = [
{
name: 'commitOrId',
description: 'the commit or ID of the release to finalize',
required: true,
parse: (commitOrId: string) => tryAsInteger(commitOrId),
},
];
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleaseFinalizeCmd);
const balena = getBalenaSdk();
const release = await balena.models.release.get(params.commitOrId, {
$select: ['id', 'is_final'],
});
if (release.is_final) {
console.log(`Release ${params.commitOrId} is already finalized!`);
return;
}
await balena.models.release.finalize(release.id);
console.log(`Release ${params.commitOrId} finalized`);
}
}

View File

@ -0,0 +1,128 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import type * as BalenaSdk from 'balena-sdk';
import jsyaml = require('js-yaml');
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
composition?: boolean;
}
interface ArgsDef {
commitOrId: string | number;
}
export default class ReleaseCmd extends Command {
public static description = stripIndent`
Get info for a release.
`;
public static examples = [
'$ balena release a777f7345fe3d655c1c981aa642e5555',
'$ balena release 1234567',
];
public static usage = 'release <commitOrId>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
composition: flags.boolean({
default: false,
char: 'c',
description: 'Return the release composition',
}),
};
public static args = [
{
name: 'commitOrId',
description: 'the commit or ID of the release to get information',
required: true,
parse: (commitOrId: string) => tryAsInteger(commitOrId),
},
];
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
ReleaseCmd,
);
const balena = getBalenaSdk();
if (options.composition) {
await this.showComposition(params.commitOrId, balena);
} else {
await this.showReleaseInfo(params.commitOrId, balena);
}
}
async showComposition(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
const release = await balena.models.release.get(commitOrId, {
$select: 'composition',
});
console.log(jsyaml.dump(release.composition));
}
async showReleaseInfo(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
const fields: Array<keyof BalenaSdk.Release> = [
'id',
'commit',
'created_at',
'status',
'semver',
'is_final',
'build_log',
'start_timestamp',
'end_timestamp',
];
const release = await balena.models.release.get(commitOrId, {
$select: fields,
$expand: {
release_tag: {
$select: ['tag_key', 'value'],
},
},
});
const tagStr = release
.release_tag!.map((t) => `${t.tag_key}=${t.value}`)
.join('\n');
const _ = await import('lodash');
const values = _.mapValues(
release,
(val) => val ?? 'N/a',
) as Dictionary<string>;
values['tags'] = tagStr;
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
}
}

86
lib/commands/releases.ts Normal file
View File

@ -0,0 +1,86 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationNameNote } from '../utils/messages';
import type * as BalenaSdk from 'balena-sdk';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class ReleasesCmd extends Command {
public static description = stripIndent`
List all releases of a fleet.
List all releases of the given fleet.
${applicationNameNote.split('\n').join('\n\t\t')}
`;
public static examples = ['$ balena releases myorg/myfleet'];
public static usage = 'releases <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static args = [
{
name: 'fleet',
description: 'fleet name or slug',
required: true,
},
];
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleasesCmd);
const fields: Array<keyof BalenaSdk.Release> = [
'id',
'commit',
'created_at',
'status',
'semver',
'is_final',
];
const balena = getBalenaSdk();
const releases = await balena.models.release.getAllByApplication(
params.fleet,
{ $select: fields },
);
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
fields,
),
);
}
}

230
lib/deprecation.ts Normal file
View File

@ -0,0 +1,230 @@
/**
* @license
* Copyright 2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface ReleaseTimestampsByVersion {
[version: string]: string; // e.g. { '12.0.0': '2021-06-16T12:54:52.000Z' }
lastFetched: string; // ISO 8601 timestamp, e.g. '2021-06-27T16:46:10.000Z'
}
/**
* Warn about and enforce the CLI deprecation policy stated in the README
* file. In particular:
* The latest release of a major version will remain compatible with
* the backend services for at least one year from the date when the
* following major version is released. [...]
* Half way through to that period (6 months), old major versions of the
* balena CLI will start printing a deprecation warning message.
* At the end of that period, older major versions will abort with an error
* message unless the `--unsupported` flag is used.
*
* - Check for new balena-cli releases by querying the npm registry.
* - Cache results for a number of days to improve performance.
*
* For this feature's specification and planning, see (restricted access):
* https://jel.ly.fish/ed8d2395-9323-418c-bb67-d11d32a17d00
*/
export class DeprecationChecker {
readonly majorVersionFetchIntervalDays = 7;
readonly expiryDays = 365;
readonly deprecationDays = Math.ceil(this.expiryDays / 2);
readonly msInDay = 24 * 60 * 60 * 1000; // milliseconds in a day
readonly debugPrefix = 'Deprecation check';
readonly cacheFile = 'cachedReleaseTimestamps';
readonly now = new Date().getTime();
private initialized = false;
storage: ReturnType<typeof import('balena-settings-storage')>;
cachedTimestamps: ReleaseTimestampsByVersion;
nextMajorVersion: string; // semver without the 'v' prefix
constructor(protected currentVersion: string) {
const semver = require('semver') as typeof import('semver');
const major = semver.major(this.currentVersion, { loose: true });
this.nextMajorVersion = `${major + 1}.0.0`;
}
public async init() {
if (this.initialized) {
return;
}
this.initialized = true;
const settings = await import('balena-settings-client');
const getStorage = await import('balena-settings-storage');
const dataDirectory = settings.get<string>('dataDirectory');
this.storage = getStorage({ dataDirectory });
let stored: ReleaseTimestampsByVersion | undefined;
try {
stored = (await this.storage.get(
this.cacheFile,
)) as ReleaseTimestampsByVersion;
} catch {
// ignore
}
this.cachedTimestamps = {
...stored,
// '1970-01-01T00:00:00.000Z' is new Date(0).toISOString()
lastFetched: stored?.lastFetched || '1970-01-01T00:00:00.000Z',
};
}
/**
* Get NPM registry URL to retrieve the package.json file for a given version.
* @param version Semver without 'v' prefix, e.g. '12.0.0.'
*/
protected getNpmUrl(version: string) {
return `http://registry.npmjs.org/balena-cli/${version}`;
}
/**
* Query the npm registry (HTTP request) for a given balena-cli version.
*
* @param version semver version without the 'v' prefix, e.g. '13.0.0'
* @returns `undefined` if the request status code is 404 (version not
* published), otherwise a publishedAt date in ISO 8601 format, e.g.
* '2021-06-27T16:46:10.000Z'.
*/
protected async fetchPublishedTimestampForVersion(
version: string,
): Promise<string | undefined> {
const { default: got } = await import('got');
const url = this.getNpmUrl(version);
let response: import('got').Response<Dictionary<any>> | undefined;
try {
response = await got(url, { responseType: 'json', retry: 0 });
} catch (e) {
// 404 is expected if `version` hasn't been published yet
if (e.response?.statusCode !== 404) {
throw new Error(`Failed to query "${url}":\n${e}`);
}
}
// response.body looks like a package.json file, plus possibly a
// `versionist.publishedAt` field added by `github.com/product-os/versionist`
const publishedAt: string | undefined =
response?.body?.versionist?.publishedAt;
if (!publishedAt && process.env.DEBUG) {
console.error(`\
[debug] ${this.debugPrefix}: balena CLI next major version "${this.nextMajorVersion}" not released, \
or release date not available`);
}
return publishedAt; // ISO 8601, e.g. '2021-06-27T16:46:10.000Z'
}
/**
* Check if we already know (cached value) when the next major version
* was released. If we don't know, check how long ago the npm registry
* was last fetched, and fetch again if it has been longer than
* `majorVersionFetchIntervalDays`.
*/
public async checkForNewReleasesIfNeeded() {
if (process.env.BALENARC_UNSUPPORTED) {
return; // for the benefit of code testing
}
await this.init();
if (this.cachedTimestamps[this.nextMajorVersion]) {
// A cached value exists: no need to check the npm registry
return;
}
const lastFetched = new Date(this.cachedTimestamps.lastFetched).getTime();
const daysSinceLastFetch = (this.now - lastFetched) / this.msInDay;
if (daysSinceLastFetch < this.majorVersionFetchIntervalDays) {
if (process.env.DEBUG) {
// toFixed(5) results in a precision of ~1 second
const days = daysSinceLastFetch.toFixed(5);
console.error(`\
[debug] ${this.debugPrefix}: ${days} days since last npm registry query for next major version release date.
[debug] Will not query the registry again until at least ${this.majorVersionFetchIntervalDays} days have passed.`);
}
return;
}
if (process.env.DEBUG) {
console.error(`\
[debug] ${
this.debugPrefix
}: Cache miss for the balena CLI next major version release date.
[debug] Will query ${this.getNpmUrl(this.nextMajorVersion)}`);
}
try {
const publishedAt = await this.fetchPublishedTimestampForVersion(
this.nextMajorVersion,
);
if (publishedAt) {
this.cachedTimestamps[this.nextMajorVersion] = publishedAt;
}
} catch (e) {
if (process.env.DEBUG) {
console.error(`[debug] ${this.debugPrefix}: ${e}`);
}
}
// Refresh `lastFetched` regardless of whether or not the request to the npm
// registry was successful. Will try again after `majorVersionFetchIntervalDays`.
this.cachedTimestamps.lastFetched = new Date(this.now).toISOString();
await this.storage.set(this.cacheFile, this.cachedTimestamps);
}
/**
* Use previously cached data (local cache only, fast execution) to check
* whether this version of the CLI is deprecated as per deprecation policy,
* in which case warn about it and conditionally throw an error.
*/
public async warnAndAbortIfDeprecated() {
if (process.env.BALENARC_UNSUPPORTED) {
return; // for the benefit of code testing
}
await this.init();
const nextMajorDateStr = this.cachedTimestamps[this.nextMajorVersion];
if (!nextMajorDateStr) {
return;
}
const nextMajorDate = new Date(nextMajorDateStr).getTime();
const daysElapsed = Math.trunc((this.now - nextMajorDate) / this.msInDay);
if (daysElapsed > this.expiryDays) {
const { ExpectedError } = await import('./errors');
throw new ExpectedError(this.getExpiryMsg(daysElapsed));
} else if (daysElapsed > this.deprecationDays && process.stderr.isTTY) {
console.error(this.getDeprecationMsg(daysElapsed));
}
}
/** Separate function for the benefit of code testing */
getDeprecationMsg(daysElapsed: number) {
const { warnify } =
require('./utils/messages') as typeof import('./utils/messages');
return warnify(`\
CLI version ${this.nextMajorVersion} was released ${daysElapsed} days ago: please upgrade.
This version of the balena CLI (${this.currentVersion}) will exit with an error
message after ${this.expiryDays} days from the release of version ${this.nextMajorVersion},
as per deprecation policy: https://git.io/JRHUW#deprecation-policy
The --unsupported flag may be used to bypass this deprecation check and
allow the CLI to keep working beyond the deprecation period. However,
note that the balenaCloud or openBalena backends may be updated in a way
that is no longer compatible with this version.`);
}
/** Separate function the benefit of code testing */
getExpiryMsg(daysElapsed: number) {
return `
This version of the balena CLI (${this.currentVersion}) has expired: please upgrade.
${daysElapsed} days have passed since the release of CLI version ${this.nextMajorVersion}.
See deprecation policy at: https://git.io/JRHUW#deprecation-policy
The --unsupported flag may be used to bypass this deprecation check and
continue using this version of the CLI. However, note that the balenaCloud
or openBalena backends may be updated in a way that is no longer compatible
with this CLI version.`;
}
}

View File

@ -286,24 +286,3 @@ export const printErrorMessage = function (message: string) {
export const printExpectedErrorMessage = function (message: string) {
console.error(`${message}\n`);
};
/**
* Print a friendly error message and exit the CLI with an error code, BYPASSING
* error reporting through Sentry.io's platform (raven.Raven.captureException).
* Note that lib/errors.ts provides top-level error handling code to catch any
* otherwise uncaught errors, AND to report them through Sentry.io. But many
* "expected" errors (say, a JSON parsing error in a file provided by the user)
* don't warrant reporting through Sentry.io. For such mundane errors, catch
* them and call this function.
*
* DEPRECATED: Use `throw new ExpectedError(<message>)` instead.
* If a specific process exit code x must be set, use process.exitCode = x
*/
export function exitWithExpectedError(message: string | Error): never {
if (message instanceof Error) {
({ message } = message);
}
printErrorMessage(message);
process.exit(1);
}

View File

@ -14,27 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as _ from 'lodash';
import * as Mixpanel from 'mixpanel';
import * as packageJSON from '../package.json';
import { getBalenaSdk } from './utils/lazy';
const getMixpanel = _.once((balenaUrl: string) => {
return Mixpanel.init('balena-main', {
host: `api.${balenaUrl}`,
path: '/mixpanel',
protocol: 'https',
});
});
interface CachedUsername {
token: string;
username: string;
}
/**
* Mixpanel.com analytics tracking (information on balena CLI usage).
* Track balena CLI usage events (product improvement analytics).
*
* @param commandSignature A string like, for example:
* "push <fleetOrDevice>"
@ -60,11 +50,10 @@ export async function trackCommand(commandSignature: string) {
});
}
const settings = await import('balena-settings-client');
const balenaUrl = settings.get('balenaUrl') as string;
const username = await (async () => {
const getStorage = await import('balena-settings-storage');
const dataDirectory = settings.get('dataDirectory') as string;
const dataDirectory = settings.get<string>('dataDirectory');
const storage = getStorage({ dataDirectory });
let token;
try {
@ -94,8 +83,6 @@ export async function trackCommand(commandSignature: string) {
}
})();
const mixpanel = getMixpanel(balenaUrl);
if (!process.env.BALENARC_NO_SENTRY) {
Sentry!.configureScope((scope) => {
scope.setUser({
@ -109,16 +96,43 @@ export async function trackCommand(commandSignature: string) {
!process.env.BALENA_CLI_TEST_TYPE &&
!process.env.BALENARC_NO_ANALYTICS
) {
await mixpanel.track(`[CLI] ${commandSignature}`, {
distinct_id: username,
version: packageJSON.version,
node: process.version,
arch: process.arch,
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
platform: process.platform,
});
const balenaUrl = settings.get<string>('balenaUrl');
await sendEvent(balenaUrl, `[CLI] ${commandSignature}`, username);
}
} catch {
// ignore
}
}
/**
* Make the event tracking HTTPS request to balenaCloud's '/mixpanel' endpoint.
*/
async function sendEvent(balenaUrl: string, event: string, username?: string) {
const { default: got } = await import('got');
const trackData = {
event,
properties: {
arch: process.arch,
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
distinct_id: username,
mp_lib: 'node',
node: process.version,
platform: process.platform,
token: 'balena-main',
version: packageJSON.version,
},
};
const url = `https://api.${balenaUrl}/mixpanel/track`;
const searchParams = {
ip: 0,
verbose: 0,
data: Buffer.from(JSON.stringify(trackData)).toString('base64'),
};
try {
await got(url, { searchParams, retry: 0 });
} catch (e) {
if (process.env.DEBUG) {
console.error(`[debug] Event tracking error: ${e.message || e}`);
}
}
}

View File

@ -46,7 +46,7 @@ export default class BalenaHelp extends Help {
const subject = getHelpSubject(argv);
if (!subject) {
const verbose = argv.includes('-v') || argv.includes('--verbose');
this.showCustomRootHelp(verbose);
console.log(this.getCustomRootHelp(verbose));
return;
}
@ -80,67 +80,106 @@ export default class BalenaHelp extends Help {
throw new ExpectedError(`command ${chalk.cyan.bold(subject)} not found`);
}
showCustomRootHelp(showAllCommands: boolean): void {
const chalk = getChalk();
const bold = chalk.bold;
const cmd = chalk.cyan.bold;
getCustomRootHelp(showAllCommands: boolean): string {
const { bold, cyan } = getChalk();
let commands = this.config.commands;
commands = commands.filter((c) => this.opts.all || !c.hidden);
// Get Primary Commands, sorted as in manual list
const primaryCommands = this.manuallySortedPrimaryCommands.map((pc) => {
return commands.find((c) => c.id === pc.replace(' ', ':'));
});
const primaryCommands = this.manuallySortedPrimaryCommands
.map((pc) => {
return commands.find((c) => c.id === pc.replace(' ', ':'));
})
.filter((c): c is typeof commands[0] => !!c);
// Get the rest as Additional Commands
const additionalCommands = commands.filter(
(c) =>
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
);
// Find longest usage, and pad usage of first command in each category
// This is to ensure that both categories align visually
const usageLength = commands
.map((c) => c.usage?.length || 0)
.reduce((longest, l) => {
return l > longest ? l : longest;
});
if (
typeof primaryCommands[0]?.usage === 'string' &&
typeof additionalCommands[0]?.usage === 'string'
) {
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
additionalCommands[0].usage =
additionalCommands[0].usage.padEnd(usageLength);
let usageLength = 0;
for (const cmd of primaryCommands) {
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
}
// Output help
console.log(bold('USAGE'));
console.log('$ balena [COMMAND] [OPTIONS]');
console.log(bold('\nPRIMARY COMMANDS'));
console.log(this.formatCommands(primaryCommands));
let additionalCmdSection: string[];
if (showAllCommands) {
console.log(bold('\nADDITIONAL COMMANDS'));
console.log(this.formatCommands(additionalCommands));
// Get the rest as Additional Commands
const additionalCommands = commands.filter(
(c) =>
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
);
// Find longest usage, and pad usage of first command in each category
// This is to ensure that both categories align visually
for (const cmd of additionalCommands) {
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
}
if (
typeof primaryCommands[0].usage === 'string' &&
typeof additionalCommands[0].usage === 'string'
) {
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
additionalCommands[0].usage =
additionalCommands[0].usage.padEnd(usageLength);
}
additionalCmdSection = [
bold('\nADDITIONAL COMMANDS'),
this.formatCommands(additionalCommands),
];
} else {
console.log(
`\n${bold('...MORE')} run ${cmd(
'balena help --verbose',
)} to list additional commands.`,
const cmd = cyan.bold('balena help --verbose');
additionalCmdSection = [
`\n${bold('...MORE')} run ${cmd} to list additional commands.`,
];
}
const globalOps = [
['--help, -h', 'display command help'],
['--debug', 'enable debug output'],
[
'--unsupported',
`\
prevent exit with an error as per Deprecation Policy
See: https://git.io/JRHUW#deprecation-policy`,
],
];
globalOps[0][0] = globalOps[0][0].padEnd(usageLength);
const { deprecationPolicyNote, reachingOut } =
require('./utils/messages') as typeof import('./utils/messages');
return [
bold('USAGE'),
'$ balena [COMMAND] [OPTIONS]',
bold('\nPRIMARY COMMANDS'),
this.formatCommands(primaryCommands),
...additionalCmdSection,
bold('\nGLOBAL OPTIONS'),
this.formatGlobalOpts(globalOps),
bold('\nDeprecation Policy Reminder'),
deprecationPolicyNote,
reachingOut,
].join('\n');
}
protected formatGlobalOpts(opts: string[][]) {
const { dim } = getChalk();
const outLines: string[] = [];
let flagWidth = 0;
for (const opt of opts) {
flagWidth = Math.max(flagWidth, opt[0].length);
}
for (const opt of opts) {
const descriptionLines = opt[1].split('\n');
outLines.push(
` ${opt[0].padEnd(flagWidth + 2)}${dim(descriptionLines[0])}`,
);
outLines.push(
...descriptionLines
.slice(1)
.map((line) => ` ${' '.repeat(flagWidth + 2)}${dim(line)}`),
);
}
console.log(bold('\nGLOBAL OPTIONS'));
console.log(' --help, -h');
console.log(' --debug\n');
const { reachingOut } =
require('./utils/messages') as typeof import('./utils/messages');
console.log(reachingOut);
return outLines.join('\n');
}
protected formatCommands(commands: any[]): string {

View File

@ -28,7 +28,7 @@ export const trackPromise = new Promise((resolve) => {
* parsed by oclif, but before the command's run() function is called.
* See: https://oclif.io/docs/hooks
*
* This hook is used to track CLI command signatures with mixpanel.
* This hook is used to track CLI command signatures (usage analytics).
* A command signature is something like "env add NAME [VALUE]". That's
* literally so: 'NAME' and 'VALUE' are NOT replaced with actual values.
*/

View File

@ -14,8 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { stripIndent } from './utils/lazy';
import { exitWithExpectedError } from './errors';
export let unsupportedFlag = false;
export interface AppOptions {
// Prevent the default behavior of flushing stdout after running a command
@ -50,11 +50,10 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
}
// support global --debug flag
const debugIndex = cmdSlice.indexOf('--debug');
if (debugIndex > -1) {
if (extractBooleanFlag(cmdSlice, '--debug')) {
process.env.DEBUG = '1';
cmdSlice.splice(debugIndex, 1);
}
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
}
// Enable bluebird long stack traces when in debug mode, must be set
@ -87,11 +86,22 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
return args;
}
function extractBooleanFlag(argv: string[], flag: string): boolean {
const index = argv.indexOf(flag);
if (index >= 0) {
argv.splice(index, 1);
return true;
}
return false;
}
/**
* Check whether the command line refers to a command that has been deprecated
* and removed and, if so, exit with an informative error message.
*/
export function checkDeletedCommand(argvSlice: string[]): void {
const { ExpectedError } = require('./errors') as typeof import('./errors');
if (argvSlice[0] === 'help') {
argvSlice = argvSlice.slice(1);
}
@ -101,17 +111,16 @@ export function checkDeletedCommand(argvSlice: string[]): void {
version: string,
verb = 'replaced',
) {
exitWithExpectedError(stripIndent`
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
Please use "balena ${alternative}" instead.
`);
throw new ExpectedError(`\
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
Please use "balena ${alternative}" instead.`);
}
function removed(oldCmd: string, alternative: string, version: string) {
let msg = `Note: the command "balena ${oldCmd}" was removed in CLI version ${version}.`;
if (alternative) {
msg = [msg, alternative].join('\n');
}
exitWithExpectedError(msg);
throw new ExpectedError(msg);
}
const stopAlternative =
'Please use "balena ssh -s" to access the host OS, then use `balena-engine stop`.';

View File

@ -139,6 +139,14 @@ export async function downloadOSImage(
const displayVersion = OSVersion === 'default' ? '' : ` ${OSVersion}`;
// Override the default zlib flush value as we've seen cases of
// incomplete files being identified as successful downloads when using Z_SYNC_FLUSH.
// Using Z_NO_FLUSH results in a Z_BUF_ERROR instead of a corrupt image file.
// https://github.com/nodejs/node/blob/master/doc/api/zlib.md#zlib-constants
// Hopefully this is a temporary workaround until we can resolve
// some ongoing issues with the os download stream.
process.env.ZLIB_FLUSH = 'Z_NO_FLUSH';
const manager = await import('balena-image-manager');
const stream = await manager.get(deviceType, OSVersion);
@ -188,18 +196,21 @@ async function resolveOSVersion(deviceType: string, version: string) {
return version;
}
const { versions: vs, recommended } =
await getBalenaSdk().models.os.getSupportedVersions(deviceType);
const vs = (
(await getBalenaSdk().models.hostapp.getAllOsVersions([deviceType]))[
deviceType
] ?? []
).filter((v) => v.osType === 'default');
const choices = vs.map((v) => ({
value: v,
name: `v${v}` + (v === recommended ? ' (recommended)' : ''),
value: v.rawVersion,
name: `v${v.rawVersion}` + (v.isRecommended ? ' (recommended)' : ''),
}));
return getCliForm().ask({
message: 'Select the OS version:',
type: 'list',
choices,
default: recommended,
default: (vs.find((v) => v.isRecommended) ?? vs[0])?.rawVersion,
});
}

View File

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

View File

@ -18,6 +18,7 @@
import * as path from 'path';
import { ExpectedError } from '../errors';
import { getChalk } from './lazy';
import { isV13 } from './version';
/**
* @returns Promise<{import('./compose-types').ComposeOpts}>
@ -25,7 +26,7 @@ import { getChalk } from './lazy';
export function generateOpts(options) {
const { promises: fs } = require('fs');
if (options.gitignore && options['multi-dockerignore']) {
if (!isV13() && options.gitignore && options['multi-dockerignore']) {
throw new ExpectedError(
'The --gitignore and --multi-dockerignore options cannot be used together',
);
@ -37,7 +38,7 @@ export function generateOpts(options) {
convertEol: !options['noconvert-eol'],
dockerfilePath: options.dockerfile,
multiDockerignore: !!options['multi-dockerignore'],
nogitignore: !options.gitignore,
nogitignore: !options.gitignore, // v13: delete this line
noParentCheck: options['noparent-check'],
}));
}
@ -48,10 +49,16 @@ export function generateOpts(options) {
/**
* @param {string} composePath
* @param {string} composeStr
* @param {string | null} projectName
* @param {string | undefined} projectName The --projectName flag (build, deploy)
* @param {string | undefined} imageTag The --tag flag (build, deploy)
* @returns {import('./compose-types').ComposeProject}
*/
export function createProject(composePath, composeStr, projectName = null) {
export function createProject(
composePath,
composeStr,
projectName = '',
imageTag = '',
) {
const yml = require('js-yaml');
const compose = require('resin-compose-parse');
@ -61,7 +68,7 @@ export function createProject(composePath, composeStr, projectName = null) {
});
const composition = compose.normalize(rawComposition);
projectName ??= path.basename(composePath);
projectName ||= path.basename(composePath);
const descriptors = compose.parse(composition).map(function (descr) {
// generate an image name based on the project and service names
@ -71,9 +78,8 @@ export function createProject(composePath, composeStr, projectName = null) {
descr.image.context != null &&
descr.image.tag == null
) {
descr.image.tag = [projectName, descr.serviceName]
.join('_')
.toLowerCase();
const { makeImageName } = require('./compose_ts');
descr.image.tag = makeImageName(projectName, descr.serviceName, imageTag);
}
return descr;
});
@ -92,6 +98,8 @@ export function createProject(composePath, composeStr, projectName = null) {
* @param {string} dir Source directory
* @param {import('./compose-types').TarDirectoryOptions} param
* @returns {Promise<import('stream').Readable>}
*
* v13: delete this function
*/
export async function originalTarDirectory(dir, param) {
let {
@ -175,6 +183,9 @@ export async function originalTarDirectory(dir, param) {
* @param {number} userId
* @param {number} appId
* @param {import('resin-compose-parse').Composition} composition
* @param {boolean} draft
* @param {string|undefined} semver
* @param {string|undefined} contract
* @returns {Promise<import('./compose-types').Release>}
*/
export const createRelease = async function (
@ -183,6 +194,9 @@ export const createRelease = async function (
userId,
appId,
composition,
draft,
semver,
contract,
) {
const _ = require('lodash');
const crypto = require('crypto');
@ -197,6 +211,9 @@ export const createRelease = async function (
composition,
source: 'local',
commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(),
semver,
is_final: !draft,
contract,
});
return {
@ -207,6 +224,9 @@ export const createRelease = async function (
'commit',
'composition',
'source',
'is_final',
'contract',
'semver',
'start_timestamp',
'end_timestamp',
]),
@ -435,7 +455,6 @@ var pushProgressRenderer = function (tty, prefix) {
export class BuildProgressUI {
constructor(tty, descriptors) {
this._handleEvent = this._handleEvent.bind(this);
this._handleInterrupt = this._handleInterrupt.bind(this);
this.start = this.start.bind(this);
this.end = this.end.bind(this);
this._display = this._display.bind(this);
@ -487,14 +506,7 @@ export class BuildProgressUI {
this._serviceToDataMap[service] = event;
}
_handleInterrupt() {
this._cancelled = true;
this.end();
return process.exit(130); // 128 + SIGINT
}
start() {
process.on('SIGINT', this._handleInterrupt);
this._tty.hideCursor();
this._services.forEach((service) => {
this.streams[service].write({ status: 'Preparing...' });
@ -508,7 +520,6 @@ export class BuildProgressUI {
return;
}
this._ended = true;
process.removeListener('SIGINT', this._handleInterrupt);
this._runloop?.end();
this._runloop = null;

View File

@ -18,8 +18,9 @@ import { flags } from '@oclif/command';
import { BalenaSDK } from 'balena-sdk';
import type { TransposeOptions } from 'docker-qemu-transpose';
import type * as Dockerode from 'dockerode';
import * as _ from 'lodash';
import { promises as fs } from 'fs';
import jsyaml = require('js-yaml');
import * as _ from 'lodash';
import * as path from 'path';
import type {
BuildConfig,
@ -42,8 +43,11 @@ import {
import type { DeviceInfo } from './device/api';
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
import Logger = require('./logger');
import { isV13 } from './version';
import { exists } from './which';
const allowedContractTypes = ['sw.application', 'sw.block'];
/**
* Given an array representing the raw `--release-tag` flag of the deploy and
* push commands, parse it into separate arrays of release tag keys and values.
@ -114,6 +118,7 @@ export async function loadProject(
logger: Logger,
opts: ComposeOpts,
image?: string,
imageTag?: string,
): Promise<ComposeProject> {
const compose = await import('resin-compose-parse');
const { createProject } = await import('./compose');
@ -152,7 +157,12 @@ export async function loadProject(
}
}
logger.logDebug('Creating project...');
return createProject(opts.projectPath, composeStr, opts.projectName);
return createProject(
opts.projectPath,
composeStr,
opts.projectName,
imageTag,
);
}
/**
@ -234,7 +244,7 @@ interface Renderer {
streams: Dictionary<NodeJS.ReadWriteStream>;
}
export async function buildProject(opts: {
export interface BuildProjectOpts {
docker: Dockerode;
logger: Logger;
projectPath: string;
@ -247,84 +257,101 @@ export async function buildProject(opts: {
inlineLogs?: boolean;
convertEol: boolean;
dockerfilePath?: string;
nogitignore: boolean;
nogitignore: boolean; // v13: delete this line
multiDockerignore: boolean;
}): Promise<BuiltImage[]> {
const { logger, projectName } = opts;
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
}
let buildSummaryByService: Dictionary<string> | undefined;
export async function buildProject(
opts: BuildProjectOpts,
): Promise<BuiltImage[]> {
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
const compose = await import('resin-compose-parse');
const imageDescriptors = compose.parse(opts.composition);
const imageDescriptorsByServiceName = _.keyBy(
imageDescriptors,
'serviceName',
);
const renderer = await startRenderer({ imageDescriptors, ...opts });
let buildSummaryByService: Dictionary<string> | undefined;
try {
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors });
const tarStream = await tarDirectory(opts.projectPath, opts);
const tasks: BuildTaskPlus[] = await makeBuildTasks(
opts.composition,
tarStream,
const { awaitInterruptibleTask } = await import('./helpers');
const [images, summaryMsgByService] = await awaitInterruptibleTask(
$buildProject,
imageDescriptors,
renderer,
opts,
logger,
projectName,
);
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
const transposeOptArray: Array<TransposeOptions | undefined> =
await Promise.all(
tasks.map((task) => {
// Setup emulation if needed
if (needsQemu && !task.external) {
return qemuTransposeBuildStream({ task, ...opts });
}
}),
);
await Promise.all(
// transposeOptions may be undefined. That's OK.
transposeOptArray.map((transposeOptions, index) =>
setTaskProgressHooks({
task: tasks[index],
renderer,
transposeOptions,
...opts,
}),
),
);
logger.logDebug('Prepared tasks; building...');
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
const builder = await import('resin-multibuild');
const builtImages = await builder.performBuilds(
tasks,
opts.docker,
BALENA_ENGINE_TMP_PATH,
);
const [images, summaryMsgByService] = await inspectBuiltImages({
builtImages,
imageDescriptorsByServiceName,
tasks,
...opts,
});
buildSummaryByService = summaryMsgByService;
return images;
} finally {
renderer.end(buildSummaryByService);
}
}
async function $buildProject(
imageDescriptors: ImageDescriptor[],
renderer: Renderer,
opts: BuildProjectOpts,
): Promise<[BuiltImage[], Dictionary<string>]> {
const { logger, projectName } = opts;
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors });
const tarStream = await tarDirectory(opts.projectPath, opts);
const tasks: BuildTaskPlus[] = await makeBuildTasks(
opts.composition,
tarStream,
opts,
logger,
projectName,
);
const imageDescriptorsByServiceName = _.keyBy(
imageDescriptors,
'serviceName',
);
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
const transposeOptArray: Array<TransposeOptions | undefined> =
await Promise.all(
tasks.map((task) => {
// Setup emulation if needed
if (needsQemu && !task.external) {
return qemuTransposeBuildStream({ task, ...opts });
}
}),
);
await Promise.all(
// transposeOptions may be undefined. That's OK.
transposeOptArray.map((transposeOptions, index) =>
setTaskProgressHooks({
task: tasks[index],
renderer,
transposeOptions,
...opts,
}),
),
);
logger.logDebug('Prepared tasks; building...');
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
const builder = await import('resin-multibuild');
const builtImages = await builder.performBuilds(
tasks,
opts.docker,
BALENA_ENGINE_TMP_PATH,
);
return await inspectBuiltImages({
builtImages,
imageDescriptorsByServiceName,
tasks,
...opts,
});
}
async function startRenderer({
imageDescriptors,
inlineLogs,
@ -390,6 +417,18 @@ async function installQemuIfNeeded({
return needsQemu;
}
export function makeImageName(
projectName: string,
serviceName: string,
tag?: string,
) {
let name = `${projectName}_${serviceName}`;
if (tag) {
name = [name, tag].map((s) => s.replace(/:/g, '_')).join(':');
}
return name.toLowerCase();
}
function setTaskAttributes({
tasks,
buildOpts,
@ -405,7 +444,7 @@ function setTaskAttributes({
const d = imageDescriptorsByServiceName[task.serviceName];
// multibuild (splitBuildStream) parses the composition internally so
// any tags we've set before are lost; re-assign them here
task.tag ??= [projectName, task.serviceName].join('_').toLowerCase();
task.tag ??= makeImageName(projectName, task.serviceName, buildOpts.t);
if (isBuildConfig(d.image)) {
d.image.tag = task.tag;
}
@ -686,7 +725,7 @@ export async function getServiceDirsFromComposition(
* Return true if `image` is actually a docker-compose.yml `services.service.build`
* configuration object, rather than an "external image" (`services.service.image`).
*
* The `image` argument may therefore refere to either a `build` or `image` property
* The `image` argument may therefore refer to either a `build` or `image` property
* of a service in a docker-compose.yml file, which is a bit confusing but it matches
* the `ImageDescriptor.image` property as defined by `resin-compose-parse`.
*
@ -717,8 +756,8 @@ export async function tarDirectory(
dir: string,
param: TarDirectoryOptions,
): Promise<import('stream').Readable> {
const { nogitignore = false } = param;
if (nogitignore) {
const { nogitignore = false } = param; // v13: delete this line
if (isV13() || nogitignore) {
return newTarDirectory(dir, param);
} else {
return (await import('./compose')).originalTarDirectory(dir, param);
@ -739,11 +778,13 @@ async function newTarDirectory(
composition,
convertEol = false,
multiDockerignore = false,
nogitignore = false,
nogitignore = false, // v13: delete this line
preFinalizeCallback,
}: TarDirectoryOptions,
): Promise<import('stream').Readable> {
require('assert').strict.equal(nogitignore, true);
if (!isV13()) {
require('assert').strict.equal(nogitignore, true);
}
const { filterFilesWithDockerignore } = await import('./ignore');
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
@ -859,7 +900,8 @@ function printDockerignoreWarn(
}
}
if (msg.length) {
logFunc.call(logger, [' ', hr, ...msg, hr].join('\n'));
const { warnify } = require('./messages') as typeof import('./messages');
logFunc.call(logger, ' \n' + warnify(msg.join('\n'), ''));
}
}
@ -868,11 +910,16 @@ function printDockerignoreWarn(
* found and the --gitignore (-g) option has been provided (v11 compatibility).
* @param dockerignoreFile Absolute path to a .dockerignore file
* @param gitignoreFiles Array of absolute paths to .gitginore files
*
* v13: delete this function
*/
export function printGitignoreWarn(
dockerignoreFile: string,
gitignoreFiles: string[],
) {
if (isV13()) {
return;
}
const ignoreFiles = [dockerignoreFile, ...gitignoreFiles].filter((e) => e);
if (ignoreFiles.length === 0) {
return;
@ -1004,9 +1051,7 @@ export async function makeBuildTasks(
infoStr = `build [${task.context}]`;
}
logger.logDebug(` ${task.serviceName}: ${infoStr}`);
// Workaround for Docker v20.10 + single-arch base images. See:
// https://www.flowdock.com/app/rulemotion/i-cli/threads/RuSu1KiWOn62xaGy7O2sn8m8BUc
task.dockerPlatform = 'none';
task.logger = logger.getAdapter();
});
logger.logDebug(
@ -1063,18 +1108,21 @@ async function performResolution(
if (!buildTask.buildStream) {
continue;
}
// Consume each task.buildStream in order to trigger the
// resolution events that define fields like:
// task.dockerfile, task.dockerfilePath,
// task.projectType, task.resolved
// This mimics what is currently done in `resin-builder`.
const clonedStream: Pack = await cloneTarStream(
buildTask.buildStream,
);
buildTask.buildStream = clonedStream;
if (!buildTask.external && !buildTask.resolved) {
let error: Error | undefined;
try {
// Consume each task.buildStream in order to trigger the
// resolution events that define fields like:
// task.dockerfile, task.dockerfilePath,
// task.projectType, task.resolved
// This mimics what is currently done in `resin-builder`.
buildTask.buildStream = await cloneTarStream(buildTask.buildStream);
} catch (e) {
error = e;
}
if (error || (!buildTask.external && !buildTask.resolved)) {
const cause = error ? `${error}\n` : '';
throw new ExpectedError(
`Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
`${cause}Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
);
}
}
@ -1285,6 +1333,9 @@ async function pushServiceImages(
);
}
// TODO: This should be shared between the CLI & the Builder
const PLAIN_SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
export async function deployProject(
docker: import('dockerode'),
logger: Logger,
@ -1295,6 +1346,8 @@ export async function deployProject(
auth: string,
apiEndpoint: string,
skipLogUpload: boolean,
projectPath: string,
isDraft: boolean,
): Promise<import('balena-release/build/models').ReleaseModel> {
const releaseMod = await import('balena-release');
const { createRelease, tagServiceImages } = await import('./compose');
@ -1303,11 +1356,29 @@ export async function deployProject(
const prefix = getChalk().cyan('[Info]') + ' ';
const spinner = createSpinner();
const contractPath = path.join(projectPath, 'balena.yml');
const contract = await getContractContent(contractPath);
if (contract?.version && !PLAIN_SEMVER_REGEX.test(contract?.version)) {
throw new ExpectedError(stripIndent`\
Error: expected the version field in "${contractPath}"
to be a basic semver in the format '1.2.3'. Got '${contract.version}' instead`);
}
const $release = await runSpinner(
tty,
spinner,
`${prefix}Creating release...`,
() => createRelease(apiEndpoint, auth, userId, appId, composition),
() =>
createRelease(
apiEndpoint,
auth,
userId,
appId,
composition,
isDraft,
contract?.version,
contract ? JSON.stringify(contract) : undefined,
),
);
const { client: pineClient, release, serviceImages } = $release;
@ -1315,20 +1386,25 @@ export async function deployProject(
logger.logDebug('Tagging images...');
const taggedImages = await tagServiceImages(docker, images, serviceImages);
try {
const token = await getTokenForPreviousRepos(
logger,
appId,
apiEndpoint,
taggedImages,
);
await pushServiceImages(
docker,
logger,
pineClient,
taggedImages,
token,
skipLogUpload,
);
const { awaitInterruptibleTask } = await import('./helpers');
// awaitInterruptibleTask throws SIGINTError on CTRL-C,
// causing the release status to be set to 'failed'
await awaitInterruptibleTask(async () => {
const token = await getTokenForPreviousRepos(
logger,
appId,
apiEndpoint,
taggedImages,
);
await pushServiceImages(
docker,
logger,
pineClient,
taggedImages,
token,
skipLogUpload,
);
});
release.status = 'success';
} catch (err) {
release.status = 'failed';
@ -1392,6 +1468,42 @@ export function createRunLoop(tick: (...args: any[]) => void) {
return runloop;
}
async function getContractContent(
filePath: string,
): Promise<Dictionary<any> | undefined> {
let fileContentAsString;
try {
fileContentAsString = await fs.readFile(filePath, 'utf8');
} catch (e) {
if (e.code === 'ENOENT') {
return; // File does not exist
}
throw e;
}
let asJson;
try {
asJson = jsyaml.load(fileContentAsString);
} catch (err) {
throw new ExpectedError(
`Error parsing file "${filePath}":\n ${err.message}`,
);
}
if (!isContract(asJson)) {
throw new ExpectedError(
stripIndent`Error: application contract in '${filePath}' needs to
define a top level "type" field with an allowed application type.
Allowed application types are: ${allowedContractTypes.join(', ')}`,
);
}
return asJson;
}
function isContract(obj: any): obj is Dictionary<any> {
return obj?.type && allowedContractTypes.includes(obj.type);
}
function createLogStream(input: Readable) {
const split = require('split') as typeof import('split');
const stripAnsi = require('strip-ansi-stream');
@ -1532,22 +1644,26 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
description:
'Hide the image build log output (produce less verbose output)',
}),
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
until your project can be adapted.`,
char: 'g',
}),
...(isV13()
? {}
: {
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
until your project can be adapted.`,
char: 'g',
}),
nogitignore: flags.boolean({
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
char: 'G',
}),
}),
'multi-dockerignore': flags.boolean({
description:
'Have each service use its own .dockerignore file. See "balena help build".',
char: 'm',
}),
nogitignore: flags.boolean({
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
char: 'G',
}),
'noparent-check': flags.boolean({
description:
"Disable project validation check of 'docker-compose.yml' file in parent folder",
@ -1566,8 +1682,9 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
"Don't convert line endings from CRLF (Windows format) to LF (Unix format).",
}),
projectName: flags.string({
description:
'Specify an alternate project name; default is the directory name',
description: stripIndent`\
Name prefix for locally built images. This is the 'projectName' portion
in 'projectName_serviceName:tag'. The default is the directory name.`,
char: 'n',
}),
};

View File

@ -20,6 +20,7 @@ import * as os from 'os';
import * as request from 'request';
import type * as Stream from 'stream';
import { retry } from '../helpers';
import Logger = require('../logger');
import * as ApiErrors from './errors';
@ -211,17 +212,21 @@ export class DeviceAPI {
);
return;
}
res.socket.setKeepAlive(true, 1000);
if (os.platform() !== 'win32') {
const NetKeepalive = await import('net-keepalive');
// Certain versions of typescript won't convert
// this automatically
const sock = res.socket as any as NodeJSSocketWithFileDescriptor;
// We send a tcp keepalive probe once every 5 seconds
NetKeepalive.setKeepAliveInterval(sock, 5000);
// After 5 failed probes, the connection is marked as
// closed
NetKeepalive.setKeepAliveProbes(sock, 5);
try {
res.socket.setKeepAlive(true, 1000);
if (os.platform() !== 'win32') {
const NetKeepalive = await import('net-keepalive');
// Certain versions of typescript won't convert
// this automatically
const sock = res.socket as any as NodeJSSocketWithFileDescriptor;
// We send a tcp keepalive probe once every 5 seconds
NetKeepalive.setKeepAliveInterval(sock, 5000);
// After 5 failed probes, the connection is marked as
// closed
NetKeepalive.setKeepAliveProbes(sock, 5);
}
} catch (error) {
reject(error);
}
resolve(res);
});
@ -256,24 +261,35 @@ export class DeviceAPI {
}
}
return new Promise((resolve, reject) => {
return request(opts, (err, response, body) => {
if (err) {
return reject(err);
}
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));
}
const doRequest = async () => {
return await new Promise((resolve, reject) => {
return request(opts, (err, response, body) => {
if (err) {
return reject(err);
}
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));
}
});
});
};
return await retry({
func: doRequest,
initialDelayMs: 2000,
maxAttempts: 6,
label: `Supervisor API (${opts.method} ${(opts as any).url})`,
});
}
}

View File

@ -57,7 +57,7 @@ export interface DeviceDeployOptions {
registrySecrets: RegistrySecrets;
multiDockerignore: boolean;
nocache: boolean;
nogitignore: boolean;
nogitignore: boolean; // v13: delete this line
noParentCheck: boolean;
nolive: boolean;
pull: boolean;
@ -182,7 +182,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
convertEol: opts.convertEol,
dockerfilePath: opts.dockerfilePath,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore,
nogitignore: opts.nogitignore, // v13: delete this line
noParentCheck: opts.noParentCheck,
projectName: 'local',
projectPath: opts.source,
@ -201,7 +201,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
composition: project.composition,
convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore,
nogitignore: opts.nogitignore, // v13: delete this line
});
// Try to detect the device information
@ -310,7 +310,7 @@ function connectToDocker(host: string, port: number): Docker {
});
}
export async function performBuilds(
async function performBuilds(
composition: Composition,
tarStream: Readable,
docker: Docker,
@ -426,7 +426,7 @@ export async function rebuildSingleTask(
composition,
convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore,
nogitignore: opts.nogitignore, // v13: delete this line
});
const task = _.find(

View File

@ -80,16 +80,19 @@ async function displayDeviceLogs(
jsonStream.on('error', (e) => {
logger.logWarn(`Error parsing NDJSON log chunk: ${e}`);
});
logs.once('error', reject);
logs.once('end', () => {
logs.once('error', handleError);
logs.once('end', handleError);
logs.pipe(jsonStream);
function handleError(error?: Error | string) {
logger.logWarn(DeviceConnectionLostError.defaultMsg);
if (gotSignal) {
reject(new SIGINTError('Log streaming aborted on SIGINT signal'));
} else {
reject(new DeviceConnectionLostError());
const msg = typeof error === 'string' ? error : error?.message;
reject(new DeviceConnectionLostError(msg));
}
});
logs.pipe(jsonStream);
}
});
} finally {
process.removeListener('SIGINT', handleSignal);

View File

@ -72,7 +72,9 @@ export const dockerConnectionCliFlags: flags.Input<DockerConnectionCliFlags> = {
export const dockerCliFlags: flags.Input<DockerCliFlags> = {
tag: flags.string({
description: 'The alias to the generated image',
description: `\
Tag locally built Docker images. This is the 'tag' portion
in 'projectName_serviceName:tag'. The default is 'latest'.`,
char: 't',
}),
buildArg: flags.string({
@ -105,7 +107,7 @@ export interface BuildOpts {
pull?: boolean;
registryconfig?: import('resin-multibuild').RegistrySecrets;
squash?: boolean;
t?: string;
t?: string; // only the tag portion of the image name, e.g. 'abc' in 'myimg:abc'
}
function parseBuildArgs(args: string[]): Dictionary<string> {

View File

@ -148,23 +148,18 @@ export async function osProgressHandler(step: InitializeEmitter) {
});
}
export function getAppWithArch(
export async function getAppWithArch(
applicationName: string,
): Promise<ApplicationWithDeviceType & { arch: string }> {
return Promise.all([
getApplication(applicationName),
getBalenaSdk().models.config.getDeviceTypes(),
]).then(function ([app, deviceTypes]) {
const config = _.find<BalenaSdk.DeviceTypeJson.DeviceType>(deviceTypes, {
slug: app.is_for__device_type[0].slug,
});
const app = await getApplication(applicationName);
const { getExpanded } = await import('./pine');
if (!config) {
throw new Error(`balena API request failed for fleet ${applicationName}`);
}
return { ...app, arch: config.arch };
});
return {
...app,
arch: getExpanded(
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
)!.slug,
};
}
// TODO: Drop this. The sdk now has this baked in application.get().
@ -182,6 +177,11 @@ function getApplication(
},
is_for__device_type: {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
},
},
};
@ -463,13 +463,27 @@ export function getProxyConfig(): ProxyConfig | undefined {
}
}
export const expandForAppName: BalenaSdk.PineOptions<BalenaSdk.Device> = {
export const expandForAppName = {
$expand: {
belongs_to__application: { $select: 'app_name' },
is_of__device_type: { $select: 'slug' },
is_running__release: { $select: 'commit' },
},
};
} as const;
export const expandForAppNameAndCpuArch = {
$expand: {
...expandForAppName.$expand,
is_of__device_type: {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
},
},
} as const;
/**
* Use the `readline` library on Windows to install SIGINT handlers.

View File

@ -27,6 +27,7 @@ import { ExpectedError } from '../errors';
const { toPosixPath } = MultiBuild.PathUtils;
// v13: delete this enum
export enum IgnoreFileType {
DockerIgnore,
GitIgnore,
@ -42,6 +43,8 @@ interface IgnoreEntry {
* This class is used by the CLI v10 / v11 "original" tarDirectory function
* in `compose.js`. It is still around for the benefit of the `--gitignore`
* option, but is expected to be deleted in CLI v13.
*
* v13: delete this class
*/
export class FileIgnorer {
private dockerIgnoreEntries: IgnoreEntry[];

View File

@ -30,6 +30,14 @@ enum Level {
LIVEPUSH = 'livepush',
}
interface LoggerAdapter {
debug: (msg: string) => void;
error: (msg: string) => void;
info: (msg: string) => void;
log: (msg: string) => void;
warn: (msg: string) => void;
}
/**
* General purpose logger class with support for log streams and colours.
* Call `Logger.getLogger()` to retrieve a global shared instance of this
@ -57,6 +65,8 @@ class Logger {
protected deferredLogMessages: Array<[string, Level]>;
protected adapter: LoggerAdapter;
protected constructor() {
const logger = new StreamLogger();
const chalk = getChalk();
@ -91,6 +101,14 @@ class Logger {
this.formatMessage = logger.formatWithPrefix.bind(logger);
this.deferredLogMessages = [];
this.adapter = {
debug: (msg: string) => this.logDebug(msg),
error: (msg: string) => this.logError(msg),
info: (msg: string) => this.logInfo(msg),
log: (msg: string) => this.logLogs(msg),
warn: (msg: string) => this.logWarn(msg),
};
}
protected static logger: Logger;
@ -151,6 +169,10 @@ class Logger {
});
this.deferredLogMessages = [];
}
public getAdapter(): LoggerAdapter {
return this.adapter;
}
}
export = Logger;

View File

@ -15,6 +15,8 @@
* limitations under the License.
*/
import { isV13 } from './version';
export const reachingOut = `\
For further help or support, visit:
https://www.balena.io/docs/reference/balena-cli/#support-faq-and-troubleshooting
@ -30,6 +32,12 @@ export const help = reachingOut;
// is parsed, so its evaluation cannot happen at module loading time.
export const getHelp = () => (process.env.DEBUG ? '' : debugHint) + help;
export const deprecationPolicyNote = `\
The balena CLI enforces its deprecation policy by exiting with an error a year
after the release of the next major version, unless the --unsupported option is
used. Find out more at: https://git.io/JRHUW#deprecation-policy
`;
/**
* Take a multiline string like:
* Line One
@ -41,8 +49,9 @@ export const getHelp = () => (process.env.DEBUG ? '' : debugHint) + help;
* ---------------
* where the length of the dash rows matches the length of the longest line.
*/
export function warnify(msg: string) {
const lines = msg.split('\n').map((l) => `[Warn] ${l}`);
export function warnify(msg: string, prefix = '[Warn] ') {
let lines = msg.split('\n');
lines = prefix ? lines.map((l) => `${prefix}${l}`) : lines;
const maxLength = Math.max(...lines.map((l) => l.length));
const hr = '-'.repeat(maxLength);
return [hr, ...lines, hr].join('\n');
@ -79,7 +88,7 @@ If the --registry-secrets option is not specified, and a secrets.yml or
secrets.json file exists in the balena directory (usually $HOME/.balena),
this file will be used instead.`;
export const dockerignoreHelp =
const dockerignoreHelpV12 =
'DOCKERIGNORE AND GITIGNORE FILES \n' +
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
the project root (--source directory) in order to decide which source files to
@ -132,6 +141,60 @@ adding counter patterns to the applicable .dockerignore file(s), for example
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore`;
const dockerignoreHelpV13 =
'DOCKERIGNORE AND GITIGNORE FILES \n' +
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
the project root (--source directory) in order to decide which source files to
exclude from the "build context" (tar stream) sent to balenaCloud, Docker
daemon or balenaEngine. In a microservices (multicontainer) fleet, the
source directory is the directory that contains the "docker-compose.yml" file.
The --multi-dockerignore (-m) option may be used with microservices
(multicontainer) fleets that define a docker-compose.yml file. When this
option is used, each service subdirectory (defined by the \`build\` or
\`build.context\` service properties in the docker-compose.yml file) is
filtered separately according to a .dockerignore file defined in the service
subdirectory. If no .dockerignore file exists in a service subdirectory, then
only the default .dockerignore patterns (see below) apply for that service
subdirectory.
When the --multi-dockerignore (-m) option is used, the .dockerignore file (if
any) defined at the overall project root will be used to filter files and
subdirectories other than service subdirectories. It will not have any effect
on service subdirectories, whether or not a service subdirectory defines its
own .dockerignore file. Multiple .dockerignore files are not merged or added
together, and cannot override or extend other files. This behavior maximizes
compatibility with the standard docker-compose tool, while still allowing a
root .dockerignore file (at the overall project root) to filter files and
folders that are outside service subdirectories.
balena CLI v11 also took .gitignore files into account. This behavior was
deprecated in CLI v12 and removed in CLI v13. Please use .dockerignore files
instead.
Default .dockerignore patterns \n` +
`A few default/hardcoded dockerignore patterns are "merged" (in memory) with the
patterns found in the applicable .dockerignore files, in the following order:
\`\`\`
**/.git
< user's patterns from the applicable '.dockerignore' file, if any >
!**/.balena
!**/.resin
!**/Dockerfile
!**/Dockerfile.*
!**/docker-compose.yml
\`\`\`
These patterns always apply, whether or not .dockerignore files exist in the
project. If necessary, the effect of the \`**/.git\` pattern may be modified by
adding exception patterns to the applicable .dockerignore file(s), for example
\`!mysubmodule/.git\`. For documentation on pattern format, see:
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore`;
export const dockerignoreHelp = isV13()
? dockerignoreHelpV13
: dockerignoreHelpV12;
export const applicationIdInfo = `\
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
@ -145,6 +208,16 @@ environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.`;
export const applicationNameNote = `\
Fleets may be specified by fleet name or slug. Slugs are recommended because
they are unique and unambiguous. Slugs can be listed with the \`balena fleets\`
command. Note that slugs may change if the fleet is renamed. Fleet names are
not unique and may result in "Fleet is ambiguous" errors at any time (even if
"it used to work in the past"), for example if the name clashes with a newly
created public/open fleet, or with fleets from other balena accounts that you
may be invited to join under any role. For this reason, fleet names are
especially discouraged in scripts (e.g. CI environments).`;
export const jsonInfo = `\
The --json option is recommended when scripting the output of this command,
because field names are less likely to change in JSON format and because it
@ -184,3 +257,13 @@ the next major version of the CLI (v13). The --v13 option may be used
to enable the new names already now, and suppress a warning message.
(The --v13 option will be silently ignored in CLI v13.)
Find out more at: https://git.io/JRuZr`;
export function getNodeEngineVersionWarn(
version: string,
validVersions: string,
) {
version = version.startsWith('v') ? version.substring(1) : version;
return warnify(`\
Node.js version "${version}" does not satisfy requirement "${validVersions}"
This may cause unexpected behavior.`);
}

View File

@ -16,18 +16,12 @@ limitations under the License.
import type * as BalenaSdk from 'balena-sdk';
import _ = require('lodash');
import {
exitWithExpectedError,
instanceOf,
NotLoggedInError,
ExpectedError,
} from '../errors';
import { instanceOf, NotLoggedInError, ExpectedError } from '../errors';
import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
import validation = require('./validation');
import { delay } from './helpers';
import { isV13 } from './version';
import type { Application, Device, Organization } from 'balena-sdk';
import { getApplication } from './sdk';
export function authenticate(options: {}): Promise<void> {
const balena = getBalenaSdk();
@ -135,18 +129,16 @@ export function selectDeviceType() {
/**
* Display interactive confirmation prompt.
* If the user declines, then either an error will be thrown,
* or `exitWithExpectedError` will be called (if exitIfDeclined true).
* Throw ExpectedError if the user declines.
* @param yesOption - automatically confirm if true
* @param message - message to display with prompt
* @param yesMessage - message to display if automatically confirming
* @param exitIfDeclined - exitWithExpectedError when decline if true
*/
export async function confirm(
yesOption: boolean,
message: string,
yesMessage?: string,
exitIfDeclined = false,
defaultValue = false,
) {
if (yesOption) {
if (yesMessage) {
@ -162,16 +154,11 @@ export async function confirm(
const confirmed = await getCliForm().ask<boolean>({
message,
type: 'confirm',
default: false,
default: defaultValue,
});
if (!confirmed) {
const err = new ExpectedError('Aborted');
// TODO remove this deprecated function (exitWithExpectedError)
if (exitIfDeclined) {
exitWithExpectedError(err);
}
throw err;
throw new ExpectedError('Aborted');
}
}
@ -281,11 +268,9 @@ export async function awaitDeviceOsUpdate(
}
if (osUpdateStatus.error) {
console.error(
`Failed to complete Host OS update on device ${deviceName}!`,
throw new ExpectedError(
`Failed to complete Host OS update on device ${deviceName}\n${osUpdateStatus.error}`,
);
exitWithExpectedError(osUpdateStatus.error);
return;
}
if (osUpdateProgress !== null) {
@ -379,6 +364,7 @@ export async function getOnlineTargetDeviceUuid(
let app: Application;
try {
logger.logDebug(`Fetching fleet ${applicationOrDevice}`);
const { getApplication } = await import('./sdk');
app = await getApplication(sdk, applicationOrDevice);
} catch (err) {
const { BalenaApplicationNotFound } = await import('balena-errors');

View File

@ -19,6 +19,7 @@ import type * as BalenaSdk from 'balena-sdk';
import { ExpectedError, printErrorMessage } from '../errors';
import { getVisuals, stripIndent, getCliForm } from './lazy';
import Logger = require('./logger');
import { confirm } from './patterns';
import { exec, execBuffered, getDeviceOsRelease } from './ssh';
const MIN_BALENAOS_VERSION = 'v2.14.0';
@ -211,7 +212,7 @@ async function getOrSelectApplication(
.value();
if (!appName) {
return createOrSelectAppOrExit(sdk, compatibleDeviceTypes, deviceType);
return createOrSelectApp(sdk, compatibleDeviceTypes, deviceType);
}
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
@ -239,17 +240,14 @@ async function getOrSelectApplication(
)) as ApplicationWithDeviceType[];
if (applications.length === 0) {
const shouldCreateApp = await getCliForm().ask({
message:
`No fleet found with name "${appName}".\n` +
await confirm(
false,
`No fleet found with name "${appName}".\n` +
'Would you like to create it now?',
type: 'confirm',
default: true,
});
if (shouldCreateApp) {
return createApplication(sdk, deviceType, name);
}
process.exit(1);
undefined,
true,
);
return await createApplication(sdk, deviceType, name);
}
// We've found at least one fleet with the given name.
@ -269,10 +267,7 @@ async function getOrSelectApplication(
return selectAppFromList(applications);
}
// TODO: revisit this function's purpose. It was refactored out of
// `getOrSelectApplication` above in order to satisfy some resin-lint v3
// rules, but it looks like there's a fair amount of duplicate logic.
async function createOrSelectAppOrExit(
async function createOrSelectApp(
sdk: BalenaSdk.BalenaSDK,
compatibleDeviceTypes: string[],
deviceType: string,
@ -291,17 +286,14 @@ async function createOrSelectAppOrExit(
})) as ApplicationWithDeviceType[];
if (applications.length === 0) {
const shouldCreateApp = await getCliForm().ask({
message:
'You have no fleets this device can join.\n' +
await confirm(
false,
'You have no fleets this device can join.\n' +
'Would you like to create one now?',
type: 'confirm',
default: true,
});
if (shouldCreateApp) {
return createApplication(sdk, deviceType);
}
process.exit(1);
undefined,
true,
);
return await createApplication(sdk, deviceType);
}
return selectAppFromList(applications);

View File

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

View File

@ -42,6 +42,7 @@ export interface BuildOpts {
headless: boolean;
convertEol: boolean;
multiDockerignore: boolean;
isDraft: boolean;
}
export interface RemoteBuild {
@ -49,7 +50,7 @@ export interface RemoteBuild {
source: string;
auth: string;
baseUrl: string;
nogitignore: boolean;
nogitignore: boolean; // v13: delete this line
opts: BuildOpts;
sdk: BalenaSDK;
// For internal use
@ -92,6 +93,7 @@ async function getBuilderEndpoint(
emulated: opts.emulated,
nocache: opts.nocache,
headless: opts.headless,
isdraft: opts.isDraft,
});
// Note that using https (rather than http) is a requirement when using the
// --registry-secrets feature, as the secrets are not otherwise encrypted.
@ -319,7 +321,7 @@ async function getTarStream(build: RemoteBuild): Promise<Stream.Readable> {
preFinalizeCallback: preFinalizeCb,
convertEol: build.opts.convertEol,
multiDockerignore: build.opts.multiDockerignore,
nogitignore: build.nogitignore,
nogitignore: build.nogitignore, // v13: delete this line
});
} finally {
tarSpinner.stop();

606
npm-shrinkwrap.json generated
View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "12.45.0",
"version": "12.50.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1300,12 +1300,12 @@
"integrity": "sha512-u86QDMtkpHLlvehs3Z+yHklXRhDPL5XGCO3BCSuaD61gKzrNDUIj03cz8T/PBPPUJqn7DfWkf9sKP9VwlvxKuw=="
},
"@balena/node-web-streams": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@balena/node-web-streams/-/node-web-streams-0.2.3.tgz",
"integrity": "sha512-WaFtrO5lQUAWmLVcBn7V0tLHOuX/S9JPxmhfcEc9drLZhNUKF/psnNwWGfhPxgRwwik4hK0AqKIbjqkLLBXhSg==",
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@balena/node-web-streams/-/node-web-streams-0.2.4.tgz",
"integrity": "sha512-Q9By3GPzANMZuf1i5i7Agyh6BUe6tTa+VCCZzsFzU32iXMcuDRXYHbNIKESrcjVXxiZScPB4u++WPw4LRyK1Gg==",
"requires": {
"is-stream": "^1.1.0",
"web-streams-polyfill": "^1.3.2"
"web-streams-polyfill": "^3.1.0"
}
},
"@balena/udif": {
@ -2169,68 +2169,68 @@
"integrity": "sha512-STcqSvk+c7ArMrZgYxhM92p6O6F7t0SUbGr+zm8s9fJple5EdJAMwP3dXqgdXeF95xWhBpha5kjEqNAIdI0r4w=="
},
"@sentry/core": {
"version": "5.25.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.25.0.tgz",
"integrity": "sha512-hY6Zmo7t/RV+oZuvXHP6nyAj/QnZr2jW0e7EbL5YKMV8q0vlnjcE0LgqFXme726OJemoLk67z+sQOJic/Ztehg==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.2.tgz",
"integrity": "sha512-snXNNFLwlS7yYxKTX4DBXebvJK+6ikBWN6noQ1CHowvM3ReFBlrdrs0Z0SsSFEzXm2S4q7f6HHbm66GSQZ/8FQ==",
"requires": {
"@sentry/hub": "5.25.0",
"@sentry/minimal": "5.25.0",
"@sentry/types": "5.25.0",
"@sentry/utils": "5.25.0",
"@sentry/hub": "6.13.2",
"@sentry/minimal": "6.13.2",
"@sentry/types": "6.13.2",
"@sentry/utils": "6.13.2",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sentry/hub": {
"version": "5.25.0",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.25.0.tgz",
"integrity": "sha512-kOlOiJV8wMX50lYpzMlOXBoH7MNG0Ho4RTusdZnXZBaASq5/ljngDJkLr6uylNjceZQP21wzipCQajsJMYB7EQ==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.2.tgz",
"integrity": "sha512-sppSuJdNMiMC/vFm/dQowCBh11uTrmvks00fc190YWgxHshodJwXMdpc+pN61VSOmy2QA4MbQ5aMAgHzPzel3A==",
"requires": {
"@sentry/types": "5.25.0",
"@sentry/utils": "5.25.0",
"@sentry/types": "6.13.2",
"@sentry/utils": "6.13.2",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sentry/minimal": {
"version": "5.25.0",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.25.0.tgz",
"integrity": "sha512-9JFKuW7U+1vPO86k3+XRtJyooiVZsVOsFFO4GulBzepi3a0ckNyPgyjUY1saLH+cEHx18hu8fGgajvI8ANUF2g==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.2.tgz",
"integrity": "sha512-6iJfEvHzzpGBHDfLxSHcGObh73XU1OSQKWjuhDOe7UQDyI4BQmTfcXAC+Fr8sm8C/tIsmpVi/XJhs8cubFdSMw==",
"requires": {
"@sentry/hub": "5.25.0",
"@sentry/types": "5.25.0",
"@sentry/hub": "6.13.2",
"@sentry/types": "6.13.2",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sentry/node": {
"version": "5.25.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.25.0.tgz",
"integrity": "sha512-zxoUVdAFTeK9kdEGY95TMs6g8Zx/P55HxG4gHD80BG/XIEvWiGPcGCLOspO4IdGqYXkGS74KfBOIXmmCawWwLg==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-6.13.2.tgz",
"integrity": "sha512-0Vw22amG143MTiNaSny66YGU3+uW7HxyGI9TLGE7aJY1nNmC0DE+OgqQYGBRCrrPu+VFXRDxrOg9b15A1gKqjA==",
"requires": {
"@sentry/core": "5.25.0",
"@sentry/hub": "5.25.0",
"@sentry/tracing": "5.25.0",
"@sentry/types": "5.25.0",
"@sentry/utils": "5.25.0",
"@sentry/core": "6.13.2",
"@sentry/hub": "6.13.2",
"@sentry/tracing": "6.13.2",
"@sentry/types": "6.13.2",
"@sentry/utils": "6.13.2",
"cookie": "^0.4.1",
"https-proxy-agent": "^5.0.0",
"lru_map": "^0.3.3",
@ -2238,49 +2238,49 @@
},
"dependencies": {
"tslib": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sentry/tracing": {
"version": "5.25.0",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.25.0.tgz",
"integrity": "sha512-KcyHEGFpqSDubHrdWT/vF2hKkjw/ts6NpJ6tPDjBXUNz98BHdAyMKtLOFTCeJFply7/s5fyiAYu44M+M6IG3Bw==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.2.tgz",
"integrity": "sha512-bHJz+C/nd6biWTNcYAu91JeRilsvVgaye4POkdzWSmD0XoLWHVMrpCQobGpXe7onkp2noU3YQjhqgtBqPHtnpw==",
"requires": {
"@sentry/hub": "5.25.0",
"@sentry/minimal": "5.25.0",
"@sentry/types": "5.25.0",
"@sentry/utils": "5.25.0",
"@sentry/hub": "6.13.2",
"@sentry/minimal": "6.13.2",
"@sentry/types": "6.13.2",
"@sentry/utils": "6.13.2",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sentry/types": {
"version": "5.25.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.25.0.tgz",
"integrity": "sha512-8M4PREbcar+15wrtEqcwfcU33SS+2wBSIOd/NrJPXJPTYxi49VypCN1mZBDyWkaK+I+AuQwI3XlRPCfsId3D1A=="
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.2.tgz",
"integrity": "sha512-6WjGj/VjjN8LZDtqJH5ikeB1o39rO1gYS6anBxiS3d0sXNBb3Ux0pNNDFoBxQpOhmdDHXYS57MEptX9EV82gmg=="
},
"@sentry/utils": {
"version": "5.25.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.25.0.tgz",
"integrity": "sha512-Hz5spdIkMSRH5NR1YFOp5qbsY5Ud2lKhEQWlqxcVThMG5YNUc10aYv5ijL19v0YkrC2rqPjCRm7GrVtzOc7bXQ==",
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.2.tgz",
"integrity": "sha512-foF4PbxqPMWNbuqdXkdoOmKm3quu3PP7Q7j/0pXkri4DtCuvF/lKY92mbY0V9rHS/phCoj+3/Se5JvM2ymh2/w==",
"requires": {
"@sentry/types": "5.25.0",
"@sentry/types": "6.13.2",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
@ -2385,6 +2385,17 @@
"@types/node": "*"
}
},
"@types/cacheable-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
"integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==",
"requires": {
"@types/http-cache-semantics": "*",
"@types/keyv": "*",
"@types/node": "*",
"@types/responselike": "*"
}
},
"@types/caseless": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
@ -2534,6 +2545,11 @@
"integrity": "sha512-TikApqV8CkUsI1GGUgVydkJFrq9sYCBWv4fc/r3zvl6Oqe2YU1ASeWBrG5bw1D2XvS07YS3s05hCor/lEtIoYw==",
"dev": true
},
"@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
},
"@types/http-proxy": {
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz",
@ -2600,6 +2616,14 @@
"resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-2.2.1.tgz",
"integrity": "sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A=="
},
"@types/keyv": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.2.tgz",
"integrity": "sha512-/FvAK2p4jQOaJ6CGDHJTqZcUtbZe820qIeTg7o0Shg7drB4JHeL+V/dhSaly7NXx6u8eSee+r7coT+yuJEvDLg==",
"requires": {
"@types/node": "*"
}
},
"@types/klaw": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-3.0.2.tgz",
@ -2615,14 +2639,14 @@
"integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg=="
},
"@types/lru-cache": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.0.tgz",
"integrity": "sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w=="
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw=="
},
"@types/memoizee": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.5.tgz",
"integrity": "sha512-+ZzZZ3+0a7/ajBPeAAD4+LxrBsCat0EFZQtO3o0rwpIeLmDmSaM8KF/oYPuFxeUFAMiHIHFcGucFnY/8S4Hszg=="
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.6.tgz",
"integrity": "sha512-qJezGqoi3pW9Pset2w1Gfv8jATvmHHHnpO9Dq8x8pJGyYIpiUZJqRU0NM7xenmN0AcXEe7vqshI8H98KeFLYcg=="
},
"@types/mime": {
"version": "1.3.2",
@ -2791,6 +2815,14 @@
}
}
},
"@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
"requires": {
"@types/node": "*"
}
},
"@types/rewire": {
"version": "2.5.28",
"resolved": "https://registry.npmjs.org/@types/rewire/-/rewire-2.5.28.tgz",
@ -2905,9 +2937,9 @@
}
},
"@types/uuid": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ=="
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz",
"integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg=="
},
"@types/which": {
"version": "2.0.1",
@ -3061,9 +3093,9 @@
"dev": true
},
"agent-base": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz",
"integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"requires": {
"debug": "4"
}
@ -3732,9 +3764,9 @@
}
},
"balena-hup-action-utils": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-4.0.2.tgz",
"integrity": "sha512-N2HVaqXodwR18HKnbOwJDzDZOLpQoPzH6MD4Ibs09Sb9OGBizgjAHiK4Qs20qX1wvztqm8sy8zWJ8nXEOFAplg==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-4.0.3.tgz",
"integrity": "sha512-PdHMpSjaQriB4y4zmeAmm0Mxudencc/BVjI3jHVH3/SCZ6OqIIGlyFJU2tS0vsghED8PiEyQSjUdDCGzv7zUiQ==",
"requires": {
"balena-semver": "^2.0.0",
"tslib": "^2.0.0"
@ -3779,22 +3811,12 @@
"balena-errors": "^4.2.1",
"pinejs-client-core": "^6.9.0",
"tslib": "^2.0.1"
},
"dependencies": {
"pinejs-client-core": {
"version": "6.9.5",
"resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-6.9.5.tgz",
"integrity": "sha512-/QbmrR6IjlGZiM3JjZq9WtMdoR5UWW4zI+mCElm6Ez6kZiBYnWbHEDaVpyfEAj3mmSf6bj9ezQTxU+eLnzC1OQ==",
"requires": {
"@balena/es-version": "^1.0.0"
}
}
}
},
"balena-preload": {
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-10.4.20.tgz",
"integrity": "sha512-uKbccD1oh7BQ9XUMXYQvGklfws1v9+oaWkYxE0eheh+0GcHP+CVWr/TIUx+P3eplh7cAfeZAxw0wx0jp8H1Ttg==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-10.5.0.tgz",
"integrity": "sha512-tgnTyOSOLB3HxIqlR1NFrTsy1eiiew5Vzmplb82/eZc/vJTrOqal2tFNn6aFay6UQ8+OASUJwANC99zBu1e8mQ==",
"requires": {
"archiver": "^3.1.1",
"balena-sdk": "^15.44.0",
@ -3827,32 +3849,6 @@
"zip-stream": "^2.1.2"
}
},
"balena-sdk": {
"version": "15.45.0",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.45.0.tgz",
"integrity": "sha512-lfz6x8yL7nV+zHTC2+w30Dtv7I5rz8IxoO6Ifjb4/ewkhz6LyX1/UhH/a4k83oaQMXIuoYnmwWE6iVx/zl9xXQ==",
"requires": {
"@balena/es-version": "^1.0.0",
"@types/lodash": "^4.14.168",
"@types/memoizee": "^0.4.5",
"@types/node": "^10.17.55",
"abortcontroller-polyfill": "^1.7.1",
"balena-auth": "^4.1.0",
"balena-errors": "^4.7.1",
"balena-hup-action-utils": "~4.0.2",
"balena-pine": "^12.4.0",
"balena-register-device": "^7.1.0",
"balena-request": "^11.4.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6",
"lodash": "^4.17.21",
"memoizee": "^0.4.15",
"moment": "^2.29.1",
"ndjson": "^2.0.0",
"semver": "^7.3.4",
"tslib": "^2.1.0"
}
},
"compress-commons": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz",
@ -3938,9 +3934,9 @@
}
},
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"zip-stream": {
"version": "2.1.3",
@ -3966,16 +3962,16 @@
},
"dependencies": {
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"balena-release": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/balena-release/-/balena-release-3.0.0.tgz",
"integrity": "sha512-LYgPGBUnqJY+ajhTWE2BizyaRNxitxPSIZp4xGWDHbpVHmwKaV4p3d6nw1Hf9kEHP4dLELJvnNXq2EKV2IpTFA==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/balena-release/-/balena-release-3.2.0.tgz",
"integrity": "sha512-jwmAjIZCJ5I46/yQNN+dA73RWlre0+jBVmo2QeJl1pK83obTLyifJeWNVf5irzP8KFE7WQzo9ICK1cCpLtygFA==",
"requires": {
"@types/bluebird": "^3.5.18",
"@types/node": "^8.0.55",
@ -3987,16 +3983,16 @@
},
"dependencies": {
"@types/node": {
"version": "8.10.62",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.62.tgz",
"integrity": "sha512-76fupxOYVxk36kb7O/6KtrAPZ9jnSK3+qisAX4tQMEuGNdlvl7ycwatlHqjoE6jHfVtXFM3pCrCixZOidc5cuw=="
"version": "8.10.66",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
"integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="
}
}
},
"balena-request": {
"version": "11.4.0",
"resolved": "https://registry.npmjs.org/balena-request/-/balena-request-11.4.0.tgz",
"integrity": "sha512-wfPaWX/+NgT2xNplQqA8oCNLJXG6eLMbf9IOX8T4ZX+nqBoA9bydoIRLunGExMNfUWpxApvBh5ls8fJOd9VTjQ==",
"version": "11.4.2",
"resolved": "https://registry.npmjs.org/balena-request/-/balena-request-11.4.2.tgz",
"integrity": "sha512-J4SrFBUR4AB2Y3afsX2QAMZ7H/zjysXjOyEhEqvjNTsBfe5ReCmf17vzRQ8q2URCm00nUhQgfQtlJUq6miB1/g==",
"requires": {
"@balena/node-web-streams": "^0.2.3",
"balena-errors": "^4.7.1",
@ -4008,9 +4004,9 @@
}
},
"balena-sdk": {
"version": "15.36.0",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.36.0.tgz",
"integrity": "sha512-h55dSJpZ8XCJAwvbfinCuGPQXfSIBg/ftkpd6ObV+WQ/iN7DIpBKG0QVPdWoK91Ws0Ie/GzmW4IFvgtS/Yf3hQ==",
"version": "15.51.1",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.51.1.tgz",
"integrity": "sha512-EMCQruytqyvpfxvjq9Zd/wWnnOAIl/Wd1majqv6hqa+z104UUTjnRCQaUji4mo8YtrFLn7aUUZFFHIKiv/3sTg==",
"requires": {
"@balena/es-version": "^1.0.0",
"@types/lodash": "^4.14.168",
@ -4022,7 +4018,7 @@
"balena-hup-action-utils": "~4.0.2",
"balena-pine": "^12.4.0",
"balena-register-device": "^7.1.0",
"balena-request": "^11.4.0",
"balena-request": "^11.4.2",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6",
"lodash": "^4.17.21",
@ -4047,9 +4043,9 @@
}
},
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
@ -4065,9 +4061,9 @@
}
},
"balena-settings-client": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.6.tgz",
"integrity": "sha512-bB14Zvg1N6t7XXPJqZs48SajgTuk2WTMm2AnxcOfoIQ2d/Lh0RsEGxD9toF2v+WhF2Ip4u7ko5tKlCr2kFddXA==",
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.7.tgz",
"integrity": "sha512-1ncEgufbAbzcfcffsTpi20asNdsOEZxACiQhv8naQp1mgw6INe/0FvSNX6St+XlXtuk1FqCnYNINGIjMoStOrA==",
"requires": {
"@resin.io/types-hidepath": "1.0.1",
"@resin.io/types-home-or-tmp": "3.0.0",
@ -4597,6 +4593,11 @@
"unset-value": "^1.0.0"
}
},
"cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="
},
"cacheable-request": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
@ -6496,6 +6497,14 @@
}
}
},
"dockerfile-ast": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/dockerfile-ast/-/dockerfile-ast-0.2.1.tgz",
"integrity": "sha512-ut04CVM1G6zIITTcYPDIXhPZk9mCa21m4dfW8FcDDGxwgTQhYyHDu6U7M8klZ7QsjqVcJhryKi+TGOX6bjgKdQ==",
"requires": {
"vscode-languageserver-types": "^3.16.0"
}
},
"dockerfile-template": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dockerfile-template/-/dockerfile-template-0.2.0.tgz",
@ -6899,19 +6908,6 @@
"es6-symbol": "^3.1.1"
}
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"requires": {
"es6-promise": "^4.0.3"
}
},
"es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
@ -7778,7 +7774,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/ffi-napi/-/ffi-napi-4.0.3.tgz",
"integrity": "sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg==",
"optional": true,
"requires": {
"debug": "^4.1.1",
"get-uv-event-loop-napi-h": "^1.0.5",
@ -7789,10 +7784,9 @@
},
"dependencies": {
"node-addon-api": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==",
"optional": true
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
}
}
},
@ -8203,9 +8197,9 @@
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fp-ts": {
"version": "2.10.5",
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.10.5.tgz",
"integrity": "sha512-X2KfTIV0cxIk3d7/2Pvp/pxL/xr2MV1WooyEzKtTWYSc1+52VF4YzjBTXqeOlSiZsPCxIBpDGfT9Dyo7WEY0DQ=="
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.11.3.tgz",
"integrity": "sha512-qHI5iaVSFNFmdl6yDensWfFMk32iafAINCnqx8m486DV1+Jht/bTnA9CyahL+Xm7h2y3erinviVBIAWvv5bPYw=="
},
"fragment-cache": {
"version": "0.2.1",
@ -8433,14 +8427,12 @@
"get-symbol-from-current-process-h": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz",
"integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw==",
"optional": true
"integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw=="
},
"get-uv-event-loop-napi-h": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz",
"integrity": "sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==",
"optional": true,
"requires": {
"get-symbol-from-current-process-h": "^1.0.1"
}
@ -8465,6 +8457,31 @@
"requires": {
"got": "^6.2.0",
"is-plain-obj": "^1.1.0"
},
"dependencies": {
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"got": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
"requires": {
"create-error-class": "^3.0.0",
"duplexer3": "^0.1.4",
"get-stream": "^3.0.0",
"is-redirect": "^1.0.0",
"is-retry-allowed": "^1.0.0",
"is-stream": "^1.0.0",
"lowercase-keys": "^1.0.0",
"safe-buffer": "^5.0.1",
"timed-out": "^4.0.0",
"unzip-response": "^2.0.1",
"url-parse-lax": "^1.0.0"
}
}
}
},
"ghauth": {
@ -8941,27 +8958,111 @@
}
},
"got": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"requires": {
"create-error-class": "^3.0.0",
"duplexer3": "^0.1.4",
"get-stream": "^3.0.0",
"is-redirect": "^1.0.0",
"is-retry-allowed": "^1.0.0",
"is-stream": "^1.0.0",
"lowercase-keys": "^1.0.0",
"safe-buffer": "^5.0.1",
"timed-out": "^4.0.0",
"unzip-response": "^2.0.1",
"url-parse-lax": "^1.0.0"
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0",
"responselike": "^2.0.0"
},
"dependencies": {
"@sindresorhus/is": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz",
"integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g=="
},
"@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"requires": {
"defer-to-connect": "^2.0.0"
}
},
"cacheable-request": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
"integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
"requires": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.0.0"
}
},
"decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"requires": {
"mimic-response": "^3.1.0"
}
},
"defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
},
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"requires": {
"pump": "^3.0.0"
}
},
"json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"keyv": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz",
"integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==",
"requires": {
"json-buffer": "3.0.1"
}
},
"lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
},
"mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
},
"normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
},
"p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="
},
"responselike": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz",
"integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==",
"requires": {
"lowercase-keys": "^2.0.0"
}
}
}
},
@ -9424,6 +9525,15 @@
"sshpk": "^1.7.0"
}
},
"http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"requires": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
}
},
"https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
@ -11870,41 +11980,6 @@
}
}
},
"mixpanel": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/mixpanel/-/mixpanel-0.10.3.tgz",
"integrity": "sha512-wIYr5o+1XSzJ80o3QED35K/yfPAKi5FigZXTSfcs4vltfeKbilIjNgwxdno7LrqzhjoSjmIyDWkI7D3lr7TwDw==",
"requires": {
"https-proxy-agent": "3.0.0"
},
"dependencies": {
"agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"requires": {
"es6-promisify": "^5.0.0"
}
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"https-proxy-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz",
"integrity": "sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ==",
"requires": {
"agent-base": "^4.3.0",
"debug": "^3.1.0"
}
}
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@ -12471,10 +12546,9 @@
}
},
"net-keepalive": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/net-keepalive/-/net-keepalive-2.0.3.tgz",
"integrity": "sha512-VNWUXuLR0tUkZT2VVanJ5VqlBTJL8O5tGVsgq/FrvhHO7vjVC8gfU0ogRL+hxnfYuzSkMaiTHZfdIqwu7Q4zXA==",
"optional": true,
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/net-keepalive/-/net-keepalive-3.0.0.tgz",
"integrity": "sha512-wfDa7VPeSltY5aIQcujS7AiWnO2JHJCpO3is4nwQ7kFYs4YMpzDNMwiuILPkWwgMbPMSHzO7O1tuL8rC0SP3ag==",
"requires": {
"ffi-napi": "^4.0.1",
"ref-napi": "^3.0.0"
@ -12565,8 +12639,7 @@
"node-gyp-build": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==",
"optional": true
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg=="
},
"node-pre-gyp": {
"version": "0.14.0",
@ -13806,24 +13879,24 @@
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"pinejs-client-core": {
"version": "6.7.3",
"resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-6.7.3.tgz",
"integrity": "sha512-VXX/EpbDC/LiEPix/S9gENsaruoa2N0GHzmEbf9jYZ7qhwGNJjGlnQXQ1AnE/cjHYgHOo2RnMYKJ+iBakFgnWQ==",
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-6.9.6.tgz",
"integrity": "sha512-XUfHeYxT65PIxaV2SWZ7o/2nBUpTg9NaAcrfIRHgMkWQVlcUnh5EguHXoo2nKA4ocKds1fxIheuT29JqEg9SWQ==",
"requires": {
"@balena/es-version": "^1.0.0"
}
},
"pinejs-client-request": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/pinejs-client-request/-/pinejs-client-request-7.2.0.tgz",
"integrity": "sha512-rzUbSc3AkxHKlEz3TeJ5txABoxVsrYGSLjcv9uS3A7oVuObF8vD/CUFredJ/5m3hnvSRRIpRl0nPyugRAqTS+g==",
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/pinejs-client-request/-/pinejs-client-request-7.3.3.tgz",
"integrity": "sha512-HmJfI/yvRB5mrwPedSIMhgcdWco1g4BfGa3bIwEoncOAi808y9FhfPrbcBe1ZjWGtGZTZRE020LpkMaIyigzBg==",
"requires": {
"@types/lodash": "^4.14.159",
"@types/lodash": "^4.14.168",
"@types/lru-cache": "^5.1.0",
"@types/request": "^2.48.5",
"lodash": "^4.17.19",
"lodash": "^4.17.21",
"lru-cache": "^6.0.0",
"pinejs-client-core": "^6.6.1",
"pinejs-client-core": "^6.9.5",
"request": "^2.88.2",
"typed-error": "^3.2.1"
}
@ -14712,6 +14785,11 @@
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -15063,22 +15141,20 @@
}
},
"ref-napi": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/ref-napi/-/ref-napi-3.0.2.tgz",
"integrity": "sha512-5YE0XrvWteoTr5DR2sEqxefL06aml7c6qS7hGv3u27do4HlGQphwvB+zD1NYep9utMKScvwOZsSs9EPYdGBVsg==",
"optional": true,
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ref-napi/-/ref-napi-3.0.3.tgz",
"integrity": "sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA==",
"requires": {
"debug": "^4.1.1",
"get-symbol-from-current-process-h": "^1.0.2",
"node-addon-api": "^2.0.0",
"node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1"
},
"dependencies": {
"node-addon-api": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz",
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==",
"optional": true
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
}
}
},
@ -15086,7 +15162,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ref-struct-di/-/ref-struct-di-1.1.1.tgz",
"integrity": "sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==",
"optional": true,
"requires": {
"debug": "^3.1.0"
},
@ -15095,7 +15170,6 @@
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"optional": true,
"requires": {
"ms": "^2.1.1"
}
@ -15366,9 +15440,9 @@
}
},
"resin-compose-parse": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/resin-compose-parse/-/resin-compose-parse-2.1.3.tgz",
"integrity": "sha512-X5WQo+OHoPe+FV8JliGzSIL4glLX0PPFvtnopppYef1UqKcJm+GHaiEZBOj3C7vIEDqQrsNrKXY/BpadlOFiWA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/resin-compose-parse/-/resin-compose-parse-2.3.0.tgz",
"integrity": "sha512-9vE+ascGEXuyTYjLmhwmVOYNkws99Cq7/RtUrgT8VCYJRuhqZMpGMOJPrjEIF6PvsBY7B6+u32x1HPCR4gWHBQ==",
"requires": {
"@types/lodash": "^4.14.86",
"@types/node": "^8.0.55",
@ -15442,9 +15516,12 @@
}
},
"@types/klaw": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-1.3.5.tgz",
"integrity": "sha512-KZfv4ea6bEbdQhfwpxtDuTPO2mHAAXMQqPOZyS4MgNyCymKoLHp0FVzzYq3H2zCeIotN4h1453TahLCCm8rf2w=="
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-1.3.6.tgz",
"integrity": "sha512-4pr2RxwhfsLxFYa4Ip8JxrdXIvPX7fAqyBh9ofZPedMwf8M5CIcSQskqvX6/5Y/zpCBHtuC3218t8H+XJsg5FA==",
"requires": {
"@types/node": "*"
}
},
"bl": {
"version": "1.2.3",
@ -15641,13 +15718,14 @@
}
},
"resin-multibuild": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/resin-multibuild/-/resin-multibuild-4.11.0.tgz",
"integrity": "sha512-rIYV9GDNuI8pU9N+wGdVRIOGAnw1BFdbyt3BkvERFxbf+b/e7jpBjHkbK8VPQdRMlKPyu137ZxQlR3z7EivJBg==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/resin-multibuild/-/resin-multibuild-4.12.2.tgz",
"integrity": "sha512-FkRqGEM588wA6v03pQbodPqWQdAs6aMh+GWvYQBz5IxqSVecn4FLHaRE0pF6VFKtjf/XBuPw7dtqiFzH+NIz5g==",
"requires": {
"ajv": "^6.12.3",
"bluebird": "^3.7.2",
"docker-progress": "^5.0.0",
"dockerfile-ast": "^0.2.1",
"dockerfile-template": "^0.2.0",
"dockerode": "^2.5.8",
"fp-ts": "^2.8.1",
@ -15887,6 +15965,11 @@
"path-parse": "^1.0.6"
}
},
"resolve-alpn": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz",
"integrity": "sha512-e4FNQs+9cINYMO5NMFc6kOUCdohjqFPSgMuwuZAOUWqrfWsen+Yjy5qZFkV5K7VO7tFSLKcUL97olkED7sCBHA=="
},
"resolve-dir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
@ -16383,13 +16466,13 @@
"dev": true
},
"sinon": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.1.tgz",
"integrity": "sha512-ZSSmlkSyhUWbkF01Z9tEbxZLF/5tRC9eojCdFh33gtQaP7ITQVaMWQHGuFM7Cuf/KEfihuh1tTl3/ABju3AQMg==",
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz",
"integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.8.3",
"@sinonjs/fake-timers": "^7.1.0",
"@sinonjs/fake-timers": "^7.1.2",
"@sinonjs/samsam": "^6.0.2",
"diff": "^5.0.0",
"nise": "^5.1.0",
@ -18408,6 +18491,11 @@
}
}
},
"vscode-languageserver-types": {
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz",
"integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA=="
},
"wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@ -18417,9 +18505,9 @@
}
},
"web-streams-polyfill": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-1.3.2.tgz",
"integrity": "sha1-NxkkXpCSgtk5Z4JfRLzVUOnAOZU="
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz",
"integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q=="
},
"which": {
"version": "2.0.2",

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "12.45.0",
"version": "12.50.2",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -49,6 +49,7 @@
"postinstall": "node patches/apply-patches.js",
"prebuild": "rimraf build/ build-bin/",
"build": "npm run build:src && npm run catch-uncommitted",
"build:t": "npm run lint && npm run build:fast && npm run build:test",
"build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc && npm run build:completion",
"build:fast": "gulp pages && tsc && npx oclif-dev manifest",
"build:test": "tsc -P ./tsconfig.dev.json --noEmit && tsc -P ./tsconfig.js.json --noEmit",
@ -65,6 +66,7 @@
"test:standalone": "npm run build:standalone && npm run test:standalone:fast",
"test:standalone:fast": "cross-env BALENA_CLI_TEST_TYPE=standalone mocha --config .mocharc-standalone.js",
"test:fast": "npm run build:fast && npm run test:source",
"test:debug": "cross-env BALENA_CLI_TEST_TYPE=source mocha --inspect-brk=0.0.0.0",
"test:only": "npm run build:fast && cross-env BALENA_CLI_TEST_TYPE=source mocha \"tests/**/${npm_config_test}.spec.ts\"",
"catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted",
"ci": "npm run test && npm run catch-uncommitted",
@ -185,7 +187,7 @@
"publish-release": "^1.6.1",
"rewire": "^5.0.0",
"simple-git": "^2.40.0",
"sinon": "^11.1.1",
"sinon": "^11.1.2",
"ts-node": "^10.0.0",
"typescript": "^4.3.5"
},
@ -194,7 +196,7 @@
"@balena/es-version": "^1.0.0",
"@oclif/command": "^1.8.0",
"@resin.io/valid-email": "^0.1.0",
"@sentry/node": "^5.25.0",
"@sentry/node": "^6.13.2",
"@types/fast-levenshtein": "0.0.1",
"@types/update-notifier": "^4.1.1",
"@zeit/dockerignore": "0.0.3",
@ -204,11 +206,11 @@
"balena-errors": "^4.7.1",
"balena-image-fs": "^7.0.6",
"balena-image-manager": "^7.0.3",
"balena-preload": "^10.4.20",
"balena-release": "^3.0.0",
"balena-sdk": "^15.36.0",
"balena-preload": "^10.5.0",
"balena-release": "^3.2.0",
"balena-sdk": "^15.51.1",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6",
"balena-settings-client": "^4.0.7",
"balena-settings-storage": "^7.0.0",
"balena-sync": "^11.0.2",
"bluebird": "^3.7.2",
@ -236,6 +238,7 @@
"glob": "^7.1.7",
"global-agent": "^2.1.12",
"global-tunnel-ng": "^2.1.1",
"got": "^11.8.2",
"humanize": "0.0.9",
"ignore": "^5.1.8",
"inquirer": "^7.3.3",
@ -246,10 +249,10 @@
"livepush": "^3.5.0",
"lodash": "^4.17.21",
"minimatch": "^3.0.4",
"mixpanel": "^0.10.3",
"moment": "^2.27.0",
"moment-duration-format": "^2.3.2",
"ndjson": "^2.0.0",
"net-keepalive": "^3.0.0",
"node-cleanup": "^2.1.2",
"node-unzip-2": "^0.2.8",
"oclif": "^1.18.1",
@ -262,9 +265,9 @@
"request": "^2.88.2",
"resin-cli-form": "^2.0.2",
"resin-cli-visuals": "^1.8.0",
"resin-compose-parse": "^2.1.3",
"resin-compose-parse": "^2.3.0",
"resin-doodles": "^0.1.1",
"resin-multibuild": "^4.11.0",
"resin-multibuild": "^4.12.2",
"resin-stream-logger": "^0.1.2",
"rimraf": "^3.0.2",
"semver": "^7.3.2",
@ -283,10 +286,9 @@
"window-size": "^1.1.0"
},
"optionalDependencies": {
"net-keepalive": "^2.0.3",
"windosu": "^0.3.0"
},
"versionist": {
"publishedAt": "2021-08-09T11:15:24.420Z"
"publishedAt": "2021-10-05T09:04:41.226Z"
}
}

View File

@ -16,7 +16,7 @@
*/
import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
const HELP_MESSAGE = '';

View File

@ -21,10 +21,11 @@ import mock = require('mock-require');
import { promises as fs } from 'fs';
import * as path from 'path';
import { stripIndent } from '../../lib/utils/lazy';
import { BalenaAPIMock } from '../balena-api-mock';
import { stripIndent } from '../../build/utils/lazy';
import { isV13 } from '../../build/utils/version';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
import { DockerMock, dockerResponsePath } from '../docker-mock';
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
import { cleanOutput, runCommand } from '../helpers';
import {
ExpectedTarStreamFiles,
@ -52,6 +53,16 @@ const commonQueryParams = {
labels: '',
};
const commonQueryParamsIntel = {
...commonQueryParams,
platform: 'linux/amd64',
};
const commonQueryParamsArmV6 = {
...commonQueryParams,
platform: 'linux/arm/v6',
};
const commonComposeQueryParams = {
t: '${tag}',
buildargs: {
@ -61,8 +72,10 @@ const commonComposeQueryParams = {
labels: '',
};
const hr =
'----------------------------------------------------------------------';
const commonComposeQueryParamsIntel = {
...commonComposeQueryParams,
platform: 'linux/amd64',
};
// "itSS" means "it() Skip Standalone"
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
@ -78,7 +91,7 @@ describe('balena build', function () {
api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true });
docker.expectGetPing();
docker.expectGetVersion();
docker.expectGetVersion({ persist: true });
});
this.afterEach(() => {
@ -125,11 +138,16 @@ describe('balena build', function () {
}
}
docker.expectGetInfo({});
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -g`,
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 ${
isV13() ? '' : '-g'
}`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: Object.entries(commonQueryParams) },
expectedQueryParamsByService: {
main: Object.entries(commonQueryParamsIntel),
},
expectedResponseLines,
projectPath,
responseBody,
@ -152,7 +170,7 @@ describe('balena build', function () {
'Dockerfile-alt': { fileSize: 30, type: 'file' },
};
const expectedQueryParams = {
...commonQueryParams,
...commonQueryParamsIntel,
buildargs: '{"BARG1":"b1","barg2":"B2"}',
cachefrom: '["my/img1","my/img2"]',
};
@ -181,6 +199,7 @@ describe('balena build', function () {
}
}
docker.expectGetInfo({});
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -B BARG1=b1 -B barg2=B2 --cache-from my/img1,my/img2`,
dockerMock: docker,
@ -271,12 +290,15 @@ describe('balena build', function () {
});
mock.reRequire('../../build/utils/qemu');
docker.expectGetInfo({ OperatingSystem: 'balenaOS 2.44.0+rev1' });
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `build ${projectPath} --emulated --deviceType ${deviceType} --arch ${arch} --nogitignore`,
commandLine: `build ${projectPath} --emulated --deviceType ${deviceType} --arch ${arch} ${
isV13() ? '' : '--nogitignore'
}`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: {
main: Object.entries(commonQueryParams),
main: Object.entries(commonQueryParamsArmV6),
},
expectedResponseLines,
projectPath,
@ -325,11 +347,15 @@ describe('balena build', function () {
);
}
docker.expectGetInfo({});
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --noconvert-eol -m`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: Object.entries(commonQueryParams) },
expectedQueryParamsByService: {
main: Object.entries(commonQueryParamsIntel),
},
expectedResponseLines,
projectPath,
responseBody,
@ -358,7 +384,7 @@ describe('balena build', function () {
},
service2: {
'.dockerignore': { fileSize: 12, type: 'file' },
'Dockerfile-alt': { fileSize: 40, type: 'file' },
'Dockerfile-alt': { fileSize: 13, type: 'file' },
'file2-crlf.sh': {
fileSize: isWindows ? 12 : 14,
testStream: isWindows ? expectStreamNoCRLF : undefined,
@ -384,7 +410,7 @@ describe('balena build', function () {
}),
),
service2: Object.entries(
_.merge({}, commonComposeQueryParams, {
_.merge({}, commonComposeQueryParamsIntel, {
buildargs: {
COMPOSE_ARG: 'A',
barg: 'b',
@ -415,8 +441,12 @@ describe('balena build', function () {
);
}
docker.expectGetInfo({});
docker.expectGetManifestNucAlpine();
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -G -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2`,
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol ${
isV13() ? '' : '-G'
} -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2`,
dockerMock: docker,
expectedFilesByService,
expectedQueryParamsByService,
@ -449,7 +479,7 @@ describe('balena build', function () {
},
service2: {
'.dockerignore': { fileSize: 12, type: 'file' },
'Dockerfile-alt': { fileSize: 40, type: 'file' },
'Dockerfile-alt': { fileSize: 13, type: 'file' },
'file2-crlf.sh': {
fileSize: isWindows ? 12 : 14,
testStream: isWindows ? expectStreamNoCRLF : undefined,
@ -469,7 +499,7 @@ describe('balena build', function () {
}),
),
service2: Object.entries(
_.merge({}, commonComposeQueryParams, {
_.merge({}, commonComposeQueryParamsIntel, {
buildargs: {
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
},
@ -484,11 +514,11 @@ describe('balena build', function () {
'[Build] service2 Step 1/4 : FROM busybox',
],
...[
`[Info] ${hr}`,
`[Info] ---------------------------------------------------------------------------`,
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
'[Info] found at the project source (root) directory. Note that this file will not',
'[Info] be used to filter service subdirectories. See "balena help build".',
`[Info] ${hr}`,
`[Info] ---------------------------------------------------------------------------`,
],
];
if (isWindows) {
@ -501,6 +531,9 @@ describe('balena build', function () {
);
}
docker.expectGetInfo({});
docker.expectGetManifestBusybox();
docker.expectGetManifestNucAlpine();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m`,
dockerMock: docker,
@ -513,6 +546,99 @@ describe('balena build', function () {
services: ['service1', 'service2'],
});
});
it('should create the expected tar stream (--projectName and --tag)', async () => {
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
const service1Dockerfile = (
await fs.readFile(
path.join(projectPath, 'service1', 'Dockerfile.template'),
'utf8',
)
).replace('%%BALENA_MACHINE_NAME%%', 'nuc');
const expectedFilesByService: ExpectedTarStreamFilesByService = {
service1: {
Dockerfile: {
contents: service1Dockerfile,
fileSize: service1Dockerfile.length,
type: 'file',
},
'Dockerfile.template': { fileSize: 144, type: 'file' },
'file1.sh': { fileSize: 12, type: 'file' },
'test-ignore.txt': { fileSize: 12, type: 'file' },
},
service2: {
'.dockerignore': { fileSize: 12, type: 'file' },
'Dockerfile-alt': { fileSize: 13, type: 'file' },
'file2-crlf.sh': {
fileSize: isWindows ? 12 : 14,
testStream: isWindows ? expectStreamNoCRLF : undefined,
type: 'file',
},
},
};
const responseFilename = 'build-POST.json';
const responseBody = await fs.readFile(
path.join(dockerResponsePath, responseFilename),
'utf8',
);
const expectedQueryParamsByService = {
service1: Object.entries(
_.merge({}, commonComposeQueryParams, {
buildargs: { SERVICE1_VAR: 'This is a service specific variable' },
}),
),
service2: Object.entries(
_.merge({}, commonComposeQueryParamsIntel, {
buildargs: {
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
},
dockerfile: 'Dockerfile-alt',
}),
),
};
const expectedResponseLines: string[] = [
...commonResponseLines[responseFilename],
...[
'[Build] service1 Step 1/4 : FROM busybox',
'[Build] service2 Step 1/4 : FROM busybox',
],
...[
`[Info] ---------------------------------------------------------------------------`,
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
'[Info] found at the project source (root) directory. Note that this file will not',
'[Info] be used to filter service subdirectories. See "balena help build".',
`[Info] ---------------------------------------------------------------------------`,
],
];
if (isWindows) {
expectedResponseLines.push(
`[Info] Converting line endings CRLF -> LF for file: ${path.join(
projectPath,
'service2',
'file2-crlf.sh',
)}`,
);
}
const projectName = 'spectest';
const tag = 'myTag';
docker.expectGetInfo({});
docker.expectGetManifestBusybox();
docker.expectGetManifestNucAlpine();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m --tag ${tag} --projectName ${projectName}`,
dockerMock: docker,
expectedFilesByService,
expectedQueryParamsByService,
expectedResponseLines,
projectName,
projectPath,
responseBody,
responseCode: 200,
services: ['service1', 'service2'],
tag,
});
});
});
describe('balena build: project validation', function () {

View File

@ -15,15 +15,18 @@
* limitations under the License.
*/
import type { Request as ReleaseRequest } from 'balena-release';
import { expect } from 'chai';
import { promises as fs } from 'fs';
import * as _ from 'lodash';
import * as nock from 'nock';
import * as path from 'path';
import * as sinon from 'sinon';
import { BalenaAPIMock } from '../balena-api-mock';
import { isV13 } from '../../build/utils/version';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
import { DockerMock, dockerResponsePath } from '../docker-mock';
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
import { cleanOutput, runCommand, switchSentry } from '../helpers';
import {
ExpectedTarStreamFiles,
@ -50,6 +53,7 @@ const commonResponseLines = {
};
const commonQueryParams = [
['platform', 'linux/arm/v7'],
['t', '${tag}'],
['buildargs', '{}'],
['labels', ''],
@ -64,8 +68,10 @@ const commonComposeQueryParams = {
labels: '',
};
const hr =
'----------------------------------------------------------------------';
const commonComposeQueryParamsArmV7 = {
...commonComposeQueryParams,
platform: 'linux/arm/v7',
};
describe('balena deploy', function () {
let api: BalenaAPIMock;
@ -77,9 +83,7 @@ describe('balena deploy', function () {
docker = new DockerMock();
api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true });
api.expectGetConfigDeviceTypes();
api.expectGetApplication();
api.expectPostRelease();
api.expectGetApplication({ expandArchitecture: true });
api.expectGetRelease();
api.expectGetUser();
api.expectGetService({ serviceName: 'main' });
@ -137,12 +141,115 @@ describe('balena deploy', function () {
);
}
api.expectPostRelease({});
api.expectPatchImage({});
api.expectPatchRelease({});
api.expectPostImageLabel();
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `deploy testApp --build --source ${projectPath} -G`,
commandLine: `deploy testApp --build --source ${projectPath} ${
isV13() ? '' : '-G'
}`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams },
expectedResponseLines,
projectPath,
responseBody,
responseCode: 200,
services: ['main'],
});
});
it('should handle the contract and final status for a final (non-draft) release', async () => {
const projectPath = path.join(
projectsPath,
'no-docker-compose',
'with-contract',
);
const expectedFiles: ExpectedTarStreamFiles = {
'src/start.sh': { fileSize: 30, type: 'file' },
Dockerfile: { fileSize: 88, type: 'file' },
'balena.yml': { fileSize: 55, type: 'file' },
};
const responseFilename = 'build-POST.json';
const responseBody = await fs.readFile(
path.join(dockerResponsePath, responseFilename),
'utf8',
);
const expectedResponseLines = [
...commonResponseLines[responseFilename],
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
`[Info] Creating default composition with source: "${projectPath}"`,
];
api.expectPostRelease({
inspectRequest: (_uri: string, requestBody: nock.Body) => {
const body = requestBody.valueOf() as Partial<ReleaseRequest>;
expect(body.contract).to.be.equal(
'{"name":"testContract","type":"sw.application","version":"1.5.2"}',
);
expect(body.is_final).to.be.true;
},
});
api.expectPatchImage({});
api.expectPatchRelease({});
api.expectPostImageLabel();
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `deploy testApp --build --source ${projectPath}`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams },
expectedResponseLines,
projectPath,
responseBody,
responseCode: 200,
services: ['main'],
});
});
it('should handle the contract and final status for a draft release', async () => {
const projectPath = path.join(
projectsPath,
'no-docker-compose',
'with-contract',
);
const expectedFiles: ExpectedTarStreamFiles = {
'src/start.sh': { fileSize: 30, type: 'file' },
Dockerfile: { fileSize: 88, type: 'file' },
'balena.yml': { fileSize: 55, type: 'file' },
};
const responseFilename = 'build-POST.json';
const responseBody = await fs.readFile(
path.join(dockerResponsePath, responseFilename),
'utf8',
);
const expectedResponseLines = [
...commonResponseLines[responseFilename],
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
`[Info] Creating default composition with source: "${projectPath}"`,
];
api.expectPostRelease({
inspectRequest: (_uri: string, requestBody: nock.Body) => {
const body = requestBody.valueOf() as Partial<ReleaseRequest>;
expect(body.contract).to.be.equal(
'{"name":"testContract","type":"sw.application","version":"1.5.2"}',
);
expect(body.semver).to.be.equal('1.5.2');
expect(body.is_final).to.be.false;
},
});
api.expectPatchImage({});
api.expectPatchRelease({});
api.expectPostImageLabel();
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `deploy testApp --build --draft --source ${projectPath}`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams },
@ -176,6 +283,9 @@ describe('balena deploy', function () {
// causes the CLI to call process.exit() with process.exitCode = 1
const expectedExitCode = 1;
api.expectPostRelease({});
docker.expectGetManifestBusybox();
// Mock this patch HTTP request to return status code 500, in which case
// the release status should be saved as "failed" rather than "success"
api.expectPatchImage({
@ -204,7 +314,9 @@ describe('balena deploy', function () {
sinon.stub(process, 'exit');
await testDockerBuildStream({
commandLine: `deploy testApp --build --source ${projectPath} --noconvert-eol -G`,
commandLine: `deploy testApp --build --source ${projectPath} --noconvert-eol ${
isV13() ? '' : '-G'
}`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams },
@ -250,7 +362,7 @@ describe('balena deploy', function () {
},
service2: {
'.dockerignore': { fileSize: 12, type: 'file' },
'Dockerfile-alt': { fileSize: 40, type: 'file' },
'Dockerfile-alt': { fileSize: 13, type: 'file' },
'file2-crlf.sh': {
fileSize: isWindows ? 12 : 14,
testStream: isWindows ? expectStreamNoCRLF : undefined,
@ -270,7 +382,7 @@ describe('balena deploy', function () {
}),
),
service2: Object.entries(
_.merge({}, commonComposeQueryParams, {
_.merge({}, commonComposeQueryParamsArmV7, {
buildargs: {
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
},
@ -285,11 +397,11 @@ describe('balena deploy', function () {
'[Build] service2 Step 1/4 : FROM busybox',
],
...[
`[Info] ${hr}`,
`[Info] ---------------------------------------------------------------------------`,
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
'[Info] found at the project source (root) directory. Note that this file will not',
'[Info] be used to filter service subdirectories. See "balena help deploy".',
`[Info] ${hr}`,
`[Info] ---------------------------------------------------------------------------`,
],
];
if (isWindows) {
@ -302,9 +414,11 @@ describe('balena deploy', function () {
);
}
// docker.expectGetImages();
api.expectPostRelease({});
api.expectPatchImage({});
api.expectPatchRelease({});
docker.expectGetManifestRpi3Alpine();
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `deploy testApp --build --source ${projectPath} --multi-dockerignore`,

View File

@ -16,7 +16,7 @@
*/
import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
describe('balena device move', function () {

View File

@ -18,7 +18,7 @@
import { expect } from 'chai';
import * as path from 'path';
import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock';
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';

View File

@ -18,7 +18,7 @@
import { expect } from 'chai';
import * as path from 'path';
import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock';
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
@ -59,8 +59,9 @@ describe('balena devices', function () {
const lines = cleanOutput(out);
expect(lines[0].replace(/ +/g, ' ')).to.equal(
'ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS ' +
'IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
isV13()
? 'ID UUID DEVICE NAME DEVICE TYPE FLEET STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL'
: 'ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
);
expect(lines).to.have.lengthOf.at.least(2);

View File

@ -17,10 +17,10 @@
import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { isV13 } from '../../../lib/utils/version';
import { isV13 } from '../../../build/utils/version';
describe('balena devices supported', function () {
let api: BalenaAPIMock;

View File

@ -17,7 +17,7 @@
import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { runCommand } from '../../helpers';
describe('balena env add', function () {

View File

@ -16,16 +16,12 @@
*/
import { expect } from 'chai';
import { stripIndent } from '../../../lib/utils/lazy';
import { stripIndent } from '../../../build/utils/lazy';
import { BalenaAPIMock } from '../../balena-api-mock';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { runCommand } from '../../helpers';
import {
appToFleetFlagMsg,
appToFleetOutputMsg,
warnify,
} from '../../../build/utils/messages';
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
import { isV13 } from '../../../build/utils/version';
describe('balena envs', function () {
@ -48,13 +44,6 @@ describe('balena envs', function () {
api.done();
});
const appToFleetFlagWarn =
!isV13() &&
process.stderr.isTTY &&
process.env.BALENA_CLI_TEST_TYPE !== 'standalone'
? warnify(appToFleetFlagMsg) + '\n'
: '';
const appToFleetOutputWarn =
!isV13() &&
process.stderr.isTTY &&
@ -67,18 +56,18 @@ describe('balena envs', function () {
api.expectGetAppEnvVars();
api.expectGetAppServiceVars();
const { out, err } = await runCommand(`envs -a ${appName}`);
const { out, err } = await runCommand(`envs -f ${appName}`);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE APPLICATION SERVICE
120110 svar1 svar1-value test service1
120111 svar2 svar2-value test service2
120101 var1 var1-val test *
120102 var2 22 test *
ID NAME VALUE FLEET SERVICE
120110 svar1 svar1-value test service1
120111 svar2 svar2-value test service2
120101 var1 var1-val test *
120102 var2 22 test *
` + '\n',
);
expect(err.join('')).to.equal(appToFleetFlagWarn);
expect(err.join('')).to.equal('');
});
it('should successfully list config vars for a test fleet', async () => {
@ -101,11 +90,11 @@ describe('balena envs', function () {
api.expectGetApplication();
api.expectGetAppConfigVars();
const { out, err } = await runCommand(`envs -cja ${appName}`);
const { out, err } = await runCommand(`envs -cjf ${appName}`);
expect(JSON.parse(out.join(''))).to.deep.equal([
{
appName: 'test',
fleetName: 'test',
id: 120300,
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
value: 'false',
@ -122,18 +111,18 @@ describe('balena envs', function () {
api.expectGetAppServiceVars();
const { out, err } = await runCommand(
`envs -a ${appName} -s ${serviceName}`,
`envs -f ${appName} -s ${serviceName}`,
);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE APPLICATION SERVICE
120111 svar2 svar2-value test service2
120101 var1 var1-val test *
120102 var2 22 test *
ID NAME VALUE FLEET SERVICE
120111 svar2 svar2-value test service2
120101 var1 var1-val test *
120102 var2 22 test *
` + '\n',
);
expect(err.join('')).to.equal(appToFleetFlagWarn);
expect(err.join('')).to.equal('');
});
it('should successfully list env and service vars for a test fleet (-s flags)', async () => {
@ -144,18 +133,18 @@ describe('balena envs', function () {
api.expectGetAppServiceVars();
const { out, err } = await runCommand(
`envs -a ${appName} -s ${serviceName}`,
`envs -f ${appName} -s ${serviceName}`,
);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE APPLICATION SERVICE
120110 svar1 svar1-value test ${serviceName}
120101 var1 var1-val test *
120102 var2 22 test *
ID NAME VALUE FLEET SERVICE
120110 svar1 svar1-value test ${serviceName}
120101 var1 var1-val test *
120102 var2 22 test *
` + '\n',
);
expect(err.join('')).to.equal(appToFleetFlagWarn);
expect(err.join('')).to.equal('');
});
it('should successfully list env variables for a test device', async () => {
@ -167,22 +156,29 @@ describe('balena envs', function () {
api.expectGetDeviceServiceVars();
const uuid = shortUUID;
const { out, err } = await runCommand(`envs -d ${uuid}`);
expect(out.join('')).to.equal(
const result = await runCommand(`envs -d ${uuid}`);
const { err } = result;
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE
120110 svar1 svar1-value test * service1
120111 svar2 svar2-value test * service2
120120 svar3 svar3-value test ${uuid} service1
120121 svar4 svar4-value test ${uuid} service2
120101 var1 var1-val test * *
120102 var2 22 test * *
120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} *
` + '\n',
);
ID NAME VALUE APPLICATION DEVICE SERVICE
120110 svar1 svar1-value test * service1
120111 svar2 svar2-value test * service2
120120 svar3 svar3-value test ${uuid} service1
120121 svar4 svar4-value test ${uuid} service2
120101 var1 var1-val test * *
120102 var2 22 test * *
120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} *
` + '\n';
if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn);
});
@ -195,9 +191,7 @@ describe('balena envs', function () {
api.expectGetDeviceServiceVars();
const { out, err } = await runCommand(`envs -jd ${shortUUID}`);
expect(JSON.parse(out.join(''))).to.deep.equal(
JSON.parse(`[
let expected = `[
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "service1" },
@ -206,9 +200,14 @@ describe('balena envs', function () {
{ "id": 120121, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar4", "value": "svar4-value", "serviceName": "service2" },
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
]`),
);
]`;
if (isV13()) {
expected = expected.replace(
/"appName": "test"/g,
'"fleetName": "org/test"',
);
}
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(err.join('')).to.equal('');
});
@ -218,16 +217,23 @@ describe('balena envs', function () {
api.expectGetApplication();
api.expectGetAppConfigVars();
const { out, err } = await runCommand(`envs -d ${shortUUID} --config`);
expect(out.join('')).to.equal(
const result = await runCommand(`envs -d ${shortUUID} --config`);
const { err } = result;
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE APPLICATION DEVICE
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test *
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 test ${shortUUID}
` + '\n',
);
ID NAME VALUE APPLICATION DEVICE
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test *
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 test ${shortUUID}
` + '\n';
if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn);
});
@ -242,20 +248,27 @@ describe('balena envs', function () {
api.expectGetDeviceEnvVars();
const uuid = shortUUID;
const { out, err } = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
expect(out.join('')).to.equal(
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
const { err } = result;
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE
120111 svar2 svar2-value test * service2
120121 svar4 svar4-value test ${uuid} service2
120101 var1 var1-val test * *
120102 var2 22 test * *
120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} *
` + '\n',
);
ID NAME VALUE APPLICATION DEVICE SERVICE
120111 svar2 svar2-value test * service2
120121 svar4 svar4-value test ${uuid} service2
120101 var1 var1-val test * *
120102 var2 22 test * *
120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} *
` + '\n';
if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn);
});
@ -265,18 +278,24 @@ describe('balena envs', function () {
api.expectGetDeviceServiceVars();
const uuid = shortUUID;
const { out, err } = await runCommand(`envs -d ${uuid}`);
expect(out.join('')).to.equal(
const result = await runCommand(`envs -d ${uuid}`);
const { err } = result;
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE
120120 svar3 svar3-value N/A ${uuid} service1
120121 svar4 svar4-value N/A ${uuid} service2
120203 var3 var3-val N/A ${uuid} *
120204 var4 44 N/A ${uuid} *
` + '\n',
);
` + '\n';
if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ');
}
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn);
});
@ -291,19 +310,27 @@ describe('balena envs', function () {
api.expectGetDeviceServiceVars();
const uuid = shortUUID;
const { out, err } = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
expect(out.join('')).to.equal(
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
const { err } = result;
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE
120110 svar1 svar1-value test * ${serviceName}
120120 svar3 svar3-value test ${uuid} ${serviceName}
120101 var1 var1-val test * *
120102 var2 22 test * *
120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} *
` + '\n',
);
ID NAME VALUE APPLICATION DEVICE SERVICE
120110 svar1 svar1-value test * ${serviceName}
120120 svar3 svar3-value test ${uuid} ${serviceName}
120101 var1 var1-val test * *
120102 var2 22 test * *
120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} *
` + '\n';
if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn);
});
@ -320,17 +347,21 @@ describe('balena envs', function () {
const { out, err } = await runCommand(
`envs -d ${shortUUID} -js ${serviceName}`,
);
expect(JSON.parse(out.join(''))).to.deep.equal(
JSON.parse(`[
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" },
{ "id": 120120, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "${serviceName}" },
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
]`),
);
let expected = `[
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" },
{ "id": 120120, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "${serviceName}" },
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
]`;
if (isV13()) {
expected = expected.replace(
/"appName": "test"/g,
'"fleetName": "org/test"',
);
}
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(err.join('')).to.equal('');
});
});

View File

@ -17,7 +17,7 @@
import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { runCommand } from '../../helpers';
describe('balena env rename', function () {

View File

@ -17,7 +17,7 @@
import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { runCommand } from '../../helpers';
describe('balena env rm', function () {

View File

@ -17,7 +17,7 @@
import { expect } from 'chai';
import { BalenaAPIMock } from '../balena-api-mock';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../helpers';
import * as messages from '../../build/utils/messages';

View File

@ -17,9 +17,9 @@
import { expect } from 'chai';
import { BalenaAPIMock } from '../balena-api-mock';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../helpers';
import { SupervisorMock } from '../supervisor-mock';
import { SupervisorMock } from '../nock/supervisor-mock';
const itS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it : it.skip;

View File

@ -25,7 +25,7 @@ import * as tmp from 'tmp';
tmp.setGracefulCleanup();
const tmpNameAsync = promisify(tmp.tmpName);
import { BalenaAPIMock } from '../../balena-api-mock';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
if (process.platform !== 'win32') {
describe('balena os configure', function () {

View File

@ -19,8 +19,9 @@ import { expect } from 'chai';
import { promises as fs } from 'fs';
import * as path from 'path';
import { BalenaAPIMock } from '../balena-api-mock';
import { BuilderMock, builderResponsePath } from '../builder-mock';
import { isV13 } from '../../build/utils/version';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { BuilderMock, builderResponsePath } from '../nock/builder-mock';
import { expectStreamNoCRLF, testPushBuildStream } from '../docker-build';
import { cleanOutput, runCommand } from '../helpers';
import {
@ -34,6 +35,8 @@ import {
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
const itNoV13 = isV13() ? it.skip : it;
const commonResponseLines = {
'build-POST-v3.json': [
'[Info] Starting build for testApp, user gh_user',
@ -73,6 +76,7 @@ const commonQueryParams = [
['emulated', 'false'],
['nocache', 'false'],
['headless', 'false'],
['isdraft', 'false'],
];
const hr =
@ -233,71 +237,74 @@ describe('balena push', function () {
});
});
it('should create the expected tar stream (single container, --gitignore)', async () => {
const projectPath = path.join(
projectsPath,
'no-docker-compose',
'dockerignore1',
);
const expectedFiles: ExpectedTarStreamFiles = {
'.balena/balena.yml': { fileSize: 12, type: 'file' },
'.dockerignore': { fileSize: 438, type: 'file' },
'.gitignore': { fileSize: 20, type: 'file' },
'.git/bar.txt': { fileSize: 4, type: 'file' },
'.git/foo.txt': { fileSize: 4, type: 'file' },
'c.txt': { fileSize: 1, type: 'file' },
Dockerfile: { fileSize: 13, type: 'file' },
'src/.balena/balena.yml': { fileSize: 16, type: 'file' },
'src/.gitignore': { fileSize: 10, type: 'file' },
'vendor/.git/vendor-git-contents': { fileSize: 20, type: 'file' },
// When --gitignore (-g) is provided for v11 compatibility, the old
// `zeit/dockerignore` npm package is still used but it is broken on
// Windows (reason why we created `@balena/dockerignore`).
...(isWindows
? {
'src/src-b.txt': { fileSize: 5, type: 'file' },
'dot.git/bar.txt': { fileSize: 4, type: 'file' },
'dot.git/foo.txt': { fileSize: 4, type: 'file' },
'vendor/dot.git/vendor-git-contents': {
fileSize: 20,
type: 'file',
},
}
: {}),
};
itNoV13(
'should create the expected tar stream (single container, --gitignore)',
async () => {
const projectPath = path.join(
projectsPath,
'no-docker-compose',
'dockerignore1',
);
const expectedFiles: ExpectedTarStreamFiles = {
'.balena/balena.yml': { fileSize: 12, type: 'file' },
'.dockerignore': { fileSize: 438, type: 'file' },
'.gitignore': { fileSize: 20, type: 'file' },
'.git/bar.txt': { fileSize: 4, type: 'file' },
'.git/foo.txt': { fileSize: 4, type: 'file' },
'c.txt': { fileSize: 1, type: 'file' },
Dockerfile: { fileSize: 13, type: 'file' },
'src/.balena/balena.yml': { fileSize: 16, type: 'file' },
'src/.gitignore': { fileSize: 10, type: 'file' },
'vendor/.git/vendor-git-contents': { fileSize: 20, type: 'file' },
// When --gitignore (-g) is provided for v11 compatibility, the old
// `zeit/dockerignore` npm package is still used but it is broken on
// Windows (reason why we created `@balena/dockerignore`).
...(isWindows
? {
'src/src-b.txt': { fileSize: 5, type: 'file' },
'dot.git/bar.txt': { fileSize: 4, type: 'file' },
'dot.git/foo.txt': { fileSize: 4, type: 'file' },
'vendor/dot.git/vendor-git-contents': {
fileSize: 20,
type: 'file',
},
}
: {}),
};
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
const responseFilename = 'build-POST-v3.json';
const responseBody = await fs.readFile(
path.join(builderResponsePath, responseFilename),
'utf8',
);
const expectedResponseLines = [
...[
`[Warn] ${hr}`,
'[Warn] Using file ignore patterns from:',
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
`[Warn] * ${path.join(projectPath, '.gitignore')}`,
`[Warn] * ${path.join(projectPath, 'src', '.gitignore')}`,
'[Warn] .gitignore files are being considered because the --gitignore option was used.',
'[Warn] This option is deprecated and will be removed in the next major version release.',
"[Warn] For more information, see 'balena help push'.",
`[Warn] ${hr}`,
],
...commonResponseLines[responseFilename],
];
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
const responseFilename = 'build-POST-v3.json';
const responseBody = await fs.readFile(
path.join(builderResponsePath, responseFilename),
'utf8',
);
const expectedResponseLines = [
...[
`[Warn] ${hr}`,
'[Warn] Using file ignore patterns from:',
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
`[Warn] * ${path.join(projectPath, '.gitignore')}`,
`[Warn] * ${path.join(projectPath, 'src', '.gitignore')}`,
'[Warn] .gitignore files are being considered because the --gitignore option was used.',
'[Warn] This option is deprecated and will be removed in the next major version release.',
"[Warn] For more information, see 'balena help push'.",
`[Warn] ${hr}`,
],
...commonResponseLines[responseFilename],
];
await testPushBuildStream({
builderMock: builder,
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -g`,
expectedFiles,
expectedQueryParams: commonQueryParams,
expectedResponseLines,
projectPath,
responseBody,
responseCode: 200,
});
});
await testPushBuildStream({
builderMock: builder,
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -g`,
expectedFiles,
expectedQueryParams: commonQueryParams,
expectedResponseLines,
projectPath,
responseBody,
responseCode: 200,
});
},
);
it('should create the expected tar stream (single container, --nogitignore)', async () => {
const projectPath = path.join(
@ -369,24 +376,27 @@ describe('balena push', function () {
path.join(builderResponsePath, responseFilename),
'utf8',
);
const expectedResponseLines = isWindows
? [
`[Warn] ${hr}`,
'[Warn] Using file ignore patterns from:',
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
'[Warn] The --gitignore option was used, but no .gitignore files were found.',
'[Warn] The --gitignore option is deprecated and will be removed in the next major',
'[Warn] version release. It prevents the use of a better dockerignore parser and',
'[Warn] filter library that fixes several issues on Windows and improves compatibility',
"[Warn] with 'docker build'. For more information, see 'balena help push'.",
`[Warn] ${hr}`,
...commonResponseLines[responseFilename],
]
: commonResponseLines[responseFilename];
const expectedResponseLines =
!isV13() && isWindows
? [
`[Warn] ${hr}`,
'[Warn] Using file ignore patterns from:',
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
'[Warn] The --gitignore option was used, but no .gitignore files were found.',
'[Warn] The --gitignore option is deprecated and will be removed in the next major',
'[Warn] version release. It prevents the use of a better dockerignore parser and',
'[Warn] filter library that fixes several issues on Windows and improves compatibility',
"[Warn] with 'docker build'. For more information, see 'balena help push'.",
`[Warn] ${hr}`,
...commonResponseLines[responseFilename],
]
: commonResponseLines[responseFilename];
await testPushBuildStream({
builderMock: builder,
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} --gitignore`,
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} ${
isV13() ? '' : '--gitignore'
}`,
expectedFiles,
expectedQueryParams: commonQueryParams,
expectedResponseLines,
@ -445,7 +455,7 @@ describe('balena push', function () {
'docker-compose.yml': { fileSize: 332, type: 'file' },
'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
'service1/file1.sh': { fileSize: 12, type: 'file' },
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' },
'service2/Dockerfile-alt': { fileSize: 13, type: 'file' },
'service2/.dockerignore': { fileSize: 12, type: 'file' },
'service2/file2-crlf.sh': {
fileSize: isWindows ? 12 : 14,
@ -498,7 +508,7 @@ describe('balena push', function () {
'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
'service1/file1.sh': { fileSize: 12, type: 'file' },
'service1/test-ignore.txt': { fileSize: 12, type: 'file' },
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' },
'service2/Dockerfile-alt': { fileSize: 13, type: 'file' },
'service2/.dockerignore': { fileSize: 12, type: 'file' },
'service2/file2-crlf.sh': {
fileSize: isWindows ? 12 : 14,
@ -515,11 +525,11 @@ describe('balena push', function () {
const expectedResponseLines: string[] = [
...commonResponseLines[responseFilename],
...[
`[Info] ${hr}`,
`[Info] ---------------------------------------------------------------------------`,
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
'[Info] found at the project source (root) directory. Note that this file will not',
'[Info] be used to filter service subdirectories. See "balena help push".',
`[Info] ${hr}`,
`[Info] ---------------------------------------------------------------------------`,
],
];
if (isWindows) {
@ -570,7 +580,7 @@ describe('balena push: project validation', function () {
];
const { out, err } = await runCommand(
`push testApp --source ${projectPath} --gitignore`,
`push testApp --source ${projectPath}`,
);
expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
expect(out).to.be.empty;

View File

@ -0,0 +1,68 @@
/**
* @license
* Copyright 2019-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect } from 'chai';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../helpers';
describe('balena release', function () {
let api: BalenaAPIMock;
beforeEach(() => {
api = new BalenaAPIMock();
api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true });
});
afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
});
it('should show release details', async () => {
api.expectGetRelease();
const { out } = await runCommand('release 27fda508c');
const lines = cleanOutput(out);
expect(lines[0]).to.contain('ID: ');
expect(lines[0]).to.contain(' 142334');
expect(lines[1]).to.contain('COMMIT: ');
expect(lines[1]).to.contain(' 90247b54de4fa7a0a3cbc85e73c68039');
});
it('should return release composition', async () => {
api.expectGetRelease();
const { out } = await runCommand('release 27fda508c --composition');
const lines = cleanOutput(out);
expect(lines[0]).to.be.equal("version: '2.1'");
expect(lines[1]).to.be.equal('networks: {}');
expect(lines[2]).to.be.equal('volumes:');
expect(lines[3]).to.be.equal('resin-data: {}');
expect(lines[4]).to.be.equal('services:');
expect(lines[5]).to.be.equal('main:');
});
it('should list releases', async () => {
api.expectGetRelease();
api.expectGetApplication();
const { out } = await runCommand('releases someapp');
const lines = cleanOutput(out);
expect(lines.length).to.be.equal(2);
expect(lines[1]).to.contain('142334');
expect(lines[1]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
});
});

View File

@ -19,7 +19,7 @@ import { expect } from 'chai';
import mock = require('mock-require');
import { createServer, Server } from 'net';
import { BalenaAPIMock } from '../balena-api-mock';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../helpers';
// "itSS" means "it() Skip Standalone"

View File

@ -17,7 +17,7 @@
import { expect } from 'chai';
import * as fs from 'fs';
import { BalenaAPIMock } from '../balena-api-mock';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { runCommand } from '../helpers';
const packageJSON = JSON.parse(fs.readFileSync('./package.json', 'utf8'));

View File

@ -19,10 +19,20 @@ import { set as setEsVersion } from '@balena/es-version';
// Set the desired es version for downstream modules that support it
setEsVersion('es2018');
// Disable Sentry.io error reporting while running test code
process.env.BALENARC_NO_SENTRY = '1';
// Disable deprecation checks while running test code
// Like the global `--unsupported` flag
process.env.BALENARC_UNSUPPORTED = '1';
import * as tmp from 'tmp';
tmp.setGracefulCleanup();
// Use a temporary dir for tests data
process.env.BALENARC_DATA_DIRECTORY = tmp.dirSync().name;
console.error(
`[debug] tests/config-tests.ts: BALENARC_DATA_DIRECTORY="${process.env.BALENARC_DATA_DIRECTORY}"`,
);
import { EventEmitter } from 'events';
EventEmitter.defaultMaxListeners = 35; // it appears that 'nock' adds a bunch of listeners - bug?
@ -33,3 +43,7 @@ import { config as chaiCfg } from 'chai';
chaiCfg.showDiff = true;
// enable diff comparison of large objects / arrays
chaiCfg.truncateThreshold = 0;
// Because mocks are pointed at "production", we need to make sure this is set to prod.
// Otherwise if the user has BALENARC_BALENA_URL pointing at something else like staging, tests
// will fail.
process.env.BALENARC_BALENA_URL = 'balena-cloud.com';

326
tests/deprecation.spec.ts Normal file
View File

@ -0,0 +1,326 @@
/**
* @license
* Copyright 2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as settings from 'balena-settings-client';
import * as getStorage from 'balena-settings-storage';
import { expect } from 'chai';
import mock = require('mock-require');
import * as semver from 'semver';
import * as sinon from 'sinon';
import * as packageJSON from '../package.json';
import {
DeprecationChecker,
ReleaseTimestampsByVersion,
} from '../build/deprecation';
import { BalenaAPIMock } from './nock/balena-api-mock';
import { NpmMock } from './nock/npm-mock';
import { runCommand, TestOutput } from './helpers';
// "itSS" means "it() Skip Standalone"
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
describe('DeprecationChecker', function () {
const sandbox = sinon.createSandbox();
const now = new Date().getTime();
const anHourAgo = now - 3600000;
const currentMajor = semver.major(packageJSON.version, { loose: true });
const nextMajorVersion = `${currentMajor + 1}.0.0`;
const dataDirectory = settings.get<string>('dataDirectory');
const storageModPath = 'balena-settings-storage';
const mockStorage = getStorage({ dataDirectory });
let api: BalenaAPIMock;
let npm: NpmMock;
let checker: DeprecationChecker;
let getStub: sinon.SinonStub<
Parameters<typeof mockStorage.get>,
ReturnType<typeof mockStorage.get>
>;
let setStub: sinon.SinonStub<
Parameters<typeof mockStorage.set>,
ReturnType<typeof mockStorage.set>
>;
let originalUnsupported: string | undefined;
this.beforeAll(() => {
// Temporarily undo settings from `tests/config-tests.ts`
originalUnsupported = process.env.BALENARC_UNSUPPORTED;
delete process.env.BALENARC_UNSUPPORTED;
});
this.afterAll(() => {
process.env.BALENARC_UNSUPPORTED = originalUnsupported;
});
this.beforeEach(() => {
npm = new NpmMock();
api = new BalenaAPIMock();
api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true });
checker = new DeprecationChecker(packageJSON.version);
getStub = sandbox.stub(mockStorage, 'get').withArgs(checker.cacheFile);
setStub = sandbox
.stub(mockStorage, 'set')
.withArgs(checker.cacheFile, sinon.match.any);
mock(storageModPath, () => mockStorage);
});
this.afterEach(() => {
// Check all expected api calls have been made and clean up.
(mockStorage.get as sinon.SinonStub).restore();
(mockStorage.set as sinon.SinonStub).restore();
// originalStorage.set.restore();
api.done();
npm.done();
mock.stop(storageModPath);
});
itSS(
'should warn if this version of the CLI is deprecated (isTTY is true)',
async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just over half a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
// Force isTTY to be true. It happens to be false (undefined) when
// the tests run on balenaCI on Windows.
const originalIsTTY = process.stderr.isTTY;
process.stderr.isTTY = true;
let result: TestOutput;
try {
result = await runCommand('version');
} finally {
process.stderr.isTTY = originalIsTTY;
}
const { out, err } = result;
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal(
checker.getDeprecationMsg(checker.deprecationDays + 1) + '\n',
);
},
);
itSS(
'should NOT warn if this version of the CLI is deprecated (isTTY is false)',
async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just over half a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
// Force isTTY to be false (undefined). It happens to be true when
// the tests run on balenaCI on macOS and Linux.
const originalIsTTY = process.stderr.isTTY;
process.stderr.isTTY = undefined;
let result: TestOutput;
try {
result = await runCommand('version');
} finally {
process.stderr.isTTY = originalIsTTY;
}
const { out, err } = result;
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal('');
},
);
itSS(
'should NOT warn with --unsupported (deprecated but not expired)',
async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just over half a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
const { out, err } = await runCommand('version --unsupported');
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(0);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.be.empty;
},
);
itSS('should exit if this version of the CLI has expired', async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just over a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.expiryDays + 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
const { out, err } = await runCommand('version');
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal('');
expect(err.join('')).to.include(
checker.getExpiryMsg(checker.expiryDays + 1) + '\n\n',
);
});
itSS('should NOT exit with --unsupported (expired version)', async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just over a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.expiryDays + 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
const { out, err } = await runCommand('--unsupported version');
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(0);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.be.empty;
});
it('should query the npm registry (empty cache file)', async () => {
npm.expectGetBalenaCli({
version: nextMajorVersion,
publishedAt: new Date().toISOString(),
});
getStub.resolves(undefined);
const { out, err } = await runCommand('version');
expect(setStub.callCount).to.equal(1);
expect(setStub.firstCall.args.length).to.equal(2);
const [name, obj] = setStub.firstCall.args;
expect(name).to.equal(checker.cacheFile);
expect(obj).to.have.property(nextMajorVersion);
const lastFetched = new Date(obj[nextMajorVersion]).getTime();
expect(lastFetched).to.be.greaterThan(anHourAgo);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal('');
});
it('should query the npm registry (not recently fetched)', async () => {
npm.expectGetBalenaCli({
version: nextMajorVersion,
publishedAt: new Date().toISOString(),
});
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: new Date(
checker.now -
(checker.majorVersionFetchIntervalDays + 1) * checker.msInDay,
).toISOString(),
};
getStub.resolves(mockCache);
const { out, err } = await runCommand('version');
expect(setStub.callCount).to.equal(1);
expect(setStub.firstCall.args.length).to.equal(2);
const [name, obj] = setStub.firstCall.args;
expect(name).to.equal(checker.cacheFile);
expect(obj).to.have.property(nextMajorVersion);
const lastFetched = new Date(obj[nextMajorVersion]).getTime();
expect(lastFetched).to.be.greaterThan(anHourAgo);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal('');
});
itSS('should NOT query the npm registry (recently fetched)', async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: new Date(
checker.now -
(checker.majorVersionFetchIntervalDays - 1) * checker.msInDay,
).toISOString(),
};
getStub.resolves(mockCache);
const { out, err } = await runCommand('version');
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal('');
});
itSS('should NOT query the npm registry (cached value)', async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just under half a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.deprecationDays - 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
const { out, err } = await runCommand('version');
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal('');
});
});

View File

@ -27,9 +27,10 @@ import * as tar from 'tar-stream';
import { streamToBuffer } from 'tar-utils';
import { URL } from 'url';
import { stripIndent } from '../lib/utils/lazy';
import { BuilderMock } from './builder-mock';
import { DockerMock } from './docker-mock';
import { makeImageName } from '../build/utils/compose_ts';
import { stripIndent } from '../build/utils/lazy';
import { BuilderMock } from './nock/builder-mock';
import { DockerMock } from './nock/docker-mock';
import {
cleanOutput,
deepJsonParse,
@ -161,22 +162,24 @@ export async function testDockerBuildStream(o: {
expectedErrorLines?: string[];
expectedExitCode?: number;
expectedResponseLines: string[];
projectName?: string; // --projectName command line flag
projectPath: string;
responseCode: number;
responseBody: string;
services: string[]; // e.g. ['main'] or ['service1', 'service2']
tag?: string; // --tag command line flag
}) {
const expectedErrorLines = deepTemplateReplace(o.expectedErrorLines || [], o);
const expectedResponseLines = deepTemplateReplace(o.expectedResponseLines, o);
for (const service of o.services) {
// tagPrefix is, for example, 'myApp' if the path is 'path/to/myApp'
const tagPrefix = o.projectPath.split(path.sep).pop();
const tag = `${tagPrefix}_${service}`;
const projectName = o.projectName || path.basename(o.projectPath);
const tag = makeImageName(projectName, service, o.tag);
const expectedFiles = o.expectedFilesByService[service];
const expectedQueryParams = deepTemplateReplace(
o.expectedQueryParamsByService[service],
{ tag, ...o },
{ ...o, tag },
);
const projectPath =
service === 'main' ? o.projectPath : path.join(o.projectPath, service);
@ -195,7 +198,7 @@ export async function testDockerBuildStream(o: {
tag,
});
if (o.commandLine.startsWith('build')) {
o.dockerMock.expectGetImages();
o.dockerMock.expectGetImages({ optional: true });
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2019-2020 Balena Ltd.
* Copyright 2019-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,36 +15,59 @@
* limitations under the License.
*/
import { execFile } from 'child_process';
import intercept = require('intercept-stdout');
import * as _ from 'lodash';
import { promises as fs } from 'fs';
import * as nock from 'nock';
import * as path from 'path';
import * as balenaCLI from '../build/app';
import * as packageJSON from '../package.json';
const balenaExe = process.platform === 'win32' ? 'balena.exe' : 'balena';
const standalonePath = path.resolve(__dirname, '..', 'build-bin', balenaExe);
interface TestOutput {
export interface TestOutput {
err: string[]; // stderr
out: string[]; // stdout
exitCode?: number; // process.exitCode
}
function matchesNodeEngineVersionWarn(msg: string) {
if (/^-----+\r?\n?$/.test(msg)) {
return true;
}
const cleanup = (line: string): string[] =>
line
.replace(/-----+/g, '')
.replace(/"\d+\.\d+\.\d+"/, '"x.y.z"')
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l);
const { getNodeEngineVersionWarn } = require('../build/utils/messages');
let nodeEngineWarn: string = getNodeEngineVersionWarn(
'x.y.z',
packageJSON.engines.node,
);
const nodeEngineWarnArray = cleanup(nodeEngineWarn);
nodeEngineWarn = nodeEngineWarnArray.join('\n');
msg = cleanup(msg).join('\n');
return msg === nodeEngineWarn || nodeEngineWarnArray.includes(msg);
}
/**
* Filter stdout / stderr lines to remove lines that start with `[debug]` and
* other lines that can be ignored for testing purposes.
* @param testOutput
*/
function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
const { matchesNodeEngineVersionWarn } =
require('../automation/utils') as typeof import('../automation/utils');
export function filterCliOutputForTests({
err,
out,
}: {
err: string[];
out: string[];
}): { err: string[]; out: string[] } {
return {
exitCode: testOutput.exitCode,
err: testOutput.err.filter(
err: err.filter(
(line: string) =>
line &&
!line.match(/\[debug\]/i) &&
// TODO stop this warning message from appearing when running
// sdk.setSharedOptions multiple times in the same process
@ -52,7 +75,7 @@ function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
!line.startsWith('WARN: disabling Sentry.io error reporting') &&
!matchesNodeEngineVersionWarn(line),
),
out: testOutput.out.filter((line: string) => !line.match(/\[debug\]/i)),
out: out.filter((line: string) => line && !line.match(/\[debug\]/i)),
};
}
@ -61,6 +84,9 @@ function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
* @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix)
*/
async function runCommandInProcess(cmd: string): Promise<TestOutput> {
const balenaCLI = await import('../build/app');
const intercept = await import('intercept-stdout');
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];
const err: string[] = [];
@ -79,18 +105,19 @@ async function runCommandInProcess(cmd: string): Promise<TestOutput> {
const unhookIntercept = intercept(stdoutHook, stderrHook);
try {
await balenaCLI.run(preArgs.concat(cmd.split(' ')), {
await balenaCLI.run(preArgs.concat(cmd.split(' ').filter((c) => c)), {
noFlush: true,
});
} finally {
unhookIntercept();
}
return filterCliOutputForTests({
err,
out,
const filtered = filterCliOutputForTests({ err, out });
return {
err: filtered.err,
out: filtered.out,
// this makes sense if `process.exit()` was stubbed with sinon
exitCode: process.exitCode,
});
};
}
/**
@ -129,10 +156,11 @@ async function runCommandInSubprocess(
// override default proxy exclusion to allow proxying of requests to 127.0.0.1
BALENARC_DO_PROXY: '127.0.0.1,localhost',
};
const { execFile } = await import('child_process');
await new Promise<void>((resolve) => {
const child = execFile(
standalonePath,
cmd.split(' '),
cmd.split(' ').filter((c) => c),
{ env: { ...process.env, ...addedEnvs } },
($error, $stdout, $stderr) => {
stderr = $stderr || '';
@ -141,11 +169,12 @@ async function runCommandInSubprocess(
// non-zero exit code. Usually this is harmless/expected, as
// the CLI child process is tested for error conditions.
if ($error && process.env.DEBUG) {
console.error(`
[debug] Error (possibly expected) executing child CLI process "${standalonePath}"
------------------------------------------------------------------
${$error}
------------------------------------------------------------------`);
const msg = `
Error (possibly expected) executing child CLI process "${standalonePath}"
${$error}`;
const { warnify } =
require('../build/utils/messages') as typeof import('../build/utils/messages');
console.error(warnify(msg, '[debug] '));
}
resolve();
},
@ -166,11 +195,16 @@ ${$error}
.filter((l) => l)
.map((l) => l + '\n');
return filterCliOutputForTests({
exitCode,
const filtered = filterCliOutputForTests({
err: splitLines(stderr),
out: splitLines(stdout),
});
return {
err: filtered.err,
out: filtered.out,
// this makes sense if `process.exit()` was stubbed with sinon
exitCode,
};
}
/**
@ -190,11 +224,12 @@ export async function runCommand(cmd: string): Promise<TestOutput> {
);
}
try {
const { promises: fs } = await import('fs');
await fs.access(standalonePath);
} catch {
throw new Error(`Standalone executable not found: "${standalonePath}"`);
}
const proxy = await import('./proxy-server');
const proxy = await import('./nock/proxy-server');
const [proxyPort] = await proxy.createProxyServerOnce();
return runCommandInSubprocess(cmd, proxyPort);
} else {
@ -202,22 +237,6 @@ export async function runCommand(cmd: string): Promise<TestOutput> {
}
}
export const balenaAPIMock = () => {
if (!nock.isActive()) {
nock.activate();
}
return nock(/./).get('/config/vars').reply(200, {
reservedNames: [],
reservedNamespaces: [],
invalidRegex: '/^d|W/',
whiteListedNames: [],
whiteListedNamespaces: [],
blackListedNames: [],
configVarSchema: [],
});
};
export function cleanOutput(
output: string[] | string,
collapseBlank = false,
@ -226,11 +245,17 @@ export function cleanOutput(
? (line: string) => monochrome(line.trim()).replace(/\s{2,}/g, ' ')
: (line: string) => monochrome(line.trim());
return _(_.castArray(output))
.map((log: string) => log.split('\n').map(cleanLine))
.flatten()
.compact()
.value();
const result: string[] = [];
output = typeof output === 'string' ? [output] : output;
for (const lines of output) {
for (let line of lines.split('\n')) {
line = cleanLine(line);
if (line) {
result.push(line);
}
}
}
return result;
}
/**
@ -320,6 +345,7 @@ export function deepJsonParse(data: any): any {
export async function switchSentry(
enabled: boolean | undefined,
): Promise<boolean | undefined> {
const balenaCLI = await import('../build/app');
const sentryOpts = (await balenaCLI.setupSentry()).getClient()?.getOptions();
if (sentryOpts) {
const sentryStatus = sentryOpts.enabled;

View File

@ -21,7 +21,7 @@ import * as path from 'path';
import { NockMock, ScopeOpts } from './nock-mock';
export const apiResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'api-response'),
path.join(__dirname, '..', 'test-data', 'api-response'),
);
const jHeader = { 'Content-Type': 'application/json' };
@ -35,6 +35,7 @@ export class BalenaAPIMock extends NockMock {
notFound = false,
optional = false,
persist = false,
expandArchitecture = false,
} = {}) {
const interceptor = this.optGet(/^\/v6\/application($|[(?])/, {
optional,
@ -45,7 +46,12 @@ export class BalenaAPIMock extends NockMock {
} else {
interceptor.replyWithFile(
200,
path.join(apiResponsePath, 'application-GET-v6-expanded-app-type.json'),
path.join(
apiResponsePath,
!expandArchitecture
? 'application-GET-v6-expanded-app-type.json'
: 'application-GET-v6-expanded-app-type-cpu-arch.json',
),
jHeader,
);
}
@ -72,10 +78,10 @@ export class BalenaAPIMock extends NockMock {
}
public expectApplicationProvisioning(opts: ScopeOpts = {}) {
this.optPost(/^\/api-key\/application\/[0-9]+\/provisioning$/, opts).reply(
200,
'dummykey',
);
// The endpoint changed in balena-sdk v15.45.0:
// before: '/api-key/application/${applicationId}/provisioning'
// after: '/api-key/v1/'
this.optPost(/^\/api-key\/v[0-9]\/?$/, opts).reply(200, 'dummykey');
}
public expectGetMyApplication(opts: ScopeOpts = {}) {
@ -95,12 +101,27 @@ export class BalenaAPIMock extends NockMock {
});
}
public expectGetRelease(opts: ScopeOpts = {}) {
this.optGet(/^\/v6\/release($|[(?])/, opts).replyWithFile(
200,
path.join(apiResponsePath, 'release-GET-v6.json'),
jHeader,
);
public expectGetRelease({
notFound = false,
optional = false,
persist = false,
} = {}) {
const interceptor = this.optGet(/^\/v6\/release($|[(?])/, {
persist,
optional,
});
if (notFound) {
interceptor.reply(200, { d: [] });
} else {
this.optGet(/^\/v6\/release($|[(?])/, {
persist,
optional,
}).replyWithFile(
200,
path.join(apiResponsePath, 'release-GET-v6.json'),
jHeader,
);
}
}
/**
@ -122,11 +143,18 @@ export class BalenaAPIMock extends NockMock {
/**
* Mocks balena-release call
*/
public expectPostRelease(opts: ScopeOpts = {}) {
this.optPost(/^\/v6\/release($|[(?])/, opts).replyWithFile(
200,
path.join(apiResponsePath, 'release-POST-v6.json'),
jHeader,
public expectPostRelease({
statusCode = 200,
inspectRequest = this.inspectNoOp,
optional = false,
persist = false,
}) {
this.optPost(/^\/v6\/release($|[(?])/, { optional, persist }).reply(
statusCode,
this.getInspectedReplyFileFunction(
inspectRequest,
path.join(apiResponsePath, 'release-POST-v6.json'),
),
);
}
@ -198,7 +226,7 @@ export class BalenaAPIMock extends NockMock {
is_online: opts.isOnline,
belongs_to__application: opts.inaccessibleApp
? []
: [{ app_name: 'test' }],
: [{ app_name: 'test', slug: 'org/test' }],
},
],
});

View File

@ -22,7 +22,7 @@ import * as zlib from 'zlib';
import { NockMock } from './nock-mock';
export const builderResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'builder-response'),
path.join(__dirname, '..', 'test-data', 'builder-response'),
);
export class BuilderMock extends NockMock {

View File

@ -15,13 +15,13 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import * as path from 'path';
import * as qs from 'querystring';
import { NockMock, ScopeOpts } from './nock-mock';
export const dockerResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'docker-response'),
path.join(__dirname, '..', 'test-data', 'docker-response'),
);
export class DockerMock extends NockMock {
@ -78,7 +78,7 @@ export class DockerMock extends NockMock {
checkBuildRequestBody: (requestBody: string) => Promise<void>;
}) {
this.optPost(
new RegExp(`^/build\\?(|.+&)t=${_.escapeRegExp(opts.tag)}&`),
new RegExp(`^/build\\?(|.+&)${qs.stringify({ t: opts.tag })}&`),
opts,
).reply(async function (uri, requestBody, cb) {
let error: Error | null = null;
@ -133,4 +133,42 @@ export class DockerMock extends NockMock {
},
);
}
public expectGetManifestBusybox(opts: ScopeOpts = {}) {
// this.optGet(/^\/distribution\/.*/, opts).replyWithFile(
this.optGet('/distribution/busybox/json', opts).replyWithFile(
200,
path.join(dockerResponsePath, 'distribution-busybox-GET.json'),
{
'api-version': '1.38',
'Content-Type': 'application/json',
},
);
}
public expectGetManifestRpi3Alpine(opts: ScopeOpts = {}) {
this.optGet(
'/distribution/balenalib/raspberrypi3-alpine/json',
opts,
).replyWithFile(
200,
path.join(dockerResponsePath, 'distribution-rpi3alpine.json'),
{
'api-version': '1.38',
'Content-Type': 'application/json',
},
);
}
public expectGetManifestNucAlpine(opts: ScopeOpts = {}) {
// NOTE: This URL does no work in real life... it's "intel-nuc", not "nuc"
this.optGet('/distribution/balenalib/nuc-alpine/json', opts).replyWithFile(
200,
path.join(dockerResponsePath, 'distribution-nucalpine.json'),
{
'api-version': '1.38',
'Content-Type': 'application/json',
},
);
}
}

View File

@ -16,6 +16,7 @@
*/
import * as nock from 'nock';
import * as fs from 'fs';
export interface ScopeOpts {
optional?: boolean;
@ -32,7 +33,10 @@ export class NockMock {
public readonly expect;
protected static instanceCount = 0;
constructor(public basePathPattern: string | RegExp) {
constructor(
public basePathPattern: string | RegExp,
public allowUnmocked: boolean = false,
) {
if (NockMock.instanceCount === 0) {
if (!nock.isActive()) {
nock.activate();
@ -44,7 +48,7 @@ export class NockMock {
);
}
NockMock.instanceCount += 1;
this.scope = nock(this.basePathPattern);
this.scope = nock(this.basePathPattern, { allowUnmocked });
this.expect = this.scope;
}
@ -103,6 +107,27 @@ export class NockMock {
};
}
protected getInspectedReplyFileFunction(
inspectRequest: (uri: string, requestBody: nock.Body) => void,
replyBodyFile: string,
) {
return function (
this: nock.ReplyFnContext,
uri: string,
requestBody: nock.Body,
cb: (err: NodeJS.ErrnoException | null, result: nock.ReplyBody) => void,
) {
try {
inspectRequest(uri, requestBody);
} catch (err) {
cb(err, '');
}
const replyBody = fs.readFileSync(replyBodyFile);
cb(null, replyBody);
};
}
public done() {
try {
// scope.done() will throw an error if there are expected api calls that have not happened.

50
tests/nock/npm-mock.ts Normal file
View File

@ -0,0 +1,50 @@
/**
* @license
* Copyright 2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NockMock } from './nock-mock';
const jHeader = { 'Content-Type': 'application/json' };
export class NpmMock extends NockMock {
constructor() {
super(/registry\.npmjs\.org/);
}
public expectGetBalenaCli({
version,
publishedAt,
notFound = false,
optional = false,
persist = false,
}: {
version: string;
publishedAt: string;
notFound?: boolean;
optional?: boolean;
persist?: boolean;
}) {
const interceptor = this.optGet(`/balena-cli/${version}`, {
optional,
persist,
});
if (notFound) {
interceptor.reply(404, `version not found: ${version}`, jHeader);
} else {
interceptor.reply(200, { versionist: { publishedAt } }, jHeader);
}
}
}

View File

@ -22,7 +22,7 @@ import { Readable } from 'stream';
import { NockMock, ScopeOpts } from './nock-mock';
export const dockerResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'docker-response'),
path.join(__dirname, '..', 'test-data', 'docker-response'),
);
export class SupervisorMock extends NockMock {

View File

@ -86,7 +86,6 @@ export async function addRegSecretsEntries(
export function getDockerignoreWarn1(paths: string[], cmd: string) {
const lines = [
'[Warn] ----------------------------------------------------------------------',
'[Warn] The following .dockerignore file(s) will not be used:',
];
lines.push(...paths.map((p) => `[Warn] * ${p}`));
@ -96,7 +95,6 @@ export function getDockerignoreWarn1(paths: string[], cmd: string) {
'[Warn] root) is used. Microservices (multicontainer) fleets may use a separate',
'[Warn] .dockerignore file for each service with the --multi-dockerignore (-m)',
`[Warn] option. See "balena help ${cmd}" for more details.`,
'[Warn] ----------------------------------------------------------------------',
],
);
return lines;
@ -104,7 +102,6 @@ export function getDockerignoreWarn1(paths: string[], cmd: string) {
export function getDockerignoreWarn2(paths: string[], cmd: string) {
const lines = [
'[Warn] ----------------------------------------------------------------------',
'[Warn] The following .dockerignore file(s) will not be used:',
];
lines.push(...paths.map((p) => `[Warn] * ${p}`));
@ -114,7 +111,6 @@ export function getDockerignoreWarn2(paths: string[], cmd: string) {
"[Warn] root of each service's build context (in a microservices/multicontainer",
'[Warn] fleet), plus a .dockerignore file at the overall project root, are used.',
`[Warn] See "balena help ${cmd}" for more details.`,
'[Warn] ----------------------------------------------------------------------',
],
);
return lines;

View File

@ -0,0 +1,53 @@
{
"d": [
{
"application_type": [
{
"name": "Starter",
"slug": "microservices-starter",
"supports_multicontainer": true,
"is_legacy": false,
"__metadata": {}
}
],
"id": 1301645,
"user": {
"__deferred": {
"uri": "/resin/user(43699)"
},
"__id": 43699
},
"organization": [
{
"handle": "gh_user"
}
],
"depends_on__application": null,
"actor": 3423895,
"app_name": "testApp",
"slug": "gh_user/testApp",
"should_be__running_release": [
{
"commit": "96eec431d57e6976d3a756df33fde7e2"
}
],
"is_for__device_type": [
{
"slug": "raspberrypi3",
"is_of__cpu_architecture": [
{
"slug": "armv7hf"
}
]
}
],
"should_track_latest_release": true,
"is_accessible_by_support_until__date": null,
"is_public": false,
"is_host": false,
"__metadata": {
"uri": "/resin/application(@id)?@id=1301645"
}
}
]
}

View File

@ -1,52 +1,95 @@
{
"d": [
{
"contains__image": [
{
"image": [
{
"id": 1820810,
"created_at": "2020-01-04T01:13:08.805Z",
"start_timestamp": "2020-01-04T01:13:08.583Z",
"end_timestamp": "2020-01-04T01:13:11.920Z",
"dockerfile": "# FROM busybox\n# FROM arm32v7/busybox\n# FROM arm32v7/alpine\n# FROM eu.gcr.io/buoyant-idea-226013/arm32v7/busybox\n# FROM eu.gcr.io/buoyant-idea-226013/amd64/busybox\n# FROM balenalib/raspberrypi3-debian:jessie-build\nFROM balenalib/raspberrypi3:stretch\nENV UDEV=1\n\n# FROM sander85/rpi-busybox # armv6\n# FROM balenalib/raspberrypi3-alpine\n\n# COPY start.sh /\n# COPY /src/start.sh /src/start.sh\n# COPY /src/hello.txt /\n# COPY src/hi.txt /\n\n# RUN cat /hello.txt\n# RUN cat /hi.txt\n# RUN cat /run/secrets/my-secret.txt\n# EXPOSE 80\nRUN uname -a\n\n# FROM alpine\n# RUN apk update && apk add bash\n# SHELL [\"/bin/bash\", \"-c\"]\n# CMD for ((i=1; i > 0; i++)); do echo \"(Plain Dockerfile 34-$i) $(uname -a)\"; sleep ${INTERVAL=5}; done\n\n# CMD i=1; while :; do echo \"Plain Dockerfile 36 ($i) $(uname -a)\"; sleep 10; i=$((i+1)); done\n# ENTRYPOINT [\"/usr/bin/entry.sh\"]\nCMD [\"/bin/bash\"]\n",
"is_a_build_of__service": {
"__deferred": {
"uri": "/resin/service(233455)"
},
"__id": 233455
},
"image_size": 134320410,
"is_stored_at__image_location": "registry2.balena-cloud.com/v2/9c00c9413942cd15cfc9189c5dac359d",
"project_type": "Standard Dockerfile",
"error_message": null,
"build_log": "Step 1/4 : FROM balenalib/raspberrypi3:stretch\n ---> 8a75ea61d9c0\nStep 2/4 : ENV UDEV=1\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 159206067c8a\nStep 3/4 : RUN uname -a\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> dd1b3d9c334b\nStep 4/4 : CMD [\"/bin/bash\"]\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 5211b6f4bb72\nSuccessfully built 5211b6f4bb72\n",
"push_timestamp": "2020-01-04T01:13:14.415Z",
"status": "success",
"content_hash": "sha256:6b5471aae43ae81e8f69e10d1a516cb412569a6d5020a57eae311f8fa16d688a",
"contract": null,
"__metadata": {
"uri": "/resin/image(@id)?@id=1820810"
}
}
],
"id": 1738663,
"created_at": "2020-01-04T01:13:14.646Z",
"is_part_of__release": {
"__deferred": {
"uri": "/resin/release(1203844)"
},
"__id": 1203844
},
"__metadata": {
"uri": "/resin/image__is_part_of__release(@id)?@id=1738663"
}
{
"id": 142334,
"commit": "90247b54de4fa7a0a3cbc85e73c68039",
"created_at": "2021-08-25T22:18:34.014Z",
"status": "success",
"semver": "0.0.0",
"is_final": false,
"build_log": null,
"start_timestamp": "2021-08-25T22:18:33.624Z",
"end_timestamp": "2021-08-25T22:18:48.820Z",
"__metadata": {
"uri": "/resin/release(@id)?@id=142334"
},
"contains__image": [
{
"image": [
{
"id": 1820810,
"created_at": "2020-01-04T01:13:08.805Z",
"start_timestamp": "2020-01-04T01:13:08.583Z",
"end_timestamp": "2020-01-04T01:13:11.920Z",
"dockerfile": "# FROM busybox\n# FROM arm32v7/busybox\n# FROM arm32v7/alpine\n# FROM eu.gcr.io/buoyant-idea-226013/arm32v7/busybox\n# FROM eu.gcr.io/buoyant-idea-226013/amd64/busybox\n# FROM balenalib/raspberrypi3-debian:jessie-build\nFROM balenalib/raspberrypi3:stretch\nENV UDEV=1\n\n# FROM sander85/rpi-busybox # armv6\n# FROM balenalib/raspberrypi3-alpine\n\n# COPY start.sh /\n# COPY /src/start.sh /src/start.sh\n# COPY /src/hello.txt /\n# COPY src/hi.txt /\n\n# RUN cat /hello.txt\n# RUN cat /hi.txt\n# RUN cat /run/secrets/my-secret.txt\n# EXPOSE 80\nRUN uname -a\n\n# FROM alpine\n# RUN apk update && apk add bash\n# SHELL [\"/bin/bash\", \"-c\"]\n# CMD for ((i=1; i > 0; i++)); do echo \"(Plain Dockerfile 34-$i) $(uname -a)\"; sleep ${INTERVAL=5}; done\n\n# CMD i=1; while :; do echo \"Plain Dockerfile 36 ($i) $(uname -a)\"; sleep 10; i=$((i+1)); done\n# ENTRYPOINT [\"/usr/bin/entry.sh\"]\nCMD [\"/bin/bash\"]\n",
"is_a_build_of__service": {
"__deferred": {
"uri": "/resin/service(233455)"
},
"__id": 233455
},
"image_size": 134320410,
"is_stored_at__image_location": "registry2.balena-cloud.com/v2/9c00c9413942cd15cfc9189c5dac359d",
"project_type": "Standard Dockerfile",
"error_message": null,
"build_log": "Step 1/4 : FROM balenalib/raspberrypi3:stretch\n ---> 8a75ea61d9c0\nStep 2/4 : ENV UDEV=1\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 159206067c8a\nStep 3/4 : RUN uname -a\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> dd1b3d9c334b\nStep 4/4 : CMD [\"/bin/bash\"]\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 5211b6f4bb72\nSuccessfully built 5211b6f4bb72\n",
"push_timestamp": "2020-01-04T01:13:14.415Z",
"status": "success",
"content_hash": "sha256:6b5471aae43ae81e8f69e10d1a516cb412569a6d5020a57eae311f8fa16d688a",
"contract": null,
"__metadata": {
"uri": "/resin/image(@id)?@id=1820810"
}
}
],
"id": 1203844,
"id": 1738663,
"created_at": "2020-01-04T01:13:14.646Z",
"is_part_of__release": {
"__deferred": {
"uri": "/resin/release(1203844)"
},
"__id": 1203844
},
"__metadata": {
"uri": "/resin/release(@id)?@id=1203844"
"uri": "/resin/image__is_part_of__release(@id)?@id=1738663"
}
}
],
"release_tag": [
{
"tag_key": "testtag1",
"value": "val1",
"__metadata": {}
}
],
"composition": {
"version": "2.1",
"networks": {},
"volumes": {
"resin-data": {}
},
"services": {
"main": {
"build": {
"context": "."
},
"privileged": true,
"tty": true,
"restart": "always",
"network_mode": "host",
"volumes": [
"resin-data:/data"
],
"labels": {
"io.resin.features.kernel-modules": "1",
"io.resin.features.firmware": "1",
"io.resin.features.dbus": "1",
"io.resin.features.supervisor-api": "1",
"io.resin.features.resin-api": "1"
}
}
}
}
}
]
}
}

View File

@ -0,0 +1,53 @@
{
"Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"digest": "sha256:52f73a0a43a16cf37cd0720c90887ce972fe60ee06a687ee71fb93a7ca601df7",
"size": 2295
},
"Platforms": [
{
"architecture": "amd64",
"os": "linux"
},
{
"architecture": "arm",
"os": "linux",
"variant": "v5"
},
{
"architecture": "arm",
"os": "linux",
"variant": "v6"
},
{
"architecture": "arm",
"os": "linux",
"variant": "v7"
},
{
"architecture": "arm64",
"os": "linux",
"variant": "v8"
},
{
"architecture": "386",
"os": "linux"
},
{
"architecture": "mips64le",
"os": "linux"
},
{
"architecture": "ppc64le",
"os": "linux"
},
{
"architecture": "riscv64",
"os": "linux"
},
{
"architecture": "s390x",
"os": "linux"
}
]
}

View File

@ -0,0 +1,13 @@
{
"Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:d70bb0dd863198b41ea5d638993a9fbb912b3ea54b36480d1dc13e6b5b29021a",
"size": 2610
},
"Platforms": [
{
"architecture": "amd64",
"os": "linux"
}
]
}

View File

@ -0,0 +1,14 @@
{
"Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:2e33dc19d8514e01f7676532c507ddd95d0be20497fee25f4cbfc972cc6343d0",
"size": 2821
},
"Platforms": [
{
"architecture": "arm",
"os": "linux",
"variant": "v7"
}
]
}

View File

@ -1,5 +1,3 @@
> Warning Cannot find module 'net-keepalive' from 'build\utils\device'
%1: build\utils\device\api.js
> Warning Cannot resolve 'module'
node_modules\balena-sync\build\index.js
Dynamic require may fail at run time, because the requested file

View File

@ -1 +1 @@
alternative Dockerfile (basic/service2)
FROM busybox

View File

@ -0,0 +1,4 @@
FROM busybox
COPY ./src /usr/src/
RUN chmod a+x /usr/src/*.sh
CMD ["/usr/src/start.sh"]

View File

@ -0,0 +1,3 @@
name: testContract
type: sw.application
version: 1.5.2

View File

@ -0,0 +1,2 @@
#!/bin/sh
echo "Hello, test!"

View File

@ -7,6 +7,9 @@ import { FileIgnorer, IgnoreFileType } from '../../build/utils/ignore';
// of the FileIgnorer class to prevent a Typescript compilation error (this
// behavior is by design: see
// https://github.com/microsoft/TypeScript/issues/19335 )
//
// v13: delete this file
//
describe('File ignorer', function () {
it('should detect ignore files', function () {
const f = new FileIgnorer(`.${path.sep}`);