mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
283 Commits
Author | SHA1 | Date | |
---|---|---|---|
6606b65c9b | |||
61160fd2f5 | |||
bf71f9ea16 | |||
fe751fdb23 | |||
947f91d570 | |||
c5d4e30e24 | |||
f560aa7523 | |||
6bcfb2dd51 | |||
bf062124f7 | |||
221666f59a | |||
4369a2d161 | |||
cd6ee4ef5e | |||
872b17cf24 | |||
88e11347bc | |||
a3dd489c70 | |||
0c1c108b2b | |||
f02ed43f33 | |||
63c3d7ceee | |||
dac45a884e | |||
ec589c2639 | |||
f65e777d1b | |||
684ac9fa24 | |||
330cbc6a68 | |||
14bfca8c3a | |||
20c07d31b2 | |||
64b4f67477 | |||
a8ceadc300 | |||
973d25f467 | |||
0d06701e2f | |||
379f1cc217 | |||
7b7ae4ff89 | |||
8e83a401eb | |||
2d1891a182 | |||
8df066df12 | |||
bd59f95e1a | |||
2b982a1c0c | |||
ab64fbc904 | |||
733b98f072 | |||
7c538a3658 | |||
8298ba5765 | |||
33a23773d8 | |||
21a3b82845 | |||
8688eb5da0 | |||
5b0ea9673f | |||
44fd8adeba | |||
a5e03d55c3 | |||
80629322ea | |||
946efbcb7f | |||
be8a314d2b | |||
0a7203cafe | |||
786fed0151 | |||
9cd8228a20 | |||
652b5f22dd | |||
eed3c06789 | |||
3b283d4a98 | |||
bc6b5ba7b3 | |||
74789ae88f | |||
295d6dee74 | |||
5010a1e312 | |||
3c2f7ea622 | |||
94f02f0ad8 | |||
375f84b24e | |||
06c649dfd0 | |||
71eca70a22 | |||
53c7bc622c | |||
975ae45e49 | |||
e7c68c1a5c | |||
5beeb78220 | |||
c90ba7aa0f | |||
802ccc1b9a | |||
b6ef251625 | |||
fd707d6a07 | |||
392cd8569f | |||
e32eda26d9 | |||
d8aaccf80c | |||
d5fd5f5f2d | |||
2cb69c12f1 | |||
7c75346a1a | |||
148d15b6d9 | |||
a46a79df59 | |||
e350f9b335 | |||
bd00773f1b | |||
ef3c7f0fd6 | |||
f4f44f978e | |||
442416efc3 | |||
ef33ffedcf | |||
430d4aeaa7 | |||
171632f83f | |||
1fa7141b58 | |||
916cc36430 | |||
27b877dd33 | |||
5cbe1c410f | |||
7846af390e | |||
79d9ebc805 | |||
25b853c535 | |||
a93141343f | |||
9a467c5ecd | |||
70be2ae596 | |||
36eb0a108e | |||
0bf6fb1739 | |||
892adf4c47 | |||
5d1d004b72 | |||
dea5a60b2d | |||
652a1b7650 | |||
350843af1e | |||
e04c4a8ee3 | |||
9d0c3f7535 | |||
9561d4da2e | |||
8296dcf946 | |||
e62e8b88c2 | |||
4388a248b9 | |||
f9cf0aaf23 | |||
dc9ee09838 | |||
7cb27283c5 | |||
10a9840b34 | |||
ce3e04bfe8 | |||
52f93f8f12 | |||
af9e1a122d | |||
9017b8ec11 | |||
bf4f687a2a | |||
9d4e6eb825 | |||
fba4afb7d2 | |||
8c74f784f7 | |||
69ca1ffa59 | |||
7d1b00877e | |||
1a48fed1f7 | |||
bc86359e63 | |||
f6822f1502 | |||
398c34d842 | |||
72a893be95 | |||
7b23b0e103 | |||
0ce7878042 | |||
da8483e6a6 | |||
16f70fd946 | |||
78aa898b37 | |||
b7f94a222d | |||
7bea2c26b8 | |||
7c178b8095 | |||
865f085094 | |||
28fe69fe94 | |||
232cf8d426 | |||
22e74983b0 | |||
c88dd2257a | |||
439d8d396f | |||
6d8086c09b | |||
e85f252f29 | |||
4b818ad51c | |||
c2518448a3 | |||
e7a8deed05 | |||
0ac599d20c | |||
7d7074e6b7 | |||
35ca34d07d | |||
90d7316b4c | |||
904b4e96d9 | |||
2c46c59a79 | |||
297ff86895 | |||
a154401424 | |||
ad2713fc00 | |||
6388cfaf40 | |||
167f38e342 | |||
919b3c3435 | |||
2e1ab22173 | |||
0a23563d7e | |||
37e4ec6364 | |||
6a8b947c2e | |||
a16ac37625 | |||
cf4c7826b2 | |||
a0a26f0a1e | |||
a921139a12 | |||
36da7b66c8 | |||
3aa87544eb | |||
6121fa505e | |||
a5ba5befd1 | |||
b7214a306c | |||
d7616e941a | |||
834a2f1e4d | |||
0e5f2fe748 | |||
e0bcb5e0b9 | |||
59d4890eae | |||
51da5360da | |||
2655aef28b | |||
45d3a7a124 | |||
662e4f8940 | |||
c06993cb8e | |||
a650f30ce8 | |||
0a924b2dcb | |||
89f62683ce | |||
143d88f3df | |||
d166a65422 | |||
dd268993b3 | |||
13a35b288f | |||
81e653d31b | |||
875ec8b8bd | |||
989df9b857 | |||
0829d3c176 | |||
ce64889b04 | |||
d3a0bfc5f6 | |||
e965c603d2 | |||
0e2fb8c96c | |||
2db1d84d3c | |||
12a1916007 | |||
b4526e9895 | |||
a2d867c860 | |||
05b1c37379 | |||
906cfe9268 | |||
3c8054faa7 | |||
c6c9046826 | |||
2bbbbf6fdd | |||
9cce4001af | |||
2e944cf2f4 | |||
2b0143775c | |||
49fec7d8f2 | |||
ca1ac2bb83 | |||
50b1a7e6b0 | |||
69ce2c0473 | |||
a3b446dbe7 | |||
1032d9927f | |||
12e8a50abc | |||
a4142097f8 | |||
b388ccb6f3 | |||
e011502b7e | |||
4f167cb836 | |||
9455d438e2 | |||
a356ecf9b6 | |||
066ac591ac | |||
62f006b89a | |||
ee75ff2753 | |||
e4c9defb70 | |||
bb102c1918 | |||
24ebe2946c | |||
ba82b1fa27 | |||
e3b145e7b7 | |||
242c3731ee | |||
5f7eee8eac | |||
1833f6ff0a | |||
e5fb954645 | |||
13f76dc020 | |||
b409bdcc73 | |||
8c3cb3f585 | |||
76a8b4df50 | |||
a03680311d | |||
6ee36cb5c7 | |||
5625326c65 | |||
b912419839 | |||
fe01ead023 | |||
229c105d0c | |||
b6e044345f | |||
d9906121e1 | |||
3e019f7f34 | |||
eb34cb6f27 | |||
3a3178bcb9 | |||
cdf6580ecc | |||
c42bc74f1f | |||
35fd79f577 | |||
4ef0682e5a | |||
d0b7047189 | |||
ae3f936b66 | |||
1ef492809b | |||
5bf9dd3a9d | |||
b18a66f66b | |||
1dadfdc699 | |||
14a3f51b73 | |||
96116aeaec | |||
7fd31b6a64 | |||
299bc0db13 | |||
4b9ccae442 | |||
079ce552e3 | |||
163684e3a9 | |||
f698f561c9 | |||
cb207f18a5 | |||
76a5cdc977 | |||
a82af1d2d1 | |||
ac7d51ad80 | |||
797a739c92 | |||
666b59b463 | |||
a83d9a070c | |||
7637377471 | |||
6515f88d92 | |||
92534b9c82 | |||
c12360daa8 | |||
3d28118f3e | |||
04adfde064 | |||
d8aabfd448 |
@ -1,5 +1,5 @@
|
||||
{
|
||||
"single-quote": true,
|
||||
"trailing-comma": "all",
|
||||
"use-tabs": true
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"useTabs": true
|
||||
}
|
||||
|
@ -17,12 +17,3 @@ deploy:
|
||||
tags: true
|
||||
condition: "$TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
|
||||
repo: resin-io/resin-cli
|
||||
- provider: npm
|
||||
email: accounts@resin.io
|
||||
api_key:
|
||||
secure: phet6Du13hc1bzStbmpwy2ODNL5BFwjAmnpJ5wMcbWfI7fl0OtQ61s2+vW5hJAvm9fiRLOfiGAEiqOOtoupShZ1X8BNkC708d8+V+iZMoFh3+j6wAEz+N1sVq471PywlOuLAscOcqQNp92giCVt+4VPx2WQYh06nLsunvysGmUM=
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
condition: "$TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
|
||||
repo: resin-io/resin-cli
|
||||
|
299
CHANGELOG.md
299
CHANGELOG.md
@ -4,6 +4,305 @@ 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/).
|
||||
|
||||
## v8.0.0 - 2018-10-19
|
||||
|
||||
* Support multicontainer local mode in resin push [Cameron Diver]
|
||||
* Stop accepting resin-compose.yml as a build composition definition [Cameron Diver]
|
||||
* Send push source packages as gzipped data [Cameron Diver]
|
||||
* Respect ignore files when tarring sources [Cameron Diver]
|
||||
* Check for correct architecture when preloading, instead of correct device type [Alexis Svinartchouk]
|
||||
* Default preload boolean parameters to false [Alexis Svinartchouk]
|
||||
* Correctly error out on failed remote builds [Cameron Diver]
|
||||
* Bump tsconfig target to es6 [Cameron Diver]
|
||||
|
||||
## v7.10.9 - 2018-10-18
|
||||
|
||||
* Update sdk references in wizzard.coffee [Scott Lowe]
|
||||
|
||||
## v7.10.8 - 2018-10-18
|
||||
|
||||
* Update sdk references in notes.coffee [Scott Lowe]
|
||||
* Update sdk references in device.coffee [Scott Lowe]
|
||||
|
||||
## v7.10.7 - 2018-10-18
|
||||
|
||||
* Update sdk sdk references in auth.coffee [Scott Lowe]
|
||||
|
||||
## v7.10.6 - 2018-10-03
|
||||
|
||||
* Fix formatting of preload examples [Tim Perry]
|
||||
|
||||
## v7.10.5 - 2018-09-25
|
||||
|
||||
* README: Fix typo [Lucian Buzzo]
|
||||
|
||||
## v7.10.4 - 2018-09-24
|
||||
|
||||
* Device: When registering, print the uuid [Pablo Carranza Velez]
|
||||
|
||||
## v7.10.3 - 2018-09-19
|
||||
|
||||
* Include --emulated in the example resin build parameters [Tim Perry]
|
||||
|
||||
## v7.10.2 - 2018-09-18
|
||||
|
||||
* Dependencies: Update resin-semver version to support Balena OS [Lucian Buzzo]
|
||||
|
||||
## v7.10.1 - 2018-09-11
|
||||
|
||||
* Stop Travis deploying to npm (now handled by concourse) [Tim Perry]
|
||||
|
||||
## v7.10.0 - 2018-09-11
|
||||
|
||||
* Update resin-cli-form to 2.x [Pagan Gazzard]
|
||||
|
||||
## v7.9.4 - 2018-09-10
|
||||
|
||||
* Device api keys are no longer used in the registration process [Theodor Gherzan]
|
||||
|
||||
## v7.9.3 - 2018-08-20
|
||||
|
||||
* Fix configuration hangs with some images by expanding the threadpool #952 [Tim Perry]
|
||||
|
||||
## v7.9.2 - 2018-08-15
|
||||
|
||||
* Add warning about re-enabling automatic updates #946 [Pagan Gazzard]
|
||||
|
||||
## v7.9.1 - 2018-08-15
|
||||
|
||||
* Fix errors in `getRequestStream` not being propogated #942 [Pagan Gazzard]
|
||||
|
||||
## v7.9.0 - 2018-08-09
|
||||
|
||||
* Support emulated and nocache options for remote builds #903 [Cameron Diver]
|
||||
|
||||
## v7.8.6 - 2018-08-09
|
||||
|
||||
* Fix bug where the sudo helper failed in os initialize #939 [Tim Perry]
|
||||
|
||||
## v7.8.5 - 2018-08-09
|
||||
|
||||
* Update .resin-sync.yml docs for local push and include example env vars #934 [Tim Perry]
|
||||
|
||||
## v7.8.4 - 2018-08-02
|
||||
|
||||
* Update klaw #936 [Tim Perry]
|
||||
|
||||
## v7.8.3 - 2018-07-25
|
||||
|
||||
* Follow links found during builds #931 [Tim Perry]
|
||||
|
||||
## v7.8.2 - 2018-07-25
|
||||
|
||||
* Update reconfix to fix volume signature errors in local configure #929 [Tim Perry]
|
||||
|
||||
## v7.8.1 - 2018-07-20
|
||||
|
||||
* Be explicit about how much initial history log tailing includes #930 [Tim Perry]
|
||||
|
||||
## v7.8.0 - 2018-07-20
|
||||
|
||||
* Add join/leave commands to promote and move devices between platforms #895 [Akis Kesoglou]
|
||||
|
||||
## v7.7.4 - 2018-07-17
|
||||
|
||||
* Update TypeScript to 2.8.1 #923 [Tim Perry]
|
||||
* Add --version options to os configure & config generate #923 [Tim Perry]
|
||||
* Update OS & config actions to the latest SDK #923 [Tim Perry]
|
||||
|
||||
## v7.7.3 - 2018-07-13
|
||||
|
||||
* Update the deploy key since npm invalidated the old one #927 [Tim Perry]
|
||||
|
||||
## v7.7.2 - 2018-07-13
|
||||
|
||||
* Pin ext2fs to 1.0.7 to avoid temporary deployment issues #926 [Tim Perry]
|
||||
|
||||
## v7.7.1 - 2018-07-12
|
||||
|
||||
* Update CLI to SDK v10 (include new API logs) #925 [Tim Perry]
|
||||
|
||||
## v7.7.0 - 2018-07-11
|
||||
|
||||
* Add --generate-device-api-key parameter to config generate #921 [Tim Perry]
|
||||
|
||||
## v7.6.2 - 2018-06-28
|
||||
|
||||
* Make local commands more resilient to unnamed containers #910 [Tim Perry]
|
||||
|
||||
## v7.6.1 - 2018-06-26
|
||||
|
||||
* Make sure 'resin push' is included in the docs #907 [Tim Perry]
|
||||
|
||||
## v7.6.0 - 2018-06-20
|
||||
|
||||
* Support pinned release preloading #896 [Cameron Diver]
|
||||
|
||||
## v7.5.2 - 2018-06-12
|
||||
|
||||
* Document Pyhton native build dependency #893 [Tim Perry]
|
||||
|
||||
## v7.5.1 - 2018-06-01
|
||||
|
||||
* Add a multicontainer caveat to the env var commands #887 [Tim Perry]
|
||||
|
||||
## v7.5.0 - 2018-05-31
|
||||
|
||||
* Update resin-compose-parse dependency version to 1.10.2 #883 [Ariel Flesler]
|
||||
|
||||
## v7.4.1 - 2018-05-24
|
||||
|
||||
* Update SDK in resin device(s) to ensure the dashboard URL is correct #879 [Tim Perry]
|
||||
|
||||
## v7.4.0 - 2018-05-10
|
||||
|
||||
* Add push command which starts a build on remote resin servers #868 [Cameron Diver]
|
||||
|
||||
## v7.3.8 - 2018-05-03
|
||||
|
||||
* Catch require errors and provide helpful instructions #874 [Tim Perry]
|
||||
* Inline the entire resin-cli-errors module #874 [Tim Perry]
|
||||
|
||||
## v7.3.7 - 2018-04-30
|
||||
|
||||
* Pin node types to v9.0.0 to avoid build errors with transient dependencies #871 [Cameron Diver]
|
||||
|
||||
## v7.3.6 - 2018-04-30
|
||||
|
||||
* Update resin-image-fs to stop non-config commands failing in node 10 #870 [Tim Perry]
|
||||
|
||||
## v7.3.5 - 2018-04-18
|
||||
|
||||
* Don't show Docker container status from devices, as it can be wrong #858 [Tim Perry]
|
||||
|
||||
## v7.3.4 - 2018-04-18
|
||||
|
||||
* Include resin compose schemas in the standalone build #865 [Tim Perry]
|
||||
|
||||
## v7.3.3 - 2018-04-17
|
||||
|
||||
* Don't report lots of user input errors #861 [Tim Perry]
|
||||
* Include Sentry breadcrumbs for context in error reports #861 [Tim Perry]
|
||||
* Update to Sentry 2.x #861 [Tim Perry]
|
||||
|
||||
## v7.3.2 - 2018-04-16
|
||||
|
||||
* Update Dockerode to fix local push issue in standalone builds #862 [Tim Perry]
|
||||
|
||||
## v7.3.1 - 2018-04-13
|
||||
|
||||
* Update resin-compose-parse to v1.8.1 to fix a problem parsing ports #849 [Pablo Carranza Velez]
|
||||
|
||||
## v7.3.0 - 2018-04-12
|
||||
|
||||
* Add 'api-key generate' command #854 [Tim Perry]
|
||||
|
||||
## v7.2.4 - 2018-04-10
|
||||
|
||||
* Explicitly depend on tar-stream #852 [Tim Perry]
|
||||
* Correct documented node version requirement to 6+ #852 [Tim Perry]
|
||||
|
||||
## v7.2.3 - 2018-04-06
|
||||
|
||||
* Add a fast build script to package.json #847 [Cameron Diver]
|
||||
|
||||
## v7.2.2 - 2018-04-04
|
||||
|
||||
* Throw a clear error when logging in with an invalid token #846 [Tim Perry]
|
||||
|
||||
## v7.2.1 - 2018-03-29
|
||||
|
||||
* Update docker-qemu-transpose to avoid the broken 0.4.1 release #839 [Tim Perry]
|
||||
|
||||
## v7.2.0 - 2018-03-29
|
||||
|
||||
* Do not require a login for builds #835 [Tim Perry]
|
||||
* Allow (experimental!) login with API keys #835 [Tim Perry]
|
||||
|
||||
## v7.1.6 - 2018-03-29
|
||||
|
||||
* Fix build emulation for multi-stage builds #838 [Tim Perry]
|
||||
|
||||
## v7.1.5 - 2018-03-27
|
||||
|
||||
* Fix crash when an app is not specified for build command #834 [Akis Kesoglou]
|
||||
|
||||
## v7.1.4 - 2018-03-26
|
||||
|
||||
* Upgrade resin-sync to pull in the fix for #824 #832 [Tim Perry]
|
||||
|
||||
## v7.1.3 - 2018-03-23
|
||||
|
||||
* Prefix all pine options with '$' in preload to avoid pine warnings. #831 [Alexis Svinartchouk]
|
||||
|
||||
## v7.1.2 - 2018-03-23
|
||||
|
||||
* Update resin-preload to 6.2.0 and resin-sdk to 9.0.0-beta16 #830 [Alexis Svinartchouk]
|
||||
|
||||
## v7.1.1 - 2018-03-22
|
||||
|
||||
* Remove explicit anchor links in CLI docs #827 [Zach Walchuk]
|
||||
|
||||
## v7.1.0 - 2018-03-22
|
||||
|
||||
* Warn early if deploying a multicontainer project to an incompatible app #818 [Akis Kesoglou]
|
||||
* Add legacy deploy method back #818 [Akis Kesoglou]
|
||||
|
||||
## v7.0.7 - 2018-03-20
|
||||
|
||||
* Update resin-preload to 6.1.2 #821 [Alexis Svinartchouk]
|
||||
|
||||
## v7.0.6 - 2018-03-20
|
||||
|
||||
* Make sure image name is all lowercase #815 [Akis Kesoglou]
|
||||
* Improve handling of build log output #815 [Akis Kesoglou]
|
||||
|
||||
## v7.0.5 - 2018-03-15
|
||||
|
||||
* Add bash completions #801 [Ronald McCollam]
|
||||
|
||||
## v7.0.4 - 2018-03-15
|
||||
|
||||
* Generate consistent working anchors for both our md output & resin docs #813 [Tim Perry]
|
||||
|
||||
## v7.0.3 - 2018-03-15
|
||||
|
||||
* Fix getting window size when there’s no TTY attached #812 [Akis Kesoglou]
|
||||
|
||||
## v7.0.2 - 2018-03-13
|
||||
|
||||
* Update full CLI docs with recent installation improvements too #807 [Tim Perry]
|
||||
|
||||
## v7.0.1 - 2018-03-12
|
||||
|
||||
* Recommend using unsafe-prem to avoid permission issues on install #805 [Tim Perry]
|
||||
* Remove unnecessary resin-cli-auth dependency #805 [Tim Perry]
|
||||
|
||||
## v7.0.0 - 2018-03-09
|
||||
|
||||
* Add docker-compose-aware builds and deployments #792 [Akis Kesoglou]
|
||||
* *BREAKING*: Remove support for plugins entirely #792 [Tim Perry]
|
||||
* Update dashboard login to use the multicontainer SDK #792 [Alexis Svinartchouk]
|
||||
* Multicontainer preload: Update resin-preload to 6.0.0-beta4 #792 [Alexis Svinartchouk]
|
||||
* Update the keys action to use the multicontainer SDK #792 [Alexis Svinartchouk]
|
||||
* Require multicontainer SDK #792 [Alexis Svinartchouk]
|
||||
|
||||
## v6.13.5 - 2018-03-07
|
||||
|
||||
* Fix prettier configuration to avoid linting errors #802 [Tim Perry]
|
||||
|
||||
## v6.13.4 - 2018-02-22
|
||||
|
||||
* Fix issue where emulated builds broke Docker `ENV` commands #796 [Gergely Imreh]
|
||||
|
||||
## v6.13.3 - 2018-02-20
|
||||
|
||||
* Tweak TS & add missing deps that may cause build failures in some envs #793 [Tim Perry]
|
||||
|
||||
## v6.13.2 - 2018-02-20
|
||||
|
||||
* Ensure login does not wait for the browser process to close #794 [Tim Perry]
|
||||
|
||||
## v6.13.1 - 2018-02-07
|
||||
|
||||
* Move to the correct coffeescript (no hyphen) dependency #786 [Tim Perry]
|
||||
|
28
README.md
28
README.md
@ -13,7 +13,7 @@ Requisites
|
||||
If you want to install the CLI directly through npm, you'll need the below. If this looks difficult,
|
||||
we do now have an experimental standalone binary release available, see ['Standalone install'](#standalone-install) below.
|
||||
|
||||
- [NodeJS](https://nodejs.org) (>= v4)
|
||||
- [NodeJS](https://nodejs.org) (>= v6)
|
||||
- [Git](https://git-scm.com)
|
||||
- The following executables should be correctly installed in your shell environment:
|
||||
- `ssh`: Any recent version of the OpenSSH ssh client (required by `resin sync` and `resin ssh`)
|
||||
@ -45,28 +45,33 @@ or if you have any trouble with this, please try the new standalone install step
|
||||
This might require elevated privileges in some environments.
|
||||
|
||||
```sh
|
||||
$ npm install --global --production resin-cli
|
||||
$ npm install resin-cli -g --production --unsafe-perm
|
||||
```
|
||||
|
||||
`--unsafe-perm` is only required on systems where the global install directory is not user-writable.
|
||||
This allows npm install steps to download and save prebuilt native binaries. You may be able to omit it,
|
||||
especially if you're using a user-managed node install such as [nvm](https://github.com/creationix/nvm).
|
||||
|
||||
In some environments, this process will need to build native modules. This may require a more complex build
|
||||
environment, and notably requires Python 2.7. If you hit any problems with this, we recommend you try the
|
||||
alternative standalone install below instead.
|
||||
|
||||
### Standalone install
|
||||
|
||||
If you don't have node or a working pre-gyp environment, you can still install the CLI as a standalone
|
||||
binary. **This is in experimental and may not work perfectly yet in all environments**, but it seems to work
|
||||
binary. **This is experimental and may not work perfectly yet in all environments**, but it seems to work
|
||||
well in initial cross-platform testing, so it may be useful, and we'd love your feedback if you hit any issues.
|
||||
|
||||
To install the CLI as a standalone binary:
|
||||
|
||||
* Download the latest zip for your OS from https://github.com/resin-io/resin-cli/releases.
|
||||
* Extract the contents, putting the `resin-cli` folder somewhere appropriate for your system (e.g. `C:/resin-cli`, `/usr/local/lib/resin-cli`, etc).
|
||||
* Add the `resin-cli` folder to your `PATH`. (
|
||||
[Windows instructions](https://www.computerhope.com/issues/ch000549.htm),
|
||||
[Linux instructions](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix),
|
||||
[OSX instructions](https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently))
|
||||
* Add the `resin-cli` folder to your `PATH` ([Windows instructions](https://www.computerhope.com/issues/ch000549.htm), [Linux instructions](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix), [OSX instructions](https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently))
|
||||
* Running `resin` in a fresh command line should print the resin CLI help.
|
||||
|
||||
To update in future, simply download a new release and replace the extracted folder.
|
||||
|
||||
Have any problems, or see any unexpected behaviour? Please file an issue!
|
||||
Have any problems, or see any unexpected behaviour? [Please file an issue!](https://github.com/resin-io/resin-cli/issues/new)
|
||||
|
||||
### Login
|
||||
|
||||
@ -81,12 +86,9 @@ _(Typically useful, but not strictly required for all commands)_
|
||||
Take a look at the full command documentation at [https://docs.resin.io/tools/cli/](https://docs.resin.io/tools/cli/#table-of-contents
|
||||
), or by running `resin help`.
|
||||
|
||||
---
|
||||
### Bash completions
|
||||
|
||||
Plugins
|
||||
-------
|
||||
|
||||
The Resin CLI can be extended with plugins to automate laborious tasks and overall provide a better experience when working with Resin.io. Check the [plugin development tutorial](https://github.com/resin-io/resin-plugin-hello) to learn how to build your own!
|
||||
Optionally you can enable tab completions for the bash shell, enabling the shell to provide additional context and automatically complete arguments to`resin`. To enable bash completions, copy the `resin-completion.bash` file to the default bash completions directory (usually `/etc/bash_completion.d/`) or append it to the end of `~/.bash_completion`.
|
||||
|
||||
FAQ
|
||||
---
|
||||
|
@ -36,11 +36,11 @@ function getAnchor(command: Command) {
|
||||
'#' +
|
||||
command.signature
|
||||
.replace(/\s/g, '-')
|
||||
.replace(/</g, '60-')
|
||||
.replace(/>/g, '-62-')
|
||||
.replace(/\[/g, '')
|
||||
.replace(/</g, '-')
|
||||
.replace(/>/g, '-')
|
||||
.replace(/\[/g, '-')
|
||||
.replace(/\]/g, '-')
|
||||
.replace(/--/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/\.\.\./g, '')
|
||||
.replace(/\|/g, '')
|
||||
.toLowerCase()
|
||||
|
@ -1,2 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
require('../build/app');
|
||||
|
@ -1,115 +0,0 @@
|
||||
# coffeelint: disable=max_line_length
|
||||
|
||||
module.exports =
|
||||
title: 'Resin CLI Documentation'
|
||||
introduction: '''
|
||||
This tool allows you to interact with the resin.io api from the comfort of your command line.
|
||||
|
||||
Please make sure your system meets the requirements as specified in the [README](https://github.com/resin-io/resin-cli).
|
||||
|
||||
To get started download the CLI from npm.
|
||||
|
||||
$ npm install resin-cli -g
|
||||
|
||||
Then authenticate yourself:
|
||||
|
||||
$ resin login
|
||||
|
||||
Now you have access to all the commands referenced below.
|
||||
|
||||
## Proxy support
|
||||
|
||||
The CLI does support HTTP(S) proxies.
|
||||
|
||||
You can configure the proxy using several methods (in order of their precedence):
|
||||
|
||||
* set the `RESINRC_PROXY` environment variable in the URL format (with protocol, host, port, and optionally the basic auth),
|
||||
* use the [resin config file](https://www.npmjs.com/package/resin-settings-client#documentation) (project-specific or user-level)
|
||||
and set the `proxy` setting. This can be:
|
||||
* a string in the URL format,
|
||||
* or an object following [this format](https://www.npmjs.com/package/global-tunnel-ng#options), which allows more control,
|
||||
* or set the conventional `https_proxy` / `HTTPS_PROXY` / `http_proxy` / `HTTP_PROXY`
|
||||
environment variable (in the same standard URL format).
|
||||
'''
|
||||
|
||||
categories: [
|
||||
{
|
||||
title: 'Application'
|
||||
files: [ 'lib/actions/app.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
files: [ 'lib/actions/auth.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
files: [ 'lib/actions/device.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: [ 'lib/actions/environment-variables.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Help',
|
||||
files: [ 'lib/actions/help.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Information',
|
||||
files: [ 'lib/actions/info.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Keys',
|
||||
files: [ 'lib/actions/keys.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
files: [ 'lib/actions/logs.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Sync',
|
||||
files: [ 'lib/actions/sync.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'SSH',
|
||||
files: [ 'lib/actions/ssh.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
files: [ 'lib/actions/notes.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
files: [ 'lib/actions/os.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Config',
|
||||
files: [ 'lib/actions/config.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Preload',
|
||||
files: [ 'lib/actions/preload.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: [ 'lib/actions/settings.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Wizard',
|
||||
files: [ 'lib/actions/wizard.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Local',
|
||||
files: [ 'lib/actions/local/index.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Deploy',
|
||||
files: [
|
||||
'lib/actions/build.coffee'
|
||||
'lib/actions/deploy.coffee'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Utilities',
|
||||
files: [ 'lib/actions/util.coffee' ]
|
||||
},
|
||||
]
|
@ -5,15 +5,47 @@ This tool allows you to interact with the resin.io api from the comfort of your
|
||||
|
||||
Please make sure your system meets the requirements as specified in the [README](https://github.com/resin-io/resin-cli).
|
||||
|
||||
To get started download the CLI from npm.
|
||||
## Install the CLI
|
||||
|
||||
$ npm install resin-cli -g
|
||||
### Npm install
|
||||
|
||||
Then authenticate yourself:
|
||||
The best supported way to install the CLI is from npm:
|
||||
|
||||
$ npm install resin-cli -g --production --unsafe-perm
|
||||
|
||||
\`--unsafe-perm\` is only required on systems where the global install directory is not user-writable.
|
||||
This allows npm install steps to download and save prebuilt native binaries. You may be able to omit it,
|
||||
especially if you're using a user-managed node install such as [nvm](https://github.com/creationix/nvm).
|
||||
|
||||
### Standalone install
|
||||
|
||||
Alternatively, if you don't have a node or pre-gyp environment, you can still install the CLI as a standalone
|
||||
binary. **This is in experimental and may not work perfectly yet in all environments**, but works well in
|
||||
initial cross-platform testing, so it may be useful, and we'd love your feedback if you hit any issues.
|
||||
|
||||
To install the CLI as a standalone binary:
|
||||
|
||||
* Download the latest zip for your OS from https://github.com/resin-io/resin-cli/releases.
|
||||
* Extract the contents, putting the \`resin-cli\` folder somewhere appropriate for your system (e.g. \`C:/resin-cli\`, \`/usr/local/lib/resin-cli\`, etc).
|
||||
* Add the \`resin-cli\` folder to your \`PATH\`. (
|
||||
[Windows instructions](https://www.computerhope.com/issues/ch000549.htm),
|
||||
[Linux instructions](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix),
|
||||
[OSX instructions](https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently))
|
||||
* Running \`resin\` in a fresh command line should print the resin CLI help.
|
||||
|
||||
To update in future, simply download a new release and replace the extracted folder.
|
||||
|
||||
Have any problems, or see any unexpected behaviour? Please file an issue!
|
||||
|
||||
## Getting started
|
||||
|
||||
Once you have the CLI installed, you'll need to log in, so it can access everything in your resin.io account.
|
||||
|
||||
To authenticate yourself, run:
|
||||
|
||||
$ resin login
|
||||
|
||||
Now you have access to all the commands referenced below.
|
||||
You now have access to all the commands referenced below.
|
||||
|
||||
## Proxy support
|
||||
|
||||
@ -31,6 +63,10 @@ environment variable (in the same standard URL format).\
|
||||
`,
|
||||
|
||||
categories: [
|
||||
{
|
||||
title: 'Api keys',
|
||||
files: [ 'build/actions/api-key.js' ],
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
files: [ 'build/actions/app.js' ]
|
||||
@ -87,6 +123,10 @@ environment variable (in the same standard URL format).\
|
||||
title: 'Preload',
|
||||
files: [ 'build/actions/preload.js' ]
|
||||
},
|
||||
{
|
||||
title: 'Push',
|
||||
files: [ 'build/actions/push.js' ]
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: [ 'build/actions/settings.js' ]
|
||||
|
330
doc/cli.markdown
330
doc/cli.markdown
@ -4,15 +4,47 @@ This tool allows you to interact with the resin.io api from the comfort of your
|
||||
|
||||
Please make sure your system meets the requirements as specified in the [README](https://github.com/resin-io/resin-cli).
|
||||
|
||||
To get started download the CLI from npm.
|
||||
## Install the CLI
|
||||
|
||||
$ npm install resin-cli -g
|
||||
### Npm install
|
||||
|
||||
Then authenticate yourself:
|
||||
The best supported way to install the CLI is from npm:
|
||||
|
||||
$ npm install resin-cli -g --production --unsafe-perm
|
||||
|
||||
`--unsafe-perm` is only required on systems where the global install directory is not user-writable.
|
||||
This allows npm install steps to download and save prebuilt native binaries. You may be able to omit it,
|
||||
especially if you're using a user-managed node install such as [nvm](https://github.com/creationix/nvm).
|
||||
|
||||
### Standalone install
|
||||
|
||||
Alternatively, if you don't have a node or pre-gyp environment, you can still install the CLI as a standalone
|
||||
binary. **This is in experimental and may not work perfectly yet in all environments**, but works well in
|
||||
initial cross-platform testing, so it may be useful, and we'd love your feedback if you hit any issues.
|
||||
|
||||
To install the CLI as a standalone binary:
|
||||
|
||||
* Download the latest zip for your OS from https://github.com/resin-io/resin-cli/releases.
|
||||
* Extract the contents, putting the `resin-cli` folder somewhere appropriate for your system (e.g. `C:/resin-cli`, `/usr/local/lib/resin-cli`, etc).
|
||||
* Add the `resin-cli` folder to your `PATH`. (
|
||||
[Windows instructions](https://www.computerhope.com/issues/ch000549.htm),
|
||||
[Linux instructions](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix),
|
||||
[OSX instructions](https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently))
|
||||
* Running `resin` in a fresh command line should print the resin CLI help.
|
||||
|
||||
To update in future, simply download a new release and replace the extracted folder.
|
||||
|
||||
Have any problems, or see any unexpected behaviour? Please file an issue!
|
||||
|
||||
## Getting started
|
||||
|
||||
Once you have the CLI installed, you'll need to log in, so it can access everything in your resin.io account.
|
||||
|
||||
To authenticate yourself, run:
|
||||
|
||||
$ resin login
|
||||
|
||||
Now you have access to all the commands referenced below.
|
||||
You now have access to all the commands referenced below.
|
||||
|
||||
## Proxy support
|
||||
|
||||
@ -30,13 +62,17 @@ environment variable (in the same standard URL format).
|
||||
|
||||
# Table of contents
|
||||
|
||||
- Api keys
|
||||
|
||||
- [api-key generate <name>](#api-key-generate-name-)
|
||||
|
||||
- Application
|
||||
|
||||
- [app create <name>](#app-create-60-name-62-)
|
||||
- [app create <name>](#app-create-name-)
|
||||
- [apps](#apps)
|
||||
- [app <name>](#app-60-name-62-)
|
||||
- [app restart <name>](#app-restart-60-name-62-)
|
||||
- [app rm <name>](#app-rm-60-name-62-)
|
||||
- [app <name>](#app-name-)
|
||||
- [app restart <name>](#app-restart-name-)
|
||||
- [app rm <name>](#app-rm-name-)
|
||||
|
||||
- Authentication
|
||||
|
||||
@ -48,27 +84,27 @@ environment variable (in the same standard URL format).
|
||||
- Device
|
||||
|
||||
- [devices](#devices)
|
||||
- [device <uuid>](#device-60-uuid-62-)
|
||||
- [device <uuid>](#device-uuid-)
|
||||
- [devices supported](#devices-supported)
|
||||
- [device register <application>](#device-register-60-application-62-)
|
||||
- [device rm <uuid>](#device-rm-60-uuid-62-)
|
||||
- [device identify <uuid>](#device-identify-60-uuid-62-)
|
||||
- [device reboot <uuid>](#device-reboot-60-uuid-62-)
|
||||
- [device shutdown <uuid>](#device-shutdown-60-uuid-62-)
|
||||
- [device public-url enable <uuid>](#device-public-url-enable-60-uuid-62-)
|
||||
- [device public-url disable <uuid>](#device-public-url-disable-60-uuid-62-)
|
||||
- [device public-url <uuid>](#device-public-url-60-uuid-62-)
|
||||
- [device public-url status <uuid>](#device-public-url-status-60-uuid-62-)
|
||||
- [device rename <uuid> [newName]](#device-rename-60-uuid-62-newname-)
|
||||
- [device move <uuid>](#device-move-60-uuid-62-)
|
||||
- [device register <application>](#device-register-application-)
|
||||
- [device rm <uuid>](#device-rm-uuid-)
|
||||
- [device identify <uuid>](#device-identify-uuid-)
|
||||
- [device reboot <uuid>](#device-reboot-uuid-)
|
||||
- [device shutdown <uuid>](#device-shutdown-uuid-)
|
||||
- [device public-url enable <uuid>](#device-public-url-enable-uuid-)
|
||||
- [device public-url disable <uuid>](#device-public-url-disable-uuid-)
|
||||
- [device public-url <uuid>](#device-public-url-uuid-)
|
||||
- [device public-url status <uuid>](#device-public-url-status-uuid-)
|
||||
- [device rename <uuid> [newName]](#device-rename-uuid-newname-)
|
||||
- [device move <uuid>](#device-move-uuid-)
|
||||
- [device init](#device-init)
|
||||
|
||||
- Environment Variables
|
||||
|
||||
- [envs](#envs)
|
||||
- [env rm <id>](#env-rm-60-id-62-)
|
||||
- [env add <key> [value]](#env-add-60-key-62-value-)
|
||||
- [env rename <id> <value>](#env-rename-60-id-62-60-value-62-)
|
||||
- [env rm <id>](#env-rm-id-)
|
||||
- [env add <key> [value]](#env-add-key-value-)
|
||||
- [env rename <id> <value>](#env-rename-id-value-)
|
||||
|
||||
- Help
|
||||
|
||||
@ -81,13 +117,13 @@ environment variable (in the same standard URL format).
|
||||
- Keys
|
||||
|
||||
- [keys](#keys)
|
||||
- [key <id>](#key-60-id-62-)
|
||||
- [key rm <id>](#key-rm-60-id-62-)
|
||||
- [key add <name> [path]](#key-add-60-name-62-path-)
|
||||
- [key <id>](#key-id-)
|
||||
- [key rm <id>](#key-rm-id-)
|
||||
- [key add <name> [path]](#key-add-name-path-)
|
||||
|
||||
- Logs
|
||||
|
||||
- [logs <uuid>](#logs-60-uuid-62-)
|
||||
- [logs <uuid>](#logs-uuid-)
|
||||
|
||||
- Sync
|
||||
|
||||
@ -99,27 +135,31 @@ environment variable (in the same standard URL format).
|
||||
|
||||
- Notes
|
||||
|
||||
- [note <|note>](#note-60-note-62-)
|
||||
- [note <|note>](#note-note-)
|
||||
|
||||
- OS
|
||||
|
||||
- [os versions <type>](#os-versions-60-type-62-)
|
||||
- [os download <type>](#os-download-60-type-62-)
|
||||
- [os build-config <image> <device-type>](#os-build-config-60-image-62-60-device-type-62-)
|
||||
- [os configure <image> [uuid] [deviceApiKey]](#os-configure-60-image-62-uuid-deviceapikey-)
|
||||
- [os initialize <image>](#os-initialize-60-image-62-)
|
||||
- [os versions <type>](#os-versions-type-)
|
||||
- [os download <type>](#os-download-type-)
|
||||
- [os build-config <image> <device-type>](#os-build-config-image-device-type-)
|
||||
- [os configure <image> [uuid] [deviceApiKey]](#os-configure-image-uuid-deviceapikey-)
|
||||
- [os initialize <image>](#os-initialize-image-)
|
||||
|
||||
- Config
|
||||
|
||||
- [config read](#config-read)
|
||||
- [config write <key> <value>](#config-write-60-key-62-60-value-62-)
|
||||
- [config inject <file>](#config-inject-60-file-62-)
|
||||
- [config write <key> <value>](#config-write-key-value-)
|
||||
- [config inject <file>](#config-inject-file-)
|
||||
- [config reconfigure](#config-reconfigure)
|
||||
- [config generate](#config-generate)
|
||||
|
||||
- Preload
|
||||
|
||||
- [preload <image>](#preload-60-image-62-)
|
||||
- [preload <image>](#preload-image-)
|
||||
|
||||
- Push
|
||||
|
||||
- [push <applicationOrDevice>](#push-applicationordevice-)
|
||||
|
||||
- Settings
|
||||
|
||||
@ -131,8 +171,8 @@ environment variable (in the same standard URL format).
|
||||
|
||||
- Local
|
||||
|
||||
- [local configure <target>](#local-configure-60-target-62-)
|
||||
- [local flash <image>](#local-flash-60-image-62-)
|
||||
- [local configure <target>](#local-configure-target-)
|
||||
- [local flash <image>](#local-flash-image-)
|
||||
- [local logs [deviceIp]](#local-logs-deviceip-)
|
||||
- [local scan](#local-scan)
|
||||
- [local ssh [deviceIp]](#local-ssh-deviceip-)
|
||||
@ -142,12 +182,26 @@ environment variable (in the same standard URL format).
|
||||
- Deploy
|
||||
|
||||
- [build [source]](#build-source-)
|
||||
- [deploy <appName> [image]](#deploy-60-appname-62-image-)
|
||||
- [deploy <appName> [image]](#deploy-appname-image-)
|
||||
|
||||
- Utilities
|
||||
|
||||
- [util available-drives](#util-available-drives)
|
||||
|
||||
# Api keys
|
||||
|
||||
## api-key generate <name>
|
||||
|
||||
This command generates a new API key for the current user, with the given
|
||||
name. The key will be logged to the console.
|
||||
|
||||
This key can be used to log into the CLI using 'resin login --token <key>',
|
||||
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin api-key generate "Jenkins Key"
|
||||
|
||||
# Application
|
||||
|
||||
## app create <name>
|
||||
@ -230,7 +284,7 @@ from the dashboard.
|
||||
|
||||
- Credentials: using email/password and 2FA.
|
||||
|
||||
- Token: using the authentication token from the preferences page.
|
||||
- Token: using a session token or API key (experimental) from the preferences page.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -244,7 +298,7 @@ Examples:
|
||||
|
||||
#### --token, -t <token>
|
||||
|
||||
auth token
|
||||
session token or API key (experimental)
|
||||
|
||||
#### --web, -w
|
||||
|
||||
@ -334,13 +388,10 @@ Examples:
|
||||
|
||||
Use this command to register a device to an application.
|
||||
|
||||
Note that device api keys are only supported on ResinOS 2.0.3+
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device register MyApp
|
||||
$ resin device register MyApp --uuid <uuid>
|
||||
$ resin device register MyApp --uuid <uuid> --device-api-key <existingDeviceKey>
|
||||
|
||||
### Options
|
||||
|
||||
@ -348,10 +399,6 @@ Examples:
|
||||
|
||||
custom uuid
|
||||
|
||||
#### --deviceApiKey, -k <device-api-key>
|
||||
|
||||
custom device key - note that this is only supported on ResinOS 2.0.3+
|
||||
|
||||
## device rm <uuid>
|
||||
|
||||
Use this command to remove a device from resin.io.
|
||||
@ -521,6 +568,10 @@ This command lists all custom environment variables.
|
||||
If you want to see all environment variables, including private
|
||||
ones used by resin, use the verbose option.
|
||||
|
||||
At the moment the CLI doesn't fully support multi-container applications,
|
||||
so the following commands will only show service variables,
|
||||
without showing which service they belong to.
|
||||
|
||||
Example:
|
||||
|
||||
$ resin envs --application MyApp
|
||||
@ -572,6 +623,10 @@ device
|
||||
|
||||
Use this command to add an enviroment variable to an application.
|
||||
|
||||
At the moment the CLI doesn't fully support multi-container applications,
|
||||
so the following commands will only set service variables for the first
|
||||
service in your application.
|
||||
|
||||
If value is omitted, the tool will attempt to use the variable's value
|
||||
as defined in your host machine.
|
||||
|
||||
@ -695,10 +750,6 @@ By default, the command prints all log messages and exit.
|
||||
|
||||
To continuously stream output, and see new logs in real time, use the `--tail` option.
|
||||
|
||||
Note that for now you need to provide the whole UUID for this command to work correctly.
|
||||
|
||||
This is due to some technical limitations that we plan to address soon.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin logs 23c73a1
|
||||
@ -919,17 +970,20 @@ the path to the output JSON file
|
||||
Use this command to configure a previously downloaded operating system image for
|
||||
the specific device or for an application generally.
|
||||
|
||||
Calling this command without --version is not recommended, and may fail in
|
||||
future releases if the OS version cannot be inferred.
|
||||
|
||||
Note that device api keys are only supported on ResinOS 2.0.3+.
|
||||
|
||||
This comand still supports the *deprecated* format where the UUID and optionally device key
|
||||
This command still supports the *deprecated* format where the UUID and optionally device key
|
||||
are passed directly on the command line, but the recommended way is to pass either an --app or
|
||||
--device argument. The deprecated format will be remove in a future release.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6 --deviceApiKey <existingDeviceKey>
|
||||
$ resin os configure ../path/rpi.img --app MyApp
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
|
||||
$ resin os configure ../path/rpi.img --app MyApp --version 2.12.7
|
||||
|
||||
### Options
|
||||
|
||||
@ -949,6 +1003,10 @@ device uuid
|
||||
|
||||
custom device key - note that this is only supported on ResinOS 2.0.3+
|
||||
|
||||
#### --version <version>
|
||||
|
||||
a resinOS version
|
||||
|
||||
#### --config <config>
|
||||
|
||||
path to the config JSON file, see `resin os build-config`
|
||||
@ -1067,21 +1125,29 @@ show advanced commands
|
||||
|
||||
Use this command to generate a config.json for a device or application.
|
||||
|
||||
Calling this command without --version is not recommended, and may fail in
|
||||
future releases if the OS version cannot be inferred.
|
||||
|
||||
This is interactive by default, but you can do this automatically without interactivity
|
||||
by specifying an option for each question on the command line, if you know the questions
|
||||
that will be asked for the relevant device type.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config generate --device 7cf02a6
|
||||
$ resin config generate --device 7cf02a6 --device-api-key <existingDeviceKey>
|
||||
$ resin config generate --device 7cf02a6 --output config.json
|
||||
$ resin config generate --app MyApp
|
||||
$ resin config generate --app MyApp --output config.json
|
||||
$ resin config generate --app MyApp --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
|
||||
$ resin config generate --device 7cf02a6 --version 2.12.7
|
||||
$ resin config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
|
||||
$ resin config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
|
||||
$ resin config generate --device 7cf02a6 --version 2.12.7 --output config.json
|
||||
$ resin config generate --app MyApp --version 2.12.7
|
||||
$ resin config generate --app MyApp --version 2.12.7 --output config.json
|
||||
$ resin config generate --app MyApp --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
|
||||
|
||||
### Options
|
||||
|
||||
#### --version <version>
|
||||
|
||||
a resinOS version
|
||||
|
||||
#### --application, -a, --app <application>
|
||||
|
||||
application name
|
||||
@ -1094,6 +1160,10 @@ device uuid
|
||||
|
||||
custom device key - note that this is only supported on ResinOS 2.0.3+
|
||||
|
||||
#### --generate-device-api-key
|
||||
|
||||
generate a fresh device key for the device
|
||||
|
||||
#### --output, -o <output>
|
||||
|
||||
output
|
||||
@ -1123,12 +1193,12 @@ your shell environment. For more information (including Windows support)
|
||||
please check the README here: https://github.com/resin-io/resin-cli .
|
||||
|
||||
Use this command to preload an application to a local disk image (or
|
||||
Edison zip archive) with a built commit from Resin.io.
|
||||
This can be used with cloud builds, or images deployed with resin deploy.
|
||||
Edison zip archive) with a built release from Resin.io.
|
||||
|
||||
Examples:
|
||||
$ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png
|
||||
$ resin preload resin.img
|
||||
|
||||
$ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png
|
||||
$ resin preload resin.img
|
||||
|
||||
### Options
|
||||
|
||||
@ -1138,16 +1208,20 @@ id of the application to preload
|
||||
|
||||
#### --commit, -c <hash>
|
||||
|
||||
a specific application commit to preload, use "latest" to specify the latest commit
|
||||
the commit hash for a specific application release to preload, use "latest" to specify the latest release
|
||||
(ignored if no appId is given)
|
||||
|
||||
#### --splash-image, -s <splashImage.png>
|
||||
|
||||
path to a png image to replace the splash screen
|
||||
|
||||
#### --dont-check-device-type
|
||||
#### --dont-check-arch
|
||||
|
||||
Disables check for matching device types in image and application
|
||||
Disables check for matching architecture in image and application
|
||||
|
||||
#### --pin-device-to-release, -p
|
||||
|
||||
Pin the preloaded device to the preloaded release on provision
|
||||
|
||||
#### --docker, -P <docker>
|
||||
|
||||
@ -1173,6 +1247,45 @@ Docker host TLS certificate file
|
||||
|
||||
Docker host TLS key file
|
||||
|
||||
# Push
|
||||
|
||||
## push <applicationOrDevice>
|
||||
|
||||
This command can be used to start a build on the remote
|
||||
resin.io cloud builders, or a local mode resin device.
|
||||
|
||||
When building on the resin cloud the given source directory will be sent to the
|
||||
resin.io builder, and the build will proceed. This can be used as a drop-in
|
||||
replacement for git push to deploy.
|
||||
|
||||
When building on a local mode device, the given source directory will be built on
|
||||
device, and the resulting containers will be run on the device. Logs will be
|
||||
streamed back from the device as part of the same invocation.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin push myApp
|
||||
$ resin push myApp --source <source directory>
|
||||
$ resin push myApp -s <source directory>
|
||||
|
||||
$ resin push 10.0.0.1
|
||||
$ resin push 10.0.0.1 --source <source directory>
|
||||
$ resin push 10.0.0.1 -s <source directory>
|
||||
|
||||
### Options
|
||||
|
||||
#### --source, -s <source>
|
||||
|
||||
The source that should be sent to the resin builder to be built (defaults to the current directory)
|
||||
|
||||
#### --emulated, -e
|
||||
|
||||
Force an emulated build to occur on the remote builder
|
||||
|
||||
#### --nocache, -c
|
||||
|
||||
Don't use cache when building this project
|
||||
|
||||
# Settings
|
||||
|
||||
## settings
|
||||
@ -1330,12 +1443,14 @@ also change any option by editing '.resin-sync.yml' directly.
|
||||
Here is an example '.resin-sync.yml' :
|
||||
|
||||
$ cat $PWD/.resin-sync.yml
|
||||
destination: '/usr/src/app'
|
||||
before: 'echo Hello'
|
||||
after: 'echo Done'
|
||||
ignore:
|
||||
- .git
|
||||
- node_modules/
|
||||
local_resinos:
|
||||
app-name: local-app
|
||||
build-triggers:
|
||||
- Dockerfile: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
|
||||
- package.json: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
|
||||
environment:
|
||||
- MY_VARIABLE=123
|
||||
|
||||
|
||||
Command line options have precedence over the ones saved in '.resin-sync.yml'.
|
||||
|
||||
@ -1433,17 +1548,24 @@ name of container to stop
|
||||
|
||||
## build [source]
|
||||
|
||||
Use this command to build a container with a provided docker daemon.
|
||||
Use this command to build an image or a complete multicontainer project
|
||||
with the provided docker daemon.
|
||||
|
||||
You must provide either an application or a device-type/architecture
|
||||
pair to use the resin Dockerfile pre-processor
|
||||
(e.g. Dockerfile.template -> Dockerfile).
|
||||
|
||||
This command will look into the given source directory (or the current working
|
||||
directory if one isn't specified) for a compose file. If one is found, this
|
||||
command will build each service defined in the compose file. If a compose file
|
||||
isn't found, the command will look for a Dockerfile, and if yet that isn't found,
|
||||
it will try to generate one.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin build
|
||||
$ resin build ./source/
|
||||
$ resin build --deviceType raspberrypi3 --arch armhf
|
||||
$ resin build --deviceType raspberrypi3 --arch armhf --emulated
|
||||
$ resin build --application MyApp ./source/
|
||||
$ resin build --docker '/var/run/docker.sock'
|
||||
$ resin build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem
|
||||
@ -1462,6 +1584,18 @@ The type of device this build is for
|
||||
|
||||
The target resin.io application this build is for
|
||||
|
||||
#### --projectName, -n <projectName>
|
||||
|
||||
Specify an alternate project name; default is the directory name
|
||||
|
||||
#### --emulated, -e
|
||||
|
||||
Run an emulated build using Qemu
|
||||
|
||||
#### --logs
|
||||
|
||||
Display full log output
|
||||
|
||||
#### --docker, -P <docker>
|
||||
|
||||
Path to a local docker socket
|
||||
@ -1498,20 +1632,24 @@ Set a build-time variable (eg. "-B 'ARG=value'"). Can be specified multiple time
|
||||
|
||||
Don't use docker layer caching when building
|
||||
|
||||
#### --emulated, -e
|
||||
|
||||
Run an emulated build using Qemu
|
||||
|
||||
#### --squash
|
||||
|
||||
Squash newly built layers into a single new layer
|
||||
|
||||
## deploy <appName> [image]
|
||||
|
||||
Use this command to deploy an image to an application, optionally building it first.
|
||||
Use this command to deploy an image or a complete multicontainer project
|
||||
to an application, optionally building it first.
|
||||
|
||||
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
|
||||
|
||||
Unless an image is specified, this command will look into the current directory
|
||||
(or the one specified by --source) for a compose file. If one is found, this
|
||||
command will deploy each service defined in the compose file, building it first
|
||||
if an image for it doesn't exist. If a compose file isn't found, the command
|
||||
will look for a Dockerfile, and if yet that isn't found, it will try to
|
||||
generate one.
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
`resin deploy <appOwnerUsername>/<appName>`.
|
||||
|
||||
@ -1519,23 +1657,37 @@ Note: If building with this command, all options supported by `resin build`
|
||||
are also supported with this command.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin deploy myApp
|
||||
$ resin deploy myApp --build --source myBuildDir/
|
||||
$ resin deploy myApp myApp/myImage
|
||||
|
||||
### Options
|
||||
|
||||
#### --build, -b
|
||||
|
||||
Build image then deploy
|
||||
|
||||
#### --source, -s <source>
|
||||
|
||||
The source directory to use when building the image
|
||||
Specify an alternate source directory; default is the working directory
|
||||
|
||||
#### --build, -b
|
||||
|
||||
Force a rebuild before deploy
|
||||
|
||||
#### --nologupload
|
||||
|
||||
Don't upload build logs to the dashboard with image (if building)
|
||||
|
||||
#### --projectName, -n <projectName>
|
||||
|
||||
Specify an alternate project name; default is the directory name
|
||||
|
||||
#### --emulated, -e
|
||||
|
||||
Run an emulated build using Qemu
|
||||
|
||||
#### --logs
|
||||
|
||||
Display full log output
|
||||
|
||||
#### --docker, -P <docker>
|
||||
|
||||
Path to a local docker socket
|
||||
@ -1572,10 +1724,6 @@ Set a build-time variable (eg. "-B 'ARG=value'"). Can be specified multiple time
|
||||
|
||||
Don't use docker layer caching when building
|
||||
|
||||
#### --emulated, -e
|
||||
|
||||
Run an emulated build using Qemu
|
||||
|
||||
#### --squash
|
||||
|
||||
Squash newly built layers into a single new layer
|
||||
|
@ -28,7 +28,7 @@ gulp.task 'coffee', ->
|
||||
gulp.task 'test', ->
|
||||
gulp.src(OPTIONS.files.tests, read: false)
|
||||
.pipe(mocha({
|
||||
reporter: 'min'
|
||||
reporter: 'spec'
|
||||
}))
|
||||
|
||||
gulp.task 'build', [
|
||||
|
36
lib/actions/api-key.ts
Normal file
36
lib/actions/api-key.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
export const generate: CommandDefinition<{
|
||||
name: string;
|
||||
}> = {
|
||||
signature: 'api-key generate <name>',
|
||||
description: 'Generate a new API key with the given name',
|
||||
help: stripIndent`
|
||||
This command generates a new API key for the current user, with the given
|
||||
name. The key will be logged to the console.
|
||||
|
||||
This key can be used to log into the CLI using 'resin login --token <key>',
|
||||
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin api-key generate "Jenkins Key"
|
||||
`,
|
||||
async action(params, _options, done) {
|
||||
const resin = (await import('resin-sdk')).fromSharedOptions();
|
||||
|
||||
resin.models.apiKey
|
||||
.create(params.name)
|
||||
.then(key => {
|
||||
console.log(stripIndent`
|
||||
Registered api key '${params.name}':
|
||||
|
||||
${key}
|
||||
|
||||
This key will not be shown again, so please save it now.
|
||||
`);
|
||||
})
|
||||
.finally(done);
|
||||
},
|
||||
};
|
@ -52,7 +52,7 @@ exports.create =
|
||||
# https://github.com/resin-io/resin-cli/issues/30
|
||||
resin.models.application.has(params.name).then (hasApplication) ->
|
||||
if hasApplication
|
||||
throw new Error('You already have an application with that name!')
|
||||
patterns.exitWithExpectedError('You already have an application with that name!')
|
||||
|
||||
.then ->
|
||||
return options.type or patterns.selectDeviceType()
|
||||
|
@ -27,7 +27,7 @@ exports.login =
|
||||
|
||||
- Credentials: using email/password and 2FA.
|
||||
|
||||
- Token: using the authentication token from the preferences page.
|
||||
- Token: using a session token or API key (experimental) from the preferences page.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -40,7 +40,7 @@ exports.login =
|
||||
options: [
|
||||
{
|
||||
signature: 'token'
|
||||
description: 'auth token'
|
||||
description: 'session token or API key (experimental)'
|
||||
parameter: 'token'
|
||||
alias: 't'
|
||||
}
|
||||
@ -73,7 +73,7 @@ exports.login =
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
auth = require('../auth')
|
||||
form = require('resin-cli-form')
|
||||
patterns = require('../utils/patterns')
|
||||
@ -84,10 +84,15 @@ exports.login =
|
||||
return Promise.try ->
|
||||
return options.token if _.isString(options.token)
|
||||
return form.ask
|
||||
message: 'Token (from the preferences page)'
|
||||
message: 'Session token or API key (experimental) from the preferences page'
|
||||
name: 'token'
|
||||
type: 'input'
|
||||
.then(resin.auth.loginWithToken)
|
||||
.tap ->
|
||||
resin.auth.whoami()
|
||||
.then (username) ->
|
||||
if !username
|
||||
patterns.exitWithExpectedError('Token authentication failed')
|
||||
else if options.credentials
|
||||
return patterns.authenticate(options)
|
||||
else if options.web
|
||||
@ -97,8 +102,8 @@ exports.login =
|
||||
return patterns.askLoginType().then (loginType) ->
|
||||
|
||||
if loginType is 'register'
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
return capitanoRunAsync('signup')
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
return runCommand('signup')
|
||||
|
||||
options[loginType] = true
|
||||
return login(options)
|
||||
@ -131,7 +136,7 @@ exports.logout =
|
||||
$ resin logout
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
resin.auth.logout().nodeify(done)
|
||||
|
||||
exports.signup =
|
||||
@ -152,7 +157,7 @@ exports.signup =
|
||||
johndoe
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
validation = require('../utils/validation')
|
||||
|
||||
@ -188,7 +193,7 @@ exports.whoami =
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.props
|
||||
|
@ -2,42 +2,76 @@
|
||||
# of this action
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
compose = require('../utils/compose')
|
||||
|
||||
getBundleInfo = Promise.method (options) ->
|
||||
helpers = require('../utils/helpers')
|
||||
###
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
if options.application?
|
||||
# An application was provided
|
||||
return helpers.getArchAndDeviceType(options.application)
|
||||
.then (app) ->
|
||||
return [app.arch, app.device_type]
|
||||
else if options.arch? and options.deviceType?
|
||||
return [options.arch, options.deviceType]
|
||||
else
|
||||
# No information, cannot do resolution
|
||||
return undefined
|
||||
app: the app this build is for (optional)
|
||||
arch: the architecture to build for
|
||||
deviceType: the device type to build for
|
||||
buildEmulated
|
||||
buildOpts: arguments to forward to docker build command
|
||||
###
|
||||
buildProject = (docker, logger, composeOpts, opts) ->
|
||||
compose.loadProject(
|
||||
logger
|
||||
composeOpts.projectPath
|
||||
composeOpts.projectName
|
||||
)
|
||||
.then (project) ->
|
||||
appType = opts.app?.application_type?[0]
|
||||
if appType? and project.descriptors.length > 1 and not appType.supports_multicontainer
|
||||
logger.logWarn(
|
||||
'Target application does not support multiple containers.\n' +
|
||||
'Continuing with build, but you will not be able to deploy.'
|
||||
)
|
||||
|
||||
compose.buildProject(
|
||||
docker
|
||||
logger
|
||||
project.path
|
||||
project.name
|
||||
project.composition
|
||||
opts.arch
|
||||
opts.deviceType
|
||||
opts.buildEmulated
|
||||
opts.buildOpts
|
||||
composeOpts.inlineLogs
|
||||
)
|
||||
.then ->
|
||||
logger.logSuccess('Build succeeded!')
|
||||
.tapCatch (e) ->
|
||||
logger.logError('Build failed')
|
||||
|
||||
module.exports =
|
||||
signature: 'build [source]'
|
||||
description: 'Build a container locally'
|
||||
permission: 'user'
|
||||
description: 'Build a single image or a multicontainer project locally'
|
||||
primary: true
|
||||
help: '''
|
||||
Use this command to build a container with a provided docker daemon.
|
||||
Use this command to build an image or a complete multicontainer project
|
||||
with the provided docker daemon.
|
||||
|
||||
You must provide either an application or a device-type/architecture
|
||||
pair to use the resin Dockerfile pre-processor
|
||||
(e.g. Dockerfile.template -> Dockerfile).
|
||||
|
||||
This command will look into the given source directory (or the current working
|
||||
directory if one isn't specified) for a compose file. If one is found, this
|
||||
command will build each service defined in the compose file. If a compose file
|
||||
isn't found, the command will look for a Dockerfile, and if yet that isn't found,
|
||||
it will try to generate one.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin build
|
||||
$ resin build ./source/
|
||||
$ resin build --deviceType raspberrypi3 --arch armhf
|
||||
$ resin build --deviceType raspberrypi3 --arch armhf --emulated
|
||||
$ resin build --application MyApp ./source/
|
||||
$ resin build --docker '/var/run/docker.sock'
|
||||
$ resin build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem
|
||||
'''
|
||||
options: dockerUtils.appendOptions [
|
||||
options: dockerUtils.appendOptions compose.appendOptions [
|
||||
{
|
||||
signature: 'arch'
|
||||
parameter: 'arch'
|
||||
@ -58,7 +92,54 @@ module.exports =
|
||||
},
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Logger = require('../utils/logger')
|
||||
dockerUtils.runBuild(params, options, getBundleInfo, new Logger())
|
||||
.asCallback(done)
|
||||
# compositions with many services trigger misleading warnings
|
||||
require('events').defaultMaxListeners = 1000
|
||||
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
helpers = require('../utils/helpers')
|
||||
Logger = require('../utils/logger')
|
||||
|
||||
logger = new Logger()
|
||||
|
||||
logger.logDebug('Parsing input...')
|
||||
|
||||
Promise.try ->
|
||||
# `build` accepts `[source]` as a parameter, but compose expects it
|
||||
# as an option. swap them here
|
||||
options.source ?= params.source
|
||||
delete params.source
|
||||
|
||||
{ application, arch, deviceType } = options
|
||||
|
||||
if (not (arch? and deviceType?) and not application?) or (application? and (arch? or deviceType?))
|
||||
exitWithExpectedError('You must specify either an application or an arch/deviceType pair to build for')
|
||||
|
||||
if arch? and deviceType?
|
||||
[ undefined, arch, deviceType ]
|
||||
else
|
||||
Promise.join(
|
||||
helpers.getApplication(application)
|
||||
helpers.getArchAndDeviceType(application)
|
||||
(app, { arch, device_type }) ->
|
||||
app.arch = arch
|
||||
app.device_type = device_type
|
||||
return app
|
||||
)
|
||||
.then (app) ->
|
||||
[ app, app.arch, app.device_type ]
|
||||
|
||||
.then ([ app, arch, deviceType ]) ->
|
||||
Promise.join(
|
||||
dockerUtils.getDocker(options)
|
||||
dockerUtils.generateBuildOpts(options)
|
||||
compose.generateOpts(options)
|
||||
(docker, buildOpts, composeOpts) ->
|
||||
buildProject(docker, logger, composeOpts, {
|
||||
app
|
||||
arch
|
||||
deviceType
|
||||
buildEmulated: !!options.emulated
|
||||
buildOpts
|
||||
})
|
||||
)
|
||||
.asCallback(done)
|
||||
|
@ -44,6 +44,11 @@ exports.optionalDeviceApiKey =
|
||||
parameter: 'device-api-key'
|
||||
alias: 'k'
|
||||
|
||||
exports.optionalOsVersion =
|
||||
signature: 'version'
|
||||
description: 'a resinOS version'
|
||||
parameter: 'version'
|
||||
|
||||
exports.booleanDevice =
|
||||
signature: 'device'
|
||||
description: 'device'
|
||||
|
@ -198,7 +198,7 @@ exports.reconfigure =
|
||||
Promise = require('bluebird')
|
||||
config = require('resin-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
|
||||
Promise.try ->
|
||||
@ -212,7 +212,7 @@ exports.reconfigure =
|
||||
configureCommand = "os configure #{drive} --device #{uuid}"
|
||||
if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
return capitanoRunAsync(configureCommand)
|
||||
return runCommand(configureCommand)
|
||||
.then ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
@ -223,23 +223,34 @@ exports.generate =
|
||||
help: '''
|
||||
Use this command to generate a config.json for a device or application.
|
||||
|
||||
Calling this command without --version is not recommended, and may fail in
|
||||
future releases if the OS version cannot be inferred.
|
||||
|
||||
This is interactive by default, but you can do this automatically without interactivity
|
||||
by specifying an option for each question on the command line, if you know the questions
|
||||
that will be asked for the relevant device type.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config generate --device 7cf02a6
|
||||
$ resin config generate --device 7cf02a6 --device-api-key <existingDeviceKey>
|
||||
$ resin config generate --device 7cf02a6 --output config.json
|
||||
$ resin config generate --app MyApp
|
||||
$ resin config generate --app MyApp --output config.json
|
||||
$ resin config generate --app MyApp --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
|
||||
$ resin config generate --device 7cf02a6 --version 2.12.7
|
||||
$ resin config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
|
||||
$ resin config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
|
||||
$ resin config generate --device 7cf02a6 --version 2.12.7 --output config.json
|
||||
$ resin config generate --app MyApp --version 2.12.7
|
||||
$ resin config generate --app MyApp --version 2.12.7 --output config.json
|
||||
$ resin config generate --app MyApp --version 2.12.7 \
|
||||
--network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalOsVersion
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
commandOptions.optionalDeviceApiKey
|
||||
{
|
||||
signature: 'generate-device-api-key'
|
||||
description: 'generate a fresh device key for the device'
|
||||
boolean: true
|
||||
}
|
||||
{
|
||||
signature: 'output'
|
||||
description: 'output'
|
||||
@ -273,14 +284,16 @@ exports.generate =
|
||||
normalizeUuidProp(options, 'device')
|
||||
Promise = require('bluebird')
|
||||
writeFileAsync = Promise.promisify(require('fs').writeFile)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
deviceConfig = require('resin-device-config')
|
||||
prettyjson = require('prettyjson')
|
||||
|
||||
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if not options.device? and not options.application?
|
||||
throw new Error '''
|
||||
exitWithExpectedError '''
|
||||
You have to pass either a device or an application.
|
||||
|
||||
See the help page for examples:
|
||||
@ -300,8 +313,10 @@ exports.generate =
|
||||
# required option, that value is used (and the corresponding question is not asked)
|
||||
form.run(formOptions, override: options)
|
||||
.then (answers) ->
|
||||
answers.version = options.version
|
||||
|
||||
if resource.uuid?
|
||||
generateDeviceConfig(resource, options.deviceApiKey, answers)
|
||||
generateDeviceConfig(resource, options.deviceApiKey || options['generate-device-api-key'], answers)
|
||||
else
|
||||
generateApplicationConfig(resource, answers)
|
||||
.then (config) ->
|
||||
|
@ -1,121 +1,139 @@
|
||||
# Imported here because it's needed for the setup
|
||||
# of this action
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
compose = require('../utils/compose')
|
||||
|
||||
getBuilderPushEndpoint = (baseUrl, owner, app) ->
|
||||
querystring = require('querystring')
|
||||
args = querystring.stringify({ owner, app })
|
||||
"https://builder.#{baseUrl}/v1/push?#{args}"
|
||||
###
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
getBuilderLogPushEndpoint = (baseUrl, buildId, owner, app) ->
|
||||
querystring = require('querystring')
|
||||
args = querystring.stringify({ owner, app, buildId })
|
||||
"https://builder.#{baseUrl}/v1/pushLogs?#{args}"
|
||||
app: the application instance to deploy to
|
||||
image: the image to deploy; optional
|
||||
shouldPerformBuild
|
||||
shouldUploadLogs
|
||||
buildEmulated
|
||||
buildOpts: arguments to forward to docker build command
|
||||
###
|
||||
deployProject = (docker, logger, composeOpts, opts) ->
|
||||
_ = require('lodash')
|
||||
doodles = require('resin-doodles')
|
||||
sdk = require('resin-sdk').fromSharedOptions()
|
||||
|
||||
formatImageName = (image) ->
|
||||
image.split('/').pop()
|
||||
compose.loadProject(
|
||||
logger
|
||||
composeOpts.projectPath
|
||||
composeOpts.projectName
|
||||
opts.image
|
||||
)
|
||||
.then (project) ->
|
||||
if project.descriptors.length > 1 and !opts.app.application_type?[0]?.supports_multicontainer
|
||||
throw new Error('Target application does not support multiple containers. Aborting!')
|
||||
|
||||
parseInput = Promise.method (params, options) ->
|
||||
if not params.appName?
|
||||
throw new Error('Need an application to deploy to!')
|
||||
appName = params.appName
|
||||
image = undefined
|
||||
if params.image?
|
||||
if options.build or options.source?
|
||||
throw new Error('Build and source parameters are not applicable when specifying an image')
|
||||
options.build = false
|
||||
image = params.image
|
||||
else if options.build
|
||||
source = options.source || '.'
|
||||
else
|
||||
throw new Error('Need either an image or a build flag!')
|
||||
# find which services use images that already exist locally
|
||||
Promise.map project.descriptors, (d) ->
|
||||
# unconditionally build (or pull) if explicitly requested
|
||||
return d if opts.shouldPerformBuild
|
||||
docker.getImage(d.image.tag ? d.image).inspect()
|
||||
.return(d.serviceName)
|
||||
.catchReturn()
|
||||
.filter (d) -> !!d
|
||||
.then (servicesToSkip) ->
|
||||
# multibuild takes in a composition and always attempts to
|
||||
# build or pull all services. we workaround that here by
|
||||
# passing a modified composition.
|
||||
compositionToBuild = _.cloneDeep(project.composition)
|
||||
compositionToBuild.services = _.omit(compositionToBuild.services, servicesToSkip)
|
||||
if _.size(compositionToBuild.services) is 0
|
||||
logger.logInfo('Everything is up to date (use --build to force a rebuild)')
|
||||
return {}
|
||||
compose.buildProject(
|
||||
docker
|
||||
logger
|
||||
project.path
|
||||
project.name
|
||||
compositionToBuild
|
||||
opts.app.arch
|
||||
opts.app.device_type
|
||||
opts.buildEmulated
|
||||
opts.buildOpts
|
||||
composeOpts.inlineLogs
|
||||
)
|
||||
.then (builtImages) ->
|
||||
_.keyBy(builtImages, 'serviceName')
|
||||
.then (builtImages) ->
|
||||
project.descriptors.map (d) ->
|
||||
builtImages[d.serviceName] ? {
|
||||
serviceName: d.serviceName,
|
||||
name: d.image.tag ? d.image
|
||||
logs: 'Build skipped; image for service already exists.'
|
||||
props: {}
|
||||
}
|
||||
.then (images) ->
|
||||
if opts.app.application_type?[0]?.is_legacy
|
||||
chalk = require('chalk')
|
||||
legacyDeploy = require('../utils/deploy-legacy')
|
||||
|
||||
return [appName, options.build, source, image]
|
||||
msg = chalk.yellow('Target application requires legacy deploy method.')
|
||||
logger.logWarn(msg)
|
||||
|
||||
showPushProgress = (message) ->
|
||||
visuals = require('resin-cli-visuals')
|
||||
progressBar = new visuals.Progress(message)
|
||||
progressBar.update({ percentage: 0 })
|
||||
return progressBar
|
||||
|
||||
getBundleInfo = (options) ->
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
helpers.getArchAndDeviceType(options.appName)
|
||||
.then (app) ->
|
||||
[app.arch, app.device_type]
|
||||
|
||||
performUpload = (imageStream, token, username, url, appName, logger) ->
|
||||
request = require('request')
|
||||
progressStream = require('progress-stream')
|
||||
zlib = require('zlib')
|
||||
|
||||
# Need to strip off the newline
|
||||
progressMessage = logger.formatMessage('info', 'Deploying').slice(0, -1)
|
||||
progressBar = showPushProgress(progressMessage)
|
||||
streamWithProgress = imageStream.pipe progressStream
|
||||
time: 500,
|
||||
length: imageStream.length
|
||||
, ({ percentage, eta }) ->
|
||||
progressBar.update
|
||||
percentage: Math.min(percentage, 100)
|
||||
eta: eta
|
||||
|
||||
uploadRequest = request.post
|
||||
url: getBuilderPushEndpoint(url, username, appName)
|
||||
headers:
|
||||
'Content-Encoding': 'gzip'
|
||||
auth:
|
||||
bearer: token
|
||||
body: streamWithProgress.pipe(zlib.createGzip({
|
||||
level: 6
|
||||
}))
|
||||
|
||||
uploadToPromise(uploadRequest, logger)
|
||||
|
||||
uploadLogs = (logs, token, url, buildId, username, appName) ->
|
||||
request = require('request')
|
||||
request.post
|
||||
json: true
|
||||
url: getBuilderLogPushEndpoint(url, buildId, username, appName)
|
||||
auth:
|
||||
bearer: token
|
||||
body: Buffer.from(logs)
|
||||
|
||||
uploadToPromise = (uploadRequest, logger) ->
|
||||
new Promise (resolve, reject) ->
|
||||
|
||||
handleMessage = (data) ->
|
||||
data = data.toString()
|
||||
logger.logDebug("Received data: #{data}")
|
||||
|
||||
try
|
||||
obj = JSON.parse(data)
|
||||
catch e
|
||||
logger.logError('Error parsing reply from remote side')
|
||||
reject(e)
|
||||
return
|
||||
|
||||
if obj.type?
|
||||
switch obj.type
|
||||
when 'error' then reject(new Error("Remote error: #{obj.error}"))
|
||||
when 'success' then resolve(obj)
|
||||
when 'status' then logger.logInfo("Remote: #{obj.message}")
|
||||
else reject(new Error("Received unexpected reply from remote: #{data}"))
|
||||
else
|
||||
reject(new Error("Received unexpected reply from remote: #{data}"))
|
||||
|
||||
uploadRequest
|
||||
.on('error', reject)
|
||||
.on('data', handleMessage)
|
||||
return Promise.join(
|
||||
docker
|
||||
logger
|
||||
sdk.auth.getToken()
|
||||
sdk.auth.whoami()
|
||||
sdk.settings.get('resinUrl')
|
||||
{
|
||||
appName: opts.app.app_name
|
||||
imageName: images[0].name
|
||||
buildLogs: images[0].logs
|
||||
shouldUploadLogs: opts.shouldUploadLogs
|
||||
}
|
||||
legacyDeploy
|
||||
)
|
||||
.then (releaseId) ->
|
||||
sdk.models.release.get(releaseId, $select: [ 'commit' ])
|
||||
Promise.join(
|
||||
sdk.auth.getUserId()
|
||||
sdk.auth.getToken()
|
||||
sdk.settings.get('apiUrl')
|
||||
(userId, auth, apiEndpoint) ->
|
||||
compose.deployProject(
|
||||
docker
|
||||
logger
|
||||
project.composition
|
||||
images
|
||||
opts.app.id
|
||||
userId
|
||||
"Bearer #{auth}"
|
||||
apiEndpoint
|
||||
!opts.shouldUploadLogs
|
||||
)
|
||||
)
|
||||
.then (release) ->
|
||||
logger.logSuccess('Deploy succeeded!')
|
||||
logger.logSuccess("Release: #{release.commit}")
|
||||
console.log()
|
||||
console.log(doodles.getDoodle()) # Show charlie
|
||||
console.log()
|
||||
.tapCatch (e) ->
|
||||
logger.logError('Deploy failed')
|
||||
|
||||
module.exports =
|
||||
signature: 'deploy <appName> [image]'
|
||||
description: 'Deploy an image to a resin.io application'
|
||||
description: 'Deploy a single image or a multicontainer project to a resin.io application'
|
||||
help: '''
|
||||
Use this command to deploy an image to an application, optionally building it first.
|
||||
Use this command to deploy an image or a complete multicontainer project
|
||||
to an application, optionally building it first.
|
||||
|
||||
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
|
||||
|
||||
Unless an image is specified, this command will look into the current directory
|
||||
(or the one specified by --source) for a compose file. If one is found, this
|
||||
command will deploy each service defined in the compose file, building it first
|
||||
if an image for it doesn't exist. If a compose file isn't found, the command
|
||||
will look for a Dockerfile, and if yet that isn't found, it will try to
|
||||
generate one.
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
`resin deploy <appOwnerUsername>/<appName>`.
|
||||
|
||||
@ -123,23 +141,26 @@ module.exports =
|
||||
are also supported with this command.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin deploy myApp
|
||||
$ resin deploy myApp --build --source myBuildDir/
|
||||
$ resin deploy myApp myApp/myImage
|
||||
'''
|
||||
permission: 'user'
|
||||
options: dockerUtils.appendOptions [
|
||||
{
|
||||
signature: 'build'
|
||||
boolean: true
|
||||
description: 'Build image then deploy'
|
||||
alias: 'b'
|
||||
},
|
||||
primary: true
|
||||
options: dockerUtils.appendOptions compose.appendOptions [
|
||||
{
|
||||
signature: 'source'
|
||||
parameter: 'source'
|
||||
description: 'The source directory to use when building the image'
|
||||
description: 'Specify an alternate source directory; default is the working directory'
|
||||
alias: 's'
|
||||
},
|
||||
{
|
||||
signature: 'build'
|
||||
boolean: true
|
||||
description: 'Force a rebuild before deploy'
|
||||
alias: 'b'
|
||||
},
|
||||
{
|
||||
signature: 'nologupload'
|
||||
description: "Don't upload build logs to the dashboard with image (if building)"
|
||||
@ -147,83 +168,53 @@ module.exports =
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
tmp = require('tmp')
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
# compositions with many services trigger misleading warnings
|
||||
require('events').defaultMaxListeners = 1000
|
||||
|
||||
helpers = require('../utils/helpers')
|
||||
Logger = require('../utils/logger')
|
||||
|
||||
logger = new Logger()
|
||||
|
||||
# Ensure the tmp files gets deleted
|
||||
tmp.setGracefulCleanup()
|
||||
logger.logDebug('Parsing input...')
|
||||
|
||||
logs = ''
|
||||
Promise.try ->
|
||||
{ appName, image } = params
|
||||
|
||||
upload = (token, username, url) ->
|
||||
dockerUtils.getDocker(options)
|
||||
.then (docker) ->
|
||||
# Check input parameters
|
||||
parseInput(params, options)
|
||||
.then ([appName, build, source, imageName]) ->
|
||||
tmpNameAsync()
|
||||
.then (bufferFile) ->
|
||||
# look into "resin build" options if appName isn't given
|
||||
appName = options.application if not appName?
|
||||
delete options.application
|
||||
|
||||
# Setup the build args for how the build routine expects them
|
||||
options = _.assign({}, options, { appName })
|
||||
params = _.assign({}, params, { source })
|
||||
if not appName?
|
||||
throw new Error('Please specify the name of the application to deploy')
|
||||
|
||||
Promise.try ->
|
||||
if build
|
||||
dockerUtils.runBuild(params, options, getBundleInfo, logger)
|
||||
else
|
||||
{ image: imageName, log: '' }
|
||||
.then ({ image: imageName, log: buildLogs }) ->
|
||||
logger.logInfo('Initializing deploy...')
|
||||
if image? and options.build
|
||||
throw new Error('Build option is not applicable when specifying an image')
|
||||
|
||||
logs = buildLogs
|
||||
Promise.all [
|
||||
dockerUtils.bufferImage(docker, imageName, bufferFile)
|
||||
token
|
||||
username
|
||||
url
|
||||
params.appName
|
||||
logger
|
||||
]
|
||||
.spread(performUpload)
|
||||
.finally ->
|
||||
# If the file was never written to (for instance because an error
|
||||
# has occured before any data was written) this call will throw an
|
||||
# ugly error, just suppress it
|
||||
Promise.try ->
|
||||
require('mz/fs').unlink(bufferFile)
|
||||
.catch(_.noop)
|
||||
.tap ({ image: imageName, buildId }) ->
|
||||
logger.logSuccess("Successfully deployed image: #{formatImageName(imageName)}")
|
||||
return buildId
|
||||
.then ({ image: imageName, buildId }) ->
|
||||
if logs is '' or options.nologupload?
|
||||
return ''
|
||||
Promise.join(
|
||||
helpers.getApplication(appName)
|
||||
helpers.getArchAndDeviceType(appName)
|
||||
(app, { arch, device_type }) ->
|
||||
app.arch = arch
|
||||
app.device_type = device_type
|
||||
return app
|
||||
)
|
||||
.then (app) ->
|
||||
[ app, image, !!options.build, !options.nologupload ]
|
||||
|
||||
logger.logInfo('Uploading logs to dashboard...')
|
||||
|
||||
Promise.join(
|
||||
logs
|
||||
token
|
||||
url
|
||||
buildId
|
||||
username
|
||||
params.appName
|
||||
uploadLogs
|
||||
)
|
||||
.return('Successfully uploaded logs')
|
||||
.then (msg) ->
|
||||
logger.logSuccess(msg) if msg isnt ''
|
||||
.asCallback(done)
|
||||
|
||||
Promise.join(
|
||||
resin.auth.getToken()
|
||||
resin.auth.whoami()
|
||||
resin.settings.get('resinUrl')
|
||||
upload
|
||||
)
|
||||
.then ([ app, image, shouldPerformBuild, shouldUploadLogs ]) ->
|
||||
Promise.join(
|
||||
dockerUtils.getDocker(options)
|
||||
dockerUtils.generateBuildOpts(options)
|
||||
compose.generateOpts(options)
|
||||
(docker, buildOpts, composeOpts) ->
|
||||
deployProject(docker, logger, composeOpts, {
|
||||
app
|
||||
image
|
||||
shouldPerformBuild
|
||||
shouldUploadLogs
|
||||
buildEmulated: !!options.emulated
|
||||
buildOpts
|
||||
})
|
||||
)
|
||||
.asCallback(done)
|
||||
|
@ -18,6 +18,10 @@ commandOptions = require('./command-options')
|
||||
_ = require('lodash')
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
expandForAppName = {
|
||||
$expand: belongs_to__application: $select: 'app_name'
|
||||
}
|
||||
|
||||
exports.list =
|
||||
signature: 'devices'
|
||||
description: 'list all devices'
|
||||
@ -38,23 +42,25 @@ exports.list =
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.try ->
|
||||
if options.application?
|
||||
return resin.models.device.getAllByApplication(options.application)
|
||||
return resin.models.device.getAll()
|
||||
return resin.models.device.getAllByApplication(options.application, expandForAppName)
|
||||
return resin.models.device.getAll(expandForAppName)
|
||||
|
||||
.tap (devices) ->
|
||||
devices = _.map devices, (device) ->
|
||||
device.dashboard_url = resin.models.device.getDashboardUrl(device.uuid)
|
||||
device.application_name = device.belongs_to__application[0].app_name
|
||||
device.uuid = device.uuid.slice(0, 7)
|
||||
return device
|
||||
|
||||
console.log visuals.table.horizontal devices, [
|
||||
'id'
|
||||
'uuid'
|
||||
'name'
|
||||
'device_name'
|
||||
'device_type'
|
||||
'application_name'
|
||||
'status'
|
||||
@ -79,16 +85,19 @@ exports.info =
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.device.get(params.uuid).then (device) ->
|
||||
|
||||
resin.models.device.get(params.uuid, expandForAppName)
|
||||
.then (device) ->
|
||||
resin.models.device.getStatus(device).then (status) ->
|
||||
device.status = status
|
||||
device.dashboard_url = resin.models.device.getDashboardUrl(device.uuid)
|
||||
device.application_name = device.belongs_to__application[0].app_name
|
||||
device.commit = device.is_on__commit
|
||||
|
||||
console.log visuals.table.vertical device, [
|
||||
"$#{device.name}$"
|
||||
"$#{device.device_name}$"
|
||||
'id'
|
||||
'device_type'
|
||||
'status'
|
||||
@ -117,7 +126,7 @@ exports.supported =
|
||||
$ resin devices supported
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.config.getDeviceTypes().then (deviceTypes) ->
|
||||
@ -133,13 +142,10 @@ exports.register =
|
||||
help: '''
|
||||
Use this command to register a device to an application.
|
||||
|
||||
Note that device api keys are only supported on ResinOS 2.0.3+
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device register MyApp
|
||||
$ resin device register MyApp --uuid <uuid>
|
||||
$ resin device register MyApp --uuid <uuid> --device-api-key <existingDeviceKey>
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
@ -149,23 +155,17 @@ exports.register =
|
||||
parameter: 'uuid'
|
||||
alias: 'u'
|
||||
}
|
||||
commandOptions.optionalDeviceApiKey
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
|
||||
Promise.join(
|
||||
resin.models.application.get(params.application)
|
||||
options.uuid ? resin.models.device.generateUniqueKey()
|
||||
options.deviceApiKey ? resin.models.device.generateUniqueKey()
|
||||
(application, uuid, deviceApiKey) ->
|
||||
(application, uuid) ->
|
||||
console.info("Registering to #{application.app_name}: #{uuid}")
|
||||
if not options.deviceApiKey?
|
||||
console.info("Using generated device api key: #{deviceApiKey}")
|
||||
else
|
||||
console.info('Using provided device api key')
|
||||
return resin.models.device.register(application.id, uuid, deviceApiKey)
|
||||
return resin.models.device.register(application.id, uuid)
|
||||
)
|
||||
.get('uuid')
|
||||
.nodeify(done)
|
||||
@ -188,7 +188,7 @@ exports.remove =
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the device?').then ->
|
||||
@ -210,7 +210,7 @@ exports.identify =
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
resin.models.device.identify(params.uuid).nodeify(done)
|
||||
|
||||
exports.reboot =
|
||||
@ -227,7 +227,7 @@ exports.reboot =
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
resin.models.device.reboot(params.uuid, options).nodeify(done)
|
||||
|
||||
exports.shutdown =
|
||||
@ -244,7 +244,7 @@ exports.shutdown =
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
resin.models.device.shutdown(params.uuid, options).nodeify(done)
|
||||
|
||||
exports.enableDeviceUrl =
|
||||
@ -260,7 +260,7 @@ exports.enableDeviceUrl =
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
resin.models.device.enableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.disableDeviceUrl =
|
||||
@ -276,7 +276,7 @@ exports.disableDeviceUrl =
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
resin.models.device.disableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.getDeviceUrl =
|
||||
@ -292,7 +292,7 @@ exports.getDeviceUrl =
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
resin.models.device.getDeviceUrl(params.uuid).then (url) ->
|
||||
console.log(url)
|
||||
.nodeify(done)
|
||||
@ -310,7 +310,7 @@ exports.hasDeviceUrl =
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
resin.models.device.hasDeviceUrl(params.uuid).then (hasDeviceUrl) ->
|
||||
console.log(hasDeviceUrl)
|
||||
.nodeify(done)
|
||||
@ -332,7 +332,7 @@ exports.rename =
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
|
||||
Promise.try ->
|
||||
@ -362,14 +362,14 @@ exports.move =
|
||||
options: [ commandOptions.optionalApplication ]
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
resin.models.device.get(params.uuid).then (device) ->
|
||||
resin.models.device.get(params.uuid, expandForAppName).then (device) ->
|
||||
return options.application or patterns.selectApplication (application) ->
|
||||
return _.every [
|
||||
application.device_type is device.device_type
|
||||
device.application_name isnt application.app_name
|
||||
device.belongs_to__application[0].app_name isnt application.app_name
|
||||
]
|
||||
.tap (application) ->
|
||||
return resin.models.device.move(params.uuid, application)
|
||||
@ -406,14 +406,14 @@ exports.init =
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
rimraf = Promise.promisify(require('rimraf'))
|
||||
tmp = require('tmp')
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
||||
tmp.setGracefulCleanup()
|
||||
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
|
||||
Promise.try ->
|
||||
return options.application if options.application?
|
||||
@ -424,12 +424,12 @@ exports.init =
|
||||
download = ->
|
||||
tmpNameAsync().then (tempPath) ->
|
||||
osVersion = options['os-version'] or 'default'
|
||||
capitanoRunAsync("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
|
||||
runCommand("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
|
||||
.disposer (tempPath) ->
|
||||
return rimraf(tempPath)
|
||||
|
||||
Promise.using download(), (tempPath) ->
|
||||
capitanoRunAsync("device register #{application.app_name}")
|
||||
runCommand("device register #{application.app_name}")
|
||||
.then(resin.models.device.get)
|
||||
.tap (device) ->
|
||||
configureCommand = "os configure '#{tempPath}' --device #{device.uuid}"
|
||||
@ -437,14 +437,14 @@ exports.init =
|
||||
configureCommand += " --config '#{options.config}'"
|
||||
else if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
capitanoRunAsync(configureCommand)
|
||||
runCommand(configureCommand)
|
||||
.then ->
|
||||
osInitCommand = "os initialize '#{tempPath}' --type #{application.device_type}"
|
||||
if options.yes
|
||||
osInitCommand += ' --yes'
|
||||
if options.drive
|
||||
osInitCommand += " --drive #{options.drive}"
|
||||
capitanoRunAsync(osInitCommand)
|
||||
runCommand(osInitCommand)
|
||||
# Make sure the device resource is removed if there is an
|
||||
# error when configuring or initializing a device image
|
||||
.catch (error) ->
|
||||
|
@ -28,6 +28,10 @@ exports.list =
|
||||
If you want to see all environment variables, including private
|
||||
ones used by resin, use the verbose option.
|
||||
|
||||
At the moment the CLI doesn't fully support multi-container applications,
|
||||
so the following commands will only show service variables,
|
||||
without showing which service they belong to.
|
||||
|
||||
Example:
|
||||
|
||||
$ resin envs --application MyApp
|
||||
@ -53,17 +57,19 @@ exports.list =
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
Promise.try ->
|
||||
if options.application?
|
||||
return resin.models.environmentVariables.getAllByApplication(options.application)
|
||||
else if options.device?
|
||||
return resin.models.environmentVariables.device.getAll(options.device)
|
||||
else
|
||||
throw new Error('You must specify an application or device')
|
||||
exitWithExpectedError('You must specify an application or device')
|
||||
|
||||
.tap (environmentVariables) ->
|
||||
if _.isEmpty(environmentVariables)
|
||||
throw new Error('No environment variables found')
|
||||
exitWithExpectedError('No environment variables found')
|
||||
if not options.verbose
|
||||
isSystemVariable = resin.models.environmentVariables.isSystemVariable
|
||||
environmentVariables = _.reject(environmentVariables, isSystemVariable)
|
||||
@ -116,6 +122,10 @@ exports.add =
|
||||
help: '''
|
||||
Use this command to add an enviroment variable to an application.
|
||||
|
||||
At the moment the CLI doesn't fully support multi-container applications,
|
||||
so the following commands will only set service variables for the first
|
||||
service in your application.
|
||||
|
||||
If value is omitted, the tool will attempt to use the variable's value
|
||||
as defined in your host machine.
|
||||
|
||||
@ -141,6 +151,8 @@ exports.add =
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
Promise.try ->
|
||||
if not params.value?
|
||||
params.value = process.env[params.key]
|
||||
@ -155,7 +167,7 @@ exports.add =
|
||||
else if options.device?
|
||||
resin.models.environmentVariables.device.create(options.device, params.key, params.value)
|
||||
else
|
||||
throw new Error('You must specify an application or device')
|
||||
exitWithExpectedError('You must specify an application or device')
|
||||
.nodeify(done)
|
||||
|
||||
exports.rename =
|
||||
|
@ -18,6 +18,7 @@ _ = require('lodash')
|
||||
capitano = require('capitano')
|
||||
columnify = require('columnify')
|
||||
messages = require('../utils/messages')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
parse = (object) ->
|
||||
return _.fromPairs _.map object, (item) ->
|
||||
@ -55,8 +56,6 @@ general = (params, options, done) ->
|
||||
return command.hidden or command.isWildcard()
|
||||
|
||||
groupedCommands = _.groupBy commands, (command) ->
|
||||
if command.plugin
|
||||
return 'plugins'
|
||||
if command.primary
|
||||
return 'primary'
|
||||
return 'secondary'
|
||||
@ -64,10 +63,6 @@ general = (params, options, done) ->
|
||||
print(parse(groupedCommands.primary))
|
||||
|
||||
if options.verbose
|
||||
if not _.isEmpty(groupedCommands.plugins)
|
||||
console.log('\nInstalled plugins:\n')
|
||||
print(parse(groupedCommands.plugins))
|
||||
|
||||
console.log('\nAdditional commands:\n')
|
||||
print(parse(groupedCommands.secondary))
|
||||
else
|
||||
@ -84,7 +79,7 @@ command = (params, options, done) ->
|
||||
return done(error) if error?
|
||||
|
||||
if not command? or command.isWildcard()
|
||||
return done(new Error("Command not found: #{params.command}"))
|
||||
exitWithExpectedError("Command not found: #{params.command}")
|
||||
|
||||
console.log("Usage: #{command.signature}")
|
||||
|
||||
|
@ -16,9 +16,10 @@ limitations under the License.
|
||||
|
||||
module.exports =
|
||||
wizard: require('./wizard')
|
||||
apiKey: require('./api-key')
|
||||
app: require('./app')
|
||||
info: require('./info')
|
||||
auth: require('./auth')
|
||||
info: require('./info')
|
||||
device: require('./device')
|
||||
env: require('./environment-variables')
|
||||
keys: require('./keys')
|
||||
@ -36,3 +37,6 @@ module.exports =
|
||||
deploy: require('./deploy')
|
||||
util: require('./util')
|
||||
preload: require('./preload')
|
||||
push: require('./push')
|
||||
join: require('./join')
|
||||
leave: require('./leave')
|
||||
|
@ -35,3 +35,53 @@ exports.osInit =
|
||||
init.initialize(params.image, params.type, config)
|
||||
.then(helpers.osProgressHandler)
|
||||
.nodeify(done)
|
||||
|
||||
exports.scanDevices =
|
||||
signature: 'internal scandevices'
|
||||
description: 'scan for local resin-enabled devices and show a picker to choose one'
|
||||
help: '''
|
||||
Don't use this command directly!
|
||||
'''
|
||||
hidden: true
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
{ forms } = require('resin-sync')
|
||||
|
||||
return Promise.try ->
|
||||
forms.selectLocalResinOsDevice()
|
||||
.then (hostnameOrIp) ->
|
||||
console.error("==> Selected device: #{hostnameOrIp}")
|
||||
.nodeify(done)
|
||||
|
||||
exports.sudo =
|
||||
signature: 'internal sudo <command>'
|
||||
description: 'execute arbitrary commands in a privileged subprocess'
|
||||
help: '''
|
||||
Don't use this command directly!
|
||||
|
||||
<command> must be passed as a single argument. That means, you need to make sure
|
||||
you enclose <command> in quotes (eg. resin internal sudo 'ls -alF') if for
|
||||
whatever reason you invoke the command directly or, typically, pass <command>
|
||||
as a single argument to spawn (eg. `spawn('resin', [ 'internal', 'sudo', 'ls -alF' ])`).
|
||||
|
||||
Furthermore, this command will naively split <command> on whitespace and directly
|
||||
forward the parts as arguments to `sudo`, so be careful.
|
||||
'''
|
||||
hidden: true
|
||||
action: (params, options, done) ->
|
||||
os = require('os')
|
||||
Promise = require('bluebird')
|
||||
|
||||
return Promise.try ->
|
||||
if os.platform() is 'win32'
|
||||
windosu = require('windosu')
|
||||
windosu.exec(params.command, {})
|
||||
else
|
||||
{ spawn } = require('child_process')
|
||||
{ wait } = require('rindle')
|
||||
cmd = params.command.split(' ')
|
||||
ps = spawn('sudo', cmd, stdio: 'inherit', env: process.env)
|
||||
wait(ps)
|
||||
|
||||
.nodeify(done)
|
||||
|
62
lib/actions/join.ts
Normal file
62
lib/actions/join.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
interface Args {
|
||||
deviceIp?: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
application?: string;
|
||||
}
|
||||
|
||||
export const join: CommandDefinition<Args, Options> = {
|
||||
signature: 'join [deviceIp]',
|
||||
description:
|
||||
'Promote a local device running unmanaged resinOS to join a resin.io application',
|
||||
help: stripIndent`
|
||||
Examples:
|
||||
|
||||
$ resin join
|
||||
$ resin join resin.local
|
||||
$ resin join resin.local --application MyApp
|
||||
$ resin join 192.168.1.25
|
||||
$ resin join 192.168.1.25 --application MyApp
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'application',
|
||||
parameter: 'application',
|
||||
alias: 'a',
|
||||
description: 'The name of the application the device should join',
|
||||
},
|
||||
],
|
||||
|
||||
primary: true,
|
||||
|
||||
async action(params, options, done) {
|
||||
const resin = await import('resin-sdk');
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = resin.fromSharedOptions();
|
||||
const logger = new Logger();
|
||||
return Bluebird.try(() => {
|
||||
return promote.join(logger, sdk, params.deviceIp, options.application);
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
@ -28,7 +28,7 @@ exports.list =
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.key.getAll().then (keys) ->
|
||||
@ -50,7 +50,7 @@ exports.info =
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.key.get(params.id).then (key) ->
|
||||
@ -82,7 +82,7 @@ exports.remove =
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the key?').then ->
|
||||
@ -109,7 +109,7 @@ exports.add =
|
||||
Promise = require('bluebird')
|
||||
readFileAsync = Promise.promisify(require('fs').readFile)
|
||||
capitano = require('capitano')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
|
||||
Promise.try ->
|
||||
return readFileAsync(params.path, encoding: 'utf8') if params.path?
|
||||
|
49
lib/actions/leave.ts
Normal file
49
lib/actions/leave.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
interface Args {
|
||||
deviceIp?: string;
|
||||
}
|
||||
|
||||
export const leave: CommandDefinition<Args, {}> = {
|
||||
signature: 'leave [deviceIp]',
|
||||
description: 'Detach a local device from its resin.io application',
|
||||
help: stripIndent`
|
||||
Examples:
|
||||
|
||||
$ resin leave
|
||||
$ resin leave resin.local
|
||||
$ resin leave 192.168.1.25
|
||||
`,
|
||||
options: [],
|
||||
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
|
||||
async action(params, _options, done) {
|
||||
const resin = await import('resin-sdk');
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = resin.fromSharedOptions();
|
||||
const logger = new Logger();
|
||||
return Bluebird.try(() => {
|
||||
return promote.leave(logger, sdk, params.deviceIp);
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
@ -1,9 +1,11 @@
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
Docker = require('docker-toolbelt')
|
||||
form = require('resin-cli-form')
|
||||
chalk = require('chalk')
|
||||
|
||||
dockerUtils = require('../../utils/docker')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
exports.dockerPort = dockerPort = 2375
|
||||
exports.dockerTimeout = dockerTimeout = 2000
|
||||
|
||||
@ -13,7 +15,7 @@ exports.filterOutSupervisorContainer = filterOutSupervisorContainer = (container
|
||||
return true
|
||||
|
||||
exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor = false) ->
|
||||
docker = new Docker(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
|
||||
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
|
||||
|
||||
# List all containers, including those not running
|
||||
docker.listContainersAsync(all: true)
|
||||
@ -22,23 +24,22 @@ exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor =
|
||||
filterOutSupervisorContainer(container)
|
||||
.then (containers) ->
|
||||
if _.isEmpty(containers)
|
||||
throw new Error("No containers found in #{deviceIp}")
|
||||
exitWithExpectedError("No containers found in #{deviceIp}")
|
||||
|
||||
return form.ask
|
||||
message: 'Select a container'
|
||||
type: 'list'
|
||||
choices: _.map containers, (container) ->
|
||||
containerName = container.Names[0] or 'Untitled'
|
||||
containerName = container.Names?[0] or 'Untitled'
|
||||
shortContainerId = ('' + container.Id).substr(0, 11)
|
||||
containerStatus = container.Status
|
||||
|
||||
return {
|
||||
name: "#{containerName} (#{shortContainerId}) - #{containerStatus}"
|
||||
name: "#{containerName} (#{shortContainerId})"
|
||||
value: container.Id
|
||||
}
|
||||
|
||||
exports.pipeContainerStream = Promise.method ({ deviceIp, name, outStream, follow = false }) ->
|
||||
docker = new Docker(host: deviceIp, port: dockerPort)
|
||||
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort)
|
||||
|
||||
container = docker.getContainer(name)
|
||||
container.inspectAsync()
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
BOOT_PARTITION = { primary: 1 }
|
||||
BOOT_PARTITION = 1
|
||||
CONNECTIONS_FOLDER = '/system-connections'
|
||||
|
||||
getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
|
||||
@ -51,6 +51,8 @@ getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
|
||||
type: 'ini'
|
||||
location:
|
||||
path: CONNECTIONS_FOLDER.slice(1)
|
||||
# Reconfix still uses the older resin-image-fs, so still needs an
|
||||
# object-based partition definition.
|
||||
partition: BOOT_PARTITION
|
||||
config_json:
|
||||
type: 'json'
|
||||
|
@ -43,12 +43,14 @@ resinPushHelp = '''
|
||||
Here is an example '.resin-sync.yml' :
|
||||
|
||||
$ cat $PWD/.resin-sync.yml
|
||||
destination: '/usr/src/app'
|
||||
before: 'echo Hello'
|
||||
after: 'echo Done'
|
||||
ignore:
|
||||
- .git
|
||||
- node_modules/
|
||||
local_resinos:
|
||||
app-name: local-app
|
||||
build-triggers:
|
||||
- Dockerfile: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
|
||||
- package.json: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
|
||||
environment:
|
||||
- MY_VARIABLE=123
|
||||
|
||||
|
||||
Command line options have precedence over the ones saved in '.resin-sync.yml'.
|
||||
|
||||
|
@ -60,10 +60,11 @@ module.exports =
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
prettyjson = require('prettyjson')
|
||||
Docker = require('docker-toolbelt')
|
||||
{ discover } = require('resin-sync')
|
||||
{ SpinnerPromise } = require('resin-cli-visuals')
|
||||
{ dockerPort, dockerTimeout } = require('./common')
|
||||
dockerUtils = require('../../utils/docker')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
if options.timeout?
|
||||
options.timeout *= 1000
|
||||
@ -75,15 +76,15 @@ module.exports =
|
||||
stopMessage: 'Reporting scan results'
|
||||
.filter ({ address }) ->
|
||||
Promise.try ->
|
||||
docker = new Docker(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
docker.pingAsync()
|
||||
.return(true)
|
||||
.catchReturn(false)
|
||||
.tap (devices) ->
|
||||
if _.isEmpty(devices)
|
||||
throw new Error('Could not find any resinOS devices in the local network')
|
||||
exitWithExpectedError('Could not find any resinOS devices in the local network')
|
||||
.map ({ host, address }) ->
|
||||
docker = new Docker(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
Promise.props
|
||||
dockerInfo: docker.infoAsync().catchReturn('Could not get Docker info')
|
||||
dockerVersion: docker.versionAsync().catchReturn('Could not get Docker version')
|
||||
|
@ -67,10 +67,12 @@ module.exports =
|
||||
Promise = require 'bluebird'
|
||||
_ = require('lodash')
|
||||
{ forms } = require('resin-sync')
|
||||
|
||||
{ selectContainerFromDevice, getSubShellCommand } = require('./common')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
if (options.host is true and options.container?)
|
||||
throw new Error('Please pass either --host or --container option')
|
||||
exitWithExpectedError('Please pass either --host or --container option')
|
||||
|
||||
if not options.port?
|
||||
options.port = 22222
|
||||
|
@ -26,10 +26,6 @@ module.exports =
|
||||
|
||||
To continuously stream output, and see new logs in real time, use the `--tail` option.
|
||||
|
||||
Note that for now you need to provide the whole UUID for this command to work correctly.
|
||||
|
||||
This is due to some technical limitations that we plan to address soon.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin logs 23c73a1
|
||||
@ -47,25 +43,19 @@ module.exports =
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
moment = require('moment')
|
||||
|
||||
printLine = (line) ->
|
||||
timestamp = moment(line.timestamp).format('DD.MM.YY HH:mm:ss (ZZ)')
|
||||
console.log("#{timestamp} #{line.message}")
|
||||
|
||||
promise = resin.logs.history(params.uuid).each(printLine)
|
||||
|
||||
if not options.tail
|
||||
|
||||
# PubNub keeps the process alive after a history query.
|
||||
# Until this is fixed, we force the process to exit.
|
||||
# This of course prevents this command to be used programatically
|
||||
return promise.catch(done).finally ->
|
||||
process.exit(0)
|
||||
|
||||
promise.then ->
|
||||
resin.logs.subscribe(params.uuid).then (logs) ->
|
||||
if options.tail
|
||||
resin.logs.subscribe(params.uuid, { count: 100 }).then (logs) ->
|
||||
logs.on('line', printLine)
|
||||
logs.on('error', done)
|
||||
.catch(done)
|
||||
.catch(done)
|
||||
else
|
||||
resin.logs.history(params.uuid)
|
||||
.each(printLine)
|
||||
.catch(done)
|
||||
|
@ -43,11 +43,13 @@ exports.set =
|
||||
normalizeUuidProp(options, 'device')
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
Promise.try ->
|
||||
if _.isEmpty(params.note)
|
||||
throw new Error('Missing note content')
|
||||
exitWithExpectedError('Missing note content')
|
||||
|
||||
resin.models.device.note(options.device, params.note)
|
||||
.nodeify(done)
|
||||
|
@ -31,7 +31,7 @@ resolveVersion = (deviceType, version) ->
|
||||
return Promise.resolve(version)
|
||||
|
||||
form = require('resin-cli-form')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
|
||||
resin.models.os.getSupportedVersions(deviceType)
|
||||
.then ({ versions, recommended }) ->
|
||||
@ -57,7 +57,7 @@ exports.versions =
|
||||
$ resin os versions raspberrypi3
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
|
||||
resin.models.os.getSupportedVersions(params.type)
|
||||
.then ({ versions, recommended }) ->
|
||||
@ -203,17 +203,20 @@ exports.configure =
|
||||
Use this command to configure a previously downloaded operating system image for
|
||||
the specific device or for an application generally.
|
||||
|
||||
Calling this command without --version is not recommended, and may fail in
|
||||
future releases if the OS version cannot be inferred.
|
||||
|
||||
Note that device api keys are only supported on ResinOS 2.0.3+.
|
||||
|
||||
This comand still supports the *deprecated* format where the UUID and optionally device key
|
||||
This command still supports the *deprecated* format where the UUID and optionally device key
|
||||
are passed directly on the command line, but the recommended way is to pass either an --app or
|
||||
--device argument. The deprecated format will be remove in a future release.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6 --deviceApiKey <existingDeviceKey>
|
||||
$ resin os configure ../path/rpi.img --app MyApp
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
|
||||
$ resin os configure ../path/rpi.img --app MyApp --version 2.12.7
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
@ -221,6 +224,7 @@ exports.configure =
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
commandOptions.optionalDeviceApiKey
|
||||
commandOptions.optionalOsVersion
|
||||
{
|
||||
signature: 'config'
|
||||
description: 'path to the config JSON file, see `resin os build-config`'
|
||||
@ -233,7 +237,7 @@ exports.configure =
|
||||
fs = require('fs')
|
||||
Promise = require('bluebird')
|
||||
readFileAsync = Promise.promisify(fs.readFile)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
init = require('resin-device-init')
|
||||
helpers = require('../utils/helpers')
|
||||
patterns = require('../utils/patterns')
|
||||
@ -244,7 +248,7 @@ exports.configure =
|
||||
options.application
|
||||
params.uuid
|
||||
]).length != 1
|
||||
patterns.expectedError '''
|
||||
patterns.exitWithExpectedError '''
|
||||
To configure an image, you must provide exactly one of:
|
||||
|
||||
* A device, with --device <uuid>
|
||||
@ -279,6 +283,8 @@ exports.configure =
|
||||
.then(JSON.parse)
|
||||
return buildConfig(params.image, appOrDevice.device_type, options.advanced)
|
||||
.then (answers) ->
|
||||
answers.version = options.version
|
||||
|
||||
(if configurationResourceType == 'device'
|
||||
generateDeviceConfig(appOrDevice, deviceApiKey, answers)
|
||||
else
|
||||
|
@ -18,38 +18,60 @@ dockerUtils = require('../utils/docker')
|
||||
|
||||
LATEST = 'latest'
|
||||
|
||||
allDeviceTypes = undefined
|
||||
|
||||
getDeviceTypes = ->
|
||||
Bluebird = require('bluebird')
|
||||
if allDeviceTypes != undefined
|
||||
return Bluebird.resolve(allDeviceTypes)
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
resin.models.config.getDeviceTypes()
|
||||
.tap (dt) ->
|
||||
allDeviceTypes = dt
|
||||
|
||||
getDeviceTypesWithSameArch = (deviceTypeSlug) ->
|
||||
_ = require('lodash')
|
||||
|
||||
getDeviceTypes()
|
||||
.then (deviceTypes) ->
|
||||
deviceType = _.find(deviceTypes, slug: deviceTypeSlug)
|
||||
_(deviceTypes).filter(arch: deviceType.arch).map('slug').value()
|
||||
|
||||
getApplicationsWithSuccessfulBuilds = (deviceType) ->
|
||||
preload = require('resin-preload')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
|
||||
resin.pine.get
|
||||
resource: 'my_application'
|
||||
options:
|
||||
filter:
|
||||
device_type: deviceType
|
||||
build:
|
||||
$any:
|
||||
$alias: 'b'
|
||||
$expr:
|
||||
b:
|
||||
status: 'success'
|
||||
expand: preload.applicationExpandOptions
|
||||
select: [ 'id', 'app_name', 'device_type', 'commit' ]
|
||||
orderby: 'app_name asc'
|
||||
getDeviceTypesWithSameArch(deviceType)
|
||||
.then (deviceTypes) ->
|
||||
resin.pine.get
|
||||
resource: 'my_application'
|
||||
options:
|
||||
$filter:
|
||||
device_type:
|
||||
$in: deviceTypes
|
||||
owns__release:
|
||||
$any:
|
||||
$alias: 'r'
|
||||
$expr:
|
||||
r:
|
||||
status: 'success'
|
||||
$expand: preload.applicationExpandOptions
|
||||
$select: [ 'id', 'app_name', 'device_type', 'commit', 'should_track_latest_release' ]
|
||||
$orderby: 'app_name asc'
|
||||
|
||||
selectApplication = (deviceType) ->
|
||||
visuals = require('resin-cli-visuals')
|
||||
form = require('resin-cli-form')
|
||||
{ expectedError } = require('../utils/patterns')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and builds.')
|
||||
applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and releases.')
|
||||
applicationInfoSpinner.start()
|
||||
|
||||
getApplicationsWithSuccessfulBuilds(deviceType)
|
||||
.then (applications) ->
|
||||
applicationInfoSpinner.stop()
|
||||
if applications.length == 0
|
||||
expectedError("You have no apps with successful builds for a '#{deviceType}' device type.")
|
||||
exitWithExpectedError("You have no apps with successful releases for a '#{deviceType}' device type.")
|
||||
form.ask
|
||||
message: 'Select an application'
|
||||
type: 'list'
|
||||
@ -57,25 +79,25 @@ selectApplication = (deviceType) ->
|
||||
name: app.app_name
|
||||
value: app
|
||||
|
||||
selectApplicationCommit = (builds) ->
|
||||
selectApplicationCommit = (releases) ->
|
||||
form = require('resin-cli-form')
|
||||
{ expectedError } = require('../utils/patterns')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if builds.length == 0
|
||||
expectedError('This application has no successful builds.')
|
||||
if releases.length == 0
|
||||
exitWithExpectedError('This application has no successful releases.')
|
||||
DEFAULT_CHOICE = { 'name': LATEST, 'value': LATEST }
|
||||
choices = [ DEFAULT_CHOICE ].concat builds.map (build) ->
|
||||
name: "#{build.push_timestamp} - #{build.commit_hash}"
|
||||
value: build.commit_hash
|
||||
choices = [ DEFAULT_CHOICE ].concat releases.map (release) ->
|
||||
name: "#{release.end_timestamp} - #{release.commit}"
|
||||
value: release.commit
|
||||
return form.ask
|
||||
message: 'Select a build'
|
||||
message: 'Select a release'
|
||||
type: 'list'
|
||||
default: LATEST
|
||||
choices: choices
|
||||
|
||||
offerToDisableAutomaticUpdates = (application, commit) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
|
||||
if commit == LATEST or not application.should_track_latest_release
|
||||
@ -84,9 +106,12 @@ offerToDisableAutomaticUpdates = (application, commit) ->
|
||||
|
||||
This application is set to automatically update all devices to the latest available version.
|
||||
This might be unexpected behaviour: with this enabled, the preloaded device will still
|
||||
download and install the latest build once it is online.
|
||||
download and install the latest release once it is online.
|
||||
|
||||
Do you want to disable automatic updates for this application?
|
||||
|
||||
Warning: To re-enable this requires direct api calls,
|
||||
see https://docs.resin.io/reference/api/resources/device/#set-device-to-release
|
||||
'''
|
||||
form.ask
|
||||
message: message,
|
||||
@ -109,12 +134,12 @@ module.exports =
|
||||
please check the README here: https://github.com/resin-io/resin-cli .
|
||||
|
||||
Use this command to preload an application to a local disk image (or
|
||||
Edison zip archive) with a built commit from Resin.io.
|
||||
This can be used with cloud builds, or images deployed with resin deploy.
|
||||
Edison zip archive) with a built release from Resin.io.
|
||||
|
||||
Examples:
|
||||
$ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png
|
||||
$ resin preload resin.img
|
||||
|
||||
$ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png
|
||||
$ resin preload resin.img
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
@ -129,7 +154,7 @@ module.exports =
|
||||
signature: 'commit'
|
||||
parameter: 'hash'
|
||||
description: '''
|
||||
a specific application commit to preload, use "latest" to specify the latest commit
|
||||
the commit hash for a specific application release to preload, use "latest" to specify the latest release
|
||||
(ignored if no appId is given)
|
||||
'''
|
||||
alias: 'c'
|
||||
@ -141,20 +166,25 @@ module.exports =
|
||||
alias: 's'
|
||||
}
|
||||
{
|
||||
signature: 'dont-check-device-type'
|
||||
signature: 'dont-check-arch'
|
||||
boolean: true
|
||||
description: 'Disables check for matching device types in image and application'
|
||||
description: 'Disables check for matching architecture in image and application'
|
||||
}
|
||||
{
|
||||
signature: 'pin-device-to-release'
|
||||
boolean: true
|
||||
description: 'Pin the preloaded device to the preloaded release on provision'
|
||||
alias: 'p'
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
preload = require('resin-preload')
|
||||
errors = require('resin-errors')
|
||||
visuals = require('resin-cli-visuals')
|
||||
nodeCleanup = require('node-cleanup')
|
||||
{ expectedError } = require('../utils/patterns')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
progressBars = {}
|
||||
|
||||
@ -183,21 +213,28 @@ module.exports =
|
||||
options.splashImage = options['splash-image']
|
||||
delete options['splash-image']
|
||||
|
||||
if options['dont-check-device-type'] and not options.appId
|
||||
expectedError('You need to specify an app id if you disable the device type check.')
|
||||
options.dontCheckArch = options['dont-check-arch'] || false
|
||||
delete options['dont-check-arch']
|
||||
if options.dontCheckArch and not options.appId
|
||||
exitWithExpectedError('You need to specify an app id if you disable the architecture check.')
|
||||
|
||||
options.pinDevice = options['pin-device-to-release'] || false
|
||||
delete options['pin-device-to-release']
|
||||
|
||||
# Get a configured dockerode instance
|
||||
dockerUtils.getDocker(options)
|
||||
.then (docker) ->
|
||||
|
||||
preloader = new preload.Preloader(
|
||||
resin,
|
||||
docker,
|
||||
options.appId,
|
||||
options.commit,
|
||||
options.image,
|
||||
options.splashImage,
|
||||
options.proxy,
|
||||
resin
|
||||
docker
|
||||
options.appId
|
||||
options.commit
|
||||
options.image
|
||||
options.splashImage
|
||||
options.proxy
|
||||
options.dontCheckArch
|
||||
options.pinDevice
|
||||
)
|
||||
|
||||
gotSignal = false
|
||||
@ -221,51 +258,39 @@ module.exports =
|
||||
return new Promise (resolve, reject) ->
|
||||
preloader.on('error', reject)
|
||||
|
||||
preloader.build()
|
||||
preloader.prepare()
|
||||
.then ->
|
||||
preloader.prepare()
|
||||
.then ->
|
||||
preloader.getDeviceTypeAndPreloadedBuilds()
|
||||
.then (info) ->
|
||||
# If no appId was provided, show a list of matching apps
|
||||
Promise.try ->
|
||||
if options.appId
|
||||
return preloader.fetchApplication()
|
||||
.catch(errors.ResinApplicationNotFound, expectedError)
|
||||
selectApplication(info.device_type)
|
||||
.then (application) ->
|
||||
preloader.setApplication(application)
|
||||
# Check that the app device type and the image device type match
|
||||
if not options['dont-check-device-type'] and info.device_type != application.device_type
|
||||
expectedError(
|
||||
"Image device type (#{info.device_type}) and application device type (#{application.device_type}) do not match"
|
||||
)
|
||||
if not preloader.appId
|
||||
selectApplication(preloader.config.deviceType)
|
||||
.then (application) ->
|
||||
preloader.setApplication(application)
|
||||
.then ->
|
||||
# Use the commit given as --commit or show an interactive commit selection menu
|
||||
Promise.try ->
|
||||
if options.commit
|
||||
if options.commit == LATEST and preloader.application.commit
|
||||
# handle `--commit latest`
|
||||
return LATEST
|
||||
release = _.find preloader.application.owns__release, (release) ->
|
||||
release.commit.startsWith(options.commit)
|
||||
if not release
|
||||
exitWithExpectedError('There is no release matching this commit')
|
||||
return release.commit
|
||||
selectApplicationCommit(preloader.application.owns__release)
|
||||
.then (commit) ->
|
||||
if commit == LATEST
|
||||
preloader.commit = preloader.application.commit
|
||||
else
|
||||
preloader.commit = commit
|
||||
|
||||
# Use the commit given as --commit or show an interactive commit selection menu
|
||||
Promise.try ->
|
||||
if options.commit
|
||||
if options.commit == LATEST and application.commit
|
||||
# handle `--commit latest`
|
||||
return LATEST
|
||||
else if not _.find(application.build, commit_hash: options.commit)
|
||||
expectedError('There is no build matching this commit')
|
||||
return options.commit
|
||||
selectApplicationCommit(application.build)
|
||||
.then (commit) ->
|
||||
if commit == LATEST
|
||||
preloader.commit = application.commit
|
||||
else
|
||||
preloader.commit = commit
|
||||
|
||||
# Propose to disable automatic app updates if the commit is not the latest
|
||||
offerToDisableAutomaticUpdates(application, commit)
|
||||
.then ->
|
||||
builds = info.preloaded_builds.map (build) ->
|
||||
build.slice(-preload.BUILD_HASH_LENGTH)
|
||||
if preloader.commit in builds
|
||||
throw new preload.errors.ResinError('This build is already preloaded in this image.')
|
||||
# All options are ready: preload the image.
|
||||
preloader.preload()
|
||||
.catch(preload.errors.ResinError, expectedError)
|
||||
# Propose to disable automatic app updates if the commit is not the latest
|
||||
offerToDisableAutomaticUpdates(preloader.application, commit)
|
||||
.then ->
|
||||
# All options are ready: preload the image.
|
||||
preloader.preload()
|
||||
.catch(resin.errors.ResinError, exitWithExpectedError)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.then(done)
|
||||
|
229
lib/actions/push.ts
Normal file
229
lib/actions/push.ts
Normal file
@ -0,0 +1,229 @@
|
||||
/*
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
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 { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import { ResinSDK } from 'resin-sdk';
|
||||
|
||||
import { BuildError } from '../utils/device/errors';
|
||||
|
||||
// An regex to detect an IP address, from https://www.regular-expressions.info/ip.html
|
||||
const IP_REGEX = new RegExp(
|
||||
/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/,
|
||||
);
|
||||
|
||||
enum BuildTarget {
|
||||
Cloud,
|
||||
Device,
|
||||
}
|
||||
|
||||
function getBuildTarget(appOrDevice: string): BuildTarget | null {
|
||||
// First try the application regex from the api
|
||||
if (/^[a-zA-Z0-9_-]+$/.test(appOrDevice)) {
|
||||
return BuildTarget.Cloud;
|
||||
}
|
||||
|
||||
if (IP_REGEX.test(appOrDevice)) {
|
||||
return BuildTarget.Device;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getAppOwner(sdk: ResinSDK, appName: string) {
|
||||
const {
|
||||
exitWithExpectedError,
|
||||
selectFromList,
|
||||
} = await import('../utils/patterns');
|
||||
const _ = await import('lodash');
|
||||
|
||||
const applications = await sdk.models.application.getAll({
|
||||
$expand: {
|
||||
user: {
|
||||
$select: ['username'],
|
||||
},
|
||||
},
|
||||
$filter: {
|
||||
app_name: appName,
|
||||
},
|
||||
$select: ['id'],
|
||||
});
|
||||
|
||||
if (applications == null || applications.length === 0) {
|
||||
exitWithExpectedError(
|
||||
stripIndent`
|
||||
No applications found with name: ${appName}.
|
||||
|
||||
This could mean that the application does not exist, or you do
|
||||
not have the permissions required to access it.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (applications.length === 1) {
|
||||
return _.get(applications, '[0].user[0].username');
|
||||
}
|
||||
|
||||
// If we got more than one application with the same name it means that the
|
||||
// user has access to a collab app with the same name as a personal app. We
|
||||
// present a list to the user which shows the fully qualified application
|
||||
// name (user/appname) and allows them to select
|
||||
const entries = _.map(applications, app => {
|
||||
const username = _.get(app, 'user[0].username');
|
||||
return {
|
||||
name: `${username}/${appName}`,
|
||||
extra: username,
|
||||
};
|
||||
});
|
||||
|
||||
const selected = await selectFromList(
|
||||
`${
|
||||
entries.length
|
||||
} applications found with that name, please select the application you would like to push to`,
|
||||
entries,
|
||||
);
|
||||
|
||||
return selected.extra;
|
||||
}
|
||||
|
||||
export const push: CommandDefinition<
|
||||
{
|
||||
applicationOrDevice: string;
|
||||
},
|
||||
{
|
||||
source: string;
|
||||
emulated: boolean;
|
||||
nocache: boolean;
|
||||
}
|
||||
> = {
|
||||
signature: 'push <applicationOrDevice>',
|
||||
description:
|
||||
'Start a remote build on the resin.io cloud build servers or a local mode device',
|
||||
help: stripIndent`
|
||||
This command can be used to start a build on the remote
|
||||
resin.io cloud builders, or a local mode resin device.
|
||||
|
||||
When building on the resin cloud the given source directory will be sent to the
|
||||
resin.io builder, and the build will proceed. This can be used as a drop-in
|
||||
replacement for git push to deploy.
|
||||
|
||||
When building on a local mode device, the given source directory will be built on
|
||||
device, and the resulting containers will be run on the device. Logs will be
|
||||
streamed back from the device as part of the same invocation.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin push myApp
|
||||
$ resin push myApp --source <source directory>
|
||||
$ resin push myApp -s <source directory>
|
||||
|
||||
$ resin push 10.0.0.1
|
||||
$ resin push 10.0.0.1 --source <source directory>
|
||||
$ resin push 10.0.0.1 -s <source directory>
|
||||
`,
|
||||
permission: 'user',
|
||||
options: [
|
||||
{
|
||||
signature: 'source',
|
||||
alias: 's',
|
||||
description:
|
||||
'The source that should be sent to the resin builder to be built (defaults to the current directory)',
|
||||
parameter: 'source',
|
||||
},
|
||||
{
|
||||
signature: 'emulated',
|
||||
alias: 'e',
|
||||
description: 'Force an emulated build to occur on the remote builder',
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'nocache',
|
||||
alias: 'c',
|
||||
description: "Don't use cache when building this project",
|
||||
boolean: true,
|
||||
},
|
||||
],
|
||||
async action(params, options, done) {
|
||||
const sdk = (await import('resin-sdk')).fromSharedOptions();
|
||||
const Bluebird = await import('bluebird');
|
||||
const remote = await import('../utils/remote-build');
|
||||
const deviceDeploy = await import('../utils/device/deploy');
|
||||
const { exitWithExpectedError } = await import('../utils/patterns');
|
||||
|
||||
const appOrDevice: string | null = params.applicationOrDevice;
|
||||
if (appOrDevice == null) {
|
||||
exitWithExpectedError('You must specify an application or a device');
|
||||
}
|
||||
|
||||
const source = options.source || '.';
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[debug] Using ${source} as build source`);
|
||||
}
|
||||
|
||||
const buildTarget = getBuildTarget(appOrDevice);
|
||||
switch (buildTarget) {
|
||||
case BuildTarget.Cloud:
|
||||
const app = appOrDevice;
|
||||
Bluebird.join(
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('resinUrl'),
|
||||
getAppOwner(sdk, app),
|
||||
(token, baseUrl, owner) => {
|
||||
const opts = {
|
||||
emulated: options.emulated,
|
||||
nocache: options.nocache,
|
||||
};
|
||||
const args = {
|
||||
app,
|
||||
owner,
|
||||
source,
|
||||
auth: token,
|
||||
baseUrl,
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
|
||||
return remote.startRemoteBuild(args);
|
||||
},
|
||||
).nodeify(done);
|
||||
break;
|
||||
case BuildTarget.Device:
|
||||
const device = appOrDevice;
|
||||
// TODO: Support passing a different port
|
||||
Bluebird.resolve(
|
||||
deviceDeploy.deployToDevice({
|
||||
source,
|
||||
deviceHost: device,
|
||||
}),
|
||||
)
|
||||
.catch(BuildError, e => {
|
||||
exitWithExpectedError(e.toString());
|
||||
})
|
||||
.nodeify(done);
|
||||
break;
|
||||
default:
|
||||
exitWithExpectedError(
|
||||
stripIndent`
|
||||
Build target not recognised. Please provide either an application name or device address.
|
||||
|
||||
The only supported device addresses currently are IP addresses.
|
||||
|
||||
If you believe your build target should have been detected, and this is an error, please
|
||||
create an issue.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
@ -108,7 +108,7 @@ module.exports =
|
||||
console.info("Connecting to: #{uuid}")
|
||||
resin.models.device.get(uuid)
|
||||
.then (device) ->
|
||||
throw new Error('Device is not online') if not device.is_online
|
||||
patterns.exitWithExpectedError('Device is not online') if not device.is_online
|
||||
|
||||
Promise.props
|
||||
username: resin.auth.whoami()
|
||||
|
@ -34,29 +34,28 @@ exports.wizard =
|
||||
'''
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
|
||||
resin.auth.isLoggedIn().then (isLoggedIn) ->
|
||||
return if isLoggedIn
|
||||
console.info('Looks like you\'re not logged in yet!')
|
||||
console.info('Lets go through a quick wizard to get you started.\n')
|
||||
return capitanoRunAsync('login')
|
||||
console.info("Let's go through a quick wizard to get you started.\n")
|
||||
return runCommand('login')
|
||||
.then ->
|
||||
return if params.name?
|
||||
patterns.selectOrCreateApplication().tap (applicationName) ->
|
||||
resin.models.application.has(applicationName).then (hasApplication) ->
|
||||
return applicationName if hasApplication
|
||||
capitanoRunAsync("app create #{applicationName}")
|
||||
runCommand("app create #{applicationName}")
|
||||
.then (applicationName) ->
|
||||
params.name = applicationName
|
||||
.then ->
|
||||
return capitanoRunAsync("device init --application #{params.name}")
|
||||
return runCommand("device init --application #{params.name}")
|
||||
.tap(patterns.awaitDevice)
|
||||
.then (uuid) ->
|
||||
return capitanoRunAsync("device #{uuid}")
|
||||
return runCommand("device #{uuid}")
|
||||
.then ->
|
||||
return resin.models.application.get(params.name)
|
||||
.then (application) ->
|
||||
|
@ -17,7 +17,8 @@ limitations under the License.
|
||||
Raven = require('raven')
|
||||
Raven.disableConsoleAlerts()
|
||||
Raven.config require('./config').sentryDsn,
|
||||
captureUnhandledRejections: true
|
||||
captureUnhandledRejections: true,
|
||||
autoBreadcrumbs: true,
|
||||
release: require('../package.json').version
|
||||
.install (logged, error) ->
|
||||
console.error(error)
|
||||
@ -62,20 +63,21 @@ capitanoExecuteAsync = Promise.promisify(capitano.execute)
|
||||
|
||||
# We don't yet use resin-sdk directly everywhere, but we set up shared
|
||||
# options correctly so we can do safely in submodules
|
||||
require('resin-sdk').setSharedOptions(
|
||||
ResinSdk = require('resin-sdk')
|
||||
ResinSdk.setSharedOptions(
|
||||
apiUrl: settings.get('apiUrl')
|
||||
imageMakerUrl: settings.get('imageMakerUrl')
|
||||
dataDirectory: settings.get('dataDirectory')
|
||||
retries: 2
|
||||
)
|
||||
# Keep using sdk-preconfigured for now, but only temporarily
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
resin = ResinSdk.fromSharedOptions()
|
||||
|
||||
actions = require('./actions')
|
||||
errors = require('./errors')
|
||||
events = require('./events')
|
||||
plugins = require('./utils/plugins')
|
||||
update = require('./utils/update')
|
||||
{ exitWithExpectedError } = require('./utils/patterns')
|
||||
|
||||
# Assign bluebird as the global promise library
|
||||
# stream-to-promise will produce native promises if not
|
||||
@ -86,13 +88,13 @@ require('any-promise/register/bluebird')
|
||||
capitano.permission 'user', (done) ->
|
||||
resin.auth.isLoggedIn().then (isLoggedIn) ->
|
||||
if not isLoggedIn
|
||||
throw new Error '''
|
||||
exitWithExpectedError('''
|
||||
You have to log in to continue
|
||||
|
||||
Run the following command to go through the login wizard:
|
||||
|
||||
$ resin login
|
||||
'''
|
||||
''')
|
||||
.nodeify(done)
|
||||
|
||||
capitano.command
|
||||
@ -114,11 +116,8 @@ capitano.command(actions.help.help)
|
||||
# ---------- Wizard Module ----------
|
||||
capitano.command(actions.wizard.wizard)
|
||||
|
||||
# ---------- Auth Module ----------
|
||||
capitano.command(actions.auth.login)
|
||||
capitano.command(actions.auth.logout)
|
||||
capitano.command(actions.auth.signup)
|
||||
capitano.command(actions.auth.whoami)
|
||||
# ---------- Api key module ----------
|
||||
capitano.command(actions.apiKey.generate)
|
||||
|
||||
# ---------- App Module ----------
|
||||
capitano.command(actions.app.create)
|
||||
@ -127,6 +126,12 @@ capitano.command(actions.app.remove)
|
||||
capitano.command(actions.app.restart)
|
||||
capitano.command(actions.app.info)
|
||||
|
||||
# ---------- Auth Module ----------
|
||||
capitano.command(actions.auth.login)
|
||||
capitano.command(actions.auth.logout)
|
||||
capitano.command(actions.auth.signup)
|
||||
capitano.command(actions.auth.whoami)
|
||||
|
||||
# ---------- Device Module ----------
|
||||
capitano.command(actions.device.list)
|
||||
capitano.command(actions.device.supported)
|
||||
@ -202,22 +207,28 @@ capitano.command(actions.util.availableDrives)
|
||||
|
||||
# ---------- Internal utils ----------
|
||||
capitano.command(actions.internal.osInit)
|
||||
capitano.command(actions.internal.scanDevices)
|
||||
capitano.command(actions.internal.sudo)
|
||||
|
||||
#------------ Local build and deploy -------
|
||||
capitano.command(actions.build)
|
||||
capitano.command(actions.deploy)
|
||||
|
||||
#------------ Push/remote builds -------
|
||||
capitano.command(actions.push.push)
|
||||
|
||||
#------------ Join/Leave -------
|
||||
capitano.command(actions.join.join)
|
||||
capitano.command(actions.leave.leave)
|
||||
|
||||
update.notify()
|
||||
|
||||
plugins.register(/^resin-plugin-(.+)$/).then ->
|
||||
cli = capitano.parse(process.argv)
|
||||
|
||||
runCommand = ->
|
||||
if cli.global?.help
|
||||
capitanoExecuteAsync(command: "help #{cli.command ? ''}")
|
||||
else
|
||||
capitanoExecuteAsync(cli)
|
||||
|
||||
Promise.all([events.trackCommand(cli), runCommand()])
|
||||
cli = capitano.parse(process.argv)
|
||||
runCommand = ->
|
||||
if cli.global?.help
|
||||
capitanoExecuteAsync(command: "help #{cli.command ? ''}")
|
||||
else
|
||||
capitanoExecuteAsync(cli)
|
||||
|
||||
Promise.all([events.trackCommand(cli), runCommand()])
|
||||
.catch(errors.handle)
|
||||
|
@ -19,7 +19,7 @@ limitations under the License.
|
||||
###
|
||||
|
||||
open = require('opn')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
server = require('./server')
|
||||
utils = require('./utils')
|
||||
|
||||
@ -56,7 +56,7 @@ exports.login = ->
|
||||
# Leave a bit of time for the
|
||||
# server to get up and runing
|
||||
setTimeout ->
|
||||
open(loginUrl)
|
||||
open(loginUrl, { wait: false })
|
||||
, 1000
|
||||
|
||||
return server.awaitForToken(options)
|
||||
|
@ -77,9 +77,9 @@ exports.awaitForToken = (options) ->
|
||||
Promise.try ->
|
||||
if not token
|
||||
throw new Error('No token')
|
||||
return utils.isTokenValid(token)
|
||||
.tap (isValid) ->
|
||||
if not isValid
|
||||
return utils.loginIfTokenValid(token)
|
||||
.tap (loggedIn) ->
|
||||
if not loggedIn
|
||||
throw new Error('Invalid token')
|
||||
.then ->
|
||||
renderAndDone({ request, response, viewName: 'success', token })
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
_ = require('lodash')
|
||||
url = require('url')
|
||||
Promise = require('bluebird')
|
||||
@ -42,7 +42,7 @@ exports.getDashboardLoginURL = (callbackUrl) ->
|
||||
return url.resolve(dashboardUrl, "/login/cli/#{callbackUrl}")
|
||||
|
||||
###*
|
||||
# @summary Check if a token is valid
|
||||
# @summary Log in using a token, but only if the token is valid
|
||||
# @function
|
||||
# @protected
|
||||
#
|
||||
@ -50,21 +50,26 @@ exports.getDashboardLoginURL = (callbackUrl) ->
|
||||
# This function checks that the token is not only well-structured
|
||||
# but that it also authenticates with the server successfully.
|
||||
#
|
||||
# @param {String} sessionToken - token
|
||||
# @fulfil {Boolean} - whether is valid or not
|
||||
# If authenticated, the token is persisted, if not then the previous
|
||||
# login state is restored.
|
||||
#
|
||||
# @param {String} token - session token or api key
|
||||
# @fulfil {Boolean} - whether the login was successful or not
|
||||
# @returns {Promise}
|
||||
#
|
||||
# utils.isTokenValid('...').then (isValid) ->
|
||||
# if isValid
|
||||
# utils.loginIfTokenValid('...').then (loggedIn) ->
|
||||
# if loggedIn
|
||||
# console.log('Token is valid!')
|
||||
###
|
||||
exports.isTokenValid = (sessionToken) ->
|
||||
if not sessionToken? or _.isEmpty(sessionToken.trim())
|
||||
exports.loginIfTokenValid = (token) ->
|
||||
if not token? or _.isEmpty(token.trim())
|
||||
return Promise.resolve(false)
|
||||
|
||||
return resin.token.get().then (currentToken) ->
|
||||
resin.auth.loginWithToken(sessionToken)
|
||||
.return(sessionToken)
|
||||
return resin.auth.getToken()
|
||||
.catchReturn(undefined)
|
||||
.then (currentToken) ->
|
||||
resin.auth.loginWithToken(token)
|
||||
.return(token)
|
||||
.then(resin.auth.isLoggedIn)
|
||||
.tap (isLoggedIn) ->
|
||||
return if isLoggedIn
|
||||
|
@ -14,18 +14,98 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import errors = require('resin-cli-errors');
|
||||
import patterns = require('./utils/patterns');
|
||||
import Raven = require('raven');
|
||||
import Promise = require('bluebird');
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as Raven from 'raven';
|
||||
import * as Promise from 'bluebird';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import * as patterns from './utils/patterns';
|
||||
|
||||
const captureException = Promise.promisify<string, Error>(
|
||||
Raven.captureException,
|
||||
{ context: Raven },
|
||||
);
|
||||
|
||||
function hasCode(error: any): error is Error & { code: string } {
|
||||
return error.code != null;
|
||||
}
|
||||
|
||||
function treatFailedBindingAsMissingModule(error: any): void {
|
||||
if (error.message.startsWith('Could not locate the bindings file.')) {
|
||||
error.code = 'MODULE_NOT_FOUND';
|
||||
}
|
||||
}
|
||||
|
||||
function interpret(error: any): string | undefined {
|
||||
if (!(error instanceof Error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
treatFailedBindingAsMissingModule(error);
|
||||
|
||||
if (hasCode(error)) {
|
||||
const errorCodeHandler = messages[error.code];
|
||||
const message = errorCodeHandler && errorCodeHandler(error);
|
||||
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(error.message)) {
|
||||
return `${error.code}: ${error.message}`;
|
||||
}
|
||||
} else {
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
const messages: {
|
||||
[key: string]: (error: Error & { path?: string }) => string;
|
||||
} = {
|
||||
EISDIR: error => `File is a directory: ${error.path}`,
|
||||
|
||||
ENOENT: error => `No such file or directory: ${error.path}`,
|
||||
|
||||
ENOGIT: () => stripIndent`
|
||||
Git is not installed on this system.
|
||||
Head over to http://git-scm.com to install it and run this command again.`,
|
||||
|
||||
EPERM: () => stripIndent`
|
||||
You don't have enough privileges to run this operation.
|
||||
${
|
||||
os.platform() === 'win32'
|
||||
? 'Run a new Command Prompt as administrator and try running this command again.'
|
||||
: 'Try running this command again prefixing it with `sudo`.'
|
||||
}
|
||||
|
||||
If this is not the case, and you're trying to burn an SDCard, check that the write lock is not set.`,
|
||||
|
||||
EACCES: e => messages.EPERM(e),
|
||||
|
||||
ETIMEDOUT: () =>
|
||||
'Oops something went wrong, please check your connection and try again.',
|
||||
|
||||
MODULE_NOT_FOUND: () => stripIndent`
|
||||
Part of the CLI could not be loaded. This typically means your CLI install is in a broken state.
|
||||
${
|
||||
os.arch() === 'x64'
|
||||
? 'You can normally fix this by uninstalling and reinstalling the CLI.'
|
||||
: stripIndent`
|
||||
You're using an unsupported architecture (${os.arch()}), so this is typically caused by missing native modules.
|
||||
Reinstalling may help, but pay attention to errors in native module build steps en route.
|
||||
`
|
||||
}
|
||||
`,
|
||||
|
||||
ResinExpiredToken: () => stripIndent`
|
||||
Looks like your session token is expired.
|
||||
Please try logging in again with:
|
||||
$ resin login`,
|
||||
};
|
||||
|
||||
exports.handle = function(error: any) {
|
||||
let message = errors.interpret(error);
|
||||
let message = interpret(error);
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
@ -34,7 +114,7 @@ exports.handle = function(error: any) {
|
||||
message = error.stack;
|
||||
}
|
||||
|
||||
patterns.printErrorMessage(message);
|
||||
patterns.printErrorMessage(message!);
|
||||
|
||||
return captureException(error)
|
||||
.timeout(1000)
|
||||
|
747
lib/utils/compose.coffee
Normal file
747
lib/utils/compose.coffee
Normal file
@ -0,0 +1,747 @@
|
||||
Promise = require('bluebird')
|
||||
path = require('path')
|
||||
|
||||
exports.appendProjectOptions = appendProjectOptions = (opts) ->
|
||||
opts.concat [
|
||||
{
|
||||
signature: 'projectName'
|
||||
parameter: 'projectName'
|
||||
description: 'Specify an alternate project name; default is the directory name'
|
||||
alias: 'n'
|
||||
},
|
||||
]
|
||||
|
||||
exports.appendOptions = (opts) ->
|
||||
appendProjectOptions(opts).concat [
|
||||
{
|
||||
signature: 'emulated'
|
||||
description: 'Run an emulated build using Qemu'
|
||||
boolean: true
|
||||
alias: 'e'
|
||||
},
|
||||
{
|
||||
signature: 'logs'
|
||||
description: 'Display full log output'
|
||||
boolean: true
|
||||
},
|
||||
]
|
||||
|
||||
exports.generateOpts = (options) ->
|
||||
fs = require('mz/fs')
|
||||
fs.realpath(options.source || '.').then (projectPath) ->
|
||||
projectName: options.projectName
|
||||
projectPath: projectPath
|
||||
inlineLogs: !!options.logs
|
||||
|
||||
compositionFileNames = [
|
||||
'docker-compose.yml'
|
||||
'docker-compose.yaml'
|
||||
]
|
||||
|
||||
# look into the given directory for valid compose files and return
|
||||
# the contents of the first one found.
|
||||
exports.resolveProject = resolveProject = (rootDir) ->
|
||||
fs = require('mz/fs')
|
||||
Promise.any compositionFileNames.map (filename) ->
|
||||
fs.readFile(path.join(rootDir, filename), 'utf-8')
|
||||
|
||||
# Parse the given composition and return a structure with info. Input is:
|
||||
# - composePath: the *absolute* path to the directory containing the compose file
|
||||
# - composeStr: the contents of the compose file, as a string
|
||||
createProject = (composePath, composeStr, projectName = null) ->
|
||||
yml = require('js-yaml')
|
||||
compose = require('resin-compose-parse')
|
||||
|
||||
# both methods below may throw.
|
||||
composition = yml.safeLoad(composeStr, schema: yml.FAILSAFE_SCHEMA)
|
||||
composition = compose.normalize(composition)
|
||||
|
||||
projectName ?= path.basename(composePath)
|
||||
descriptors = compose.parse(composition).map (descr) ->
|
||||
# generate an image name based on the project and service names
|
||||
# if one is not given and the service requires a build
|
||||
if descr.image.context? and not descr.image.tag?
|
||||
descr.image.tag = [ projectName, descr.serviceName ].join('_').toLowerCase()
|
||||
return descr
|
||||
return {
|
||||
path: composePath,
|
||||
name: projectName,
|
||||
composition,
|
||||
descriptors
|
||||
}
|
||||
|
||||
# high-level function resolving a project and creating a composition out
|
||||
# of it in one go. if image is given, it'll create a default project for
|
||||
# that without looking for a project. falls back to creating a default
|
||||
# project if none is found at the given projectPath.
|
||||
exports.loadProject = (logger, projectPath, projectName, image) ->
|
||||
compose = require('resin-compose-parse')
|
||||
logger.logDebug('Loading project...')
|
||||
|
||||
Promise.try ->
|
||||
if image?
|
||||
logger.logInfo("Creating default composition with image: #{image}")
|
||||
return compose.defaultComposition(image)
|
||||
|
||||
logger.logDebug('Resolving project...')
|
||||
|
||||
resolveProject(projectPath)
|
||||
.tap ->
|
||||
logger.logInfo('Compose file detected')
|
||||
.catch (e) ->
|
||||
logger.logDebug("Failed to resolve project: #{e}")
|
||||
logger.logInfo("Creating default composition with source: #{projectPath}")
|
||||
return compose.defaultComposition()
|
||||
.then (composeStr) ->
|
||||
logger.logDebug('Creating project...')
|
||||
createProject(projectPath, composeStr, projectName)
|
||||
|
||||
toPosixPath = (systemPath) ->
|
||||
path = require('path')
|
||||
systemPath.replace(new RegExp('\\' + path.sep, 'g'), '/')
|
||||
|
||||
exports.tarDirectory = tarDirectory = (dir) ->
|
||||
tar = require('tar-stream')
|
||||
klaw = require('klaw')
|
||||
path = require('path')
|
||||
fs = require('mz/fs')
|
||||
streamToPromise = require('stream-to-promise')
|
||||
{ FileIgnorer } = require('./ignore')
|
||||
|
||||
getFiles = ->
|
||||
streamToPromise(klaw(dir))
|
||||
.filter((item) -> not item.stats.isDirectory())
|
||||
.map((item) -> item.path)
|
||||
|
||||
ignore = new FileIgnorer(dir)
|
||||
pack = tar.pack()
|
||||
getFiles(dir)
|
||||
.each (file) ->
|
||||
type = ignore.getIgnoreFileType(path.relative(dir, file))
|
||||
if type?
|
||||
ignore.addIgnoreFile(file, type)
|
||||
.filter(ignore.filter)
|
||||
.map (file) ->
|
||||
relPath = path.relative(path.resolve(dir), file)
|
||||
Promise.join relPath, fs.stat(file), fs.readFile(file),
|
||||
(filename, stats, data) ->
|
||||
pack.entry({ name: toPosixPath(filename), size: stats.size, mode: stats.mode }, data)
|
||||
.then ->
|
||||
pack.finalize()
|
||||
return pack
|
||||
|
||||
truncateString = (str, len) ->
|
||||
return str if str.length < len
|
||||
str = str.slice(0, len)
|
||||
# return everything up to the last line. this is a cheeky way to avoid
|
||||
# having to deal with splitting the string midway through some special
|
||||
# character sequence.
|
||||
return str.slice(0, str.lastIndexOf('\n'))
|
||||
|
||||
LOG_LENGTH_MAX = 512 * 1024 # 512KB
|
||||
|
||||
exports.buildProject = (
|
||||
docker, logger,
|
||||
projectPath, projectName, composition,
|
||||
arch, deviceType,
|
||||
emulated, buildOpts,
|
||||
inlineLogs
|
||||
) ->
|
||||
_ = require('lodash')
|
||||
humanize = require('humanize')
|
||||
compose = require('resin-compose-parse')
|
||||
builder = require('resin-multibuild')
|
||||
transpose = require('docker-qemu-transpose')
|
||||
qemu = require('./qemu')
|
||||
|
||||
logger.logInfo("Building for #{arch}/#{deviceType}")
|
||||
|
||||
imageDescriptors = compose.parse(composition)
|
||||
imageDescriptorsByServiceName = _.keyBy(imageDescriptors, 'serviceName')
|
||||
|
||||
if inlineLogs
|
||||
renderer = new BuildProgressInline(logger.streams['build'], imageDescriptors)
|
||||
else
|
||||
tty = require('./tty')(process.stdout)
|
||||
renderer = new BuildProgressUI(tty, imageDescriptors)
|
||||
renderer.start()
|
||||
|
||||
qemu.installQemuIfNeeded(emulated, logger)
|
||||
.tap (needsQemu) ->
|
||||
return if not needsQemu
|
||||
logger.logInfo('Emulation is enabled')
|
||||
# Copy qemu into all build contexts
|
||||
Promise.map imageDescriptors, (d) ->
|
||||
return if not d.image.context? # external image
|
||||
return qemu.copyQemu(path.join(projectPath, d.image.context))
|
||||
.then (needsQemu) ->
|
||||
# Tar up the directory, ready for the build stream
|
||||
tarDirectory(projectPath)
|
||||
.then (tarStream) ->
|
||||
builder.splitBuildStream(composition, tarStream)
|
||||
.tap (tasks) ->
|
||||
# Updates each task as a side-effect
|
||||
builder.performResolution(tasks, arch, deviceType)
|
||||
.map (task) ->
|
||||
if not task.external and not task.resolved
|
||||
throw new Error(
|
||||
"Project type for service '#{task.serviceName}' could not be determined. " +
|
||||
'Please add a Dockerfile'
|
||||
)
|
||||
.map (task) ->
|
||||
d = imageDescriptorsByServiceName[task.serviceName]
|
||||
|
||||
# multibuild parses the composition internally so any tags we've
|
||||
# set before are lost; re-assign them here
|
||||
task.tag ?= [ projectName, task.serviceName ].join('_').toLowerCase()
|
||||
if d.image.context?
|
||||
d.image.tag = task.tag
|
||||
|
||||
# configure build opts appropriately
|
||||
task.dockerOpts ?= {}
|
||||
_.merge(task.dockerOpts, buildOpts, { t: task.tag })
|
||||
if d.image.context?.args?
|
||||
task.dockerOpts.buildargs ?= {}
|
||||
_.merge(task.dockerOpts.buildargs, d.image.context.args)
|
||||
|
||||
# Get the service-specific log stream
|
||||
# Caveat: `multibuild.BuildTask` defines no `logStream` property
|
||||
# but it's convenient to store it there; it's JS ultimately.
|
||||
task.logStream = renderer.streams[task.serviceName]
|
||||
task.logBuffer = []
|
||||
|
||||
# Setup emulation if needed
|
||||
return [ task, null ] if task.external or not needsQemu
|
||||
binPath = qemu.qemuPathInContext(path.join(projectPath, task.context))
|
||||
transpose.transposeTarStream task.buildStream,
|
||||
hostQemuPath: toPosixPath(binPath)
|
||||
containerQemuPath: "/tmp/#{qemu.QEMU_BIN_NAME}"
|
||||
.then (stream) ->
|
||||
task.buildStream = stream
|
||||
.return([ task, binPath ])
|
||||
.map ([ task, qemuPath ]) ->
|
||||
Promise.resolve(task).tap (task) ->
|
||||
captureStream = buildLogCapture(task.external, task.logBuffer)
|
||||
|
||||
if task.external
|
||||
# External image -- there's no build to be performed,
|
||||
# just follow pull progress.
|
||||
captureStream.pipe(task.logStream)
|
||||
task.progressHook = pullProgressAdapter(captureStream)
|
||||
else
|
||||
task.streamHook = (stream) ->
|
||||
stream = createLogStream(stream)
|
||||
if qemuPath?
|
||||
buildThroughStream = transpose.getBuildThroughStream
|
||||
hostQemuPath: toPosixPath(qemuPath)
|
||||
containerQemuPath: "/tmp/#{qemu.QEMU_BIN_NAME}"
|
||||
rawStream = stream.pipe(buildThroughStream)
|
||||
else
|
||||
rawStream = stream
|
||||
# `stream` sends out raw strings in contrast to `task.progressHook`
|
||||
# where we're given objects. capture these strings as they come
|
||||
# before we parse them.
|
||||
rawStream
|
||||
.pipe(dropEmptyLinesStream())
|
||||
.pipe(captureStream)
|
||||
.pipe(buildProgressAdapter(inlineLogs))
|
||||
.pipe(task.logStream)
|
||||
.then (tasks) ->
|
||||
logger.logDebug 'Prepared tasks; building...'
|
||||
builder.performBuilds(tasks, docker)
|
||||
.map (builtImage) ->
|
||||
if not builtImage.successful
|
||||
builtImage.error.serviceName = builtImage.serviceName
|
||||
throw builtImage.error
|
||||
|
||||
d = imageDescriptorsByServiceName[builtImage.serviceName]
|
||||
task = _.find(tasks, serviceName: builtImage.serviceName)
|
||||
|
||||
image =
|
||||
serviceName: d.serviceName
|
||||
name: d.image.tag ? d.image
|
||||
logs: truncateString(task.logBuffer.join('\n'), LOG_LENGTH_MAX)
|
||||
props:
|
||||
dockerfile: builtImage.dockerfile
|
||||
projectType: builtImage.projectType
|
||||
|
||||
# Times here are timestamps, so test whether they're null
|
||||
# before creating a date out of them, as `new Date(null)`
|
||||
# creates a date representing UNIX time 0.
|
||||
if (startTime = builtImage.startTime)
|
||||
image.props.startTime = new Date(startTime)
|
||||
if (endTime = builtImage.endTime)
|
||||
image.props.endTime = new Date(endTime)
|
||||
docker.getImage(image.name).inspect().get('Size').then (size) ->
|
||||
image.props.size = size
|
||||
.return(image)
|
||||
.tap (images) ->
|
||||
summary = _(images).map ({ serviceName, props }) ->
|
||||
[ serviceName, "Image size: #{humanize.filesize(props.size)}" ]
|
||||
.fromPairs()
|
||||
.value()
|
||||
renderer.end(summary)
|
||||
.finally(renderer.end)
|
||||
|
||||
createRelease = (apiEndpoint, auth, userId, appId, composition) ->
|
||||
_ = require('lodash')
|
||||
crypto = require('crypto')
|
||||
releaseMod = require('resin-release')
|
||||
|
||||
client = releaseMod.createClient({ apiEndpoint, auth })
|
||||
|
||||
releaseMod.create
|
||||
client: client
|
||||
user: userId
|
||||
application: appId
|
||||
composition: composition
|
||||
source: 'local'
|
||||
commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase()
|
||||
.then ({ release, serviceImages }) ->
|
||||
release = _.omit(release, [
|
||||
'created_at'
|
||||
'belongs_to__application'
|
||||
'is_created_by__user'
|
||||
'__metadata'
|
||||
])
|
||||
_.keys serviceImages, (serviceName) ->
|
||||
serviceImages[serviceName] = _.omit(serviceImages[serviceName], [
|
||||
'created_at'
|
||||
'is_a_build_of__service'
|
||||
'__metadata'
|
||||
])
|
||||
return { client, release, serviceImages }
|
||||
|
||||
tagServiceImages = (docker, images, serviceImages) ->
|
||||
Promise.map images, (d) ->
|
||||
serviceImage = serviceImages[d.serviceName]
|
||||
imageName = serviceImage.is_stored_at__image_location
|
||||
[ _match, registry, repo, tag = 'latest' ] = /(.*?)\/(.*?)(?::([^/]*))?$/.exec(imageName)
|
||||
name = "#{registry}/#{repo}"
|
||||
docker.getImage(d.name).tag({ repo: name, tag, force: true })
|
||||
.then ->
|
||||
docker.getImage("#{name}:#{tag}")
|
||||
.then (localImage) ->
|
||||
serviceName: d.serviceName
|
||||
serviceImage: serviceImage
|
||||
localImage: localImage
|
||||
registry: registry
|
||||
repo: repo
|
||||
logs: d.logs
|
||||
props: d.props
|
||||
|
||||
authorizePush = (tokenAuthEndpoint, registry, images) ->
|
||||
_ = require('lodash')
|
||||
sdk = require('resin-sdk').fromSharedOptions()
|
||||
|
||||
if not _.isArray(images)
|
||||
images = [ images ]
|
||||
|
||||
sdk.request.send
|
||||
baseUrl: tokenAuthEndpoint
|
||||
url: '/auth/v1/token'
|
||||
qs:
|
||||
service: registry
|
||||
scope: images.map (repo) ->
|
||||
"repository:#{repo}:pull,push"
|
||||
.get('body')
|
||||
.get('token')
|
||||
.catchReturn({})
|
||||
|
||||
pushAndUpdateServiceImages = (docker, token, images, afterEach) ->
|
||||
chalk = require('chalk')
|
||||
{ DockerProgress } = require('docker-progress')
|
||||
tty = require('./tty')(process.stdout)
|
||||
|
||||
opts = { authconfig: registrytoken: token }
|
||||
|
||||
progress = new DockerProgress(dockerToolbelt: docker)
|
||||
renderer = pushProgressRenderer(tty, chalk.blue('[Push]') + ' ')
|
||||
reporters = progress.aggregateProgress(images.length, renderer)
|
||||
|
||||
Promise.using tty.cursorHidden(), ->
|
||||
Promise.map images, ({ serviceImage, localImage, props, logs }, index) ->
|
||||
Promise.join(
|
||||
localImage.inspect().get('Size')
|
||||
progress.push(localImage.name, reporters[index], opts).finally(renderer.end)
|
||||
(size, digest) ->
|
||||
serviceImage.image_size = size
|
||||
serviceImage.content_hash = digest
|
||||
serviceImage.build_log = logs
|
||||
serviceImage.dockerfile = props.dockerfile
|
||||
serviceImage.project_type = props.projectType
|
||||
serviceImage.start_timestamp = props.startTime if props.startTime
|
||||
serviceImage.end_timestamp = props.endTime if props.endTime
|
||||
serviceImage.push_timestamp = new Date()
|
||||
serviceImage.status = 'success'
|
||||
)
|
||||
.tapCatch (e) ->
|
||||
serviceImage.error_message = '' + e
|
||||
serviceImage.status = 'failed'
|
||||
.finally ->
|
||||
afterEach?(serviceImage, props)
|
||||
|
||||
exports.deployProject = (
|
||||
docker, logger,
|
||||
composition, images,
|
||||
appId, userId, auth,
|
||||
apiEndpoint,
|
||||
skipLogUpload
|
||||
) ->
|
||||
_ = require('lodash')
|
||||
chalk = require('chalk')
|
||||
releaseMod = require('resin-release')
|
||||
tty = require('./tty')(process.stdout)
|
||||
|
||||
prefix = chalk.cyan('[Info]') + ' '
|
||||
spinner = createSpinner()
|
||||
runloop = runSpinner(tty, spinner, "#{prefix}Creating release...")
|
||||
|
||||
createRelease(apiEndpoint, auth, userId, appId, composition)
|
||||
.finally(runloop.end)
|
||||
.then ({ client, release, serviceImages }) ->
|
||||
logger.logDebug('Tagging images...')
|
||||
tagServiceImages(docker, images, serviceImages)
|
||||
.tap (images) ->
|
||||
logger.logDebug('Authorizing push...')
|
||||
authorizePush(apiEndpoint, images[0].registry, _.map(images, 'repo'))
|
||||
.then (token) ->
|
||||
logger.logInfo('Pushing images to registry...')
|
||||
pushAndUpdateServiceImages docker, token, images, (serviceImage) ->
|
||||
logger.logDebug("Saving image #{serviceImage.is_stored_at__image_location}")
|
||||
if skipLogUpload
|
||||
delete serviceImage.build_log
|
||||
releaseMod.updateImage(client, serviceImage.id, serviceImage)
|
||||
.finally ->
|
||||
logger.logDebug('Untagging images...')
|
||||
Promise.map images, ({ localImage }) ->
|
||||
localImage.remove()
|
||||
.then ->
|
||||
release.status = 'success'
|
||||
.tapCatch (e) ->
|
||||
release.status = 'failed'
|
||||
.finally ->
|
||||
runloop = runSpinner(tty, spinner, "#{prefix}Saving release...")
|
||||
release.end_timestamp = new Date()
|
||||
releaseMod.updateRelease(client, release.id, release)
|
||||
.finally(runloop.end)
|
||||
.return(release)
|
||||
|
||||
# utilities
|
||||
|
||||
renderProgressBar = (percentage, stepCount) ->
|
||||
_ = require('lodash')
|
||||
percentage = _.clamp(percentage, 0, 100)
|
||||
barCount = stepCount * percentage // 100
|
||||
spaceCount = stepCount - barCount
|
||||
bar = "[#{_.repeat('=', barCount)}>#{_.repeat(' ', spaceCount)}]"
|
||||
return "#{bar} #{_.padStart(percentage, 3)}%"
|
||||
|
||||
pushProgressRenderer = (tty, prefix) ->
|
||||
fn = (e) ->
|
||||
{ error, percentage } = e
|
||||
throw new Error(error) if error?
|
||||
bar = renderProgressBar(percentage, 40)
|
||||
tty.replaceLine("#{prefix}#{bar}\r")
|
||||
fn.end = ->
|
||||
tty.clearLine()
|
||||
return fn
|
||||
|
||||
createLogStream = (input) ->
|
||||
split = require('split')
|
||||
stripAnsi = require('strip-ansi-stream')
|
||||
return input.pipe(stripAnsi()).pipe(split())
|
||||
|
||||
dropEmptyLinesStream = ->
|
||||
through = require('through2')
|
||||
through (data, enc, cb) ->
|
||||
str = data.toString('utf-8')
|
||||
@push(str) if str.trim()
|
||||
cb()
|
||||
|
||||
buildLogCapture = (objectMode, buffer) ->
|
||||
through = require('through2')
|
||||
|
||||
through { objectMode }, (data, enc, cb) ->
|
||||
# data from pull stream
|
||||
if data.error
|
||||
buffer.push("#{data.error}")
|
||||
else if data.progress and data.status
|
||||
buffer.push("#{data.progress}% #{data.status}")
|
||||
else if data.status
|
||||
buffer.push("#{data.status}")
|
||||
|
||||
# data from build stream
|
||||
else
|
||||
buffer.push(data)
|
||||
|
||||
cb(null, data)
|
||||
|
||||
buildProgressAdapter = (inline) ->
|
||||
through = require('through2')
|
||||
|
||||
stepRegex = /^\s*Step\s+(\d+)\/(\d+)\s*: (.+)$/
|
||||
|
||||
[ step, numSteps, progress ] = [ null, null, undefined ]
|
||||
|
||||
through { objectMode: true }, (str, enc, cb) ->
|
||||
return cb(null, str) if not str?
|
||||
|
||||
if inline
|
||||
return cb(null, { status: str })
|
||||
|
||||
if /^Successfully tagged /.test(str)
|
||||
progress = undefined
|
||||
else
|
||||
if (match = stepRegex.exec(str))
|
||||
step = match[1]
|
||||
numSteps ?= match[2]
|
||||
str = match[3]
|
||||
if step?
|
||||
str = "Step #{step}/#{numSteps}: #{str}"
|
||||
progress = parseInt(step, 10) * 100 // parseInt(numSteps, 10)
|
||||
|
||||
cb(null, { status: str, progress })
|
||||
|
||||
pullProgressAdapter = (outStream) ->
|
||||
return ({ status, id, percentage, error, errorDetail }) ->
|
||||
if status?
|
||||
status = status.replace(/^Status: /, '')
|
||||
if id?
|
||||
status = "#{id}: #{status}"
|
||||
if percentage is 100
|
||||
percentage = undefined
|
||||
outStream.write
|
||||
status: status
|
||||
progress: percentage
|
||||
error: errorDetail?.message ? error
|
||||
|
||||
createSpinner = ->
|
||||
chars = '|/-\\'
|
||||
index = 0
|
||||
-> chars[(index++) % chars.length]
|
||||
|
||||
runSpinner = (tty, spinner, msg) ->
|
||||
runloop = createRunLoop ->
|
||||
tty.clearLine()
|
||||
tty.writeLine("#{msg} #{spinner()}")
|
||||
tty.cursorUp()
|
||||
runloop.onEnd = ->
|
||||
tty.clearLine()
|
||||
tty.writeLine(msg)
|
||||
return runloop
|
||||
|
||||
createRunLoop = (tick) ->
|
||||
timerId = setInterval(tick, 1000 / 10)
|
||||
runloop = {
|
||||
onEnd: ->
|
||||
end: ->
|
||||
clearInterval(timerId)
|
||||
runloop.onEnd()
|
||||
}
|
||||
return runloop
|
||||
|
||||
class BuildProgressUI
|
||||
constructor: (tty, descriptors) ->
|
||||
_ = require('lodash')
|
||||
chalk = require('chalk')
|
||||
through = require('through2')
|
||||
|
||||
eventHandler = @_handleEvent
|
||||
services = _.map(descriptors, 'serviceName')
|
||||
|
||||
streams = _(services).map (service) ->
|
||||
stream = through.obj (event, _enc, cb) ->
|
||||
eventHandler(service, event)
|
||||
cb()
|
||||
stream.pipe(tty.stream)
|
||||
[ service, stream ]
|
||||
.fromPairs()
|
||||
.value()
|
||||
|
||||
@_tty = tty
|
||||
@_serviceToDataMap = {}
|
||||
@_services = services
|
||||
|
||||
# Logger magically prefixes the log line with [Build] etc., but it doesn't
|
||||
# work well with the spinner we're also showing. Manually build the prefix
|
||||
# here and bypass the logger.
|
||||
prefix = chalk.blue('[Build]') + ' '
|
||||
|
||||
offset = 10 # account for escape sequences inserted for colouring
|
||||
@_prefixWidth = offset + prefix.length + _.max(_.map(services, 'length'))
|
||||
@_prefix = prefix
|
||||
|
||||
# these are to handle window wrapping
|
||||
@_maxLineWidth = null
|
||||
@_lineWidths = []
|
||||
|
||||
@_startTime = null
|
||||
@_ended = false
|
||||
@_cancelled = false
|
||||
@_spinner = createSpinner()
|
||||
|
||||
@streams = streams
|
||||
|
||||
_handleEvent: (service, event) =>
|
||||
@_serviceToDataMap[service] = event
|
||||
|
||||
_handleInterrupt: =>
|
||||
@_cancelled = true
|
||||
@end()
|
||||
process.exit(130) # 128 + SIGINT
|
||||
|
||||
start: =>
|
||||
process.on('SIGINT', @_handleInterrupt)
|
||||
@_tty.hideCursor()
|
||||
@_services.forEach (service) =>
|
||||
@streams[service].write({ status: 'Preparing...' })
|
||||
@_runloop = createRunLoop(@_display)
|
||||
@_startTime = Date.now()
|
||||
|
||||
end: (summary = null) =>
|
||||
return if @_ended
|
||||
@_ended = true
|
||||
process.removeListener('SIGINT', @_handleInterrupt)
|
||||
@_runloop.end()
|
||||
@_runloop = null
|
||||
|
||||
@_clear()
|
||||
@_renderStatus(true)
|
||||
@_renderSummary(summary ? @_getServiceSummary())
|
||||
@_tty.showCursor()
|
||||
|
||||
_display: =>
|
||||
@_clear()
|
||||
@_renderStatus()
|
||||
@_renderSummary(@_getServiceSummary())
|
||||
@_tty.cursorUp(@_services.length + 1) # for status line
|
||||
|
||||
_clear: ->
|
||||
@_tty.deleteToEnd()
|
||||
@_maxLineWidth = @_tty.currentWindowSize().width
|
||||
|
||||
_getServiceSummary: ->
|
||||
_ = require('lodash')
|
||||
|
||||
services = @_services
|
||||
serviceToDataMap = @_serviceToDataMap
|
||||
|
||||
_(services).map (service) ->
|
||||
{ status, progress, error } = serviceToDataMap[service] ? {}
|
||||
if error
|
||||
return "#{error}"
|
||||
else if progress
|
||||
bar = renderProgressBar(progress, 20)
|
||||
return "#{bar} #{status}" if status
|
||||
return "#{bar}"
|
||||
else if status
|
||||
return "#{status}"
|
||||
else
|
||||
return 'Waiting...'
|
||||
.map (data, index) ->
|
||||
[ services[index], data ]
|
||||
.fromPairs()
|
||||
.value()
|
||||
|
||||
_renderStatus: (end = false) ->
|
||||
moment = require('moment')
|
||||
require('moment-duration-format')(moment)
|
||||
|
||||
@_tty.clearLine()
|
||||
@_tty.write(@_prefix)
|
||||
if end and @_cancelled
|
||||
@_tty.writeLine('Build cancelled')
|
||||
else if end
|
||||
serviceCount = @_services.length
|
||||
serviceStr = if serviceCount is 1 then '1 service' else "#{serviceCount} services"
|
||||
runTime = Date.now() - @_startTime
|
||||
durationStr = moment.duration(runTime // 1000, 'seconds').format()
|
||||
@_tty.writeLine("Built #{serviceStr} in #{durationStr}")
|
||||
else
|
||||
@_tty.writeLine("Building services... #{@_spinner()}")
|
||||
|
||||
_renderSummary: (serviceToStrMap) ->
|
||||
_ = require('lodash')
|
||||
chalk = require('chalk')
|
||||
truncate = require('cli-truncate')
|
||||
strlen = require('string-width')
|
||||
|
||||
@_services.forEach (service, index) =>
|
||||
str = _.padEnd(@_prefix + chalk.bold(service), @_prefixWidth)
|
||||
str += serviceToStrMap[service]
|
||||
if @_maxLineWidth?
|
||||
str = truncate(str, @_maxLineWidth)
|
||||
@_lineWidths[index] = strlen(str)
|
||||
|
||||
@_tty.clearLine()
|
||||
@_tty.writeLine(str)
|
||||
|
||||
class BuildProgressInline
|
||||
constructor: (outStream, descriptors) ->
|
||||
_ = require('lodash')
|
||||
through = require('through2')
|
||||
|
||||
services = _.map(descriptors, 'serviceName')
|
||||
eventHandler = @_renderEvent
|
||||
streams = _(services).map (service) ->
|
||||
stream = through.obj (event, _enc, cb) ->
|
||||
eventHandler(service, event)
|
||||
cb()
|
||||
stream.pipe(outStream)
|
||||
[ service, stream ]
|
||||
.fromPairs()
|
||||
.value()
|
||||
|
||||
offset = 10 # account for escape sequences inserted for colouring
|
||||
@_prefixWidth = offset + _.max(_.map(services, 'length'))
|
||||
@_outStream = outStream
|
||||
@_services = services
|
||||
@_startTime = null
|
||||
@_ended = false
|
||||
|
||||
@streams = streams
|
||||
|
||||
start: =>
|
||||
@_outStream.write('Building services...\n')
|
||||
@_services.forEach (service) =>
|
||||
@streams[service].write({ status: 'Preparing...' })
|
||||
@_startTime = Date.now()
|
||||
|
||||
end: (summary = null) =>
|
||||
moment = require('moment')
|
||||
require('moment-duration-format')(moment)
|
||||
|
||||
return if @_ended
|
||||
@_ended = true
|
||||
|
||||
if summary?
|
||||
@_services.forEach (service) =>
|
||||
@_renderEvent(service, summary[service])
|
||||
|
||||
if @_cancelled
|
||||
@_outStream.write('Build cancelled\n')
|
||||
else
|
||||
serviceCount = @_services.length
|
||||
serviceStr = if serviceCount is 1 then '1 service' else "#{serviceCount} services"
|
||||
runTime = Date.now() - @_startTime
|
||||
durationStr = moment.duration(runTime // 1000, 'seconds').format()
|
||||
@_outStream.write("Built #{serviceStr} in #{durationStr}\n")
|
||||
|
||||
_renderEvent: (service, event) =>
|
||||
_ = require('lodash')
|
||||
chalk = require('chalk')
|
||||
|
||||
str = do ->
|
||||
{ status, error } = event
|
||||
if error
|
||||
return "#{error}"
|
||||
else if status
|
||||
return "#{status}"
|
||||
else
|
||||
return 'Waiting...'
|
||||
|
||||
prefix = _.padEnd(chalk.bold(service), @_prefixWidth)
|
||||
@_outStream.write(prefix)
|
||||
@_outStream.write(str)
|
||||
@_outStream.write('\n')
|
32
lib/utils/compose.d.ts
vendored
Normal file
32
lib/utils/compose.d.ts
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as Stream from 'stream';
|
||||
import { Composition } from 'resin-compose-parse';
|
||||
import Logger = require('./logger');
|
||||
|
||||
interface Image {
|
||||
context: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
interface Descriptor {
|
||||
image: Image | string;
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
export function resolveProject(projectRoot: string): Bluebird<string>;
|
||||
|
||||
export interface ComposeProject {
|
||||
path: string;
|
||||
name: string;
|
||||
composition: Composition;
|
||||
descriptors: Descriptor[];
|
||||
}
|
||||
|
||||
export function loadProject(
|
||||
logger: Logger,
|
||||
projectPath: string,
|
||||
projectName: string,
|
||||
image?: string,
|
||||
): Bluebird<ComposeProject>;
|
||||
|
||||
export function tarDirectory(source: string): Promise<Stream.Readable>;
|
@ -13,24 +13,37 @@ 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 fs from 'fs';
|
||||
|
||||
import Promise = require('bluebird');
|
||||
import ResinSdk = require('resin-sdk');
|
||||
import _ = require('lodash');
|
||||
import deviceConfig = require('resin-device-config');
|
||||
import * as semver from 'resin-semver';
|
||||
|
||||
const resin = ResinSdk.fromSharedOptions();
|
||||
|
||||
function readRootCa(): Promise<string | void> {
|
||||
const caFile = process.env.NODE_EXTRA_CA_CERTS;
|
||||
if (!caFile) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.fromCallback(cb =>
|
||||
fs.readFile(caFile, { encoding: 'utf8' }, cb),
|
||||
)
|
||||
.then(pem => Buffer.from(pem).toString('base64'))
|
||||
.catch({ code: 'ENOENT' }, () => {});
|
||||
}
|
||||
|
||||
export function generateBaseConfig(
|
||||
application: ResinSdk.Application,
|
||||
options: {},
|
||||
options: { version?: string; appUpdatePollInterval?: number },
|
||||
) {
|
||||
options = _.mapValues(options, function(value, key) {
|
||||
if (key === 'appUpdatePollInterval') {
|
||||
return value * 60 * 1000;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
if (options.appUpdatePollInterval) {
|
||||
options = {
|
||||
...options,
|
||||
appUpdatePollInterval: options.appUpdatePollInterval * 60 * 1000,
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.props({
|
||||
userId: resin.auth.getUserId(),
|
||||
@ -40,6 +53,9 @@ export function generateBaseConfig(
|
||||
registryUrl: resin.settings.get('registryUrl'),
|
||||
deltaUrl: resin.settings.get('deltaUrl'),
|
||||
apiConfig: resin.models.config.getAll(),
|
||||
rootCA: readRootCa().catch(() => {
|
||||
console.warn('Could not read root CA');
|
||||
}),
|
||||
}).then(results => {
|
||||
return deviceConfig.generate(
|
||||
{
|
||||
@ -58,6 +74,7 @@ export function generateBaseConfig(
|
||||
mixpanel: {
|
||||
token: results.apiConfig.mixpanelToken,
|
||||
},
|
||||
balenaRootCA: results.rootCA,
|
||||
},
|
||||
options,
|
||||
);
|
||||
@ -66,27 +83,30 @@ export function generateBaseConfig(
|
||||
|
||||
export function generateApplicationConfig(
|
||||
application: ResinSdk.Application,
|
||||
options: {},
|
||||
options: { version?: string },
|
||||
) {
|
||||
return generateBaseConfig(application, options).tap(config =>
|
||||
addApplicationKey(config, application.id),
|
||||
);
|
||||
return generateBaseConfig(application, options).tap(config => {
|
||||
if (semver.satisfies(options.version, '>=2.7.8')) {
|
||||
return addProvisioningKey(config, application.id);
|
||||
} else {
|
||||
return addApplicationKey(config, application.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function generateDeviceConfig(
|
||||
device: ResinSdk.Device & { application_name: string },
|
||||
deviceApiKey: string | null,
|
||||
options: {},
|
||||
device: ResinSdk.Device & { belongs_to__application: ResinSdk.PineDeferred },
|
||||
deviceApiKey: string | true | null,
|
||||
options: { version?: string },
|
||||
) {
|
||||
return resin.models.application
|
||||
.get(device.application_name)
|
||||
.get(device.belongs_to__application.__id)
|
||||
.then(application => {
|
||||
return generateBaseConfig(application, options).tap(config => {
|
||||
// Device API keys are only safe for ResinOS 2.0.3+. We could somehow obtain
|
||||
// the expected version for this config and generate one when we know it's safe,
|
||||
// but instead for now we fall back to app keys unless the user has explicitly opted in.
|
||||
if (deviceApiKey) {
|
||||
return addDeviceKey(config, device.uuid, deviceApiKey);
|
||||
} else if (semver.satisfies(options.version, '>=2.0.3')) {
|
||||
return addDeviceKey(config, device.uuid, true);
|
||||
} else {
|
||||
return addApplicationKey(config, application.id);
|
||||
}
|
||||
@ -111,9 +131,25 @@ function addApplicationKey(config: any, applicationNameOrId: string | number) {
|
||||
});
|
||||
}
|
||||
|
||||
function addDeviceKey(config: any, uuid: string, customDeviceApiKey: string) {
|
||||
function addProvisioningKey(config: any, applicationNameOrId: string | number) {
|
||||
return resin.models.application
|
||||
.generateProvisioningKey(applicationNameOrId)
|
||||
.tap(apiKey => {
|
||||
config.apiKey = apiKey;
|
||||
});
|
||||
}
|
||||
|
||||
function addDeviceKey(
|
||||
config: any,
|
||||
uuid: string,
|
||||
customDeviceApiKey: string | true,
|
||||
) {
|
||||
return Promise.try(() => {
|
||||
return customDeviceApiKey || resin.models.device.generateDeviceKey(uuid);
|
||||
if (customDeviceApiKey === true) {
|
||||
return resin.models.device.generateDeviceKey(uuid);
|
||||
} else {
|
||||
return customDeviceApiKey;
|
||||
}
|
||||
}).tap(deviceApiKey => {
|
||||
config.deviceApiKey = deviceApiKey;
|
||||
});
|
||||
|
136
lib/utils/deploy-legacy.coffee
Normal file
136
lib/utils/deploy-legacy.coffee
Normal file
@ -0,0 +1,136 @@
|
||||
Promise = require('bluebird')
|
||||
|
||||
getBuilderPushEndpoint = (baseUrl, owner, app) ->
|
||||
querystring = require('querystring')
|
||||
args = querystring.stringify({ owner, app })
|
||||
"https://builder.#{baseUrl}/v1/push?#{args}"
|
||||
|
||||
getBuilderLogPushEndpoint = (baseUrl, buildId, owner, app) ->
|
||||
querystring = require('querystring')
|
||||
args = querystring.stringify({ owner, app, buildId })
|
||||
"https://builder.#{baseUrl}/v1/pushLogs?#{args}"
|
||||
|
||||
bufferImage = (docker, imageId, bufferFile) ->
|
||||
Promise = require('bluebird')
|
||||
streamUtils = require('./streams')
|
||||
|
||||
image = docker.getImage(imageId)
|
||||
imageMetadata = image.inspect()
|
||||
|
||||
Promise.join image.get(), imageMetadata.get('Size'), (imageStream, imageSize) ->
|
||||
streamUtils.buffer(imageStream, bufferFile)
|
||||
.tap (bufferedStream) ->
|
||||
bufferedStream.length = imageSize
|
||||
|
||||
showPushProgress = (message) ->
|
||||
visuals = require('resin-cli-visuals')
|
||||
progressBar = new visuals.Progress(message)
|
||||
progressBar.update({ percentage: 0 })
|
||||
return progressBar
|
||||
|
||||
uploadToPromise = (uploadRequest, logger) ->
|
||||
new Promise (resolve, reject) ->
|
||||
|
||||
handleMessage = (data) ->
|
||||
data = data.toString()
|
||||
logger.logDebug("Received data: #{data}")
|
||||
|
||||
try
|
||||
obj = JSON.parse(data)
|
||||
catch e
|
||||
logger.logError('Error parsing reply from remote side')
|
||||
reject(e)
|
||||
return
|
||||
|
||||
switch obj.type
|
||||
when 'error' then reject(new Error("Remote error: #{obj.error}"))
|
||||
when 'success' then resolve(obj)
|
||||
when 'status' then logger.logInfo(obj.message)
|
||||
else reject(new Error("Received unexpected reply from remote: #{data}"))
|
||||
|
||||
uploadRequest
|
||||
.on('error', reject)
|
||||
.on('data', handleMessage)
|
||||
|
||||
uploadImage = (imageStream, token, username, url, appName, logger) ->
|
||||
request = require('request')
|
||||
progressStream = require('progress-stream')
|
||||
zlib = require('zlib')
|
||||
|
||||
# Need to strip off the newline
|
||||
progressMessage = logger.formatMessage('info', 'Uploading').slice(0, -1)
|
||||
progressBar = showPushProgress(progressMessage)
|
||||
streamWithProgress = imageStream.pipe progressStream
|
||||
time: 500,
|
||||
length: imageStream.length
|
||||
, ({ percentage, eta }) ->
|
||||
progressBar.update
|
||||
percentage: Math.min(percentage, 100)
|
||||
eta: eta
|
||||
|
||||
uploadRequest = request.post
|
||||
url: getBuilderPushEndpoint(url, username, appName)
|
||||
headers:
|
||||
'Content-Encoding': 'gzip'
|
||||
auth:
|
||||
bearer: token
|
||||
body: streamWithProgress.pipe(zlib.createGzip({
|
||||
level: 6
|
||||
}))
|
||||
|
||||
uploadToPromise(uploadRequest, logger)
|
||||
|
||||
uploadLogs = (logs, token, url, buildId, username, appName) ->
|
||||
request = require('request')
|
||||
request.post
|
||||
json: true
|
||||
url: getBuilderLogPushEndpoint(url, buildId, username, appName)
|
||||
auth:
|
||||
bearer: token
|
||||
body: Buffer.from(logs)
|
||||
|
||||
###
|
||||
opts must be a hash with the following keys:
|
||||
|
||||
- appName: the name of the app to deploy to
|
||||
- imageName: the name of the image to deploy
|
||||
- buildLogs: a string with build output
|
||||
- shouldUploadLogs
|
||||
###
|
||||
module.exports = (docker, logger, token, username, url, opts) ->
|
||||
tmp = require('tmp')
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
||||
|
||||
# Ensure the tmp files gets deleted
|
||||
tmp.setGracefulCleanup()
|
||||
|
||||
{ appName, imageName, buildLogs, shouldUploadLogs } = opts
|
||||
logs = buildLogs
|
||||
|
||||
tmpNameAsync()
|
||||
.then (bufferFile) ->
|
||||
logger.logInfo('Initializing deploy...')
|
||||
bufferImage(docker, imageName, bufferFile)
|
||||
.then (stream) ->
|
||||
uploadImage(stream, token, username, url, appName, logger)
|
||||
.finally ->
|
||||
# If the file was never written to (for instance because an error
|
||||
# has occured before any data was written) this call will throw an
|
||||
# ugly error, just suppress it
|
||||
Promise.try ->
|
||||
require('mz/fs').unlink(bufferFile)
|
||||
.catchReturn()
|
||||
.tap ({ buildId }) ->
|
||||
return if not shouldUploadLogs
|
||||
|
||||
logger.logInfo('Uploading logs...')
|
||||
Promise.join(
|
||||
logs
|
||||
token
|
||||
url
|
||||
buildId
|
||||
username
|
||||
appName
|
||||
uploadLogs
|
||||
)
|
||||
.get('buildId')
|
188
lib/utils/device/api.ts
Normal file
188
lib/utils/device/api.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as request from 'request';
|
||||
import * as Stream from 'stream';
|
||||
|
||||
import Logger = require('../logger');
|
||||
|
||||
import * as ApiErrors from './errors';
|
||||
|
||||
export interface DeviceResponse {
|
||||
[key: string]: any;
|
||||
|
||||
status: 'success' | 'failed';
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface DeviceInfo {
|
||||
deviceType: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
const deviceEndpoints = {
|
||||
setTargetState: 'v2/local/target-state',
|
||||
getTargetState: 'v2/local/target-state',
|
||||
getDeviceInformation: 'v2/local/device-info',
|
||||
logs: 'v2/local/logs',
|
||||
ping: 'ping',
|
||||
version: 'v2/version',
|
||||
};
|
||||
|
||||
export class DeviceAPI {
|
||||
private deviceAddress: string;
|
||||
|
||||
public constructor(
|
||||
private logger: Logger,
|
||||
addr: string,
|
||||
port: number = 48484,
|
||||
) {
|
||||
this.deviceAddress = `http://${addr}:${port}/`;
|
||||
}
|
||||
|
||||
// Either return nothing, or throw an error with the info
|
||||
public async setTargetState(state: any): Promise<void> {
|
||||
const url = this.getUrlForAction('setTargetState');
|
||||
return DeviceAPI.promisifiedRequest(
|
||||
request.post,
|
||||
{
|
||||
url,
|
||||
json: true,
|
||||
body: state,
|
||||
},
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
public async getTargetState(): Promise<any> {
|
||||
const url = this.getUrlForAction('getTargetState');
|
||||
|
||||
return DeviceAPI.promisifiedRequest(
|
||||
request.get,
|
||||
{
|
||||
url,
|
||||
json: true,
|
||||
},
|
||||
this.logger,
|
||||
).then(body => {
|
||||
return body.state;
|
||||
});
|
||||
}
|
||||
|
||||
public async getDeviceInformation(): Promise<DeviceInfo> {
|
||||
const url = this.getUrlForAction('getDeviceInformation');
|
||||
|
||||
return DeviceAPI.promisifiedRequest(
|
||||
request.get,
|
||||
{
|
||||
url,
|
||||
json: true,
|
||||
},
|
||||
this.logger,
|
||||
).then(body => {
|
||||
return body.info;
|
||||
});
|
||||
}
|
||||
|
||||
public async ping(): Promise<void> {
|
||||
const url = this.getUrlForAction('ping');
|
||||
|
||||
return DeviceAPI.promisifiedRequest(
|
||||
request.get,
|
||||
{
|
||||
url,
|
||||
},
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
public getVersion(): Promise<string> {
|
||||
const url = this.getUrlForAction('version');
|
||||
|
||||
return DeviceAPI.promisifiedRequest(request.get, {
|
||||
url,
|
||||
json: true,
|
||||
}).then(body => {
|
||||
if (body.status !== 'success') {
|
||||
throw new ApiErrors.DeviceAPIError(
|
||||
'Non-successful response from supervisor version endpoint',
|
||||
);
|
||||
}
|
||||
|
||||
return body.version;
|
||||
});
|
||||
}
|
||||
|
||||
public getLogStream(): Bluebird<Stream.Readable> {
|
||||
const url = this.getUrlForAction('logs');
|
||||
|
||||
// Don't use the promisified version here as we want to stream the output
|
||||
return new Bluebird((resolve, reject) => {
|
||||
const req = request.get(url);
|
||||
|
||||
req.on('error', reject).on('response', res => {
|
||||
if (res.statusCode !== 200) {
|
||||
reject(
|
||||
new ApiErrors.DeviceAPIError(
|
||||
'Non-200 response from log streaming endpoint',
|
||||
),
|
||||
);
|
||||
}
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getUrlForAction(action: keyof typeof deviceEndpoints): string {
|
||||
return `${this.deviceAddress}${deviceEndpoints[action]}`;
|
||||
}
|
||||
|
||||
// A helper method for promisifying general (non-streaming) requests. Streaming
|
||||
// requests should use a seperate setup
|
||||
private static async promisifiedRequest<T>(
|
||||
requestMethod: (
|
||||
opts: T,
|
||||
cb: (err?: any, res?: any, body?: any) => void,
|
||||
) => void,
|
||||
opts: T,
|
||||
logger?: Logger,
|
||||
): Promise<any> {
|
||||
const Bluebird = await import('bluebird');
|
||||
const _ = await import('lodash');
|
||||
|
||||
type ObjectWithUrl = { url?: string };
|
||||
|
||||
if (logger != null) {
|
||||
let url: string | null = null;
|
||||
if (_.isObject(opts) && (opts as ObjectWithUrl).url != null) {
|
||||
// the `as string` shouldn't be necessary, but the type system
|
||||
// is getting a little confused
|
||||
url = (opts as ObjectWithUrl).url as string;
|
||||
} else if (_.isString(opts)) {
|
||||
url = opts;
|
||||
}
|
||||
|
||||
if (url != null) {
|
||||
logger.logDebug(`Sending request to ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
return Bluebird.fromCallback(
|
||||
cb => {
|
||||
return requestMethod(opts, cb);
|
||||
},
|
||||
{ multiArgs: true },
|
||||
).then(([response, body]) => {
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
return body;
|
||||
case 400:
|
||||
throw new ApiErrors.BadRequestDeviceAPIError(body.message);
|
||||
case 503:
|
||||
throw new ApiErrors.ServiceUnavailableAPIError(body.message);
|
||||
default:
|
||||
throw new ApiErrors.DeviceAPIError(body.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default DeviceAPI;
|
306
lib/utils/device/deploy.ts
Normal file
306
lib/utils/device/deploy.ts
Normal file
@ -0,0 +1,306 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as Docker from 'dockerode';
|
||||
import * as _ from 'lodash';
|
||||
import { Composition } from 'resin-compose-parse';
|
||||
import { BuildTask, LocalImage } from 'resin-multibuild';
|
||||
import * as semver from 'resin-semver';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import Logger = require('../logger');
|
||||
import { displayBuildLog } from './logs';
|
||||
|
||||
import { DeviceInfo } from './api';
|
||||
import * as LocalPushErrors from './errors';
|
||||
|
||||
// Define the logger here so the debug output
|
||||
// can be used everywhere
|
||||
const logger = new Logger();
|
||||
|
||||
export interface DeviceDeployOptions {
|
||||
source: string;
|
||||
deviceHost: string;
|
||||
devicePort?: number;
|
||||
}
|
||||
|
||||
async function checkSource(source: string): Promise<boolean> {
|
||||
const { fs } = await import('mz');
|
||||
return (await fs.exists(source)) && (await fs.stat(source)).isDirectory();
|
||||
}
|
||||
|
||||
export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
const { loadProject, tarDirectory } = await import('../compose');
|
||||
const { exitWithExpectedError } = await import('../patterns');
|
||||
|
||||
const { DeviceAPI } = await import('./api');
|
||||
const { displayDeviceLogs } = await import('./logs');
|
||||
|
||||
if (!(await checkSource(opts.source))) {
|
||||
exitWithExpectedError(`Could not access source directory: ${opts.source}`);
|
||||
}
|
||||
|
||||
const api = new DeviceAPI(logger, opts.deviceHost);
|
||||
|
||||
// First check that we can access the device with a ping
|
||||
try {
|
||||
await api.ping();
|
||||
} catch (e) {
|
||||
exitWithExpectedError(
|
||||
`Could not communicate with local mode device at address ${
|
||||
opts.deviceHost
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
const versionError = new Error(
|
||||
'The supervisor version on this remote device does not support multicontainer local mode. ' +
|
||||
'Please update your device to resinOS v2.20.0 or greater from the dashboard.',
|
||||
);
|
||||
|
||||
try {
|
||||
const version = await api.getVersion();
|
||||
if (!semver.satisfies(version, '>=7.21.4')) {
|
||||
exitWithExpectedError(versionError);
|
||||
}
|
||||
} catch {
|
||||
exitWithExpectedError(versionError);
|
||||
}
|
||||
|
||||
logger.logInfo(`Starting build on device ${opts.deviceHost}`);
|
||||
|
||||
const project = await loadProject(logger, opts.source, 'local');
|
||||
|
||||
// Attempt to attach to the device's docker daemon
|
||||
const docker = connectToDocker(
|
||||
opts.deviceHost,
|
||||
opts.devicePort != null ? opts.devicePort : 2375,
|
||||
);
|
||||
|
||||
const tarStream = await tarDirectory(opts.source);
|
||||
|
||||
// Try to detect the device information
|
||||
const deviceInfo = await api.getDeviceInformation();
|
||||
|
||||
await performBuilds(
|
||||
project.composition,
|
||||
tarStream,
|
||||
docker,
|
||||
deviceInfo,
|
||||
logger,
|
||||
);
|
||||
|
||||
logger.logDebug('Setting device state...');
|
||||
// Now set the target state on the device
|
||||
|
||||
const currentTargetState = await api.getTargetState();
|
||||
|
||||
const targetState = generateTargetState(
|
||||
currentTargetState,
|
||||
project.composition,
|
||||
);
|
||||
logger.logDebug(`Sending target state: ${JSON.stringify(targetState)}`);
|
||||
|
||||
await api.setTargetState(targetState);
|
||||
|
||||
// Print an empty newline to seperate the build output
|
||||
// from the device output
|
||||
console.log();
|
||||
logger.logInfo('Streaming device logs...');
|
||||
// Now all we need to do is stream back the logs
|
||||
const logStream = await api.getLogStream();
|
||||
|
||||
await displayDeviceLogs(logStream, logger);
|
||||
}
|
||||
|
||||
function connectToDocker(host: string, port: number): Docker {
|
||||
return new Docker({
|
||||
host,
|
||||
port,
|
||||
Promise: Bluebird as any,
|
||||
});
|
||||
}
|
||||
|
||||
export async function performBuilds(
|
||||
composition: Composition,
|
||||
tarStream: Readable,
|
||||
docker: Docker,
|
||||
deviceInfo: DeviceInfo,
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
const multibuild = await import('resin-multibuild');
|
||||
|
||||
const buildTasks = await multibuild.splitBuildStream(composition, tarStream);
|
||||
|
||||
logger.logDebug('Found build tasks:');
|
||||
_.each(buildTasks, task => {
|
||||
let infoStr: string;
|
||||
if (task.external) {
|
||||
infoStr = `image pull [${task.imageName}]`;
|
||||
} else {
|
||||
infoStr = `build [${task.context}]`;
|
||||
}
|
||||
logger.logDebug(` ${task.serviceName}: ${infoStr}`);
|
||||
});
|
||||
|
||||
logger.logDebug(
|
||||
`Resolving services with [${deviceInfo.deviceType}|${deviceInfo.arch}]`,
|
||||
);
|
||||
await multibuild.performResolution(
|
||||
buildTasks,
|
||||
deviceInfo.arch,
|
||||
deviceInfo.deviceType,
|
||||
);
|
||||
|
||||
logger.logDebug('Found project types:');
|
||||
_.each(buildTasks, task => {
|
||||
if (!task.external) {
|
||||
logger.logDebug(` ${task.serviceName}: ${task.projectType}`);
|
||||
} else {
|
||||
logger.logDebug(` ${task.serviceName}: External image`);
|
||||
}
|
||||
});
|
||||
|
||||
logger.logDebug('Probing remote daemon for cache images');
|
||||
await assignDockerBuildOpts(docker, buildTasks);
|
||||
|
||||
logger.logDebug('Starting builds...');
|
||||
await assignOutputHandlers(buildTasks, logger);
|
||||
const localImages = await multibuild.performBuilds(buildTasks, docker);
|
||||
|
||||
// Check for failures
|
||||
await inspectBuildResults(localImages);
|
||||
|
||||
// Now tag any external images with the correct name that they should be,
|
||||
// as this won't be done by resin-multibuild
|
||||
await Bluebird.map(localImages, async localImage => {
|
||||
if (localImage.external) {
|
||||
// We can be sure that localImage.name is set here, because of the failure code above
|
||||
const image = docker.getImage(localImage.name!);
|
||||
await image.tag({
|
||||
repo: generateImageName(localImage.serviceName),
|
||||
force: true,
|
||||
});
|
||||
await image.remove({ force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function assignOutputHandlers(buildTasks: BuildTask[], logger: Logger) {
|
||||
_.each(buildTasks, task => {
|
||||
if (task.external) {
|
||||
task.progressHook = progressObj => {
|
||||
displayBuildLog(
|
||||
{ serviceName: task.serviceName, message: progressObj.progress },
|
||||
logger,
|
||||
);
|
||||
};
|
||||
} else {
|
||||
task.streamHook = stream => {
|
||||
stream.on('data', (buf: Buffer) => {
|
||||
const str = buf.toString().trimRight();
|
||||
if (str !== '') {
|
||||
displayBuildLog(
|
||||
{ serviceName: task.serviceName, message: str },
|
||||
logger,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getDeviceDockerImages(docker: Docker): Promise<string[]> {
|
||||
const images = await docker.listImages();
|
||||
|
||||
return _.map(images, 'Id');
|
||||
}
|
||||
|
||||
// Mutates buildTasks
|
||||
async function assignDockerBuildOpts(
|
||||
docker: Docker,
|
||||
buildTasks: BuildTask[],
|
||||
): Promise<void> {
|
||||
// Get all of the images on the remote docker daemon, so
|
||||
// that we can use all of them for cache
|
||||
const images = await getDeviceDockerImages(docker);
|
||||
|
||||
logger.logDebug(`Using ${images.length} on-device images for cache...`);
|
||||
|
||||
_.each(buildTasks, (task: BuildTask) => {
|
||||
task.dockerOpts = {
|
||||
cachefrom: images,
|
||||
labels: {
|
||||
'io.resin.local.image': '1',
|
||||
'io.resin.local.service': task.serviceName,
|
||||
},
|
||||
t: generateImageName(task.serviceName),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function generateImageName(serviceName: string): string {
|
||||
return `local_image_${serviceName}:latest`;
|
||||
}
|
||||
|
||||
function generateTargetState(
|
||||
currentTargetState: any,
|
||||
composition: Composition,
|
||||
): any {
|
||||
const services: { [serviceId: string]: any } = {};
|
||||
let idx = 1;
|
||||
_.each(composition.services, (opts, name) => {
|
||||
// Get rid of any build specific stuff
|
||||
opts = _.cloneDeep(opts);
|
||||
delete opts.build;
|
||||
delete opts.image;
|
||||
|
||||
const defaults = {
|
||||
environment: {},
|
||||
labels: {},
|
||||
};
|
||||
|
||||
services[idx] = _.merge(defaults, opts, {
|
||||
imageId: idx,
|
||||
serviceName: name,
|
||||
serviceId: idx,
|
||||
image: generateImageName(name),
|
||||
running: true,
|
||||
});
|
||||
idx += 1;
|
||||
});
|
||||
|
||||
const targetState = _.cloneDeep(currentTargetState);
|
||||
delete targetState.local.apps;
|
||||
|
||||
targetState.local.apps = {
|
||||
1: {
|
||||
name: 'localapp',
|
||||
commit: 'localcommit',
|
||||
releaseId: '1',
|
||||
services,
|
||||
volumes: composition.volumes || {},
|
||||
networks: composition.networks || {},
|
||||
},
|
||||
};
|
||||
|
||||
return targetState;
|
||||
}
|
||||
|
||||
async function inspectBuildResults(images: LocalImage[]): Promise<void> {
|
||||
const { exitWithExpectedError } = await import('../patterns');
|
||||
|
||||
const failures: LocalPushErrors.BuildFailure[] = [];
|
||||
|
||||
_.each(images, image => {
|
||||
if (!image.successful) {
|
||||
failures.push({
|
||||
error: image.error!,
|
||||
serviceName: image.serviceName,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (failures.length > 0) {
|
||||
exitWithExpectedError(new LocalPushErrors.BuildError(failures));
|
||||
}
|
||||
}
|
30
lib/utils/device/errors.ts
Normal file
30
lib/utils/device/errors.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import * as _ from 'lodash';
|
||||
import { TypedError } from 'typed-error';
|
||||
|
||||
export interface BuildFailure {
|
||||
error: Error;
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
export class BuildError extends TypedError {
|
||||
private failures: BuildFailure[];
|
||||
|
||||
public constructor(failures: BuildFailure[]) {
|
||||
super('Build error');
|
||||
|
||||
this.failures = failures;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
let str = 'Some services failed to build:\n';
|
||||
_.each(this.failures, failure => {
|
||||
str += `\t${failure.serviceName}: ${failure.error.message}\n`;
|
||||
});
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
export class DeviceAPIError extends TypedError {}
|
||||
|
||||
export class BadRequestDeviceAPIError extends DeviceAPIError {}
|
||||
export class ServiceUnavailableAPIError extends DeviceAPIError {}
|
83
lib/utils/device/logs.ts
Normal file
83
lib/utils/device/logs.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import chalk from 'chalk';
|
||||
import ColorHash = require('color-hash');
|
||||
import * as _ from 'lodash';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import Logger = require('../logger');
|
||||
|
||||
interface Log {
|
||||
message: string;
|
||||
timestamp?: number;
|
||||
serviceName?: string;
|
||||
|
||||
// There's also a serviceId and imageId, but they're
|
||||
// meaningless in local mode
|
||||
}
|
||||
|
||||
interface BuildLog {
|
||||
serviceName: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display logs from a device logging stream. This function will return
|
||||
* when the log stream ends.
|
||||
*
|
||||
* @param logs A stream which produces newline seperated log objects
|
||||
*/
|
||||
export function displayDeviceLogs(
|
||||
logs: Readable,
|
||||
logger: Logger,
|
||||
): Bluebird<void> {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
logs.on('data', log => {
|
||||
displayLogLine(log, logger);
|
||||
});
|
||||
|
||||
logs.on('error', reject);
|
||||
logs.on('end', resolve);
|
||||
});
|
||||
}
|
||||
|
||||
export function displayBuildLog(log: BuildLog, logger: Logger): void {
|
||||
const toPrint = `${getServiceColourFn(log.serviceName)(
|
||||
`[${log.serviceName}]`,
|
||||
)} ${log.message}`;
|
||||
logger.logBuild(toPrint);
|
||||
}
|
||||
|
||||
// mutates serviceColours
|
||||
function displayLogLine(log: string | Buffer, logger: Logger): void {
|
||||
try {
|
||||
const obj: Log = JSON.parse(log.toString());
|
||||
|
||||
let toPrint: string;
|
||||
if (obj.timestamp != null) {
|
||||
toPrint = `[${new Date(obj.timestamp).toLocaleString()}]`;
|
||||
} else {
|
||||
toPrint = `[${new Date().toLocaleString()}]`;
|
||||
}
|
||||
|
||||
if (obj.serviceName != null) {
|
||||
const colourFn = getServiceColourFn(obj.serviceName);
|
||||
|
||||
toPrint += ` ${colourFn(`[${obj.serviceName}]`)}`;
|
||||
}
|
||||
|
||||
toPrint += ` ${obj.message}`;
|
||||
|
||||
logger.logLogs(toPrint);
|
||||
} catch (e) {
|
||||
logger.logDebug(`Dropping device log due to failed parsing: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
const getServiceColourFn = _.memoize(_getServiceColourFn);
|
||||
|
||||
const colorHash = new ColorHash();
|
||||
function _getServiceColourFn(serviceName: string): (msg: string) => string {
|
||||
const [r, g, b] = colorHash.rgb(serviceName);
|
||||
|
||||
return chalk.rgb(r, g, b);
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
# Functions to help actions which rely on using docker
|
||||
|
||||
QEMU_VERSION = 'v2.5.50-resin-execve'
|
||||
QEMU_BIN_NAME = 'qemu-execve'
|
||||
Promise = require('bluebird')
|
||||
|
||||
# Use this function to seed an action's list of capitano options
|
||||
# with the docker options. Using this interface means that
|
||||
@ -71,12 +70,6 @@ exports.appendOptions = (opts) ->
|
||||
description: "Don't use docker layer caching when building"
|
||||
boolean: true
|
||||
},
|
||||
{
|
||||
signature: 'emulated'
|
||||
description: 'Run an emulated build using Qemu'
|
||||
boolean: true
|
||||
alias: 'e'
|
||||
},
|
||||
{
|
||||
signature: 'squash'
|
||||
description: 'Squash newly built layers into a single new layer'
|
||||
@ -84,8 +77,7 @@ exports.appendOptions = (opts) ->
|
||||
}
|
||||
]
|
||||
|
||||
exports.generateConnectOpts = generateConnectOpts = (opts) ->
|
||||
Promise = require('bluebird')
|
||||
generateConnectOpts = (opts) ->
|
||||
buildDockerodeOpts = require('dockerode-options')
|
||||
fs = require('mz/fs')
|
||||
_ = require('lodash')
|
||||
@ -131,294 +123,57 @@ exports.generateConnectOpts = generateConnectOpts = (opts) ->
|
||||
|
||||
return connectOpts
|
||||
|
||||
exports.tarDirectory = tarDirectory = (dir) ->
|
||||
Promise = require('bluebird')
|
||||
tar = require('tar-stream')
|
||||
klaw = require('klaw')
|
||||
path = require('path')
|
||||
fs = require('mz/fs')
|
||||
streamToPromise = require('stream-to-promise')
|
||||
|
||||
getFiles = ->
|
||||
streamToPromise(klaw(dir))
|
||||
.filter((item) -> not item.stats.isDirectory())
|
||||
.map((item) -> item.path)
|
||||
|
||||
pack = tar.pack()
|
||||
getFiles(dir)
|
||||
.map (file) ->
|
||||
relPath = path.relative(path.resolve(dir), file)
|
||||
Promise.join relPath, fs.stat(file), fs.readFile(file),
|
||||
(filename, stats, data) ->
|
||||
pack.entryAsync({ name: toPosixPath(filename), size: stats.size, mode: stats.mode }, data)
|
||||
.then ->
|
||||
pack.finalize()
|
||||
return pack
|
||||
|
||||
cacheHighlightStream = ->
|
||||
colors = require('colors/safe')
|
||||
es = require('event-stream')
|
||||
{ EOL } = require('os')
|
||||
|
||||
extractArrowMessage = (message) ->
|
||||
arrowTest = /^\s*-+>\s*(.+)/i
|
||||
if (match = arrowTest.exec(message))
|
||||
match[1]
|
||||
else
|
||||
undefined
|
||||
|
||||
es.mapSync (data) ->
|
||||
msg = extractArrowMessage(data)
|
||||
if msg? and msg.toLowerCase() == 'using cache'
|
||||
data = colors.bgGreen.black(msg)
|
||||
return data + EOL
|
||||
|
||||
parseBuildArgs = (args, onError) ->
|
||||
parseBuildArgs = (args) ->
|
||||
_ = require('lodash')
|
||||
if not _.isArray(args)
|
||||
args = [ args ]
|
||||
buildArgs = {}
|
||||
args.forEach (str) ->
|
||||
pair = /^([^\s]+?)=(.*)$/.exec(str)
|
||||
args.forEach (arg) ->
|
||||
pair = /^([^\s]+?)=(.*)$/.exec(arg)
|
||||
if pair?
|
||||
buildArgs[pair[1]] = pair[2]
|
||||
buildArgs[pair[1]] = pair[2] ? ''
|
||||
else
|
||||
onError(str)
|
||||
throw new Error("Could not parse build argument: '#{arg}'")
|
||||
return buildArgs
|
||||
|
||||
# Pass in the command line parameters and options and also
|
||||
# a function which will return the information about the bundle
|
||||
exports.runBuild = (params, options, getBundleInfo, logger) ->
|
||||
Promise = require('bluebird')
|
||||
dockerBuild = require('resin-docker-build')
|
||||
resolver = require('resin-bundle-resolve')
|
||||
es = require('event-stream')
|
||||
doodles = require('resin-doodles')
|
||||
transpose = require('docker-qemu-transpose')
|
||||
path = require('path')
|
||||
|
||||
# The default build context is the current directory
|
||||
params.source ?= '.'
|
||||
logs = ''
|
||||
# Only used in emulated builds
|
||||
qemuPath = ''
|
||||
|
||||
Promise.try ->
|
||||
return if not (options.emulated and platformNeedsQemu())
|
||||
|
||||
hasQemu()
|
||||
.then (present) ->
|
||||
if !present
|
||||
logger.logInfo('Installing qemu for ARM emulation...')
|
||||
installQemu()
|
||||
.then ->
|
||||
# Copy the qemu binary into the build context
|
||||
copyQemu(params.source)
|
||||
.then (binPath) ->
|
||||
qemuPath = path.relative(params.source, binPath)
|
||||
.then ->
|
||||
# Tar up the directory, ready for the build stream
|
||||
tarDirectory(params.source)
|
||||
.then (tarStream) ->
|
||||
new Promise (resolve, reject) ->
|
||||
hooks =
|
||||
buildSuccess: (image) ->
|
||||
# Show charlie. In the interest of cloud parity,
|
||||
# use console.log, not the standard logging streams
|
||||
doodle = doodles.getDoodle()
|
||||
console.log()
|
||||
console.log(doodle)
|
||||
console.log()
|
||||
|
||||
resolve({ image, log: logs + '\n' + doodle + '\n' } )
|
||||
|
||||
buildFailure: reject
|
||||
buildStream: (stream) ->
|
||||
if options.emulated
|
||||
logger.logInfo('Running emulated build')
|
||||
|
||||
getBundleInfo(options)
|
||||
.then (info) ->
|
||||
if !info?
|
||||
logger.logWarn '''
|
||||
Warning: No architecture/device type or application information provided.
|
||||
Dockerfile/project pre-processing will not be performed.
|
||||
'''
|
||||
return tarStream
|
||||
else
|
||||
[arch, deviceType] = info
|
||||
# Perform type resolution on the project
|
||||
bundle = new resolver.Bundle(tarStream, deviceType, arch)
|
||||
resolver.resolveBundle(bundle, resolver.getDefaultResolvers())
|
||||
.then (resolved) ->
|
||||
logger.logInfo("Building #{resolved.projectType} project")
|
||||
|
||||
return resolved.tarStream
|
||||
.then (buildStream) ->
|
||||
# if we need emulation
|
||||
if options.emulated and platformNeedsQemu()
|
||||
return transpose.transposeTarStream buildStream,
|
||||
hostQemuPath: toPosixPath(qemuPath)
|
||||
containerQemuPath: "/tmp/#{QEMU_BIN_NAME}"
|
||||
else
|
||||
return buildStream
|
||||
.then (buildStream) ->
|
||||
# Send the resolved tar stream to the docker daemon
|
||||
buildStream.pipe(stream)
|
||||
.catch(reject)
|
||||
|
||||
# And print the output
|
||||
logThroughStream = es.through (data) ->
|
||||
logs += data.toString()
|
||||
this.emit('data', data)
|
||||
|
||||
if options.emulated and platformNeedsQemu()
|
||||
buildThroughStream = transpose.getBuildThroughStream
|
||||
hostQemuPath: toPosixPath(qemuPath)
|
||||
containerQemuPath: "/tmp/#{QEMU_BIN_NAME}"
|
||||
|
||||
newStream = stream.pipe(buildThroughStream)
|
||||
else
|
||||
newStream = stream
|
||||
|
||||
newStream
|
||||
.pipe(logThroughStream)
|
||||
.pipe(cacheHighlightStream())
|
||||
.pipe(logger.streams.build)
|
||||
|
||||
# Create a builder
|
||||
generateConnectOpts(options)
|
||||
.tap (connectOpts) ->
|
||||
ensureDockerSeemsAccessible(connectOpts)
|
||||
.then (connectOpts) ->
|
||||
# Allow degugging output, hidden behind an env var
|
||||
logger.logDebug('Connecting with the following options:')
|
||||
logger.logDebug(JSON.stringify(connectOpts, null, ' '))
|
||||
|
||||
builder = new dockerBuild.Builder(connectOpts)
|
||||
opts = {}
|
||||
|
||||
if options.tag?
|
||||
opts['t'] = options.tag
|
||||
if options.nocache?
|
||||
opts['nocache'] = true
|
||||
if options.buildArg?
|
||||
opts['buildargs'] = parseBuildArgs options.buildArg, (arg) ->
|
||||
logger.logWarn("Could not parse variable: '#{arg}'")
|
||||
if options.squash?
|
||||
opts['squash'] = true
|
||||
|
||||
builder.createBuildStream(opts, hooks, reject)
|
||||
|
||||
# Given an image id or tag, export the image to a tar archive,
|
||||
# gzip the result, and buffer it to disk.
|
||||
exports.bufferImage = (docker, imageId, bufferFile) ->
|
||||
Promise = require('bluebird')
|
||||
streamUtils = require('./streams')
|
||||
|
||||
image = docker.getImage(imageId)
|
||||
imageMetadata = image.inspectAsync()
|
||||
|
||||
Promise.join image.get(), imageMetadata.get('Size'), (imageStream, imageSize) ->
|
||||
streamUtils.buffer(imageStream, bufferFile)
|
||||
.tap (bufferedStream) ->
|
||||
bufferedStream.length = imageSize
|
||||
exports.generateBuildOpts = (options) ->
|
||||
opts = {}
|
||||
if options.tag?
|
||||
opts.t = options.tag
|
||||
if options.nocache?
|
||||
opts.nocache = true
|
||||
if options.squash?
|
||||
opts.squash = true
|
||||
if options.buildArg?
|
||||
opts.buildargs = parseBuildArgs(options.buildArg)
|
||||
return opts
|
||||
|
||||
exports.getDocker = (options) ->
|
||||
Docker = require('dockerode')
|
||||
Promise = require('bluebird')
|
||||
|
||||
generateConnectOpts(options)
|
||||
.tap (connectOpts) ->
|
||||
ensureDockerSeemsAccessible(connectOpts)
|
||||
.then (connectOpts) ->
|
||||
# Use bluebird's promises
|
||||
connectOpts['Promise'] = Promise
|
||||
new Docker(connectOpts)
|
||||
.then(createClient)
|
||||
.tap(ensureDockerSeemsAccessible)
|
||||
|
||||
ensureDockerSeemsAccessible = (options) ->
|
||||
fs = require('mz/fs')
|
||||
exports.createClient = createClient = do ->
|
||||
# docker-toolbelt v3 is not backwards compatible as it removes all *Async
|
||||
# methods that are in wide use in the CLI. The workaround for now is to
|
||||
# manually promisify the client and replace all `new Docker()` calls with
|
||||
# this shared function that returns a promisified client.
|
||||
#
|
||||
# **New code must not use the *Async methods.**
|
||||
#
|
||||
Docker = require('docker-toolbelt')
|
||||
Promise.promisifyAll Docker.prototype, {
|
||||
filter: (name) -> name == 'run'
|
||||
multiArgs: true
|
||||
}
|
||||
Promise.promisifyAll(Docker.prototype)
|
||||
Promise.promisifyAll(new Docker({}).getImage().constructor.prototype)
|
||||
Promise.promisifyAll(new Docker({}).getContainer().constructor.prototype)
|
||||
|
||||
if options.socketPath?
|
||||
# If we're trying to use a socket, check it exists and we have access to it
|
||||
fs.access(options.socketPath, (fs.constants || fs).R_OK | (fs.constants || fs).W_OK)
|
||||
.return(true)
|
||||
.catch (err) ->
|
||||
throw new Error(
|
||||
"Docker seems to be unavailable (using socket #{options.socketPath}). Is it
|
||||
installed, and do you have permission to talk to it?"
|
||||
)
|
||||
else
|
||||
# Otherwise, we think we're probably ok
|
||||
Promise.resolve(true)
|
||||
return (opts) ->
|
||||
return new Docker(opts)
|
||||
|
||||
hasQemu = ->
|
||||
fs = require('mz/fs')
|
||||
|
||||
getQemuPath()
|
||||
.then(fs.stat)
|
||||
.return(true)
|
||||
.catchReturn(false)
|
||||
|
||||
getQemuPath = ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
path = require('path')
|
||||
fs = require('mz/fs')
|
||||
|
||||
resin.settings.get('binDirectory')
|
||||
.then (binDir) ->
|
||||
# The directory might not be created already,
|
||||
# if not, create it
|
||||
fs.access(binDir)
|
||||
.catch code: 'ENOENT', ->
|
||||
fs.mkdir(binDir)
|
||||
.then ->
|
||||
path.join(binDir, QEMU_BIN_NAME)
|
||||
|
||||
platformNeedsQemu = ->
|
||||
os = require('os')
|
||||
os.platform() == 'linux'
|
||||
|
||||
installQemu = ->
|
||||
request = require('request')
|
||||
fs = require('fs')
|
||||
zlib = require('zlib')
|
||||
|
||||
getQemuPath()
|
||||
.then (qemuPath) ->
|
||||
new Promise (resolve, reject) ->
|
||||
installStream = fs.createWriteStream(qemuPath)
|
||||
qemuUrl = "https://github.com/resin-io/qemu/releases/download/#{QEMU_VERSION}/#{QEMU_BIN_NAME}.gz"
|
||||
request(qemuUrl)
|
||||
.pipe(zlib.createGunzip())
|
||||
.pipe(installStream)
|
||||
.on('error', reject)
|
||||
.on('finish', resolve)
|
||||
|
||||
copyQemu = (context) ->
|
||||
path = require('path')
|
||||
fs = require('mz/fs')
|
||||
# Create a hidden directory in the build context, containing qemu
|
||||
binDir = path.join(context, '.resin')
|
||||
binPath = path.join(binDir, QEMU_BIN_NAME)
|
||||
|
||||
fs.access(binDir)
|
||||
.catch code: 'ENOENT', ->
|
||||
fs.mkdir(binDir)
|
||||
.then ->
|
||||
getQemuPath()
|
||||
.then (qemu) ->
|
||||
new Promise (resolve, reject) ->
|
||||
read = fs.createReadStream(qemu)
|
||||
write = fs.createWriteStream(binPath)
|
||||
read
|
||||
.pipe(write)
|
||||
.on('error', reject)
|
||||
.on('finish', resolve)
|
||||
.then ->
|
||||
fs.chmod(binPath, '755')
|
||||
.return(binPath)
|
||||
|
||||
toPosixPath = (systemPath) ->
|
||||
path = require('path')
|
||||
systemPath.replace(new RegExp('\\' + path.sep, 'g'), '/')
|
||||
ensureDockerSeemsAccessible = (docker) ->
|
||||
{ exitWithExpectedError } = require('./patterns')
|
||||
docker.ping().catch ->
|
||||
exitWithExpectedError('Docker seems to be unavailable. Is it installed and running?')
|
||||
|
@ -19,16 +19,12 @@ import Promise = require('bluebird');
|
||||
import _ = require('lodash');
|
||||
import chalk from 'chalk';
|
||||
import rindle = require('rindle');
|
||||
import imagefs = require('resin-image-fs');
|
||||
import visuals = require('resin-cli-visuals');
|
||||
import ResinSdk = require('resin-sdk');
|
||||
|
||||
import { execute } from 'president';
|
||||
import { InitializeEmitter, OperationState } from 'resin-device-init';
|
||||
|
||||
const extractStreamAsync = Promise.promisify(rindle.extract);
|
||||
const waitStreamAsync = Promise.promisify(rindle.wait);
|
||||
const presidentExecuteAsync = Promise.promisify(execute);
|
||||
|
||||
const resin = ResinSdk.fromSharedOptions();
|
||||
|
||||
@ -64,32 +60,40 @@ export function stateToString(state: OperationState) {
|
||||
}
|
||||
}
|
||||
|
||||
export function sudo(command: string[]) {
|
||||
export function sudo(
|
||||
command: string[],
|
||||
{ stderr, msg }: { stderr?: NodeJS.WritableStream; msg?: string } = {},
|
||||
) {
|
||||
const { executeWithPrivileges } = require('./sudo');
|
||||
|
||||
if (os.platform() !== 'win32') {
|
||||
console.log('If asked please type your computer password to continue');
|
||||
console.log(
|
||||
msg || 'If asked please type your computer password to continue',
|
||||
);
|
||||
}
|
||||
|
||||
command = _.union(_.take(process.argv, 2), command);
|
||||
return executeWithPrivileges(command, stderr);
|
||||
}
|
||||
|
||||
return presidentExecuteAsync(command);
|
||||
export function runCommand(command: string): Promise<void> {
|
||||
const capitano = require('capitano');
|
||||
return Promise.fromCallback(resolver => capitano.run(command, resolver));
|
||||
}
|
||||
|
||||
export function getManifest(
|
||||
image: string,
|
||||
deviceType: string,
|
||||
): Promise<ResinSdk.DeviceType> {
|
||||
const imagefs = require('resin-image-fs');
|
||||
// Attempt to read manifest from the first
|
||||
// partition, but fallback to the API if
|
||||
// we encounter any errors along the way.
|
||||
return imagefs
|
||||
.read({
|
||||
.readFile({
|
||||
image,
|
||||
partition: {
|
||||
primary: 1,
|
||||
},
|
||||
partition: 1,
|
||||
path: '/device-type.json',
|
||||
})
|
||||
.then(extractStreamAsync)
|
||||
.then(JSON.parse)
|
||||
.catch(() => resin.models.device.getManifestBySlug(deviceType));
|
||||
}
|
||||
@ -133,16 +137,28 @@ export function getArchAndDeviceType(
|
||||
);
|
||||
}
|
||||
|
||||
function getApplication(applicationName: string) {
|
||||
export function getApplication(applicationName: string) {
|
||||
// Check for an app of the form `user/application`, and send
|
||||
// that off to a special handler (before importing any modules)
|
||||
const match = /(\w+)\/(\w+)/.exec(applicationName);
|
||||
const match = applicationName.split('/');
|
||||
|
||||
if (match) {
|
||||
return resin.models.application.getAppByOwner(match[2], match[1]);
|
||||
const extraOptions = {
|
||||
$expand: {
|
||||
application_type: {
|
||||
$select: ['name', 'slug', 'supports_multicontainer', 'is_legacy'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (match.length > 1) {
|
||||
return resin.models.application.getAppByOwner(
|
||||
match[1],
|
||||
match[0],
|
||||
extraOptions,
|
||||
);
|
||||
}
|
||||
|
||||
return resin.models.application.get(applicationName);
|
||||
return resin.models.application.get(applicationName, extraOptions);
|
||||
}
|
||||
|
||||
// A function to reliably execute a command
|
||||
|
149
lib/utils/ignore.ts
Normal file
149
lib/utils/ignore.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import * as _ from 'lodash';
|
||||
import { fs } from 'mz';
|
||||
import * as path from 'path';
|
||||
|
||||
import dockerIgnore = require('@zeit/dockerignore');
|
||||
import ignore from 'ignore';
|
||||
|
||||
export enum IgnoreFileType {
|
||||
DockerIgnore,
|
||||
GitIgnore,
|
||||
}
|
||||
|
||||
interface IgnoreEntry {
|
||||
pattern: string;
|
||||
// The relative file path from the base path of the build context
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
export class FileIgnorer {
|
||||
private dockerIgnoreEntries: IgnoreEntry[];
|
||||
private gitIgnoreEntries: IgnoreEntry[];
|
||||
|
||||
private static ignoreFiles: Array<{
|
||||
pattern: string;
|
||||
type: IgnoreFileType;
|
||||
allowSubdirs: boolean;
|
||||
}> = [
|
||||
{
|
||||
pattern: '.gitignore',
|
||||
type: IgnoreFileType.GitIgnore,
|
||||
allowSubdirs: true,
|
||||
},
|
||||
{
|
||||
pattern: '.dockerignore',
|
||||
type: IgnoreFileType.DockerIgnore,
|
||||
allowSubdirs: false,
|
||||
},
|
||||
];
|
||||
|
||||
public constructor(public basePath: string) {
|
||||
this.dockerIgnoreEntries = [];
|
||||
this.gitIgnoreEntries = [];
|
||||
}
|
||||
/**
|
||||
* @param {string} relativePath
|
||||
* The relative pathname from the build context, for example a root level .gitignore should be
|
||||
* ./.gitignore
|
||||
* @returns IgnoreFileType
|
||||
* The type of ignore file, or null
|
||||
*/
|
||||
public getIgnoreFileType(relativePath: string): IgnoreFileType | null {
|
||||
for (const { pattern, type, allowSubdirs } of FileIgnorer.ignoreFiles) {
|
||||
if (
|
||||
path.basename(relativePath) === pattern &&
|
||||
(allowSubdirs || path.dirname(relativePath) === '.')
|
||||
) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* @param {string} fullPath
|
||||
* The full path on disk of the ignore file
|
||||
* @param {IgnoreFileType} type
|
||||
* @returns Promise
|
||||
*/
|
||||
public async addIgnoreFile(
|
||||
fullPath: string,
|
||||
type: IgnoreFileType,
|
||||
): Promise<void> {
|
||||
const contents = await fs.readFile(fullPath, 'utf8');
|
||||
|
||||
contents.split('\n').forEach(line => {
|
||||
// ignore empty lines and comments
|
||||
if (/\s*#/.test(line) || _.isEmpty(line)) {
|
||||
return;
|
||||
}
|
||||
this.addEntry(line, fullPath, type);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass this function as a predicate to a filter function, and it will filter
|
||||
// any ignored files
|
||||
public filter = (filename: string): boolean => {
|
||||
const dockerIgnoreHandle = dockerIgnore();
|
||||
const gitIgnoreHandle = ignore();
|
||||
|
||||
interface IgnoreHandle {
|
||||
add: (pattern: string) => void;
|
||||
ignores: (file: string) => boolean;
|
||||
}
|
||||
|
||||
const ignoreTypes: Array<{
|
||||
handle: IgnoreHandle;
|
||||
entries: IgnoreEntry[];
|
||||
}> = [
|
||||
{ handle: dockerIgnoreHandle, entries: this.dockerIgnoreEntries },
|
||||
{ handle: gitIgnoreHandle, entries: this.gitIgnoreEntries },
|
||||
];
|
||||
|
||||
const relFile = path.relative(this.basePath, filename);
|
||||
|
||||
_.each(ignoreTypes, ({ handle, entries }) => {
|
||||
_.each(entries, ({ pattern, filePath }) => {
|
||||
if (FileIgnorer.contains(path.posix.dirname(filePath), filename)) {
|
||||
handle.add(pattern);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return !_.some(ignoreTypes, ({ handle }) => handle.ignores(relFile));
|
||||
};
|
||||
|
||||
private addEntry(
|
||||
pattern: string,
|
||||
filePath: string,
|
||||
type: IgnoreFileType,
|
||||
): void {
|
||||
const entry: IgnoreEntry = { pattern, filePath };
|
||||
switch (type) {
|
||||
case IgnoreFileType.DockerIgnore:
|
||||
this.dockerIgnoreEntries.push(entry);
|
||||
break;
|
||||
case IgnoreFileType.GitIgnore:
|
||||
this.gitIgnoreEntries.push(entry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two paths, check whether the first contains the second
|
||||
* @param path1 The potentially containing path
|
||||
* @param path2 The potentially contained path
|
||||
* @return A boolean indicating whether `path1` contains `path2`
|
||||
*/
|
||||
private static contains(path1: string, path2: string): boolean {
|
||||
// First normalise the input, to remove any path weirdness
|
||||
path1 = path.posix.normalize(path1);
|
||||
path2 = path.posix.normalize(path2);
|
||||
|
||||
// Now test if the start of the relative path contains ../ ,
|
||||
// which would tell us that path1 is not part of path2
|
||||
return !/^\.\.\//.test(path.posix.relative(path1, path2));
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { EOL as eol } from 'os';
|
||||
import _ = require('lodash');
|
||||
import chalk from 'chalk';
|
||||
import _ = require('lodash');
|
||||
import { EOL as eol } from 'os';
|
||||
import { StreamLogger } from 'resin-stream-logger';
|
||||
|
||||
class Logger {
|
||||
@ -11,6 +11,7 @@ class Logger {
|
||||
success: NodeJS.ReadWriteStream;
|
||||
warn: NodeJS.ReadWriteStream;
|
||||
error: NodeJS.ReadWriteStream;
|
||||
logs: NodeJS.ReadWriteStream;
|
||||
};
|
||||
|
||||
public formatMessage: (name: string, message: string) => string;
|
||||
@ -23,6 +24,7 @@ class Logger {
|
||||
logger.addPrefix('success', chalk.green('[Success]'));
|
||||
logger.addPrefix('warn', chalk.yellow('[Warn]'));
|
||||
logger.addPrefix('error', chalk.red('[Error]'));
|
||||
logger.addPrefix('logs', chalk.green('[Logs]'));
|
||||
|
||||
this.streams = {
|
||||
build: logger.createLogStream('build'),
|
||||
@ -31,6 +33,7 @@ class Logger {
|
||||
success: logger.createLogStream('success'),
|
||||
warn: logger.createLogStream('warn'),
|
||||
error: logger.createLogStream('error'),
|
||||
logs: logger.createLogStream('logs'),
|
||||
};
|
||||
|
||||
_.forEach(this.streams, function(stream, key) {
|
||||
@ -61,6 +64,14 @@ class Logger {
|
||||
logError(msg: string) {
|
||||
return this.streams.error.write(msg + eol);
|
||||
}
|
||||
|
||||
logBuild(msg: string) {
|
||||
return this.streams.build.write(msg + eol);
|
||||
}
|
||||
|
||||
logLogs(msg: string) {
|
||||
return this.streams.logs.write(msg + eol);
|
||||
}
|
||||
}
|
||||
|
||||
export = Logger;
|
||||
|
@ -18,13 +18,11 @@ import _ = require('lodash');
|
||||
import Promise = require('bluebird');
|
||||
import form = require('resin-cli-form');
|
||||
import visuals = require('resin-cli-visuals');
|
||||
import ResinSdk = require('resin-sdk');
|
||||
import resin = require('resin-sdk-preconfigured');
|
||||
import chalk from 'chalk';
|
||||
import validation = require('./validation');
|
||||
import messages = require('./messages');
|
||||
|
||||
const resin = ResinSdk.fromSharedOptions();
|
||||
|
||||
export function authenticate(options: {}): Promise<void> {
|
||||
return form
|
||||
.run(
|
||||
@ -132,10 +130,8 @@ export function confirm(
|
||||
});
|
||||
}
|
||||
|
||||
export function selectApplication(
|
||||
filter: (app: ResinSdk.Application) => boolean,
|
||||
) {
|
||||
resin.models.application
|
||||
export function selectApplication(filter: (app: resin.Application) => boolean) {
|
||||
return resin.models.application
|
||||
.hasAny()
|
||||
.then(function(hasAnyApplications) {
|
||||
if (!hasAnyApplications) {
|
||||
@ -165,7 +161,7 @@ export function selectOrCreateApplication() {
|
||||
|
||||
return resin.models.application.getAll().then(applications => {
|
||||
const appOptions = _.map<
|
||||
ResinSdk.Application,
|
||||
resin.Application,
|
||||
{ name: string; value: string | null }
|
||||
>(applications, application => ({
|
||||
name: `${application.app_name} (${application.device_type})`,
|
||||
@ -227,13 +223,15 @@ export function awaitDevice(uuid: string) {
|
||||
export function inferOrSelectDevice(preferredUuid: string) {
|
||||
return resin.models.device
|
||||
.getAll()
|
||||
.filter<ResinSdk.Device>(device => device.is_online)
|
||||
.filter<resin.Device>(device => device.is_online)
|
||||
.then(onlineDevices => {
|
||||
if (_.isEmpty(onlineDevices)) {
|
||||
throw new Error("You don't have any devices online");
|
||||
}
|
||||
|
||||
const defaultUuid = _.map(onlineDevices, 'uuid').includes(preferredUuid)
|
||||
const defaultUuid = _(onlineDevices)
|
||||
.map('uuid')
|
||||
.includes(preferredUuid)
|
||||
? preferredUuid
|
||||
: onlineDevices[0].uuid;
|
||||
|
||||
@ -249,12 +247,26 @@ export function inferOrSelectDevice(preferredUuid: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function selectFromList<T>(
|
||||
message: string,
|
||||
choices: Array<T & { name: string }>,
|
||||
): Promise<T> {
|
||||
return form.ask({
|
||||
message,
|
||||
type: 'list',
|
||||
choices: _.map(choices, s => ({
|
||||
name: s.name,
|
||||
value: s,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export function printErrorMessage(message: string) {
|
||||
console.error(chalk.red(message));
|
||||
console.error(chalk.red(`\n${messages.getHelp}\n`));
|
||||
}
|
||||
|
||||
export function expectedError(message: string | Error) {
|
||||
export function exitWithExpectedError(message: string | Error) {
|
||||
if (message instanceof Error) {
|
||||
({ message } = message);
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
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 nplugm = require('nplugm');
|
||||
import _ = require('lodash');
|
||||
import capitano = require('capitano');
|
||||
import patterns = require('./patterns');
|
||||
|
||||
export function register(regex: RegExp): Promise<void> {
|
||||
return nplugm
|
||||
.list(regex)
|
||||
.map(async function(plugin: any) {
|
||||
const command = await import(plugin);
|
||||
command.plugin = true;
|
||||
if (!_.isArray(command)) {
|
||||
return capitano.command(command);
|
||||
}
|
||||
return _.each(command, capitano.command);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
return patterns.printErrorMessage(error.message);
|
||||
});
|
||||
}
|
323
lib/utils/promote.ts
Normal file
323
lib/utils/promote.ts
Normal file
@ -0,0 +1,323 @@
|
||||
import { stripIndent } from 'common-tags';
|
||||
import { ResinSDK, Application } from 'resin-sdk';
|
||||
|
||||
import Logger = require('./logger');
|
||||
|
||||
import { runCommand } from './helpers';
|
||||
import { exec, execBuffered } from './ssh';
|
||||
|
||||
const MIN_RESINOS_VERSION = 'v2.14.0';
|
||||
|
||||
export async function join(
|
||||
logger: Logger,
|
||||
sdk: ResinSDK,
|
||||
deviceHostnameOrIp?: string,
|
||||
appName?: string,
|
||||
): Promise<void> {
|
||||
logger.logDebug('Checking login...');
|
||||
const isLoggedIn = await sdk.auth.isLoggedIn();
|
||||
if (!isLoggedIn) {
|
||||
logger.logInfo("Looks like you're not logged in yet!");
|
||||
logger.logInfo("Let's go through a quick wizard to get you started.\n");
|
||||
await runCommand('login');
|
||||
}
|
||||
|
||||
logger.logDebug('Determining device...');
|
||||
const deviceIp = await getOrSelectLocalDevice(deviceHostnameOrIp);
|
||||
await assertDeviceIsCompatible(deviceIp);
|
||||
logger.logDebug(`Using device: ${deviceIp}`);
|
||||
|
||||
logger.logDebug('Determining device type...');
|
||||
const deviceType = await getDeviceType(deviceIp);
|
||||
logger.logDebug(`Device type: ${deviceType}`);
|
||||
|
||||
logger.logDebug('Determining application...');
|
||||
const app = await getOrSelectApplication(sdk, deviceType, appName);
|
||||
logger.logDebug(`Using application: ${app.app_name} (${app.device_type})`);
|
||||
if (app.device_type != deviceType) {
|
||||
logger.logDebug(`Forcing device type to: ${deviceType}`);
|
||||
app.device_type = deviceType;
|
||||
}
|
||||
|
||||
logger.logDebug('Generating application config...');
|
||||
const config = await generateApplicationConfig(sdk, app);
|
||||
logger.logDebug(`Using config: ${JSON.stringify(config, null, 2)}`);
|
||||
|
||||
logger.logDebug('Configuring...');
|
||||
await configure(deviceIp, config);
|
||||
logger.logDebug('All done.');
|
||||
|
||||
const platformUrl = await sdk.settings.get('resinUrl');
|
||||
logger.logSuccess(`Device successfully joined ${platformUrl}!`);
|
||||
}
|
||||
|
||||
export async function leave(
|
||||
logger: Logger,
|
||||
_sdk: ResinSDK,
|
||||
deviceHostnameOrIp?: string,
|
||||
): Promise<void> {
|
||||
logger.logDebug('Determining device...');
|
||||
const deviceIp = await getOrSelectLocalDevice(deviceHostnameOrIp);
|
||||
await assertDeviceIsCompatible(deviceIp);
|
||||
logger.logDebug(`Using device: ${deviceIp}`);
|
||||
|
||||
logger.logDebug('Deconfiguring...');
|
||||
await deconfigure(deviceIp);
|
||||
logger.logDebug('All done.');
|
||||
|
||||
logger.logSuccess('Device successfully left the platform.');
|
||||
}
|
||||
|
||||
async function execCommand(
|
||||
deviceIp: string,
|
||||
cmd: string,
|
||||
msg: string,
|
||||
): Promise<void> {
|
||||
const through = await import('through2');
|
||||
const visuals = await import('resin-cli-visuals');
|
||||
|
||||
const spinner = new visuals.Spinner(`[${deviceIp}] Connecting...`);
|
||||
const innerSpinner = spinner.spinner;
|
||||
|
||||
const stream = through(function(data, _enc, cb) {
|
||||
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
|
||||
cb(null, data);
|
||||
});
|
||||
|
||||
spinner.start();
|
||||
await exec(deviceIp, cmd, stream);
|
||||
spinner.stop();
|
||||
}
|
||||
|
||||
async function configure(deviceIp: string, config: any): Promise<void> {
|
||||
// Passing the JSON is slightly tricky due to the many layers of indirection
|
||||
// so we just base64-encode it here and decode it at the other end, when invoking
|
||||
// os-config.
|
||||
const json = JSON.stringify(config);
|
||||
const b64 = Buffer.from(json).toString('base64');
|
||||
const str = `"$(base64 -d <<< ${b64})"`;
|
||||
await execCommand(deviceIp, `os-config join '${str}'`, 'Configuring...');
|
||||
}
|
||||
|
||||
async function deconfigure(deviceIp: string): Promise<void> {
|
||||
await execCommand(deviceIp, 'os-config leave', 'Configuring...');
|
||||
}
|
||||
|
||||
async function assertDeviceIsCompatible(deviceIp: string): Promise<void> {
|
||||
const { exitWithExpectedError } = await import('../utils/patterns');
|
||||
try {
|
||||
await execBuffered(deviceIp, 'os-config --version');
|
||||
} catch (err) {
|
||||
exitWithExpectedError(stripIndent`
|
||||
Device "${deviceIp}" is incompatible and cannot join or leave an application.
|
||||
Please select or provision device with resinOS newer than ${MIN_RESINOS_VERSION}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getDeviceType(deviceIp: string): Promise<string> {
|
||||
const output = await execBuffered(deviceIp, 'cat /etc/os-release');
|
||||
const match = /^SLUG="([^"]+)"$/m.exec(output);
|
||||
if (!match) {
|
||||
throw new Error('Failed to determine device type');
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
async function getOrSelectLocalDevice(deviceIp?: string): Promise<string> {
|
||||
if (deviceIp) {
|
||||
return deviceIp;
|
||||
}
|
||||
|
||||
const through = await import('through2');
|
||||
|
||||
let ip: string | null = null;
|
||||
const stream = through(function(data, _enc, cb) {
|
||||
const match = /^==> Selected device: (.*)$/m.exec(data.toString());
|
||||
if (match) {
|
||||
ip = match[1];
|
||||
cb();
|
||||
} else {
|
||||
cb(null, data);
|
||||
}
|
||||
});
|
||||
|
||||
stream.pipe(process.stderr);
|
||||
|
||||
const { sudo } = await import('../utils/helpers');
|
||||
const command = process.argv.slice(0, 2).concat(['internal', 'scandevices']);
|
||||
await sudo(command, {
|
||||
stderr: stream,
|
||||
msg:
|
||||
'Scanning for local devices. If asked, please type your computer password.',
|
||||
});
|
||||
|
||||
if (!ip) {
|
||||
throw new Error('No device selected');
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
async function getOrSelectApplication(
|
||||
sdk: ResinSDK,
|
||||
deviceType: string,
|
||||
appName?: string,
|
||||
): Promise<Application> {
|
||||
const _ = await import('lodash');
|
||||
const form = await import('resin-cli-form');
|
||||
const { selectFromList } = await import('../utils/patterns');
|
||||
|
||||
const allDeviceTypes = await sdk.models.config.getDeviceTypes();
|
||||
const deviceTypeManifest = _.find(allDeviceTypes, { slug: deviceType });
|
||||
if (!deviceTypeManifest) {
|
||||
throw new Error(`"${deviceType}" is not a valid device type`);
|
||||
}
|
||||
const compatibleDeviceTypes = _(allDeviceTypes)
|
||||
.filter({ arch: deviceTypeManifest.arch })
|
||||
.map(type => type.slug)
|
||||
.value();
|
||||
|
||||
const options: any = {
|
||||
$expand: { user: { $select: ['username'] } },
|
||||
$filter: { device_type: { $in: compatibleDeviceTypes } },
|
||||
};
|
||||
|
||||
if (!appName) {
|
||||
// No application specified, show a list to select one.
|
||||
const applications = await sdk.models.application.getAll(options);
|
||||
if (applications.length === 0) {
|
||||
const shouldCreateApp = await form.ask({
|
||||
message:
|
||||
'You have no applications 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);
|
||||
}
|
||||
return selectFromList(
|
||||
'Select application',
|
||||
_.map(applications, app => _.merge({ name: app.app_name }, app)),
|
||||
);
|
||||
}
|
||||
|
||||
// We're given an application; resolve it if it's ambiguous and also validate
|
||||
// it's of appropriate device type.
|
||||
options.$filter = { app_name: appName };
|
||||
|
||||
// Check for an app of the form `user/application` and update the API query.
|
||||
const match = appName.split('/');
|
||||
if (match.length > 1) {
|
||||
// These will match at most one app, so we'll return early.
|
||||
options.$expand.user.$filter = { username: match[0] };
|
||||
options.$filter.app_name = match[1];
|
||||
}
|
||||
|
||||
// Fetch all applications with the given name that are accessible to the user
|
||||
const applications = await sdk.pine.get<Application>({
|
||||
resource: 'application',
|
||||
options,
|
||||
});
|
||||
|
||||
if (applications.length === 0) {
|
||||
const shouldCreateApp = await form.ask({
|
||||
message:
|
||||
`No application found with name "${appName}".\n` +
|
||||
'Would you like to create it now?',
|
||||
type: 'confirm',
|
||||
default: true,
|
||||
});
|
||||
if (shouldCreateApp) {
|
||||
return createApplication(sdk, deviceType, options.$filter.app_name);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// We've found at least one app with the given name.
|
||||
// Filter out apps for non-matching device types and see what we're left with.
|
||||
const validApplications = applications.filter(app =>
|
||||
_.includes(compatibleDeviceTypes, app.device_type),
|
||||
);
|
||||
|
||||
if (validApplications.length === 1) {
|
||||
return validApplications[0];
|
||||
}
|
||||
|
||||
// If we got more than one application with the same name it means that the
|
||||
// user has access to a collab app with the same name as a personal app. We
|
||||
// present a list to the user which shows the fully qualified application
|
||||
// name (user/appname) and allows them to select
|
||||
return selectFromList(
|
||||
'Found multiple applications with that name; please select the one to use',
|
||||
_.map(validApplications, app => {
|
||||
const owner = _.get(app, 'user[0].username');
|
||||
return _.merge({ name: `${owner}/${app.app_name}` }, app);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function createApplication(
|
||||
sdk: ResinSDK,
|
||||
deviceType: string,
|
||||
name?: string,
|
||||
): Promise<Application> {
|
||||
const form = await import('resin-cli-form');
|
||||
const validation = await import('./validation');
|
||||
const patterns = await import('./patterns');
|
||||
|
||||
const user = await sdk.auth.getUserId();
|
||||
const queryOptions = {
|
||||
$filter: { user },
|
||||
};
|
||||
|
||||
const appName = await new Promise<string>(async (resolve, reject) => {
|
||||
while (true) {
|
||||
try {
|
||||
const appName = await form.ask({
|
||||
message: 'Enter a name for your new application:',
|
||||
type: 'input',
|
||||
default: name,
|
||||
validate: validation.validateApplicationName,
|
||||
});
|
||||
|
||||
try {
|
||||
await sdk.models.application.get(appName, queryOptions);
|
||||
patterns.printErrorMessage(
|
||||
'You already have an application with that name; please choose another.',
|
||||
);
|
||||
continue;
|
||||
} catch (err) {
|
||||
return resolve(appName);
|
||||
}
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return sdk.models.application.create({
|
||||
name: appName,
|
||||
deviceType,
|
||||
});
|
||||
}
|
||||
|
||||
async function generateApplicationConfig(sdk: ResinSDK, app: Application) {
|
||||
const form = await import('resin-cli-form');
|
||||
const { generateApplicationConfig: configGen } = await import('./config');
|
||||
|
||||
const manifest = await sdk.models.device.getManifestBySlug(app.device_type);
|
||||
const opts =
|
||||
manifest.options && manifest.options.filter(opt => opt.name !== 'network');
|
||||
const values = await form.run(opts);
|
||||
|
||||
const config = await configGen(app, values);
|
||||
if (config.connectivity === 'connman') {
|
||||
delete config.connectivity;
|
||||
delete config.files;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
86
lib/utils/qemu.coffee
Normal file
86
lib/utils/qemu.coffee
Normal file
@ -0,0 +1,86 @@
|
||||
Promise = require('bluebird')
|
||||
|
||||
exports.QEMU_VERSION = QEMU_VERSION = 'v2.5.50-resin-execve'
|
||||
exports.QEMU_BIN_NAME = QEMU_BIN_NAME = 'qemu-execve'
|
||||
|
||||
exports.installQemuIfNeeded = Promise.method (emulated, logger) ->
|
||||
return false if not (emulated and platformNeedsQemu())
|
||||
|
||||
hasQemu()
|
||||
.then (present) ->
|
||||
if !present
|
||||
logger.logInfo('Installing qemu for ARM emulation...')
|
||||
installQemu()
|
||||
.return(true)
|
||||
|
||||
exports.qemuPathInContext = (context) ->
|
||||
path = require('path')
|
||||
binDir = path.join(context, '.resin')
|
||||
binPath = path.join(binDir, QEMU_BIN_NAME)
|
||||
path.relative(context, binPath)
|
||||
|
||||
exports.copyQemu = (context) ->
|
||||
path = require('path')
|
||||
fs = require('mz/fs')
|
||||
# Create a hidden directory in the build context, containing qemu
|
||||
binDir = path.join(context, '.resin')
|
||||
binPath = path.join(binDir, QEMU_BIN_NAME)
|
||||
|
||||
Promise.resolve(fs.mkdir(binDir))
|
||||
.catch(code: 'EEXIST', ->)
|
||||
.then ->
|
||||
getQemuPath()
|
||||
.then (qemu) ->
|
||||
new Promise (resolve, reject) ->
|
||||
read = fs.createReadStream(qemu)
|
||||
write = fs.createWriteStream(binPath)
|
||||
read
|
||||
.pipe(write)
|
||||
.on('error', reject)
|
||||
.on('finish', resolve)
|
||||
.then ->
|
||||
fs.chmod(binPath, '755')
|
||||
.then ->
|
||||
path.relative(context, binPath)
|
||||
|
||||
hasQemu = ->
|
||||
fs = require('mz/fs')
|
||||
|
||||
getQemuPath()
|
||||
.then(fs.stat)
|
||||
.return(true)
|
||||
.catchReturn(false)
|
||||
|
||||
getQemuPath = ->
|
||||
resin = require('resin-sdk').fromSharedOptions()
|
||||
path = require('path')
|
||||
fs = require('mz/fs')
|
||||
|
||||
resin.settings.get('binDirectory')
|
||||
.then (binDir) ->
|
||||
Promise.resolve(fs.mkdir(binDir))
|
||||
.catch(code: 'EEXIST', ->)
|
||||
.then ->
|
||||
path.join(binDir, QEMU_BIN_NAME)
|
||||
|
||||
platformNeedsQemu = ->
|
||||
os = require('os')
|
||||
os.platform() == 'linux'
|
||||
|
||||
installQemu = ->
|
||||
request = require('request')
|
||||
fs = require('fs')
|
||||
zlib = require('zlib')
|
||||
|
||||
getQemuPath()
|
||||
.then (qemuPath) ->
|
||||
new Promise (resolve, reject) ->
|
||||
installStream = fs.createWriteStream(qemuPath)
|
||||
qemuUrl = "https://github.com/resin-io/qemu/releases/download/#{QEMU_VERSION}/#{QEMU_BIN_NAME}.gz"
|
||||
request(qemuUrl)
|
||||
.on('error', reject)
|
||||
.pipe(zlib.createGunzip())
|
||||
.on('error', reject)
|
||||
.pipe(installStream)
|
||||
.on('error', reject)
|
||||
.on('finish', resolve)
|
255
lib/utils/remote-build.ts
Normal file
255
lib/utils/remote-build.ts
Normal file
@ -0,0 +1,255 @@
|
||||
/*
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
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 JSONStream from 'JSONStream';
|
||||
import * as request from 'request';
|
||||
import { ResinSDK } from 'resin-sdk';
|
||||
import * as Stream from 'stream';
|
||||
import { TypedError } from 'typed-error';
|
||||
|
||||
import { tarDirectory } from './compose';
|
||||
|
||||
const DEBUG_MODE = !!process.env.DEBUG;
|
||||
|
||||
const CURSOR_METADATA_REGEX = /([a-z]+)([0-9]+)?/;
|
||||
const TRIM_REGEX = /\n+$/;
|
||||
|
||||
export interface BuildOpts {
|
||||
emulated: boolean;
|
||||
nocache: boolean;
|
||||
}
|
||||
|
||||
export interface RemoteBuild {
|
||||
app: string;
|
||||
owner: string;
|
||||
source: string;
|
||||
auth: string;
|
||||
baseUrl: string;
|
||||
opts: BuildOpts;
|
||||
|
||||
sdk: ResinSDK;
|
||||
|
||||
// For internal use
|
||||
releaseId?: number;
|
||||
hadError?: boolean;
|
||||
}
|
||||
|
||||
interface BuilderMessage {
|
||||
message: string;
|
||||
type?: string;
|
||||
replace?: boolean;
|
||||
isError?: boolean;
|
||||
// These will be set when the type === 'metadata'
|
||||
resource?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export class RemoteBuildFailedError extends TypedError {
|
||||
public constructor(message = 'Remote build failed') {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getBuilderEndpoint(
|
||||
baseUrl: string,
|
||||
owner: string,
|
||||
app: string,
|
||||
opts: BuildOpts,
|
||||
): Promise<string> {
|
||||
const querystring = await import('querystring');
|
||||
const args = querystring.stringify({
|
||||
owner,
|
||||
app,
|
||||
emulated: opts.emulated,
|
||||
nocache: opts.nocache,
|
||||
});
|
||||
return `https://builder.${baseUrl}/v3/build?${args}`;
|
||||
}
|
||||
|
||||
export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
|
||||
const Bluebird = await import('bluebird');
|
||||
|
||||
const stream = await getRequestStream(build);
|
||||
|
||||
// Special windows handling (win64 also reports win32)
|
||||
if (process.platform === 'win32') {
|
||||
const readline = (await import('readline')).createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
readline.on('SIGINT', () => process.emit('SIGINT'));
|
||||
}
|
||||
|
||||
return new Bluebird((resolve, reject) => {
|
||||
// Setup interrupt handlers so we can cancel the build if the user presses
|
||||
// ctrl+c
|
||||
|
||||
// This is necessary because the `exit-hook` module is used by several
|
||||
// dependencies, and will exit without calling the following handler.
|
||||
// Once https://github.com/resin-io/resin-cli/issues/867 has been solved,
|
||||
// we are free to (and definitely should) remove the below line
|
||||
process.removeAllListeners('SIGINT');
|
||||
process.on('SIGINT', () => {
|
||||
process.stderr.write('Received SIGINT, cleaning up. Please wait.\n');
|
||||
cancelBuildIfNecessary(build).then(() => {
|
||||
stream.end();
|
||||
process.exit(130);
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('data', getBuilderMessageHandler(build));
|
||||
stream.on('end', resolve);
|
||||
stream.on('error', reject);
|
||||
}).then(() => {
|
||||
if (build.hadError) {
|
||||
throw new RemoteBuildFailedError();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleBuilderMetadata(obj: BuilderMessage, build: RemoteBuild) {
|
||||
const { stripIndent } = await import('common-tags');
|
||||
|
||||
switch (obj.resource) {
|
||||
case 'cursor':
|
||||
const readline = await import('readline');
|
||||
|
||||
if (obj.value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = obj.value.match(CURSOR_METADATA_REGEX);
|
||||
|
||||
if (!match) {
|
||||
// FIXME: Make this error nicer.
|
||||
console.log(
|
||||
stripIndent`
|
||||
Warning: ignoring unknown builder command. You may experience
|
||||
odd build output. Maybe you need to update resin-cli?`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = match[1];
|
||||
const amount = match[2] || 1;
|
||||
|
||||
switch (value) {
|
||||
case 'erase':
|
||||
readline.clearLine(process.stdout, 0);
|
||||
process.stdout.write('\r');
|
||||
break;
|
||||
case 'up':
|
||||
readline.moveCursor(process.stdout, 0, -amount);
|
||||
break;
|
||||
case 'down':
|
||||
readline.moveCursor(process.stdout, 0, amount);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'buildLogId':
|
||||
// The name of this resource is slightly dated, but this is the release
|
||||
// id from the API. We need to save this so that if the user ctrl+c's the
|
||||
// build we can cancel it on the API.
|
||||
build.releaseId = parseInt(obj.value!, 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function getBuilderMessageHandler(
|
||||
build: RemoteBuild,
|
||||
): (obj: BuilderMessage) => Promise<void> {
|
||||
return async (obj: BuilderMessage) => {
|
||||
if (DEBUG_MODE) {
|
||||
console.log(`[debug] handling message: ${JSON.stringify(obj)}`);
|
||||
}
|
||||
if (obj.type != null && obj.type === 'metadata') {
|
||||
return handleBuilderMetadata(obj, build);
|
||||
}
|
||||
if (obj.message) {
|
||||
const readline = await import('readline');
|
||||
readline.clearLine(process.stdout, 0);
|
||||
|
||||
const message = obj.message.replace(TRIM_REGEX, '');
|
||||
if (obj.replace) {
|
||||
process.stdout.write(`\r${message}`);
|
||||
} else {
|
||||
process.stdout.write(`\r${message}\n`);
|
||||
}
|
||||
}
|
||||
if (obj.isError) {
|
||||
build.hadError = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function cancelBuildIfNecessary(build: RemoteBuild): Promise<void> {
|
||||
if (build.releaseId != null) {
|
||||
await build.sdk.pine.patch({
|
||||
resource: 'release',
|
||||
id: build.releaseId,
|
||||
body: {
|
||||
status: 'cancelled',
|
||||
end_timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getRequestStream(build: RemoteBuild): Promise<Stream.Duplex> {
|
||||
const path = await import('path');
|
||||
const visuals = await import('resin-cli-visuals');
|
||||
const zlib = await import('zlib');
|
||||
|
||||
const tarSpinner = new visuals.Spinner('Packaging the project source...');
|
||||
tarSpinner.start();
|
||||
// Tar the directory so that we can send it to the builder
|
||||
const tarStream = await tarDirectory(path.resolve(build.source));
|
||||
tarSpinner.stop();
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
console.log('[debug] Opening builder connection');
|
||||
}
|
||||
const post = request.post({
|
||||
url: await getBuilderEndpoint(
|
||||
build.baseUrl,
|
||||
build.owner,
|
||||
build.app,
|
||||
build.opts,
|
||||
),
|
||||
auth: {
|
||||
bearer: build.auth,
|
||||
},
|
||||
headers: {
|
||||
'Content-Encoding': 'gzip',
|
||||
},
|
||||
body: tarStream.pipe(
|
||||
zlib.createGzip({
|
||||
level: 6,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const uploadSpinner = new visuals.Spinner(
|
||||
'Uploading source package to resin cloud',
|
||||
);
|
||||
uploadSpinner.start();
|
||||
|
||||
const parseStream = post.pipe(JSONStream.parse('*'));
|
||||
parseStream.on('data', () => uploadSpinner.stop());
|
||||
return parseStream as Stream.Duplex;
|
||||
}
|
65
lib/utils/ssh.ts
Normal file
65
lib/utils/ssh.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { spawn } from 'child_process';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { TypedError } from 'typed-error';
|
||||
|
||||
import { getSubShellCommand } from './helpers';
|
||||
|
||||
export class ExecError extends TypedError {
|
||||
public cmd: string;
|
||||
public exitCode: number;
|
||||
|
||||
constructor(cmd: string, exitCode: number) {
|
||||
super(`Command '${cmd}' failed with error: ${exitCode}`);
|
||||
this.cmd = cmd;
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
}
|
||||
|
||||
export async function exec(
|
||||
deviceIp: string,
|
||||
cmd: string,
|
||||
stdout?: NodeJS.WritableStream,
|
||||
): Promise<void> {
|
||||
const command = `ssh \
|
||||
-t \
|
||||
-p 22222 \
|
||||
-o LogLevel=ERROR \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
root@${deviceIp} \
|
||||
${cmd}`;
|
||||
|
||||
const stdio = ['ignore', stdout ? 'pipe' : 'inherit', 'ignore'];
|
||||
const { program, args } = getSubShellCommand(command);
|
||||
|
||||
const exitCode = await new Bluebird<number>((resolve, reject) => {
|
||||
const ps = spawn(program, args, { stdio })
|
||||
.on('error', reject)
|
||||
.on('close', resolve);
|
||||
|
||||
if (stdout) {
|
||||
ps.stdout.pipe(stdout);
|
||||
}
|
||||
});
|
||||
if (exitCode != 0) {
|
||||
throw new ExecError(cmd, exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
export async function execBuffered(
|
||||
deviceIp: string,
|
||||
cmd: string,
|
||||
enc?: string,
|
||||
): Promise<string> {
|
||||
const through = await import('through2');
|
||||
const buffer: string[] = [];
|
||||
await exec(
|
||||
deviceIp,
|
||||
cmd,
|
||||
through(function(data, _enc, cb) {
|
||||
buffer.push(data.toString(enc));
|
||||
cb();
|
||||
}),
|
||||
);
|
||||
return buffer.join('');
|
||||
}
|
26
lib/utils/sudo.ts
Normal file
26
lib/utils/sudo.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as rindle from 'rindle';
|
||||
|
||||
export async function executeWithPrivileges(
|
||||
command: string[],
|
||||
stderr?: NodeJS.WritableStream,
|
||||
): Promise<void> {
|
||||
const opts = {
|
||||
stdio: ['inherit', 'inherit', stderr ? 'pipe' : 'inherit'],
|
||||
env: process.env,
|
||||
};
|
||||
|
||||
const args = process.argv
|
||||
.slice(0, 2)
|
||||
.concat(['internal', 'sudo', command.join(' ')]);
|
||||
|
||||
const ps = spawn(args[0], args.slice(1), opts);
|
||||
|
||||
if (stderr) {
|
||||
ps.stderr.pipe(stderr);
|
||||
}
|
||||
|
||||
return Bluebird.fromCallback(resolver => rindle.wait(ps, resolver));
|
||||
}
|
66
lib/utils/tty.coffee
Normal file
66
lib/utils/tty.coffee
Normal file
@ -0,0 +1,66 @@
|
||||
|
||||
windowSize = {}
|
||||
|
||||
updateWindowSize = ->
|
||||
size = require('window-size')?.get()
|
||||
windowSize.width = size?.width
|
||||
windowSize.height = size?.height
|
||||
|
||||
process.stdout.on('resize', updateWindowSize)
|
||||
|
||||
module.exports = (stream = process.stdout) ->
|
||||
# make sure we get initial metrics
|
||||
updateWindowSize()
|
||||
|
||||
currentWindowSize = ->
|
||||
# always return a copy.
|
||||
# width/height can be undefined if no TTY.
|
||||
width: windowSize.width
|
||||
height: windowSize.height
|
||||
|
||||
hideCursor = ->
|
||||
stream.write('\u001B[?25l')
|
||||
|
||||
showCursor = ->
|
||||
stream.write('\u001B[?25h')
|
||||
|
||||
cursorUp = (rows = 0) ->
|
||||
stream.write("\u001B[#{rows}A")
|
||||
|
||||
cursorDown = (rows = 0) ->
|
||||
stream.write("\u001B[#{rows}B")
|
||||
|
||||
cursorHidden = ->
|
||||
Promise = require('bluebird')
|
||||
Promise.try(hideCursor).disposer(showCursor)
|
||||
|
||||
write = (str) ->
|
||||
stream.write(str)
|
||||
|
||||
writeLine = (str) ->
|
||||
stream.write("#{str}\n")
|
||||
|
||||
clearLine = ->
|
||||
stream.write('\u001B[2K\r')
|
||||
|
||||
replaceLine = (str) ->
|
||||
clearLine()
|
||||
write(str)
|
||||
|
||||
deleteToEnd = ->
|
||||
stream.write('\u001b[0J')
|
||||
|
||||
return {
|
||||
stream
|
||||
currentWindowSize
|
||||
hideCursor
|
||||
showCursor
|
||||
cursorHidden
|
||||
cursorUp
|
||||
cursorDown
|
||||
write
|
||||
writeLine
|
||||
clearLine
|
||||
replaceLine
|
||||
deleteToEnd
|
||||
}
|
99
package.json
99
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "resin-cli",
|
||||
"version": "6.13.1",
|
||||
"version": "8.0.0",
|
||||
"description": "The official resin.io CLI tool",
|
||||
"main": "./build/actions/index.js",
|
||||
"homepage": "https://github.com/resin-io/resin-cli",
|
||||
@ -21,7 +21,9 @@
|
||||
"pkg": {
|
||||
"scripts": [
|
||||
"node_modules/resin-sync/build/capitano/*.js",
|
||||
"node_modules/resin-sync/build/sync/*.js"
|
||||
"node_modules/resin-sync/build/sync/*.js",
|
||||
"node_modules/resin-compose-parse/src/schemas/*.json",
|
||||
"node_modules/raven/lib/instrumentation/*.js"
|
||||
],
|
||||
"assets": [
|
||||
"build/auth/pages/*.ejs",
|
||||
@ -31,15 +33,17 @@
|
||||
"scripts": {
|
||||
"prebuild": "rimraf build/ build-bin/ build-zip/",
|
||||
"build": "npm run build:src && npm run build:bin",
|
||||
"build:src": "npm run prettify && npm run lint && gulp build && tsc && npm run build:doc",
|
||||
"build:src": "npm run prettify && npm run lint && npm run build:fast && npm run build:doc",
|
||||
"build:fast": "gulp build && tsc",
|
||||
"build:doc": "mkdirp doc/ && ts-node automation/capitanodoc/index.ts > doc/cli.markdown",
|
||||
"build:bin": "ts-node --type-check -P automation automation/build-bin.ts",
|
||||
"release": "npm run build && ts-node --type-check -P automation automation/deploy-bin.ts",
|
||||
"pretest": "npm run build",
|
||||
"test": "gulp test",
|
||||
"test:fast": "npm run build:fast && gulp test",
|
||||
"ci": "npm run test && catch-uncommitted",
|
||||
"watch": "gulp watch",
|
||||
"prettify": "prettier --write \"{lib,tests,automation,typings}/**/*.ts\"",
|
||||
"prettify": "prettier --write \"{lib,tests,automation,typings}/**/*.ts\" --config ./node_modules/resin-lint/config/.prettierrc",
|
||||
"lint": "resin-lint lib/ tests/ && resin-lint --typescript automation/ lib/ typings/ tests/",
|
||||
"prepublish": "require-npm4-to-publish",
|
||||
"prepublishOnly": "npm run build"
|
||||
@ -54,13 +58,17 @@
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archiver": "^2.0.1",
|
||||
"@types/bluebird": "^3.5.19",
|
||||
"@types/fs-extra": "^5.0.0",
|
||||
"@types/is-root": "^1.0.0",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
"@types/archiver": "2.1.2",
|
||||
"@types/bluebird": "3.5.19",
|
||||
"@types/common-tags": "1.4.0",
|
||||
"@types/es6-promise": "0.0.32",
|
||||
"@types/fs-extra": "5.0.4",
|
||||
"@types/is-root": "1.0.0",
|
||||
"@types/lodash": "4.14.103",
|
||||
"@types/mkdirp": "0.5.2",
|
||||
"@types/node": "10.5.2",
|
||||
"@types/prettyjson": "0.0.28",
|
||||
"@types/raven": "^2.1.2",
|
||||
"@types/raven": "2.5.1",
|
||||
"catch-uncommitted": "^1.0.0",
|
||||
"ent": "^2.2.0",
|
||||
"filehound": "^1.16.2",
|
||||
@ -72,15 +80,20 @@
|
||||
"gulp-shell": "^0.5.2",
|
||||
"mochainon": "^2.0.0",
|
||||
"pkg": "^4.3.0-beta.1",
|
||||
"prettier": "^1.9.2",
|
||||
"prettier": "^1.14.2",
|
||||
"publish-release": "^1.3.3",
|
||||
"require-npm4-to-publish": "^1.0.0",
|
||||
"resin-lint": "^1.5.0",
|
||||
"resin-lint": "^2.0.0",
|
||||
"rewire": "^3.0.2",
|
||||
"ts-node": "^4.0.1",
|
||||
"typescript": "2.4.0"
|
||||
"typescript": "2.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@resin.io/valid-email": "^0.1.0",
|
||||
"@types/stream-to-promise": "2.2.0",
|
||||
"@types/through2": "^2.0.33",
|
||||
"@zeit/dockerignore": "0.0.1",
|
||||
"JSONStream": "^1.0.3",
|
||||
"ansi-escapes": "^2.0.0",
|
||||
"any-promise": "^1.3.0",
|
||||
"archiver": "^2.1.0",
|
||||
@ -89,64 +102,80 @@
|
||||
"body-parser": "^1.14.1",
|
||||
"capitano": "^1.7.0",
|
||||
"chalk": "^2.3.0",
|
||||
"cli-truncate": "^1.1.0",
|
||||
"coffeescript": "^1.12.6",
|
||||
"color-hash": "^1.0.3",
|
||||
"columnify": "^1.5.2",
|
||||
"common-tags": "^1.7.2",
|
||||
"denymount": "^2.2.0",
|
||||
"docker-qemu-transpose": "^0.2.2",
|
||||
"docker-toolbelt": "^1.3.3",
|
||||
"dockerode": "^2.5.0",
|
||||
"docker-progress": "^3.0.1",
|
||||
"docker-qemu-transpose": "^0.5.1",
|
||||
"docker-toolbelt": "^3.1.0",
|
||||
"dockerode": "^2.5.5",
|
||||
"dockerode-options": "^0.2.1",
|
||||
"drivelist": "^5.0.22",
|
||||
"ejs": "^2.5.7",
|
||||
"etcher-image-write": "^9.0.3",
|
||||
"express": "^4.13.3",
|
||||
"ext2fs": "1.0.7",
|
||||
"global-tunnel-ng": "^2.1.1",
|
||||
"hasbin": "^1.2.3",
|
||||
"humanize": "0.0.9",
|
||||
"ignore": "^5.0.2",
|
||||
"inquirer": "^3.1.1",
|
||||
"is-root": "^1.0.0",
|
||||
"js-yaml": "^3.7.0",
|
||||
"klaw": "^1.3.1",
|
||||
"js-yaml": "^3.10.0",
|
||||
"klaw": "^3.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"minimatch": "^3.0.4",
|
||||
"mixpanel": "^0.4.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"moment": "^2.12.0",
|
||||
"moment": "^2.20.1",
|
||||
"moment-duration-format": "^2.2.1",
|
||||
"mz": "^2.6.0",
|
||||
"node-cleanup": "^2.1.2",
|
||||
"nplugm": "^3.0.0",
|
||||
"opn": "^5.1.0",
|
||||
"president": "^2.0.1",
|
||||
"prettyjson": "^1.1.3",
|
||||
"progress-stream": "^2.0.0",
|
||||
"raven": "^1.2.0",
|
||||
"reconfix": "^0.0.3",
|
||||
"raven": "^2.5.0",
|
||||
"reconfix": "^0.1.0",
|
||||
"request": "^2.81.0",
|
||||
"resin-bundle-resolve": "^0.0.2",
|
||||
"resin-cli-errors": "^1.2.0",
|
||||
"resin-cli-form": "^1.4.1",
|
||||
"resin-bundle-resolve": "^0.6.0",
|
||||
"resin-cli-form": "^2.0.0",
|
||||
"resin-cli-visuals": "^1.4.0",
|
||||
"resin-compose-parse": "^2.0.0",
|
||||
"resin-config-json": "^1.0.0",
|
||||
"resin-device-config": "^4.0.0",
|
||||
"resin-device-init": "^4.0.0",
|
||||
"resin-docker-build": "^0.4.0",
|
||||
"resin-doodles": "0.0.1",
|
||||
"resin-image-fs": "^2.3.0",
|
||||
"resin-image-fs": "^5.0.2",
|
||||
"resin-image-manager": "^5.0.0",
|
||||
"resin-preload": "^5.0.0",
|
||||
"resin-sdk": "^7.0.0",
|
||||
"resin-multibuild": "^0.9.0",
|
||||
"resin-preload": "^7.0.0",
|
||||
"resin-release": "^1.2.0",
|
||||
"resin-sdk": "10.0.0-beta2",
|
||||
"resin-sdk-preconfigured": "^6.9.0",
|
||||
"resin-semver": "^1.3.0",
|
||||
"resin-settings-client": "^3.6.1",
|
||||
"resin-stream-logger": "^0.1.0",
|
||||
"resin-sync": "^9.2.3",
|
||||
"resin-sync": "^9.3.3",
|
||||
"rimraf": "^2.4.3",
|
||||
"rindle": "^1.0.0",
|
||||
"semver": "^5.3.0",
|
||||
"stream-to-promise": "^2.2.0",
|
||||
"split": "^1.0.1",
|
||||
"string-width": "^2.1.1",
|
||||
"strip-ansi-stream": "^1.0.0",
|
||||
"tar-stream": "^1.5.5",
|
||||
"through2": "^2.0.3",
|
||||
"tmp": "0.0.31",
|
||||
"typed-error": "^3.0.0",
|
||||
"umount": "^1.1.6",
|
||||
"unzip2": "^0.2.5",
|
||||
"update-notifier": "^2.2.0"
|
||||
"update-notifier": "^2.2.0",
|
||||
"window-size": "^1.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"removedrive": "^1.0.0"
|
||||
"removedrive": "^1.0.0",
|
||||
"windosu": "^0.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
73
resin-completion.bash
Normal file
73
resin-completion.bash
Normal file
@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
_resin_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
commands="app apps build config deploy device devices env envs help key \
|
||||
keys local login logout logs note os preload quickstart settings \
|
||||
signup ssh sync util version whoami"
|
||||
# Sub-completions
|
||||
app_cmds="create restart rm"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="identify init move public-url reboot register rename rm \
|
||||
shutdown"
|
||||
device_public_url_cmds="disable enable status"
|
||||
env_cmds="add rename rm"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash logs push scan ssh stop"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
util_cmds="available-drives"
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"app")
|
||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||
;;
|
||||
"config")
|
||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||
;;
|
||||
"device")
|
||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||
;;
|
||||
"env")
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
"key")
|
||||
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||
;;
|
||||
"local")
|
||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||
;;
|
||||
"os")
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
"util")
|
||||
COMPREPLY=( $(compgen -W "$util_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
elif [ $COMP_CWORD -eq 3 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"public-url")
|
||||
COMPREPLY=( $(compgen -W "$device_public_url_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _resin_complete resin
|
@ -51,11 +51,11 @@ describe 'Server:', ->
|
||||
describe 'given the token authenticates with the server', ->
|
||||
|
||||
beforeEach ->
|
||||
@utilsIsTokenValidStub = m.sinon.stub(utils, 'isTokenValid')
|
||||
@utilsIsTokenValidStub.returns(Promise.resolve(true))
|
||||
@loginIfTokenValidStub = m.sinon.stub(utils, 'loginIfTokenValid')
|
||||
@loginIfTokenValidStub.returns(Promise.resolve(true))
|
||||
|
||||
afterEach ->
|
||||
@utilsIsTokenValidStub.restore()
|
||||
@loginIfTokenValidStub.restore()
|
||||
|
||||
it 'should eventually be the token', (done) ->
|
||||
promise = server.awaitForToken(options)
|
||||
@ -74,11 +74,11 @@ describe 'Server:', ->
|
||||
describe 'given the token does not authenticate with the server', ->
|
||||
|
||||
beforeEach ->
|
||||
@utilsIsTokenValidStub = m.sinon.stub(utils, 'isTokenValid')
|
||||
@utilsIsTokenValidStub.returns(Promise.resolve(false))
|
||||
@loginIfTokenValidStub = m.sinon.stub(utils, 'loginIfTokenValid')
|
||||
@loginIfTokenValidStub.returns(Promise.resolve(false))
|
||||
|
||||
afterEach ->
|
||||
@utilsIsTokenValidStub.restore()
|
||||
@loginIfTokenValidStub.restore()
|
||||
|
||||
it 'should be rejected', (done) ->
|
||||
promise = server.awaitForToken(options)
|
||||
|
@ -1,64 +1,64 @@
|
||||
m = require('mochainon')
|
||||
url = require('url')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
utils = require('../../build/auth/utils')
|
||||
|
||||
tokens = require('./tokens.json')
|
||||
|
||||
rewire = require('rewire')
|
||||
utils = rewire('../../build/auth/utils')
|
||||
resin = utils.__get__('resin')
|
||||
|
||||
describe 'Utils:', ->
|
||||
|
||||
describe '.getDashboardLoginURL()', ->
|
||||
|
||||
it 'should eventually be a valid url', (done) ->
|
||||
it 'should eventually be a valid url', ->
|
||||
utils.getDashboardLoginURL('https://127.0.0.1:3000/callback').then (loginUrl) ->
|
||||
m.chai.expect ->
|
||||
url.parse(loginUrl)
|
||||
.to.not.throw(Error)
|
||||
.nodeify(done)
|
||||
|
||||
it 'should eventually contain an https protocol', (done) ->
|
||||
|
||||
it 'should eventually contain an https protocol', ->
|
||||
Promise.props
|
||||
dashboardUrl: resin.settings.get('dashboardUrl')
|
||||
loginUrl: utils.getDashboardLoginURL('https://127.0.0.1:3000/callback')
|
||||
.then ({ dashboardUrl, loginUrl }) ->
|
||||
protocol = url.parse(loginUrl).protocol
|
||||
m.chai.expect(protocol).to.equal(url.parse(dashboardUrl).protocol)
|
||||
.nodeify(done)
|
||||
|
||||
it 'should correctly escape a callback url without a path', (done) ->
|
||||
it 'should correctly escape a callback url without a path', ->
|
||||
Promise.props
|
||||
dashboardUrl: resin.settings.get('dashboardUrl')
|
||||
loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000')
|
||||
.then ({ dashboardUrl, loginUrl }) ->
|
||||
expectedUrl = "#{dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000"
|
||||
m.chai.expect(loginUrl).to.equal(expectedUrl)
|
||||
.nodeify(done)
|
||||
|
||||
it 'should correctly escape a callback url with a path', (done) ->
|
||||
it 'should correctly escape a callback url with a path', ->
|
||||
Promise.props
|
||||
dashboardUrl: resin.settings.get('dashboardUrl')
|
||||
loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000/callback')
|
||||
.then ({ dashboardUrl, loginUrl }) ->
|
||||
expectedUrl = "#{dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000%252Fcallback"
|
||||
m.chai.expect(loginUrl).to.equal(expectedUrl)
|
||||
.nodeify(done)
|
||||
|
||||
describe '.isTokenValid()', ->
|
||||
describe '.loginIfTokenValid()', ->
|
||||
|
||||
it 'should eventually be false if token is undefined', ->
|
||||
promise = utils.isTokenValid(undefined)
|
||||
promise = utils.loginIfTokenValid(undefined)
|
||||
m.chai.expect(promise).to.eventually.be.false
|
||||
|
||||
it 'should eventually be false if token is null', ->
|
||||
promise = utils.isTokenValid(null)
|
||||
promise = utils.loginIfTokenValid(null)
|
||||
m.chai.expect(promise).to.eventually.be.false
|
||||
|
||||
it 'should eventually be false if token is an empty string', ->
|
||||
promise = utils.isTokenValid('')
|
||||
promise = utils.loginIfTokenValid('')
|
||||
m.chai.expect(promise).to.eventually.be.false
|
||||
|
||||
it 'should eventually be false if token is a string containing only spaces', ->
|
||||
promise = utils.isTokenValid(' ')
|
||||
promise = utils.loginIfTokenValid(' ')
|
||||
m.chai.expect(promise).to.eventually.be.false
|
||||
|
||||
describe 'given the token does not authenticate with the server', ->
|
||||
@ -71,31 +71,31 @@ describe 'Utils:', ->
|
||||
@resinAuthIsLoggedInStub.restore()
|
||||
|
||||
it 'should eventually be false', ->
|
||||
promise = utils.isTokenValid(tokens.johndoe.token)
|
||||
promise = utils.loginIfTokenValid(tokens.johndoe.token)
|
||||
m.chai.expect(promise).to.eventually.be.false
|
||||
|
||||
describe 'given there was a token already', ->
|
||||
|
||||
beforeEach (done) ->
|
||||
resin.auth.loginWithToken(tokens.janedoe.token).nodeify(done)
|
||||
beforeEach ->
|
||||
resin.auth.loginWithToken(tokens.janedoe.token)
|
||||
|
||||
it 'should preserve the old token', (done) ->
|
||||
it 'should preserve the old token', ->
|
||||
resin.auth.getToken().then (originalToken) ->
|
||||
m.chai.expect(originalToken).to.equal(tokens.janedoe.token)
|
||||
return utils.isTokenValid(tokens.johndoe.token)
|
||||
return utils.loginIfTokenValid(tokens.johndoe.token)
|
||||
.then(resin.auth.getToken).then (currentToken) ->
|
||||
m.chai.expect(currentToken).to.equal(tokens.janedoe.token)
|
||||
.nodeify(done)
|
||||
|
||||
describe 'given there was no token', ->
|
||||
|
||||
beforeEach (done) ->
|
||||
resin.auth.logout().nodeify(done)
|
||||
beforeEach ->
|
||||
resin.auth.logout()
|
||||
|
||||
it 'should stay without a token', (done) ->
|
||||
utils.isTokenValid(tokens.johndoe.token).then ->
|
||||
m.chai.expect(resin.token.get()).to.eventually.not.exist
|
||||
.nodeify(done)
|
||||
it 'should stay without a token', ->
|
||||
utils.loginIfTokenValid(tokens.johndoe.token).then ->
|
||||
resin.auth.isLoggedIn()
|
||||
.then (isLoggedIn) ->
|
||||
m.chai.expect(isLoggedIn).to.equal(false)
|
||||
|
||||
describe 'given the token does authenticate with the server', ->
|
||||
|
||||
@ -107,5 +107,5 @@ describe 'Utils:', ->
|
||||
@resinAuthIsLoggedInStub.restore()
|
||||
|
||||
it 'should eventually be true', ->
|
||||
promise = utils.isTokenValid(tokens.johndoe.token)
|
||||
promise = utils.loginIfTokenValid(tokens.johndoe.token)
|
||||
m.chai.expect(promise).to.eventually.be.true
|
||||
|
85
tests/utils/ignore.spec.coffee
Normal file
85
tests/utils/ignore.spec.coffee
Normal file
@ -0,0 +1,85 @@
|
||||
require 'mocha'
|
||||
chai = require 'chai'
|
||||
_ = require 'lodash'
|
||||
path = require('path')
|
||||
|
||||
expect = chai.expect
|
||||
|
||||
{ FileIgnorer, IgnoreFileType } = require '../../build/utils/ignore'
|
||||
|
||||
describe 'File ignorer', ->
|
||||
|
||||
it 'should detect ignore files', ->
|
||||
f = new FileIgnorer('.' + path.sep)
|
||||
expect(f.getIgnoreFileType('.gitignore')).to.equal(IgnoreFileType.GitIgnore)
|
||||
expect(f.getIgnoreFileType('.dockerignore')).to.equal(IgnoreFileType.DockerIgnore)
|
||||
expect(f.getIgnoreFileType('./.gitignore')).to.equal(IgnoreFileType.GitIgnore)
|
||||
expect(f.getIgnoreFileType('./.dockerignore')).to.equal(IgnoreFileType.DockerIgnore)
|
||||
|
||||
# gitignore files can appear in subdirectories, but dockerignore files cannot
|
||||
expect(f.getIgnoreFileType('./subdir/.gitignore')).to.equal(IgnoreFileType.GitIgnore)
|
||||
expect(f.getIgnoreFileType('./subdir/.dockerignore')).to.equal(null)
|
||||
expect(f.getIgnoreFileType('./subdir/subdir2/.gitignore')).to.equal(IgnoreFileType.GitIgnore)
|
||||
|
||||
expect(f.getIgnoreFileType('file')).to.equal(null)
|
||||
expect(f.getIgnoreFileType('./file')).to.equal(null)
|
||||
|
||||
it 'should filter files from the root directory', ->
|
||||
|
||||
ignore = new FileIgnorer('.' + path.sep)
|
||||
ignore.gitIgnoreEntries = [
|
||||
{ pattern: '*.ignore', filePath: '.gitignore' }
|
||||
]
|
||||
ignore.dockerIgnoreEntries = [
|
||||
{ pattern: '*.ignore2', filePath: '.dockerignore' }
|
||||
]
|
||||
files = [
|
||||
'a'
|
||||
'a/b'
|
||||
'a/b/c'
|
||||
'file.ignore'
|
||||
'file2.ignore'
|
||||
'file.ignore2'
|
||||
'file2.ignore'
|
||||
]
|
||||
|
||||
expect(_.filter(files, ignore.filter.bind(ignore))).to.deep.equal([
|
||||
'a'
|
||||
'a/b'
|
||||
'a/b/c'
|
||||
])
|
||||
|
||||
it 'should filter files from subdirectories', ->
|
||||
|
||||
ignore = new FileIgnorer('.' + path.sep)
|
||||
ignore.gitIgnoreEntries = [
|
||||
{ pattern: '*.ignore', filePath: 'lib/.gitignore' }
|
||||
]
|
||||
files = [
|
||||
'test.ignore'
|
||||
'root.ignore'
|
||||
'lib/normal-file'
|
||||
'lib/should.ignore'
|
||||
'lib/thistoo.ignore'
|
||||
]
|
||||
expect(_.filter(files, ignore.filter.bind(ignore))).to.deep.equal([
|
||||
'test.ignore'
|
||||
'root.ignore'
|
||||
'lib/normal-file'
|
||||
])
|
||||
|
||||
ignore.gitIgnoreEntries = [
|
||||
{ pattern: '*.ignore', filePath: './lib/.gitignore' }
|
||||
]
|
||||
files = [
|
||||
'test.ignore'
|
||||
'root.ignore'
|
||||
'lib/normal-file'
|
||||
'lib/should.ignore'
|
||||
'lib/thistoo.ignore'
|
||||
]
|
||||
expect(_.filter(files, ignore.filter.bind(ignore))).to.deep.equal([
|
||||
'test.ignore'
|
||||
'root.ignore'
|
||||
'lib/normal-file'
|
||||
])
|
@ -1,14 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es6",
|
||||
"outDir": "build",
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"preserveConstEnums": true,
|
||||
"removeComments": true,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
// es5 defaults:
|
||||
"dom",
|
||||
|
53
typings/JSONStream.d.ts
vendored
Normal file
53
typings/JSONStream.d.ts
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
// These are the DefinitelyTyped typings for JSONStream, but because of this
|
||||
// mismatch in case of jsonstream and JSONStream, it is necessary to include
|
||||
// them this way, with an upper case module declaration. They have also
|
||||
// been slightly edited to remove the extra `declare` keyworks (which are
|
||||
// not necessary or accepted inside a `declare module '...' {` block)
|
||||
declare module 'JSONStream' {
|
||||
// Type definitions for JSONStream v0.8.0
|
||||
// Project: https://github.com/dominictarr/JSONStream
|
||||
// Definitions by: Bart van der Schoor <https://github.com/Bartvds>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
export interface Options {
|
||||
recurse: boolean;
|
||||
}
|
||||
|
||||
export function parse(pattern: any): NodeJS.ReadWriteStream;
|
||||
export function parse(patterns: any[]): NodeJS.ReadWriteStream;
|
||||
|
||||
/**
|
||||
* Create a writable stream.
|
||||
* you may pass in custom open, close, and seperator strings. But, by default,
|
||||
* JSONStream.stringify() will create an array,
|
||||
* (with default options open='[\n', sep='\n,\n', close='\n]\n')
|
||||
*/
|
||||
export function stringify(): NodeJS.ReadWriteStream;
|
||||
|
||||
/** If you call JSONStream.stringify(false) the elements will only be seperated by a newline. */
|
||||
export function stringify(
|
||||
newlineOnly: NewlineOnlyIndicator,
|
||||
): NodeJS.ReadWriteStream;
|
||||
type NewlineOnlyIndicator = false;
|
||||
|
||||
/**
|
||||
* Create a writable stream.
|
||||
* you may pass in custom open, close, and seperator strings. But, by default,
|
||||
* JSONStream.stringify() will create an array,
|
||||
* (with default options open='[\n', sep='\n,\n', close='\n]\n')
|
||||
*/
|
||||
export function stringify(
|
||||
open: string,
|
||||
sep: string,
|
||||
close: string,
|
||||
): NodeJS.ReadWriteStream;
|
||||
|
||||
export function stringifyObject(): NodeJS.ReadWriteStream;
|
||||
export function stringifyObject(
|
||||
open: string,
|
||||
sep: string,
|
||||
close: string,
|
||||
): NodeJS.ReadWriteStream;
|
||||
}
|
2
typings/capitano.d.ts
vendored
2
typings/capitano.d.ts
vendored
@ -21,6 +21,8 @@ declare module 'capitano' {
|
||||
help: string;
|
||||
options?: OptionDefinition[];
|
||||
permission?: 'user';
|
||||
root?: boolean;
|
||||
primary?: boolean;
|
||||
action(params: P, options: O, done: () => void): void;
|
||||
}
|
||||
|
||||
|
12
typings/color-hash.d.ts
vendored
Normal file
12
typings/color-hash.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
declare module 'color-hash' {
|
||||
interface Hasher {
|
||||
hex(text: string): string;
|
||||
}
|
||||
|
||||
class ColorHash {
|
||||
hex(text: string): string;
|
||||
rgb(text: string): [number, number, number];
|
||||
}
|
||||
|
||||
export = ColorHash;
|
||||
}
|
13
typings/dockerfile-template.d.ts
vendored
Normal file
13
typings/dockerfile-template.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
declare module 'dockerfile-template' {
|
||||
/**
|
||||
* Variables which define what will be replaced, and what they will be replaced with.
|
||||
*/
|
||||
export interface TemplateVariables {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export function process(
|
||||
content: string,
|
||||
variables: TemplateVariables,
|
||||
): string;
|
||||
}
|
1
typings/resin-cli-errors.d.ts
vendored
1
typings/resin-cli-errors.d.ts
vendored
@ -1 +0,0 @@
|
||||
declare module 'resin-cli-errors';
|
2
typings/resin-image-fs.d.ts
vendored
2
typings/resin-image-fs.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
declare module 'resin-image-fs' {
|
||||
import Promise = require('bluebird');
|
||||
|
||||
export function read(options: {}): Promise<NodeJS.ReadableStream>;
|
||||
export function readFile(options: {}): Promise<string>;
|
||||
}
|
||||
|
802
typings/resin-sdk-preconfigured.d.ts
vendored
802
typings/resin-sdk-preconfigured.d.ts
vendored
@ -1,5 +1,801 @@
|
||||
declare module 'resin-sdk-preconfigured' {
|
||||
import { ResinSDK } from 'resin-sdk';
|
||||
let sdk: ResinSDK;
|
||||
export = sdk;
|
||||
import * as Promise from 'bluebird';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as ResinErrors from 'resin-errors';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
/* tslint:disable:no-namespace */
|
||||
|
||||
namespace Pine {
|
||||
// based on https://github.com/resin-io/pinejs-client-js/blob/master/core.d.ts
|
||||
|
||||
type RawFilter =
|
||||
| string
|
||||
| Array<string | Filter<any>>
|
||||
| { $string: string; [index: string]: Filter<any> | string };
|
||||
type Lambda<T> = {
|
||||
$alias: string;
|
||||
$expr: Filter<T>;
|
||||
};
|
||||
|
||||
type OrderByValues = 'asc' | 'desc';
|
||||
type OrderBy = string | string[] | { [index: string]: OrderByValues };
|
||||
|
||||
type ResourceObjFilter<T> = { [k in keyof T]?: object | number | string };
|
||||
|
||||
interface FilterArray<T> extends Array<Filter<T>> {}
|
||||
|
||||
type FilterExpressions<T> = {
|
||||
$raw?: RawFilter;
|
||||
|
||||
$?: string | string[];
|
||||
|
||||
$and?: Filter<T> | FilterArray<T>;
|
||||
$or?: Filter<T> | FilterArray<T>;
|
||||
|
||||
$in?: Filter<T> | FilterArray<T>;
|
||||
|
||||
$not?: Filter<T> | FilterArray<T>;
|
||||
|
||||
$any?: Lambda<T>;
|
||||
$all?: Lambda<T>;
|
||||
};
|
||||
|
||||
type Filter<T> = ResourceObjFilter<T> & FilterExpressions<T>;
|
||||
|
||||
type BaseExpandFor<T> = { [k in keyof T]?: object } | keyof T;
|
||||
|
||||
export type Expand<T> = BaseExpandFor<T> | Array<BaseExpandFor<T>>;
|
||||
}
|
||||
|
||||
namespace ResinRequest {
|
||||
interface ResinRequestOptions {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
||||
url: string;
|
||||
apiKey?: string;
|
||||
body?: any;
|
||||
}
|
||||
|
||||
interface ResinRequestResponse extends Response {
|
||||
body: any;
|
||||
}
|
||||
|
||||
interface ResinRequest {
|
||||
send: (options: ResinRequestOptions) => Promise<ResinRequestResponse>;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ResinSdk {
|
||||
interface Interceptor {
|
||||
request?(response: any): Promise<any>;
|
||||
response?(response: any): Promise<any>;
|
||||
requestError?(error: Error): Promise<any>;
|
||||
responseError?(error: Error): Promise<any>;
|
||||
}
|
||||
|
||||
interface Config {
|
||||
deployment: string | null;
|
||||
deviceUrlsBase: string;
|
||||
adminUrl: string;
|
||||
apiUrl: string;
|
||||
actionsUrl: string;
|
||||
gitServerUrl: string;
|
||||
pubnub: {
|
||||
subscribe_key: string;
|
||||
publish_key: string;
|
||||
};
|
||||
ga?: GaConfig;
|
||||
mixpanelToken?: string;
|
||||
intercomAppId?: string;
|
||||
recurlyPublicKey?: string;
|
||||
deviceTypes: DeviceType[];
|
||||
DEVICE_ONLINE_ICON: string;
|
||||
DEVICE_OFFLINE_ICON: string;
|
||||
signupCodeRequired: boolean;
|
||||
supportedSocialProviders: string[];
|
||||
}
|
||||
|
||||
interface GaConfig {
|
||||
site: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface DeviceType {
|
||||
slug: string;
|
||||
name: string;
|
||||
|
||||
arch: string;
|
||||
state?: string;
|
||||
|
||||
isDependent?: boolean;
|
||||
instructions?: string[] | DeviceTypeInstructions;
|
||||
gettingStartedLink?: string | DeviceTypeGettingStartedLink;
|
||||
stateInstructions?: { [key: string]: string[] };
|
||||
options?: DeviceTypeOptions[];
|
||||
initialization?: {
|
||||
options?: DeviceInitializationOptions[];
|
||||
operations: Array<{
|
||||
command: string;
|
||||
}>;
|
||||
};
|
||||
supportsBlink?: boolean;
|
||||
yocto: {
|
||||
fstype?: string;
|
||||
deployArtifact: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeviceTypeInstructions {
|
||||
linux: string[];
|
||||
osx: string[];
|
||||
windows: string[];
|
||||
}
|
||||
|
||||
interface DeviceTypeGettingStartedLink {
|
||||
linux: string;
|
||||
osx: string;
|
||||
windows: string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface DeviceTypeOptions {
|
||||
options: DeviceTypeOptionsGroup[];
|
||||
collapsed: boolean;
|
||||
isCollapsible: boolean;
|
||||
isGroup: boolean;
|
||||
message: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface DeviceInitializationOptions {
|
||||
message: string;
|
||||
type: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface DeviceTypeOptionsGroup {
|
||||
default: number | string;
|
||||
message: string;
|
||||
name: string;
|
||||
type: string;
|
||||
min?: number;
|
||||
choices?: string[] | number[];
|
||||
choicesLabels?: { [key: string]: string };
|
||||
}
|
||||
|
||||
interface WithId {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface PineParams {
|
||||
resource: string;
|
||||
id?: number;
|
||||
body?: object;
|
||||
options?: PineOptions;
|
||||
}
|
||||
|
||||
interface PineOptions {
|
||||
filter?: object;
|
||||
expand?: object | string;
|
||||
orderBy?: Pine.OrderBy;
|
||||
top?: string;
|
||||
skip?: string;
|
||||
select?: string | string[];
|
||||
}
|
||||
|
||||
interface PineParamsFor<T> extends PineParams {
|
||||
body?: Partial<T>;
|
||||
options?: PineOptionsFor<T>;
|
||||
}
|
||||
|
||||
interface PineParamsWithIdFor<T> extends PineParamsFor<T> {
|
||||
id: number;
|
||||
}
|
||||
|
||||
type PineFilterFor<T> = Pine.Filter<T>;
|
||||
type PineExpandFor<T> = Pine.Expand<T>;
|
||||
|
||||
interface PineOptionsFor<T> extends PineOptions {
|
||||
filter?: PineFilterFor<T>;
|
||||
expand?: PineExpandFor<T>;
|
||||
select?: Array<keyof T> | keyof T;
|
||||
}
|
||||
|
||||
interface PineDeferred {
|
||||
__id: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* When not selected-out holds a deferred.
|
||||
* When expanded hold an array with a single element.
|
||||
*/
|
||||
type NavigationResource<T = WithId> = T[] | PineDeferred;
|
||||
|
||||
/**
|
||||
* When expanded holds an array, otherwise the property is not present.
|
||||
* Selecting is not suggested,
|
||||
* in that case it holds a deferred to the original resource.
|
||||
*/
|
||||
type ReverseNavigationResource<T = WithId> = T[] | undefined;
|
||||
|
||||
interface SocialServiceAccount {
|
||||
provider: string;
|
||||
display_name: string;
|
||||
created_at: string;
|
||||
id: number;
|
||||
remote_id: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
email?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
company?: string;
|
||||
account_type?: string;
|
||||
has_disabled_newsletter?: boolean;
|
||||
jwt_secret: string;
|
||||
created_at: string;
|
||||
twoFactorRequired?: boolean;
|
||||
hasPasswordSet?: boolean;
|
||||
needsPasswordReset?: boolean;
|
||||
public_key?: boolean;
|
||||
features?: string[];
|
||||
intercomUserName?: string;
|
||||
intercomUserHash?: string;
|
||||
permissions?: string[];
|
||||
loginAs?: boolean;
|
||||
actualUser?: number;
|
||||
|
||||
// this is what the api route returns
|
||||
social_service_account: ReverseNavigationResource<SocialServiceAccount>;
|
||||
}
|
||||
|
||||
interface Application {
|
||||
app_name: string;
|
||||
device_type: string;
|
||||
git_repository: string;
|
||||
commit: string;
|
||||
id: number;
|
||||
device_type_info?: any;
|
||||
has_dependent?: boolean;
|
||||
should_track_latest_release: boolean;
|
||||
|
||||
user: NavigationResource<User>;
|
||||
|
||||
application_tag: ReverseNavigationResource<ApplicationTag>;
|
||||
}
|
||||
|
||||
type BuildStatus =
|
||||
| 'cancelled'
|
||||
| 'error'
|
||||
| 'interrupted'
|
||||
| 'local'
|
||||
| 'running'
|
||||
| 'success'
|
||||
| 'timeout'
|
||||
| null;
|
||||
|
||||
interface Build {
|
||||
log: string;
|
||||
commit_hash: string;
|
||||
created_at: string;
|
||||
end_timestamp: string;
|
||||
id: number;
|
||||
message: string | null;
|
||||
project_type: string;
|
||||
push_timestamp: string | null;
|
||||
start_timestamp: string;
|
||||
status: BuildStatus;
|
||||
update_timestamp: string | null;
|
||||
}
|
||||
|
||||
interface BillingAccountAddressInfo {
|
||||
address1: string;
|
||||
address2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zip: string;
|
||||
country: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
interface BillingAccountInfo {
|
||||
account_state: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
company_name: string;
|
||||
cc_emails: string;
|
||||
vat_number: string;
|
||||
address: BillingAccountAddressInfo;
|
||||
}
|
||||
|
||||
type BillingInfoType = 'bank_account' | 'credit_card' | 'paypal';
|
||||
|
||||
interface BillingInfo {
|
||||
full_name: string;
|
||||
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
company: string;
|
||||
vat_number: string;
|
||||
address1: string;
|
||||
address2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zip: string;
|
||||
country: string;
|
||||
phone: string;
|
||||
|
||||
type?: BillingInfoType;
|
||||
}
|
||||
|
||||
interface CardBillingInfo extends BillingInfo {
|
||||
card_type: string;
|
||||
year: string;
|
||||
month: string;
|
||||
first_one: string;
|
||||
last_four: string;
|
||||
}
|
||||
|
||||
interface BankAccountBillingInfo extends BillingInfo {
|
||||
account_type: string;
|
||||
last_four: string;
|
||||
name_on_account: string;
|
||||
routing_number: string;
|
||||
}
|
||||
|
||||
interface TokenBillingSubmitInfo {
|
||||
token_id: string;
|
||||
}
|
||||
|
||||
interface BillingPlanInfo {
|
||||
name: string;
|
||||
billing?: BillingPlanBillingInfo;
|
||||
}
|
||||
|
||||
interface BillingPlanBillingInfo {
|
||||
currency: string;
|
||||
currencySymbol?: string;
|
||||
}
|
||||
|
||||
interface InvoiceInfo {
|
||||
closed_at: string;
|
||||
created_at: string;
|
||||
currency: string;
|
||||
invoice_number: string;
|
||||
subtotal_in_cents: string;
|
||||
total_in_cents: string;
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
interface Device {
|
||||
created_at: string;
|
||||
device_type: string;
|
||||
id: number;
|
||||
name: string;
|
||||
os_version: string;
|
||||
os_variant?: string;
|
||||
status_sort_index?: number;
|
||||
uuid: string;
|
||||
ip_address: string | null;
|
||||
vpn_address: string | null;
|
||||
last_connectivity_event: string;
|
||||
is_in_local_mode?: boolean;
|
||||
app_name?: string;
|
||||
state?: { key: string; name: string };
|
||||
status: string;
|
||||
provisioning_state: string;
|
||||
is_online: boolean;
|
||||
is_connected_to_vpn: boolean;
|
||||
supervisor_version: string;
|
||||
is_web_accessible: boolean;
|
||||
has_dependent: boolean;
|
||||
note: string;
|
||||
location: string;
|
||||
latitude?: string;
|
||||
longitude?: string;
|
||||
custom_latitude?: string;
|
||||
custom_longitude?: string;
|
||||
download_progress?: number;
|
||||
provisioning_progress?: number;
|
||||
local_id?: string;
|
||||
|
||||
device_environment_variable: ReverseNavigationResource<
|
||||
DeviceEnvironmentVariable
|
||||
>;
|
||||
device_tag: ReverseNavigationResource<DeviceTag>;
|
||||
}
|
||||
|
||||
interface LogMessage {
|
||||
message: string;
|
||||
isSystem: boolean;
|
||||
timestamp: number | null;
|
||||
serviceId: number | null;
|
||||
}
|
||||
|
||||
interface LogsSubscription extends EventEmitter {
|
||||
unsubscribe(): void;
|
||||
}
|
||||
|
||||
interface SSHKey {
|
||||
title: string;
|
||||
public_key: string;
|
||||
id: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
type ImgConfigOptions = {
|
||||
network?: 'ethernet' | 'wifi';
|
||||
appUpdatePollInterval?: number;
|
||||
wifiKey?: string;
|
||||
wifiSsid?: string;
|
||||
ip?: string;
|
||||
gateway?: string;
|
||||
netmask?: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
type OsVersions = {
|
||||
latest: string;
|
||||
recommended: string;
|
||||
default: string;
|
||||
versions: string[];
|
||||
};
|
||||
|
||||
interface EnvironmentVariableBase {
|
||||
id: number;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface EnvironmentVariable extends EnvironmentVariableBase {
|
||||
application: NavigationResource<Application>;
|
||||
}
|
||||
|
||||
interface DeviceEnvironmentVariable extends EnvironmentVariableBase {
|
||||
env_var_name?: string;
|
||||
|
||||
device: NavigationResource<Device>;
|
||||
}
|
||||
|
||||
interface ResourceTagBase {
|
||||
id: number;
|
||||
tag_key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ApplicationTag extends ResourceTagBase {
|
||||
application: NavigationResource<Application>;
|
||||
}
|
||||
|
||||
interface DeviceTag extends ResourceTagBase {
|
||||
device: NavigationResource<Device>;
|
||||
}
|
||||
|
||||
type LogsPromise = Promise<LogMessage[]>;
|
||||
|
||||
interface ResinSDK {
|
||||
auth: {
|
||||
register: (
|
||||
credentials: { email: string; password: string },
|
||||
) => Promise<string>;
|
||||
authenticate: (
|
||||
credentials: { email: string; password: string },
|
||||
) => Promise<string>;
|
||||
login: (
|
||||
credentials: { email: string; password: string },
|
||||
) => Promise<void>;
|
||||
loginWithToken: (authToken: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
getToken: () => Promise<string>;
|
||||
whoami: () => Promise<string | undefined>;
|
||||
isLoggedIn: () => Promise<boolean>;
|
||||
getUserId: () => Promise<number>;
|
||||
getEmail: () => Promise<string>;
|
||||
twoFactor: {
|
||||
isEnabled: () => Promise<boolean>;
|
||||
isPassed: () => Promise<boolean>;
|
||||
challenge: (code: string) => Promise<void>;
|
||||
};
|
||||
};
|
||||
|
||||
settings: {
|
||||
get(key: string): Promise<string>;
|
||||
getAll(): Promise<{ [key: string]: string }>;
|
||||
};
|
||||
|
||||
request: ResinRequest.ResinRequest;
|
||||
|
||||
errors: {
|
||||
ResinAmbiguousApplication: ResinErrors.ResinAmbiguousApplication;
|
||||
ResinAmbiguousDevice: ResinErrors.ResinAmbiguousDevice;
|
||||
ResinApplicationNotFound: ResinErrors.ResinApplicationNotFound;
|
||||
ResinBuildNotFound: ResinErrors.ResinBuildNotFound;
|
||||
ResinDeviceNotFound: ResinErrors.ResinDeviceNotFound;
|
||||
ResinExpiredToken: ResinErrors.ResinExpiredToken;
|
||||
ResinInvalidDeviceType: ResinErrors.ResinInvalidDeviceType;
|
||||
ResinInvalidParameterError: ResinErrors.ResinInvalidParameterError;
|
||||
ResinKeyNotFound: ResinErrors.ResinKeyNotFound;
|
||||
ResinMalformedToken: ResinErrors.ResinMalformedToken;
|
||||
ResinNotLoggedIn: ResinErrors.ResinNotLoggedIn;
|
||||
ResinRequestError: ResinErrors.ResinRequestError;
|
||||
ResinSupervisorLockedError: ResinErrors.ResinSupervisorLockedError;
|
||||
};
|
||||
|
||||
models: {
|
||||
application: {
|
||||
create(
|
||||
name: string,
|
||||
deviceType: string,
|
||||
parentNameOrId?: number | string,
|
||||
): Promise<Application>;
|
||||
get(
|
||||
nameOrId: string | number,
|
||||
options?: PineOptionsFor<Application>,
|
||||
): Promise<Application>;
|
||||
getAppByOwner(
|
||||
appName: string,
|
||||
owner: string,
|
||||
options?: PineOptionsFor<Application>,
|
||||
): Promise<Application>;
|
||||
getAll(options?: PineOptionsFor<Application>): Promise<Application[]>;
|
||||
has(name: string): Promise<boolean>;
|
||||
hasAny(): Promise<boolean>;
|
||||
remove(nameOrId: string | number): Promise<void>;
|
||||
restart(nameOrId: string | number): Promise<void>;
|
||||
enableDeviceUrls(nameOrId: string | number): Promise<void>;
|
||||
disableDeviceUrls(nameOrId: string | number): Promise<void>;
|
||||
grantSupportAccess(
|
||||
nameOrId: string | number,
|
||||
expiryTimestamp: number,
|
||||
): Promise<void>;
|
||||
revokeSupportAccess(nameOrId: string | number): Promise<void>;
|
||||
reboot(appId: number, { force }: { force?: boolean }): Promise<void>;
|
||||
shutdown(
|
||||
appId: number,
|
||||
{ force }: { force?: boolean },
|
||||
): Promise<void>;
|
||||
purge(appId: number): Promise<void>;
|
||||
generateApiKey(nameOrId: string | number): Promise<string>;
|
||||
generateProvisioningKey(nameOrId: string | number): Promise<string>;
|
||||
tags: {
|
||||
getAllByApplication(
|
||||
nameOrId: string | number,
|
||||
options?: PineOptionsFor<ApplicationTag>,
|
||||
): Promise<ApplicationTag[]>;
|
||||
getAll(
|
||||
options?: PineOptionsFor<ApplicationTag>,
|
||||
): Promise<ApplicationTag[]>;
|
||||
set(
|
||||
nameOrId: string | number,
|
||||
tagKey: string,
|
||||
value: string,
|
||||
): Promise<void>;
|
||||
remove(nameOrId: string | number, tagKey: string): Promise<void>;
|
||||
};
|
||||
};
|
||||
build: {
|
||||
get(id: number, options?: PineOptionsFor<Build>): Promise<Build>;
|
||||
getAllByApplication(
|
||||
nameOrId: string | number,
|
||||
options?: PineOptionsFor<Build>,
|
||||
): Promise<Build[]>;
|
||||
};
|
||||
billing: {
|
||||
getAccount(): Promise<BillingAccountInfo>;
|
||||
getPlan(): Promise<BillingPlanInfo>;
|
||||
getBillingInfo(): Promise<BillingInfo>;
|
||||
updateBillingInfo(
|
||||
billingInfo: TokenBillingSubmitInfo,
|
||||
): Promise<BillingInfo>;
|
||||
getInvoices(): Promise<InvoiceInfo[]>;
|
||||
downloadInvoice(invoiceNumber: string): Promise<Blob>;
|
||||
};
|
||||
device: {
|
||||
get(
|
||||
uuidOrId: string | number,
|
||||
options?: PineOptionsFor<Device>,
|
||||
): Promise<Device>;
|
||||
getByName(
|
||||
nameOrId: string | number,
|
||||
options?: PineOptionsFor<Device>,
|
||||
): Promise<Device[]>;
|
||||
getAll(options?: PineOptionsFor<Device>): Promise<Device[]>;
|
||||
getAllByApplication(
|
||||
nameOrId: string | number,
|
||||
options?: PineOptionsFor<Device>,
|
||||
): Promise<Device[]>;
|
||||
getAllByParentDevice(
|
||||
parentUuidOrId: string | number,
|
||||
options?: PineOptionsFor<Device>,
|
||||
): Promise<Device[]>;
|
||||
getName(uuidOrId: string | number): Promise<string>;
|
||||
getApplicationName(uuidOrId: string | number): Promise<string>;
|
||||
getApplicationInfo(
|
||||
uuidOrId: string | number,
|
||||
): Promise<{
|
||||
appId: string;
|
||||
commit: string;
|
||||
containerId: string;
|
||||
env: { [key: string]: string | number };
|
||||
imageId: string;
|
||||
}>;
|
||||
has(uuidOrId: string | number): Promise<boolean>;
|
||||
isOnline(uuidOrId: string | number): Promise<boolean>;
|
||||
getLocalIPAddressess(uuidOrId: string | number): Promise<string[]>;
|
||||
getDashboardUrl(uuid: string): string;
|
||||
getSupportedDeviceTypes(): Promise<string[]>;
|
||||
getManifestBySlug(slugOrName: string): Promise<DeviceType>;
|
||||
getManifestByApplication(
|
||||
nameOrId: string | number,
|
||||
): Promise<DeviceType>;
|
||||
move(
|
||||
uuidOrId: string | number,
|
||||
applicationNameOrId: string | number,
|
||||
): Promise<void>;
|
||||
note(uuidOrId: string | number, note: string): Promise<void>;
|
||||
remove(uuidOrId: string | number): Promise<void>;
|
||||
rename(uuidOrId: string | number, newName: string): Promise<void>;
|
||||
setCustomLocation(
|
||||
uuidOrId: string | number,
|
||||
location: { latitude: number; longitude: number },
|
||||
): Promise<void>;
|
||||
unsetCustomLocation(uuidOrId: string | number): Promise<void>;
|
||||
identify(uuidOrId: string | number): Promise<void>;
|
||||
startApplication(uuidOrId: string | number): Promise<void>;
|
||||
stopApplication(uuidOrId: string | number): Promise<void>;
|
||||
restartApplication(uuidOrId: string | number): Promise<void>;
|
||||
grantSupportAccess(
|
||||
uuidOrId: string | number,
|
||||
expiryTimestamp: number,
|
||||
): Promise<void>;
|
||||
revokeSupportAccess(uuidOrId: string | number): Promise<void>;
|
||||
reboot(
|
||||
uuidOrId: string | number,
|
||||
{ force }?: { force?: boolean },
|
||||
): Promise<void>;
|
||||
shutdown(
|
||||
uuidOrId: string | number,
|
||||
{ force }?: { force?: boolean },
|
||||
): Promise<void>;
|
||||
purge(uuidOrId: string | number): Promise<void>;
|
||||
update(
|
||||
uuidOrId: string | number,
|
||||
{ force }?: { force?: boolean },
|
||||
): Promise<void>;
|
||||
getDisplayName(deviceTypeName: string): string;
|
||||
getDeviceSlug(deviceTypeName: string): string;
|
||||
generateUniqueKey(): string;
|
||||
register(
|
||||
applicationNameOrId: string | number,
|
||||
uuid?: string,
|
||||
): Promise<object>;
|
||||
generateDeviceKey(uuidOrId: string | number): Promise<string>;
|
||||
enableDeviceUrl(uuidOrId: string | number): Promise<void>;
|
||||
disableDeviceUrl(uuidOrId: string | number): Promise<void>;
|
||||
hasDeviceUrl(uuidOrId: string | number): Promise<boolean>;
|
||||
getDeviceUrl(uuidOrId: string | number): Promise<string>;
|
||||
enableTcpPing(uuidOrId: string | number): Promise<void>;
|
||||
disableTcpPing(uuidOrId: string | number): Promise<void>;
|
||||
ping(uuidOrId: string | number): Promise<void>;
|
||||
getStatus(device: object): string;
|
||||
lastOnline(device: Device): string;
|
||||
tags: {
|
||||
getAllByApplication(
|
||||
nameOrId: string | number,
|
||||
options?: PineOptionsFor<DeviceTag>,
|
||||
): Promise<DeviceTag[]>;
|
||||
getAllByDevice(
|
||||
uuidOrId: string | number,
|
||||
options?: PineOptionsFor<DeviceTag>,
|
||||
): Promise<DeviceTag[]>;
|
||||
getAll(options?: PineOptionsFor<DeviceTag>): Promise<DeviceTag[]>;
|
||||
set(
|
||||
uuidOrId: string | number,
|
||||
tagKey: string,
|
||||
value: string,
|
||||
): Promise<void>;
|
||||
remove(uuidOrId: string | number, tagKey: string): Promise<void>;
|
||||
};
|
||||
};
|
||||
environmentVariables: {
|
||||
device: {
|
||||
getAll(id: number): Promise<DeviceEnvironmentVariable[]>;
|
||||
getAllByApplication(
|
||||
applicationNameOrId: number | string,
|
||||
): Promise<DeviceEnvironmentVariable[]>;
|
||||
update(id: number, value: string): Promise<void>;
|
||||
create(
|
||||
uuidOrId: number | string,
|
||||
name: string,
|
||||
value: string,
|
||||
): Promise<void>;
|
||||
remove(id: number): Promise<void>;
|
||||
};
|
||||
getAllByApplication(
|
||||
applicationNameOrId: number | string,
|
||||
): Promise<EnvironmentVariable[]>;
|
||||
update(id: number, value: string): Promise<void>;
|
||||
create(
|
||||
applicationNameOrId: number | string,
|
||||
name: string,
|
||||
value: string,
|
||||
): Promise<void>;
|
||||
remove(id: number): Promise<void>;
|
||||
isSystemVariable(variable: { name: string }): boolean;
|
||||
};
|
||||
config: {
|
||||
getAll: () => Promise<Config>;
|
||||
getDeviceTypes: () => Promise<DeviceType[]>;
|
||||
getDeviceOptions(
|
||||
deviceType: string,
|
||||
): Promise<Array<DeviceTypeOptions | DeviceInitializationOptions>>;
|
||||
};
|
||||
key: {
|
||||
getAll(options?: PineOptionsFor<SSHKey>): Promise<SSHKey[]>;
|
||||
get(id: string | number): Promise<SSHKey>;
|
||||
remove(id: string | number): Promise<void>;
|
||||
create(title: string, key: string): Promise<SSHKey>;
|
||||
};
|
||||
os: {
|
||||
getConfig(
|
||||
nameOrId: string | number,
|
||||
options?: ImgConfigOptions,
|
||||
): Promise<object>;
|
||||
getDownloadSize(slug: string, version?: string): Promise<number>;
|
||||
getSupportedVersions(slug: string): Promise<OsVersions>;
|
||||
getMaxSatisfyingVersion(
|
||||
deviceType: string,
|
||||
versionOrRange: string,
|
||||
): string;
|
||||
getLastModified(deviceType: string, version?: string): Promise<Date>;
|
||||
download(deviceType: string, version?: string): Promise<Readable>;
|
||||
};
|
||||
};
|
||||
|
||||
logs: {
|
||||
history(uuid: string): LogsPromise;
|
||||
historySinceLastClear(uuid: string): LogsPromise;
|
||||
subscribe(uuid: string): Promise<LogsSubscription>;
|
||||
clear(uuid: string): void;
|
||||
};
|
||||
|
||||
pine: {
|
||||
delete<T>(
|
||||
params: PineParamsWithIdFor<T> | PineParamsFor<T>,
|
||||
): Promise<string>;
|
||||
get<T>(params: PineParamsWithIdFor<T>): Promise<T>;
|
||||
get<T>(params: PineParamsFor<T>): Promise<T[]>;
|
||||
get<T, Result>(params: PineParamsFor<T>): Promise<Result>;
|
||||
post<T>(params: PineParams): Promise<T>;
|
||||
patch<T>(params: PineParamsWithIdFor<T>): Promise<T>;
|
||||
};
|
||||
interceptors: Interceptor[];
|
||||
}
|
||||
}
|
||||
|
||||
interface SdkOptions {
|
||||
apiUrl?: string;
|
||||
/**
|
||||
* @deprecated Use resin.auth.loginWithToken(apiKey) instead
|
||||
*/
|
||||
apiKey?: string;
|
||||
imageMakerUrl?: string;
|
||||
dataDirectory?: string;
|
||||
isBrowser?: boolean;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
interface SdkConstructor {
|
||||
(options?: SdkOptions): ResinSdk.ResinSDK;
|
||||
|
||||
setSharedOptions(options: SdkOptions): void;
|
||||
fromSharedOptions: () => ResinSdk.ResinSDK;
|
||||
}
|
||||
|
||||
const ResinSdk: ResinSdk.ResinSDK;
|
||||
|
||||
export = ResinSdk;
|
||||
}
|
||||
|
Reference in New Issue
Block a user