mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
542 Commits
code-cover
...
v12.0.0
Author | SHA1 | Date | |
---|---|---|---|
a3cab32b4e | |||
d709e06f48 | |||
98f101643d | |||
c619bd4b99 | |||
19c3069b22 | |||
7e1d58546c | |||
2c01f8adee | |||
3ecf461d55 | |||
06ab84fd10 | |||
a7b78d2ccd | |||
432109060e | |||
b32ae4a667 | |||
36e4b3249c | |||
41e5fdbe27 | |||
6dc0fe10bc | |||
656591fde4 | |||
d967b942e0 | |||
7e34fdfeeb | |||
995f8a3338 | |||
ff282205d5 | |||
be144fafa2 | |||
683037cd2f | |||
555096db6b | |||
a85c482416 | |||
523d563b4e | |||
1569915fae | |||
b1552f8e9b | |||
f455602c73 | |||
3e97669b3c | |||
728c4f4296 | |||
bf073942f0 | |||
b9290f4859 | |||
626d328194 | |||
21dd959344 | |||
5c8d822aee | |||
2ab8ae1c10 | |||
fcc13f9476 | |||
a38b41f339 | |||
2fc0728a09 | |||
040c4987fc | |||
254d9c49a4 | |||
6e5e1c4f5f | |||
d7213e868f | |||
d82b019480 | |||
1693bd91c0 | |||
e4f605d6ac | |||
fd7e7f57eb | |||
1d073af31a | |||
fcaaec1fff | |||
ac3a688d46 | |||
979284b071 | |||
bc4aa6006e | |||
2cad44915b | |||
889c7b08cf | |||
56a196210d | |||
a23759a1ba | |||
ba0024645d | |||
3cb184c8af | |||
644d54a113 | |||
a6f905b71c | |||
3b426e4a53 | |||
d241523d93 | |||
1c354c800b | |||
e5861a708e | |||
6a019af25f | |||
8522363cd3 | |||
480228d8f4 | |||
175413af34 | |||
d1b4560b37 | |||
77f3fa4b6c | |||
a21d3fe2d2 | |||
df440f0580 | |||
92bfa574e3 | |||
d33b7ec585 | |||
08d5a77734 | |||
744122b1b8 | |||
c3a8bb3de6 | |||
e50d92727e | |||
3bb5e495a6 | |||
803a9070fd | |||
a84ab793a0 | |||
81269e92d5 | |||
11d5deef4c | |||
c98bc3280d | |||
8c2a40cb39 | |||
2fdd023a64 | |||
aff370e9c3 | |||
be21c8d43e | |||
5b33826309 | |||
052c8d138e | |||
d0228f20fd | |||
4577d72ead | |||
0dde84ec0b | |||
81b620f55e | |||
4b056b4d4c | |||
5723e69267 | |||
2d341cac48 | |||
4e50d08f7b | |||
aff5cd9b0d | |||
2bb0933a42 | |||
8d60cd1f92 | |||
9756efb539 | |||
61ed6ff69d | |||
127560fa65 | |||
2611ea22f9 | |||
d7021a556e | |||
9412a21d40 | |||
f63be0b4cd | |||
c2561938c1 | |||
98a2c0635d | |||
ee54d638ad | |||
d9b044c1b8 | |||
dd20a8b00f | |||
01147c31a4 | |||
2f6889cca1 | |||
83286e6729 | |||
97def08ec5 | |||
1a57385626 | |||
1301f62981 | |||
6ae337db8a | |||
b84cdd6230 | |||
2f24e591ef | |||
597b894917 | |||
3b53b75626 | |||
9b1c3c665b | |||
153cdf4bb0 | |||
1d9a397f71 | |||
6167c7b8b3 | |||
cbcd7694a9 | |||
52bece7f17 | |||
4f6550e7eb | |||
ec17ed6ef2 | |||
0df6368ab9 | |||
b5cac122cf | |||
ae75e1396e | |||
3b519f0258 | |||
275fa9c16b | |||
a00db0f5d8 | |||
2a8eb3a6ed | |||
bcd49e0292 | |||
d8e1cd6597 | |||
52c2b041da | |||
d5d0486c3f | |||
6f51807e8c | |||
ab526c9ed8 | |||
14c5b27cdd | |||
ce01ce73b1 | |||
0f6d160b2e | |||
6d7d1956ea | |||
692eddf43f | |||
eb5cfecfaf | |||
73d6d7b264 | |||
14ced9f384 | |||
4d8cd1cc46 | |||
dbe9a727d5 | |||
8ac65c3800 | |||
4ae91ef846 | |||
7311cfa755 | |||
a200bf268d | |||
d398e22c58 | |||
b51d2fffbb | |||
5fef98bdf8 | |||
203ccaf97b | |||
a348528ed3 | |||
04c4250fba | |||
a97398950e | |||
f55376df32 | |||
3f285cc26d | |||
6d95c5bad5 | |||
6b33f95661 | |||
9ab34c2deb | |||
db247307db | |||
ad0b667bc7 | |||
d98bc9fb06 | |||
74cdd80b51 | |||
5c39952002 | |||
2874a69d7d | |||
6ec05e8dcf | |||
6602845202 | |||
e8cd4153c7 | |||
0cfa1a0dfb | |||
00ce3ab751 | |||
5c1323d583 | |||
d9f42b888d | |||
0db8c85fc8 | |||
bc601d07e3 | |||
e1a91035ae | |||
8dced8afe2 | |||
ffded6736a | |||
1a851f552e | |||
68b64016ab | |||
edac54ccfe | |||
560b0abbe7 | |||
b48d238be6 | |||
a10d5b9abe | |||
23f2242e22 | |||
f8612fd748 | |||
36446ff488 | |||
a5ce0436c7 | |||
3302e2f639 | |||
c3c1c5fc41 | |||
9f59b6dde5 | |||
8be56ef092 | |||
e9f8cadb73 | |||
3e4f9f9572 | |||
f7d4a37060 | |||
d0e268815a | |||
c3454d3abb | |||
6b0f645094 | |||
ada7801a0d | |||
da5e26f37e | |||
9447195c26 | |||
cd59496f11 | |||
9fda165d34 | |||
fe0ad92b43 | |||
81c5a62380 | |||
ebdd04ec73 | |||
e6264ced7a | |||
247f31a3cc | |||
a2b761ec4b | |||
e3672bc655 | |||
028141c0b0 | |||
e3c42cf63e | |||
0ae138db03 | |||
9350af9ddf | |||
88e4009e88 | |||
cb7692690d | |||
82e17cea6a | |||
9ed363da9e | |||
8aa4bd6173 | |||
5f098e7410 | |||
2de33d185a | |||
66b9f5a337 | |||
bbcb3a702f | |||
57d0014e32 | |||
8d9133e6a6 | |||
be82bcfa63 | |||
7c9a23451b | |||
1319e0642b | |||
e3b6db25d8 | |||
655534469a | |||
a8b0573699 | |||
99963cbb89 | |||
92715c3182 | |||
264c8535b4 | |||
159ee44d7e | |||
7e4b62c28a | |||
52b2ba6a30 | |||
cc1ba3d84e | |||
01d05fb148 | |||
cff9e50a22 | |||
eba2e7e4fb | |||
9a9d56b419 | |||
320b4864d9 | |||
7f79451376 | |||
68fa831843 | |||
3aa72dde4c | |||
f72d78954d | |||
cf87ca95a0 | |||
a50ca78eef | |||
4fe5a10029 | |||
9812239862 | |||
bc3fe29624 | |||
7e2ee7ab93 | |||
f151a208e5 | |||
292ad89b7e | |||
c062e6e876 | |||
dcb1c11700 | |||
96e28f3d45 | |||
ff319d67f3 | |||
f14e44a2e8 | |||
9aa6b0bc57 | |||
cbe12d5be7 | |||
c177f222ba | |||
d2fd1ec80a | |||
77873cf919 | |||
07c09c5f89 | |||
159cb752d1 | |||
a74f0413df | |||
43b1c5c24f | |||
45e7e9cb32 | |||
1a71bad8bb | |||
2d55df4704 | |||
6c0b3a5e53 | |||
3e955f3a91 | |||
30738d93b0 | |||
be76b8adbd | |||
d6a065a230 | |||
7b8e86372b | |||
bc15ad6e05 | |||
fcad35402a | |||
49b00e18ae | |||
a6ccd87069 | |||
0c1904fbdb | |||
e5d2661c96 | |||
eca3e91512 | |||
eb5ad08649 | |||
b3b22d6399 | |||
217cba819a | |||
c8275b52c3 | |||
47e85da789 | |||
c8cade95da | |||
a4de7143b1 | |||
6574745a23 | |||
1ee74df67e | |||
3e1b10007a | |||
448211e49c | |||
8658104647 | |||
6ec8bcddaa | |||
d138c40ebd | |||
f24c4a036c | |||
dabe81c31b | |||
c2f0f9a894 | |||
46b695cf22 | |||
9b79f79bac | |||
dddfad9dec | |||
0690554a94 | |||
62ea7518bc | |||
f30e486562 | |||
809a5fae25 | |||
eccb1bd9ad | |||
f859d5025a | |||
18d3ca3413 | |||
a826f16469 | |||
505c3ec7d3 | |||
47fa2a6151 | |||
b4b19637f4 | |||
5f552cf9a8 | |||
e42650f433 | |||
731bd909d6 | |||
2860535c45 | |||
122b5a0655 | |||
ec66c82d3f | |||
09a59ab03f | |||
3d2e109e7f | |||
26803067f1 | |||
7dc3977e82 | |||
10cbf514a2 | |||
e2114f73d7 | |||
2f448951c9 | |||
385d3e107b | |||
d98b2fa72f | |||
c6baa7a908 | |||
daa34feeda | |||
f813dad4d9 | |||
d7633b5f08 | |||
f44c2b777f | |||
bcfba693a5 | |||
08f40c0566 | |||
5a80654305 | |||
d2df2c7b60 | |||
36d3d1256e | |||
b77cb56cd0 | |||
524397fc9b | |||
ec73ee270b | |||
b83431c2e0 | |||
40c559322a | |||
2c5cf9dab6 | |||
ca8272b477 | |||
d6e7359400 | |||
af8d7283a5 | |||
9470e804c0 | |||
00943463a4 | |||
3f6d770233 | |||
c4a6086e9c | |||
4e61c00255 | |||
1713988e94 | |||
fe4e1d09d7 | |||
766695ceef | |||
e50a3270ba | |||
235c13bea9 | |||
62e4930e5b | |||
fb321b8c5b | |||
98152c0b09 | |||
0ab0e417b8 | |||
3642943896 | |||
7c62e34455 | |||
86af954f3b | |||
0c7947e185 | |||
48b281d7c6 | |||
8598223b61 | |||
cdd67e25f0 | |||
eac6bb5e5c | |||
077d1db9b7 | |||
d86f213b68 | |||
cdfd1d124b | |||
28c00696b8 | |||
dec570a6e2 | |||
9067558d18 | |||
4abdd71ce7 | |||
36f2f491b3 | |||
5e750b33c3 | |||
03053e125f | |||
bdc7c0fa39 | |||
ad4981328f | |||
3f35d6fde6 | |||
f2be811e18 | |||
6439aa5552 | |||
977fadab69 | |||
95c93d24da | |||
278d7fd02c | |||
59b9429570 | |||
9e870b08a7 | |||
671dca8287 | |||
a15060e9fc | |||
0738dd1520 | |||
5dbace353d | |||
e773549297 | |||
a1c406a479 | |||
5e196b8f63 | |||
054e59c6af | |||
88a1e413a3 | |||
1a74dcf4cf | |||
d48672fa93 | |||
9a7fcfffe8 | |||
f9ece2ce7d | |||
9d04e616a8 | |||
b8c7f23443 | |||
2b04763ac0 | |||
bff845a0e4 | |||
5076ca7532 | |||
93ba5832d8 | |||
af86ac73e6 | |||
173a48eede | |||
a4b34c109d | |||
69714a646b | |||
a41ef3764e | |||
f1220c6377 | |||
cefb3acc1f | |||
277da3ea9c | |||
99f84c2f6a | |||
a9c0899c32 | |||
8d3fb8fef5 | |||
4de41ce3e0 | |||
4b8cec652a | |||
2dd8e71adc | |||
05d478b759 | |||
9a7a364776 | |||
2cb5e28258 | |||
02e8429155 | |||
467afb3de6 | |||
324a406e7f | |||
17bf061853 | |||
6d543b79ff | |||
85aaf77e44 | |||
83c5684491 | |||
6bc4fbb750 | |||
1da96a0eb0 | |||
be209f1626 | |||
654d1dcff8 | |||
0a03e79d9d | |||
3f84045127 | |||
544f8fb4bd | |||
76997c99dc | |||
f4525bc11e | |||
f732c5bf5d | |||
2bc3348aff | |||
895be0be5d | |||
0f17129c2e | |||
9005affe64 | |||
4502f2a203 | |||
da3c11533c | |||
6acff945ef | |||
b3948d538c | |||
f53a69feb1 | |||
405b92114d | |||
27e1f3f7d7 | |||
1417875110 | |||
f58a49d6c3 | |||
f9743b269a | |||
0f5f65e0d3 | |||
58e7880f1d | |||
041823189f | |||
38194e6175 | |||
c04e9665ad | |||
1e37c97ffb | |||
913f09924a | |||
ceb47e9969 | |||
305755549e | |||
77931b314a | |||
b38b5b0b61 | |||
5cf407b483 | |||
8f6902f4cb | |||
751f67e997 | |||
be1a260af6 | |||
9db6961a7e | |||
b978230f9e | |||
cc5fe60a15 | |||
bbea58a9c8 | |||
56e35f6e9f | |||
95b5ac1c7f | |||
df3e1f1886 | |||
5d34659991 | |||
aca794b267 | |||
cd6072ac73 | |||
bda696ad8c | |||
ef4ee54a00 | |||
a2ca8e8f73 | |||
620a0abf31 | |||
95561864a6 | |||
51adfeaa3b | |||
76447a2177 | |||
a6153869e5 | |||
3466be1992 | |||
95843dd816 | |||
edd755d41c | |||
290c06074a | |||
dd7d9d1570 | |||
c4829153fc | |||
615f24edd3 | |||
a94e6d550e | |||
75044030cf | |||
046743071d | |||
4e95cb0cca | |||
4666019c84 | |||
323c9191b6 | |||
024bf2996b | |||
5210d474a9 | |||
3cce8d822c | |||
65250e431e | |||
29cc75598f | |||
33552724a1 | |||
c88b317143 | |||
658b0a5233 | |||
7fd436cd91 | |||
7c1faa6de0 | |||
90e184ea1f | |||
38920a1c59 | |||
df58ac7673 | |||
630d53311a | |||
b1eda160e8 | |||
a63c766f04 | |||
53325b7c05 | |||
622c510d65 | |||
890bea549f | |||
bb19903826 | |||
33210b896b | |||
c2a0e457c0 | |||
f464597069 | |||
02dcff5b67 | |||
2f4539b4d1 | |||
6c3429eb0c |
15
.gitattributes
vendored
Normal file
15
.gitattributes
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# Set all files to use line feed endings (since we can't match only ones without an extension)
|
||||
* eol=lf
|
||||
# And then reset all the files with extensions back to default
|
||||
*.* -eol
|
||||
|
||||
*.sh text eol=lf
|
||||
|
||||
# lf for the docs as it's auto-generated and will otherwise trigger an uncommited error on windows
|
||||
doc/cli.markdown text eol=lf
|
||||
# crlf for the eol conversion test files
|
||||
tests/test-data/projects/docker-compose/basic/service2/file2-crlf.sh eol=crlf
|
||||
tests/test-data/projects/no-docker-compose/basic/src/windows-crlf.sh eol=crlf
|
||||
|
||||
# Prevent auto merging of the npm-shrinkwrap.json file: see notes in CONTRIBUTING.md
|
||||
/npm-shrinkwrap.json merge=binary
|
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -1 +1 @@
|
||||
* @CameronDiver @hedss @pdcastro @srlowe @thgreasi
|
||||
* @CameronDiver @pdcastro @srlowe @thgreasi
|
||||
|
83
.github/ISSUE_TEMPLATE.md
vendored
83
.github/ISSUE_TEMPLATE.md
vendored
@ -1,16 +1,75 @@
|
||||
- **balena CLI version:** e.g. 11.2.1 (output of the `"balena version"` command)
|
||||
- **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5
|
||||
- **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor
|
||||
- **Install method:** npm or zip or executable installer
|
||||
- **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1
|
||||
|
||||
# About this issue tracker
|
||||
|
||||
*The balena CLI (Command Line Interface) is a tool used to interact with the balena platform.
|
||||
This GitHub issue tracker is used for bug reports and feature requests regarding the CLI
|
||||
tool. General and troubleshooting questions (such as setting up your project to work with a
|
||||
balenalib base image) are encouraged to be posted to the [balena
|
||||
forums](https://forums.balena.io), which are monitored by balena's support team and where the
|
||||
community can both contribute and benefit from the answers.*
|
||||
|
||||
*Please also check that this issue is not a duplicate. If there is another issue describing
|
||||
the same problem or feature please add comments to the existing issue.*
|
||||
|
||||
*Thank you for your time and effort creating the issue report, and helping us improve the
|
||||
balena CLI!*
|
||||
|
||||
---
|
||||
|
||||
*Please keep in mind that we try to use the issue tracker of this repository for specific bug
|
||||
reports & CLI feature requests. General & troubleshooting questions are encouraged to be posted to
|
||||
the [balena forums](https://forums.balena.io), which are monitored by balena's support team and
|
||||
where the community can both contribute and benefit from the answers.*
|
||||
# Expected Behavior
|
||||
|
||||
*Before submitting this issue please check that this issue is not a duplicate. If there is another
|
||||
issue describing the same problem or feature please add your information to the existing issue's
|
||||
comments.*
|
||||
Please describe what you were expecting to happen. If applicable, please add links to
|
||||
documentation you were following, or to projects that you were trying to push/build.
|
||||
|
||||
# Actual Behavior
|
||||
|
||||
Please describe what actually happened instead:
|
||||
* Quoting logs and error message is useful. If possible, quote the **full** output of the
|
||||
CLI, not just the error message.
|
||||
* Please quote the **full command line** too. Sometimes users report that they were
|
||||
"pushing" or "building" a project, but there are several ways to do so and several
|
||||
possible "targets" such as balenaCloud, openBalena, local balenaOS device, etc.
|
||||
Examples:
|
||||
|
||||
```
|
||||
balena push myApp
|
||||
balena push 192.168.0.12
|
||||
balena deploy myApp
|
||||
balena deploy myApp --build
|
||||
balena build . -a myApp
|
||||
balena build . -A armv7hf -d raspberrypi3
|
||||
```
|
||||
|
||||
Each of the above command lines executes different code behind the scenes, so quoting the
|
||||
full command line is very helpful.
|
||||
|
||||
Running the CLI in debug mode (`--debug` flag or `DEBUG=1` environment variable) may reveal
|
||||
additional information. The `--logs` option reveals additional information for the commands:
|
||||
|
||||
```
|
||||
balena build . --logs
|
||||
balena deploy myApp --build --logs
|
||||
```
|
||||
|
||||
# Steps to Reproduce the Problem
|
||||
|
||||
This is the most important and helpful part of a bug report. If we cannot reproduce the
|
||||
problem, it is difficult to tell what the fix should be, or whether code changes have
|
||||
fixed it.
|
||||
|
||||
1.
|
||||
1.
|
||||
1.
|
||||
|
||||
# Specifications
|
||||
|
||||
- **balena CLI version:** e.g. 1.2.3 (output of the `"balena version -a"` command)
|
||||
- **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5
|
||||
- **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor
|
||||
- **Install method:** npm or zip package or executable installer
|
||||
- **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1
|
||||
|
||||
# Additional References
|
||||
|
||||
If applicable, please add additional links to GitHub projects, forums.balena.io threads,
|
||||
gist.github.com, Google Drive attachments, etc.
|
||||
|
31
.github/PULL_REQUEST_TEMPLATE.md
vendored
31
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,11 +1,26 @@
|
||||
<!-- You can remove tags that do not apply. -->
|
||||
Resolves: # <!-- Refer an issue of this repository that this PR fixes -->
|
||||
See: <url> <!-- Refer to any external resource, like a PR, document or discussion -->
|
||||
Depends-on: <url> <!-- This change depends on a PR to get merged/deployed first -->
|
||||
Change-type: major|minor|patch <!-- The change type of this PR -->
|
||||
Resolves: # <!-- Refer an issue of this repository that this PR fixes -->
|
||||
Change-type: major|minor|patch <!-- See https://semver.org/ -->
|
||||
Depends-on: <url> <!-- This change depends on a PR to get merged/deployed first -->
|
||||
See: <url> <!-- Refer to any external resource, like a PR, document or discussion -->
|
||||
|
||||
---
|
||||
##### Contributor checklist
|
||||
<!-- For completed items, change [ ] to [x]. -->
|
||||
- [ ] Introduces security considerations
|
||||
- [ ] Affects the development, build or deployment processes of the component
|
||||
Please check the CONTRIBUTING.md file for relevant information and some
|
||||
guidance. Keep in mind that the CLI is a cross-platform application that runs
|
||||
on Windows, macOS and Linux. Tests will be automatically run by balena CI on
|
||||
all three operating systems, but this will only help if you have added test
|
||||
code that exercises the modified or added feature code.
|
||||
|
||||
Note that each commit message (currently only the first line) will be
|
||||
automatically copied to the CHANGELOG.md file, so try writing it in a way
|
||||
that describes the feature or fix for CLI users.
|
||||
|
||||
If there isn't a linked issue or if the linked issue doesn't quite match the
|
||||
PR, please add a PR description to explain its purpose or the features that it
|
||||
implements. Adding PR comments to blocks of code that aren't self explanatory
|
||||
usually helps with the review process.
|
||||
|
||||
If the PR introduces security considerations or affects the development, build
|
||||
or release process, please be sure to highlight this in the PR description.
|
||||
|
||||
Thank you very much for your contribution!
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,7 @@ lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
@ -1,5 +1,2 @@
|
||||
coffee_script:
|
||||
config_file: coffeelint.json
|
||||
|
||||
javascript:
|
||||
enabled: false
|
||||
|
@ -5,31 +5,26 @@ npm:
|
||||
os: alpine
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "8"
|
||||
- "10"
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86
|
||||
node_versions:
|
||||
- "8"
|
||||
- "10"
|
||||
- name: darwin
|
||||
os: macos
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "8"
|
||||
- "10"
|
||||
- name: windows
|
||||
os: windows
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "8"
|
||||
- "10"
|
||||
- name: windows
|
||||
os: windows
|
||||
architecture: x86
|
||||
node_versions:
|
||||
- "8"
|
||||
- "10"
|
||||
|
||||
docker:
|
||||
|
File diff suppressed because it is too large
Load Diff
848
CHANGELOG.md
848
CHANGELOG.md
@ -4,6 +4,854 @@ All notable changes to this project will be documented in this file
|
||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 12.0.0 - 2020-06-16
|
||||
|
||||
* v12 RELEASE NOTES: see https://git.io/Jf7hz [Paulo Castro]
|
||||
* Update 'balena-lint' and apply new prettier rules [Paulo Castro]
|
||||
* Convert 'logs' command to async/await and add tests [Paulo Castro]
|
||||
* Add tests for standalone executable via proxy server [Paulo Castro]
|
||||
* Update 'global-agent' (fix proxy server issues with unauthenticated setup) [Paulo Castro]
|
||||
* Update 'balena-sdk' from v12 to v13 and update code and tests as needed [Paulo Castro]
|
||||
* Update 'pkg' dependency (improve support for Node v14) [Paulo Castro]
|
||||
* Turn v12 feature switch on [Paulo Castro]
|
||||
* Update minimum Node.js requirement from v8 to v10 [Paulo Castro]
|
||||
|
||||
## 11.36.0 - 2020-06-11
|
||||
|
||||
* balena device: Add the mac_address field [Thodoris Greasidis]
|
||||
|
||||
## 11.35.21 - 2020-06-11
|
||||
|
||||
* Allow setting the initialDeviceName [Rich Bayliss]
|
||||
|
||||
## 11.35.20 - 2020-06-10
|
||||
|
||||
* Restrict error handler typing [Scott Lowe]
|
||||
|
||||
## 11.35.19 - 2020-06-09
|
||||
|
||||
* Fix handling of BalenaExpiredToken error [Scott Lowe]
|
||||
|
||||
## 11.35.18 - 2020-06-05
|
||||
|
||||
* v12 preparations: Add feature switch for default eol-converson [Scott Lowe]
|
||||
* v12 preparations: Fix dockerignore tests on Windows [Paulo Castro]
|
||||
|
||||
## 11.35.17 - 2020-06-02
|
||||
|
||||
* Convert 'balena device public-url' commands to oclif [Scott Lowe]
|
||||
|
||||
## 11.35.16 - 2020-06-02
|
||||
|
||||
* v12 preparations: Add feature switch for build/deploy `--logs` option [Paulo Castro]
|
||||
|
||||
## 11.35.15 - 2020-05-29
|
||||
|
||||
* v12 preparations: Add feature switch for project directory validation [Paulo Castro]
|
||||
* v12 preparations: Add feature switch for 'balena apps --verbose' [Paulo Castro]
|
||||
* v12 preparations: Add feature switch for 'devices supported' default columns [Paulo Castro]
|
||||
* v12 preparations: Amend test cases for '--nogitignore' option [Paulo Castro]
|
||||
|
||||
## 11.35.14 - 2020-05-29
|
||||
|
||||
* v12 preparations: Add feature switch for 'envs --all' [Scott Lowe]
|
||||
|
||||
## 11.35.13 - 2020-05-29
|
||||
|
||||
* v12 preparations: Add feature switch to remove id from 'tags' output [Scott Lowe]
|
||||
|
||||
## 11.35.12 - 2020-05-29
|
||||
|
||||
* v12 preparations: Add feature switch for '--nogitignore' [Paulo Castro]
|
||||
|
||||
## 11.35.11 - 2020-05-28
|
||||
|
||||
* Convert `tags`, `tag set`, `tag rm` to oclif. [Scott Lowe]
|
||||
|
||||
## 11.35.10 - 2020-05-27
|
||||
|
||||
* v12 preparations: Add version switch, update login message. [Scott Lowe]
|
||||
|
||||
## 11.35.9 - 2020-05-25
|
||||
|
||||
* balena deploy: Fix "access denied" pushing images to registry [Paulo Castro]
|
||||
|
||||
## 11.35.8 - 2020-05-25
|
||||
|
||||
* Fix lazy loading in utils/compose [Pagan Gazzard]
|
||||
|
||||
## 11.35.7 - 2020-05-22
|
||||
|
||||
* Replace windows dns workaround with single lookup [Scott Lowe]
|
||||
|
||||
## 11.35.6 - 2020-05-21
|
||||
|
||||
* Convert selected functions to Typescript and async/await (compose.js) [Paulo Castro]
|
||||
* Add tests for 'balena deploy' [Paulo Castro]
|
||||
|
||||
## 11.35.5 - 2020-05-21
|
||||
|
||||
* Fix caching by preserving all file stats when pushing to device or cloud [Cameron Diver]
|
||||
|
||||
## 11.35.4 - 2020-05-19
|
||||
|
||||
* Add unit tests for errors module [Scott Lowe]
|
||||
|
||||
## 11.35.3 - 2020-05-18
|
||||
|
||||
* Update typescript to 3.9 [Pagan Gazzard]
|
||||
|
||||
## 11.35.2 - 2020-05-16
|
||||
|
||||
* Fix 'balena login' web authorization hanging with Google Chrome [Paulo Castro]
|
||||
* Update web page wording for 'balena login' web authorization [Paulo Castro]
|
||||
* Update `balena preload` help message (clarify accepted image formats) [Paulo Castro]
|
||||
* Update pre-commit script error message (automation/check-doc.js) [Paulo Castro]
|
||||
|
||||
## 11.35.1 - 2020-05-14
|
||||
|
||||
* Update GitHub templates for new issues and pull requests [Paulo Castro]
|
||||
|
||||
## 11.35.0 - 2020-05-14
|
||||
|
||||
* balena apps: add --verbose option to list application slugs (full app name) [Paulo Castro]
|
||||
* balena app create: fix application existence check [Paulo Castro]
|
||||
|
||||
## 11.34.0 - 2020-05-13
|
||||
|
||||
* push/build/deploy: add --nogitignore option and update dockerignore filter library [Paulo Castro]
|
||||
|
||||
## 11.33.4 - 2020-05-12
|
||||
|
||||
* Re-create standalone zip package (release asset) for Windows [Paulo Castro]
|
||||
|
||||
## 11.33.3 - 2020-05-12
|
||||
|
||||
* Fix usage of livepush v3 features [Cameron Diver]
|
||||
|
||||
## 11.33.2 - 2020-05-11
|
||||
|
||||
* Fix 'balena app' (rm, restart, info) with numeric app IDs [Paulo Castro]
|
||||
|
||||
## 11.33.1 - 2020-05-11
|
||||
|
||||
* Update resin-multibuild [Scott Lowe]
|
||||
|
||||
## 11.33.0 - 2020-05-11
|
||||
|
||||
* Add a deprecation policy [Thodoris Greasidis]
|
||||
|
||||
## 11.32.15 - 2020-05-06
|
||||
|
||||
* Improve presentation of errors, help [Scott Lowe]
|
||||
|
||||
## 11.32.14 - 2020-05-04
|
||||
|
||||
* Disable oclif's ts-node registering when running against built code [Pagan Gazzard]
|
||||
|
||||
## 11.32.13 - 2020-05-04
|
||||
|
||||
* Convert `balena api-key generate` to oclif [Scott Lowe]
|
||||
|
||||
## 11.32.12 - 2020-05-04
|
||||
|
||||
* Configure the sentry command scope earlier [Pagan Gazzard]
|
||||
|
||||
## 11.32.11 - 2020-05-01
|
||||
|
||||
* Avoid unnecessary api calls in `balena build` and `balena deploy` [Pagan Gazzard]
|
||||
|
||||
## 11.32.10 - 2020-05-01
|
||||
|
||||
* Refactor: move error related functions into error module [Scott Lowe]
|
||||
* Refactor: use checkLoggedIn() instead of exitIfNotLoggedIn() [Scott Lowe]
|
||||
|
||||
## 11.32.9 - 2020-05-01
|
||||
|
||||
* Convert qemu.js to typescript [Pagan Gazzard]
|
||||
|
||||
## 11.32.8 - 2020-05-01
|
||||
|
||||
* Enforce lazy loading via tslint import-blacklist [Pagan Gazzard]
|
||||
|
||||
## 11.32.7 - 2020-05-01
|
||||
|
||||
* Convert app commands to oclif [Scott Lowe]
|
||||
|
||||
## 11.32.6 - 2020-05-01
|
||||
|
||||
* Improve oclif missing argument/flag errors [Scott Lowe]
|
||||
|
||||
## 11.32.5 - 2020-05-01
|
||||
|
||||
* Modify oclif help to match balena conventions [Scott Lowe]
|
||||
|
||||
## 11.32.4 - 2020-04-30
|
||||
|
||||
* Convert gulpfile.coffee to javascript [Pagan Gazzard]
|
||||
* Convert lib/app-capitano.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.32.3 - 2020-04-30
|
||||
|
||||
* Convert lib/actions/index.coffee to typescript [Pagan Gazzard]
|
||||
|
||||
## 11.32.2 - 2020-04-30
|
||||
|
||||
* Convert lib/utils/deploy.coffee to javascript [Pagan Gazzard]
|
||||
* Convert lib/actions/build.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.32.1 - 2020-04-30
|
||||
|
||||
* Only notify of an update if the new version is actually newer [Pagan Gazzard]
|
||||
|
||||
## 11.32.0 - 2020-04-30
|
||||
|
||||
* Integrate livepush v3 and live directives [Scott Lowe]
|
||||
|
||||
## 11.31.28 - 2020-04-30
|
||||
|
||||
* Convert lib/utils/deploy-legacy.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.31.27 - 2020-04-30
|
||||
|
||||
* Convert lib/actions/help.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.31.26 - 2020-04-30
|
||||
|
||||
* balena build/deploy: Update QEMU version to support newer balenalib images [Paulo Castro]
|
||||
|
||||
## 11.31.25 - 2020-04-30
|
||||
|
||||
* Add support for global --debug flag [Scott Lowe]
|
||||
|
||||
## 11.31.24 - 2020-04-29
|
||||
|
||||
* balena deploy: Fix "TypeError: images.push is not iterable" [Paulo Castro]
|
||||
|
||||
## 11.31.23 - 2020-04-28
|
||||
|
||||
* Fix unhandled promise rejection when using `balena deploy` [Pagan Gazzard]
|
||||
|
||||
## 11.31.22 - 2020-04-25
|
||||
|
||||
* Convert lib/actions/device.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.31.21 - 2020-04-25
|
||||
|
||||
* Install types for modules used in javascript to improve type checking [Pagan Gazzard]
|
||||
|
||||
## 11.31.20 - 2020-04-24
|
||||
|
||||
* Convert lib/actions/preload.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.31.19 - 2020-04-24
|
||||
|
||||
* Convert lib/actions/config.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.31.18 - 2020-04-24
|
||||
|
||||
* Convert lib/utils/compose.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.31.17 - 2020-04-24
|
||||
|
||||
* Convert lib/utils/docker-coffee.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.31.16 - 2020-04-24
|
||||
|
||||
* Convert lib/actions/os.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.31.15 - 2020-04-24
|
||||
|
||||
* Convert lib/utils/qemu.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.31.14 - 2020-04-23
|
||||
|
||||
* device os-update: Refactor to use the overall_progress field [Thodoris Greasidis]
|
||||
|
||||
## 11.31.13 - 2020-04-23
|
||||
|
||||
* Remove unnecessary files [Pagan Gazzard]
|
||||
|
||||
## 11.31.12 - 2020-04-23
|
||||
|
||||
* Convert lib/actions/local/index.coffee to typescript [Pagan Gazzard]
|
||||
* Convert lib/actions/local/configure.coffee to javascript [Pagan Gazzard]
|
||||
* Convert lib/utils/tty to typescript [Pagan Gazzard]
|
||||
|
||||
## 11.31.11 - 2020-04-22
|
||||
|
||||
* Avoid patch-package warning with 'npm install -g --production' [Paulo Castro]
|
||||
|
||||
## 11.31.10 - 2020-04-22
|
||||
|
||||
* Convert command `scan` to TypeScript, migrate to oclif [Scott Lowe]
|
||||
|
||||
## 11.31.9 - 2020-04-22
|
||||
|
||||
* Update patch-package (fix remaining source of seemingly random ENOENT error) [Paulo Castro]
|
||||
|
||||
## 11.31.8 - 2020-04-22
|
||||
|
||||
* Update to balena-release [Pagan Gazzard]
|
||||
* Update dependencies to pick up performance improvements [Pagan Gazzard]
|
||||
|
||||
## 11.31.7 - 2020-04-21
|
||||
|
||||
* Update codeowners [Scott Lowe]
|
||||
|
||||
## 11.31.6 - 2020-04-21
|
||||
|
||||
* Add test coverage for validation module [Scott Lowe]
|
||||
|
||||
## 11.31.5 - 2020-04-20
|
||||
|
||||
* convert commands `key`, `keys`, `key add`, `key rm` to oclif. [Scott Lowe]
|
||||
|
||||
## 11.31.4 - 2020-04-18
|
||||
|
||||
* Review CONTRIBUTING.md and add 'instanceof' usage advice [Paulo Castro]
|
||||
* Review 'instanceof' usage with classes of external packages [Paulo Castro]
|
||||
* Unpin balena-sdk (bump balena-sdk to v12.33.0) [Paulo Castro]
|
||||
|
||||
## 11.31.3 - 2020-04-16
|
||||
|
||||
* Fix balena ssh "Application not found" (pin balena-sdk to v12.30.0) [Paulo Castro]
|
||||
|
||||
## 11.31.2 - 2020-04-15
|
||||
|
||||
* Fix seemingly random ENOENT error (update 'is-installed-globally' dependency) [Paulo Castro]
|
||||
|
||||
## 11.31.1 - 2020-04-15
|
||||
|
||||
* improve input validation for `key`, `key rm` [Scott Lowe]
|
||||
|
||||
## 11.31.0 - 2020-04-15
|
||||
|
||||
* device os-update: allow host OS upgrade with development balenaOS images [Scott Lowe]
|
||||
|
||||
## 11.30.17 - 2020-04-09
|
||||
|
||||
* Convert commands join, leave to oclif. [Scott Lowe]
|
||||
|
||||
## 11.30.16 - 2020-04-07
|
||||
|
||||
* Minor grammar fix in balena ssh documentation [Hugh Brown]
|
||||
|
||||
## 11.30.15 - 2020-04-03
|
||||
|
||||
* Convert `internal scandevices`, `internal osinit` to typescript & oclif [Scott Lowe]
|
||||
|
||||
## 11.30.14 - 2020-04-03
|
||||
|
||||
* Updated dependencies (vulnerability advisory CVE-2019-20149) [Paulo Castro]
|
||||
|
||||
## 11.30.13 - 2020-04-02
|
||||
|
||||
* Fix project directory validation for 'balena deploy' with pre-built image [Paulo Castro]
|
||||
|
||||
## 11.30.12 - 2020-04-01
|
||||
|
||||
* Remove unused code from balena note [Scott Lowe]
|
||||
|
||||
## 11.30.11 - 2020-04-01
|
||||
|
||||
* Check logged in for `balena build` if application specified Correct eroneous -f flag in `balena build` help [Scott Lowe]
|
||||
|
||||
## 11.30.10 - 2020-03-31
|
||||
|
||||
* Add '-t' option to 'balena ssh' to bypass TTY autodetection (force allocation) [Paulo Castro]
|
||||
* Handle ssh process exit codes [Paulo Castro]
|
||||
|
||||
## 11.30.9 - 2020-03-31
|
||||
|
||||
* Convert lib/actions/local/common.coffee to javascript [Pagan Gazzard]
|
||||
|
||||
## 11.30.8 - 2020-03-30
|
||||
|
||||
* Update README regarding proxy server support [Paulo Castro]
|
||||
* Fix "the input device is not a TTY" when piping to 'balena ssh' (local device) [Paulo Castro]
|
||||
* Fix 'balena ssh' on MSYS Windows shell ("unexpected end of file") [Paulo Castro]
|
||||
* Delete unused code (ssh.coffee) [Paulo Castro]
|
||||
|
||||
## 11.30.7 - 2020-03-30
|
||||
|
||||
* Convert command `note` to oclif Add oclif support for piped input [Scott Lowe]
|
||||
* Convert command `settings` to oclif [Scott Lowe]
|
||||
|
||||
## 11.30.6 - 2020-03-26
|
||||
|
||||
* Clarify `balena device os-update` help re balenaCloud [Scott Lowe]
|
||||
|
||||
## 11.30.5 - 2020-03-25
|
||||
|
||||
* Use balena-lint for javascript linting and add javascript type-checking [Pagan Gazzard]
|
||||
|
||||
## 11.30.4 - 2020-03-24
|
||||
|
||||
* Deduplicate `balenaUrl` fetching in events [Pagan Gazzard]
|
||||
|
||||
## 11.30.3 - 2020-03-24
|
||||
|
||||
* Preserve symlinks for the sake of the balenaCI worker [Pagan Gazzard]
|
||||
* Add type checking for tests [Pagan Gazzard]
|
||||
|
||||
## 11.30.2 - 2020-03-24
|
||||
|
||||
* Add support for authentication checking to oclif [Scott Lowe]
|
||||
|
||||
## 11.30.1 - 2020-03-19
|
||||
|
||||
* Add support for `root` property on oclif commands [Scott Lowe]
|
||||
|
||||
## 11.30.0 - 2020-03-19
|
||||
|
||||
* Add support for primary/secondary oclif commands [Scott Lowe]
|
||||
|
||||
## 11.29.5 - 2020-03-18
|
||||
|
||||
* INSTALL.md: emphasize the standalone zip package recommendation for WSL [Paulo Castro]
|
||||
|
||||
## 11.29.4 - 2020-03-16
|
||||
|
||||
* Switch to native number check [Pagan Gazzard]
|
||||
* Switch to native string check [Pagan Gazzard]
|
||||
* Switch to native `Array.isArray` instead of aliases [Pagan Gazzard]
|
||||
|
||||
## 11.29.3 - 2020-03-13
|
||||
|
||||
* Remove unused typings [Pagan Gazzard]
|
||||
|
||||
## 11.29.2 - 2020-03-12
|
||||
|
||||
* Fix opn patch (npm installation warning) [Paulo Castro]
|
||||
|
||||
## 11.29.1 - 2020-03-12
|
||||
|
||||
* Fix `balena local flash` [Pagan Gazzard]
|
||||
|
||||
## 11.29.0 - 2020-03-12
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update dependencies [Pagan Gazzard] </summary>
|
||||
|
||||
> ### balena-sdk-12.29.1 - 2020-03-09
|
||||
>
|
||||
> * tests: Improve the dependent application test case [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.29.0 - 2020-03-09
|
||||
>
|
||||
> * typings: Add the contract field to the Image [Thodoris Greasidis]
|
||||
> * typings: Add is_of__actor on the ApiKey [Thodoris Greasidis]
|
||||
> * typings: Add `is_public` to the application model [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.28.2 - 2020-03-06
|
||||
>
|
||||
> * Unify the way that the models get exported [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.28.1 - 2020-03-05
|
||||
>
|
||||
> * Convert OS model to typescript [Stevche Radevski]
|
||||
|
||||
> ### balena-sdk-12.28.0 - 2020-03-04
|
||||
>
|
||||
> * typings: Add DeviceType logoUrl property [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.27.1 - 2020-03-04
|
||||
>
|
||||
> * Update dependencies [Pagan Gazzard]
|
||||
|
||||
> ### balena-sdk-12.27.0 - 2020-03-03
|
||||
>
|
||||
> * Add missing deviceType typings to ImgConfigOptions [Stevche Radevski]
|
||||
</details>
|
||||
|
||||
## 11.28.17 - 2020-03-12
|
||||
|
||||
* Make windows installer remove old files before installation. [Scott Lowe]
|
||||
|
||||
## 11.28.16 - 2020-03-11
|
||||
|
||||
* Update CONTRIBUTING.md regarding ./bin/balena-dev and oclif commands [Paulo Castro]
|
||||
* Update CONTRIBUTING.md regarding Coffeescript to Typescript conversion [Paulo Castro]
|
||||
* Prevent auto merge of npm-shrinkwrap.json and explain it in CONTRIBUTING.md [Paulo Castro]
|
||||
* Add test case for `build --emulated` [Paulo Castro]
|
||||
|
||||
## 11.28.15 - 2020-03-11
|
||||
|
||||
* Fix 'balena login' web auth on Linux standalone zip install (xdg-open ENOENT) [Paulo Castro]
|
||||
|
||||
## 11.28.14 - 2020-03-10
|
||||
|
||||
* Avoid Sentry reporting of selected common "expected" errors [Paulo Castro]
|
||||
* Fix occasional "CLI prints 'null' and exits" (replace old Raven/Sentry SDK) [Paulo Castro]
|
||||
* Don't send the full command line to Sentry.io [Paulo Castro]
|
||||
* Fix occasionally missed command tracking request (oclif commands) [Paulo Castro]
|
||||
|
||||
## 11.28.13 - 2020-03-06
|
||||
|
||||
* Improve the UX by only printing effective file changes in livepush [Cameron Diver]
|
||||
|
||||
## 11.28.12 - 2020-03-06
|
||||
|
||||
* Fix `build --emulated` on Linux ("exec format error") [Paulo Castro]
|
||||
|
||||
## 11.28.11 - 2020-03-02
|
||||
|
||||
* Don't ignore BALENARC_NO_PROXY env var if HTTP(S)_PROXY env vars are defined [Paulo Castro]
|
||||
* Use types for global-agent and global-tunnel-ng [Pagan Gazzard]
|
||||
* Remove lodash usage in proxy setup [Pagan Gazzard]
|
||||
* Don't try to setup a proxy agent when there's no proxy configured [Pagan Gazzard]
|
||||
|
||||
## 11.28.10 - 2020-03-02
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update dependencies [Pagan Gazzard] </summary>
|
||||
|
||||
> ### balena-sdk-12.26.7 - 2020-02-29
|
||||
>
|
||||
> * Lazy-load the models props [Pagan Gazzard]
|
||||
> * Lazy-load the sdk template props [Pagan Gazzard]
|
||||
|
||||
> ### balena-sdk-12.26.6 - 2020-02-28
|
||||
>
|
||||
> * Remove unnecessary lodash/forEach usage [Pagan Gazzard]
|
||||
|
||||
> ### balena-sdk-12.26.5 - 2020-02-28
|
||||
>
|
||||
> * Convert the billing model to typescript [Stevche Radevski]
|
||||
|
||||
> ### balena-sdk-12.26.4 - 2020-02-27
|
||||
>
|
||||
> * Convert image model to typescript [Stevche Radevski]
|
||||
|
||||
> ### balena-sdk-12.26.3 - 2020-02-26
|
||||
>
|
||||
> * Update dtslint to v3.1.0 [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.26.2 - 2020-02-26
|
||||
>
|
||||
> * typings_tests/pine-options: Update to work with TypeScript v3.8 [Thodoris Greasidis]
|
||||
> * Bump TypeScript version to ^3.8.2, so that's used in tests [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.26.1 - 2020-02-26
|
||||
>
|
||||
> * application: Fix linter warning [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.26.0 - 2020-02-24
|
||||
>
|
||||
> * Add overall_progress typings to device model [Stevche Radevski]
|
||||
|
||||
> ### balena-sdk-12.25.1 - 2020-02-21
|
||||
>
|
||||
> * Convert service model to typescript [Stevche Radevski]
|
||||
|
||||
> ### balena-sdk-12.25.0 - 2020-02-19
|
||||
>
|
||||
> * Add device statuses enum to device resource [Stevche Radevski]
|
||||
> * Add device status enum and typings [Stevche Radevski]
|
||||
|
||||
> ### balena-sdk-12.24.4 - 2020-02-17
|
||||
>
|
||||
> * Fix a test case name typo for auth.whoami() [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.24.3 - 2020-02-17
|
||||
>
|
||||
> * auth.getEmail: Fix confusing call expression [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.24.2 - 2020-02-17
|
||||
>
|
||||
> * Fix concealing network errors in auth.whoami() and auth.isLoggedIn() [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.24.1 - 2020-02-15
|
||||
>
|
||||
> * .gitignore: `.idea` directory generated by JetBrains IDE [Thomas Manning]
|
||||
|
||||
> ### balena-sdk-12.24.0 - 2020-02-14
|
||||
>
|
||||
> * Update `application.getDashboardUrl` example with `application.get` call to get application id [Thomas Manning]
|
||||
> * Added `getDashboardUrl(id)` to application model [Thomas Manning]
|
||||
|
||||
> ### balena-sdk-12.23.9 - 2020-02-12
|
||||
>
|
||||
> * appveyor: Run node & browser tests in parallel [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.23.8 - 2020-02-11
|
||||
>
|
||||
> * appveyor: Set to test against node 8 [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.23.7 - 2020-02-07
|
||||
>
|
||||
> * Update balena-register-device to 6.0.1 [Pagan Gazzard]
|
||||
|
||||
> ### balena-sdk-12.23.6 - 2020-02-07
|
||||
>
|
||||
> * Re-enable balenaCI autoRebase [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.23.5 - 2020-02-07
|
||||
>
|
||||
> * Fix the tag tests failing b/c of public apps [Thodoris Greasidis]
|
||||
> * Fix the dependent app test randomly failing b/c of public apps [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.23.4 - 2020-02-07
|
||||
>
|
||||
> * Fix the build failing on node v12 [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.23.3 - 2020-02-06
|
||||
>
|
||||
> * Disable balenaCI auto rebase [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.23.2 - 2020-02-06
|
||||
>
|
||||
> * Switch to resin-lint for linting [Pagan Gazzard]
|
||||
|
||||
> ### balena-sdk-12.23.1 - 2020-02-06
|
||||
>
|
||||
> * Remove unused code [Pagan Gazzard]
|
||||
|
||||
> ### balena-sdk-12.23.0 - 2020-01-28
|
||||
>
|
||||
> * app.getWithDeviceServiceDetails: Add the release commit in the services [Thodoris Greasidis]
|
||||
> * device.getWithServiceDetails: Remove unused 'id' selection [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.22.0 - 2020-01-27
|
||||
>
|
||||
> * Allow retrieving applications by application case insensitive slug [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.21.3 - 2020-01-25
|
||||
>
|
||||
> * release.createFromUrl: Fix the non tar url error handling [Thodoris Greasidis]
|
||||
|
||||
> ### balena-sdk-12.21.2 - 2020-01-24
|
||||
>
|
||||
> * Convert some of the tests to TypeScript [Thodoris Greasidis]
|
||||
> * Properly type billing.downloadInvoice result [Thodoris Greasidis]
|
||||
> * Properly type os.download result [Thodoris Greasidis]
|
||||
> * Add typings for balena-request stream [Thodoris Greasidis]
|
||||
</details>
|
||||
|
||||
## 11.28.9 - 2020-02-29
|
||||
|
||||
* Switch to object spreading in favor of _.assign [Pagan Gazzard]
|
||||
|
||||
## 11.28.8 - 2020-02-28
|
||||
|
||||
* Lazy-load chalk [Pagan Gazzard]
|
||||
|
||||
## 11.28.7 - 2020-02-28
|
||||
|
||||
* Simplify lazy-loading of resin-cli-visuals with a shared function [Pagan Gazzard]
|
||||
|
||||
## 11.28.6 - 2020-02-28
|
||||
|
||||
* Make use of capitano's promise support to simplify the code [Pagan Gazzard]
|
||||
|
||||
## 11.28.5 - 2020-02-27
|
||||
|
||||
* Simplify lazy-loading of balena-sdk by utilizing a shared function [Pagan Gazzard]
|
||||
|
||||
## 11.28.4 - 2020-02-25
|
||||
|
||||
* Fix build/deploy commands with QEMU emulation and alternative Dockerfile name [Paulo Castro]
|
||||
* Fix CONTRIBUTING markdown [Paulo Castro]
|
||||
|
||||
## 11.28.3 - 2020-02-24
|
||||
|
||||
* Update type deps [Pagan Gazzard]
|
||||
|
||||
## 11.28.2 - 2020-02-21
|
||||
|
||||
* Add pre-commit check for cli.markdown updates and coffeelint execution [Paulo Castro]
|
||||
* Fix 'test:fast' npm script definition [Paulo Castro]
|
||||
|
||||
## 11.28.1 - 2020-02-21
|
||||
|
||||
* Add a script to automate nested changelogs [Thodoris Greasidis]
|
||||
|
||||
## 11.28.0 - 2020-02-18
|
||||
|
||||
* Update resin-multibuild and add app and release template vars [Cameron Diver]
|
||||
|
||||
## 11.27.0 - 2020-02-17
|
||||
|
||||
* Add tests for project directory validation [Paulo Castro]
|
||||
* Add project directory validation for balena push / build / deploy commands [Paulo Castro]
|
||||
* Refactor 'balena push' error handling [Paulo Castro]
|
||||
* Add and refactor tests for push/build/deploy commands (docker-compose) [Paulo Castro]
|
||||
|
||||
## 11.26.0 - 2020-02-14
|
||||
|
||||
* Add '--cache-from' option to balena build and deploy commands [Paulo Castro]
|
||||
|
||||
## 11.25.18 - 2020-02-13
|
||||
|
||||
* Fix balena push "Segmentation fault" on Windows (replace 'mmmagic' with 'isBinaryFile') [Paulo Castro]
|
||||
|
||||
## 11.25.17 - 2020-02-12
|
||||
|
||||
* Convert lib/actions/auth to typescript [Pagan Gazzard]
|
||||
|
||||
## 11.25.16 - 2020-02-12
|
||||
|
||||
* Convert lib/auth/index to typescript [Pagan Gazzard]
|
||||
|
||||
## 11.25.15 - 2020-02-12
|
||||
|
||||
* Convert lib/auth/server to typescript [Pagan Gazzard]
|
||||
|
||||
## 11.25.14 - 2020-02-11
|
||||
|
||||
* Convert lib/actions/keys to typescript [Pagan Gazzard]
|
||||
|
||||
## 11.25.13 - 2020-02-10
|
||||
|
||||
* Convert lib/actions/notes to typescript [Pagan Gazzard]
|
||||
|
||||
## 11.25.12 - 2020-02-10
|
||||
|
||||
* Convert lib/actions/app to typescript [Pagan Gazzard]
|
||||
|
||||
## 11.25.11 - 2020-02-10
|
||||
|
||||
* Convert lib/auth/utils to typescript [Pagan Gazzard]
|
||||
|
||||
## 11.25.10 - 2020-02-08
|
||||
|
||||
* CI builds: revert patch-package upgrade to fix patch errors [Paulo Castro]
|
||||
* Node 13 compatibility: upgrade ext2fs module [Scott Lowe]
|
||||
|
||||
## 11.25.9 - 2020-02-07
|
||||
|
||||
* Add .gitattributes to check out with the correct line-ending on windows [Pagan Gazzard]
|
||||
|
||||
## 11.25.8 - 2020-02-07
|
||||
|
||||
* Merge resin-lint linting and fixing steps into one [Pagan Gazzard]
|
||||
* Remove redundant type checking of tests [Pagan Gazzard]
|
||||
* Remove duplicate type checking of automation code [Pagan Gazzard]
|
||||
|
||||
## 11.25.7 - 2020-02-07
|
||||
|
||||
* Fix Windows standalone zip installer (missing mmmagic db for CRLF conversion) [Paulo Castro]
|
||||
|
||||
## 11.25.6 - 2020-02-06
|
||||
|
||||
* Switch from opn to its new name of open [Pagan Gazzard]
|
||||
|
||||
## 11.25.5 - 2020-02-06
|
||||
|
||||
* Add debug instructions for powershell [Pagan Gazzard]
|
||||
|
||||
## 11.25.4 - 2020-02-06
|
||||
|
||||
* Use resin-lint for automatic lint fixing [Pagan Gazzard]
|
||||
|
||||
## 11.25.3 - 2020-02-06
|
||||
|
||||
* Avoid loading 'mmmagic' on Linux (fix "could not load any valid magic files") [Paulo Castro]
|
||||
|
||||
## 11.25.2 - 2020-02-05
|
||||
|
||||
* Debug mode can now be disabled with DEBUG=0 env var Added assignment to `process.env.DEBUG` if `process.env.DEBUG` is negative string to `lib/app.ts` and `automation/run.ts` entrypoints [Thomas Manning]
|
||||
|
||||
## 11.25.1 - 2020-02-03
|
||||
|
||||
* Remove unnecessary code now that typescript understands `process.exit` [Pagan Gazzard]
|
||||
|
||||
## 11.25.0 - 2020-02-02
|
||||
|
||||
* Add more tests for push/build/deploy commands (--convert-eol) [Paulo Castro]
|
||||
* Add more tests for push/build/deploy commands (--dockerfile) [Paulo Castro]
|
||||
* Add support for auto-conversion of CRLF line endings. Applies to commands: balena push balena build balena deploy --build [Scott Lowe]
|
||||
* Add support for deferred log messages. eg. so that info can be output at the end of the process. [Scott Lowe]
|
||||
|
||||
## 11.24.0 - 2020-01-27
|
||||
|
||||
* Fix proxy support and add proxy exclusion feature (Node.js >= 10.16.0 only) [Paulo Castro]
|
||||
* Update Github's templates for new CLI pull requests and issues [Paulo Castro]
|
||||
|
||||
## 11.23.0 - 2020-01-24
|
||||
|
||||
* Update dependencies [Pagan Gazzard]
|
||||
|
||||
## 11.22.0 - 2020-01-21
|
||||
|
||||
* Configure: Allow passing system-connection files to 'os configure' command [Rich Bayliss]
|
||||
|
||||
## 11.21.8 - 2020-01-20
|
||||
|
||||
* Add `catch-uncommitted` to balena CI build [Paulo Castro]
|
||||
* Update resin-lint and prettier, and re-prettify [Paulo Castro]
|
||||
* Add tests for push, deploy and build commands [Paulo Castro]
|
||||
|
||||
## 11.21.7 - 2020-01-20
|
||||
|
||||
* Prevent file ignorer from ignoring Dockerfile (and variants), docker-compose.yml [Scott Lowe]
|
||||
|
||||
## 11.21.6 - 2020-01-20
|
||||
|
||||
* Add Windows-specific hint to 'balena scan' output [Graham McCulloch]
|
||||
|
||||
## 11.21.5 - 2020-01-14
|
||||
|
||||
* Change the balena app action to present the slug instead of the git_repository [Thodoris Greasidis]
|
||||
|
||||
## 11.21.4 - 2020-01-14
|
||||
|
||||
* Fix 'balena join' when the user is not logged in [Paulo Castro]
|
||||
* Fix join and leave commands on Windows (hanging on stdin and argument escaping) [Paulo Castro]
|
||||
|
||||
## 11.21.3 - 2020-01-14
|
||||
|
||||
* Increase default mocha test timeout to avoid spurious CI failures [Paulo Castro]
|
||||
* Fix 'balena push' hanging on Windows (CTRL-C was required after the unicorn) [Paulo Castro]
|
||||
* Add hint about the 'jq' utility in the documentation of the --json option [Paulo Castro]
|
||||
* Add '.nyc_output' folder to '.gitignore' (test coverage reporting) [Paulo Castro]
|
||||
|
||||
## 11.21.2 - 2020-01-14
|
||||
|
||||
* Update CONTRIBUTING.md regarding npm installation and some common gotchas [Paulo Castro]
|
||||
|
||||
## 11.21.1 - 2020-01-13
|
||||
|
||||
* Meta: Americanize all spellings [Matthew McGinn]
|
||||
|
||||
## 11.21.0 - 2019-12-27
|
||||
|
||||
* Add --verbose and --json options to the 'devices supported' command [Paulo Castro]
|
||||
|
||||
## 11.20.2 - 2019-12-17
|
||||
|
||||
* Update livepush to fix windows path issue. [Scott Lowe]
|
||||
|
||||
## 11.20.1 - 2019-12-13
|
||||
|
||||
* Fix issues with devices associated with inaccessible applications. [Scott Lowe]
|
||||
|
||||
## 11.20.0 - 2019-12-12
|
||||
|
||||
* Add multicontainer (microservices) support for 'balena env rename' [Paulo Castro]
|
||||
* Add multicontainer (microservices) support for 'balena env rm' [Paulo Castro]
|
||||
* Add multicontainer (microservices) support for 'balena env add' [Paulo Castro]
|
||||
* Add multicontainer (microservices) support for 'balena envs' [Paulo Castro]
|
||||
* Add balena envs '-j' option to produce JSON output [Paulo Castro]
|
||||
* Add logged-in check for balena 'env' commands [Paulo Castro]
|
||||
|
||||
## 11.19.1 - 2019-12-06
|
||||
|
||||
* Introduce workaround that fixes windows dns issue on `balena push` using .local device names. Improve error handling in deployToDevice so that versionErrors don't mask other errors. [Scott Lowe]
|
||||
|
||||
## 11.19.0 - 2019-12-05
|
||||
|
||||
* Update app/create and device/supported tests to use new api-mock. [Scott Lowe]
|
||||
* Introduce balena-api-mock module to simplify api mocking. Upgrade nock to latest. [Scott Lowe]
|
||||
|
||||
## 11.18.3 - 2019-11-21
|
||||
|
||||
* Fix 'balena help join' docs re moving devices between apps on the same server [Paulo Castro]
|
||||
* Add README note regarding Git for Windows console installation choice [Paulo Castro]
|
||||
|
||||
## 11.18.2 - 2019-11-15
|
||||
|
||||
* Use helpers version of `cleanOutput` in tests. Simplify expect semantics in tests. [Scott Lowe]
|
||||
|
193
CONTRIBUTING.md
193
CONTRIBUTING.md
@ -2,13 +2,47 @@
|
||||
|
||||
The balena CLI is an open source project and your contribution is welcome!
|
||||
|
||||
After cloning this repository and running `npm install`, the CLI can be built with `npm run build`
|
||||
and executed with `./bin/balena`. In order to ease development:
|
||||
* Install the dependencies listed in the [NPM Installation](./INSTALL.md#npm-installation)
|
||||
section of the `INSTALL.md` file. Check the section [Additional
|
||||
Dependencies](./INSTALL.md#additional-dependencies) too.
|
||||
* Clone the `balena-cli` repository, `cd` to it and run `npm install`.
|
||||
* Build the CLI with `npm run build` or `npm test`, and execute it with `./bin/balena`
|
||||
(on a Windows command prompt, you may need to run `node .\bin\balena`).
|
||||
|
||||
In order to ease development:
|
||||
|
||||
* `npm run build:fast` skips some of the build steps for interactive testing, or
|
||||
* `./bin/balena-dev` uses `ts-node/register` and `coffeescript/register` to transpile on the fly.
|
||||
* `npm run test:source` skips testing the standalone zip packages (which is rather slow)
|
||||
* `./bin/balena-dev` uses `ts-node/register` to transpile on the fly.
|
||||
|
||||
Before opening a PR, please be sure to test your changes with `npm test`.
|
||||
Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is
|
||||
meant to run on Linux, macOS and Windows. balena CI will run test code on all three platforms, but
|
||||
this will only help if you add some test cases for your new code!
|
||||
|
||||
## ./bin/balena-dev and oclif
|
||||
|
||||
When using `./bin/balena-dev` with oclif-converted commands, it is currently necessary to manually
|
||||
edit the `oclif` section of `package.json` to replace `./build` with `./lib` as follows:
|
||||
|
||||
Change from:
|
||||
```
|
||||
"oclif": {
|
||||
"commands": "./build/actions-oclif",
|
||||
"hooks": {
|
||||
"prerun": "./build/hooks/prerun/track"
|
||||
```
|
||||
|
||||
To:
|
||||
```
|
||||
"oclif": {
|
||||
"commands": "./lib/actions-oclif",
|
||||
"hooks": {
|
||||
"prerun": "./lib/hooks/prerun/track"
|
||||
```
|
||||
|
||||
And then remember to change it back before pushing the pull request. This is obviously error prone
|
||||
and inconvenient, and improvement suggestions are welcome: is there a better solution than
|
||||
automatically editing `package.json`? It is doable, if it is what needs to be done.
|
||||
|
||||
## Semantic versioning and commit messages
|
||||
|
||||
@ -45,21 +79,148 @@ The `INSTALL.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||
## Windows
|
||||
|
||||
Please note that `npm run build:installer` (which generates the `.exe` executable installer on
|
||||
Windows) requires [MSYS2](https://www.msys2.org/) to be installed. Other than that, the standard
|
||||
Command Prompt or PowerShell can be used.
|
||||
Windows) specifically requires [MSYS2](https://www.msys2.org/) to be installed. Other than that,
|
||||
the standard Command Prompt or PowerShell can be used (though MSYS2 is still handy, as it provides
|
||||
'git' and a number of common unix utilities). If you make changes to `package.json` scripts, check
|
||||
they also run on a standard Windows Command Prompt.
|
||||
|
||||
## TypeScript vs CoffeeScript, and Capitano vs oclif
|
||||
## Updating the 'npm-shrinkwrap.json' file
|
||||
|
||||
The CLI was originally written in [CoffeeScript](https://coffeescript.org), but we decided to
|
||||
migrate to [TypeScript](https://www.typescriptlang.org/) in order to take advantage of static
|
||||
typing and formal programming interfaces. The migration is taking place gradually, as part of
|
||||
maintenance work or the implementation of new features.
|
||||
The `npm-shrinkwrap.json` file is used to control package dependencies, as documented at
|
||||
https://docs.npmjs.com/files/shrinkwrap.json.
|
||||
|
||||
While developing, the `package.json` file is often modified by, or before, running `npm install`
|
||||
in order to add, remove or modify dependencies. When `npm install` is executed, it automatically
|
||||
updates the `npm-shrinkwrap.json` file as well, **taking into account not only the `package.json`
|
||||
file but also the current state of the `node_modules` folder in your computer.**
|
||||
|
||||
Meanwhile, as a text (JSON) file, `git` is capable of merging the `npm-shrinkwrap.json` file during
|
||||
operations like `rebase`, `cherry-pick` and `pull`. But git's automated merge is not the
|
||||
recommended way of updating the `npm-shrinkwrap.json` file, because it does not take into account
|
||||
duplicates or conflicts in the dependency tree, or indeed the state of the `package.json` file
|
||||
(which may have just been merged). In extreme cases, the automated merge may actually result in a
|
||||
broken installation. For these reasons, automatic merging of the `npm-shrinkwrap.json` was disabled
|
||||
through the `.gitattributes` file (the "binary merge driver" allows diff'ing but prevents automatic
|
||||
merging). Operations like `git rebase` may then result in an error like:
|
||||
|
||||
```text
|
||||
$ git rebase master
|
||||
warning: Cannot merge binary files: npm-shrinkwrap.json (HEAD vs. c34942b9... test)
|
||||
Auto-merging npm-shrinkwrap.json
|
||||
CONFLICT (content): Merge conflict in npm-shrinkwrap.json
|
||||
error: Failed to merge in the changes.
|
||||
```
|
||||
|
||||
Whether or not there is a merge error, the following commands are the recommended way of updating
|
||||
and committing the `npm-shrinkwrap.json` file:
|
||||
|
||||
```bash
|
||||
$ rm -rf node_modules # Linux / Mac
|
||||
$ rmdir /s node_modules # Windows Command Prompt
|
||||
$ npm checkout master -- npm-shrinkwrap.json # revert it to the master branch state
|
||||
$ npm install # "cleanly" update the npm-shrinkwrap.json file
|
||||
$ git add npm-shrinkwrap.json # add it for committing (solve merge errors)
|
||||
```
|
||||
|
||||
## TypeScript and oclif
|
||||
|
||||
The CLI currently contains a mix of plain JavaScript and
|
||||
[TypeScript](https://www.typescriptlang.org/) code. The goal is to have all code written in
|
||||
Typescript, in order to take advantage of static typing and formal programming interfaces.
|
||||
The migration towards Typescript is taking place gradually, as part of maintenance work or
|
||||
the implementation of new features. Historically, the CLI was originally written in
|
||||
[CoffeeScript](https://coffeescript.org), but all CoffeeScript code was migrated to either
|
||||
Javascript or Typescript.
|
||||
|
||||
Similarly, [Capitano](https://github.com/balena-io/capitano) was originally adopted as the CLI's
|
||||
framework, but we recently decided to take advantage of [oclif](https://oclif.io/)'s features such
|
||||
framework, but later we decided to take advantage of [oclif](https://oclif.io/)'s features such
|
||||
as native installers for Windows, macOS and Linux, and support for custom flag parsing (for
|
||||
example, we're still battling with Capitano's behavior of dropping leading zeros of arguments that
|
||||
look like integers such as some abbreviated UUIDs, and migrating to oclif is a solution). Again the
|
||||
migration is taking place gradually, with some CLI commands parsed by oclif and others by Capitano
|
||||
(a simple command line pre-parsing takes place in `app.ts` to decide whether to route full parsing
|
||||
to Capitano or oclif).
|
||||
look like integers, such as some abbreviated UUIDs). Again, the migration is taking place
|
||||
gradually, with some CLI commands parsed by oclif and others by Capitano. A simple command line
|
||||
pre-parsing takes place in `preparser.ts`, to decide whether to route full parsing to Capitano or
|
||||
to oclif.
|
||||
|
||||
## Programming style
|
||||
|
||||
`npm run build` also runs [balena-lint](https://www.npmjs.com/package/@balena/lint), which automatically
|
||||
reformats the code. Beyond that, we have a preference for Javascript promises over callbacks, and for
|
||||
`async/await` over `.then()`.
|
||||
|
||||
## Updating upstream dependencies
|
||||
|
||||
In order to get proper nested changelogs, when updating upstream modules that are in the repo.yml
|
||||
(like the balena-sdk), the commit body has to contain a line with the following format:
|
||||
```
|
||||
Update balena-sdk from 12.0.0 to 12.1.0
|
||||
```
|
||||
|
||||
Since this is error prone, it's suggested to use the following npm script:
|
||||
```
|
||||
npm run update balena-sdk ^12.1.0
|
||||
```
|
||||
|
||||
This will create a new branch (only if you are currently on master), run `npm update` with the
|
||||
version you provided as a target and commit the package.json & npm-shrinkwrap.json. The script by
|
||||
default will set the `Change-type` to `patch` or `minor`, depending on the semver change of the
|
||||
updated dependency, but if you need to use a different one (eg `major`) you can specify it as an
|
||||
extra argument:
|
||||
```
|
||||
npm run update balena-sdk ^12.14.0 patch
|
||||
npm run update balena-sdk ^13.0.0 major
|
||||
```
|
||||
|
||||
## Common gotchas
|
||||
|
||||
One thing that most CLI bugs have in common is the absence of test cases exercising the broken
|
||||
code, so writing some test code is a great idea. Having said that, there are also some common
|
||||
gotchas to bear in mind:
|
||||
|
||||
* Forward slashes ('/') _vs._ backslashes ('\') in file paths. The Node.js
|
||||
[path.sep](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_sep) variable stores a
|
||||
platform-specific path separator character: the backslash on Windows and the forward slash on
|
||||
Linux and macOS. The
|
||||
[path.join](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_join_paths) function
|
||||
builds paths using such platform-specific path separator. However:
|
||||
* Note that Windows (kernel, cmd.exe, PowerShell, many applications) accepts ***both*** forward
|
||||
slashes and backslashes as path separators (including mixing them in a path string), so code
|
||||
like `mypath.split(path.sep)` may fail on Windows if `mypath` contains forward slashes. The
|
||||
[path.parse](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_parse_path) function
|
||||
understands both forward slashes and backslashes on Windows, and the
|
||||
[path.normalize](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_normalize_path)
|
||||
function will _replace_ forward slashes with backslashes.
|
||||
* In [tar](https://en.wikipedia.org/wiki/Tar_(computing)#File_format) streams sent to the Docker
|
||||
daemon and to balenaCloud, the forward slash is the only acceptable path separator, regardless
|
||||
of the OS where the CLI is running. Therefore, `path.sep` and `path.join` should never be used
|
||||
when handling paths in tar streams! `path.posix.join` may be used instead of `path.join`.
|
||||
|
||||
* Avoid using the system shell to execute external commands, for example:
|
||||
`child_process.exec('ssh "arg1" "arg2"');`
|
||||
`child_process.spawn('ssh "arg1" "arg2"', { shell: true });`
|
||||
Besides the usual security concerns of unsanitized strings, another problem is to get argument
|
||||
escaping right because of the differences between the Windows 'cmd.exe' shell and the Unix
|
||||
'/bin/sh'. For example, 'cmd.exe' doesn't recognize single quotes like '/bin/sh', and uses the
|
||||
caret (^) instead of the backslash as the escape character. Bug territory! Most of the time,
|
||||
it is possible to avoid relying on the shell altogether by providing a Javascript array of
|
||||
arguments:
|
||||
`spawn('ssh', ['arg1', 'arg2'], { shell: false});`
|
||||
To allow for logging and debugging, the [which](https://www.npmjs.com/package/which) package may
|
||||
be used to get the full path of a command before executing it, without relying on any shell:
|
||||
`const fullPath = await which('ssh');`
|
||||
`console.log(fullPath); # 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'`
|
||||
`spawn(fullPath, ['arg1', 'arg2'], { shell: false });`
|
||||
|
||||
* Avoid the `instanceof` operator when testing against classes/types from external packages
|
||||
(including base classes), because `npm install` may result in multiple versions of the same
|
||||
package being installed (to satisfy declared dependencies) and a false negative may result when
|
||||
comparing an object instance from one package version with a class of another package version
|
||||
(even if the implementations are identical in both packages). For example, once we fixed a bug
|
||||
where the test:
|
||||
`error instanceof BalenaApplicationNotFound`
|
||||
changed from true to false because `npm install` added an additional copy of the `balena-errors`
|
||||
package to satisfy a minor `balena-sdk` version update:
|
||||
`$ find node_modules -name balena-errors`
|
||||
`node_modules/balena-errors`
|
||||
`node_modules/balena-sdk/node_modules/balena-errors`
|
||||
In the case of subclasses of `TypedError`, a string comparison may be used instead:
|
||||
`error.name === 'BalenaApplicationNotFound'`
|
||||
|
70
INSTALL.md
70
INSTALL.md
@ -2,13 +2,13 @@
|
||||
|
||||
There are 3 options to choose from to install balena's CLI:
|
||||
|
||||
* [Executable Installer](#executable-installer): the easiest method, using the traditional
|
||||
graphical desktop application installers for Windows and macOS (coming soon for Linux users too).
|
||||
* [Executable Installer](#executable-installer): the easiest method on Windows and macOS, using the
|
||||
traditional graphical desktop application installers.
|
||||
* [Standalone Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI
|
||||
executable in them. Recommended for scripted installation in CI (continuous integration)
|
||||
environments.
|
||||
* [NPM Installation](#npm-installation): recommended for developers who may be interested in
|
||||
integrating the balena CLI in their existing Node.js projects or workflow.
|
||||
executable in them: extract and run. Available for all platforms: Linux, Windows, macOS.
|
||||
Recommended also for scripted installation in CI (continuous integration) environments.
|
||||
* [NPM Installation](#npm-installation): recommended for Node.js developers who may be interested
|
||||
in integrating the balena CLI in their existing projects or workflow.
|
||||
|
||||
Some specific CLI commands have a few extra installation steps: see section [Additional
|
||||
Dependencies](#additional-dependencies).
|
||||
@ -18,17 +18,18 @@ Dependencies](#additional-dependencies).
|
||||
> and getting started with the balena CLI on Windows. (The video uses the standalone zip package
|
||||
> option.)
|
||||
> * If you are using Microsoft's [Windows Subsystem for
|
||||
> Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL), the recommendation is to
|
||||
> install a balena CLI release for Linux rather than Windows, like the Linux standalone zip
|
||||
> package. An installation with the graphical executable installer for Windows will not run on
|
||||
> WSL.
|
||||
> Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL), install a balena CLI release
|
||||
> for Linux rather than for Windows, like the standalone zip package for Linux. An installation
|
||||
> with the graphical executable installer for Windows will **not** work with WSL.
|
||||
|
||||
## Executable Installer
|
||||
|
||||
Recommended for Windows (but not Windows Subsystem for Linux) and macOS:
|
||||
|
||||
1. Download the latest installer from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
||||
Look for a file name that ends with "-installer", for example:
|
||||
`balena-cli-v11.6.0-windows-x64-installer.exe`
|
||||
`balena-cli-v11.6.0-macOS-x64-installer.pkg`
|
||||
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
|
||||
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
|
||||
|
||||
2. Double click the downloaded file to run the installer.
|
||||
_If you are using macOS Catalina (10.15), [check this known issue and
|
||||
@ -59,19 +60,28 @@ macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
|
||||
|
||||
1. Download the latest zip file from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
||||
Look for a file name that ends with the word "standalone", for example:
|
||||
`balena-cli-v10.13.6-linux-x64-standalone.zip`
|
||||
`balena-cli-v10.13.6-macOS-x64-standalone.zip`
|
||||
`balena-cli-v10.13.6-windows-x64-standalone.zip`
|
||||
`balena-cli-vX.Y.Z-linux-x64-standalone.zip` ← _also for the Windows Subsystem for Linux_
|
||||
`balena-cli-vX.Y.Z-macOS-x64-standalone.zip`
|
||||
`balena-cli-vX.Y.Z-windows-x64-standalone.zip`
|
||||
|
||||
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
||||
`balena-cli` folder.
|
||||
|
||||
3. Add the `balena-cli` folder to the system's `PATH` environment variable.
|
||||
See instructions for:
|
||||
[Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
|
||||
[macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) |
|
||||
[Windows](https://www.computerhope.com/issues/ch000549.htm)
|
||||
|
||||
> _If you are using macOS Catalina (10.15), [check this known issue and
|
||||
> workaround](https://github.com/balena-io/balena-cli/issues/1479)._
|
||||
> * If you are using macOS Catalina (10.15), [check this known issue and
|
||||
> workaround](https://github.com/balena-io/balena-cli/issues/1479).
|
||||
> * **Linux Alpine** and **Busybox:** the standalone zip package is not currently compatible with
|
||||
> these "compact" Linux distributions, because of the alternative C libraries they ship with.
|
||||
> It should however work with all "desktop" or "server" distributions, e.g. Ubuntu, Debian, Suse,
|
||||
> Fedora, Arch Linux and many more.
|
||||
> * Note that moving the `balena` executable out of the extracted `balena-cli` folder on its own
|
||||
> (e.g. moving it to `/usr/local/bin/balena`) will **not** work, as it depends on the other
|
||||
> folders and files also present in the `balena-cli` folder.
|
||||
|
||||
To update the CLI to a new version, download a new release zip file and replace the previous
|
||||
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
|
||||
@ -86,9 +96,9 @@ some additional development tools to be installed first:
|
||||
* [Node.js](https://nodejs.org/) version 8, 10 or 12
|
||||
* **Linux, macOS** and **Windows Subsystem for Linux (WSL):**
|
||||
Installing Node via [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md) is recommended.
|
||||
With some Linux distributions like Ubuntu, users sometimes report permission errors when using
|
||||
the system's Node installation (i.e. when Node is installed via `apt-get`), hence the
|
||||
[nvm](https://github.com/creationix/nvm) recommendation. This [sample
|
||||
When the "system" or "default" Node.js and npm packages are installed with "apt-get" in Linux
|
||||
distributions like Ubuntu, users often report permission or compilation errors when running
|
||||
"npm install". This [sample
|
||||
Dockerfile](https://gist.github.com/pdcastro/5d4d96652181e7da685a32caf629dd44) shows the CLI
|
||||
installation steps on an Ubuntu 18.04 base image.
|
||||
* If using **Node v8,** upgrade `npm` to version 6.9.0 or later with `"npm install -g npm"`
|
||||
@ -141,11 +151,13 @@ especially if you're using a user-managed node install such as [nvm](https://git
|
||||
([more information](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse)).
|
||||
For other versions of Windows, there are several ssh/OpenSSH clients provided by 3rd parties.
|
||||
|
||||
* If you need SSH to work behind a proxy, you will also need to install
|
||||
[`proxytunnel`](http://proxytunnel.sourceforge.net/) (available as a `proxytunnel` package
|
||||
for Ubuntu, for example).
|
||||
Check the [README](https://github.com/balena-io/balena-cli/blob/master/README.md) file
|
||||
for proxy configuration instructions.
|
||||
* The [`proxytunnel`](http://proxytunnel.sourceforge.net/) package (command-line tool) is needed
|
||||
for the `balena ssh` command to work behind a proxy. It is available for Linux distributions
|
||||
like Ubuntu/Debian (`apt install proxytunnel`), and for macOS through
|
||||
[Homebrew](https://brew.sh/). Windows support is limited to the Windows Subsystem for Linux
|
||||
(e.g., by installing Ubuntu through the Microsoft App Store). Check the
|
||||
[README](https://github.com/balena-io/balena-cli/blob/master/README.md) file for proxy
|
||||
configuration instructions.
|
||||
|
||||
* The `balena preload`, `balena build` and `balena deploy --build` commands require
|
||||
[Docker](https://docs.docker.com/install/overview/) or [balenaEngine](https://www.balena.io/engine/)
|
||||
@ -170,6 +182,14 @@ especially if you're using a user-managed node install such as [nvm](https://git
|
||||
Windows, check the [FAQ item "Docker seems to be
|
||||
unavailable"](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md#docker-seems-to-be-unavailable-error-when-using-windows-subsystem-for-linux-wsl).
|
||||
|
||||
* The `balena scan` command requires a multicast DNS (mDNS) service like Bonjour or Avahi:
|
||||
* On Windows, check if 'Bonjour' is installed (Control Panel > Programs and Features).
|
||||
If not, you can download Bonjour for Windows from https://support.apple.com/kb/DL999
|
||||
* Most 'desktop' Linux distributions ship with [Avahi](https://en.wikipedia.org/wiki/Avahi_(software)).
|
||||
Search for the installation command for your distribution. E.g. for Ubuntu:
|
||||
`sudo apt-get install avahi-daemon`
|
||||
* macOS comes with [Bonjour](https://en.wikipedia.org/wiki/Bonjour_(software)) built-in.
|
||||
|
||||
## Configuring SSH keys
|
||||
|
||||
The `balena ssh` command requires an SSH key to be added to your balena account. If you had
|
||||
|
91
README.md
91
README.md
@ -36,6 +36,11 @@ including:
|
||||
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
|
||||
* [MSYS](http://www.mingw.org/wiki/MSYS): select the `msys-rsync` and `msys-openssh` packages too
|
||||
* [Git for Windows](https://git-for-windows.github.io/)
|
||||
* During the installation, you will be prompted to choose between _"Use MinTTY"_ and _"Use
|
||||
Windows' default console window"._ Choose the latter, because of the same [MSYS2
|
||||
bug](https://github.com/msys2/MINGW-packages/issues/1633) mentioned above (Git for Windows
|
||||
actually uses MSYS2). For a screenshot, check this
|
||||
[comment](https://github.com/balena-io/balena-cli/issues/598#issuecomment-556513098).
|
||||
* Microsoft's [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about)
|
||||
(WSL). In this case, a Linux distribution like Ubuntu is installed via the Microsoft Store, and a
|
||||
balena CLI release **for Linux** is recommended. See
|
||||
@ -59,19 +64,67 @@ $ balena login
|
||||
|
||||
### Proxy support
|
||||
|
||||
HTTP(S) proxies can be configured through any of the following methods, in order of preference:
|
||||
HTTP(S) proxies can be configured through any of the following methods, in precedence order
|
||||
(from higher to lower):
|
||||
|
||||
* Set the `BALENARC_PROXY` environment variable in URL format (with protocol, host, port, and
|
||||
optionally basic auth).
|
||||
* Alternatively, use the [balena config file](https://www.npmjs.com/package/balena-settings-client#documentation)
|
||||
(project-specific or user-level) and set the `proxy` setting. It can be:
|
||||
* A string in URL format, or
|
||||
* An object in the [global-tunnel-ng options format](https://www.npmjs.com/package/global-tunnel-ng#options) (which allows more control).
|
||||
* Alternatively, set the conventional `https_proxy` / `HTTPS_PROXY` / `http_proxy` / `HTTP_PROXY`
|
||||
environment variable (in the same standard URL format).
|
||||
* The `BALENARC_PROXY` environment variable in URL format, with protocol (`http` or `https`),
|
||||
host, port and optionally basic auth. Examples:
|
||||
* `export BALENARC_PROXY='https://bob:secret@proxy.company.com:12345'`
|
||||
* `export BALENARC_PROXY='http://localhost:8000'`
|
||||
|
||||
To get a proxy to work with the `balena ssh` command, check the
|
||||
[installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
* The `proxy` setting in the [CLI config
|
||||
file](https://www.npmjs.com/package/balena-settings-client#documentation). It may be:
|
||||
* A string in URL format, e.g. `proxy: 'http://localhost:8000'`
|
||||
* An object in the format:
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
protocol: 'http'
|
||||
host: 'proxy.company.com'
|
||||
port: 12345
|
||||
proxyAuth: 'bob:secret'
|
||||
```
|
||||
|
||||
* The `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables, in the same URL format as
|
||||
`BALENARC_PROXY`.
|
||||
|
||||
> Note: The `balena ssh` command has additional setup requirements to work behind a proxy.
|
||||
> Check the [installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md),
|
||||
> and ensure that the proxy server is configured to allow proxy requests to ssh port 22, using
|
||||
> SSL encryption. For example, in the case of the [Squid](http://www.squid-cache.org/) proxy
|
||||
> server, it should be configured with the following rules in the `squid.conf` file:
|
||||
> `acl SSL_ports port 22`
|
||||
> `acl Safe_ports port 22`
|
||||
|
||||
#### Proxy exclusion
|
||||
|
||||
The `BALENARC_NO_PROXY` variable may be used to exclude specified destinations from proxying.
|
||||
|
||||
> * This feature requires balena CLI version 11.30.8 or later. In the case of the npm [installation
|
||||
> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires
|
||||
> Node.js version 10.16.0 or later.
|
||||
> * To exclude a `balena ssh` target from proxying (IP address or `.local` hostname), the
|
||||
> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable.
|
||||
|
||||
By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4
|
||||
addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` hostnames are excluded
|
||||
from proxying. Other hostnames that resolve to private IPv4 addresses are **not** excluded by
|
||||
default, because matching takes place before name resolution.
|
||||
|
||||
`localhost` and `127.0.0.1` are always excluded from proxying, regardless of the value of
|
||||
BALENARC_NO_PROXY.
|
||||
|
||||
The format of the `BALENARC_NO_PROXY` environment variable is a comma-separated list of patterns
|
||||
that are matched against hostnames or IP addresses. For example:
|
||||
|
||||
```
|
||||
export BALENARC_NO_PROXY='*.local,dev*.mycompany.com,192.168.*'
|
||||
```
|
||||
|
||||
Matched patterns are excluded from proxying. Wildcard expressions are documented at
|
||||
[matcher](https://www.npmjs.com/package/matcher#usage). Matching takes place _before_ name
|
||||
resolution, so a pattern like `'192.168.*'` will **not** match a hostname that resolves to an IP
|
||||
address like `192.168.1.2`.
|
||||
|
||||
## Command reference documentation
|
||||
|
||||
@ -87,6 +140,22 @@ If you come across any problems or would like to get in touch:
|
||||
* For bug reports or feature requests,
|
||||
[have a look at the GitHub issues or create a new one](https://github.com/balena-io/balena-cli/issues/).
|
||||
|
||||
## Deprecation policy
|
||||
|
||||
The balena CLI uses [semver versioning](https://semver.org/), with the concepts
|
||||
of major, minor and patch version releases.
|
||||
|
||||
The latest release of the previous major version of the balena CLI will remain
|
||||
compatible with the balenaCloud backend services for one year from the date when
|
||||
the next major version is released. For example, balena CLI v10.17.5, as the
|
||||
latest v10 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v11.0.0 is released.
|
||||
|
||||
At the end of this period, the older major version is considered deprecated and
|
||||
some of the functionality that depends on balenaCloud services may stop working
|
||||
at any time.
|
||||
Users are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
## Contributing (including editing documentation files)
|
||||
|
||||
Please have a look at the [CONTRIBUTING.md](./CONTRIBUTING.md) file for some guidance before
|
||||
|
@ -18,7 +18,7 @@
|
||||
import { run as oclifRun } from '@oclif/dev-cli';
|
||||
import * as archiver from 'archiver';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { execFile, spawn } from 'child_process';
|
||||
import { execFile } from 'child_process';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as filehound from 'filehound';
|
||||
import * as fs from 'fs-extra';
|
||||
@ -27,15 +27,17 @@ import * as path from 'path';
|
||||
import { exec as execPkg } from 'pkg';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as semver from 'semver';
|
||||
import * as shellEscape from 'shell-escape';
|
||||
import * as util from 'util';
|
||||
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
// Note: the following 'tslint disable' line was only required to
|
||||
// satisfy ts-node under Appveyor's MSYS2 on Windows -- oddly specific.
|
||||
// Maybe something to do with '/' vs '\' in paths in some tslint file.
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
export const packageJSON = require(path.join(ROOT, 'package.json'));
|
||||
import {
|
||||
getSubprocessStdout,
|
||||
loadPackageJson,
|
||||
MSYS2_BASH,
|
||||
ROOT,
|
||||
whichSpawn,
|
||||
} from './utils';
|
||||
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
|
||||
@ -69,34 +71,6 @@ export const finalReleaseAssets: { [platform: string]: string[] } = {
|
||||
linux: [standaloneZips['linux']],
|
||||
};
|
||||
|
||||
const MSYS2_BASH = 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
|
||||
/**
|
||||
* Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
|
||||
* The given argv arguments are escaped using the 'shell-escape' package,
|
||||
* so that backslashes in Windows paths, and other bash-special characters,
|
||||
* are preserved. If argv is not provided, defaults to process.argv, to the
|
||||
* effect that this current (parent) process is re-executed under MSYS2 bash.
|
||||
* This is useful to change the default shell from cmd.exe to MSYS2 bash on
|
||||
* Windows.
|
||||
* @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
|
||||
*/
|
||||
export async function runUnderMsys(argv?: string[]) {
|
||||
const newArgv = argv || process.argv;
|
||||
await new Promise((resolve, reject) => {
|
||||
const args = ['-lc', shellEscape(newArgv)];
|
||||
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
|
||||
child.on('close', code => {
|
||||
if (code) {
|
||||
console.log(`runUnderMsys: child process exited with code ${code}`);
|
||||
reject(code);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the 'pkg' module to create a single large executable file with
|
||||
* the contents of 'node_modules' and the CLI's javascript code.
|
||||
@ -121,17 +95,18 @@ async function buildPkg() {
|
||||
|
||||
await execPkg(args);
|
||||
|
||||
const xpaths: Array<[string, string[]]> = [
|
||||
// [platform, [path, to, file]]
|
||||
['*', ['opn', 'xdg-open']],
|
||||
['darwin', ['denymount', 'bin', 'denymount']],
|
||||
const paths: Array<[string, string[], string[]]> = [
|
||||
// [platform, [source path], [destination path]]
|
||||
['*', ['open', 'xdg-open'], ['xdg-open']],
|
||||
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
|
||||
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
||||
];
|
||||
await Bluebird.map(xpaths, ([platform, xpath]) => {
|
||||
await Bluebird.map(paths, ([platform, source, dest]) => {
|
||||
if (platform === '*' || platform === process.platform) {
|
||||
// eg copy from node_modules/opn/xdg-open to build-bin/xdg-open
|
||||
// eg copy from node_modules/open/xdg-open to build-bin/xdg-open
|
||||
return fs.copy(
|
||||
path.join(ROOT, 'node_modules', ...xpath),
|
||||
path.join(ROOT, 'build-bin', xpath.pop()!),
|
||||
path.join(ROOT, 'node_modules', ...source),
|
||||
path.join(ROOT, 'build-bin', ...dest),
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -143,7 +118,7 @@ async function buildPkg() {
|
||||
|
||||
console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`);
|
||||
|
||||
await Bluebird.map(nativeExtensionPaths, extPath =>
|
||||
await Bluebird.map(nativeExtensionPaths, (extPath) =>
|
||||
fs.copy(
|
||||
extPath,
|
||||
extPath.replace(
|
||||
@ -183,9 +158,7 @@ async function testPkg() {
|
||||
}
|
||||
if (semver.major(process.version) !== pkgNodeMajorVersion) {
|
||||
throw new Error(
|
||||
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${
|
||||
process.version
|
||||
}"`,
|
||||
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
|
||||
);
|
||||
}
|
||||
console.log('Success! (standalone package test successful)');
|
||||
@ -295,7 +268,7 @@ export async function buildOclifInstaller() {
|
||||
}
|
||||
for (const dir of dirs) {
|
||||
console.log(`rimraf(${dir})`);
|
||||
await Bluebird.fromCallback(cb => rimraf(dir, cb));
|
||||
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
|
||||
}
|
||||
console.log('=======================================================');
|
||||
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
|
||||
@ -315,62 +288,25 @@ export async function buildOclifInstaller() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
|
||||
* as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
|
||||
* Wrapper around the npm `catch-uncommitted` package in order to run it
|
||||
* conditionally, only when:
|
||||
* - A CI env var is set (CI=true), and
|
||||
* - The OS is not Windows. (`catch-uncommitted` fails on Windows)
|
||||
*/
|
||||
export function fixPathForMsys(p: string): string {
|
||||
return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the executable at execPath as a child process, and resolve a promise
|
||||
* to the executable's stdout output as a string. Reject the promise if
|
||||
* anything is printed to stderr, or if the child process exits with a
|
||||
* non-zero exit code.
|
||||
* @param execPath Executable path
|
||||
* @param args Command-line argument for the executable
|
||||
*/
|
||||
async function getSubprocessStdout(
|
||||
execPath: string,
|
||||
args: string[],
|
||||
): Promise<string> {
|
||||
const child = spawn(execPath, args);
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = '';
|
||||
child.stdout.on('error', reject);
|
||||
child.stderr.on('error', reject);
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
try {
|
||||
stdout = data.toString();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
try {
|
||||
const stderr = data.toString();
|
||||
|
||||
// ignore any debug lines, but ensure that we parse
|
||||
// every line provided to the stderr stream
|
||||
const lines = _.filter(
|
||||
stderr.trim().split(/\r?\n/),
|
||||
line => !line.startsWith('[debug]'),
|
||||
);
|
||||
if (lines.length > 0) {
|
||||
reject(
|
||||
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.on('exit', (code: number) => {
|
||||
if (code) {
|
||||
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
export async function catchUncommitted(): Promise<void> {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] CI=${process.env.CI} platform=${process.platform}`);
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
['true', 'yes', '1'].includes(process.env.CI.toLowerCase()) &&
|
||||
process.platform !== 'win32'
|
||||
) {
|
||||
await whichSpawn('npx', [
|
||||
'catch-uncommitted',
|
||||
'--catch-no-git',
|
||||
'--skip-node-versionbot-changes',
|
||||
'--ignore-space-at-eol',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import { MarkdownFileParser } from './utils';
|
||||
|
||||
/**
|
||||
@ -32,11 +31,17 @@ const capitanoDoc = {
|
||||
categories: [
|
||||
{
|
||||
title: 'API keys',
|
||||
files: ['build/actions/api-key.js'],
|
||||
files: ['build/actions-oclif/api-key/generate.js'],
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
files: ['build/actions/app.js'],
|
||||
files: [
|
||||
'build/actions-oclif/apps.js',
|
||||
'build/actions-oclif/app/index.js',
|
||||
'build/actions-oclif/app/create.js',
|
||||
'build/actions-oclif/app/rm.js',
|
||||
'build/actions-oclif/app/restart.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
@ -44,7 +49,11 @@ const capitanoDoc = {
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
files: ['build/actions/device.js'],
|
||||
files: [
|
||||
'build/actions/device.js',
|
||||
'build/actions-oclif/devices/supported.js',
|
||||
'build/actions-oclif/device/public-url.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
@ -57,7 +66,11 @@ const capitanoDoc = {
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
files: ['build/actions/tags.js'],
|
||||
files: [
|
||||
'build/actions-oclif/tags.js',
|
||||
'build/actions-oclif/tag/rm.js',
|
||||
'build/actions-oclif/tag/set.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Help and Version',
|
||||
@ -65,7 +78,12 @@ const capitanoDoc = {
|
||||
},
|
||||
{
|
||||
title: 'Keys',
|
||||
files: ['build/actions/keys.js'],
|
||||
files: [
|
||||
'build/actions-oclif/keys.js',
|
||||
'build/actions-oclif/key/index.js',
|
||||
'build/actions-oclif/key/add.js',
|
||||
'build/actions-oclif/key/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
@ -74,14 +92,14 @@ const capitanoDoc = {
|
||||
{
|
||||
title: 'Network',
|
||||
files: [
|
||||
'build/actions/scan.js',
|
||||
'build/actions-oclif/scan.js',
|
||||
'build/actions/ssh.js',
|
||||
'build/actions/tunnel.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
files: ['build/actions/notes.js'],
|
||||
files: ['build/actions-oclif/note.js'],
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
@ -101,7 +119,7 @@ const capitanoDoc = {
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: ['build/actions/settings.js'],
|
||||
files: ['build/actions-oclif/settings.js'],
|
||||
},
|
||||
{
|
||||
title: 'Local',
|
||||
@ -113,7 +131,7 @@ const capitanoDoc = {
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
files: ['build/actions/join.js', 'build/actions/leave.js'],
|
||||
files: ['build/actions-oclif/join.js', 'build/actions-oclif/leave.js'],
|
||||
},
|
||||
{
|
||||
title: 'Utilities',
|
||||
@ -146,6 +164,7 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
|
||||
mdParser.getSectionOfTitle('Installation'),
|
||||
mdParser.getSectionOfTitle('Getting Started'),
|
||||
mdParser.getSectionOfTitle('Support, FAQ and troubleshooting'),
|
||||
mdParser.getSectionOfTitle('Deprecation policy'),
|
||||
]);
|
||||
capitanoDoc.introduction = sections.join('\n');
|
||||
return capitanoDoc;
|
||||
|
@ -57,17 +57,20 @@ function importCapitanoCommands(jsFilename: string): CapitanoCommand[] {
|
||||
const commands: CapitanoCommand[] = [];
|
||||
|
||||
if (actions.signature) {
|
||||
commands.push(_.omit(actions, 'action'));
|
||||
commands.push(_.omit(actions, 'action') as any);
|
||||
} else {
|
||||
for (const actionName of Object.keys(actions)) {
|
||||
const actionCommand = actions[actionName];
|
||||
commands.push(_.omit(actionCommand, 'action'));
|
||||
commands.push(_.omit(actionCommand, 'action') as any);
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
function importOclifCommands(jsFilename: string): OclifCommand[] {
|
||||
// TODO: Currently oclif commands with no `usage` overridden will cause
|
||||
// an error when parsed. This should be improved so that `usage` does not have
|
||||
// to be overridden if not necessary.
|
||||
const command: OclifCommand = require(path.join(process.cwd(), jsFilename))
|
||||
.default as OclifCommand;
|
||||
return [command];
|
||||
|
@ -24,7 +24,7 @@ import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
|
||||
import * as utils from './utils';
|
||||
|
||||
function renderCapitanoCommand(command: CapitanoCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.signature)}`, command.help];
|
||||
const result = [`## ${ent.encode(command.signature)}`, command.help!];
|
||||
|
||||
if (!_.isEmpty(command.options)) {
|
||||
result.push('### Options');
|
||||
@ -33,6 +33,9 @@ function renderCapitanoCommand(command: CapitanoCommand): string[] {
|
||||
if (option == null) {
|
||||
throw new Error(`Undefined option in markdown generation!`);
|
||||
}
|
||||
if (option.description == null) {
|
||||
throw new Error(`Undefined option.description in markdown generation!`);
|
||||
}
|
||||
result.push(
|
||||
`#### ${utils.parseCapitanoOption(option)}`,
|
||||
option.description,
|
||||
@ -52,7 +55,7 @@ function renderOclifCommand(command: OclifCommand): string[] {
|
||||
result.push(description);
|
||||
|
||||
if (!_.isEmpty(command.examples)) {
|
||||
result.push('Examples:', command.examples!.map(v => `\t${v}`).join('\n'));
|
||||
result.push('Examples:', command.examples!.map((v) => `\t${v}`).join('\n'));
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.args)) {
|
||||
@ -103,7 +106,7 @@ function renderToc(categories: Category[]): string[] {
|
||||
result.push(`- ${category.title}`);
|
||||
result.push(
|
||||
category.commands
|
||||
.map(command => {
|
||||
.map((command) => {
|
||||
const signature =
|
||||
typeof command === 'object'
|
||||
? command.signature // Capitano
|
||||
@ -136,10 +139,7 @@ function sortCommands(doc: Document): void {
|
||||
(cmd: CapitanoCommand | OclifCommand, x: string) =>
|
||||
typeof cmd === 'object' // Capitano vs oclif command
|
||||
? cmd.signature.replace(/\W+/g, ' ').includes(x)
|
||||
: (cmd.usage || '')
|
||||
.toString()
|
||||
.replace(/\W+/g, ' ')
|
||||
.includes(x),
|
||||
: (cmd.usage || '').toString().replace(/\W+/g, ' ').includes(x),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -18,7 +18,6 @@
|
||||
import { OptionDefinition } from 'capitano';
|
||||
import * as ent from 'ent';
|
||||
import * as fs from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import * as readline from 'readline';
|
||||
|
||||
export function getOptionPrefix(signature: string) {
|
||||
@ -36,11 +35,11 @@ export function getOptionSignature(signature: string) {
|
||||
export function parseCapitanoOption(option: OptionDefinition): string {
|
||||
let result = getOptionSignature(option.signature);
|
||||
|
||||
if (_.isArray(option.alias)) {
|
||||
if (Array.isArray(option.alias)) {
|
||||
for (const alias of option.alias) {
|
||||
result += `, ${getOptionSignature(alias)}`;
|
||||
}
|
||||
} else if (_.isString(option.alias)) {
|
||||
} else if (typeof option.alias === 'string') {
|
||||
result += `, ${getOptionSignature(option.alias)}`;
|
||||
}
|
||||
|
||||
@ -94,7 +93,7 @@ export class MarkdownFileParser {
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
rl.on('line', line => {
|
||||
rl.on('line', (line) => {
|
||||
// try to match a line like "## Getting Started", where the number
|
||||
// of '#' characters is the sectionLevel ('##' -> 2), and the
|
||||
// sectionTitle is "Getting Started"
|
||||
@ -127,9 +126,7 @@ export class MarkdownFileParser {
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Markdown section not found: title="${title}" file="${
|
||||
this.mdFilePath
|
||||
}"`,
|
||||
`Markdown section not found: title="${title}" file="${this.mdFilePath}"`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
84
automation/check-doc.js
Normal file
84
automation/check-doc.js
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { stripIndent } = require('common-tags');
|
||||
const _ = require('lodash');
|
||||
const { fs } = require('mz');
|
||||
const path = require('path');
|
||||
const simplegit = require('simple-git/promise');
|
||||
|
||||
const ROOT = path.normalize(path.join(__dirname, '..'));
|
||||
|
||||
/**
|
||||
* Compare the timestamp of cli.markdown with the timestamp of staged files,
|
||||
* issuing an error if cli.markdown is older.
|
||||
* If cli.markdown does not require updating and the developer cannot run
|
||||
* `npm run build` on their laptop, the error message suggests a workaround
|
||||
* using `touch`.
|
||||
*/
|
||||
async function checkBuildTimestamps() {
|
||||
const git = simplegit(ROOT);
|
||||
const docFile = path.join(ROOT, 'doc', 'cli.markdown');
|
||||
const [docStat, gitStatus] = await Promise.all([
|
||||
fs.stat(docFile),
|
||||
git.status(),
|
||||
]);
|
||||
const stagedFiles = _.uniq([
|
||||
...gitStatus.created,
|
||||
...gitStatus.staged,
|
||||
...gitStatus.renamed.map((o) => o.to),
|
||||
])
|
||||
// select only staged files that start with lib/ or typings/
|
||||
.filter((f) => f.match(/^(lib|typings)[/\\]/))
|
||||
.map((f) => path.join(ROOT, f));
|
||||
|
||||
const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));
|
||||
fStats.forEach((fStat, index) => {
|
||||
if (fStat.mtimeMs > docStat.mtimeMs) {
|
||||
const fPath = stagedFiles[index];
|
||||
throw new Error(stripIndent`
|
||||
--------------------------------------------------------------------------------
|
||||
ERROR: at least one staged file: "${fPath}"
|
||||
has a more recent modification timestamp than the documentation file:
|
||||
"${docFile}"
|
||||
|
||||
This probably means that \`npm run build\` or \`npm test\` have not been executed,
|
||||
and this error can be fixed by doing so. Running \`npm run build\` or \`npm test\`
|
||||
before commiting is required in order to update the CLI markdown documentation
|
||||
(in case any command-line options were updated, added or removed) and also to
|
||||
catch Typescript type check errors sooner and reduce overall waiting time, given
|
||||
that the CI build/tests are currently rather lengthy.
|
||||
|
||||
If you need/wish to bypass this check without running \`npm run build\`, run:
|
||||
npx touch -am "${docFile}"
|
||||
and then try again.
|
||||
--------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await checkBuildTimestamps();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
@ -7,16 +7,18 @@
|
||||
* We don't `require('semver')` to allow this script to be run as a npm
|
||||
* 'preinstall' hook, at which point no dependencies have been installed.
|
||||
*/
|
||||
function semverGte(v1, v2) {
|
||||
let v1Array, v2Array; // number[]
|
||||
try {
|
||||
const [, major1, minor1, patch1] = /v?(\d+)\.(\d+).(\d+)/.exec(v1);
|
||||
const [, major2, minor2, patch2] = /v?(\d+)\.(\d+).(\d+)/.exec(v2);
|
||||
v1Array = [parseInt(major1), parseInt(minor1), parseInt(patch1)];
|
||||
v2Array = [parseInt(major2), parseInt(minor2), parseInt(patch2)];
|
||||
} catch (err) {
|
||||
throw new Error(`Invalid semver versions: '${v1}' or '${v2}'`);
|
||||
function parseSemver(version) {
|
||||
const match = /v?(\d+)\.(\d+).(\d+)/.exec(version);
|
||||
if (match == null) {
|
||||
throw new Error(`Invalid semver version: ${version}`);
|
||||
}
|
||||
const [, major, minor, patch] = match;
|
||||
return [parseInt(major, 10), parseInt(minor, 10), parseInt(patch, 10)];
|
||||
}
|
||||
|
||||
function semverGte(v1, v2) {
|
||||
let v1Array = parseSemver(v1);
|
||||
let v2Array = parseSemver(v2);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (v1Array[i] < v2Array[i]) {
|
||||
return false;
|
||||
@ -27,29 +29,9 @@ function semverGte(v1, v2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function _testSemverGet() {
|
||||
const assert = require('assert').strict;
|
||||
assert(semverGte('6.4.1', '6.4.1'));
|
||||
assert(semverGte('6.4.1', 'v6.4.1'));
|
||||
assert(semverGte('v6.4.1', '6.4.1'));
|
||||
assert(semverGte('v6.4.1', 'v6.4.1'));
|
||||
assert(semverGte('6.4.1', '6.4.0'));
|
||||
assert(semverGte('6.4.1', '6.3.1'));
|
||||
assert(semverGte('6.4.1', '5.4.1'));
|
||||
assert(!semverGte('6.4.1', '6.4.2'));
|
||||
assert(!semverGte('6.4.1', '6.5.1'));
|
||||
assert(!semverGte('6.4.1', '7.4.1'));
|
||||
|
||||
assert(semverGte('v6.4.1', 'v4.0.0'));
|
||||
assert(!semverGte('v6.4.1', 'v6.9.0'));
|
||||
assert(!semverGte('v6.4.1', 'v7.0.0'));
|
||||
}
|
||||
|
||||
function checkNpmVersion() {
|
||||
const execSync = require('child_process').execSync;
|
||||
const npmVersion = execSync('npm --version')
|
||||
.toString()
|
||||
.trim();
|
||||
const npmVersion = execSync('npm --version').toString().trim();
|
||||
const requiredVersion = '6.9.0';
|
||||
if (!semverGte(npmVersion, requiredVersion)) {
|
||||
// In case you take issue with the error message below:
|
||||
|
@ -14,6 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as semver from 'semver';
|
||||
@ -59,24 +60,17 @@ export async function release() {
|
||||
}
|
||||
}
|
||||
|
||||
let cachedOctokit: any;
|
||||
|
||||
/** Return a cached Octokit instance, creating a new one as needed. */
|
||||
function getOctokit(): any {
|
||||
if (cachedOctokit) {
|
||||
return cachedOctokit;
|
||||
}
|
||||
const Octokit = require('@octokit/rest').plugin(
|
||||
const getOctokit = _.once(function () {
|
||||
const Octokit = (require('@octokit/rest') as typeof import('@octokit/rest')).Octokit.plugin(
|
||||
require('@octokit/plugin-throttling'),
|
||||
);
|
||||
return (cachedOctokit = new Octokit({
|
||||
return new Octokit({
|
||||
auth: GITHUB_TOKEN,
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter: number, options: any) => {
|
||||
console.warn(
|
||||
`Request quota exhausted for request ${options.method} ${
|
||||
options.url
|
||||
}`,
|
||||
`Request quota exhausted for request ${options.method} ${options.url}`,
|
||||
);
|
||||
// retries 3 times
|
||||
if (options.request.retryCount < 3) {
|
||||
@ -91,8 +85,8 @@ function getOctokit(): any {
|
||||
);
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract pagination information (current page, total pages, ordinal number)
|
||||
@ -174,9 +168,7 @@ async function updateGitHubReleaseDescriptions(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${
|
||||
cliRelease.id
|
||||
})`;
|
||||
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${cliRelease.id})`;
|
||||
if (cliRelease.draft === true) {
|
||||
console.info(`${skipMsg}: draft release`);
|
||||
continue;
|
||||
@ -201,9 +193,7 @@ async function updateGitHubReleaseDescriptions(
|
||||
}
|
||||
}
|
||||
console.info(
|
||||
`${prefix} updating release "${cliRelease.tag_name}" (${
|
||||
cliRelease.id
|
||||
}) old body="${oldBodyPreview}"`,
|
||||
`${prefix} updating release "${cliRelease.tag_name}" (${cliRelease.id}) old body="${oldBodyPreview}"`,
|
||||
);
|
||||
try {
|
||||
await octokit.repos.updateRelease(updatedRelease);
|
||||
|
@ -20,19 +20,24 @@ import * as _ from 'lodash';
|
||||
import {
|
||||
buildOclifInstaller,
|
||||
buildStandaloneZip,
|
||||
fixPathForMsys,
|
||||
ROOT,
|
||||
runUnderMsys,
|
||||
catchUncommitted,
|
||||
} from './build-bin';
|
||||
import {
|
||||
release,
|
||||
updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
} from './deploy-bin';
|
||||
import { fixPathForMsys, ROOT, runUnderMsys } from './utils';
|
||||
|
||||
// DEBUG set to falsy for negative values else is truthy
|
||||
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
process.env.DEBUG?.toLowerCase(),
|
||||
)
|
||||
? ''
|
||||
: '1';
|
||||
|
||||
function exitWithError(error: Error | string): never {
|
||||
console.error(`Error: ${error}`);
|
||||
process.exit(1);
|
||||
throw error; // to please the Typescript compiler
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,9 +59,10 @@ export async function run(args?: string[]) {
|
||||
if (_.isEmpty(args)) {
|
||||
return exitWithError('missing command-line arguments');
|
||||
}
|
||||
const commands: { [cmd: string]: () => void } = {
|
||||
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
||||
'build:installer': buildOclifInstaller,
|
||||
'build:standalone': buildStandaloneZip,
|
||||
'catch-uncommitted': catchUncommitted,
|
||||
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
release,
|
||||
};
|
||||
|
@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2017",
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"preserveConstEnums": true,
|
||||
"removeComments": true,
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"typeRoots" : [
|
||||
"../node_modules/@types",
|
||||
"../typings"
|
||||
]
|
||||
}
|
||||
}
|
140
automation/update-module.ts
Normal file
140
automation/update-module.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { exec } from 'child_process';
|
||||
import * as semver from 'semver';
|
||||
|
||||
const changeTypes = ['major', 'minor', 'patch'] as const;
|
||||
|
||||
const validateChangeType = (maybeChangeType: string = 'minor') => {
|
||||
maybeChangeType = maybeChangeType.toLowerCase();
|
||||
switch (maybeChangeType) {
|
||||
case 'patch':
|
||||
case 'minor':
|
||||
case 'major':
|
||||
return maybeChangeType;
|
||||
default:
|
||||
console.error(`Invalid change type: '${maybeChangeType}'`);
|
||||
return process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const compareSemverChangeType = (oldVersion: string, newVersion: string) => {
|
||||
const oldSemver = semver.parse(oldVersion)!;
|
||||
const newSemver = semver.parse(newVersion)!;
|
||||
|
||||
for (const changeType of changeTypes) {
|
||||
if (oldSemver[changeType] !== newSemver[changeType]) {
|
||||
return changeType;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const run = async (cmd: string) => {
|
||||
console.info(`Running '${cmd}'`);
|
||||
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
||||
const p = exec(cmd, { encoding: 'utf8' }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
p.stdout.pipe(process.stdout);
|
||||
p.stderr.pipe(process.stderr);
|
||||
});
|
||||
};
|
||||
|
||||
const getVersion = async (module: string): Promise<string> => {
|
||||
const { stdout } = await run(`npm ls --json --depth 0 ${module}`);
|
||||
return JSON.parse(stdout).dependencies[module].version;
|
||||
};
|
||||
|
||||
interface Upstream {
|
||||
repo: string;
|
||||
url: string;
|
||||
module?: string;
|
||||
}
|
||||
|
||||
const getUpstreams = async () => {
|
||||
const fs = await import('fs');
|
||||
const repoYaml = fs.readFileSync(__dirname + '/../repo.yml', 'utf8');
|
||||
|
||||
const yaml = await import('js-yaml');
|
||||
const { upstream } = yaml.safeLoad(repoYaml) as {
|
||||
upstream: Upstream[];
|
||||
};
|
||||
|
||||
return upstream;
|
||||
};
|
||||
|
||||
const printUsage = (upstreams: Upstream[], upstreamName: string) => {
|
||||
console.error(
|
||||
`
|
||||
Usage: npm run update ${upstreamName} $version [$changeType=minor]
|
||||
|
||||
Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
|
||||
`,
|
||||
);
|
||||
return process.exit(1);
|
||||
};
|
||||
|
||||
// TODO: Drop the wrapper function once we move to TS 3.8,
|
||||
// which will support top level await.
|
||||
async function main() {
|
||||
const upstreams = await getUpstreams();
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
return printUsage(upstreams, '$upstreamName');
|
||||
}
|
||||
|
||||
const upstreamName = process.argv[2];
|
||||
|
||||
const upstream = upstreams.find((v) => v.repo === upstreamName);
|
||||
|
||||
if (!upstream) {
|
||||
console.error(
|
||||
`Invalid upstream name '${upstreamName}', valid options: ${upstreams
|
||||
.map(({ repo }) => repo)
|
||||
.join(', ')}`,
|
||||
);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
printUsage(upstreams, upstreamName);
|
||||
}
|
||||
|
||||
const packageName = upstream.module || upstream.repo;
|
||||
|
||||
const oldVersion = await getVersion(packageName);
|
||||
await run(`npm install ${packageName}@${process.argv[3]}`);
|
||||
const newVersion = await getVersion(packageName);
|
||||
if (newVersion === oldVersion) {
|
||||
console.error(`Already on version '${newVersion}'`);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
|
||||
const semverChangeType = compareSemverChangeType(oldVersion, newVersion);
|
||||
|
||||
const changeType = process.argv[4]
|
||||
? // if the caller specified a change type, use that one
|
||||
validateChangeType(process.argv[4])
|
||||
: // use the same change type as in the dependency, but avoid major bumps
|
||||
semverChangeType && semverChangeType !== 'major'
|
||||
? semverChangeType
|
||||
: 'minor';
|
||||
console.log(`Using Change-type: ${changeType}`);
|
||||
|
||||
let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD');
|
||||
currentBranch = currentBranch.trim();
|
||||
console.log(`Currenty on branch: '${currentBranch}'`);
|
||||
if (currentBranch === 'master') {
|
||||
await run(`git checkout -b "update-${upstreamName}-${newVersion}"`);
|
||||
}
|
||||
|
||||
await run(`git add package.json npm-shrinkwrap.json`);
|
||||
await run(
|
||||
`git commit --message "Update ${upstreamName} to ${newVersion}" --message "Update ${upstreamName} from ${oldVersion} to ${newVersion}" --message "Change-type: ${changeType}"`,
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
174
automation/utils.ts
Normal file
174
automation/utils.ts
Normal file
@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as shellEscape from 'shell-escape';
|
||||
|
||||
export const MSYS2_BASH = 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
|
||||
export function loadPackageJson() {
|
||||
return require(path.join(ROOT, 'package.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
|
||||
* as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
|
||||
*/
|
||||
export function fixPathForMsys(p: string): string {
|
||||
return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
|
||||
* The given argv arguments are escaped using the 'shell-escape' package,
|
||||
* so that backslashes in Windows paths, and other bash-special characters,
|
||||
* are preserved. If argv is not provided, defaults to process.argv, to the
|
||||
* effect that this current (parent) process is re-executed under MSYS2 bash.
|
||||
* This is useful to change the default shell from cmd.exe to MSYS2 bash on
|
||||
* Windows.
|
||||
* @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
|
||||
*/
|
||||
export async function runUnderMsys(argv?: string[]) {
|
||||
const newArgv = argv || process.argv;
|
||||
await new Promise((resolve, reject) => {
|
||||
const args = ['-lc', shellEscape(newArgv)];
|
||||
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
|
||||
child.on('close', (code) => {
|
||||
if (code) {
|
||||
console.log(`runUnderMsys: child process exited with code ${code}`);
|
||||
reject(code);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the executable at execPath as a child process, and resolve a promise
|
||||
* to the executable's stdout output as a string. Reject the promise if
|
||||
* anything is printed to stderr, or if the child process exits with a
|
||||
* non-zero exit code.
|
||||
* @param execPath Executable path
|
||||
* @param args Command-line argument for the executable
|
||||
*/
|
||||
export async function getSubprocessStdout(
|
||||
execPath: string,
|
||||
args: string[],
|
||||
): Promise<string> {
|
||||
const child = spawn(execPath, args);
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = '';
|
||||
child.stdout.on('error', reject);
|
||||
child.stderr.on('error', reject);
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
try {
|
||||
stdout = data.toString();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
try {
|
||||
const stderr = data.toString();
|
||||
|
||||
// ignore any debug lines, but ensure that we parse
|
||||
// every line provided to the stderr stream
|
||||
const lines = _.filter(
|
||||
stderr.trim().split(/\r?\n/),
|
||||
(line) => !line.startsWith('[debug]'),
|
||||
);
|
||||
if (lines.length > 0) {
|
||||
reject(
|
||||
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.on('exit', (code: number) => {
|
||||
if (code) {
|
||||
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handling wrapper around the npm `which` package:
|
||||
* "Like the unix which utility. Finds the first instance of a specified
|
||||
* executable in the PATH environment variable. Does not cache the results,
|
||||
* so hash -r is not needed when the PATH changes."
|
||||
*
|
||||
* @param program Basename of a program, for example 'ssh'
|
||||
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
|
||||
*/
|
||||
export async function which(program: string): Promise<string> {
|
||||
const whichMod = await import('which');
|
||||
let programPath: string;
|
||||
try {
|
||||
programPath = await whichMod(program);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
throw new Error(`'${program}' program not found. Is it installed?`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return programPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call which(programName) and spawn() with the given arguments. Throw an error
|
||||
* if the process exit code is not zero.
|
||||
*/
|
||||
export async function whichSpawn(
|
||||
programName: string,
|
||||
args: string[],
|
||||
): Promise<void> {
|
||||
const program = await which(programName);
|
||||
let error: Error | undefined;
|
||||
let exitCode: number | undefined;
|
||||
try {
|
||||
exitCode = await new Promise<number>((resolve, reject) => {
|
||||
try {
|
||||
spawn(program, args, { stdio: 'inherit' })
|
||||
.on('error', reject)
|
||||
.on('close', resolve);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
if (error || exitCode) {
|
||||
const msg = [
|
||||
`${programName} failed with exit code ${exitCode}:`,
|
||||
`"${program}" [${args}]`,
|
||||
];
|
||||
if (error) {
|
||||
msg.push(`${error}`);
|
||||
}
|
||||
throw new Error(msg.join('\n'));
|
||||
}
|
||||
}
|
@ -4,6 +4,9 @@
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
// Disable oclif registering ts-node
|
||||
process.env.OCLIF_TS_NODE = 0;
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheFile: __dirname + '/.fast-boot.json'
|
||||
|
@ -13,7 +13,6 @@ process.env.UV_THREADPOOL_SIZE = '64';
|
||||
require('fast-boot2').start({
|
||||
cacheFile: '.fast-boot.json',
|
||||
});
|
||||
require('coffeescript/register');
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
|
127
coffeelint.json
127
coffeelint.json
@ -1,127 +0,0 @@
|
||||
{
|
||||
"coffeescript_error": {
|
||||
"level": "error"
|
||||
},
|
||||
"arrow_spacing": {
|
||||
"name": "arrow_spacing",
|
||||
"level": "error"
|
||||
},
|
||||
"no_tabs": {
|
||||
"name": "no_tabs",
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_trailing_whitespace": {
|
||||
"name": "no_trailing_whitespace",
|
||||
"level": "error",
|
||||
"allowed_in_comments": false,
|
||||
"allowed_in_empty_lines": false
|
||||
},
|
||||
"max_line_length": {
|
||||
"name": "max_line_length",
|
||||
"value": 120,
|
||||
"level": "error",
|
||||
"limitComments": true
|
||||
},
|
||||
"line_endings": {
|
||||
"name": "line_endings",
|
||||
"level": "ignore",
|
||||
"value": "unix"
|
||||
},
|
||||
"no_trailing_semicolons": {
|
||||
"name": "no_trailing_semicolons",
|
||||
"level": "error"
|
||||
},
|
||||
"indentation": {
|
||||
"name": "indentation",
|
||||
"value": 1,
|
||||
"level": "error"
|
||||
},
|
||||
"camel_case_classes": {
|
||||
"name": "camel_case_classes",
|
||||
"level": "error"
|
||||
},
|
||||
"colon_assignment_spacing": {
|
||||
"name": "colon_assignment_spacing",
|
||||
"level": "error",
|
||||
"spacing": {
|
||||
"left": 0,
|
||||
"right": 1
|
||||
}
|
||||
},
|
||||
"no_implicit_braces": {
|
||||
"name": "no_implicit_braces",
|
||||
"level": "ignore",
|
||||
"strict": false
|
||||
},
|
||||
"no_plusplus": {
|
||||
"name": "no_plusplus",
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_throwing_strings": {
|
||||
"name": "no_throwing_strings",
|
||||
"level": "error"
|
||||
},
|
||||
"no_backticks": {
|
||||
"name": "no_backticks",
|
||||
"level": "error"
|
||||
},
|
||||
"no_implicit_parens": {
|
||||
"name": "no_implicit_parens",
|
||||
"strict": false,
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_empty_param_list": {
|
||||
"name": "no_empty_param_list",
|
||||
"level": "error"
|
||||
},
|
||||
"no_stand_alone_at": {
|
||||
"name": "no_stand_alone_at",
|
||||
"level": "ignore"
|
||||
},
|
||||
"space_operators": {
|
||||
"name": "space_operators",
|
||||
"level": "error"
|
||||
},
|
||||
"duplicate_key": {
|
||||
"name": "duplicate_key",
|
||||
"level": "error"
|
||||
},
|
||||
"empty_constructor_needs_parens": {
|
||||
"name": "empty_constructor_needs_parens",
|
||||
"level": "ignore"
|
||||
},
|
||||
"cyclomatic_complexity": {
|
||||
"name": "cyclomatic_complexity",
|
||||
"value": 10,
|
||||
"level": "ignore"
|
||||
},
|
||||
"newlines_after_classes": {
|
||||
"name": "newlines_after_classes",
|
||||
"value": 3,
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_unnecessary_fat_arrows": {
|
||||
"name": "no_unnecessary_fat_arrows",
|
||||
"level": "error"
|
||||
},
|
||||
"missing_fat_arrows": {
|
||||
"name": "missing_fat_arrows",
|
||||
"level": "ignore"
|
||||
},
|
||||
"non_empty_constructor_needs_parens": {
|
||||
"name": "non_empty_constructor_needs_parens",
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_unnecessary_double_quotes": {
|
||||
"name": "no_unnecessary_double_quotes",
|
||||
"level": "error"
|
||||
},
|
||||
"no_debugger": {
|
||||
"name": "no_debugger",
|
||||
"level": "warn"
|
||||
},
|
||||
"no_interpolation_in_single_quotes": {
|
||||
"name": "no_interpolation_in_single_quotes",
|
||||
"level": "error"
|
||||
}
|
||||
}
|
967
doc/cli.markdown
967
doc/cli.markdown
File diff suppressed because it is too large
Load Diff
@ -1,33 +0,0 @@
|
||||
path = require('path')
|
||||
gulp = require('gulp')
|
||||
coffee = require('gulp-coffee')
|
||||
inlinesource = require('gulp-inline-source')
|
||||
shell = require('gulp-shell')
|
||||
packageJSON = require('./package.json')
|
||||
|
||||
OPTIONS =
|
||||
files:
|
||||
coffee: [ 'lib/**/*.coffee', 'gulpfile.coffee' ]
|
||||
app: 'lib/**/*.coffee'
|
||||
tests: 'tests/**/*.spec.js'
|
||||
pages: 'lib/auth/pages/*.ejs'
|
||||
directories:
|
||||
build: 'build/'
|
||||
|
||||
gulp.task 'pages', ->
|
||||
gulp.src(OPTIONS.files.pages)
|
||||
.pipe(inlinesource())
|
||||
.pipe(gulp.dest('build/auth/pages'))
|
||||
|
||||
gulp.task 'coffee', ->
|
||||
gulp.src(OPTIONS.files.app)
|
||||
.pipe(coffee(bare: true, header: true))
|
||||
.pipe(gulp.dest(OPTIONS.directories.build))
|
||||
|
||||
gulp.task 'build', gulp.series [
|
||||
'coffee',
|
||||
'pages'
|
||||
]
|
||||
|
||||
gulp.task 'watch', gulp.series [ 'build' ], ->
|
||||
gulp.watch([ OPTIONS.files.coffee ], [ 'build' ])
|
15
gulpfile.js
Normal file
15
gulpfile.js
Normal file
@ -0,0 +1,15 @@
|
||||
const gulp = require('gulp');
|
||||
const inlinesource = require('gulp-inline-source');
|
||||
|
||||
const OPTIONS = {
|
||||
files: {
|
||||
pages: 'lib/auth/pages/*.ejs',
|
||||
},
|
||||
};
|
||||
|
||||
gulp.task('pages', () =>
|
||||
gulp
|
||||
.src(OPTIONS.files.pages)
|
||||
.pipe(inlinesource())
|
||||
.pipe(gulp.dest('build/auth/pages')),
|
||||
);
|
86
lib/actions-oclif/api-key/generate.ts
Normal file
86
lib/actions-oclif/api-key/generate.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class GenerateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Generate a new balenaCloud API key.
|
||||
|
||||
Generate a new balenaCloud API key for the current user, with the given
|
||||
name. The key will be logged to the console.
|
||||
|
||||
This key can be used to log into the CLI using 'balena login --token <key>',
|
||||
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||
`;
|
||||
public static examples = ['$ balena api-key generate "Jenkins Key"'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'the API key name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'api-key generate <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(GenerateCmd);
|
||||
|
||||
let key;
|
||||
try {
|
||||
key = await getBalenaSdk().models.apiKey.create(params.name);
|
||||
} catch (e) {
|
||||
if (e.name === 'BalenaNotLoggedIn') {
|
||||
throw new ExpectedError(stripIndent`
|
||||
This command cannot be run when logged in with an API key.
|
||||
Please login again with 'balena login' and select an alternative method.
|
||||
`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(stripIndent`
|
||||
Registered api key '${params.name}':
|
||||
|
||||
${key}
|
||||
|
||||
This key will not be shown again, so please save it now.
|
||||
`);
|
||||
}
|
||||
}
|
102
lib/actions-oclif/app/create.ts
Normal file
102
lib/actions-oclif/app/create.ts
Normal file
@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type?: string; // application device type
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create an application.
|
||||
|
||||
Create a new balena application.
|
||||
|
||||
You can specify the application device type with the \`--type\` option.
|
||||
Otherwise, an interactive dropdown will be shown for you to select from.
|
||||
|
||||
You can see a list of supported device types with:
|
||||
|
||||
$ balena devices supported
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena app create MyApp',
|
||||
'$ balena app create MyApp --type raspberry-pi',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app create <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'application device type (Check available types with `balena devices supported`)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
AppCreateCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = await import('../../utils/patterns');
|
||||
|
||||
// Create application
|
||||
const deviceType = options.type || (await patterns.selectDeviceType());
|
||||
let application: import('balena-sdk').Application;
|
||||
try {
|
||||
application = await balena.models.application.create({
|
||||
name: params.name,
|
||||
deviceType,
|
||||
});
|
||||
} catch (err) {
|
||||
// BalenaRequestError: Request error: Unique key constraint violated
|
||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||
throw new ExpectedError(
|
||||
`Error: application "${params.name}" already exists`,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
console.info(
|
||||
`Application created: ${application.slug} (${application.device_type}, id ${application.id})`,
|
||||
);
|
||||
}
|
||||
}
|
75
lib/actions-oclif/app/index.ts
Normal file
75
lib/actions-oclif/app/index.ts
Normal file
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display information about a single application.
|
||||
|
||||
Display detailed information about a single balena application.
|
||||
`;
|
||||
public static examples = ['$ balena app MyApp'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name or numeric ID',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppCmd);
|
||||
|
||||
const application = await getBalenaSdk().models.application.get(
|
||||
tryAsInteger(params.name),
|
||||
);
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(application, [
|
||||
`$${application.app_name}$`,
|
||||
'id',
|
||||
'device_type',
|
||||
'slug',
|
||||
'commit',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
62
lib/actions-oclif/app/restart.ts
Normal file
62
lib/actions-oclif/app/restart.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppRestartCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Restart an application.
|
||||
|
||||
Restart all devices that belongs to a certain application.
|
||||
`;
|
||||
public static examples = ['$ balena app restart MyApp'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name or numeric ID',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app restart <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
|
||||
|
||||
await getBalenaSdk().models.application.restart(tryAsInteger(params.name));
|
||||
}
|
||||
}
|
80
lib/actions-oclif/app/rm.ts
Normal file
80
lib/actions-oclif/app/rm.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove an application.
|
||||
|
||||
Permanently remove a balena application.
|
||||
|
||||
The --yes option may be used to avoid interactive confirmation.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena app rm MyApp',
|
||||
'$ balena app rm MyApp --yes',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name or numeric ID',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app rm <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
AppRmCmd,
|
||||
);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
|
||||
// Confirm
|
||||
await patterns.confirm(
|
||||
options.yes ?? false,
|
||||
`Are you sure you want to delete application ${params.name}?`,
|
||||
);
|
||||
|
||||
// Remove
|
||||
await getBalenaSdk().models.application.remove(tryAsInteger(params.name));
|
||||
}
|
||||
}
|
96
lib/actions-oclif/apps.ts
Normal file
96
lib/actions-oclif/apps.ts
Normal file
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Application } from 'balena-sdk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
import { isV12 } from '../utils/version';
|
||||
|
||||
interface ExtendedApplication extends Application {
|
||||
device_count?: number;
|
||||
online_devices?: number;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export default class AppsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all applications.
|
||||
|
||||
list all your balena applications.
|
||||
|
||||
For detailed information on a particular application,
|
||||
use \`balena app <name> instead\`.
|
||||
`;
|
||||
public static examples = ['$ balena apps'];
|
||||
|
||||
public static usage = 'apps';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
verbose: flags.boolean({
|
||||
char: 'v',
|
||||
description: isV12()
|
||||
? 'No-op since release v12.0.0'
|
||||
: 'add extra columns in the tabular output (SLUG)',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(AppsCmd);
|
||||
|
||||
const _ = await import('lodash');
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Get applications
|
||||
const applications: ExtendedApplication[] = await balena.models.application.getAll(
|
||||
{
|
||||
$select: ['id', 'app_name', 'slug', 'device_type'],
|
||||
$expand: { owns__device: { $select: 'is_online' } },
|
||||
},
|
||||
);
|
||||
|
||||
// Add extended properties
|
||||
applications.forEach((application) => {
|
||||
application.device_count = _.size(application.owns__device);
|
||||
application.online_devices = _.sumBy(application.owns__device, (d) =>
|
||||
d.is_online === true ? 1 : 0,
|
||||
);
|
||||
});
|
||||
|
||||
// Display
|
||||
console.log(
|
||||
getVisuals().table.horizontal(applications, [
|
||||
'id',
|
||||
'app_name',
|
||||
options.verbose || isV12() ? 'slug' : '',
|
||||
'device_type',
|
||||
'online_devices',
|
||||
'device_count',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
149
lib/actions-oclif/device/public-url.ts
Normal file
149
lib/actions-oclif/device/public-url.ts
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { IArg } from '@oclif/parser/lib/args';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
enable: boolean;
|
||||
disable: boolean;
|
||||
status: boolean;
|
||||
help?: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
// Optional hidden arg to support old command format
|
||||
legacyUuid?: string;
|
||||
}
|
||||
|
||||
export default class DevicePublicUrlCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Get or manage the public URL for a device.
|
||||
|
||||
This command will output the current public URL for the
|
||||
specified device. It can also enable or disable the URL,
|
||||
or output the enabled status, using the respective options.
|
||||
|
||||
The old command style 'balena device public-url enable <uuid>'
|
||||
is deprecated, but still supported.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena device public-url 23c73a1',
|
||||
'$ balena device public-url 23c73a1 --enable',
|
||||
'$ balena device public-url 23c73a1 --disable',
|
||||
'$ balena device public-url 23c73a1 --status',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to manage',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
// Optional hidden arg to support old command format
|
||||
name: 'legacyUuid',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device public-url <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
enable: flags.boolean({
|
||||
description: 'enable the public URL',
|
||||
exclusive: ['disable', 'status'],
|
||||
}),
|
||||
disable: flags.boolean({
|
||||
description: 'disable the public URL',
|
||||
exclusive: ['enable', 'status'],
|
||||
}),
|
||||
status: flags.boolean({
|
||||
description: 'determine if public URL is enabled',
|
||||
exclusive: ['enable', 'disable'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DevicePublicUrlCmd,
|
||||
);
|
||||
|
||||
// Legacy command format support.
|
||||
// Previously this command used the following format
|
||||
// (changed due to oclif technicalities):
|
||||
// `balena device public-url enable|disable|status <uuid>`
|
||||
if (params.legacyUuid) {
|
||||
const action = params.uuid;
|
||||
if (!['enable', 'disable', 'status'].includes(action)) {
|
||||
throw new ExpectedError(
|
||||
`Unexpected arguments: ${params.uuid} ${params.legacyUuid}`,
|
||||
);
|
||||
}
|
||||
|
||||
options.enable = action === 'enable';
|
||||
options.disable = action === 'disable';
|
||||
options.status = action === 'status';
|
||||
params.uuid = params.legacyUuid;
|
||||
delete params.legacyUuid;
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
if (options.enable) {
|
||||
// Enable public URL
|
||||
await balena.models.device.enableDeviceUrl(params.uuid);
|
||||
} else if (options.disable) {
|
||||
// Disable public URL
|
||||
await balena.models.device.disableDeviceUrl(params.uuid);
|
||||
} else if (options.status) {
|
||||
// Output bool indicating if public URL enabled
|
||||
const hasUrl = await balena.models.device.hasDeviceUrl(params.uuid);
|
||||
console.log(hasUrl);
|
||||
} else {
|
||||
// Output public URL
|
||||
try {
|
||||
const url = await balena.models.device.getDeviceUrl(params.uuid);
|
||||
console.log(url);
|
||||
} catch (e) {
|
||||
if (e.message.includes('Device is not web accessible')) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Public URL is not enabled for this device.
|
||||
|
||||
To enable, use:
|
||||
balena device public-url ${params.uuid} --enable
|
||||
`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
117
lib/actions-oclif/devices/supported.ts
Normal file
117
lib/actions-oclif/devices/supported.ts
Normal file
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flags } from '@oclif/command';
|
||||
import * as SDK from 'balena-sdk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
import { isV12 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
discontinued: boolean;
|
||||
help: void;
|
||||
json?: boolean;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export default class DevicesSupportedCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||
|
||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||
|
||||
The --verbose option adds extra columns/fields to the output, including the
|
||||
"STATE" column whose values are one of 'beta', 'released' or 'discontinued'.
|
||||
However, 'discontinued' device types are only listed if the '--discontinued'
|
||||
option is used.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents data
|
||||
types like lists and empty strings (for example, the ALIASES column contains a
|
||||
list of zero or more values). The 'jq' utility may be helpful in shell scripts
|
||||
(https://stedolan.github.io/jq/manual/).
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena devices supported',
|
||||
'$ balena devices supported --verbose',
|
||||
'$ balena devices supported -vj',
|
||||
];
|
||||
|
||||
public static usage = (
|
||||
'devices supported ' +
|
||||
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
|
||||
).trim();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
discontinued: flags.boolean({
|
||||
description: 'include "discontinued" device types',
|
||||
}),
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
}),
|
||||
verbose: flags.boolean({
|
||||
char: 'v',
|
||||
description:
|
||||
'add extra columns in the tabular output (ALIASES, ARCH, STATE)',
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
|
||||
let deviceTypes: Array<Partial<SDK.DeviceType>> = await getBalenaSdk()
|
||||
.models.config.getDeviceTypes()
|
||||
.map((d) => {
|
||||
if (d.aliases && d.aliases.length) {
|
||||
// remove aliases that are equal to the slug
|
||||
d.aliases = d.aliases.filter((alias: string) => alias !== d.slug);
|
||||
if (!options.json) {
|
||||
// stringify the aliases array with commas and spaces
|
||||
d.aliases = [d.aliases.join(', ')];
|
||||
}
|
||||
} else {
|
||||
// ensure it is always an array (for the benefit of JSON output)
|
||||
d.aliases = [];
|
||||
}
|
||||
return d;
|
||||
});
|
||||
if (!options.discontinued) {
|
||||
deviceTypes = deviceTypes.filter((dt) => dt.state !== 'DISCONTINUED');
|
||||
}
|
||||
const fields = options.verbose
|
||||
? ['slug', 'aliases', 'arch', 'state', 'name']
|
||||
: isV12()
|
||||
? ['slug', 'aliases', 'arch', 'name']
|
||||
: ['slug', 'name'];
|
||||
deviceTypes = _.sortBy(
|
||||
deviceTypes.map((d) => _.pick(d, fields) as Partial<SDK.DeviceType>),
|
||||
fields,
|
||||
);
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(deviceTypes, null, 4));
|
||||
} else {
|
||||
const visuals = getVisuals();
|
||||
const output = await visuals.table.horizontal(deviceTypes, fields);
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
}
|
147
lib/actions-oclif/env/add.ts
vendored
147
lib/actions-oclif/env/add.ts
vendored
@ -15,18 +15,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { flags } from '@oclif/command';
|
||||
import * as BalenaSdk from 'balena-sdk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../../command';
|
||||
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
device?: string;
|
||||
application?: string; // application name
|
||||
device?: string; // device UUID
|
||||
help: void;
|
||||
quiet: boolean;
|
||||
service?: string; // service name
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -36,22 +41,37 @@ interface ArgsDef {
|
||||
|
||||
export default class EnvAddCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Add an environment or config variable to an application or device.
|
||||
Add an environment or config variable to an application, device or service.
|
||||
|
||||
Add an environment or config variable to an application or device, as selected
|
||||
by the respective command-line options.
|
||||
Add an environment or config variable to an application, device or service,
|
||||
as selected by the respective command-line options. Either the --application
|
||||
or the --device option must be provided, and either may be be used alongside
|
||||
the --service option to define a service-specific variable. (A service is an
|
||||
application container in a "microservices" application.) When the --service
|
||||
option is used in conjunction with the --device option, the service variable
|
||||
applies to the selected device only. Otherwise, it applies to all devices of
|
||||
the selected application (i.e., the application's fleet). If the --service
|
||||
option is omitted, the variable applies to all services.
|
||||
|
||||
If VALUE is omitted, the CLI will attempt to use the value of the environment
|
||||
variable of same name in the CLI process' environment. In this case, a warning
|
||||
message will be printed. Use \`--quiet\` to suppress it.
|
||||
|
||||
Service-specific variables are not currently supported. The given command line
|
||||
examples variables that apply to all services in an app or device.
|
||||
'BALENA_' or 'RESIN_' are reserved variable name prefixes used to identify
|
||||
"configuration variables". Configuration variables control balena platform
|
||||
features and are treated specially by balenaOS and the balena supervisor
|
||||
running on devices. They are also stored differently in the balenaCloud API
|
||||
database. Configuration variables cannot be set for specific services,
|
||||
therefore the --service option cannot be used when the variable name starts
|
||||
with a reserved prefix. When defining custom application variables, please
|
||||
avoid the reserved prefixes.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena env add TERM --application MyApp',
|
||||
'$ balena env add EDITOR vim --application MyApp',
|
||||
'$ balena env add EDITOR vim --application MyApp --service MyService',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6 --service MyService',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
@ -64,7 +84,7 @@ export default class EnvAddCmd extends Command {
|
||||
name: 'value',
|
||||
required: false,
|
||||
description:
|
||||
"variable value; if omitted, use value from CLI's environment",
|
||||
"variable value; if omitted, use value from this process' environment",
|
||||
},
|
||||
];
|
||||
|
||||
@ -73,10 +93,11 @@ export default class EnvAddCmd extends Command {
|
||||
'env add ' + new CommandHelp({ args: EnvAddCmd.args }).defaultUsage();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: _.assign({ exclusive: ['device'] }, cf.application),
|
||||
device: _.assign({ exclusive: ['application'] }, cf.device),
|
||||
application: { exclusive: ['device'], ...cf.application },
|
||||
device: { exclusive: ['application'], ...cf.device },
|
||||
help: cf.help,
|
||||
quiet: cf.quiet,
|
||||
service: cf.service,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
@ -84,15 +105,21 @@ export default class EnvAddCmd extends Command {
|
||||
EnvAddCmd,
|
||||
);
|
||||
const cmd = this;
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const { exitWithExpectedError } = await import('../../utils/patterns');
|
||||
|
||||
if (!options.application && !options.device) {
|
||||
throw new ExpectedError(
|
||||
'Either the --application or the --device option must always be used',
|
||||
);
|
||||
}
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
if (params.value == null) {
|
||||
params.value = process.env[params.name];
|
||||
|
||||
if (params.value == null) {
|
||||
throw new Error(
|
||||
`Environment value not found for variable: ${params.name}`,
|
||||
`Value not found for environment variable: ${params.name}`,
|
||||
);
|
||||
} else if (!options.quiet) {
|
||||
cmd.warn(
|
||||
@ -101,12 +128,26 @@ export default class EnvAddCmd extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
const reservedPrefixes = await getReservedPrefixes();
|
||||
const isConfigVar = _.some(reservedPrefixes, prefix =>
|
||||
const balena = getBalenaSdk();
|
||||
const reservedPrefixes = await getReservedPrefixes(balena);
|
||||
const isConfigVar = _.some(reservedPrefixes, (prefix) =>
|
||||
_.startsWith(params.name, prefix),
|
||||
);
|
||||
const varType = isConfigVar ? 'configVar' : 'envVar';
|
||||
|
||||
if (options.service) {
|
||||
if (isConfigVar) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Configuration variables prefixed with "${reservedPrefixes.join(
|
||||
'" or "',
|
||||
)}" cannot be set per service.
|
||||
Hint: remove the --service option or rename the variable.
|
||||
`);
|
||||
}
|
||||
await setServiceVars(balena, params, options);
|
||||
return;
|
||||
}
|
||||
|
||||
const varType = isConfigVar ? 'configVar' : 'envVar';
|
||||
if (options.application) {
|
||||
await balena.models.application[varType].set(
|
||||
options.application,
|
||||
@ -119,16 +160,78 @@ export default class EnvAddCmd extends Command {
|
||||
params.name,
|
||||
params.value,
|
||||
);
|
||||
} else {
|
||||
exitWithExpectedError('You must specify an application or device');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getReservedPrefixes(): Promise<string[]> {
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const settings = await balena.settings.getAll();
|
||||
/**
|
||||
* Add service variables for a device or application.
|
||||
*/
|
||||
async function setServiceVars(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
params: ArgsDef,
|
||||
options: FlagsDef,
|
||||
) {
|
||||
if (options.application) {
|
||||
const serviceId = await getServiceIdForApp(
|
||||
sdk,
|
||||
options.application,
|
||||
options.service!,
|
||||
);
|
||||
await sdk.models.service.var.set(serviceId, params.name, params.value!);
|
||||
} else {
|
||||
const { getDeviceAndAppFromUUID } = await import('../../utils/cloud');
|
||||
const [device, app] = await getDeviceAndAppFromUUID(
|
||||
sdk,
|
||||
options.device!,
|
||||
['id'],
|
||||
['app_name'],
|
||||
);
|
||||
const serviceId = await getServiceIdForApp(
|
||||
sdk,
|
||||
app.app_name,
|
||||
options.service!,
|
||||
);
|
||||
await sdk.models.device.serviceVar.set(
|
||||
device.id,
|
||||
serviceId,
|
||||
params.name,
|
||||
params.value!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sevice ID for the given app name and service name.
|
||||
*/
|
||||
async function getServiceIdForApp(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
appName: string,
|
||||
serviceName: string,
|
||||
): Promise<number> {
|
||||
let serviceId: number | undefined;
|
||||
const services = await sdk.models.service.getAllByApplication(appName, {
|
||||
$filter: { service_name: serviceName },
|
||||
});
|
||||
if (!_.isEmpty(services)) {
|
||||
serviceId = services[0].id;
|
||||
}
|
||||
if (serviceId === undefined) {
|
||||
throw new ExpectedError(
|
||||
`Cannot find service ${serviceName} for application ${appName}`,
|
||||
);
|
||||
}
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of variable name prefixes like: [ 'RESIN_', 'BALENA_' ].
|
||||
* These prefixes can be used to identify "configuration variables".
|
||||
*/
|
||||
async function getReservedPrefixes(
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
): Promise<string[]> {
|
||||
const settings = await balena.settings.getAll();
|
||||
const response = await balena.request.send({
|
||||
baseUrl: settings.apiUrl,
|
||||
url: '/config/vars',
|
||||
|
54
lib/actions-oclif/env/rename.ts
vendored
54
lib/actions-oclif/env/rename.ts
vendored
@ -14,16 +14,22 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ec from '../../utils/env-common';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
config: boolean;
|
||||
device: boolean;
|
||||
service: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -34,59 +40,57 @@ interface ArgsDef {
|
||||
|
||||
export default class EnvRenameCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Change the value of an environment variable for an app or device.
|
||||
Change the value of a config or env var for an app, device or service.
|
||||
|
||||
Change the value of an environment variable for an application or device,
|
||||
as selected by the '--device' option. The variable is identified by its
|
||||
database ID, rather than its name. The 'balena envs' command can be used
|
||||
to list the variable's ID.
|
||||
Change the value of a configuration or environment variable for an application,
|
||||
device or service, as selected by command-line options.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples modify variables that apply to all services in an app or device.
|
||||
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena env rename 376 emacs',
|
||||
'$ balena env rename 376 emacs --device',
|
||||
'$ balena env rename 123123 emacs',
|
||||
'$ balena env rename 234234 emacs --service',
|
||||
'$ balena env rename 345345 emacs --device',
|
||||
'$ balena env rename 456456 emacs --device --service',
|
||||
'$ balena env rename 567567 1 --config',
|
||||
'$ balena env rename 678678 1 --device --config',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'id',
|
||||
required: true,
|
||||
description: 'environment variable numeric database ID',
|
||||
parse: input => parseInt(input, 10),
|
||||
description: "variable's numeric database ID",
|
||||
parse: (input) => parseAsInteger(input, 'id'),
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
required: true,
|
||||
description:
|
||||
"variable value; if omitted, use value from CLI's environment",
|
||||
"variable value; if omitted, use value from this process' environment",
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
|
||||
// hardcoded 'env rename' to avoid oclif's 'env:rename' topic syntax
|
||||
public static usage =
|
||||
'env rename ' + new CommandHelp({ args: EnvRenameCmd.args }).defaultUsage();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
device: flags.boolean({
|
||||
char: 'd',
|
||||
description:
|
||||
'select a device variable instead of an application variable',
|
||||
}),
|
||||
config: ec.booleanConfig,
|
||||
device: ec.booleanDevice,
|
||||
service: ec.booleanService,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvRenameCmd,
|
||||
);
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
|
||||
await balena.pine.patch({
|
||||
resource: options.device
|
||||
? 'device_environment_variable'
|
||||
: 'application_environment_variable',
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
await getBalenaSdk().pine.patch({
|
||||
resource: ec.getVarResourceName(opt.config, opt.device, opt.service),
|
||||
id: params.id,
|
||||
body: {
|
||||
value: params.value,
|
||||
|
86
lib/actions-oclif/env/rm.ts
vendored
86
lib/actions-oclif/env/rm.ts
vendored
@ -15,14 +15,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as ec from '../../utils/env-common';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
config: boolean;
|
||||
device: boolean;
|
||||
service: boolean;
|
||||
yes: boolean;
|
||||
}
|
||||
|
||||
@ -32,88 +39,69 @@ interface ArgsDef {
|
||||
|
||||
export default class EnvRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove an environment variable from an application or device.
|
||||
Remove a config or env var from an application, device or service.
|
||||
|
||||
Remove a configuration or environment variable from an application or device,
|
||||
as selected by command-line options.
|
||||
Remove a configuration or environment variable from an application, device
|
||||
or service, as selected by command-line options.
|
||||
|
||||
Note that this command asks for confirmation interactively.
|
||||
You can avoid this by passing the \`--yes\` boolean option.
|
||||
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
|
||||
|
||||
The --device option selects a device instead of an application.
|
||||
The --config option selects a config var instead of an env var.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples remove variables that apply to all services in an app or device.
|
||||
Interactive confirmation is normally asked before the variable is deleted.
|
||||
The --yes option disables this behavior.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena env rm 215',
|
||||
'$ balena env rm 215 --yes',
|
||||
'$ balena env rm 215 --config',
|
||||
'$ balena env rm 215 --device',
|
||||
'$ balena env rm 215 --device --config',
|
||||
'$ balena env rm 123123',
|
||||
'$ balena env rm 234234 --yes',
|
||||
'$ balena env rm 345345 --config',
|
||||
'$ balena env rm 456456 --service',
|
||||
'$ balena env rm 567567 --device',
|
||||
'$ balena env rm 678678 --device --config',
|
||||
'$ balena env rm 789789 --device --service --yes',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'id',
|
||||
required: true,
|
||||
description: 'environment variable numeric database ID',
|
||||
description: "variable's numeric database ID",
|
||||
parse: (input) => parseAsInteger(input, 'id'),
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
|
||||
// hardcoded 'env rm' to avoid oclif's 'env:rm' topic syntax
|
||||
public static usage =
|
||||
'env rm ' + new CommandHelp({ args: EnvRmCmd.args }).defaultUsage();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
device: flags.boolean({
|
||||
char: 'd',
|
||||
description:
|
||||
'Selects a device environment variable instead of an application environment variable',
|
||||
default: false,
|
||||
}),
|
||||
config: flags.boolean({
|
||||
char: 'c',
|
||||
description:
|
||||
'Selects a configuration variable instead of an environment variable',
|
||||
default: false,
|
||||
}),
|
||||
config: ec.booleanConfig,
|
||||
device: ec.booleanDevice,
|
||||
service: ec.booleanService,
|
||||
yes: flags.boolean({
|
||||
char: 'y',
|
||||
description: 'Run in non-interactive mode',
|
||||
description:
|
||||
'do not prompt for confirmation before deleting the variable',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvRmCmd,
|
||||
);
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const patterns = await import('../../utils/patterns');
|
||||
const balena = getBalenaSdk();
|
||||
const { confirm } = await import('../../utils/patterns');
|
||||
|
||||
if (isNaN(params.id) || !Number.isInteger(Number(params.id))) {
|
||||
patterns.exitWithExpectedError(
|
||||
'The environment variable id must be an integer',
|
||||
);
|
||||
}
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
await patterns.confirm(
|
||||
options.yes || false,
|
||||
await confirm(
|
||||
opt.yes || false,
|
||||
'Are you sure you want to delete the environment variable?',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
await balena.pine.delete({
|
||||
resource: options.device
|
||||
? options.config
|
||||
? 'device_config_variable'
|
||||
: 'device_environment_variable'
|
||||
: options.config
|
||||
? 'application_config_variable'
|
||||
: 'application_environment_variable',
|
||||
resource: ec.getVarResourceName(opt.config, opt.device, opt.service),
|
||||
id: params.id,
|
||||
});
|
||||
}
|
||||
|
@ -14,82 +14,432 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { ApplicationVariable, DeviceVariable } from 'balena-sdk';
|
||||
import { flags } from '@oclif/command';
|
||||
import * as SDK from 'balena-sdk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../command';
|
||||
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
import { CommandHelp } from '../utils/oclif-utils';
|
||||
import { isV12 } from '../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
all?: boolean; // whether to include application-wide, device-wide variables //TODO: REMOVE
|
||||
application?: string; // application name
|
||||
config: boolean;
|
||||
device?: string;
|
||||
device?: string; // device UUID
|
||||
json: boolean;
|
||||
help: void;
|
||||
service?: string; // service name
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
|
||||
appName?: string | null; // application name
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
|
||||
interface DeviceServiceEnvironmentVariableInfo
|
||||
extends SDK.DeviceServiceEnvironmentVariable {
|
||||
appName?: string; // application name
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
|
||||
interface ServiceEnvironmentVariableInfo
|
||||
extends SDK.ServiceEnvironmentVariable {
|
||||
appName?: string; // application name
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
|
||||
export default class EnvsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List the environment or config variables of an app or device.
|
||||
public static description = isV12()
|
||||
? stripIndent`
|
||||
List the environment or config variables of an application, device or service.
|
||||
|
||||
List the environment or config variables of an application or device,
|
||||
as selected by the respective command-line options.
|
||||
List the environment or configuration variables of an application, device or
|
||||
service, as selected by the respective command-line options. (A service is
|
||||
an application container in a "microservices" application.)
|
||||
|
||||
The --config option is used to list "configuration variables" that
|
||||
control balena features.
|
||||
The results include application-wide (fleet), device-wide (multiple services on
|
||||
a device) and service-specific variables that apply to the selected application,
|
||||
device or service. It can be thought of as including "inherited" variables;
|
||||
for example, a service inherits device-wide variables, and a device inherits
|
||||
application-wide variables.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples list variables that apply to all services in an app or device.
|
||||
The printed output may include DEVICE and/or SERVICE columns to distinguish
|
||||
between application-wide, device-specific and service-specific variables.
|
||||
An asterisk in these columns indicates that the variable applies to
|
||||
"all devices" or "all services".
|
||||
|
||||
The --config option is used to list "configuration variables" that control
|
||||
balena platform features, as opposed to custom environment variables defined
|
||||
by the user. The --config and the --service options are mutually exclusive
|
||||
because configuration variables cannot be set for specific services.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents data
|
||||
types like lists and empty strings. The 'jq' utility may be helpful in shell
|
||||
scripts (https://stedolan.github.io/jq/manual/). When --json is used, an empty
|
||||
JSON array ([]) is printed instead of an error message when no variables exist
|
||||
for the given query. When querying variables for a device, note that the
|
||||
application name may be null in JSON output (or 'N/A' in tabular output) if the
|
||||
application linked to the device is no longer accessible by the current user
|
||||
(for example, in case the current user has been removed from the application
|
||||
by its owner).
|
||||
`
|
||||
: stripIndent`
|
||||
List the environment or config variables of an application, device or service.
|
||||
|
||||
List the environment or configuration variables of an application, device or
|
||||
service, as selected by the respective command-line options. (A service is
|
||||
an application container in a "microservices" application.)
|
||||
|
||||
The --config option is used to list "configuration variables" that control
|
||||
balena platform features, as opposed to custom environment variables defined
|
||||
by the user. The --config and the --service options are mutually exclusive
|
||||
because configuration variables cannot be set for specific services.
|
||||
|
||||
The --all option is used to include application-wide (fleet), device-wide
|
||||
(multiple services on a device) and service-specific variables that apply to
|
||||
the selected application, device or service. It can be thought of as including
|
||||
"inherited" variables: for example, a service inherits device-wide variables,
|
||||
and a device inherits application-wide variables. Variables are still filtered
|
||||
out by type with the --config option, such that configuration and non-
|
||||
configuration variables are never listed together.
|
||||
|
||||
When the --all option is used, the printed output may include DEVICE and/or
|
||||
SERVICE columns to distinguish between application-wide, device-specific and
|
||||
service-specific variables. An asterisk in these columns indicates that the
|
||||
variable applies to "all devices" or "all services".
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents data
|
||||
types like lists and empty strings. The 'jq' utility may be helpful in shell
|
||||
scripts (https://stedolan.github.io/jq/manual/). When --json is used, an empty
|
||||
JSON array ([]) is printed instead of an error message when no variables exist
|
||||
for the given query. When querying variables for a device, note that the
|
||||
application name may be null in JSON output (or 'N/A' in tabular output) if the
|
||||
application linked to the device is no longer accessible by the current user
|
||||
(for example, in case the current user has been removed from the application
|
||||
by its owner).
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena envs --application MyApp',
|
||||
'$ balena envs --application MyApp --config',
|
||||
'$ balena envs --device 7cf02a6',
|
||||
];
|
||||
public static examples = isV12()
|
||||
? [
|
||||
'$ balena envs --application MyApp',
|
||||
'$ balena envs --application MyApp --json',
|
||||
'$ balena envs --application MyApp --service MyService',
|
||||
'$ balena envs --application MyApp --service MyService',
|
||||
'$ balena envs --application MyApp --config',
|
||||
'$ balena envs --device 7cf02a6',
|
||||
'$ balena envs --device 7cf02a6 --json',
|
||||
'$ balena envs --device 7cf02a6 --config --json',
|
||||
'$ balena envs --device 7cf02a6 --service MyService',
|
||||
]
|
||||
: [
|
||||
'$ balena envs --application MyApp',
|
||||
'$ balena envs --application MyApp --all --json',
|
||||
'$ balena envs --application MyApp --service MyService',
|
||||
'$ balena envs --application MyApp --all --service MyService',
|
||||
'$ balena envs --application MyApp --config',
|
||||
'$ balena envs --device 7cf02a6',
|
||||
'$ balena envs --device 7cf02a6 --all --json',
|
||||
'$ balena envs --device 7cf02a6 --config --all --json',
|
||||
'$ balena envs --device 7cf02a6 --all --service MyService',
|
||||
];
|
||||
|
||||
public static usage = (
|
||||
'envs ' + new CommandHelp({ args: EnvsCmd.args }).defaultUsage()
|
||||
).trim();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: _.assign({ exclusive: ['device'] }, cf.application),
|
||||
...(isV12()
|
||||
? {
|
||||
all: flags.boolean({
|
||||
description: stripIndent`
|
||||
No-op since balena CLI v12.0.0.`,
|
||||
hidden: true,
|
||||
}),
|
||||
}
|
||||
: {
|
||||
all: flags.boolean({
|
||||
description: stripIndent`
|
||||
include app-wide, device-wide variables that apply to the selected device or service.
|
||||
Variables are still filtered out by type with the --config option.`,
|
||||
}),
|
||||
}),
|
||||
application: { exclusive: ['device'], ...cf.application },
|
||||
config: flags.boolean({
|
||||
char: 'c',
|
||||
description: 'show config variables',
|
||||
description: 'show configuration variables only',
|
||||
exclusive: ['service'],
|
||||
}),
|
||||
device: _.assign({ exclusive: ['application'] }, cf.device),
|
||||
device: { exclusive: ['application'], ...cf.device },
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
}),
|
||||
verbose: cf.verbose,
|
||||
service: { exclusive: ['config'], ...cf.service },
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const visuals = await import('resin-cli-visuals');
|
||||
const { exitWithExpectedError } = await import('../utils/patterns');
|
||||
const cmd = this;
|
||||
const variables: EnvironmentVariableInfo[] = [];
|
||||
|
||||
let environmentVariables: ApplicationVariable[] | DeviceVariable[];
|
||||
if (options.application) {
|
||||
environmentVariables = await balena.models.application[
|
||||
options.config ? 'configVar' : 'envVar'
|
||||
].getAllByApplication(options.application);
|
||||
} else if (options.device) {
|
||||
environmentVariables = await balena.models.device[
|
||||
options.config ? 'configVar' : 'envVar'
|
||||
].getAllByDevice(options.device);
|
||||
options.all = options.all || isV12();
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
if (!options.application && !options.device) {
|
||||
throw new ExpectedError('You must specify an application or device');
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const { getDeviceAndMaybeAppFromUUID } = await import('../utils/cloud');
|
||||
|
||||
let appName = options.application;
|
||||
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
|
||||
|
||||
if (options.device) {
|
||||
const [device, app] = await getDeviceAndMaybeAppFromUUID(
|
||||
balena,
|
||||
options.device,
|
||||
['uuid'],
|
||||
['app_name'],
|
||||
);
|
||||
fullUUID = device.uuid;
|
||||
if (app) {
|
||||
appName = app.app_name;
|
||||
}
|
||||
}
|
||||
if (appName && options.service) {
|
||||
await validateServiceName(balena, options.service, appName);
|
||||
}
|
||||
if (options.application || options.all) {
|
||||
variables.push(...(await getAppVars(balena, appName, options)));
|
||||
}
|
||||
if (fullUUID) {
|
||||
variables.push(
|
||||
...(await getDeviceVars(balena, fullUUID, appName, options)),
|
||||
);
|
||||
}
|
||||
if (!options.json && _.isEmpty(variables)) {
|
||||
const target =
|
||||
(options.service ? `service "${options.service}" of ` : '') +
|
||||
(options.application
|
||||
? `application "${options.application}"`
|
||||
: `device "${options.device}"`);
|
||||
throw new ExpectedError(`No environment variables found for ${target}`);
|
||||
}
|
||||
|
||||
await this.printVariables(variables, options);
|
||||
}
|
||||
|
||||
protected async printVariables(
|
||||
varArray: EnvironmentVariableInfo[],
|
||||
options: FlagsDef,
|
||||
) {
|
||||
const fields = ['id', 'name', 'value'];
|
||||
|
||||
if (options.all) {
|
||||
// Replace undefined app names with 'N/A' or null
|
||||
varArray = _.map(varArray, (i: EnvironmentVariableInfo) => {
|
||||
i.appName = i.appName || (options.json ? null : 'N/A');
|
||||
return i;
|
||||
});
|
||||
|
||||
fields.push(options.json ? 'appName' : 'appName => APPLICATION');
|
||||
if (options.device) {
|
||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||
}
|
||||
if (!options.config) {
|
||||
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
this.log(
|
||||
stringifyVarArray<SDK.EnvironmentVariableBase>(varArray, fields),
|
||||
);
|
||||
} else {
|
||||
return exitWithExpectedError('You must specify an application or device');
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isEmpty(environmentVariables)) {
|
||||
return exitWithExpectedError('No environment variables found');
|
||||
}
|
||||
|
||||
cmd.log(
|
||||
visuals.table.horizontal(environmentVariables, ['id', 'name', 'value']),
|
||||
async function validateServiceName(
|
||||
sdk: SDK.BalenaSDK,
|
||||
serviceName: string,
|
||||
appName: string,
|
||||
) {
|
||||
const services = await sdk.models.service.getAllByApplication(appName, {
|
||||
$filter: { service_name: serviceName },
|
||||
});
|
||||
if (_.isEmpty(services)) {
|
||||
throw new ExpectedError(
|
||||
`Service "${serviceName}" not found for application "${appName}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch application-wide config / env / service vars.
|
||||
* If options.application is undefined, an attempt is made to obtain the
|
||||
* application name from the device UUID (options.device). If this attempt
|
||||
* fails because the device does not belong to any application, an emtpy
|
||||
* array is returned.
|
||||
*/
|
||||
async function getAppVars(
|
||||
sdk: SDK.BalenaSDK,
|
||||
appName: string | undefined,
|
||||
options: FlagsDef,
|
||||
): Promise<EnvironmentVariableInfo[]> {
|
||||
const appVars: EnvironmentVariableInfo[] = [];
|
||||
if (!appName) {
|
||||
return appVars;
|
||||
}
|
||||
if (options.config || options.all || !options.service) {
|
||||
const vars = await sdk.models.application[
|
||||
options.config ? 'configVar' : 'envVar'
|
||||
].getAllByApplication(appName);
|
||||
fillInInfoFields(vars, appName);
|
||||
appVars.push(...vars);
|
||||
}
|
||||
if (!options.config && (options.service || options.all)) {
|
||||
const pineOpts: SDK.PineOptionsFor<SDK.ServiceEnvironmentVariable> = {
|
||||
$expand: {
|
||||
service: {},
|
||||
},
|
||||
};
|
||||
if (options.service) {
|
||||
pineOpts.$filter = {
|
||||
service: {
|
||||
service_name: options.service,
|
||||
},
|
||||
};
|
||||
}
|
||||
const serviceVars = await sdk.models.service.var.getAllByApplication(
|
||||
appName,
|
||||
pineOpts,
|
||||
);
|
||||
fillInInfoFields(serviceVars, appName);
|
||||
appVars.push(...serviceVars);
|
||||
}
|
||||
return appVars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch config / env / service vars when the '--device' option is provided.
|
||||
* Precondition: options.device must be defined.
|
||||
*/
|
||||
async function getDeviceVars(
|
||||
sdk: SDK.BalenaSDK,
|
||||
fullUUID: string,
|
||||
appName: string | undefined,
|
||||
options: FlagsDef,
|
||||
): Promise<EnvironmentVariableInfo[]> {
|
||||
const printedUUID = options.json ? fullUUID : options.device!;
|
||||
const deviceVars: EnvironmentVariableInfo[] = [];
|
||||
if (options.config) {
|
||||
const deviceConfigVars = await sdk.models.device.configVar.getAllByDevice(
|
||||
fullUUID,
|
||||
);
|
||||
fillInInfoFields(deviceConfigVars, appName, printedUUID);
|
||||
deviceVars.push(...deviceConfigVars);
|
||||
} else {
|
||||
if (options.service || options.all) {
|
||||
const pineOpts: SDK.PineOptionsFor<SDK.DeviceServiceEnvironmentVariable> = {
|
||||
$expand: {
|
||||
service_install: {
|
||||
$expand: 'installs__service',
|
||||
},
|
||||
},
|
||||
};
|
||||
if (options.service) {
|
||||
pineOpts.$filter = {
|
||||
service_install: {
|
||||
installs__service: { service_name: options.service },
|
||||
},
|
||||
};
|
||||
}
|
||||
const deviceServiceVars = await sdk.models.device.serviceVar.getAllByDevice(
|
||||
fullUUID,
|
||||
pineOpts,
|
||||
);
|
||||
fillInInfoFields(deviceServiceVars, appName, printedUUID);
|
||||
deviceVars.push(...deviceServiceVars);
|
||||
}
|
||||
if (!options.service || options.all) {
|
||||
const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
|
||||
fullUUID,
|
||||
);
|
||||
fillInInfoFields(deviceEnvVars, appName, printedUUID);
|
||||
deviceVars.push(...deviceEnvVars);
|
||||
}
|
||||
}
|
||||
return deviceVars;
|
||||
}
|
||||
|
||||
/**
|
||||
* For each env var object in varArray, fill in its top-level serviceName
|
||||
* and deviceUUID fields. An asterisk is used to indicate that the variable
|
||||
* applies to "all services" or "all devices".
|
||||
*/
|
||||
function fillInInfoFields(
|
||||
varArray:
|
||||
| EnvironmentVariableInfo[]
|
||||
| DeviceServiceEnvironmentVariableInfo[]
|
||||
| ServiceEnvironmentVariableInfo[],
|
||||
appName?: string,
|
||||
deviceUUID?: string,
|
||||
) {
|
||||
for (const envVar of varArray) {
|
||||
if ('service' in envVar) {
|
||||
// envVar is of type ServiceEnvironmentVariableInfo
|
||||
envVar.serviceName = _.at(envVar as any, 'service[0].service_name')[0];
|
||||
} else if ('service_install' in envVar) {
|
||||
// envVar is of type DeviceServiceEnvironmentVariableInfo
|
||||
envVar.serviceName = _.at(
|
||||
envVar as any,
|
||||
'service_install[0].installs__service[0].service_name',
|
||||
)[0];
|
||||
}
|
||||
envVar.appName = appName;
|
||||
envVar.serviceName = envVar.serviceName || '*';
|
||||
envVar.deviceUUID = deviceUUID || '*';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform each object (item) of varArray to preserve only the
|
||||
* fields (keys) listed in the fields argument.
|
||||
*/
|
||||
function stringifyVarArray<T = Dictionary<any>>(
|
||||
varArray: T[],
|
||||
fields: string[],
|
||||
): string {
|
||||
const transformed = _.map(varArray, (o: Dictionary<any>) =>
|
||||
_.transform(
|
||||
o,
|
||||
(result, value, key) => {
|
||||
if (fields.includes(key)) {
|
||||
result[key] = value;
|
||||
}
|
||||
},
|
||||
{} as Dictionary<any>,
|
||||
),
|
||||
);
|
||||
return JSON.stringify(transformed, null, 4);
|
||||
}
|
||||
|
81
lib/actions-oclif/internal/osinit.ts
Normal file
81
lib/actions-oclif/internal/osinit.ts
Normal file
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
// 'Internal' commands are called during the execution of other commands.
|
||||
// `osinit` is called during `os initialize`
|
||||
// TODO: These should be refactored to modules/functions, and removed
|
||||
// See previous `internal sudo` refactor:
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455/files
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
|
||||
|
||||
interface ArgsDef {
|
||||
image: string;
|
||||
type: string;
|
||||
config: string;
|
||||
}
|
||||
|
||||
export default class OsinitCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Do actual init of the device with the preconfigured os image.
|
||||
|
||||
Don't use this command directly!
|
||||
Use \`balena os initialize <image>\` instead.
|
||||
`;
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'image',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'config',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = (
|
||||
'internal osinit ' +
|
||||
new CommandHelp({ args: OsinitCmd.args }).defaultUsage()
|
||||
).trim();
|
||||
|
||||
public static hidden = true;
|
||||
public static root = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<{}, ArgsDef>(OsinitCmd);
|
||||
|
||||
const { initialize } = await import('balena-device-init');
|
||||
const { getManifest, osProgressHandler } = await import(
|
||||
'../../utils/helpers'
|
||||
);
|
||||
|
||||
const config = JSON.parse(params.config);
|
||||
const manifest = await getManifest(params.image, params.type);
|
||||
|
||||
const initializeEmitter = await initialize(params.image, manifest, config);
|
||||
await osProgressHandler(initializeEmitter);
|
||||
}
|
||||
}
|
46
lib/actions-oclif/internal/scandevices.ts
Normal file
46
lib/actions-oclif/internal/scandevices.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
|
||||
// 'Internal' commands are called during the execution of other commands.
|
||||
// `scandevices` is called during by `join`,`leave'.
|
||||
// TODO: These should be refactored to modules/functions, and removed
|
||||
// See previous `internal sudo` refactor:
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455/files
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
|
||||
|
||||
export default class ScandevicesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Scan for local balena-enabled devices and show a picker to choose one.
|
||||
|
||||
Don't use this command directly!
|
||||
`;
|
||||
|
||||
public static usage = 'internal scandevices';
|
||||
|
||||
public static root = true;
|
||||
public static hidden = true;
|
||||
|
||||
public async run() {
|
||||
const { forms } = await import('balena-sync');
|
||||
const hostnameOrIp = await forms.selectLocalBalenaOsDevice();
|
||||
return console.error(`==> Selected device: ${hostnameOrIp}`);
|
||||
}
|
||||
}
|
98
lib/actions-oclif/join.ts
Normal file
98
lib/actions-oclif/join.ts
Normal file
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
help?: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
deviceIpOrHostname?: string;
|
||||
}
|
||||
|
||||
export default class JoinCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Move a local device to an application on another balena server.
|
||||
|
||||
Move a local device to an application on another balena server, causing
|
||||
the device to "join" the new server. The device must be running balenaOS.
|
||||
|
||||
For example, you could provision a device against an openBalena installation
|
||||
where you perform end-to-end tests and then move it to balenaCloud when it's
|
||||
ready for production.
|
||||
|
||||
To move a device between applications on the same server, use the
|
||||
\`balena device move\` command instead of \`balena join\`.
|
||||
|
||||
If you don't specify a device hostname or IP, this command will automatically
|
||||
scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This requires root privileges. Likewise, if
|
||||
the application flag is not provided then a picker will be shown.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena join',
|
||||
'$ balena join balena.local',
|
||||
'$ balena join balena.local --application MyApp',
|
||||
'$ balena join 192.168.1.25',
|
||||
'$ balena join 192.168.1.25 --application MyApp',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'deviceIpOrHostname',
|
||||
description: 'the IP or hostname of device',
|
||||
},
|
||||
];
|
||||
|
||||
// Hardcoded to preserve camelcase
|
||||
public static usage = 'join [deviceIpOrHostname]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: {
|
||||
description: 'the name of the application the device should join',
|
||||
...cf.application,
|
||||
},
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
JoinCmd,
|
||||
);
|
||||
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = getBalenaSdk();
|
||||
const logger = Logger.getLogger();
|
||||
return promote.join(
|
||||
logger,
|
||||
sdk,
|
||||
params.deviceIpOrHostname,
|
||||
options.application,
|
||||
);
|
||||
}
|
||||
}
|
87
lib/actions-oclif/key/add.ts
Normal file
87
lib/actions-oclif/key/add.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export default class KeyAddCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Add an SSH key to balenaCloud.
|
||||
|
||||
Register an SSH in balenaCloud for the logged in user.
|
||||
|
||||
If \`path\` is omitted, the command will attempt
|
||||
to read the SSH key from stdin.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena key add Main ~/.ssh/id_rsa.pub',
|
||||
'$ cat ~/.ssh/id_rsa.pub | balena key add Main',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'the SSH key name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: `path`,
|
||||
description: `the path to the public key file`,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'key add <name> [path]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static readStdin = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(KeyAddCmd);
|
||||
|
||||
let key: string;
|
||||
if (params.path != null) {
|
||||
const { promisify } = await import('util');
|
||||
const readFileAsync = promisify((await import('fs')).readFile);
|
||||
key = await readFileAsync(params.path, 'utf8');
|
||||
} else if (this.stdin.length > 0) {
|
||||
key = this.stdin;
|
||||
} else {
|
||||
throw new ExpectedError('No public key file or path provided.');
|
||||
}
|
||||
|
||||
await getBalenaSdk().models.key.create(params.name, key);
|
||||
}
|
||||
}
|
79
lib/actions-oclif/key/index.ts
Normal file
79
lib/actions-oclif/key/index.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default class KeyCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display an SSH key.
|
||||
|
||||
Display a single SSH key registered in balenaCloud for the logged in user.
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena key 17'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'id',
|
||||
description: 'balenaCloud ID for the SSH key',
|
||||
parse: (x) => parseAsInteger(x, 'id'),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'key <id>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
|
||||
|
||||
const key = await getBalenaSdk().models.key.get(params.id);
|
||||
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKey = {
|
||||
id: key.id,
|
||||
name: key.title,
|
||||
};
|
||||
|
||||
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
|
||||
|
||||
// Since the public key string is long, it might
|
||||
// wrap to lines below, causing the table layout to break.
|
||||
// See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key);
|
||||
}
|
||||
}
|
79
lib/actions-oclif/key/rm.ts
Normal file
79
lib/actions-oclif/key/rm.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default class KeyRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove an SSH key from balenaCloud.
|
||||
|
||||
Remove a single SSH key registered in balenaCloud for the logged in user.
|
||||
|
||||
The --yes option may be used to avoid interactive confirmation.
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena key rm 17', '$ balena key rm 17 --yes'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'id',
|
||||
description: 'balenaCloud ID for the SSH key',
|
||||
parse: (x) => parseAsInteger(x, 'id'),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'key rm <id>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
KeyRmCmd,
|
||||
);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
|
||||
await patterns.confirm(
|
||||
options.yes ?? false,
|
||||
`Are you sure you want to delete key ${params.id}?`,
|
||||
);
|
||||
|
||||
await getBalenaSdk().models.key.remove(params.id);
|
||||
}
|
||||
}
|
56
lib/actions-oclif/keys.ts
Normal file
56
lib/actions-oclif/keys.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class KeysCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List the SSH keys in balenaCloud.
|
||||
|
||||
List all SSH keys registered in balenaCloud for the logged in user.
|
||||
`;
|
||||
public static examples = ['$ balena keys'];
|
||||
|
||||
public static usage = 'keys';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(KeysCmd);
|
||||
|
||||
const keys = await getBalenaSdk().models.key.getAll();
|
||||
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKeys: Array<{ id: number; name: string }> = keys.map((k) => {
|
||||
return { id: k.id, name: k.title };
|
||||
});
|
||||
|
||||
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
|
||||
}
|
||||
}
|
80
lib/actions-oclif/leave.ts
Normal file
80
lib/actions-oclif/leave.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help?: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
deviceIpOrHostname?: string;
|
||||
}
|
||||
|
||||
export default class LeaveCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove a local device from its balena application.
|
||||
|
||||
Remove a local device from its balena application, causing the device to
|
||||
"leave" the server it is provisioned on. This effectively makes the device
|
||||
"unmanaged". The device must be running balenaOS.
|
||||
|
||||
The device entry on the server is preserved after running this command,
|
||||
so the device can subsequently re-join the server if needed.
|
||||
|
||||
If you don't specify a device hostname or IP, this command will automatically
|
||||
scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This usually requires root privileges.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena leave',
|
||||
'$ balena leave balena.local',
|
||||
'$ balena leave 192.168.1.25',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'deviceIpOrHostname',
|
||||
description: 'the device IP or hostname',
|
||||
},
|
||||
];
|
||||
|
||||
// Hardcoded to preserve camelcase
|
||||
public static usage = 'leave [deviceIpOrHostname]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(LeaveCmd);
|
||||
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = getBalenaSdk();
|
||||
const logger = Logger.getLogger();
|
||||
return promote.leave(logger, sdk, params.deviceIpOrHostname);
|
||||
}
|
||||
}
|
94
lib/actions-oclif/note.ts
Normal file
94
lib/actions-oclif/note.ts
Normal file
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
device?: string; // device UUID
|
||||
dev?: string; // Alias for device.
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
note: string;
|
||||
}
|
||||
|
||||
export default class NoteCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Set a device note.
|
||||
|
||||
Set or update a device note. If the note argument is not provided,
|
||||
it will be read from stdin.
|
||||
|
||||
To view device notes, use the \`balena device <uuid>\` command.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena note "My useful note" --device 7cf02a6',
|
||||
'$ cat note.txt | balena note --device 7cf02a6',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'note',
|
||||
description: 'note content',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'note <|note>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
device: { exclusive: ['dev'], ...cf.device },
|
||||
dev: flags.string({
|
||||
exclusive: ['device'],
|
||||
hidden: true,
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static readStdin = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
NoteCmd,
|
||||
);
|
||||
|
||||
params.note = params.note || this.stdin;
|
||||
|
||||
if (_.isEmpty(params.note)) {
|
||||
throw new ExpectedError('Missing note content');
|
||||
}
|
||||
|
||||
options.device = options.device || options.dev;
|
||||
delete options.dev;
|
||||
|
||||
if (_.isEmpty(options.device)) {
|
||||
throw new ExpectedError('Missing device UUID (--device)');
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
return balena.models.device.note(options.device!, params.note);
|
||||
}
|
||||
}
|
@ -15,15 +15,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { flags } from '@oclif/command';
|
||||
import BalenaSdk = require('balena-sdk');
|
||||
import Bluebird = require('bluebird');
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import Command from '../../command';
|
||||
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
const BOOT_PARTITION = 1;
|
||||
const CONNECTIONS_FOLDER = '/system-connections';
|
||||
|
||||
interface FlagsDef {
|
||||
advanced?: boolean;
|
||||
app?: string;
|
||||
@ -38,6 +45,8 @@ interface FlagsDef {
|
||||
'device-type'?: string;
|
||||
help?: void;
|
||||
version?: string;
|
||||
'system-connection': string[];
|
||||
'initial-device-name'?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
@ -67,7 +76,7 @@ export default class OsConfigureCmd extends Command {
|
||||
|
||||
Configure a previously downloaded balenaOS image for a specific device type or
|
||||
balena application.
|
||||
|
||||
|
||||
Configuration settings such as WiFi authentication will be taken from the
|
||||
following sources, in precedence order:
|
||||
1. Command-line options like \`--config-wifi-ssid\`
|
||||
@ -77,6 +86,13 @@ export default class OsConfigureCmd extends Command {
|
||||
The --device-type option may be used to override the application's default
|
||||
device type, in case of an application with mixed device types.
|
||||
|
||||
The --system-connection (-c) option can be used to inject NetworkManager connection
|
||||
profiles for additional network interfaces, such as cellular/GSM or additional
|
||||
WiFi or ethernet connections. This option may be passed multiple times in case there
|
||||
are multiple files to inject. See connection profile examples and reference at:
|
||||
https://www.balena.io/docs/reference/OS/network/2.x/
|
||||
https://developer.gnome.org/NetworkManager/stable/nm-settings.html
|
||||
|
||||
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
@ -111,7 +127,7 @@ export default class OsConfigureCmd extends Command {
|
||||
description: "same as '--application'",
|
||||
exclusive: ['application', 'device'],
|
||||
}),
|
||||
application: _.assign({ exclusive: ['app', 'device'] }, cf.application),
|
||||
application: { exclusive: ['app', 'device'], ...cf.application },
|
||||
config: flags.string({
|
||||
description:
|
||||
'path to a pre-generated config.json file to be injected in the OS image',
|
||||
@ -130,7 +146,7 @@ export default class OsConfigureCmd extends Command {
|
||||
'config-wifi-ssid': flags.string({
|
||||
description: 'WiFi SSID (network name) (non-interactive configuration)',
|
||||
}),
|
||||
device: _.assign({ exclusive: ['app', 'application'] }, cf.device),
|
||||
device: { exclusive: ['app', 'application'], ...cf.device },
|
||||
'device-api-key': flags.string({
|
||||
char: 'k',
|
||||
description:
|
||||
@ -140,10 +156,21 @@ export default class OsConfigureCmd extends Command {
|
||||
description:
|
||||
'device type slug (e.g. "raspberrypi3") to override the application device type',
|
||||
}),
|
||||
'initial-device-name': flags.string({
|
||||
description:
|
||||
'This option will set the device name when the device provisions',
|
||||
}),
|
||||
help: cf.help,
|
||||
version: flags.string({
|
||||
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
||||
}),
|
||||
'system-connection': flags.string({
|
||||
multiple: true,
|
||||
char: 'c',
|
||||
required: false,
|
||||
description:
|
||||
"paths to local files to place into the 'system-connections' directory",
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
@ -157,16 +184,17 @@ export default class OsConfigureCmd extends Command {
|
||||
await validateOptions(options);
|
||||
|
||||
const devInit = await import('balena-device-init');
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const fs = await import('mz/fs');
|
||||
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
||||
'../../utils/config'
|
||||
);
|
||||
const helpers = await import('../../utils/helpers');
|
||||
const imagefs = await require('resin-image-fs');
|
||||
let app: BalenaSdk.Application | undefined;
|
||||
let device: BalenaSdk.Device | undefined;
|
||||
let deviceTypeSlug: string;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
if (options.device) {
|
||||
device = await balena.models['device'].get(options.device);
|
||||
deviceTypeSlug = device.device_type;
|
||||
@ -211,16 +239,51 @@ export default class OsConfigureCmd extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
options['initial-device-name'] &&
|
||||
options['initial-device-name'] !== ''
|
||||
) {
|
||||
configJson!.initialDeviceName = options['initial-device-name'];
|
||||
}
|
||||
|
||||
console.info('Configuring operating system image');
|
||||
|
||||
const image = params.image;
|
||||
await helpers.osProgressHandler(
|
||||
await devInit.configure(
|
||||
params.image,
|
||||
image,
|
||||
deviceTypeManifest,
|
||||
configJson || {},
|
||||
answers,
|
||||
),
|
||||
);
|
||||
|
||||
if (options['system-connection']) {
|
||||
const files = await Bluebird.map(
|
||||
options['system-connection'],
|
||||
async (filePath) => {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const name = path.basename(filePath);
|
||||
|
||||
return {
|
||||
name,
|
||||
content,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await Bluebird.each(files, async ({ name, content }) => {
|
||||
await imagefs.writeFile(
|
||||
{
|
||||
image,
|
||||
partition: BOOT_PARTITION,
|
||||
path: path.join(CONNECTIONS_FOLDER, name),
|
||||
},
|
||||
content,
|
||||
);
|
||||
console.info(`Copied system-connection file: ${name}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,8 +320,8 @@ async function validateOptions(options: FlagsDef) {
|
||||
-------------------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
const { checkLoggedIn } = await import('../../utils/patterns');
|
||||
await checkLoggedIn();
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -306,9 +369,7 @@ async function checkDeviceTypeCompatibility(
|
||||
const helpers = await import('../../utils/helpers');
|
||||
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
|
||||
throw new ExpectedError(
|
||||
`Device type ${
|
||||
options['device-type']
|
||||
} is incompatible with application ${options.application}`,
|
||||
`Device type ${options['device-type']} is incompatible with application ${options.application}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -424,7 +485,7 @@ function camelifyConfigOptions(options: FlagsDef): { [key: string]: any } {
|
||||
if (key.startsWith('config-')) {
|
||||
return key
|
||||
.substring('config-'.length)
|
||||
.replace(/-[a-z]/g, match => match.substring(1).toUpperCase());
|
||||
.replace(/-[a-z]/g, (match) => match.substring(1).toUpperCase());
|
||||
}
|
||||
return key;
|
||||
});
|
||||
|
174
lib/actions-oclif/scan.ts
Normal file
174
lib/actions-oclif/scan.ts
Normal file
@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { LocalBalenaOsDevice } from 'balena-sync';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getVisuals } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
verbose: boolean;
|
||||
timeout?: number;
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class ScanCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Scan for balenaOS devices on your local network.
|
||||
|
||||
Scan for balenaOS devices on your local network.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena scan',
|
||||
'$ balena scan --timeout 120',
|
||||
'$ balena scan --verbose',
|
||||
];
|
||||
|
||||
public static usage = 'scan';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
verbose: flags.boolean({
|
||||
char: 'v',
|
||||
default: false,
|
||||
description: 'display full info',
|
||||
}),
|
||||
timeout: flags.integer({
|
||||
char: 't',
|
||||
description: 'scan timeout in seconds',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
public static root = true;
|
||||
|
||||
public async run() {
|
||||
const Bluebird = await import('bluebird');
|
||||
const _ = await import('lodash');
|
||||
const { SpinnerPromise } = getVisuals();
|
||||
const { discover } = await import('balena-sync');
|
||||
const prettyjson = await import('prettyjson');
|
||||
const { ExpectedError } = await import('../errors');
|
||||
const { dockerPort, dockerTimeout } = await import(
|
||||
'../actions/local/common'
|
||||
);
|
||||
const dockerUtils = await import('../utils/docker');
|
||||
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ScanCmd);
|
||||
|
||||
const discoverTimeout =
|
||||
options.timeout != null ? options.timeout * 1000 : undefined;
|
||||
|
||||
// Find active local devices
|
||||
const activeLocalDevices: LocalBalenaOsDevice[] = await new SpinnerPromise({
|
||||
promise: discover.discoverLocalBalenaOsDevices(discoverTimeout),
|
||||
startMessage: 'Scanning for local balenaOS devices..',
|
||||
stopMessage: 'Reporting scan results',
|
||||
}).filter(async ({ address }: { address: string }) => {
|
||||
const docker = dockerUtils.createClient({
|
||||
host: address,
|
||||
port: dockerPort,
|
||||
timeout: dockerTimeout,
|
||||
}) as any;
|
||||
try {
|
||||
await docker.pingAsync();
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Exit with message if no devices found
|
||||
if (_.isEmpty(activeLocalDevices)) {
|
||||
// TODO: Consider whether this should really be an error
|
||||
throw new ExpectedError(
|
||||
process.platform === 'win32'
|
||||
? ScanCmd.noDevicesFoundMessage + ScanCmd.windowsTipMessage
|
||||
: ScanCmd.noDevicesFoundMessage,
|
||||
);
|
||||
}
|
||||
|
||||
// Query devices for info
|
||||
const devicesInfo = await Bluebird.map(
|
||||
activeLocalDevices,
|
||||
({ host, address }) => {
|
||||
const docker = dockerUtils.createClient({
|
||||
host: address,
|
||||
port: dockerPort,
|
||||
timeout: dockerTimeout,
|
||||
}) as any;
|
||||
return Bluebird.props({
|
||||
host,
|
||||
address,
|
||||
dockerInfo: docker
|
||||
.infoAsync()
|
||||
.catchReturn('Could not get Docker info'),
|
||||
dockerVersion: docker
|
||||
.versionAsync()
|
||||
.catchReturn('Could not get Docker version'),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Reduce properties if not --verbose
|
||||
if (!options.verbose) {
|
||||
devicesInfo.forEach((d: any) => {
|
||||
d.dockerInfo = _.isObject(d.dockerInfo)
|
||||
? _.pick(d.dockerInfo, ScanCmd.dockerInfoProperties)
|
||||
: d.dockerInfo;
|
||||
d.dockerVersion = _.isObject(d.dockerVersion)
|
||||
? _.pick(d.dockerVersion, ScanCmd.dockerVersionProperties)
|
||||
: d.dockerVersion;
|
||||
});
|
||||
}
|
||||
|
||||
// Output results
|
||||
console.log(prettyjson.render(devicesInfo, { noColor: true }));
|
||||
}
|
||||
|
||||
protected static dockerInfoProperties = [
|
||||
'Containers',
|
||||
'ContainersRunning',
|
||||
'ContainersPaused',
|
||||
'ContainersStopped',
|
||||
'Images',
|
||||
'Driver',
|
||||
'SystemTime',
|
||||
'KernelVersion',
|
||||
'OperatingSystem',
|
||||
'Architecture',
|
||||
];
|
||||
|
||||
protected static dockerVersionProperties = ['Version', 'ApiVersion'];
|
||||
|
||||
protected static noDevicesFoundMessage =
|
||||
'Could not find any balenaOS devices on the local network.';
|
||||
|
||||
protected static windowsTipMessage = `
|
||||
|
||||
Note for Windows users:
|
||||
The 'scan' command relies on the Bonjour service. Check whether Bonjour is
|
||||
installed (Control Panel > Programs and Features). If not, you can download
|
||||
Bonjour for Windows (included with Bonjour Print Services) from here:
|
||||
https://support.apple.com/kb/DL999
|
||||
|
||||
After installing Bonjour, restart your PC and run the 'balena scan' command
|
||||
again.`;
|
||||
}
|
52
lib/actions-oclif/settings.ts
Normal file
52
lib/actions-oclif/settings.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class SettingsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Print current settings.
|
||||
|
||||
Use this command to display current balena CLI settings.
|
||||
`;
|
||||
public static examples = ['$ balena settings'];
|
||||
|
||||
public static usage = 'settings';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(SettingsCmd);
|
||||
|
||||
const prettyjson = await import('prettyjson');
|
||||
|
||||
return getBalenaSdk()
|
||||
.settings.getAll()
|
||||
.then(prettyjson.render)
|
||||
.then(console.log);
|
||||
}
|
||||
}
|
134
lib/actions-oclif/tag/rm.ts
Normal file
134
lib/actions-oclif/tag/rm.ts
Normal file
@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { disambiguateReleaseParam } from '../../utils/normalization';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
help: void;
|
||||
app?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
tagKey: string;
|
||||
}
|
||||
|
||||
export default class TagRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove a tag from an application, device or release.
|
||||
|
||||
Remove a tag from an application, device or release.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena tag rm myTagKey --application MyApp',
|
||||
'$ balena tag rm myTagKey --device 7cf02a6',
|
||||
'$ balena tag rm myTagKey --release 1234',
|
||||
'$ balena tag rm myTagKey --release b376b0e544e9429483b656490e5b9443b4349bd6',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'tagKey',
|
||||
description: 'the key string of the tag',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'tag rm <tagKey>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
app: flags.string({
|
||||
description: "same as '--application'",
|
||||
exclusive: ['application', 'device', 'release'],
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
TagRmCmd,
|
||||
);
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Check user has specified one of application/device/release
|
||||
if (!options.application && !options.device && !options.release) {
|
||||
throw new ExpectedError(TagRmCmd.missingResourceMessage);
|
||||
}
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application.tags.remove(
|
||||
tryAsInteger(options.application),
|
||||
params.tagKey,
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
return balena.models.device.tags.remove(
|
||||
tryAsInteger(options.device),
|
||||
params.tagKey,
|
||||
);
|
||||
}
|
||||
if (options.release) {
|
||||
const releaseParam = await disambiguateReleaseParam(
|
||||
balena,
|
||||
options.release,
|
||||
);
|
||||
|
||||
return balena.models.release.tags.remove(releaseParam, params.tagKey);
|
||||
}
|
||||
}
|
||||
|
||||
protected static missingResourceMessage = stripIndent`
|
||||
To remove a resource tag, you must provide exactly one of:
|
||||
|
||||
* An application, with --application <appname>
|
||||
* A device, with --device <uuid>
|
||||
* A release, with --release <id or commit>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help tag rm
|
||||
`;
|
||||
}
|
157
lib/actions-oclif/tag/set.ts
Normal file
157
lib/actions-oclif/tag/set.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { disambiguateReleaseParam } from '../../utils/normalization';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
help: void;
|
||||
app?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
tagKey: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export default class TagSetCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Set a tag on an application, device or release.
|
||||
|
||||
Set a tag on an application, device or release.
|
||||
|
||||
You can optionally provide a value to be associated with the created
|
||||
tag, as an extra argument after the tag key. If a value isn't
|
||||
provided, a tag with an empty value is created.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena tag set mySimpleTag --application MyApp',
|
||||
'$ balena tag set myCompositeTag myTagValue --application MyApp',
|
||||
'$ balena tag set myCompositeTag myTagValue --device 7cf02a6',
|
||||
'$ balena tag set myCompositeTag "my tag value with whitespaces" --device 7cf02a6',
|
||||
'$ balena tag set myCompositeTag myTagValue --release 1234',
|
||||
'$ balena tag set myCompositeTag --release 1234',
|
||||
'$ balena tag set myCompositeTag --release b376b0e544e9429483b656490e5b9443b4349bd6',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'tagKey',
|
||||
description: 'the key string of the tag',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
description: 'the optional value associated with the tag',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'tag set <tagKey> [value]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
app: flags.string({
|
||||
description: "same as '--application'",
|
||||
exclusive: ['application', 'device', 'release'],
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
TagSetCmd,
|
||||
);
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Check user has specified one of application/device/release
|
||||
if (!options.application && !options.device && !options.release) {
|
||||
throw new ExpectedError(TagSetCmd.missingResourceMessage);
|
||||
}
|
||||
|
||||
if (params.value == null) {
|
||||
params.value = '';
|
||||
}
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application.tags.set(
|
||||
tryAsInteger(options.application),
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
return balena.models.device.tags.set(
|
||||
tryAsInteger(options.device),
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
}
|
||||
if (options.release) {
|
||||
const releaseParam = await disambiguateReleaseParam(
|
||||
balena,
|
||||
options.release,
|
||||
);
|
||||
|
||||
return balena.models.release.tags.set(
|
||||
releaseParam,
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected static missingResourceMessage = stripIndent`
|
||||
To set a resource tag, you must provide exactly one of:
|
||||
|
||||
* An application, with --application <appname>
|
||||
* A device, with --device <uuid>
|
||||
* A release, with --release <id or commit>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help tag set
|
||||
`;
|
||||
}
|
132
lib/actions-oclif/tags.ts
Normal file
132
lib/actions-oclif/tags.ts
Normal file
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
import { disambiguateReleaseParam } from '../utils/normalization';
|
||||
import { tryAsInteger } from '../utils/validation';
|
||||
import { isV12 } from '../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
help: void;
|
||||
app?: string;
|
||||
}
|
||||
|
||||
export default class TagsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all tags for an application, device or release.
|
||||
|
||||
List all tags and their values for a particular application,
|
||||
device or release.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena tags --application MyApp',
|
||||
'$ balena tags --device 7cf02a6',
|
||||
'$ balena tags --release 1234',
|
||||
'$ balena tags --release b376b0e544e9429483b656490e5b9443b4349bd6',
|
||||
];
|
||||
|
||||
public static usage = 'tags';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
app: flags.string({
|
||||
description: "same as '--application'",
|
||||
exclusive: ['application', 'device', 'release'],
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(TagsCmd);
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Check user has specified one of application/device/release
|
||||
if (!options.application && !options.device && !options.release) {
|
||||
throw new ExpectedError(this.missingResourceMessage);
|
||||
}
|
||||
|
||||
let tags;
|
||||
|
||||
if (options.application) {
|
||||
tags = await balena.models.application.tags.getAllByApplication(
|
||||
tryAsInteger(options.application),
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
tags = await balena.models.device.tags.getAllByDevice(
|
||||
tryAsInteger(options.device),
|
||||
);
|
||||
}
|
||||
if (options.release) {
|
||||
const releaseParam = await disambiguateReleaseParam(
|
||||
balena,
|
||||
options.release,
|
||||
);
|
||||
|
||||
tags = await balena.models.release.tags.getAllByRelease(releaseParam);
|
||||
}
|
||||
|
||||
if (!tags || tags.length === 0) {
|
||||
throw new ExpectedError('No tags found');
|
||||
}
|
||||
|
||||
console.log(
|
||||
isV12()
|
||||
? getVisuals().table.horizontal(tags, ['tag_key', 'value'])
|
||||
: getVisuals().table.horizontal(tags, ['id', 'tag_key', 'value']),
|
||||
);
|
||||
}
|
||||
|
||||
protected missingResourceMessage = stripIndent`
|
||||
To list tags for a resource, you must provide exactly one of:
|
||||
|
||||
* An application, with --application <appname>
|
||||
* A device, with --device <uuid>
|
||||
* A release, with --release <id or commit>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help tags
|
||||
`;
|
||||
}
|
@ -15,8 +15,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
|
||||
interface FlagsDef {
|
||||
all?: boolean;
|
||||
@ -34,8 +35,11 @@ export default class VersionCmd extends Command {
|
||||
Display version information for the balena CLI and/or Node.js.
|
||||
|
||||
Display version information for the balena CLI and/or Node.js.
|
||||
If you intend to parse the output, please use the -j option for
|
||||
JSON output, as its format is more stable.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents
|
||||
data types like lists and empty strings. The 'jq' utility may be helpful
|
||||
in shell scripts (https://stedolan.github.io/jq/manual/).
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena version',
|
||||
|
@ -1,36 +0,0 @@
|
||||
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 'balena login --token <key>',
|
||||
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena api-key generate "Jenkins Key"
|
||||
`,
|
||||
async action(params, _options, done) {
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
|
||||
balena.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);
|
||||
},
|
||||
};
|
@ -1,173 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
|
||||
exports.create =
|
||||
signature: 'app create <name>'
|
||||
description: 'create an application'
|
||||
help: '''
|
||||
Use this command to create a new balena application.
|
||||
|
||||
You can specify the application device type with the `--type` option.
|
||||
Otherwise, an interactive dropdown will be shown for you to select from.
|
||||
|
||||
You can see a list of supported device types with
|
||||
|
||||
$ balena devices supported
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app create MyApp
|
||||
$ balena app create MyApp --type raspberry-pi
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
parameter: 'type'
|
||||
description: 'application device type (Check available types with `balena devices supported`)'
|
||||
alias: 't'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
# Validate the the application name is available
|
||||
# before asking the device type.
|
||||
# https://github.com/balena-io/balena-cli/issues/30
|
||||
balena.models.application.has(params.name).then (hasApplication) ->
|
||||
if hasApplication
|
||||
patterns.exitWithExpectedError('You already have an application with that name!')
|
||||
|
||||
.then ->
|
||||
return options.type or patterns.selectDeviceType()
|
||||
.then (deviceType) ->
|
||||
return balena.models.application.create({
|
||||
name: params.name
|
||||
deviceType
|
||||
})
|
||||
.then (application) ->
|
||||
console.info("Application created: #{application.app_name} (#{application.device_type}, id #{application.id})")
|
||||
.nodeify(done)
|
||||
|
||||
exports.list =
|
||||
signature: 'apps'
|
||||
description: 'list all applications'
|
||||
help: '''
|
||||
Use this command to list all your applications.
|
||||
|
||||
Notice this command only shows the most important bits of information for each app.
|
||||
If you want detailed information, use balena app <name> instead.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena apps
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
balena.models.application.getAll
|
||||
$select: [
|
||||
'id'
|
||||
'app_name'
|
||||
'device_type'
|
||||
]
|
||||
$expand: owns__device: $select: 'is_online'
|
||||
.then (applications) ->
|
||||
applications.forEach (application) ->
|
||||
application.device_count = _.size(application.owns__device)
|
||||
application.online_devices = _.filter(application.owns__device, (d) -> d.is_online == true).length
|
||||
|
||||
console.log visuals.table.horizontal applications, [
|
||||
'id'
|
||||
'app_name'
|
||||
'device_type'
|
||||
'online_devices'
|
||||
'device_count'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.info =
|
||||
signature: 'app <name>'
|
||||
description: 'list a single application'
|
||||
help: '''
|
||||
Use this command to show detailed information for a single application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app MyApp
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
balena.models.application.get(params.name).then (application) ->
|
||||
console.log visuals.table.vertical application, [
|
||||
"$#{application.app_name}$"
|
||||
'id'
|
||||
'device_type'
|
||||
'git_repository'
|
||||
'commit'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.restart =
|
||||
signature: 'app restart <name>'
|
||||
description: 'restart an application'
|
||||
help: '''
|
||||
Use this command to restart all devices that belongs to a certain application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app restart MyApp
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.application.restart(params.name).nodeify(done)
|
||||
|
||||
exports.remove =
|
||||
signature: 'app rm <name>'
|
||||
description: 'remove an application'
|
||||
help: '''
|
||||
Use this command to remove a balena application.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app rm MyApp
|
||||
$ balena app rm MyApp --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the application?').then ->
|
||||
balena.models.application.remove(params.name)
|
||||
.nodeify(done)
|
@ -1,170 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
exports.login =
|
||||
signature: 'login'
|
||||
description: 'login to balena'
|
||||
help: '''
|
||||
Use this command to login to your balena account.
|
||||
|
||||
This command will prompt you to login using the following login types:
|
||||
|
||||
- Web authorization: open your web browser and prompt you to authorize the CLI
|
||||
from the dashboard.
|
||||
|
||||
- Credentials: using email/password and 2FA.
|
||||
|
||||
- Token: using a session token or API key from the preferences page.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena login
|
||||
$ balena login --web
|
||||
$ balena login --token "..."
|
||||
$ balena login --credentials
|
||||
$ balena login --credentials --email johndoe@gmail.com --password secret
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'token'
|
||||
description: 'session token or API key'
|
||||
parameter: 'token'
|
||||
alias: 't'
|
||||
}
|
||||
{
|
||||
signature: 'web'
|
||||
description: 'web-based login'
|
||||
boolean: true
|
||||
alias: 'w'
|
||||
}
|
||||
{
|
||||
signature: 'credentials'
|
||||
description: 'credential-based login'
|
||||
boolean: true
|
||||
alias: 'c'
|
||||
}
|
||||
{
|
||||
signature: 'email'
|
||||
parameter: 'email'
|
||||
description: 'email'
|
||||
alias: [ 'e', 'u' ]
|
||||
}
|
||||
{
|
||||
signature: 'password'
|
||||
parameter: 'password'
|
||||
description: 'password'
|
||||
alias: 'p'
|
||||
}
|
||||
]
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
auth = require('../auth')
|
||||
form = require('resin-cli-form')
|
||||
patterns = require('../utils/patterns')
|
||||
messages = require('../utils/messages')
|
||||
|
||||
login = (options) ->
|
||||
if options.token?
|
||||
return Promise.try ->
|
||||
return options.token if _.isString(options.token)
|
||||
return form.ask
|
||||
message: 'Session token or API key from the preferences page'
|
||||
name: 'token'
|
||||
type: 'input'
|
||||
.then(balena.auth.loginWithToken)
|
||||
.tap ->
|
||||
balena.auth.whoami()
|
||||
.then (username) ->
|
||||
if !username
|
||||
patterns.exitWithExpectedError('Token authentication failed')
|
||||
else if options.credentials
|
||||
return patterns.authenticate(options)
|
||||
else if options.web
|
||||
console.info('Connecting to the web dashboard')
|
||||
return auth.login()
|
||||
|
||||
return patterns.askLoginType().then (loginType) ->
|
||||
|
||||
if loginType is 'register'
|
||||
signupUrl = 'https://dashboard.balena-cloud.com/signup'
|
||||
require('opn')(signupUrl, { wait: false })
|
||||
patterns.exitWithExpectedError("Please sign up at #{signupUrl}")
|
||||
|
||||
options[loginType] = true
|
||||
return login(options)
|
||||
|
||||
balena.settings.get('balenaUrl').then (balenaUrl) ->
|
||||
console.log(messages.balenaAsciiArt)
|
||||
console.log("\nLogging in to #{balenaUrl}")
|
||||
return login(options)
|
||||
.then(balena.auth.whoami)
|
||||
.tap (username) ->
|
||||
console.info("Successfully logged in as: #{username}")
|
||||
console.info """
|
||||
|
||||
Find out about the available commands by running:
|
||||
|
||||
$ balena help
|
||||
|
||||
#{messages.reachingOut}
|
||||
"""
|
||||
.nodeify(done)
|
||||
|
||||
exports.logout =
|
||||
signature: 'logout'
|
||||
description: 'logout from balena'
|
||||
help: '''
|
||||
Use this command to logout from your balena account.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena logout
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.auth.logout().nodeify(done)
|
||||
|
||||
exports.whoami =
|
||||
signature: 'whoami'
|
||||
description: 'get current username and email address'
|
||||
help: '''
|
||||
Use this command to find out the current logged in username and email address.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena whoami
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.props
|
||||
username: balena.auth.whoami()
|
||||
email: balena.auth.getEmail()
|
||||
url: balena.settings.get('balenaUrl')
|
||||
.then (results) ->
|
||||
console.log visuals.table.vertical results, [
|
||||
'$account information$'
|
||||
'username'
|
||||
'email'
|
||||
'url'
|
||||
]
|
||||
.nodeify(done)
|
196
lib/actions/auth.ts
Normal file
196
lib/actions/auth.ts
Normal file
@ -0,0 +1,196 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
|
||||
export const login: CommandDefinition<
|
||||
{},
|
||||
{
|
||||
token: string | boolean;
|
||||
web: boolean;
|
||||
credentials: boolean;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
> = {
|
||||
signature: 'login',
|
||||
description: 'login to balena',
|
||||
help: `\
|
||||
Use this command to login to your balena account.
|
||||
|
||||
This command will prompt you to login using the following login types:
|
||||
|
||||
- Web authorization: open your web browser and prompt you to authorize the CLI
|
||||
from the dashboard.
|
||||
|
||||
- Credentials: using email/password and 2FA.
|
||||
|
||||
- Token: using a session token or API key from the preferences page.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena login
|
||||
$ balena login --web
|
||||
$ balena login --token "..."
|
||||
$ balena login --credentials
|
||||
$ balena login --credentials --email johndoe@gmail.com --password secret\
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'token',
|
||||
description: 'session token or API key',
|
||||
parameter: 'token',
|
||||
alias: 't',
|
||||
},
|
||||
{
|
||||
signature: 'web',
|
||||
description: 'web-based login',
|
||||
boolean: true,
|
||||
alias: 'w',
|
||||
},
|
||||
{
|
||||
signature: 'credentials',
|
||||
description: 'credential-based login',
|
||||
boolean: true,
|
||||
alias: 'c',
|
||||
},
|
||||
{
|
||||
signature: 'email',
|
||||
parameter: 'email',
|
||||
description: 'email',
|
||||
alias: ['e', 'u'],
|
||||
},
|
||||
{
|
||||
signature: 'password',
|
||||
parameter: 'password',
|
||||
description: 'password',
|
||||
alias: 'p',
|
||||
},
|
||||
],
|
||||
primary: true,
|
||||
async action(_params, options) {
|
||||
type Options = typeof options;
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = await import('../utils/patterns');
|
||||
const messages = await import('../utils/messages');
|
||||
const { exitWithExpectedError } = await import('../errors');
|
||||
|
||||
const doLogin = async (loginOptions: Options): Promise<void> => {
|
||||
if (loginOptions.token != null) {
|
||||
let token: string;
|
||||
if (typeof loginOptions.token === 'string') {
|
||||
token = loginOptions.token;
|
||||
} else {
|
||||
const form = await import('resin-cli-form');
|
||||
token = await form.ask({
|
||||
message: 'Session token or API key from the preferences page',
|
||||
name: 'token',
|
||||
type: 'input',
|
||||
});
|
||||
}
|
||||
await balena.auth.loginWithToken(token);
|
||||
if (!(await balena.auth.whoami())) {
|
||||
exitWithExpectedError('Token authentication failed');
|
||||
}
|
||||
return;
|
||||
} else if (loginOptions.credentials) {
|
||||
return patterns.authenticate(loginOptions);
|
||||
} else if (loginOptions.web) {
|
||||
const auth = await import('../auth');
|
||||
await auth.login();
|
||||
return;
|
||||
}
|
||||
|
||||
const loginType = await patterns.askLoginType();
|
||||
if (loginType === 'register') {
|
||||
const signupUrl = 'https://dashboard.balena-cloud.com/signup';
|
||||
const open = await import('open');
|
||||
open(signupUrl, { wait: false });
|
||||
return exitWithExpectedError(`Please sign up at ${signupUrl}`);
|
||||
}
|
||||
|
||||
loginOptions[loginType] = true;
|
||||
return doLogin(loginOptions);
|
||||
};
|
||||
|
||||
const balenaUrl = await balena.settings.get('balenaUrl');
|
||||
|
||||
console.log(messages.balenaAsciiArt);
|
||||
console.log(`\nLogging in to ${balenaUrl}`);
|
||||
await doLogin(options);
|
||||
const username = await balena.auth.whoami();
|
||||
|
||||
console.info(`Successfully logged in as: ${username}`);
|
||||
console.info(`\
|
||||
|
||||
Find out about the available commands by running:
|
||||
|
||||
$ balena help
|
||||
|
||||
${messages.reachingOut}`);
|
||||
|
||||
if (options.web) {
|
||||
const { shutdownServer } = await import('../auth');
|
||||
shutdownServer();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const logout: CommandDefinition = {
|
||||
signature: 'logout',
|
||||
description: 'logout from balena',
|
||||
help: `\
|
||||
Use this command to logout from your balena account.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena logout\
|
||||
`,
|
||||
async action(_params) {
|
||||
await getBalenaSdk().auth.logout();
|
||||
},
|
||||
};
|
||||
|
||||
export const whoami: CommandDefinition = {
|
||||
signature: 'whoami',
|
||||
description: 'get current username and email address',
|
||||
help: `\
|
||||
Use this command to find out the current logged in username and email address.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena whoami\
|
||||
`,
|
||||
permission: 'user',
|
||||
async action() {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const [username, email, url] = await Promise.all([
|
||||
balena.auth.whoami(),
|
||||
balena.auth.getEmail(),
|
||||
balena.settings.get('balenaUrl'),
|
||||
]);
|
||||
console.log(
|
||||
getVisuals().table.vertical({ username, email, url }, [
|
||||
'$account information$',
|
||||
'username',
|
||||
'email',
|
||||
'url',
|
||||
]),
|
||||
);
|
||||
},
|
||||
};
|
@ -1,155 +0,0 @@
|
||||
# Imported here because it's needed for the setup
|
||||
# of this action
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
compose = require('../utils/compose')
|
||||
{ registrySecretsHelp } = require('../utils/messages')
|
||||
|
||||
###
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
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
|
||||
undefined # image: name of pre-built image
|
||||
composeOpts.dockerfilePath # ok if undefined
|
||||
)
|
||||
.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 single image or a multicontainer project locally'
|
||||
primary: true
|
||||
help: """
|
||||
Use this command to build an image or a complete multicontainer project with
|
||||
the provided docker daemon in your development machine or balena device.
|
||||
(See also the `balena push` command for the option of building images in the
|
||||
balenaCloud build servers.)
|
||||
|
||||
You must provide either an application or a device-type/architecture pair to use
|
||||
the balena 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 docker-compose.yml file. If it 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[.template] file (or
|
||||
alternative Dockerfile specified with the `-f` option), and if yet that isn't
|
||||
found, it will try to generate one.
|
||||
|
||||
#{registrySecretsHelp}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena build
|
||||
$ balena build ./source/
|
||||
$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated
|
||||
$ balena build --application MyApp ./source/
|
||||
$ balena build --docker /var/run/docker.sock # Linux, Mac
|
||||
$ balena build --docker //./pipe/docker_engine # Windows
|
||||
$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem
|
||||
"""
|
||||
options: dockerUtils.appendOptions compose.appendOptions [
|
||||
{
|
||||
signature: 'arch'
|
||||
parameter: 'arch'
|
||||
description: 'The architecture to build for'
|
||||
alias: 'A'
|
||||
},
|
||||
{
|
||||
signature: 'deviceType'
|
||||
parameter: 'deviceType'
|
||||
description: 'The type of device this build is for'
|
||||
alias: 'd'
|
||||
},
|
||||
{
|
||||
signature: 'application'
|
||||
parameter: 'application'
|
||||
description: 'The target balena application this build is for'
|
||||
alias: 'a'
|
||||
},
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
# compositions with many services trigger misleading warnings
|
||||
require('events').defaultMaxListeners = 1000
|
||||
|
||||
sdk = (require('balena-sdk')).fromSharedOptions()
|
||||
{ validateComposeOptions } = require('../utils/compose_ts')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
helpers = require('../utils/helpers')
|
||||
Logger = require('../utils/logger')
|
||||
|
||||
logger = Logger.getLogger()
|
||||
logger.logDebug('Parsing input...')
|
||||
|
||||
# `build` accepts `[source]` as a parameter, but compose expects it
|
||||
# as an option. swap them here
|
||||
options.source ?= params.source
|
||||
delete params.source
|
||||
|
||||
Promise.resolve(validateComposeOptions(sdk, options))
|
||||
.then ->
|
||||
{ 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)
|
207
lib/actions/build.js
Normal file
207
lib/actions/build.js
Normal file
@ -0,0 +1,207 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Imported here because it's needed for the setup
|
||||
// of this action
|
||||
import * as Promise from 'bluebird';
|
||||
|
||||
import * as dockerUtils from '../utils/docker';
|
||||
import * as compose from '../utils/compose';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
/*
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
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
|
||||
*/
|
||||
const buildProject = function (docker, logger, composeOpts, opts) {
|
||||
const { loadProject } = require('../utils/compose_ts');
|
||||
return Promise.resolve(loadProject(logger, composeOpts))
|
||||
.then(function (project) {
|
||||
const appType = opts.app?.application_type?.[0];
|
||||
if (
|
||||
appType != null &&
|
||||
project.descriptors.length > 1 &&
|
||||
!appType.supports_multicontainer
|
||||
) {
|
||||
logger.logWarn(
|
||||
'Target application does not support multiple containers.\n' +
|
||||
'Continuing with build, but you will not be able to deploy.',
|
||||
);
|
||||
}
|
||||
|
||||
return compose.buildProject(
|
||||
docker,
|
||||
logger,
|
||||
project.path,
|
||||
project.name,
|
||||
project.composition,
|
||||
opts.arch,
|
||||
opts.deviceType,
|
||||
opts.buildEmulated,
|
||||
opts.buildOpts,
|
||||
composeOpts.inlineLogs,
|
||||
composeOpts.convertEol,
|
||||
composeOpts.dockerfilePath,
|
||||
composeOpts.nogitignore,
|
||||
);
|
||||
})
|
||||
.then(function () {
|
||||
logger.outputDeferredMessages();
|
||||
logger.logSuccess('Build succeeded!');
|
||||
})
|
||||
.tapCatch(() => {
|
||||
logger.logError('Build failed');
|
||||
});
|
||||
};
|
||||
|
||||
export const build = {
|
||||
signature: 'build [source]',
|
||||
description: 'Build a single image or a multicontainer project locally',
|
||||
primary: true,
|
||||
help: `\
|
||||
Use this command to build an image or a complete multicontainer project with
|
||||
the provided docker daemon in your development machine or balena device.
|
||||
(See also the \`balena push\` command for the option of building images in the
|
||||
balenaCloud build servers.)
|
||||
|
||||
You must provide either an application or a device-type/architecture pair to use
|
||||
the balena 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 docker-compose.yml file, and if found,
|
||||
each service defined in the compose file will be built. If a compose file isn't
|
||||
found, it will look for a Dockerfile[.template] file (or alternative Dockerfile
|
||||
specified with the \`--dockerfile\` option), and if no dockerfile is found, it
|
||||
will try to generate one.
|
||||
|
||||
${registrySecretsHelp}
|
||||
|
||||
${dockerignoreHelp}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena build
|
||||
$ balena build ./source/
|
||||
$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated
|
||||
$ balena build --application MyApp ./source/
|
||||
$ balena build --docker /var/run/docker.sock # Linux, Mac
|
||||
$ balena build --docker //./pipe/docker_engine # Windows
|
||||
$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem\
|
||||
`,
|
||||
options: dockerUtils.appendOptions(
|
||||
compose.appendOptions([
|
||||
{
|
||||
signature: 'arch',
|
||||
parameter: 'arch',
|
||||
description: 'The architecture to build for',
|
||||
alias: 'A',
|
||||
},
|
||||
{
|
||||
signature: 'deviceType',
|
||||
parameter: 'deviceType',
|
||||
description: 'The type of device this build is for',
|
||||
alias: 'd',
|
||||
},
|
||||
{
|
||||
signature: 'application',
|
||||
parameter: 'application',
|
||||
description: 'The target balena application this build is for',
|
||||
alias: 'a',
|
||||
},
|
||||
]),
|
||||
),
|
||||
action(params, options) {
|
||||
// compositions with many services trigger misleading warnings
|
||||
// @ts-ignore editing property that isn't typed but does exist
|
||||
require('events').defaultMaxListeners = 1000;
|
||||
|
||||
const sdk = getBalenaSdk();
|
||||
const { ExpectedError } = require('../errors');
|
||||
const { checkLoggedIn } = require('../utils/patterns');
|
||||
const { validateProjectDirectory } = require('../utils/compose_ts');
|
||||
const helpers = require('../utils/helpers');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
const logger = Logger.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
// `build` accepts `[source]` as a parameter, but compose expects it
|
||||
// as an option. swap them here
|
||||
if (options.source == null) {
|
||||
options.source = params.source;
|
||||
}
|
||||
delete params.source;
|
||||
|
||||
const { application, arch, deviceType } = options;
|
||||
|
||||
return Promise.try(function () {
|
||||
if (
|
||||
(application == null && (arch == null || deviceType == null)) ||
|
||||
(application != null && (arch != null || deviceType != null))
|
||||
) {
|
||||
throw new ExpectedError(
|
||||
'You must specify either an application or an arch/deviceType pair to build for',
|
||||
);
|
||||
}
|
||||
if (application) {
|
||||
return checkLoggedIn();
|
||||
}
|
||||
})
|
||||
.then(() =>
|
||||
validateProjectDirectory(sdk, {
|
||||
dockerfilePath: options.dockerfile,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
projectPath: options.source || '.',
|
||||
registrySecretsPath: options['registry-secrets'],
|
||||
}),
|
||||
)
|
||||
.then(function ({ dockerfilePath, registrySecrets }) {
|
||||
options.dockerfile = dockerfilePath;
|
||||
options['registry-secrets'] = registrySecrets;
|
||||
|
||||
if (arch != null && deviceType != null) {
|
||||
return [undefined, arch, deviceType];
|
||||
} else {
|
||||
return helpers
|
||||
.getAppWithArch(application)
|
||||
.then((app) => [app, app.arch, app.device_type]);
|
||||
}
|
||||
})
|
||||
|
||||
.then(function ([app, resolvedArch, resolvedDeviceType]) {
|
||||
return Promise.join(
|
||||
dockerUtils.getDocker(options),
|
||||
dockerUtils.generateBuildOpts(options),
|
||||
compose.generateOpts(options),
|
||||
(docker, buildOpts, composeOpts) =>
|
||||
buildProject(docker, logger, composeOpts, {
|
||||
app,
|
||||
arch: resolvedArch,
|
||||
deviceType: resolvedDeviceType,
|
||||
buildEmulated: !!options.emulated,
|
||||
buildOpts,
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import _ = require('lodash');
|
||||
|
||||
export const yes = {
|
||||
signature: 'yes',
|
||||
description: 'confirm non interactively',
|
||||
@ -34,10 +32,10 @@ export const optionalApplication = {
|
||||
alias: ['a', 'app'],
|
||||
};
|
||||
|
||||
export const application = _.defaults(
|
||||
{ required: 'You have to specify an application' },
|
||||
optionalApplication,
|
||||
);
|
||||
export const application = {
|
||||
...optionalApplication,
|
||||
required: 'You have to specify an application',
|
||||
};
|
||||
|
||||
export const optionalRelease = {
|
||||
signature: 'release',
|
||||
@ -75,12 +73,10 @@ export const optionalOsVersion = {
|
||||
|
||||
export type OptionalOsVersionOption = Partial<OsVersionOption>;
|
||||
|
||||
export const osVersion = _.defaults(
|
||||
{
|
||||
required: 'You have to specify an exact os version',
|
||||
},
|
||||
exports.optionalOsVersion,
|
||||
);
|
||||
export const osVersion = {
|
||||
...exports.optionalOsVersion,
|
||||
required: 'You have to specify an exact os version',
|
||||
};
|
||||
|
||||
export interface OsVersionOption {
|
||||
version?: string;
|
||||
|
@ -1,357 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2018 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
exports.read =
|
||||
signature: 'config read'
|
||||
description: 'read a device configuration'
|
||||
help: '''
|
||||
Use this command to read the config.json file from the mounted filesystem (e.g. SD card) of a provisioned device"
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config read --type raspberry-pi
|
||||
$ balena config read --type raspberry-pi --drive /dev/disk2
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
prettyjson = require('prettyjson')
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
return config.read(drive, options.type)
|
||||
.tap (configJSON) ->
|
||||
console.info(prettyjson.render(configJSON))
|
||||
.nodeify(done)
|
||||
|
||||
exports.write =
|
||||
signature: 'config write <key> <value>'
|
||||
description: 'write a device configuration'
|
||||
help: '''
|
||||
Use this command to write the config.json file to the mounted filesystem (e.g. SD card) of a provisioned device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config write --type raspberry-pi username johndoe
|
||||
$ balena config write --type raspberry-pi --drive /dev/disk2 username johndoe
|
||||
$ balena config write --type raspberry-pi files.network/settings "..."
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
config.read(drive, options.type).then (configJSON) ->
|
||||
console.info("Setting #{params.key} to #{params.value}")
|
||||
_.set(configJSON, params.key, params.value)
|
||||
return configJSON
|
||||
.tap ->
|
||||
return umountAsync(drive)
|
||||
.then (configJSON) ->
|
||||
return config.write(drive, options.type, configJSON)
|
||||
.tap ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
|
||||
exports.inject =
|
||||
signature: 'config inject <file>'
|
||||
description: 'inject a device configuration file'
|
||||
help: '''
|
||||
Use this command to inject a config.json file to the mounted filesystem
|
||||
(e.g. SD card or mounted balenaOS image) of a provisioned device"
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config inject my/config.json --type raspberry-pi
|
||||
$ balena config inject my/config.json --type raspberry-pi --drive /dev/disk2
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
readFileAsync = Promise.promisify(require('fs').readFile)
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
readFileAsync(params.file, 'utf8').then(JSON.parse).then (configJSON) ->
|
||||
return config.write(drive, options.type, configJSON)
|
||||
.tap ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
|
||||
exports.reconfigure =
|
||||
signature: 'config reconfigure'
|
||||
description: 'reconfigure a provisioned device'
|
||||
help: '''
|
||||
Use this command to reconfigure a provisioned device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config reconfigure --type raspberry-pi
|
||||
$ balena config reconfigure --type raspberry-pi --advanced
|
||||
$ balena config reconfigure --type raspberry-pi --drive /dev/disk2
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
{
|
||||
signature: 'advanced'
|
||||
description: 'show advanced commands'
|
||||
boolean: true
|
||||
alias: 'v'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
config.read(drive, options.type).get('uuid')
|
||||
.tap ->
|
||||
umountAsync(drive)
|
||||
.then (uuid) ->
|
||||
configureCommand = "os configure #{drive} --device #{uuid}"
|
||||
if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
return runCommand(configureCommand)
|
||||
.then ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
|
||||
exports.generate =
|
||||
signature: 'config generate'
|
||||
description: 'generate a config.json file'
|
||||
help: '''
|
||||
Use this command to generate a config.json for a device or application.
|
||||
|
||||
Calling this command with the exact version number of the targeted image is required.
|
||||
|
||||
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.
|
||||
|
||||
In case that you want to configure an image for an application with mixed device types,
|
||||
you can pass the --device-type argument along with --app to specify the target device type.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json
|
||||
$ balena config generate --app MyApp --version 2.12.7
|
||||
$ balena config generate --app MyApp --version 2.12.7 --device-type fincm3
|
||||
$ balena config generate --app MyApp --version 2.12.7 --output config.json
|
||||
$ balena config generate --app MyApp --version 2.12.7 \
|
||||
--network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
|
||||
'''
|
||||
options: [
|
||||
commandOptions.osVersion
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
commandOptions.optionalDeviceApiKey
|
||||
commandOptions.optionalDeviceType
|
||||
{
|
||||
signature: 'generate-device-api-key'
|
||||
description: 'generate a fresh device key for the device'
|
||||
boolean: true
|
||||
}
|
||||
{
|
||||
signature: 'output'
|
||||
description: 'output'
|
||||
parameter: 'output'
|
||||
alias: 'o'
|
||||
}
|
||||
# Options for non-interactive configuration
|
||||
{
|
||||
signature: 'network'
|
||||
description: 'the network type to use: ethernet or wifi'
|
||||
parameter: 'network'
|
||||
}
|
||||
{
|
||||
signature: 'wifiSsid'
|
||||
description: 'the wifi ssid to use (used only if --network is set to wifi)'
|
||||
parameter: 'wifiSsid'
|
||||
}
|
||||
{
|
||||
signature: 'wifiKey'
|
||||
description: 'the wifi key to use (used only if --network is set to wifi)'
|
||||
parameter: 'wifiKey'
|
||||
}
|
||||
{
|
||||
signature: 'appUpdatePollInterval'
|
||||
description: 'how frequently (in minutes) to poll for application updates'
|
||||
parameter: 'appUpdatePollInterval'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(options, 'device')
|
||||
Promise = require('bluebird')
|
||||
writeFileAsync = Promise.promisify(require('fs').writeFile)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
prettyjson = require('prettyjson')
|
||||
|
||||
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
|
||||
helpers = require('../utils/helpers')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if not options.device? and not options.application?
|
||||
exitWithExpectedError '''
|
||||
You have to pass either a device or an application.
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate
|
||||
'''
|
||||
|
||||
if !options.application and options.deviceType
|
||||
exitWithExpectedError '''
|
||||
Specifying a different device type is only supported when
|
||||
generating a config for an application:
|
||||
|
||||
* An application, with --app <appname>
|
||||
* A specific device type, with --device-type <deviceTypeSlug>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate
|
||||
'''
|
||||
|
||||
Promise.try ->
|
||||
if options.device?
|
||||
return balena.models.device.get(options.device)
|
||||
return balena.models.application.get(options.application)
|
||||
.then (resource) ->
|
||||
deviceType = options.deviceType || resource.device_type
|
||||
manifestPromise = balena.models.device.getManifestBySlug(deviceType)
|
||||
|
||||
if options.application && options.deviceType
|
||||
app = resource
|
||||
appManifestPromise = balena.models.device.getManifestBySlug(app.device_type)
|
||||
manifestPromise = manifestPromise.tap (paramDeviceType) ->
|
||||
appManifestPromise.then (appDeviceType) ->
|
||||
if not helpers.areDeviceTypesCompatible(appDeviceType, paramDeviceType)
|
||||
throw new balena.errors.BalenaInvalidDeviceType(
|
||||
"Device type #{options.deviceType} is incompatible with application #{options.application}"
|
||||
)
|
||||
|
||||
manifestPromise.get('options')
|
||||
.then (formOptions) ->
|
||||
# Pass params as an override: if there is any param with exactly the same name as a
|
||||
# required option, that value is used (and the corresponding question is not asked)
|
||||
form.run(formOptions, override: options)
|
||||
.then (answers) ->
|
||||
answers.version = options.version
|
||||
|
||||
if resource.uuid?
|
||||
generateDeviceConfig(resource, options.deviceApiKey || options['generate-device-api-key'], answers)
|
||||
else
|
||||
answers.deviceType = deviceType
|
||||
generateApplicationConfig(resource, answers)
|
||||
.then (config) ->
|
||||
if options.output?
|
||||
return writeFileAsync(options.output, JSON.stringify(config))
|
||||
|
||||
console.log(prettyjson.render(config))
|
||||
.nodeify(done)
|
417
lib/actions/config.js
Normal file
417
lib/actions/config.js
Normal file
@ -0,0 +1,417 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as commandOptions from './command-options';
|
||||
|
||||
import { normalizeUuidProp } from '../utils/normalization';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
|
||||
export const read = {
|
||||
signature: 'config read',
|
||||
description: 'read a device configuration',
|
||||
help: `\
|
||||
Use this command to read the config.json file from the mounted filesystem (e.g. SD card) of a provisioned device"
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config read --type raspberry-pi
|
||||
$ balena config read --type raspberry-pi --drive /dev/disk2\
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'type',
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
parameter: 'type',
|
||||
alias: 't',
|
||||
required: 'You have to specify a device type',
|
||||
},
|
||||
{
|
||||
signature: 'drive',
|
||||
description: 'drive',
|
||||
parameter: 'drive',
|
||||
alias: 'd',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
root: true,
|
||||
action(_params, options) {
|
||||
const Promise = require('bluebird');
|
||||
const config = require('balena-config-json');
|
||||
const umountAsync = Promise.promisify(require('umount').umount);
|
||||
const prettyjson = require('prettyjson');
|
||||
|
||||
return Promise.try(
|
||||
() => options.drive || getVisuals().drive('Select the device drive'),
|
||||
)
|
||||
.tap(umountAsync)
|
||||
.then((drive) => config.read(drive, options.type))
|
||||
.tap((configJSON) => {
|
||||
console.info(prettyjson.render(configJSON));
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const write = {
|
||||
signature: 'config write <key> <value>',
|
||||
description: 'write a device configuration',
|
||||
help: `\
|
||||
Use this command to write the config.json file to the mounted filesystem (e.g. SD card) of a provisioned device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config write --type raspberry-pi username johndoe
|
||||
$ balena config write --type raspberry-pi --drive /dev/disk2 username johndoe
|
||||
$ balena config write --type raspberry-pi files.network/settings "..."\
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'type',
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
parameter: 'type',
|
||||
alias: 't',
|
||||
required: 'You have to specify a device type',
|
||||
},
|
||||
{
|
||||
signature: 'drive',
|
||||
description: 'drive',
|
||||
parameter: 'drive',
|
||||
alias: 'd',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
root: true,
|
||||
action(params, options) {
|
||||
const Promise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const config = require('balena-config-json');
|
||||
const umountAsync = Promise.promisify(require('umount').umount);
|
||||
|
||||
return Promise.try(
|
||||
() => options.drive || getVisuals().drive('Select the device drive'),
|
||||
)
|
||||
.tap(umountAsync)
|
||||
.then((drive) =>
|
||||
config
|
||||
.read(drive, options.type)
|
||||
.then(function (configJSON) {
|
||||
console.info(`Setting ${params.key} to ${params.value}`);
|
||||
_.set(configJSON, params.key, params.value);
|
||||
return configJSON;
|
||||
})
|
||||
.tap(() => umountAsync(drive))
|
||||
.then((configJSON) => config.write(drive, options.type, configJSON)),
|
||||
)
|
||||
.tap(() => {
|
||||
console.info('Done');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const inject = {
|
||||
signature: 'config inject <file>',
|
||||
description: 'inject a device configuration file',
|
||||
help: `\
|
||||
Use this command to inject a config.json file to the mounted filesystem
|
||||
(e.g. SD card or mounted balenaOS image) of a provisioned device"
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config inject my/config.json --type raspberry-pi
|
||||
$ balena config inject my/config.json --type raspberry-pi --drive /dev/disk2\
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'type',
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
parameter: 'type',
|
||||
alias: 't',
|
||||
required: 'You have to specify a device type',
|
||||
},
|
||||
{
|
||||
signature: 'drive',
|
||||
description: 'drive',
|
||||
parameter: 'drive',
|
||||
alias: 'd',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
root: true,
|
||||
action(params, options) {
|
||||
const Promise = require('bluebird');
|
||||
const util = require('util');
|
||||
const config = require('balena-config-json');
|
||||
const umountAsync = Promise.promisify(require('umount').umount);
|
||||
const readFileAsync = util.promisify(require('fs').readFile);
|
||||
|
||||
return Promise.try(
|
||||
() => options.drive || getVisuals().drive('Select the device drive'),
|
||||
)
|
||||
.tap(umountAsync)
|
||||
.then((drive) =>
|
||||
readFileAsync(params.file, 'utf8')
|
||||
.then(JSON.parse)
|
||||
.then((configJSON) => config.write(drive, options.type, configJSON)),
|
||||
)
|
||||
.tap(() => {
|
||||
console.info('Done');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const reconfigure = {
|
||||
signature: 'config reconfigure',
|
||||
description: 'reconfigure a provisioned device',
|
||||
help: `\
|
||||
Use this command to reconfigure a provisioned device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config reconfigure --type raspberry-pi
|
||||
$ balena config reconfigure --type raspberry-pi --advanced
|
||||
$ balena config reconfigure --type raspberry-pi --drive /dev/disk2\
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'type',
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
parameter: 'type',
|
||||
alias: 't',
|
||||
required: 'You have to specify a device type',
|
||||
},
|
||||
{
|
||||
signature: 'drive',
|
||||
description: 'drive',
|
||||
parameter: 'drive',
|
||||
alias: 'd',
|
||||
},
|
||||
{
|
||||
signature: 'advanced',
|
||||
description: 'show advanced commands',
|
||||
boolean: true,
|
||||
alias: 'v',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
root: true,
|
||||
action(_params, options) {
|
||||
const Promise = require('bluebird');
|
||||
const config = require('balena-config-json');
|
||||
const { runCommand } = require('../utils/helpers');
|
||||
const umountAsync = Promise.promisify(require('umount').umount);
|
||||
|
||||
return Promise.try(
|
||||
() => options.drive || getVisuals().drive('Select the device drive'),
|
||||
)
|
||||
.tap(umountAsync)
|
||||
.then((drive) =>
|
||||
config
|
||||
.read(drive, options.type)
|
||||
.get('uuid')
|
||||
.tap(() => umountAsync(drive))
|
||||
.then(function (uuid) {
|
||||
let configureCommand = `os configure ${drive} --device ${uuid}`;
|
||||
if (options.advanced) {
|
||||
configureCommand += ' --advanced';
|
||||
}
|
||||
return runCommand(configureCommand);
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
console.info('Done');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const generate = {
|
||||
signature: 'config generate',
|
||||
description: 'generate a config.json file',
|
||||
help: `\
|
||||
Use this command to generate a config.json for a device or application.
|
||||
|
||||
Calling this command with the exact version number of the targeted image is required.
|
||||
|
||||
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.
|
||||
|
||||
In case that you want to configure an image for an application with mixed device types,
|
||||
you can pass the --device-type argument along with --app to specify the target device type.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json
|
||||
$ balena config generate --app MyApp --version 2.12.7
|
||||
$ balena config generate --app MyApp --version 2.12.7 --device-type fincm3
|
||||
$ balena config generate --app MyApp --version 2.12.7 --output config.json
|
||||
$ balena config generate --app MyApp --version 2.12.7 \
|
||||
--network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1\
|
||||
`,
|
||||
options: [
|
||||
commandOptions.osVersion,
|
||||
commandOptions.optionalApplication,
|
||||
commandOptions.optionalDevice,
|
||||
commandOptions.optionalDeviceApiKey,
|
||||
commandOptions.optionalDeviceType,
|
||||
{
|
||||
signature: 'generate-device-api-key',
|
||||
description: 'generate a fresh device key for the device',
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'output',
|
||||
description: 'output',
|
||||
parameter: 'output',
|
||||
alias: 'o',
|
||||
},
|
||||
// Options for non-interactive configuration
|
||||
{
|
||||
signature: 'network',
|
||||
description: 'the network type to use: ethernet or wifi',
|
||||
parameter: 'network',
|
||||
},
|
||||
{
|
||||
signature: 'wifiSsid',
|
||||
description:
|
||||
'the wifi ssid to use (used only if --network is set to wifi)',
|
||||
parameter: 'wifiSsid',
|
||||
},
|
||||
{
|
||||
signature: 'wifiKey',
|
||||
description:
|
||||
'the wifi key to use (used only if --network is set to wifi)',
|
||||
parameter: 'wifiKey',
|
||||
},
|
||||
{
|
||||
signature: 'appUpdatePollInterval',
|
||||
description:
|
||||
'how frequently (in minutes) to poll for application updates',
|
||||
parameter: 'appUpdatePollInterval',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
action(_params, options) {
|
||||
normalizeUuidProp(options, 'device');
|
||||
const Promise = require('bluebird');
|
||||
const writeFileAsync = Promise.promisify(require('fs').writeFile);
|
||||
const balena = getBalenaSdk();
|
||||
const form = require('resin-cli-form');
|
||||
const prettyjson = require('prettyjson');
|
||||
|
||||
const {
|
||||
generateDeviceConfig,
|
||||
generateApplicationConfig,
|
||||
} = require('../utils/config');
|
||||
const helpers = require('../utils/helpers');
|
||||
const { exitWithExpectedError } = require('../errors');
|
||||
|
||||
if (options.device == null && options.application == null) {
|
||||
exitWithExpectedError(`\
|
||||
You have to pass either a device or an application.
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate\
|
||||
`);
|
||||
}
|
||||
|
||||
if (!options.application && options.deviceType) {
|
||||
exitWithExpectedError(`\
|
||||
Specifying a different device type is only supported when
|
||||
generating a config for an application:
|
||||
|
||||
* An application, with --app <appname>
|
||||
* A specific device type, with --device-type <deviceTypeSlug>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate\
|
||||
`);
|
||||
}
|
||||
|
||||
return Promise.try(
|
||||
/** @returns {Promise<any>} */ function () {
|
||||
if (options.device != null) {
|
||||
return balena.models.device.get(options.device);
|
||||
}
|
||||
return balena.models.application.get(options.application);
|
||||
},
|
||||
)
|
||||
.then(function (resource) {
|
||||
const deviceType = options.deviceType || resource.device_type;
|
||||
let manifestPromise = balena.models.device.getManifestBySlug(
|
||||
deviceType,
|
||||
);
|
||||
|
||||
if (options.application && options.deviceType) {
|
||||
const app = resource;
|
||||
const appManifestPromise = balena.models.device.getManifestBySlug(
|
||||
app.device_type,
|
||||
);
|
||||
manifestPromise = manifestPromise.tap((paramDeviceType) =>
|
||||
appManifestPromise.then(function (appDeviceType) {
|
||||
if (
|
||||
!helpers.areDeviceTypesCompatible(
|
||||
appDeviceType,
|
||||
paramDeviceType,
|
||||
)
|
||||
) {
|
||||
throw new balena.errors.BalenaInvalidDeviceType(
|
||||
`Device type ${options.deviceType} is incompatible with application ${options.application}`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return manifestPromise
|
||||
.get('options')
|
||||
.then((
|
||||
formOptions, // Pass params as an override: if there is any param with exactly the same name as a
|
||||
) =>
|
||||
// required option, that value is used (and the corresponding question is not asked)
|
||||
form.run(formOptions, { override: options }),
|
||||
)
|
||||
.then(function (answers) {
|
||||
answers.version = options.version;
|
||||
|
||||
if (resource.uuid != null) {
|
||||
return generateDeviceConfig(
|
||||
resource,
|
||||
options.deviceApiKey || options['generate-device-api-key'],
|
||||
answers,
|
||||
);
|
||||
} else {
|
||||
answers.deviceType = deviceType;
|
||||
return generateApplicationConfig(resource, answers);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function (config) {
|
||||
if (options.output != null) {
|
||||
return writeFileAsync(options.output, JSON.stringify(config));
|
||||
}
|
||||
|
||||
console.log(prettyjson.render(config));
|
||||
});
|
||||
},
|
||||
};
|
@ -1,234 +0,0 @@
|
||||
# Imported here because it's needed for the setup
|
||||
# of this action
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
compose = require('../utils/compose')
|
||||
{ registrySecretsHelp } = require('../utils/messages')
|
||||
|
||||
###
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
app: the application instance to deploy to
|
||||
image: the image to deploy; optional
|
||||
dockerfilePath: name of an alternative Dockerfile; 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('balena-sdk').fromSharedOptions()
|
||||
|
||||
compose.loadProject(
|
||||
logger
|
||||
composeOpts.projectPath
|
||||
composeOpts.projectName
|
||||
opts.image
|
||||
composeOpts.dockerfilePath # ok if undefined
|
||||
)
|
||||
.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!')
|
||||
|
||||
# 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')
|
||||
|
||||
msg = chalk.yellow('Target application requires legacy deploy method.')
|
||||
logger.logWarn(msg)
|
||||
|
||||
return Promise.join(
|
||||
docker
|
||||
logger
|
||||
sdk.auth.getToken()
|
||||
sdk.auth.whoami()
|
||||
sdk.settings.get('balenaUrl')
|
||||
{
|
||||
# opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||
appName: opts.appName
|
||||
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 a single image or a multicontainer project to a balena application'
|
||||
help: """
|
||||
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
|
||||
|
||||
Use this command to deploy an image or a complete multicontainer project to an
|
||||
application, optionally building it first. The source images are searched for
|
||||
(and optionally built) using the docker daemon in your development machine or
|
||||
balena device. (See also the `balena push` command for the option of building
|
||||
the image in the balenaCloud build servers.)
|
||||
|
||||
Unless an image is specified, this command will look into the current directory
|
||||
(or the one specified by --source) for a docker-compose.yml 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[.template] file (or alternative
|
||||
Dockerfile specified with the `-f` option), 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
|
||||
`balena deploy <appOwnerUsername>/<appName>`.
|
||||
|
||||
When --build is used, all options supported by `balena build` are also supported
|
||||
by this command.
|
||||
|
||||
#{registrySecretsHelp}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena deploy myApp
|
||||
$ balena deploy myApp --build --source myBuildDir/
|
||||
$ balena deploy myApp myApp/myImage
|
||||
"""
|
||||
permission: 'user'
|
||||
primary: true
|
||||
options: dockerUtils.appendOptions compose.appendOptions [
|
||||
{
|
||||
signature: 'source'
|
||||
parameter: 'source'
|
||||
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)"
|
||||
boolean: true
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
# compositions with many services trigger misleading warnings
|
||||
require('events').defaultMaxListeners = 1000
|
||||
sdk = (require('balena-sdk')).fromSharedOptions()
|
||||
{ validateComposeOptions } = require('../utils/compose_ts')
|
||||
helpers = require('../utils/helpers')
|
||||
Logger = require('../utils/logger')
|
||||
|
||||
logger = Logger.getLogger()
|
||||
logger.logDebug('Parsing input...')
|
||||
|
||||
# when Capitano converts a positional parameter (but not an option)
|
||||
# to a number, the original value is preserved with the _raw suffix
|
||||
{ appName, appName_raw, image } = params
|
||||
|
||||
# look into "balena build" options if appName isn't given
|
||||
appName = appName_raw || appName || options.application
|
||||
delete options.application
|
||||
|
||||
Promise.resolve(validateComposeOptions(sdk, options))
|
||||
.then ->
|
||||
if not appName?
|
||||
throw new Error('Please specify the name of the application to deploy')
|
||||
|
||||
if image? and options.build
|
||||
throw new Error('Build option is not applicable when specifying an image')
|
||||
|
||||
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 ]
|
||||
|
||||
.then ([ app, image, shouldPerformBuild, shouldUploadLogs ]) ->
|
||||
Promise.join(
|
||||
dockerUtils.getDocker(options)
|
||||
dockerUtils.generateBuildOpts(options)
|
||||
compose.generateOpts(options)
|
||||
(docker, buildOpts, composeOpts) ->
|
||||
deployProject(docker, logger, composeOpts, {
|
||||
app
|
||||
appName # may be prefixed by 'owner/', unlike app.app_name
|
||||
image
|
||||
shouldPerformBuild
|
||||
shouldUploadLogs
|
||||
buildEmulated: !!options.emulated
|
||||
buildOpts
|
||||
})
|
||||
)
|
||||
.asCallback(done)
|
317
lib/actions/deploy.js
Normal file
317
lib/actions/deploy.js
Normal file
@ -0,0 +1,317 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Imported here because it's needed for the setup
|
||||
// of this action
|
||||
import * as Promise from 'bluebird';
|
||||
|
||||
import * as dockerUtils from '../utils/docker';
|
||||
import * as compose from '../utils/compose';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
||||
|
||||
/*
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
app: the application instance to deploy to
|
||||
image: the image to deploy; optional
|
||||
dockerfilePath: name of an alternative Dockerfile; optional
|
||||
shouldPerformBuild
|
||||
shouldUploadLogs
|
||||
buildEmulated
|
||||
buildOpts: arguments to forward to docker build command
|
||||
*/
|
||||
const deployProject = function (docker, logger, composeOpts, opts) {
|
||||
const _ = require('lodash');
|
||||
const doodles = require('resin-doodles');
|
||||
const sdk = getBalenaSdk();
|
||||
const {
|
||||
deployProject: $deployProject,
|
||||
loadProject,
|
||||
} = require('../utils/compose_ts');
|
||||
|
||||
return Promise.resolve(loadProject(logger, composeOpts, opts.image))
|
||||
.then(function (project) {
|
||||
if (
|
||||
project.descriptors.length > 1 &&
|
||||
!opts.app.application_type?.[0]?.supports_multicontainer
|
||||
) {
|
||||
throw new Error(
|
||||
'Target application does not support multiple containers. Aborting!',
|
||||
);
|
||||
}
|
||||
|
||||
// find which services use images that already exist locally
|
||||
return (
|
||||
Promise.map(project.descriptors, function (d) {
|
||||
// unconditionally build (or pull) if explicitly requested
|
||||
if (opts.shouldPerformBuild) {
|
||||
return d;
|
||||
}
|
||||
return docker
|
||||
.getImage(typeof d.image === 'string' ? d.image : d.image.tag)
|
||||
.inspect()
|
||||
.return(d.serviceName)
|
||||
.catchReturn();
|
||||
})
|
||||
.filter((d) => !!d)
|
||||
.then(function (servicesToSkip) {
|
||||
// multibuild takes in a composition and always attempts to
|
||||
// build or pull all services. we workaround that here by
|
||||
// passing a modified composition.
|
||||
const compositionToBuild = _.cloneDeep(project.composition);
|
||||
compositionToBuild.services = _.omit(
|
||||
compositionToBuild.services,
|
||||
servicesToSkip,
|
||||
);
|
||||
if (_.size(compositionToBuild.services) === 0) {
|
||||
logger.logInfo(
|
||||
'Everything is up to date (use --build to force a rebuild)',
|
||||
);
|
||||
return {};
|
||||
}
|
||||
return compose
|
||||
.buildProject(
|
||||
docker,
|
||||
logger,
|
||||
project.path,
|
||||
project.name,
|
||||
compositionToBuild,
|
||||
opts.app.arch,
|
||||
opts.app.device_type,
|
||||
opts.buildEmulated,
|
||||
opts.buildOpts,
|
||||
composeOpts.inlineLogs,
|
||||
composeOpts.convertEol,
|
||||
composeOpts.dockerfilePath,
|
||||
composeOpts.nogitignore,
|
||||
)
|
||||
.then((builtImages) => _.keyBy(builtImages, 'serviceName'));
|
||||
})
|
||||
.then((builtImages) =>
|
||||
project.descriptors.map(
|
||||
(d) =>
|
||||
builtImages[d.serviceName] ?? {
|
||||
serviceName: d.serviceName,
|
||||
name: typeof d.image === 'string' ? d.image : d.image.tag,
|
||||
logs: 'Build skipped; image for service already exists.',
|
||||
props: {},
|
||||
},
|
||||
),
|
||||
)
|
||||
// @ts-ignore slightly different return types of partial vs non-partial release
|
||||
.then(function (images) {
|
||||
if (opts.app.application_type?.[0]?.is_legacy) {
|
||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||
|
||||
const msg = getChalk().yellow(
|
||||
'Target application requires legacy deploy method.',
|
||||
);
|
||||
logger.logWarn(msg);
|
||||
|
||||
return Promise.join(
|
||||
docker,
|
||||
logger,
|
||||
sdk.auth.getToken(),
|
||||
sdk.auth.whoami(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
{
|
||||
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||
appName: opts.appName,
|
||||
imageName: images[0].name,
|
||||
buildLogs: images[0].logs,
|
||||
shouldUploadLogs: opts.shouldUploadLogs,
|
||||
},
|
||||
deployLegacy,
|
||||
).then((releaseId) =>
|
||||
// @ts-ignore releaseId should be inferred as a number because that's what deployLegacy is
|
||||
// typed as returning but the .js type-checking doesn't manage to infer it correctly due to
|
||||
// Promise.join typings
|
||||
sdk.models.release.get(releaseId, { $select: ['commit'] }),
|
||||
);
|
||||
}
|
||||
return Promise.join(
|
||||
sdk.auth.getUserId(),
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('apiUrl'),
|
||||
(userId, auth, apiEndpoint) =>
|
||||
$deployProject(
|
||||
docker,
|
||||
logger,
|
||||
project.composition,
|
||||
images,
|
||||
opts.app.id,
|
||||
userId,
|
||||
`Bearer ${auth}`,
|
||||
apiEndpoint,
|
||||
!opts.shouldUploadLogs,
|
||||
),
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function (release) {
|
||||
logger.outputDeferredMessages();
|
||||
logger.logSuccess('Deploy succeeded!');
|
||||
logger.logSuccess(`Release: ${release.commit}`);
|
||||
console.log();
|
||||
console.log(doodles.getDoodle()); // Show charlie
|
||||
console.log();
|
||||
})
|
||||
.tapCatch(() => {
|
||||
logger.logError('Deploy failed');
|
||||
});
|
||||
};
|
||||
|
||||
export const deploy = {
|
||||
signature: 'deploy <appName> [image]',
|
||||
description:
|
||||
'Deploy a single image or a multicontainer project to a balena application',
|
||||
help: `\
|
||||
Usage: \`deploy <appName> ([image] | --build [--source build-dir])\`
|
||||
|
||||
Use this command to deploy an image or a complete multicontainer project to an
|
||||
application, optionally building it first. The source images are searched for
|
||||
(and optionally built) using the docker daemon in your development machine or
|
||||
balena device. (See also the \`balena push\` command for the option of building
|
||||
the image in the balenaCloud build servers.)
|
||||
|
||||
Unless an image is specified, this command will look into the current directory
|
||||
(or the one specified by --source) for a docker-compose.yml 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[.template] file (or alternative
|
||||
Dockerfile specified with the \`-f\` option), 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
|
||||
\`balena deploy <appOwnerUsername>/<appName>\`.
|
||||
|
||||
When --build is used, all options supported by \`balena build\` are also supported
|
||||
by this command.
|
||||
|
||||
${registrySecretsHelp}
|
||||
|
||||
${dockerignoreHelp}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena deploy myApp
|
||||
$ balena deploy myApp --build --source myBuildDir/
|
||||
$ balena deploy myApp myApp/myImage\
|
||||
`,
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
options: dockerUtils.appendOptions(
|
||||
compose.appendOptions([
|
||||
{
|
||||
signature: 'source',
|
||||
parameter: 'source',
|
||||
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)",
|
||||
boolean: true,
|
||||
},
|
||||
]),
|
||||
),
|
||||
action(params, options) {
|
||||
// compositions with many services trigger misleading warnings
|
||||
// @ts-ignore editing property that isn't typed but does exist
|
||||
require('events').defaultMaxListeners = 1000;
|
||||
const sdk = getBalenaSdk();
|
||||
const {
|
||||
getRegistrySecrets,
|
||||
validateProjectDirectory,
|
||||
} = require('../utils/compose_ts');
|
||||
const helpers = require('../utils/helpers');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
const logger = Logger.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
// when Capitano converts a positional parameter (but not an option)
|
||||
// to a number, the original value is preserved with the _raw suffix
|
||||
let { appName, appName_raw, image } = params;
|
||||
|
||||
// look into "balena build" options if appName isn't given
|
||||
appName = appName_raw || appName || options.application;
|
||||
delete options.application;
|
||||
|
||||
return Promise.try(function () {
|
||||
if (appName == null) {
|
||||
throw new ExpectedError(
|
||||
'Please specify the name of the application to deploy',
|
||||
);
|
||||
}
|
||||
|
||||
if (image != null && options.build) {
|
||||
throw new ExpectedError(
|
||||
'Build option is not applicable when specifying an image',
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(function () {
|
||||
if (image) {
|
||||
return getRegistrySecrets(sdk, options['registry-secrets']).then(
|
||||
(registrySecrets) => {
|
||||
options['registry-secrets'] = registrySecrets;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return validateProjectDirectory(sdk, {
|
||||
dockerfilePath: options.dockerfile,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
projectPath: options.source || '.',
|
||||
registrySecretsPath: options['registry-secrets'],
|
||||
}).then(function ({ dockerfilePath, registrySecrets }) {
|
||||
options.dockerfile = dockerfilePath;
|
||||
options['registry-secrets'] = registrySecrets;
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => helpers.getAppWithArch(appName))
|
||||
.then(function (app) {
|
||||
return Promise.join(
|
||||
dockerUtils.getDocker(options),
|
||||
dockerUtils.generateBuildOpts(options),
|
||||
compose.generateOpts(options),
|
||||
(docker, buildOpts, composeOpts) =>
|
||||
deployProject(docker, logger, composeOpts, {
|
||||
app,
|
||||
appName, // may be prefixed by 'owner/', unlike app.app_name
|
||||
image,
|
||||
shouldPerformBuild: !!options.build,
|
||||
shouldUploadLogs: !options.nologupload,
|
||||
buildEmulated: !!options.emulated,
|
||||
buildOpts,
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
@ -1,451 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
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'
|
||||
help: '''
|
||||
Use this command to list all devices that belong to you.
|
||||
|
||||
You can filter the devices by application by using the `--application` option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena devices
|
||||
$ balena devices --application MyApp
|
||||
$ balena devices --app MyApp
|
||||
$ balena devices -a MyApp
|
||||
'''
|
||||
options: [ commandOptions.optionalApplication ]
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.try ->
|
||||
if options.application?
|
||||
return balena.models.device.getAllByApplication(options.application, expandForAppName)
|
||||
return balena.models.device.getAll(expandForAppName)
|
||||
|
||||
.tap (devices) ->
|
||||
devices = _.map devices, (device) ->
|
||||
device.dashboard_url = balena.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'
|
||||
'device_name'
|
||||
'device_type'
|
||||
'application_name'
|
||||
'status'
|
||||
'is_online'
|
||||
'supervisor_version'
|
||||
'os_version'
|
||||
'dashboard_url'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.info =
|
||||
signature: 'device <uuid>'
|
||||
description: 'list a single device'
|
||||
help: '''
|
||||
Use this command to show information about a single device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device 7cf02a6
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
balena.models.device.get(params.uuid, expandForAppName)
|
||||
.then (device) ->
|
||||
balena.models.device.getStatus(device).then (status) ->
|
||||
device.status = status
|
||||
device.dashboard_url = balena.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.device_name}$"
|
||||
'id'
|
||||
'device_type'
|
||||
'status'
|
||||
'is_online'
|
||||
'ip_address'
|
||||
'application_name'
|
||||
'last_seen'
|
||||
'uuid'
|
||||
'commit'
|
||||
'supervisor_version'
|
||||
'is_web_accessible'
|
||||
'note'
|
||||
'os_version'
|
||||
'dashboard_url'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.register =
|
||||
signature: 'device register <application>'
|
||||
description: 'register a device'
|
||||
help: '''
|
||||
Use this command to register a device to an application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device register MyApp
|
||||
$ balena device register MyApp --uuid <uuid>
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
{
|
||||
signature: 'uuid'
|
||||
description: 'custom uuid'
|
||||
parameter: 'uuid'
|
||||
alias: 'u'
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
Promise.join(
|
||||
balena.models.application.get(params.application)
|
||||
options.uuid ? balena.models.device.generateUniqueKey()
|
||||
(application, uuid) ->
|
||||
console.info("Registering to #{application.app_name}: #{uuid}")
|
||||
return balena.models.device.register(application.id, uuid)
|
||||
)
|
||||
.get('uuid')
|
||||
.nodeify(done)
|
||||
|
||||
exports.remove =
|
||||
signature: 'device rm <uuid>'
|
||||
description: 'remove a device'
|
||||
help: '''
|
||||
Use this command to remove a device from balena.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device rm 7cf02a6
|
||||
$ balena device rm 7cf02a6 --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the device?').then ->
|
||||
balena.models.device.remove(params.uuid)
|
||||
.nodeify(done)
|
||||
|
||||
exports.identify =
|
||||
signature: 'device identify <uuid>'
|
||||
description: 'identify a device with a UUID'
|
||||
help: '''
|
||||
Use this command to identify a device.
|
||||
|
||||
In the Raspberry Pi, the ACT led is blinked several times.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device identify 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.identify(params.uuid).nodeify(done)
|
||||
|
||||
exports.reboot =
|
||||
signature: 'device reboot <uuid>'
|
||||
description: 'restart a device'
|
||||
help: '''
|
||||
Use this command to remotely reboot a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device reboot 23c73a1
|
||||
'''
|
||||
options: [ commandOptions.forceUpdateLock ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.reboot(params.uuid, options).nodeify(done)
|
||||
|
||||
exports.shutdown =
|
||||
signature: 'device shutdown <uuid>'
|
||||
description: 'shutdown a device'
|
||||
help: '''
|
||||
Use this command to remotely shutdown a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device shutdown 23c73a1
|
||||
'''
|
||||
options: [ commandOptions.forceUpdateLock ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.shutdown(params.uuid, options).nodeify(done)
|
||||
|
||||
exports.enableDeviceUrl =
|
||||
signature: 'device public-url enable <uuid>'
|
||||
description: 'enable public URL for a device'
|
||||
help: '''
|
||||
Use this command to enable public URL for a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device public-url enable 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.enableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.disableDeviceUrl =
|
||||
signature: 'device public-url disable <uuid>'
|
||||
description: 'disable public URL for a device'
|
||||
help: '''
|
||||
Use this command to disable public URL for a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device public-url disable 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.disableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.getDeviceUrl =
|
||||
signature: 'device public-url <uuid>'
|
||||
description: 'gets the public URL of a device'
|
||||
help: '''
|
||||
Use this command to get the public URL of a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device public-url 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.getDeviceUrl(params.uuid).then (url) ->
|
||||
console.log(url)
|
||||
.nodeify(done)
|
||||
|
||||
exports.hasDeviceUrl =
|
||||
signature: 'device public-url status <uuid>'
|
||||
description: 'Returns true if public URL is enabled for a device'
|
||||
help: '''
|
||||
Use this command to determine if public URL is enabled for a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device public-url status 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.hasDeviceUrl(params.uuid).then (hasDeviceUrl) ->
|
||||
console.log(hasDeviceUrl)
|
||||
.nodeify(done)
|
||||
|
||||
exports.rename =
|
||||
signature: 'device rename <uuid> [newName]'
|
||||
description: 'rename a balena device'
|
||||
help: '''
|
||||
Use this command to rename a device.
|
||||
|
||||
If you omit the name, you'll get asked for it interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device rename 7cf02a6
|
||||
$ balena device rename 7cf02a6 MyPi
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
|
||||
Promise.try ->
|
||||
return params.newName if not _.isEmpty(params.newName)
|
||||
|
||||
form.ask
|
||||
message: 'How do you want to name this device?'
|
||||
type: 'input'
|
||||
|
||||
.then(_.partial(balena.models.device.rename, params.uuid))
|
||||
.nodeify(done)
|
||||
|
||||
exports.move =
|
||||
signature: 'device move <uuid>'
|
||||
description: 'move a device to another application'
|
||||
help: '''
|
||||
Use this command to move a device to another application you own.
|
||||
|
||||
If you omit the application, you'll get asked for it interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device move 7cf02a6
|
||||
$ balena device move 7cf02a6 --application MyNewApp
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [ commandOptions.optionalApplication ]
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
balena.models.device.get(params.uuid, expandForAppName).then (device) ->
|
||||
return options.application if options.application
|
||||
|
||||
return Promise.all([
|
||||
balena.models.device.getManifestBySlug(device.device_type)
|
||||
balena.models.config.getDeviceTypes()
|
||||
]).then ([deviceDeviceType, deviceTypes]) ->
|
||||
compatibleDeviceTypes = deviceTypes.filter (dt) ->
|
||||
balena.models.os.isArchitectureCompatibleWith(deviceDeviceType.arch, dt.arch) &&
|
||||
!!dt.isDependent == !!deviceDeviceType.isDependent &&
|
||||
dt.state != 'DISCONTINUED'
|
||||
|
||||
return patterns.selectApplication (application) ->
|
||||
return _.every [
|
||||
_.some(compatibleDeviceTypes, (dt) -> dt.slug == application.device_type)
|
||||
device.belongs_to__application[0].app_name isnt application.app_name
|
||||
]
|
||||
.tap (application) ->
|
||||
return balena.models.device.move(params.uuid, application)
|
||||
.then (application) ->
|
||||
console.info("#{params.uuid} was moved to #{application}")
|
||||
.nodeify(done)
|
||||
|
||||
exports.init =
|
||||
signature: 'device init'
|
||||
description: 'initialise a device with balenaOS'
|
||||
help: '''
|
||||
Use this command to download the OS image of a certain application and write it to an SD Card.
|
||||
|
||||
Notice this command may ask for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device init
|
||||
$ balena device init --application MyApp
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.yes
|
||||
commandOptions.advancedConfig
|
||||
_.assign({}, commandOptions.osVersionOrSemver, { signature: 'os-version', parameter: 'os-version' })
|
||||
commandOptions.drive
|
||||
{
|
||||
signature: 'config'
|
||||
description: 'path to the config JSON file, see `balena os build-config`'
|
||||
parameter: 'config'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
rimraf = Promise.promisify(require('rimraf'))
|
||||
tmp = require('tmp')
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
||||
tmp.setGracefulCleanup()
|
||||
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
|
||||
Promise.try ->
|
||||
return options.application if options.application?
|
||||
return patterns.selectApplication()
|
||||
.then(balena.models.application.get)
|
||||
.then (application) ->
|
||||
|
||||
download = ->
|
||||
tmpNameAsync().then (tempPath) ->
|
||||
osVersion = options['os-version'] or 'default'
|
||||
runCommand("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
|
||||
.disposer (tempPath) ->
|
||||
return rimraf(tempPath)
|
||||
|
||||
Promise.using download(), (tempPath) ->
|
||||
runCommand("device register #{application.app_name}")
|
||||
.then(balena.models.device.get)
|
||||
.tap (device) ->
|
||||
configureCommand = "os configure '#{tempPath}' --device #{device.uuid}"
|
||||
if options.config
|
||||
configureCommand += " --config '#{options.config}'"
|
||||
else if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
runCommand(configureCommand)
|
||||
.then ->
|
||||
osInitCommand = "os initialize '#{tempPath}' --type #{application.device_type}"
|
||||
if options.yes
|
||||
osInitCommand += ' --yes'
|
||||
if options.drive
|
||||
osInitCommand += " --drive #{options.drive}"
|
||||
runCommand(osInitCommand)
|
||||
# Make sure the device resource is removed if there is an
|
||||
# error when configuring or initializing a device image
|
||||
.catch (error) ->
|
||||
balena.models.device.remove(device.uuid).finally ->
|
||||
throw error
|
||||
.then (device) ->
|
||||
console.log('Done')
|
||||
return device.uuid
|
||||
|
||||
.nodeify(done)
|
||||
|
||||
tsActions = require('./device_ts')
|
||||
exports.osUpdate = tsActions.osUpdate
|
||||
exports.supported = tsActions.supported
|
460
lib/actions/device.js
Normal file
460
lib/actions/device.js
Normal file
@ -0,0 +1,460 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as commandOptions from './command-options';
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { normalizeUuidProp } from '../utils/normalization';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
|
||||
/** @type {import('balena-sdk').PineOptionsFor<import('balena-sdk').Device>} */
|
||||
const expandForAppName = {
|
||||
$expand: { belongs_to__application: { $select: 'app_name' } },
|
||||
};
|
||||
|
||||
export const list = {
|
||||
signature: 'devices',
|
||||
description: 'list all devices',
|
||||
help: `\
|
||||
Use this command to list all devices that belong to you.
|
||||
|
||||
You can filter the devices by application by using the \`--application\` option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena devices
|
||||
$ balena devices --application MyApp
|
||||
$ balena devices --app MyApp
|
||||
$ balena devices -a MyApp\
|
||||
`,
|
||||
options: [commandOptions.optionalApplication],
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
action(_params, options) {
|
||||
const Promise = require('bluebird');
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
return Promise.try(function () {
|
||||
if (options.application != null) {
|
||||
return balena.models.device.getAllByApplication(
|
||||
options.application,
|
||||
expandForAppName,
|
||||
);
|
||||
}
|
||||
return balena.models.device.getAll(expandForAppName);
|
||||
}).tap(function (devices) {
|
||||
devices = _.map(devices, function (device) {
|
||||
// @ts-ignore extending the device object with extra props
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(
|
||||
device.uuid,
|
||||
);
|
||||
// @ts-ignore extending the device object with extra props
|
||||
device.application_name = device.belongs_to__application?.[0]
|
||||
? device.belongs_to__application[0].app_name
|
||||
: 'N/a';
|
||||
device.uuid = device.uuid.slice(0, 7);
|
||||
return device;
|
||||
});
|
||||
|
||||
console.log(
|
||||
getVisuals().table.horizontal(devices, [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'application_name',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
]),
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const info = {
|
||||
signature: 'device <uuid>',
|
||||
description: 'list a single device',
|
||||
help: `\
|
||||
Use this command to show information about a single device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device 7cf02a6\
|
||||
`,
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
action(params) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
return balena.models.device
|
||||
.get(params.uuid, expandForAppName)
|
||||
.then((device) =>
|
||||
// @ts-ignore `device.getStatus` requires a device with service info, but
|
||||
// this device isn't typed with them, possibly needs fixing?
|
||||
balena.models.device.getStatus(params.uuid).then(function (status) {
|
||||
device.status = status;
|
||||
// @ts-ignore extending the device object with extra props
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(
|
||||
device.uuid,
|
||||
);
|
||||
// @ts-ignore extending the device object with extra props
|
||||
device.application_name = device.belongs_to__application?.[0]
|
||||
? device.belongs_to__application[0].app_name
|
||||
: 'N/a';
|
||||
// @ts-ignore extending the device object with extra props
|
||||
device.commit = device.is_on__commit;
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(device, [
|
||||
`$${device.device_name}$`,
|
||||
'id',
|
||||
'device_type',
|
||||
'status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'mac_address',
|
||||
'application_name',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
]),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const register = {
|
||||
signature: 'device register <application>',
|
||||
description: 'register a device',
|
||||
help: `\
|
||||
Use this command to register a device to an application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device register MyApp
|
||||
$ balena device register MyApp --uuid <uuid>\
|
||||
`,
|
||||
permission: 'user',
|
||||
options: [
|
||||
{
|
||||
signature: 'uuid',
|
||||
description: 'custom uuid',
|
||||
parameter: 'uuid',
|
||||
alias: 'u',
|
||||
},
|
||||
],
|
||||
action(params, options) {
|
||||
const Promise = require('bluebird');
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
return Promise.join(
|
||||
balena.models.application.get(params.application),
|
||||
options.uuid ?? balena.models.device.generateUniqueKey(),
|
||||
function (application, uuid) {
|
||||
console.info(`Registering to ${application.app_name}: ${uuid}`);
|
||||
return balena.models.device.register(application.id, uuid);
|
||||
},
|
||||
).get('uuid');
|
||||
},
|
||||
};
|
||||
|
||||
export const remove = {
|
||||
signature: 'device rm <uuid>',
|
||||
description: 'remove a device',
|
||||
help: `\
|
||||
Use this command to remove a device from balena.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the \`--yes\` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device rm 7cf02a6
|
||||
$ balena device rm 7cf02a6 --yes\
|
||||
`,
|
||||
options: [commandOptions.yes],
|
||||
permission: 'user',
|
||||
action(params, options) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = require('../utils/patterns');
|
||||
|
||||
return patterns
|
||||
.confirm(options.yes, 'Are you sure you want to delete the device?')
|
||||
.then(() => balena.models.device.remove(params.uuid));
|
||||
},
|
||||
};
|
||||
|
||||
export const identify = {
|
||||
signature: 'device identify <uuid>',
|
||||
description: 'identify a device with a UUID',
|
||||
help: `\
|
||||
Use this command to identify a device.
|
||||
|
||||
In the Raspberry Pi, the ACT led is blinked several times.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device identify 23c73a1\
|
||||
`,
|
||||
permission: 'user',
|
||||
action(params) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = getBalenaSdk();
|
||||
return balena.models.device.identify(params.uuid);
|
||||
},
|
||||
};
|
||||
|
||||
export const reboot = {
|
||||
signature: 'device reboot <uuid>',
|
||||
description: 'restart a device',
|
||||
help: `\
|
||||
Use this command to remotely reboot a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device reboot 23c73a1\
|
||||
`,
|
||||
options: [commandOptions.forceUpdateLock],
|
||||
permission: 'user',
|
||||
action(params, options) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = getBalenaSdk();
|
||||
return balena.models.device.reboot(params.uuid, options);
|
||||
},
|
||||
};
|
||||
|
||||
export const shutdown = {
|
||||
signature: 'device shutdown <uuid>',
|
||||
description: 'shutdown a device',
|
||||
help: `\
|
||||
Use this command to remotely shutdown a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device shutdown 23c73a1\
|
||||
`,
|
||||
options: [commandOptions.forceUpdateLock],
|
||||
permission: 'user',
|
||||
action(params, options) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = getBalenaSdk();
|
||||
return balena.models.device.shutdown(params.uuid, options);
|
||||
},
|
||||
};
|
||||
|
||||
export const rename = {
|
||||
signature: 'device rename <uuid> [newName]',
|
||||
description: 'rename a balena device',
|
||||
help: `\
|
||||
Use this command to rename a device.
|
||||
|
||||
If you omit the name, you'll get asked for it interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device rename 7cf02a6
|
||||
$ balena device rename 7cf02a6 MyPi\
|
||||
`,
|
||||
permission: 'user',
|
||||
action(params) {
|
||||
normalizeUuidProp(params);
|
||||
const Promise = require('bluebird');
|
||||
const balena = getBalenaSdk();
|
||||
const form = require('resin-cli-form');
|
||||
|
||||
return Promise.try(function () {
|
||||
if (!_.isEmpty(params.newName)) {
|
||||
return params.newName;
|
||||
}
|
||||
|
||||
return form.ask({
|
||||
message: 'How do you want to name this device?',
|
||||
type: 'input',
|
||||
});
|
||||
}).then(_.partial(balena.models.device.rename, params.uuid));
|
||||
},
|
||||
};
|
||||
|
||||
export const move = {
|
||||
signature: 'device move <uuid>',
|
||||
description: 'move a device to another application',
|
||||
help: `\
|
||||
Use this command to move a device to another application you own.
|
||||
|
||||
If you omit the application, you'll get asked for it interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device move 7cf02a6
|
||||
$ balena device move 7cf02a6 --application MyNewApp\
|
||||
`,
|
||||
permission: 'user',
|
||||
options: [commandOptions.optionalApplication],
|
||||
action(params, options) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = require('../utils/patterns');
|
||||
|
||||
return balena.models.device
|
||||
.get(params.uuid, expandForAppName)
|
||||
.then(function (device) {
|
||||
// @ts-ignore extending the device object with extra props
|
||||
device.application_name = device.belongs_to__application?.[0]
|
||||
? device.belongs_to__application[0].app_name
|
||||
: 'N/a';
|
||||
if (options.application) {
|
||||
return options.application;
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
balena.models.device.getManifestBySlug(device.device_type),
|
||||
balena.models.config.getDeviceTypes(),
|
||||
]).then(function ([deviceDeviceType, deviceTypes]) {
|
||||
const compatibleDeviceTypes = deviceTypes.filter(
|
||||
(dt) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceDeviceType.arch,
|
||||
dt.arch,
|
||||
) &&
|
||||
!!dt.isDependent === !!deviceDeviceType.isDependent &&
|
||||
dt.state !== 'DISCONTINUED',
|
||||
);
|
||||
|
||||
return patterns.selectApplication((application) =>
|
||||
_.every([
|
||||
_.some(
|
||||
compatibleDeviceTypes,
|
||||
(dt) => dt.slug === application.device_type,
|
||||
),
|
||||
// @ts-ignore using the extended device object prop
|
||||
device.application_name !== application.app_name,
|
||||
]),
|
||||
);
|
||||
});
|
||||
})
|
||||
.tap((application) => balena.models.device.move(params.uuid, application))
|
||||
.then((application) => {
|
||||
console.info(`${params.uuid} was moved to ${application}`);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const init = {
|
||||
signature: 'device init',
|
||||
description: 'initialise a device with balenaOS',
|
||||
help: `\
|
||||
Use this command to download the OS image of a certain application and write it to an SD Card.
|
||||
|
||||
Notice this command may ask for confirmation interactively.
|
||||
You can avoid this by passing the \`--yes\` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device init
|
||||
$ balena device init --application MyApp\
|
||||
`,
|
||||
options: [
|
||||
commandOptions.optionalApplication,
|
||||
commandOptions.yes,
|
||||
commandOptions.advancedConfig,
|
||||
_.assign({}, commandOptions.osVersionOrSemver, {
|
||||
signature: 'os-version',
|
||||
parameter: 'os-version',
|
||||
}),
|
||||
commandOptions.drive,
|
||||
{
|
||||
signature: 'config',
|
||||
description: 'path to the config JSON file, see `balena os build-config`',
|
||||
parameter: 'config',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
action(_params, options) {
|
||||
const Promise = require('bluebird');
|
||||
const rimraf = Promise.promisify(require('rimraf'));
|
||||
const tmp = require('tmp');
|
||||
const tmpNameAsync = Promise.promisify(tmp.tmpName);
|
||||
tmp.setGracefulCleanup();
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = require('../utils/patterns');
|
||||
const { runCommand } = require('../utils/helpers');
|
||||
|
||||
return Promise.try(function () {
|
||||
if (options.application != null) {
|
||||
return options.application;
|
||||
}
|
||||
return patterns.selectApplication();
|
||||
})
|
||||
.then(balena.models.application.get)
|
||||
.then(function (application) {
|
||||
const download = () =>
|
||||
tmpNameAsync()
|
||||
.then(function (tempPath) {
|
||||
const osVersion = options['os-version'] || 'default';
|
||||
return runCommand(
|
||||
`os download ${application.device_type} --output '${tempPath}' --version ${osVersion}`,
|
||||
);
|
||||
})
|
||||
.disposer((tempPath) => rimraf(tempPath));
|
||||
|
||||
return Promise.using(download(), (tempPath) =>
|
||||
runCommand(`device register ${application.app_name}`)
|
||||
.then(balena.models.device.get)
|
||||
.tap(function (device) {
|
||||
let configureCommand = `os configure '${tempPath}' --device ${device.uuid}`;
|
||||
if (options.config) {
|
||||
configureCommand += ` --config '${options.config}'`;
|
||||
} else if (options.advanced) {
|
||||
configureCommand += ' --advanced';
|
||||
}
|
||||
return runCommand(configureCommand)
|
||||
.then(function () {
|
||||
let osInitCommand = `os initialize '${tempPath}' --type ${application.device_type}`;
|
||||
if (options.yes) {
|
||||
osInitCommand += ' --yes';
|
||||
}
|
||||
if (options.drive) {
|
||||
osInitCommand += ` --drive ${options.drive}`;
|
||||
}
|
||||
return runCommand(osInitCommand);
|
||||
})
|
||||
.catch((error) =>
|
||||
balena.models.device.remove(device.uuid).finally(function () {
|
||||
throw error;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
).then(function (device) {
|
||||
console.log('Done');
|
||||
return device.uuid;
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export { osUpdate } from './device_ts';
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
Copyright 2016-2020 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -16,34 +16,11 @@ limitations under the License.
|
||||
import { Device } from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
import { normalizeUuidProp } from '../utils/normalization';
|
||||
import * as commandOptions from './command-options';
|
||||
|
||||
export const supported: CommandDefinition<{}, {}> = {
|
||||
signature: 'devices supported',
|
||||
description: 'list all supported devices',
|
||||
help: stripIndent`
|
||||
Use this command to get the list of all supported devices.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena devices supported
|
||||
`,
|
||||
async action(_params, _options) {
|
||||
const sdk = (await import('balena-sdk')).fromSharedOptions();
|
||||
const visuals = await import('resin-cli-visuals');
|
||||
const _ = await import('lodash');
|
||||
|
||||
let deviceTypes = await sdk.models.config.getDeviceTypes();
|
||||
const fields = ['slug', 'name'];
|
||||
deviceTypes = _.sortBy(deviceTypes, fields).filter(
|
||||
dt => dt.state !== 'DISCONTINUED',
|
||||
);
|
||||
const output = await visuals.table.horizontal(deviceTypes, fields);
|
||||
console.log(output);
|
||||
},
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:no-namespace
|
||||
namespace OsUpdate {
|
||||
export interface Args {
|
||||
@ -63,6 +40,8 @@ export const osUpdate: CommandDefinition<OsUpdate.Args, OsUpdate.Options> = {
|
||||
Notice this command will ask for confirmation interactively.
|
||||
You can avoid this by passing the \`--yes\` boolean option.
|
||||
|
||||
Requires balenaCloud; will not work with openBalena or standalone balenaOS.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device os-update 23c73a1
|
||||
@ -70,71 +49,74 @@ export const osUpdate: CommandDefinition<OsUpdate.Args, OsUpdate.Options> = {
|
||||
`,
|
||||
options: [commandOptions.optionalOsVersion, commandOptions.yes],
|
||||
permission: 'user',
|
||||
async action(params, options, done) {
|
||||
async action(params, options) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = await import('balena-sdk');
|
||||
const _ = await import('lodash');
|
||||
const sdk = balena.fromSharedOptions();
|
||||
const sdk = getBalenaSdk();
|
||||
const patterns = await import('../utils/patterns');
|
||||
const form = await import('resin-cli-form');
|
||||
|
||||
return sdk.models.device
|
||||
.get(params.uuid, {
|
||||
$select: ['uuid', 'device_type', 'os_version', 'os_variant'],
|
||||
})
|
||||
.then(async ({ uuid, device_type, os_version, os_variant }) => {
|
||||
const currentOsVersion = sdk.models.device.getOsVersion({
|
||||
os_version,
|
||||
os_variant,
|
||||
} as Device);
|
||||
if (!currentOsVersion) {
|
||||
patterns.exitWithExpectedError(
|
||||
'The current os version of the device is not available',
|
||||
);
|
||||
// Just to make TS happy
|
||||
return;
|
||||
}
|
||||
// Get device info
|
||||
const {
|
||||
uuid,
|
||||
device_type,
|
||||
os_version,
|
||||
os_variant,
|
||||
} = await sdk.models.device.get(params.uuid, {
|
||||
$select: ['uuid', 'device_type', 'os_version', 'os_variant'],
|
||||
});
|
||||
|
||||
return sdk.models.os
|
||||
.getSupportedOsUpdateVersions(device_type, currentOsVersion)
|
||||
.then(hupVersionInfo => {
|
||||
if (hupVersionInfo.versions.length === 0) {
|
||||
patterns.exitWithExpectedError(
|
||||
'There are no available Host OS update targets for this device',
|
||||
);
|
||||
}
|
||||
// Get current device OS version
|
||||
const currentOsVersion = sdk.models.device.getOsVersion({
|
||||
os_version,
|
||||
os_variant,
|
||||
} as Device);
|
||||
if (!currentOsVersion) {
|
||||
throw new ExpectedError(
|
||||
'The current os version of the device is not available',
|
||||
);
|
||||
}
|
||||
|
||||
if (options.version != null) {
|
||||
if (!_.includes(hupVersionInfo.versions, options.version)) {
|
||||
patterns.exitWithExpectedError(
|
||||
'The provided version is not in the Host OS update targets for this device',
|
||||
);
|
||||
}
|
||||
return options.version;
|
||||
}
|
||||
// Get supported OS update versions
|
||||
const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions(
|
||||
device_type,
|
||||
currentOsVersion,
|
||||
);
|
||||
if (hupVersionInfo.versions.length === 0) {
|
||||
throw new ExpectedError(
|
||||
'There are no available Host OS update targets for this device',
|
||||
);
|
||||
}
|
||||
|
||||
return form.ask({
|
||||
message: 'Target OS version',
|
||||
type: 'list',
|
||||
choices: hupVersionInfo.versions.map(version => ({
|
||||
name:
|
||||
hupVersionInfo.recommended === version
|
||||
? `${version} (recommended)`
|
||||
: version,
|
||||
value: version,
|
||||
})),
|
||||
});
|
||||
})
|
||||
.then(version =>
|
||||
patterns
|
||||
.confirm(
|
||||
options.yes || false,
|
||||
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
|
||||
)
|
||||
.then(() => sdk.models.device.startOsUpdate(uuid, version))
|
||||
.then(() => patterns.awaitDeviceOsUpdate(uuid, version)),
|
||||
);
|
||||
})
|
||||
.nodeify(done);
|
||||
// Get target OS version
|
||||
let targetOsVersion = options.version;
|
||||
if (targetOsVersion != null) {
|
||||
if (!_.includes(hupVersionInfo.versions, targetOsVersion)) {
|
||||
throw new ExpectedError(
|
||||
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
targetOsVersion = await form.ask({
|
||||
message: 'Target OS version',
|
||||
type: 'list',
|
||||
choices: hupVersionInfo.versions.map((version) => ({
|
||||
name:
|
||||
hupVersionInfo.recommended === version
|
||||
? `${version} (recommended)`
|
||||
: version,
|
||||
value: version,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// Confirm and start update
|
||||
await patterns.confirm(
|
||||
options.yes || false,
|
||||
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
|
||||
);
|
||||
|
||||
await sdk.models.device.startOsUpdate(uuid, targetOsVersion);
|
||||
await patterns.awaitDeviceOsUpdate(uuid, targetOsVersion);
|
||||
},
|
||||
};
|
||||
|
@ -1,150 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
_ = require('lodash')
|
||||
capitano = require('capitano')
|
||||
columnify = require('columnify')
|
||||
|
||||
messages = require('../utils/messages')
|
||||
{ getManualSortCompareFunction } = require('../utils/helpers')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
{ getOclifHelpLinePairs } = require('./help_ts')
|
||||
|
||||
parse = (object) ->
|
||||
return _.map object, (item) ->
|
||||
|
||||
# Hacky way to determine if an object is
|
||||
# a function or a command
|
||||
if item.alias?
|
||||
signature = item.toString()
|
||||
else
|
||||
signature = item.signature.toString()
|
||||
|
||||
return [
|
||||
signature
|
||||
item.description
|
||||
]
|
||||
|
||||
indent = (text) ->
|
||||
text = _.map text.split('\n'), (line) ->
|
||||
return ' ' + line
|
||||
return text.join('\n')
|
||||
|
||||
print = (usageDescriptionPairs) ->
|
||||
console.log indent columnify _.fromPairs(usageDescriptionPairs),
|
||||
showHeaders: false
|
||||
minWidth: 35
|
||||
|
||||
manuallySortedPrimaryCommands = [
|
||||
'help',
|
||||
'login',
|
||||
'push',
|
||||
'logs',
|
||||
'ssh',
|
||||
'apps',
|
||||
'app',
|
||||
'devices',
|
||||
'device',
|
||||
'tunnel',
|
||||
'preload',
|
||||
'build',
|
||||
'deploy',
|
||||
'join',
|
||||
'leave',
|
||||
'local scan',
|
||||
]
|
||||
|
||||
general = (params, options, done) ->
|
||||
console.log('Usage: balena [COMMAND] [OPTIONS]\n')
|
||||
console.log(messages.reachingOut)
|
||||
console.log('\nPrimary commands:\n')
|
||||
|
||||
# We do not want the wildcard command
|
||||
# to be printed in the help screen.
|
||||
commands = _.reject capitano.state.commands, (command) ->
|
||||
return command.hidden or command.isWildcard()
|
||||
|
||||
groupedCommands = _.groupBy commands, (command) ->
|
||||
if command.primary
|
||||
return 'primary'
|
||||
return 'secondary'
|
||||
|
||||
print parse(groupedCommands.primary).sort(getManualSortCompareFunction(
|
||||
manuallySortedPrimaryCommands,
|
||||
([signature, description], manualItem) ->
|
||||
signature == manualItem or signature.startsWith("#{manualItem} ")
|
||||
))
|
||||
|
||||
if options.verbose
|
||||
console.log('\nAdditional commands:\n')
|
||||
secondaryCommandPromise = getOclifHelpLinePairs()
|
||||
.then (oclifHelpLinePairs) ->
|
||||
print parse(groupedCommands.secondary).concat(oclifHelpLinePairs).sort()
|
||||
else
|
||||
console.log('\nRun `balena help --verbose` to list additional commands')
|
||||
secondaryCommandPromise = Promise.resolve()
|
||||
|
||||
secondaryCommandPromise
|
||||
.then ->
|
||||
if not _.isEmpty(capitano.state.globalOptions)
|
||||
console.log('\nGlobal Options:\n')
|
||||
print parse(capitano.state.globalOptions).sort()
|
||||
done()
|
||||
.catch(done)
|
||||
|
||||
command = (params, options, done) ->
|
||||
capitano.state.getMatchCommand params.command, (error, command) ->
|
||||
return done(error) if error?
|
||||
|
||||
if not command? or command.isWildcard()
|
||||
exitWithExpectedError("Command not found: #{params.command}")
|
||||
|
||||
console.log("Usage: #{command.signature}")
|
||||
|
||||
if command.help?
|
||||
console.log("\n#{command.help}")
|
||||
else if command.description?
|
||||
console.log("\n#{_.capitalize(command.description)}")
|
||||
|
||||
if not _.isEmpty(command.options)
|
||||
console.log('\nOptions:\n')
|
||||
print parse(command.options).sort()
|
||||
|
||||
return done()
|
||||
|
||||
exports.help =
|
||||
signature: 'help [command...]'
|
||||
description: 'show help'
|
||||
help: '''
|
||||
Get detailed help for an specific command.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena help apps
|
||||
$ balena help os download
|
||||
'''
|
||||
primary: true
|
||||
options: [
|
||||
signature: 'verbose'
|
||||
description: 'show additional commands'
|
||||
boolean: true
|
||||
alias: 'v'
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
if params.command?
|
||||
command(params, options, done)
|
||||
else
|
||||
general(params, options, done)
|
190
lib/actions/help.js
Normal file
190
lib/actions/help.js
Normal file
@ -0,0 +1,190 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import * as capitano from 'capitano';
|
||||
import * as columnify from 'columnify';
|
||||
import * as messages from '../utils/messages';
|
||||
import { getManualSortCompareFunction } from '../utils/helpers';
|
||||
import { exitWithExpectedError } from '../errors';
|
||||
import { getOclifHelpLinePairs } from './help_ts';
|
||||
|
||||
const parse = (object) =>
|
||||
_.map(object, function (item) {
|
||||
// Hacky way to determine if an object is
|
||||
// a function or a command
|
||||
let signature;
|
||||
if (item.alias != null) {
|
||||
signature = item.toString();
|
||||
} else {
|
||||
signature = item.signature.toString();
|
||||
}
|
||||
|
||||
return [signature, item.description];
|
||||
});
|
||||
|
||||
const indent = function (text) {
|
||||
text = _.map(text.split('\n'), (line) => ' ' + line);
|
||||
return text.join('\n');
|
||||
};
|
||||
|
||||
const print = (usageDescriptionPairs) =>
|
||||
console.log(
|
||||
indent(
|
||||
columnify(_.fromPairs(usageDescriptionPairs), {
|
||||
showHeaders: false,
|
||||
minWidth: 35,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const manuallySortedPrimaryCommands = [
|
||||
'help',
|
||||
'login',
|
||||
'push',
|
||||
'logs',
|
||||
'ssh',
|
||||
'apps',
|
||||
'app',
|
||||
'devices',
|
||||
'device',
|
||||
'tunnel',
|
||||
'preload',
|
||||
'build',
|
||||
'deploy',
|
||||
'join',
|
||||
'leave',
|
||||
'local scan',
|
||||
];
|
||||
|
||||
const general = function (_params, options, done) {
|
||||
console.log('Usage: balena [COMMAND] [OPTIONS]\n');
|
||||
|
||||
console.log('Primary commands:\n');
|
||||
|
||||
// We do not want the wildcard command
|
||||
// to be printed in the help screen.
|
||||
const commands = capitano.state.commands.filter(
|
||||
(command) => !command.hidden && !command.isWildcard(),
|
||||
);
|
||||
|
||||
const capitanoCommands = _.groupBy(commands, function (command) {
|
||||
if (command.primary) {
|
||||
return 'primary';
|
||||
}
|
||||
return 'secondary';
|
||||
});
|
||||
|
||||
return getOclifHelpLinePairs()
|
||||
.then(function (oclifHelpLinePairs) {
|
||||
const primaryHelpLinePairs = parse(capitanoCommands.primary)
|
||||
.concat(oclifHelpLinePairs.primary)
|
||||
.sort(
|
||||
getManualSortCompareFunction(manuallySortedPrimaryCommands, function (
|
||||
[signature],
|
||||
manualItem,
|
||||
) {
|
||||
return (
|
||||
signature === manualItem || signature.startsWith(`${manualItem} `)
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const secondaryHelpLinePairs = parse(capitanoCommands.secondary)
|
||||
.concat(oclifHelpLinePairs.secondary)
|
||||
.sort();
|
||||
|
||||
print(primaryHelpLinePairs);
|
||||
|
||||
if (options.verbose) {
|
||||
console.log('\nAdditional commands:\n');
|
||||
print(secondaryHelpLinePairs);
|
||||
} else {
|
||||
console.log(
|
||||
'\nRun `balena help --verbose` to list additional commands',
|
||||
);
|
||||
}
|
||||
|
||||
if (!_.isEmpty(capitano.state.globalOptions)) {
|
||||
console.log('\nGlobal Options:\n');
|
||||
print(parse(capitano.state.globalOptions).sort());
|
||||
}
|
||||
console.log(indent('--debug\n'));
|
||||
|
||||
console.log(messages.help);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(done);
|
||||
};
|
||||
|
||||
const commandHelp = (params, _options, done) =>
|
||||
capitano.state.getMatchCommand(params.command, function (error, command) {
|
||||
if (error != null) {
|
||||
return done(error);
|
||||
}
|
||||
|
||||
if (command == null || command.isWildcard()) {
|
||||
exitWithExpectedError(`Command not found: ${params.command}`);
|
||||
}
|
||||
|
||||
console.log(`Usage: ${command.signature}`);
|
||||
|
||||
if (command.help != null) {
|
||||
console.log(`\n${command.help}`);
|
||||
} else if (command.description != null) {
|
||||
console.log(`\n${_.capitalize(command.description)}`);
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.options)) {
|
||||
console.log('\nOptions:\n');
|
||||
print(parse(command.options).sort());
|
||||
}
|
||||
|
||||
console.log();
|
||||
|
||||
return done();
|
||||
});
|
||||
|
||||
export const help = {
|
||||
signature: 'help [command...]',
|
||||
description: 'show help',
|
||||
help: `\
|
||||
Get detailed help for an specific command.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena help apps
|
||||
$ balena help os download\
|
||||
`,
|
||||
primary: true,
|
||||
options: [
|
||||
{
|
||||
signature: 'verbose',
|
||||
description: 'show additional commands',
|
||||
boolean: true,
|
||||
alias: 'v',
|
||||
},
|
||||
],
|
||||
action(params, options, done) {
|
||||
if (params.command != null) {
|
||||
return commandHelp(params, options, done);
|
||||
} else {
|
||||
return general(params, options, done);
|
||||
}
|
||||
},
|
||||
};
|
@ -15,32 +15,40 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command } from '@oclif/command';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import Command from '../command';
|
||||
|
||||
import { capitanoizeOclifUsage } from '../utils/oclif-utils';
|
||||
|
||||
export async function getOclifHelpLinePairs(): Promise<
|
||||
Array<[string, string]>
|
||||
> {
|
||||
export async function getOclifHelpLinePairs() {
|
||||
const { convertedCommands } = await import('../preparser');
|
||||
const cmdClasses: Array<Promise<typeof Command>> = [];
|
||||
const primary: Array<[string, string]> = [];
|
||||
const secondary: Array<[string, string]> = [];
|
||||
|
||||
for (const convertedCmd of convertedCommands) {
|
||||
const [topic, cmd] = convertedCmd.split(':');
|
||||
const pathComponents = ['..', 'actions-oclif', topic];
|
||||
if (cmd) {
|
||||
pathComponents.push(cmd);
|
||||
}
|
||||
// note that `import(path)` returns a promise
|
||||
cmdClasses.push(import(path.join(...pathComponents)));
|
||||
|
||||
const cmdModule = await import(path.join(...pathComponents));
|
||||
const command: typeof Command = cmdModule.default;
|
||||
|
||||
if (!command.hidden) {
|
||||
if (command.primary) {
|
||||
primary.push(getCmdUsageDescriptionLinePair(command));
|
||||
} else {
|
||||
secondary.push(getCmdUsageDescriptionLinePair(command));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Bluebird.map(cmdClasses, getCmdUsageDescriptionLinePair);
|
||||
|
||||
return { primary, secondary };
|
||||
}
|
||||
|
||||
function getCmdUsageDescriptionLinePair(cmdModule: any): [string, string] {
|
||||
const cmd: typeof Command = cmdModule.default;
|
||||
function getCmdUsageDescriptionLinePair(cmd: typeof Command): [string, string] {
|
||||
const usage = capitanoizeOclifUsage(cmd.usage);
|
||||
let description = '';
|
||||
// note: [^] matches any characters (including line breaks), achieving the
|
||||
|
@ -1,41 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
apiKey: require('./api-key')
|
||||
app: require('./app')
|
||||
auth: require('./auth')
|
||||
device: require('./device')
|
||||
tags: require('./tags')
|
||||
keys: require('./keys')
|
||||
logs: require('./logs')
|
||||
local: require('./local')
|
||||
scan: require('./scan')
|
||||
notes: require('./notes')
|
||||
help: require('./help')
|
||||
os: require('./os')
|
||||
settings: require('./settings')
|
||||
config: require('./config')
|
||||
ssh: require('./ssh')
|
||||
internal: require('./internal')
|
||||
build: require('./build')
|
||||
deploy: require('./deploy')
|
||||
util: require('./util')
|
||||
preload: require('./preload')
|
||||
push: require('./push')
|
||||
join: require('./join')
|
||||
leave: require('./leave')
|
||||
tunnel: require('./tunnel')
|
35
lib/actions/index.ts
Normal file
35
lib/actions/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as auth from './auth';
|
||||
import * as config from './config';
|
||||
import * as device from './device';
|
||||
import * as help from './help';
|
||||
import * as local from './local';
|
||||
import * as logs from './logs';
|
||||
import * as os from './os';
|
||||
import * as push from './push';
|
||||
import * as ssh from './ssh';
|
||||
import * as tunnel from './tunnel';
|
||||
import * as util from './util';
|
||||
|
||||
export { auth, device, logs, local, help, os, config, ssh, util, push, tunnel };
|
||||
|
||||
export { build } from './build';
|
||||
|
||||
export { deploy } from './deploy';
|
||||
|
||||
export { preload } from './preload';
|
@ -1,56 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
# These are internal commands we want to be runnable from the outside
|
||||
# One use-case for this is spawning the minimal operation with root priviledges
|
||||
|
||||
exports.osInit =
|
||||
signature: 'internal osinit <image> <type> <config>'
|
||||
description: 'do actual init of the device with the preconfigured os image'
|
||||
help: '''
|
||||
Don't use this command directly! Use `balena os initialize <image>` instead.
|
||||
'''
|
||||
hidden: true
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
init = require('balena-device-init')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
configPromise = Promise.try -> JSON.parse(params.config)
|
||||
manifestPromise = helpers.getManifest(params.image, params.type)
|
||||
Promise.join configPromise, manifestPromise, (config, manifest) ->
|
||||
init.initialize(params.image, manifest, config)
|
||||
.then(helpers.osProgressHandler)
|
||||
.nodeify(done)
|
||||
|
||||
exports.scanDevices =
|
||||
signature: 'internal scandevices'
|
||||
description: 'scan for local balena-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('balena-sync')
|
||||
|
||||
return Promise.try ->
|
||||
forms.selectLocalBalenaOsDevice()
|
||||
.then (hostnameOrIp) ->
|
||||
console.error("==> Selected device: #{hostnameOrIp}")
|
||||
.nodeify(done)
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
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 balenaOS to join an application on a balena server',
|
||||
help: stripIndent`
|
||||
Use this command to move a local device to an application on another balena server.
|
||||
|
||||
For example, you could provision a device against an openBalena installation
|
||||
where you perform end-to-end tests and then move it to balenaCloud when it's
|
||||
ready for production.
|
||||
|
||||
Moving a device between applications on the same server is not supported.
|
||||
|
||||
If you don't specify a device hostname or IP, this command will automatically
|
||||
scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This usually requires root privileges.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena join
|
||||
$ balena join balena.local
|
||||
$ balena join balena.local --application MyApp
|
||||
$ balena join 192.168.1.25
|
||||
$ balena 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 balena = await import('balena-sdk');
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = balena.fromSharedOptions();
|
||||
const logger = Logger.getLogger();
|
||||
return Bluebird.try(() => {
|
||||
return promote.join(logger, sdk, params.deviceIp, options.application);
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
@ -1,123 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
|
||||
exports.list =
|
||||
signature: 'keys'
|
||||
description: 'list all ssh keys'
|
||||
help: '''
|
||||
Use this command to list all your SSH keys.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena keys
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
balena.models.key.getAll().then (keys) ->
|
||||
console.log visuals.table.horizontal keys, [
|
||||
'id'
|
||||
'title'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.info =
|
||||
signature: 'key <id>'
|
||||
description: 'list a single ssh key'
|
||||
help: '''
|
||||
Use this command to show information about a single SSH key.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena key 17
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
balena.models.key.get(params.id).then (key) ->
|
||||
console.log visuals.table.vertical key, [
|
||||
'id'
|
||||
'title'
|
||||
]
|
||||
|
||||
# Since the public key string is long, it might
|
||||
# wrap to lines below, causing the table layout to break.
|
||||
# See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key)
|
||||
.nodeify(done)
|
||||
|
||||
exports.remove =
|
||||
signature: 'key rm <id>'
|
||||
description: 'remove a ssh key'
|
||||
help: '''
|
||||
Use this command to remove a SSH key from balena.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena key rm 17
|
||||
$ balena key rm 17 --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the key?').then ->
|
||||
balena.models.key.remove(params.id)
|
||||
.nodeify(done)
|
||||
|
||||
exports.add =
|
||||
signature: 'key add <name> [path]'
|
||||
description: 'add a SSH key to balena'
|
||||
help: '''
|
||||
Use this command to associate a new SSH key with your account.
|
||||
|
||||
If `path` is omitted, the command will attempt
|
||||
to read the SSH key from stdin.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena key add Main ~/.ssh/id_rsa.pub
|
||||
$ cat ~/.ssh/id_rsa.pub | balena key add Main
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
readFileAsync = Promise.promisify(require('fs').readFile)
|
||||
capitano = require('capitano')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
Promise.try ->
|
||||
return readFileAsync(params.path, encoding: 'utf8') if params.path?
|
||||
|
||||
# TODO: should this be promisified for consistency?
|
||||
Promise.fromNode (callback) ->
|
||||
capitano.utils.getStdin (data) ->
|
||||
return callback(null, data)
|
||||
|
||||
.then(_.partial(balena.models.key.create, params.name))
|
||||
.nodeify(done)
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
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 balena application',
|
||||
help: stripIndent`
|
||||
Use this command to make a local device leave the balena server it is
|
||||
provisioned on. This effectively makes the device "unmanaged".
|
||||
|
||||
The device entry on the server is preserved after running this command,
|
||||
so the device can subsequently re-join the server if needed.
|
||||
|
||||
If you don't specify a device hostname or IP, this command will automatically
|
||||
scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This usually requires root privileges.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena leave
|
||||
$ balena leave balena.local
|
||||
$ balena leave 192.168.1.25
|
||||
`,
|
||||
options: [],
|
||||
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
|
||||
async action(params, _options, done) {
|
||||
const balena = await import('balena-sdk');
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = balena.fromSharedOptions();
|
||||
const logger = Logger.getLogger();
|
||||
return Bluebird.try(() => {
|
||||
return promote.leave(logger, sdk, params.deviceIp);
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
@ -1,62 +0,0 @@
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
chalk = require('chalk')
|
||||
|
||||
dockerUtils = require('../../utils/docker')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
exports.dockerPort = dockerPort = 2375
|
||||
exports.dockerTimeout = dockerTimeout = 2000
|
||||
|
||||
exports.filterOutSupervisorContainer = filterOutSupervisorContainer = (container) ->
|
||||
for name in container.Names
|
||||
return false if (name.includes('resin_supervisor') or name.includes('balena_supervisor'))
|
||||
return true
|
||||
|
||||
exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor = false) ->
|
||||
form = require('resin-cli-form')
|
||||
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
|
||||
|
||||
# List all containers, including those not running
|
||||
docker.listContainersAsync(all: true)
|
||||
.filter (container) ->
|
||||
return true if not filterSupervisor
|
||||
filterOutSupervisorContainer(container)
|
||||
.then (containers) ->
|
||||
if _.isEmpty(containers)
|
||||
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'
|
||||
shortContainerId = ('' + container.Id).substr(0, 11)
|
||||
|
||||
return {
|
||||
name: "#{containerName} (#{shortContainerId})"
|
||||
value: container.Id
|
||||
}
|
||||
|
||||
exports.pipeContainerStream = Promise.method ({ deviceIp, name, outStream, follow = false }) ->
|
||||
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort)
|
||||
|
||||
container = docker.getContainer(name)
|
||||
container.inspectAsync()
|
||||
.then (containerInfo) ->
|
||||
return containerInfo?.State?.Running
|
||||
.then (isRunning) ->
|
||||
container.attachAsync
|
||||
logs: not follow or not isRunning
|
||||
stream: follow and isRunning
|
||||
stdout: true
|
||||
stderr: true
|
||||
.then (containerStream) ->
|
||||
containerStream.pipe(outStream)
|
||||
.catch (err) ->
|
||||
err = '' + err.statusCode
|
||||
if err is '404'
|
||||
return console.log(chalk.red.bold("Container '#{name}' not found."))
|
||||
throw err
|
||||
|
||||
exports.getSubShellCommand = require('../../utils/helpers').getSubShellCommand
|
97
lib/actions/local/common.js
Normal file
97
lib/actions/local/common.js
Normal file
@ -0,0 +1,97 @@
|
||||
import * as Promise from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as dockerUtils from '../../utils/docker';
|
||||
import { exitWithExpectedError } from '../../errors';
|
||||
import { getChalk } from '../../utils/lazy';
|
||||
|
||||
export const dockerPort = 2375;
|
||||
export const dockerTimeout = 2000;
|
||||
|
||||
export const filterOutSupervisorContainer = function (container) {
|
||||
for (const name of container.Names) {
|
||||
if (
|
||||
name.includes('resin_supervisor') ||
|
||||
name.includes('balena_supervisor')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const selectContainerFromDevice = Promise.method(function (
|
||||
deviceIp,
|
||||
filterSupervisor,
|
||||
) {
|
||||
if (filterSupervisor == null) {
|
||||
filterSupervisor = false;
|
||||
}
|
||||
const form = require('resin-cli-form');
|
||||
const docker = dockerUtils.createClient({
|
||||
host: deviceIp,
|
||||
port: dockerPort,
|
||||
timeout: dockerTimeout,
|
||||
});
|
||||
|
||||
// List all containers, including those not running
|
||||
return docker.listContainers({ all: true }).then(function (containers) {
|
||||
containers = containers.filter(function (container) {
|
||||
if (!filterSupervisor) {
|
||||
return true;
|
||||
}
|
||||
return filterOutSupervisorContainer(container);
|
||||
});
|
||||
if (_.isEmpty(containers)) {
|
||||
exitWithExpectedError(`No containers found in ${deviceIp}`);
|
||||
}
|
||||
|
||||
return form.ask({
|
||||
message: 'Select a container',
|
||||
type: 'list',
|
||||
choices: _.map(containers, function (container) {
|
||||
const containerName = container.Names?.[0] || 'Untitled';
|
||||
const shortContainerId = ('' + container.Id).substr(0, 11);
|
||||
|
||||
return {
|
||||
name: `${containerName} (${shortContainerId})`,
|
||||
value: container.Id,
|
||||
};
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export const pipeContainerStream = Promise.method(function ({
|
||||
deviceIp,
|
||||
name,
|
||||
outStream,
|
||||
follow,
|
||||
}) {
|
||||
if (follow == null) {
|
||||
follow = false;
|
||||
}
|
||||
const docker = dockerUtils.createClient({ host: deviceIp, port: dockerPort });
|
||||
|
||||
const container = docker.getContainer(name);
|
||||
return container
|
||||
.inspect()
|
||||
.then((containerInfo) => containerInfo?.State?.Running)
|
||||
.then((isRunning) =>
|
||||
container.attach({
|
||||
logs: !follow || !isRunning,
|
||||
stream: follow && isRunning,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
}),
|
||||
)
|
||||
.then((containerStream) => containerStream.pipe(outStream))
|
||||
.catch(function (err) {
|
||||
err = '' + err.statusCode;
|
||||
if (err === '404') {
|
||||
return console.log(
|
||||
getChalk().red.bold(`Container '${name}' not found.`),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
});
|
@ -1,237 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
BOOT_PARTITION = 1
|
||||
CONNECTIONS_FOLDER = '/system-connections'
|
||||
|
||||
getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
|
||||
mapper: [
|
||||
{
|
||||
template:
|
||||
persistentLogging: '{{persistentLogging}}'
|
||||
domain: [
|
||||
[ 'config_json', 'persistentLogging' ]
|
||||
]
|
||||
}
|
||||
{
|
||||
template:
|
||||
hostname: '{{hostname}}'
|
||||
domain: [
|
||||
[ 'config_json', 'hostname' ]
|
||||
]
|
||||
}
|
||||
{
|
||||
template:
|
||||
wifi:
|
||||
ssid: '{{networkSsid}}'
|
||||
'wifi-security':
|
||||
psk: '{{networkKey}}'
|
||||
domain: [
|
||||
[ 'system_connections', connnectionFileName, 'wifi' ]
|
||||
[ 'system_connections', connnectionFileName, 'wifi-security' ]
|
||||
]
|
||||
}
|
||||
]
|
||||
files:
|
||||
system_connections:
|
||||
fileset: true
|
||||
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'
|
||||
location:
|
||||
path: 'config.json'
|
||||
partition: BOOT_PARTITION
|
||||
|
||||
inquirerOptions = (data) -> [
|
||||
{
|
||||
message: 'Network SSID'
|
||||
type: 'input'
|
||||
name: 'networkSsid'
|
||||
default: data.networkSsid
|
||||
}
|
||||
{
|
||||
message: 'Network Key'
|
||||
type: 'input'
|
||||
name: 'networkKey'
|
||||
default: data.networkKey
|
||||
}
|
||||
{
|
||||
message: 'Do you want to set advanced settings?'
|
||||
type: 'confirm'
|
||||
name: 'advancedSettings'
|
||||
default: false
|
||||
}
|
||||
{
|
||||
message: 'Device Hostname'
|
||||
type: 'input'
|
||||
name: 'hostname'
|
||||
default: data.hostname,
|
||||
when: (answers) ->
|
||||
answers.advancedSettings
|
||||
}
|
||||
{
|
||||
message: 'Do you want to enable persistent logging?'
|
||||
type: 'confirm'
|
||||
name: 'persistentLogging'
|
||||
default: data.persistentLogging
|
||||
when: (answers) ->
|
||||
answers.advancedSettings
|
||||
}
|
||||
]
|
||||
|
||||
getConfiguration = (data) ->
|
||||
_ = require('lodash')
|
||||
inquirer = require('inquirer')
|
||||
|
||||
# `persistentLogging` can be `undefined`, so we want
|
||||
# to make sure that case defaults to `false`
|
||||
data = _.assign data,
|
||||
persistentLogging: data.persistentLogging or false
|
||||
|
||||
inquirer.prompt(inquirerOptions(data))
|
||||
.then (answers) ->
|
||||
return _.merge(data, answers)
|
||||
|
||||
# Taken from https://goo.gl/kr1kCt
|
||||
CONNECTION_FILE = '''
|
||||
[connection]
|
||||
id=resin-wifi
|
||||
type=wifi
|
||||
|
||||
[wifi]
|
||||
hidden=true
|
||||
mode=infrastructure
|
||||
ssid=My_Wifi_Ssid
|
||||
|
||||
[wifi-security]
|
||||
auth-alg=open
|
||||
key-mgmt=wpa-psk
|
||||
psk=super_secret_wifi_password
|
||||
|
||||
[ipv4]
|
||||
method=auto
|
||||
|
||||
[ipv6]
|
||||
addr-gen-mode=stable-privacy
|
||||
method=auto
|
||||
'''
|
||||
|
||||
###
|
||||
* if the `resin-wifi` file exists (previously configured image or downloaded from the UI) it's used and reconfigured
|
||||
* if the `resin-sample.ignore` exists it's copied to `resin-wifi`
|
||||
* if the `resin-sample` exists it's reconfigured (legacy mode, will be removed eventually)
|
||||
* otherwise, the new file is created
|
||||
###
|
||||
prepareConnectionFile = (target) ->
|
||||
_ = require('lodash')
|
||||
imagefs = require('resin-image-fs')
|
||||
|
||||
imagefs.listDirectory
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: CONNECTIONS_FOLDER
|
||||
.then (files) ->
|
||||
# The required file already exists
|
||||
if _.includes(files, 'resin-wifi')
|
||||
return null
|
||||
|
||||
# Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
||||
if _.includes(files, 'resin-sample.ignore')
|
||||
return imagefs.copy
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: "#{CONNECTIONS_FOLDER}/resin-sample.ignore"
|
||||
,
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: "#{CONNECTIONS_FOLDER}/resin-wifi"
|
||||
.thenReturn(null)
|
||||
|
||||
# Legacy mode, to be removed later
|
||||
# We return the file name override from this branch
|
||||
# When it is removed the following cleanup should be done:
|
||||
# * delete all the null returns from this method
|
||||
# * turn `getConfigurationSchema` back into the constant, with the connection filename always being `resin-wifi`
|
||||
# * drop the final `then` from this method
|
||||
# * adapt the code in the main listener to not receive the config from this method, and use that constant instead
|
||||
if _.includes(files, 'resin-sample')
|
||||
return 'resin-sample'
|
||||
|
||||
# In case there's no file at all (shouldn't happen normally, but the file might have been removed)
|
||||
return imagefs.writeFile
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: "#{CONNECTIONS_FOLDER}/resin-wifi"
|
||||
, CONNECTION_FILE
|
||||
.thenReturn(null)
|
||||
|
||||
.then (connectionFileName) ->
|
||||
return getConfigurationSchema(connectionFileName)
|
||||
|
||||
removeHostname = (schema) ->
|
||||
_ = require('lodash')
|
||||
schema.mapper = _.reject schema.mapper, (mapper) ->
|
||||
_.isEqual(Object.keys(mapper.template), ['hostname'])
|
||||
|
||||
module.exports =
|
||||
signature: 'local configure <target>'
|
||||
description: '(Re)configure a balenaOS drive or image'
|
||||
help: '''
|
||||
Use this command to configure or reconfigure a balenaOS drive or image.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local configure /dev/sdc
|
||||
$ balena local configure path/to/image.img
|
||||
'''
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
path = require('path')
|
||||
umount = require('umount')
|
||||
umountAsync = Promise.promisify(umount.umount)
|
||||
isMountedAsync = Promise.promisify(umount.isMounted)
|
||||
reconfix = require('reconfix')
|
||||
denymount = Promise.promisify(require('denymount'))
|
||||
|
||||
prepareConnectionFile(params.target)
|
||||
.tap ->
|
||||
isMountedAsync(params.target).then (isMounted) ->
|
||||
return if not isMounted
|
||||
umountAsync(params.target)
|
||||
.then (configurationSchema) ->
|
||||
dmOpts = {}
|
||||
if process.pkg
|
||||
# when running in a standalone pkg install, the 'denymount'
|
||||
# executable is placed on the same folder as process.execPath
|
||||
dmOpts.executablePath = path.join(path.dirname(process.execPath), 'denymount')
|
||||
dmHandler = (cb) ->
|
||||
reconfix.readConfiguration(configurationSchema, params.target)
|
||||
.then(getConfiguration)
|
||||
.then (answers) ->
|
||||
if not answers.hostname
|
||||
removeHostname(configurationSchema)
|
||||
reconfix.writeConfiguration(configurationSchema, answers, params.target)
|
||||
.asCallback(cb)
|
||||
denymount params.target, dmHandler, dmOpts
|
||||
.then ->
|
||||
console.log('Done!')
|
||||
.asCallback(done)
|
285
lib/actions/local/configure.js
Normal file
285
lib/actions/local/configure.js
Normal file
@ -0,0 +1,285 @@
|
||||
/*
|
||||
Copyright 2017-2020 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const BOOT_PARTITION = 1;
|
||||
const CONNECTIONS_FOLDER = '/system-connections';
|
||||
|
||||
const getConfigurationSchema = function (connnectionFileName) {
|
||||
if (connnectionFileName == null) {
|
||||
connnectionFileName = 'resin-wifi';
|
||||
}
|
||||
return {
|
||||
mapper: [
|
||||
{
|
||||
template: {
|
||||
persistentLogging: '{{persistentLogging}}',
|
||||
},
|
||||
domain: [['config_json', 'persistentLogging']],
|
||||
},
|
||||
{
|
||||
template: {
|
||||
hostname: '{{hostname}}',
|
||||
},
|
||||
domain: [['config_json', 'hostname']],
|
||||
},
|
||||
{
|
||||
template: {
|
||||
wifi: {
|
||||
ssid: '{{networkSsid}}',
|
||||
},
|
||||
'wifi-security': {
|
||||
psk: '{{networkKey}}',
|
||||
},
|
||||
},
|
||||
domain: [
|
||||
['system_connections', connnectionFileName, 'wifi'],
|
||||
['system_connections', connnectionFileName, 'wifi-security'],
|
||||
],
|
||||
},
|
||||
],
|
||||
files: {
|
||||
system_connections: {
|
||||
fileset: true,
|
||||
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',
|
||||
location: {
|
||||
path: 'config.json',
|
||||
partition: BOOT_PARTITION,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const inquirerOptions = (data) => [
|
||||
{
|
||||
message: 'Network SSID',
|
||||
type: 'input',
|
||||
name: 'networkSsid',
|
||||
default: data.networkSsid,
|
||||
},
|
||||
{
|
||||
message: 'Network Key',
|
||||
type: 'input',
|
||||
name: 'networkKey',
|
||||
default: data.networkKey,
|
||||
},
|
||||
{
|
||||
message: 'Do you want to set advanced settings?',
|
||||
type: 'confirm',
|
||||
name: 'advancedSettings',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
message: 'Device Hostname',
|
||||
type: 'input',
|
||||
name: 'hostname',
|
||||
default: data.hostname,
|
||||
when(answers) {
|
||||
return answers.advancedSettings;
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'Do you want to enable persistent logging?',
|
||||
type: 'confirm',
|
||||
name: 'persistentLogging',
|
||||
default: data.persistentLogging,
|
||||
when(answers) {
|
||||
return answers.advancedSettings;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getConfiguration = function (data) {
|
||||
const _ = require('lodash');
|
||||
const inquirer = require('inquirer');
|
||||
|
||||
// `persistentLogging` can be `undefined`, so we want
|
||||
// to make sure that case defaults to `false`
|
||||
data = _.assign(data, { persistentLogging: data.persistentLogging || false });
|
||||
|
||||
return inquirer
|
||||
.prompt(inquirerOptions(data))
|
||||
.then((answers) => _.merge(data, answers));
|
||||
};
|
||||
|
||||
// Taken from https://goo.gl/kr1kCt
|
||||
const CONNECTION_FILE = `\
|
||||
[connection]
|
||||
id=resin-wifi
|
||||
type=wifi
|
||||
|
||||
[wifi]
|
||||
hidden=true
|
||||
mode=infrastructure
|
||||
ssid=My_Wifi_Ssid
|
||||
|
||||
[wifi-security]
|
||||
auth-alg=open
|
||||
key-mgmt=wpa-psk
|
||||
psk=super_secret_wifi_password
|
||||
|
||||
[ipv4]
|
||||
method=auto
|
||||
|
||||
[ipv6]
|
||||
addr-gen-mode=stable-privacy
|
||||
method=auto\
|
||||
`;
|
||||
|
||||
/*
|
||||
* if the `resin-wifi` file exists (previously configured image or downloaded from the UI) it's used and reconfigured
|
||||
* if the `resin-sample.ignore` exists it's copied to `resin-wifi`
|
||||
* if the `resin-sample` exists it's reconfigured (legacy mode, will be removed eventually)
|
||||
* otherwise, the new file is created
|
||||
*/
|
||||
const prepareConnectionFile = function (target) {
|
||||
const _ = require('lodash');
|
||||
const imagefs = require('resin-image-fs');
|
||||
|
||||
return imagefs
|
||||
.listDirectory({
|
||||
image: target,
|
||||
partition: BOOT_PARTITION,
|
||||
path: CONNECTIONS_FOLDER,
|
||||
})
|
||||
.then(function (files) {
|
||||
// The required file already exists
|
||||
if (_.includes(files, 'resin-wifi')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
||||
if (_.includes(files, 'resin-sample.ignore')) {
|
||||
return imagefs
|
||||
.copy(
|
||||
{
|
||||
image: target,
|
||||
partition: BOOT_PARTITION,
|
||||
path: `${CONNECTIONS_FOLDER}/resin-sample.ignore`,
|
||||
},
|
||||
{
|
||||
image: target,
|
||||
partition: BOOT_PARTITION,
|
||||
path: `${CONNECTIONS_FOLDER}/resin-wifi`,
|
||||
},
|
||||
)
|
||||
.thenReturn(null);
|
||||
}
|
||||
|
||||
// Legacy mode, to be removed later
|
||||
// We return the file name override from this branch
|
||||
// When it is removed the following cleanup should be done:
|
||||
// * delete all the null returns from this method
|
||||
// * turn `getConfigurationSchema` back into the constant, with the connection filename always being `resin-wifi`
|
||||
// * drop the final `then` from this method
|
||||
// * adapt the code in the main listener to not receive the config from this method, and use that constant instead
|
||||
if (_.includes(files, 'resin-sample')) {
|
||||
return 'resin-sample';
|
||||
}
|
||||
|
||||
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
|
||||
return imagefs
|
||||
.writeFile(
|
||||
{
|
||||
image: target,
|
||||
partition: BOOT_PARTITION,
|
||||
path: `${CONNECTIONS_FOLDER}/resin-wifi`,
|
||||
},
|
||||
CONNECTION_FILE,
|
||||
)
|
||||
.thenReturn(null);
|
||||
})
|
||||
.then((connectionFileName) => getConfigurationSchema(connectionFileName));
|
||||
};
|
||||
|
||||
const removeHostname = function (schema) {
|
||||
const _ = require('lodash');
|
||||
schema.mapper = _.reject(schema.mapper, (mapper) =>
|
||||
_.isEqual(Object.keys(mapper.template), ['hostname']),
|
||||
);
|
||||
};
|
||||
|
||||
export const configure = {
|
||||
signature: 'local configure <target>',
|
||||
description: '(Re)configure a balenaOS drive or image',
|
||||
help: `\
|
||||
Use this command to configure or reconfigure a balenaOS drive or image.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local configure /dev/sdc
|
||||
$ balena local configure path/to/image.img\
|
||||
`,
|
||||
root: true,
|
||||
action(params) {
|
||||
const Promise = require('bluebird');
|
||||
const path = require('path');
|
||||
const umount = require('umount');
|
||||
const umountAsync = Promise.promisify(umount.umount);
|
||||
const isMountedAsync = Promise.promisify(umount.isMounted);
|
||||
const reconfix = require('reconfix');
|
||||
const denymount = Promise.promisify(require('denymount'));
|
||||
|
||||
return prepareConnectionFile(params.target)
|
||||
.tap(() =>
|
||||
isMountedAsync(params.target).then(function (isMounted) {
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
return umountAsync(params.target);
|
||||
}),
|
||||
)
|
||||
.then(function (configurationSchema) {
|
||||
const dmOpts = {};
|
||||
if (process.pkg) {
|
||||
// when running in a standalone pkg install, the 'denymount'
|
||||
// executable is placed on the same folder as process.execPath
|
||||
dmOpts.executablePath = path.join(
|
||||
path.dirname(process.execPath),
|
||||
'denymount',
|
||||
);
|
||||
}
|
||||
const dmHandler = (cb) =>
|
||||
reconfix
|
||||
.readConfiguration(configurationSchema, params.target)
|
||||
.then(getConfiguration)
|
||||
.then(function (answers) {
|
||||
if (!answers.hostname) {
|
||||
removeHostname(configurationSchema);
|
||||
}
|
||||
return reconfix.writeConfiguration(
|
||||
configurationSchema,
|
||||
answers,
|
||||
params.target,
|
||||
);
|
||||
})
|
||||
.asCallback(cb);
|
||||
return denymount(params.target, dmHandler, dmOpts);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Done!');
|
||||
});
|
||||
},
|
||||
};
|
@ -15,32 +15,29 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import chalk from 'chalk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as SDK from 'etcher-sdk';
|
||||
import { getChalk, getVisuals } from '../../utils/lazy';
|
||||
|
||||
async function getDrive(options: {
|
||||
drive?: string;
|
||||
}): Promise<SDK.sourceDestination.BlockDevice> {
|
||||
const drive = options.drive || (await getVisuals().drive('Select a drive'));
|
||||
|
||||
const sdk = await import('etcher-sdk');
|
||||
|
||||
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
|
||||
const scanner = new sdk.scanner.Scanner([adapter]);
|
||||
await scanner.start();
|
||||
let drive: SDK.sourceDestination.BlockDevice;
|
||||
if (options.drive !== undefined) {
|
||||
const d = scanner.getBy('device', options.drive);
|
||||
try {
|
||||
const d = scanner.getBy('device', drive);
|
||||
if (d === undefined || !(d instanceof sdk.sourceDestination.BlockDevice)) {
|
||||
throw new Error(`Drive not found: ${options.drive}`);
|
||||
}
|
||||
drive = d;
|
||||
} else {
|
||||
const { DriveList } = await import('../../utils/visuals/drive-list');
|
||||
const driveList = new DriveList(scanner);
|
||||
drive = await driveList.run();
|
||||
return d;
|
||||
} finally {
|
||||
scanner.stop();
|
||||
}
|
||||
scanner.stop();
|
||||
return drive;
|
||||
}
|
||||
|
||||
export const flash: CommandDefinition<
|
||||
@ -73,7 +70,6 @@ export const flash: CommandDefinition<
|
||||
},
|
||||
],
|
||||
async action(params, options) {
|
||||
const visuals = await import('resin-cli-visuals');
|
||||
const form = await import('resin-cli-form');
|
||||
const { sourceDestination, multiWrite } = await import('etcher-sdk');
|
||||
|
||||
@ -88,7 +84,7 @@ export const flash: CommandDefinition<
|
||||
default: false,
|
||||
}));
|
||||
if (yes !== true) {
|
||||
console.log(chalk.red.bold('Aborted image flash'));
|
||||
console.log(getChalk().red.bold('Aborted image flash'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@ -98,6 +94,7 @@ export const flash: CommandDefinition<
|
||||
);
|
||||
const source = await file.getInnerSource();
|
||||
|
||||
const visuals = getVisuals();
|
||||
const progressBars: { [key: string]: any } = {
|
||||
flashing: new visuals.Progress('Flashing'),
|
||||
verifying: new visuals.Progress('Validating'),
|
||||
@ -108,7 +105,7 @@ export const flash: CommandDefinition<
|
||||
[drive],
|
||||
(_, error) => {
|
||||
// onFail
|
||||
console.log(chalk.red.bold(error.message));
|
||||
console.log(getChalk().red.bold(error.message));
|
||||
},
|
||||
(progress: SDK.multiWrite.MultiDestinationProgress) => {
|
||||
// onProgress
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
/*
|
||||
Copyright 2017-2020 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -12,7 +12,7 @@ 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.
|
||||
###
|
||||
*/
|
||||
|
||||
exports.configure = require('./configure')
|
||||
exports.flash = require('./flash').flash
|
||||
export { configure } from './configure';
|
||||
export { flash } from './flash';
|
@ -1,66 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
# A function to reliably execute a command
|
||||
# in all supported operating systems, including
|
||||
# different Windows environments like `cmd.exe`
|
||||
# and `Cygwin` should be encapsulated in a
|
||||
# re-usable package.
|
||||
#
|
||||
module.exports =
|
||||
signature: 'local logs [deviceIp]'
|
||||
description: 'Get or attach to logs of a running container on a balenaOS device'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local logs
|
||||
$ balena local logs -f
|
||||
$ balena local logs 192.168.1.10
|
||||
$ balena local logs 192.168.1.10 -f
|
||||
$ balena local logs 192.168.1.10 -f --app-name myapp
|
||||
'''
|
||||
options: [
|
||||
signature: 'follow'
|
||||
boolean: true
|
||||
description: 'follow log'
|
||||
alias: 'f'
|
||||
,
|
||||
signature: 'app-name'
|
||||
parameter: 'name'
|
||||
description: 'name of container to get logs from'
|
||||
alias: 'a'
|
||||
]
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
{ forms } = require('balena-sync')
|
||||
{ selectContainerFromDevice, pipeContainerStream } = require('./common')
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (@deviceIp) =>
|
||||
if not options['app-name']?
|
||||
return selectContainerFromDevice(@deviceIp)
|
||||
return options['app-name']
|
||||
.then (appName) =>
|
||||
pipeContainerStream
|
||||
deviceIp: @deviceIp
|
||||
name: appName
|
||||
outStream: process.stdout
|
||||
follow: options['follow']
|
@ -1,95 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
# Loads '.balena-sync.yml' configuration from 'source' directory.
|
||||
# Returns the configuration object on success
|
||||
#
|
||||
|
||||
_ = require('lodash')
|
||||
|
||||
balenaPush = require('balena-sync').capitano('balena-toolbox')
|
||||
originalAction = balenaPush.action
|
||||
|
||||
# TODO: This is a temporary workaround to reuse the existing `rdt push`
|
||||
# capitano frontend in `balena local push`.
|
||||
|
||||
# coffeelint: disable-next-line ("Line ends with trailing whitespace")
|
||||
deprecationMsg = '''
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Deprecation notice: `balena local push` is deprecated and will be removed in a
|
||||
future release of the CLI. Please use `balena push <ipAddress>` instead.
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
'''
|
||||
|
||||
balenaPushHelp = """#{deprecationMsg}
|
||||
Use this command to push your local changes to a container on a LAN-accessible
|
||||
balenaOS device on the fly.
|
||||
|
||||
This command requires an openssh-compatible 'ssh' client and 'rsync' to be
|
||||
available in the executable PATH of the shell environment. For more information
|
||||
(including Windows support) please check the README at:
|
||||
https://github.com/balena-io/balena-cli
|
||||
|
||||
If `Dockerfile` or any file in the 'build-triggers' list is changed,
|
||||
a new container will be built and run on your device.
|
||||
If not, changes will simply be synced with `rsync` into the application container.
|
||||
|
||||
After every 'balena local push' the updated settings will be saved in
|
||||
'<source>/.balena-sync.yml' and will be used in later invocations. You can
|
||||
also change any option by editing '.balena-sync.yml' directly.
|
||||
|
||||
Here is an example '.balena-sync.yml' :
|
||||
|
||||
$ cat $PWD/.balena-sync.yml
|
||||
local_balenaos:
|
||||
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 '.balena-sync.yml'.
|
||||
|
||||
If '.gitignore' is found in the source directory then all explicitly listed files will be
|
||||
excluded when using rsync to update the container. You can choose to change this default behavior with the
|
||||
'--skip-gitignore' option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local push
|
||||
$ balena local push --app-name test-server --build-triggers package.json,requirements.txt
|
||||
$ balena local push --force-build
|
||||
$ balena local push --force-build --skip-logs
|
||||
$ balena local push --ignore lib/
|
||||
$ balena local push --verbose false
|
||||
$ balena local push 192.168.2.10 --source . --destination /usr/src/app
|
||||
$ balena local push 192.168.2.10 -s /home/user/balenaProject -d /usr/src/app --before 'echo Hello' --after 'echo Done'
|
||||
"""
|
||||
|
||||
|
||||
|
||||
module.exports = _.assign balenaPush,
|
||||
signature: 'local push [deviceIp]'
|
||||
description: '[deprecated: use "balena push ipAddress"] ' + balenaPush.description
|
||||
help: balenaPushHelp
|
||||
primary: false
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
console.log deprecationMsg
|
||||
originalAction(params, options, done)
|
@ -1,114 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
{ hostOSAccess } = require('../command-options')
|
||||
_ = require('lodash')
|
||||
|
||||
localHostOSAccessOption = _.cloneDeep(hostOSAccess)
|
||||
localHostOSAccessOption.description = 'get a shell into the host OS'
|
||||
|
||||
module.exports =
|
||||
signature: 'local ssh [deviceIp]'
|
||||
description: 'Get a shell into a balenaOS device'
|
||||
help: '''
|
||||
Warning: 'balena local ssh' requires an openssh-compatible client to be correctly
|
||||
installed in your shell environment. For more information (including Windows
|
||||
support) please check the README here: https://github.com/balena-io/balena-cli
|
||||
|
||||
Use this command to get a shell into the running application container of
|
||||
your device.
|
||||
|
||||
The '--host' option will get you a shell into the Host OS of the balenaOS device.
|
||||
No option will return a list of containers to enter or you can explicitly select
|
||||
one by passing its name to the --container option
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local ssh
|
||||
$ balena local ssh --host
|
||||
$ balena local ssh --container chaotic_water
|
||||
$ balena local ssh --container chaotic_water --port 22222
|
||||
$ balena local ssh --verbose
|
||||
'''
|
||||
options: [
|
||||
signature: 'verbose'
|
||||
boolean: true
|
||||
description: 'increase verbosity'
|
||||
alias: 'v'
|
||||
,
|
||||
localHostOSAccessOption,
|
||||
signature: 'container'
|
||||
parameter: 'container'
|
||||
default: null
|
||||
description: 'name of container to access'
|
||||
alias: 'c'
|
||||
,
|
||||
signature: 'port'
|
||||
parameter: 'port'
|
||||
description: 'ssh port number (default: 22222)'
|
||||
alias: 'p'
|
||||
]
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
child_process = require('child_process')
|
||||
Promise = require 'bluebird'
|
||||
_ = require('lodash')
|
||||
{ forms } = require('balena-sync')
|
||||
|
||||
{ selectContainerFromDevice, getSubShellCommand } = require('./common')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
if (options.host is true and options.container?)
|
||||
exitWithExpectedError('Please pass either --host or --container option')
|
||||
|
||||
if not options.port?
|
||||
options.port = 22222
|
||||
|
||||
verbose = if options.verbose then '-vvv' else ''
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (deviceIp) ->
|
||||
_.assign(options, { deviceIp })
|
||||
|
||||
return if options.host
|
||||
|
||||
if not options.container?
|
||||
return selectContainerFromDevice(deviceIp)
|
||||
|
||||
return options.container
|
||||
.then (container) ->
|
||||
|
||||
command = "ssh \
|
||||
#{verbose} \
|
||||
-t \
|
||||
-p #{options.port} \
|
||||
-o LogLevel=ERROR \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
root@#{options.deviceIp}"
|
||||
|
||||
if not options.host
|
||||
shellCmd = '''/bin/sh -c $"'if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi'"'''
|
||||
dockerCmd = "'$(if [ -f /usr/bin/balena ]; then echo \"balena\"; else echo \"docker\"; fi)'"
|
||||
command += " #{dockerCmd} exec -ti #{container} #{shellCmd}"
|
||||
|
||||
subShellCommand = getSubShellCommand(command)
|
||||
child_process.spawn subShellCommand.program, subShellCommand.args,
|
||||
stdio: 'inherit'
|
||||
.nodeify(done)
|
@ -1,79 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
# A function to reliably execute a command
|
||||
# in all supported operating systems, including
|
||||
# different Windows environments like `cmd.exe`
|
||||
# and `Cygwin` should be encapsulated in a
|
||||
# re-usable package.
|
||||
#
|
||||
module.exports =
|
||||
signature: 'local stop [deviceIp]'
|
||||
description: 'Stop a running container on a balenaOS device'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local stop
|
||||
$ balena local stop --app-name myapp
|
||||
$ balena local stop --all
|
||||
$ balena local stop 192.168.1.10
|
||||
$ balena local stop 192.168.1.10 --app-name myapp
|
||||
'''
|
||||
options: [
|
||||
signature: 'all'
|
||||
boolean: true
|
||||
description: 'stop all containers'
|
||||
,
|
||||
signature: 'app-name'
|
||||
parameter: 'name'
|
||||
description: 'name of container to stop'
|
||||
alias: 'a'
|
||||
]
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
chalk = require('chalk')
|
||||
{ forms, config, BalenaLocalDockerUtils } = require('balena-sync')
|
||||
{ selectContainerFromDevice, filterOutSupervisorContainer } = require('./common')
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (@deviceIp) =>
|
||||
@docker = new BalenaLocalDockerUtils(@deviceIp)
|
||||
|
||||
if options.all
|
||||
# Only list running containers
|
||||
return @docker.docker.listContainersAsync(all: false)
|
||||
.filter(filterOutSupervisorContainer)
|
||||
.then (containers) =>
|
||||
Promise.map containers, ({ Names, Id }) =>
|
||||
console.log(chalk.yellow.bold("* Stopping container #{Names[0]}"))
|
||||
@docker.stopContainer(Id)
|
||||
|
||||
ymlConfig = config.load()
|
||||
@appName = options['app-name'] ? ymlConfig['local_balenaos']?['app-name']
|
||||
@docker.checkForRunningContainer(@appName)
|
||||
.then (isRunning) =>
|
||||
if not isRunning
|
||||
return selectContainerFromDevice(@deviceIp, true)
|
||||
|
||||
console.log(chalk.yellow.bold("* Stopping container #{@appName}"))
|
||||
return @appName
|
||||
.then (runningContainerName) =>
|
||||
@docker.stopContainer(runningContainerName)
|
@ -14,25 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { LogMessage } from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
import { normalizeUuidProp } from '../utils/normalization';
|
||||
import { validateDotLocalUrl } from '../utils/validation';
|
||||
|
||||
type CloudLog =
|
||||
| {
|
||||
isSystem: false;
|
||||
serviceId: number;
|
||||
timestamp: number;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
isSystem: true;
|
||||
timestamp: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const logs: CommandDefinition<
|
||||
{
|
||||
uuidOrDevice: string;
|
||||
@ -94,30 +83,28 @@ export const logs: CommandDefinition<
|
||||
},
|
||||
],
|
||||
primary: true,
|
||||
async action(params, options, done) {
|
||||
async action(params, options) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const isArray = await import('lodash/isArray');
|
||||
const balena = getBalenaSdk();
|
||||
const { ExpectedError } = await import('../errors');
|
||||
const { serviceIdToName } = await import('../utils/cloud');
|
||||
const { displayDeviceLogs, displayLogObject } = await import(
|
||||
'../utils/device/logs'
|
||||
);
|
||||
const { validateIPAddress } = await import('../utils/validation');
|
||||
const { exitIfNotLoggedIn, exitWithExpectedError } = await import(
|
||||
'../utils/patterns'
|
||||
);
|
||||
const { checkLoggedIn } = await import('../utils/patterns');
|
||||
const Logger = await import('../utils/logger');
|
||||
|
||||
const logger = Logger.getLogger();
|
||||
|
||||
const servicesToDisplay =
|
||||
options.service != null
|
||||
? isArray(options.service)
|
||||
? Array.isArray(options.service)
|
||||
? options.service
|
||||
: [options.service]
|
||||
: undefined;
|
||||
|
||||
const displayCloudLog = async (line: CloudLog) => {
|
||||
const displayCloudLog = async (line: LogMessage) => {
|
||||
if (!line.isSystem) {
|
||||
let serviceName = await serviceIdToName(balena, line.serviceId);
|
||||
if (serviceName == null) {
|
||||
@ -149,35 +136,33 @@ export const logs: CommandDefinition<
|
||||
try {
|
||||
await deviceApi.ping();
|
||||
} catch (e) {
|
||||
exitWithExpectedError(
|
||||
new Error(
|
||||
`Cannot access local mode device at address ${params.uuidOrDevice}`,
|
||||
),
|
||||
throw new ExpectedError(
|
||||
`Cannot access local mode device at address ${params.uuidOrDevice}`,
|
||||
);
|
||||
}
|
||||
|
||||
const logStream = await deviceApi.getLogStream();
|
||||
displayDeviceLogs(
|
||||
await displayDeviceLogs(
|
||||
logStream,
|
||||
logger,
|
||||
options.system || false,
|
||||
servicesToDisplay,
|
||||
);
|
||||
} else {
|
||||
await exitIfNotLoggedIn();
|
||||
await checkLoggedIn();
|
||||
if (options.tail) {
|
||||
return balena.logs
|
||||
.subscribe(params.uuidOrDevice, { count: 100 })
|
||||
.then(function(logStream) {
|
||||
logStream.on('line', displayCloudLog);
|
||||
logStream.on('error', done);
|
||||
})
|
||||
.catch(done);
|
||||
const logStream = await balena.logs.subscribe(params.uuidOrDevice, {
|
||||
count: 100,
|
||||
});
|
||||
// Never resolve (quit with CTRL-C), but reject on a broken connection
|
||||
await new Promise((_resolve, reject) => {
|
||||
logStream.on('line', displayCloudLog);
|
||||
logStream.on('error', reject);
|
||||
});
|
||||
} else {
|
||||
return balena.logs
|
||||
.history(params.uuidOrDevice)
|
||||
.each(displayCloudLog)
|
||||
.catch(done);
|
||||
const logMessages = await balena.logs.history(params.uuidOrDevice);
|
||||
for (const logMessage of logMessages) {
|
||||
await displayCloudLog(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,55 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
exports.set =
|
||||
signature: 'note <|note>'
|
||||
description: 'set a device note'
|
||||
help: '''
|
||||
Use this command to set or update a device note.
|
||||
|
||||
If note command isn't passed, the tool attempts to read from `stdin`.
|
||||
|
||||
To view the notes, use $ balena device <uuid>.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena note "My useful note" --device 7cf02a6
|
||||
$ cat note.txt | balena note --device 7cf02a6
|
||||
'''
|
||||
options: [
|
||||
signature: 'device'
|
||||
parameter: 'device'
|
||||
description: 'device uuid'
|
||||
alias: [ 'd', 'dev' ]
|
||||
required: 'You have to specify a device'
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(options, 'device')
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
Promise.try ->
|
||||
if _.isEmpty(params.note)
|
||||
exitWithExpectedError('Missing note content')
|
||||
|
||||
balena.models.device.note(options.device, params.note)
|
||||
.nodeify(done)
|
@ -1,290 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
_ = require('lodash')
|
||||
|
||||
formatVersion = (v, isRecommended) ->
|
||||
result = "v#{v}"
|
||||
if isRecommended
|
||||
result += ' (recommended)'
|
||||
return result
|
||||
|
||||
resolveVersion = (deviceType, version) ->
|
||||
if version isnt 'menu'
|
||||
if version[0] == 'v'
|
||||
version = version.slice(1)
|
||||
return Promise.resolve(version)
|
||||
|
||||
form = require('resin-cli-form')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
balena.models.os.getSupportedVersions(deviceType)
|
||||
.then ({ versions, recommended }) ->
|
||||
choices = versions.map (v) ->
|
||||
value: v
|
||||
name: formatVersion(v, v is recommended)
|
||||
|
||||
return form.ask
|
||||
message: 'Select the OS version:'
|
||||
type: 'list'
|
||||
choices: choices
|
||||
default: recommended
|
||||
|
||||
exports.versions =
|
||||
signature: 'os versions <type>'
|
||||
description: 'show the available balenaOS versions for the given device type'
|
||||
help: '''
|
||||
Use this command to show the available balenaOS versions for a certain device type.
|
||||
Check available types with `balena devices supported`
|
||||
|
||||
Example:
|
||||
|
||||
$ balena os versions raspberrypi3
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
balena.models.os.getSupportedVersions(params.type)
|
||||
.then ({ versions, recommended }) ->
|
||||
versions.forEach (v) ->
|
||||
console.log(formatVersion(v, v is recommended))
|
||||
|
||||
exports.download =
|
||||
signature: 'os download <type>'
|
||||
description: 'download an unconfigured os image'
|
||||
help: '''
|
||||
Use this command to download an unconfigured os image for a certain device type.
|
||||
Check available types with `balena devices supported`
|
||||
|
||||
If version is not specified the newest stable (non-pre-release) version of OS
|
||||
is downloaded if available, or the newest version otherwise (if all existing
|
||||
versions for the given device type are pre-release).
|
||||
|
||||
You can pass `--version menu` to pick the OS version from the interactive menu
|
||||
of all available versions.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 1.24.1
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^1.20.0
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
{
|
||||
signature: 'output'
|
||||
description: 'output path'
|
||||
parameter: 'output'
|
||||
alias: 'o'
|
||||
required: 'You have to specify the output location'
|
||||
}
|
||||
commandOptions.osVersionOrSemver
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
unzip = require('unzip2')
|
||||
fs = require('fs')
|
||||
rindle = require('rindle')
|
||||
manager = require('balena-image-manager')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
console.info("Getting device operating system for #{params.type}")
|
||||
|
||||
displayVersion = ''
|
||||
Promise.try ->
|
||||
if not options.version
|
||||
console.warn('OS version is not specified, using the default version:
|
||||
the newest stable (non-pre-release) version if available,
|
||||
or the newest version otherwise (if all existing
|
||||
versions for the given device type are pre-release).')
|
||||
return 'default'
|
||||
return resolveVersion(params.type, options.version)
|
||||
.then (version) ->
|
||||
if version isnt 'default'
|
||||
displayVersion = " #{version}"
|
||||
return manager.get(params.type, version)
|
||||
.then (stream) ->
|
||||
bar = new visuals.Progress("Downloading Device OS#{displayVersion}")
|
||||
spinner = new visuals.Spinner("Downloading Device OS#{displayVersion} (size unknown)")
|
||||
|
||||
stream.on 'progress', (state) ->
|
||||
if state?
|
||||
bar.update(state)
|
||||
else
|
||||
spinner.start()
|
||||
|
||||
stream.on 'end', ->
|
||||
spinner.stop()
|
||||
|
||||
# We completely rely on the `mime` custom property
|
||||
# to make this decision.
|
||||
# The actual stream should be checked instead.
|
||||
if stream.mime is 'application/zip'
|
||||
output = unzip.Extract(path: options.output)
|
||||
else
|
||||
output = fs.createWriteStream(options.output)
|
||||
|
||||
return rindle.wait(stream.pipe(output)).return(options.output)
|
||||
.tap (output) ->
|
||||
console.info('The image was downloaded successfully')
|
||||
.nodeify(done)
|
||||
|
||||
buildConfigForDeviceType = (deviceType, advanced = false) ->
|
||||
form = require('resin-cli-form')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
questions = deviceType.options
|
||||
if not advanced
|
||||
advancedGroup = _.find questions,
|
||||
name: 'advanced'
|
||||
isGroup: true
|
||||
|
||||
if advancedGroup?
|
||||
override = helpers.getGroupDefaults(advancedGroup)
|
||||
|
||||
return form.run(questions, { override })
|
||||
|
||||
buildConfig = (image, deviceTypeSlug, advanced = false) ->
|
||||
Promise = require('bluebird')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
Promise.resolve(helpers.getManifest(image, deviceTypeSlug))
|
||||
.then (deviceTypeManifest) ->
|
||||
buildConfigForDeviceType(deviceTypeManifest, advanced)
|
||||
|
||||
exports.buildConfig =
|
||||
signature: 'os build-config <image> <device-type>'
|
||||
description: 'build the OS config and save it to the JSON file'
|
||||
help: '''
|
||||
Use this command to prebuild the OS config once and skip the interactive part of `balena os configure`.
|
||||
|
||||
Example:
|
||||
|
||||
$ balena os build-config ../path/rpi3.img raspberrypi3 --output rpi3-config.json
|
||||
$ balena os configure ../path/rpi3.img --device 7cf02a6 --config rpi3-config.json
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
commandOptions.advancedConfig
|
||||
{
|
||||
signature: 'output'
|
||||
description: 'the path to the output JSON file'
|
||||
alias: 'o'
|
||||
required: 'the output path is required'
|
||||
parameter: 'output'
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
fs = require('fs')
|
||||
Promise = require('bluebird')
|
||||
writeFileAsync = Promise.promisify(fs.writeFile)
|
||||
|
||||
buildConfig(params.image, params['device-type'], options.advanced)
|
||||
.then (answers) ->
|
||||
writeFileAsync(options.output, JSON.stringify(answers, null, 4))
|
||||
.nodeify(done)
|
||||
|
||||
INIT_WARNING_MESSAGE = '''
|
||||
Note: Initializing the device may ask for administrative permissions
|
||||
because we need to access the raw devices directly.
|
||||
'''
|
||||
|
||||
exports.initialize =
|
||||
signature: 'os initialize <image>'
|
||||
description: 'initialize an os image'
|
||||
help: """
|
||||
Use this command to initialize a device with previously configured operating system image.
|
||||
|
||||
#{INIT_WARNING_MESSAGE}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena os initialize ../path/rpi.img --type 'raspberry-pi'
|
||||
"""
|
||||
permission: 'user'
|
||||
options: [
|
||||
commandOptions.yes
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
commandOptions.drive
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
form = require('resin-cli-form')
|
||||
patterns = require('../utils/patterns')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
console.info("""
|
||||
Initializing device
|
||||
|
||||
#{INIT_WARNING_MESSAGE}
|
||||
""")
|
||||
Promise.resolve(helpers.getManifest(params.image, options.type))
|
||||
.then (manifest) ->
|
||||
return manifest.initialization?.options
|
||||
.then (questions) ->
|
||||
return form.run questions,
|
||||
override:
|
||||
drive: options.drive
|
||||
.tap (answers) ->
|
||||
return if not answers.drive?
|
||||
patterns.confirm(
|
||||
options.yes
|
||||
"This will erase #{answers.drive}. Are you sure?"
|
||||
"Going to erase #{answers.drive}."
|
||||
true
|
||||
)
|
||||
.return(answers.drive)
|
||||
.then(umountAsync)
|
||||
.tap (answers) ->
|
||||
return helpers.sudo([
|
||||
'internal'
|
||||
'osinit'
|
||||
params.image
|
||||
options.type
|
||||
JSON.stringify(answers)
|
||||
])
|
||||
.then (answers) ->
|
||||
return if not answers.drive?
|
||||
|
||||
# TODO: balena local makes use of ejectAsync, see below
|
||||
# DO we need this / should we do that here?
|
||||
|
||||
# getDrive = (drive) ->
|
||||
# driveListAsync().then (drives) ->
|
||||
# selectedDrive = _.find(drives, device: drive)
|
||||
|
||||
# if not selectedDrive?
|
||||
# throw new Error("Drive not found: #{drive}")
|
||||
|
||||
# return selectedDrive
|
||||
# if (os.platform() is 'win32') and selectedDrive.mountpoint?
|
||||
# ejectAsync = Promise.promisify(require('removedrive').eject)
|
||||
# return ejectAsync(selectedDrive.mountpoint)
|
||||
|
||||
umountAsync(answers.drive).tap ->
|
||||
console.info("You can safely remove #{answers.drive} now")
|
||||
.nodeify(done)
|
353
lib/actions/os.js
Normal file
353
lib/actions/os.js
Normal file
@ -0,0 +1,353 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as commandOptions from './command-options';
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
|
||||
const formatVersion = function (v, isRecommended) {
|
||||
let result = `v${v}`;
|
||||
if (isRecommended) {
|
||||
result += ' (recommended)';
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const resolveVersion = function (deviceType, version) {
|
||||
if (version !== 'menu') {
|
||||
if (version[0] === 'v') {
|
||||
version = version.slice(1);
|
||||
}
|
||||
return Promise.resolve(version);
|
||||
}
|
||||
|
||||
const form = require('resin-cli-form');
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
return balena.models.os
|
||||
.getSupportedVersions(deviceType)
|
||||
.then(function ({ versions: vs, recommended }) {
|
||||
const choices = vs.map((v) => ({
|
||||
value: v,
|
||||
name: formatVersion(v, v === recommended),
|
||||
}));
|
||||
|
||||
return form.ask({
|
||||
message: 'Select the OS version:',
|
||||
type: 'list',
|
||||
choices,
|
||||
default: recommended,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const versions = {
|
||||
signature: 'os versions <type>',
|
||||
description: 'show the available balenaOS versions for the given device type',
|
||||
help: `\
|
||||
Use this command to show the available balenaOS versions for a certain device type.
|
||||
Check available types with \`balena devices supported\`
|
||||
|
||||
Example:
|
||||
|
||||
$ balena os versions raspberrypi3\
|
||||
`,
|
||||
action(params) {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
return balena.models.os
|
||||
.getSupportedVersions(params.type)
|
||||
.then(({ versions: vs, recommended }) => {
|
||||
vs.forEach((v) => {
|
||||
console.log(formatVersion(v, v === recommended));
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const download = {
|
||||
signature: 'os download <type>',
|
||||
description: 'download an unconfigured os image',
|
||||
help: `\
|
||||
Use this command to download an unconfigured os image for a certain device type.
|
||||
Check available types with \`balena devices supported\`
|
||||
|
||||
If version is not specified the newest stable (non-pre-release) version of OS
|
||||
is downloaded if available, or the newest version otherwise (if all existing
|
||||
versions for the given device type are pre-release).
|
||||
|
||||
You can pass \`--version menu\` to pick the OS version from the interactive menu
|
||||
of all available versions.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 1.24.1
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^1.20.0
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu\
|
||||
`,
|
||||
permission: 'user',
|
||||
options: [
|
||||
{
|
||||
signature: 'output',
|
||||
description: 'output path',
|
||||
parameter: 'output',
|
||||
alias: 'o',
|
||||
required: 'You have to specify the output location',
|
||||
},
|
||||
commandOptions.osVersionOrSemver,
|
||||
],
|
||||
action(params, options) {
|
||||
const Promise = require('bluebird');
|
||||
const unzip = require('node-unzip-2');
|
||||
const fs = require('fs');
|
||||
const rindle = require('rindle');
|
||||
const manager = require('balena-image-manager');
|
||||
|
||||
console.info(`Getting device operating system for ${params.type}`);
|
||||
|
||||
let displayVersion = '';
|
||||
return Promise.try(function () {
|
||||
if (!options.version) {
|
||||
console.warn(`OS version is not specified, using the default version: \
|
||||
the newest stable (non-pre-release) version if available, \
|
||||
or the newest version otherwise (if all existing \
|
||||
versions for the given device type are pre-release).`);
|
||||
return 'default';
|
||||
}
|
||||
return resolveVersion(params.type, options.version);
|
||||
})
|
||||
.then(function (version) {
|
||||
if (version !== 'default') {
|
||||
displayVersion = ` ${version}`;
|
||||
}
|
||||
return manager.get(params.type, version);
|
||||
})
|
||||
.then(function (stream) {
|
||||
const visuals = getVisuals();
|
||||
const bar = new visuals.Progress(
|
||||
`Downloading Device OS${displayVersion}`,
|
||||
);
|
||||
const spinner = new visuals.Spinner(
|
||||
`Downloading Device OS${displayVersion} (size unknown)`,
|
||||
);
|
||||
|
||||
stream.on('progress', function (state) {
|
||||
if (state != null) {
|
||||
return bar.update(state);
|
||||
} else {
|
||||
return spinner.start();
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
spinner.stop();
|
||||
});
|
||||
|
||||
// We completely rely on the `mime` custom property
|
||||
// to make this decision.
|
||||
// The actual stream should be checked instead.
|
||||
let output;
|
||||
if (stream.mime === 'application/zip') {
|
||||
output = unzip.Extract({ path: options.output });
|
||||
} else {
|
||||
output = fs.createWriteStream(options.output);
|
||||
}
|
||||
|
||||
return rindle.wait(stream.pipe(output)).return(options.output);
|
||||
})
|
||||
.tap(() => {
|
||||
console.info('The image was downloaded successfully');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const buildConfigForDeviceType = function (deviceType, advanced) {
|
||||
if (advanced == null) {
|
||||
advanced = false;
|
||||
}
|
||||
const form = require('resin-cli-form');
|
||||
const helpers = require('../utils/helpers');
|
||||
|
||||
let override;
|
||||
const questions = deviceType.options;
|
||||
if (!advanced) {
|
||||
const advancedGroup = _.find(questions, {
|
||||
name: 'advanced',
|
||||
isGroup: true,
|
||||
});
|
||||
|
||||
if (advancedGroup != null) {
|
||||
override = helpers.getGroupDefaults(advancedGroup);
|
||||
}
|
||||
}
|
||||
|
||||
return form.run(questions, { override });
|
||||
};
|
||||
|
||||
const $buildConfig = function (image, deviceTypeSlug, advanced) {
|
||||
if (advanced == null) {
|
||||
advanced = false;
|
||||
}
|
||||
const Promise = require('bluebird');
|
||||
const helpers = require('../utils/helpers');
|
||||
|
||||
return Promise.resolve(
|
||||
helpers.getManifest(image, deviceTypeSlug),
|
||||
).then((deviceTypeManifest) =>
|
||||
buildConfigForDeviceType(deviceTypeManifest, advanced),
|
||||
);
|
||||
};
|
||||
|
||||
export const buildConfig = {
|
||||
signature: 'os build-config <image> <device-type>',
|
||||
description: 'build the OS config and save it to the JSON file',
|
||||
help: `\
|
||||
Use this command to prebuild the OS config once and skip the interactive part of \`balena os configure\`.
|
||||
|
||||
Example:
|
||||
|
||||
$ balena os build-config ../path/rpi3.img raspberrypi3 --output rpi3-config.json
|
||||
$ balena os configure ../path/rpi3.img --device 7cf02a6 --config rpi3-config.json\
|
||||
`,
|
||||
permission: 'user',
|
||||
options: [
|
||||
commandOptions.advancedConfig,
|
||||
{
|
||||
signature: 'output',
|
||||
description: 'the path to the output JSON file',
|
||||
alias: 'o',
|
||||
required: 'the output path is required',
|
||||
parameter: 'output',
|
||||
},
|
||||
],
|
||||
action(params, options) {
|
||||
const fs = require('fs');
|
||||
const Promise = require('bluebird');
|
||||
const writeFileAsync = Promise.promisify(fs.writeFile);
|
||||
|
||||
return $buildConfig(
|
||||
params.image,
|
||||
params['device-type'],
|
||||
options.advanced,
|
||||
).then((answers) =>
|
||||
writeFileAsync(options.output, JSON.stringify(answers, null, 4)),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const INIT_WARNING_MESSAGE = `\
|
||||
Note: Initializing the device may ask for administrative permissions
|
||||
because we need to access the raw devices directly.\
|
||||
`;
|
||||
|
||||
export const initialize = {
|
||||
signature: 'os initialize <image>',
|
||||
description: 'initialize an os image',
|
||||
help: `\
|
||||
Use this command to initialize a device with previously configured operating system image.
|
||||
|
||||
${INIT_WARNING_MESSAGE}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena os initialize ../path/rpi.img --type 'raspberry-pi'\
|
||||
`,
|
||||
permission: 'user',
|
||||
options: [
|
||||
commandOptions.yes,
|
||||
{
|
||||
signature: 'type',
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
parameter: 'type',
|
||||
alias: 't',
|
||||
required: 'You have to specify a device type',
|
||||
},
|
||||
commandOptions.drive,
|
||||
],
|
||||
action(params, options) {
|
||||
const Promise = require('bluebird');
|
||||
const umountAsync = Promise.promisify(require('umount').umount);
|
||||
const form = require('resin-cli-form');
|
||||
const patterns = require('../utils/patterns');
|
||||
const helpers = require('../utils/helpers');
|
||||
|
||||
console.info(`\
|
||||
Initializing device
|
||||
|
||||
${INIT_WARNING_MESSAGE}\
|
||||
`);
|
||||
return Promise.resolve(helpers.getManifest(params.image, options.type))
|
||||
.then((manifest) =>
|
||||
form.run(manifest.initialization?.options, {
|
||||
override: {
|
||||
drive: options.drive,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.tap(function (answers) {
|
||||
if (answers.drive == null) {
|
||||
return;
|
||||
}
|
||||
return patterns
|
||||
.confirm(
|
||||
options.yes,
|
||||
`This will erase ${answers.drive}. Are you sure?`,
|
||||
`Going to erase ${answers.drive}.`,
|
||||
true,
|
||||
)
|
||||
.return(answers.drive)
|
||||
.then(umountAsync);
|
||||
})
|
||||
.tap((answers) =>
|
||||
helpers.sudo([
|
||||
'internal',
|
||||
'osinit',
|
||||
params.image,
|
||||
options.type,
|
||||
JSON.stringify(answers),
|
||||
]),
|
||||
)
|
||||
.then(function (answers) {
|
||||
if (answers.drive == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: balena local makes use of ejectAsync, see below
|
||||
// DO we need this / should we do that here?
|
||||
|
||||
// getDrive = (drive) ->
|
||||
// driveListAsync().then (drives) ->
|
||||
// selectedDrive = _.find(drives, device: drive)
|
||||
|
||||
// if not selectedDrive?
|
||||
// throw new Error("Drive not found: #{drive}")
|
||||
|
||||
// return selectedDrive
|
||||
// if (os.platform() is 'win32') and selectedDrive.mountpoint?
|
||||
// ejectAsync = Promise.promisify(require('removedrive').eject)
|
||||
// return ejectAsync(selectedDrive.mountpoint)
|
||||
|
||||
return umountAsync(answers.drive).tap(() => {
|
||||
console.info(`You can safely remove ${answers.drive} now`);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
@ -1,337 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
_ = require('lodash')
|
||||
|
||||
dockerUtils = require('../utils/docker')
|
||||
|
||||
allDeviceTypes = undefined
|
||||
|
||||
isCurrent = (commit) ->
|
||||
return commit == 'latest' or commit == 'current'
|
||||
|
||||
getDeviceTypes = ->
|
||||
Bluebird = require('bluebird')
|
||||
_ = require('lodash')
|
||||
if allDeviceTypes != undefined
|
||||
return Bluebird.resolve(allDeviceTypes)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.config.getDeviceTypes()
|
||||
.then (deviceTypes) ->
|
||||
_.sortBy(deviceTypes, 'name')
|
||||
.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('balena-preload')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
getDeviceTypesWithSameArch(deviceType)
|
||||
.then (deviceTypes) ->
|
||||
balena.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')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and releases.')
|
||||
applicationInfoSpinner.start()
|
||||
|
||||
getApplicationsWithSuccessfulBuilds(deviceType)
|
||||
.then (applications) ->
|
||||
applicationInfoSpinner.stop()
|
||||
if applications.length == 0
|
||||
exitWithExpectedError("You have no apps with successful releases for a '#{deviceType}' device type.")
|
||||
form.ask
|
||||
message: 'Select an application'
|
||||
type: 'list'
|
||||
choices: applications.map (app) ->
|
||||
name: app.app_name
|
||||
value: app
|
||||
|
||||
selectApplicationCommit = (releases) ->
|
||||
form = require('resin-cli-form')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if releases.length == 0
|
||||
exitWithExpectedError('This application has no successful releases.')
|
||||
DEFAULT_CHOICE = { 'name': 'current', 'value': 'current' }
|
||||
choices = [ DEFAULT_CHOICE ].concat releases.map (release) ->
|
||||
name: "#{release.end_timestamp} - #{release.commit}"
|
||||
value: release.commit
|
||||
return form.ask
|
||||
message: 'Select a release'
|
||||
type: 'list'
|
||||
default: 'current'
|
||||
choices: choices
|
||||
|
||||
offerToDisableAutomaticUpdates = (application, commit, pinDevice) ->
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
|
||||
if isCurrent(commit) or not application.should_track_latest_release or pinDevice
|
||||
return Promise.resolve()
|
||||
message = '''
|
||||
|
||||
This application is set to automatically update all devices to the current version.
|
||||
This might be unexpected behaviour: with this enabled, the preloaded device will still
|
||||
download and install the current 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://balena.io/docs/reference/api/resources/device/#set-device-to-release
|
||||
|
||||
Alternatively you can pass the --pin-device-to-release flag to pin only this device to the selected release.
|
||||
'''
|
||||
form.ask
|
||||
message: message,
|
||||
type: 'confirm'
|
||||
.then (update) ->
|
||||
if not update
|
||||
return
|
||||
balena.pine.patch
|
||||
resource: 'application'
|
||||
id: application.id
|
||||
body:
|
||||
should_track_latest_release: false
|
||||
|
||||
preloadOptions = dockerUtils.appendConnectionOptions [
|
||||
{
|
||||
signature: 'app'
|
||||
parameter: 'appId'
|
||||
description: 'id of the application to preload'
|
||||
alias: 'a'
|
||||
}
|
||||
{
|
||||
signature: 'commit'
|
||||
parameter: 'hash'
|
||||
description: '''
|
||||
The commit hash for a specific application release to preload, use "current" to specify the current
|
||||
release (ignored if no appId is given). The current release is usually also the latest, but can be
|
||||
manually pinned using https://github.com/balena-io-projects/staged-releases .
|
||||
'''
|
||||
alias: 'c'
|
||||
}
|
||||
{
|
||||
signature: 'splash-image'
|
||||
parameter: 'splashImage.png'
|
||||
description: 'path to a png image to replace the splash screen'
|
||||
alias: 's'
|
||||
}
|
||||
{
|
||||
signature: 'dont-check-arch'
|
||||
boolean: true
|
||||
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'
|
||||
}
|
||||
{
|
||||
signature: 'add-certificate'
|
||||
parameter: 'certificate.crt'
|
||||
description: '''
|
||||
Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container.
|
||||
The file name must end with '.crt' and must not be already contained in the preloader's
|
||||
/etc/ssl/certs folder.
|
||||
Can be repeated to add multiple certificates.
|
||||
'''
|
||||
}
|
||||
]
|
||||
# Remove dockerPort `-p` alias as it conflicts with pin-device-to-release
|
||||
delete _.find(preloadOptions, signature: 'dockerPort').alias
|
||||
|
||||
module.exports =
|
||||
signature: 'preload <image>'
|
||||
description: 'preload an app on a disk image (or Edison zip archive)'
|
||||
help: '''
|
||||
Preload a balena application release (app images/containers), and optionally
|
||||
a balenaOS splash screen, in a previously downloaded balenaOS image file (or
|
||||
Edison zip archive) in the local disk. The balenaOS image file can then be
|
||||
flashed to a device's SD card. When the device boots, it will not need to
|
||||
download the application, as it was preloaded.
|
||||
|
||||
Warning: "balena preload" requires Docker to be correctly installed in
|
||||
your shell environment. For more information (including Windows support)
|
||||
check: https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena preload balena.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png
|
||||
$ balena preload balena.img
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
options: preloadOptions
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
preload = require('balena-preload')
|
||||
visuals = require('resin-cli-visuals')
|
||||
nodeCleanup = require('node-cleanup')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
progressBars = {}
|
||||
|
||||
progressHandler = (event) ->
|
||||
progressBar = progressBars[event.name]
|
||||
if not progressBar
|
||||
progressBar = progressBars[event.name] = new visuals.Progress(event.name)
|
||||
progressBar.update(percentage: event.percentage)
|
||||
|
||||
spinners = {}
|
||||
|
||||
spinnerHandler = (event) ->
|
||||
spinner = spinners[event.name]
|
||||
if not spinner
|
||||
spinner = spinners[event.name] = new visuals.Spinner(event.name)
|
||||
if event.action == 'start'
|
||||
spinner.start()
|
||||
else
|
||||
console.log()
|
||||
spinner.stop()
|
||||
|
||||
options.commit = if isCurrent(options.commit) then 'latest' else options.commit
|
||||
options.image = params.image
|
||||
options.appId = options.app
|
||||
delete options.app
|
||||
|
||||
options.splashImage = options['splash-image']
|
||||
delete options['splash-image']
|
||||
|
||||
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']
|
||||
|
||||
if _.isArray(options['add-certificate'])
|
||||
certificates = options['add-certificate']
|
||||
else if options['add-certificate'] == undefined
|
||||
certificates = []
|
||||
else
|
||||
certificates = [ options['add-certificate'] ]
|
||||
for certificate in certificates
|
||||
if not certificate.endsWith('.crt')
|
||||
exitWithExpectedError('Certificate file name must end with ".crt"')
|
||||
|
||||
# Get a configured dockerode instance
|
||||
dockerUtils.getDocker(options)
|
||||
.then (docker) ->
|
||||
|
||||
preloader = new preload.Preloader(
|
||||
balena
|
||||
docker
|
||||
options.appId
|
||||
options.commit
|
||||
options.image
|
||||
options.splashImage
|
||||
options.proxy
|
||||
options.dontCheckArch
|
||||
options.pinDevice
|
||||
certificates
|
||||
)
|
||||
|
||||
gotSignal = false
|
||||
|
||||
nodeCleanup (exitCode, signal) ->
|
||||
if signal
|
||||
gotSignal = true
|
||||
nodeCleanup.uninstall() # don't call cleanup handler again
|
||||
preloader.cleanup()
|
||||
.then ->
|
||||
# calling process.exit() won't inform parent process of signal
|
||||
process.kill(process.pid, signal)
|
||||
return false
|
||||
|
||||
if process.env.DEBUG
|
||||
preloader.stderr.pipe(process.stderr)
|
||||
|
||||
preloader.on('progress', progressHandler)
|
||||
preloader.on('spinner', spinnerHandler)
|
||||
|
||||
return new Promise (resolve, reject) ->
|
||||
preloader.on('error', reject)
|
||||
|
||||
preloader.prepare()
|
||||
.then ->
|
||||
# If no appId was provided, show a list of matching apps
|
||||
Promise.try ->
|
||||
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 isCurrent(options.commit) and preloader.application.commit
|
||||
# handle `--commit current` (and its `--commit latest` synonym)
|
||||
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 isCurrent(commit)
|
||||
preloader.commit = preloader.application.commit
|
||||
else
|
||||
preloader.commit = commit
|
||||
|
||||
# Propose to disable automatic app updates if the commit is not the current release
|
||||
offerToDisableAutomaticUpdates(preloader.application, commit, options.pinDevice)
|
||||
.then ->
|
||||
# All options are ready: preload the image.
|
||||
preloader.preload()
|
||||
.catch(balena.errors.BalenaError, exitWithExpectedError)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.then(done)
|
||||
.finally ->
|
||||
if not gotSignal
|
||||
preloader.cleanup()
|
423
lib/actions/preload.js
Normal file
423
lib/actions/preload.js
Normal file
@ -0,0 +1,423 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
import * as dockerUtils from '../utils/docker';
|
||||
|
||||
const isCurrent = (commit) => commit === 'latest' || commit === 'current';
|
||||
|
||||
let allDeviceTypes;
|
||||
const getDeviceTypes = function () {
|
||||
const Bluebird = require('bluebird');
|
||||
if (allDeviceTypes !== undefined) {
|
||||
return Bluebird.resolve(allDeviceTypes);
|
||||
}
|
||||
const balena = getBalenaSdk();
|
||||
return balena.models.config
|
||||
.getDeviceTypes()
|
||||
.then((deviceTypes) => _.sortBy(deviceTypes, 'name'))
|
||||
.tap((dt) => {
|
||||
allDeviceTypes = dt;
|
||||
});
|
||||
};
|
||||
|
||||
const getDeviceTypesWithSameArch = function (deviceTypeSlug) {
|
||||
return getDeviceTypes().then(function (deviceTypes) {
|
||||
const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug });
|
||||
return _(deviceTypes).filter({ arch: deviceType.arch }).map('slug').value();
|
||||
});
|
||||
};
|
||||
|
||||
const getApplicationsWithSuccessfulBuilds = function (deviceType) {
|
||||
const balenaPreload = require('balena-preload');
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
return getDeviceTypesWithSameArch(deviceType).then((deviceTypes) => {
|
||||
/** @type {import('balena-sdk').PineOptionsFor<import('balena-sdk').Application>} */
|
||||
const options = {
|
||||
$filter: {
|
||||
device_type: {
|
||||
$in: deviceTypes,
|
||||
},
|
||||
owns__release: {
|
||||
$any: {
|
||||
$alias: 'r',
|
||||
$expr: {
|
||||
r: {
|
||||
status: 'success',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
$expand: balenaPreload.applicationExpandOptions,
|
||||
$select: [
|
||||
'id',
|
||||
'app_name',
|
||||
'device_type',
|
||||
'commit',
|
||||
'should_track_latest_release',
|
||||
],
|
||||
$orderby: 'app_name asc',
|
||||
};
|
||||
return balena.pine.get({
|
||||
resource: 'my_application',
|
||||
options,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const selectApplication = function (deviceType) {
|
||||
const visuals = getVisuals();
|
||||
const form = require('resin-cli-form');
|
||||
const { exitWithExpectedError } = require('../errors');
|
||||
|
||||
const applicationInfoSpinner = new visuals.Spinner(
|
||||
'Downloading list of applications and releases.',
|
||||
);
|
||||
applicationInfoSpinner.start();
|
||||
|
||||
return getApplicationsWithSuccessfulBuilds(deviceType).then(function (
|
||||
applications,
|
||||
) {
|
||||
applicationInfoSpinner.stop();
|
||||
if (applications.length === 0) {
|
||||
exitWithExpectedError(
|
||||
`You have no apps with successful releases for a '${deviceType}' device type.`,
|
||||
);
|
||||
}
|
||||
return form.ask({
|
||||
message: 'Select an application',
|
||||
type: 'list',
|
||||
choices: applications.map((app) => ({
|
||||
name: app.app_name,
|
||||
value: app,
|
||||
})),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const selectApplicationCommit = function (releases) {
|
||||
const form = require('resin-cli-form');
|
||||
const { exitWithExpectedError } = require('../errors');
|
||||
|
||||
if (releases.length === 0) {
|
||||
exitWithExpectedError('This application has no successful releases.');
|
||||
}
|
||||
const DEFAULT_CHOICE = { name: 'current', value: 'current' };
|
||||
const choices = [DEFAULT_CHOICE].concat(
|
||||
releases.map((release) => ({
|
||||
name: `${release.end_timestamp} - ${release.commit}`,
|
||||
value: release.commit,
|
||||
})),
|
||||
);
|
||||
return form.ask({
|
||||
message: 'Select a release',
|
||||
type: 'list',
|
||||
default: 'current',
|
||||
choices,
|
||||
});
|
||||
};
|
||||
|
||||
const offerToDisableAutomaticUpdates = function (
|
||||
application,
|
||||
commit,
|
||||
pinDevice,
|
||||
) {
|
||||
const Promise = require('bluebird');
|
||||
const balena = getBalenaSdk();
|
||||
const form = require('resin-cli-form');
|
||||
|
||||
if (
|
||||
isCurrent(commit) ||
|
||||
!application.should_track_latest_release ||
|
||||
pinDevice
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const message = `\
|
||||
|
||||
This application is set to automatically update all devices to the current version.
|
||||
This might be unexpected behavior: with this enabled, the preloaded device will still
|
||||
download and install the current 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://balena.io/docs/reference/api/resources/device/#set-device-to-release
|
||||
|
||||
Alternatively you can pass the --pin-device-to-release flag to pin only this device to the selected release.\
|
||||
`;
|
||||
return form
|
||||
.ask({
|
||||
message,
|
||||
type: 'confirm',
|
||||
})
|
||||
.then(function (update) {
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
return balena.pine.patch({
|
||||
resource: 'application',
|
||||
id: application.id,
|
||||
body: {
|
||||
should_track_latest_release: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const preloadOptions = dockerUtils.appendConnectionOptions([
|
||||
{
|
||||
signature: 'app',
|
||||
parameter: 'appId',
|
||||
description: 'id of the application to preload',
|
||||
alias: 'a',
|
||||
},
|
||||
{
|
||||
signature: 'commit',
|
||||
parameter: 'hash',
|
||||
description: `\
|
||||
The commit hash for a specific application release to preload, use "current" to specify the current
|
||||
release (ignored if no appId is given). The current release is usually also the latest, but can be
|
||||
manually pinned using https://github.com/balena-io-projects/staged-releases .\
|
||||
`,
|
||||
alias: 'c',
|
||||
},
|
||||
{
|
||||
signature: 'splash-image',
|
||||
parameter: 'splashImage.png',
|
||||
description: 'path to a png image to replace the splash screen',
|
||||
alias: 's',
|
||||
},
|
||||
{
|
||||
signature: 'dont-check-arch',
|
||||
boolean: true,
|
||||
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',
|
||||
},
|
||||
{
|
||||
signature: 'add-certificate',
|
||||
parameter: 'certificate.crt',
|
||||
description: `\
|
||||
Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container.
|
||||
The file name must end with '.crt' and must not be already contained in the preloader's
|
||||
/etc/ssl/certs folder.
|
||||
Can be repeated to add multiple certificates.\
|
||||
`,
|
||||
},
|
||||
]);
|
||||
// Remove dockerPort `-p` alias as it conflicts with pin-device-to-release
|
||||
delete _.find(preloadOptions, { signature: 'dockerPort' }).alias;
|
||||
|
||||
export const preload = {
|
||||
signature: 'preload <image>',
|
||||
description: 'preload an app on a disk image (or Edison zip archive)',
|
||||
help: `\
|
||||
Preload a balena application release (app images/containers), and optionally
|
||||
a balenaOS splash screen, in a previously downloaded '.img' balenaOS image file
|
||||
in the local disk (a zip file is only accepted for the Intel Edison device type).
|
||||
After preloading, the balenaOS image file can be flashed to a device's SD card.
|
||||
When the device boots, it will not need to download the application, as it was
|
||||
preloaded.
|
||||
|
||||
Warning: "balena preload" requires Docker to be correctly installed in
|
||||
your shell environment. For more information (including Windows support)
|
||||
check: https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena preload balena.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png
|
||||
$ balena preload balena.img\
|
||||
`,
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
options: preloadOptions,
|
||||
action(params, options, done) {
|
||||
let certificates;
|
||||
const Promise = require('bluebird');
|
||||
const balena = getBalenaSdk();
|
||||
const balenaPreload = require('balena-preload');
|
||||
const visuals = getVisuals();
|
||||
const nodeCleanup = require('node-cleanup');
|
||||
const { exitWithExpectedError } = require('../errors');
|
||||
|
||||
const progressBars = {};
|
||||
|
||||
const progressHandler = function (event) {
|
||||
let progressBar = progressBars[event.name];
|
||||
if (!progressBar) {
|
||||
progressBar = progressBars[event.name] = new visuals.Progress(
|
||||
event.name,
|
||||
);
|
||||
}
|
||||
return progressBar.update({ percentage: event.percentage });
|
||||
};
|
||||
|
||||
const spinners = {};
|
||||
|
||||
const spinnerHandler = function (event) {
|
||||
let spinner = spinners[event.name];
|
||||
if (!spinner) {
|
||||
spinner = spinners[event.name] = new visuals.Spinner(event.name);
|
||||
}
|
||||
if (event.action === 'start') {
|
||||
return spinner.start();
|
||||
} else {
|
||||
console.log();
|
||||
return spinner.stop();
|
||||
}
|
||||
};
|
||||
|
||||
options.commit = isCurrent(options.commit) ? 'latest' : options.commit;
|
||||
options.image = params.image;
|
||||
options.appId = options.app;
|
||||
delete options.app;
|
||||
|
||||
options.splashImage = options['splash-image'];
|
||||
delete options['splash-image'];
|
||||
|
||||
options.dontCheckArch = options['dont-check-arch'] || false;
|
||||
delete options['dont-check-arch'];
|
||||
if (options.dontCheckArch && !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'];
|
||||
|
||||
if (Array.isArray(options['add-certificate'])) {
|
||||
certificates = options['add-certificate'];
|
||||
} else if (options['add-certificate'] === undefined) {
|
||||
certificates = [];
|
||||
} else {
|
||||
certificates = [options['add-certificate']];
|
||||
}
|
||||
for (let certificate of certificates) {
|
||||
if (!certificate.endsWith('.crt')) {
|
||||
exitWithExpectedError('Certificate file name must end with ".crt"');
|
||||
}
|
||||
}
|
||||
|
||||
// Get a configured dockerode instance
|
||||
return dockerUtils.getDocker(options).then(function (docker) {
|
||||
const preloader = new balenaPreload.Preloader(
|
||||
balena,
|
||||
docker,
|
||||
options.appId,
|
||||
options.commit,
|
||||
options.image,
|
||||
options.splashImage,
|
||||
options.proxy,
|
||||
options.dontCheckArch,
|
||||
options.pinDevice,
|
||||
certificates,
|
||||
);
|
||||
|
||||
let gotSignal = false;
|
||||
|
||||
nodeCleanup(function (_exitCode, signal) {
|
||||
if (signal) {
|
||||
gotSignal = true;
|
||||
nodeCleanup.uninstall(); // don't call cleanup handler again
|
||||
preloader.cleanup().then(() => {
|
||||
// calling process.exit() won't inform parent process of signal
|
||||
process.kill(process.pid, signal);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
preloader.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
preloader.on('progress', progressHandler);
|
||||
preloader.on('spinner', spinnerHandler);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
preloader.on('error', reject);
|
||||
|
||||
return preloader
|
||||
.prepare()
|
||||
.then(() => {
|
||||
// If no appId was provided, show a list of matching apps
|
||||
if (!preloader.appId) {
|
||||
return selectApplication(
|
||||
preloader.config.deviceType,
|
||||
).then((application) => preloader.setApplication(application));
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// Use the commit given as --commit or show an interactive commit selection menu
|
||||
if (options.commit) {
|
||||
if (isCurrent(options.commit) && preloader.application.commit) {
|
||||
// handle `--commit current` (and its `--commit latest` synonym)
|
||||
return 'latest';
|
||||
}
|
||||
const release = _.find(preloader.application.owns__release, (r) =>
|
||||
r.commit.startsWith(options.commit),
|
||||
);
|
||||
if (!release) {
|
||||
exitWithExpectedError(
|
||||
'There is no release matching this commit',
|
||||
);
|
||||
}
|
||||
return release.commit;
|
||||
}
|
||||
return selectApplicationCommit(preloader.application.owns__release);
|
||||
})
|
||||
.then(function (commit) {
|
||||
if (isCurrent(commit)) {
|
||||
preloader.commit = preloader.application.commit;
|
||||
} else {
|
||||
preloader.commit = commit;
|
||||
}
|
||||
|
||||
// Propose to disable automatic app updates if the commit is not the current release
|
||||
return offerToDisableAutomaticUpdates(
|
||||
preloader.application,
|
||||
commit,
|
||||
options.pinDevice,
|
||||
);
|
||||
})
|
||||
.then(() =>
|
||||
// All options are ready: preload the image.
|
||||
preloader.preload(),
|
||||
)
|
||||
.catch(balena.errors.BalenaError, exitWithExpectedError)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
})
|
||||
.then(done)
|
||||
.finally(function () {
|
||||
if (!gotSignal) {
|
||||
return preloader.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016-2019 Balena Ltd.
|
||||
Copyright 2016-2020 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -18,12 +18,15 @@ import { BalenaSDK } from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { registrySecretsHelp } from '../utils/messages';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import {
|
||||
validateApplicationName,
|
||||
validateDotLocalUrl,
|
||||
validateIPAddress,
|
||||
} from '../utils/validation';
|
||||
import { isV12 } from '../utils/version';
|
||||
|
||||
enum BuildTarget {
|
||||
Cloud,
|
||||
@ -44,9 +47,7 @@ function getBuildTarget(appOrDevice: string): BuildTarget | null {
|
||||
}
|
||||
|
||||
async function getAppOwner(sdk: BalenaSDK, appName: string) {
|
||||
const { exitWithExpectedError, selectFromList } = await import(
|
||||
'../utils/patterns'
|
||||
);
|
||||
const { selectFromList } = await import('../utils/patterns');
|
||||
const _ = await import('lodash');
|
||||
|
||||
const applications = await sdk.models.application.getAll({
|
||||
@ -62,7 +63,7 @@ async function getAppOwner(sdk: BalenaSDK, appName: string) {
|
||||
});
|
||||
|
||||
if (applications == null || applications.length === 0) {
|
||||
exitWithExpectedError(
|
||||
throw new ExpectedError(
|
||||
stripIndent`
|
||||
No applications found with name: ${appName}.
|
||||
|
||||
@ -79,7 +80,7 @@ async function getAppOwner(sdk: BalenaSDK, appName: string) {
|
||||
// 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 entries = _.map(applications, (app) => {
|
||||
const username = _.get(app, 'user[0].username');
|
||||
return {
|
||||
name: `${username}/${appName}`,
|
||||
@ -88,9 +89,7 @@ async function getAppOwner(sdk: BalenaSDK, appName: string) {
|
||||
});
|
||||
|
||||
const selected = await selectFromList(
|
||||
`${
|
||||
entries.length
|
||||
} applications found with that name, please select the application you would like to push to`,
|
||||
`${entries.length} applications found with that name, please select the application you would like to push to`,
|
||||
entries,
|
||||
);
|
||||
|
||||
@ -109,12 +108,16 @@ export const push: CommandDefinition<
|
||||
emulated?: boolean;
|
||||
dockerfile?: string; // DeviceDeployOptions.dockerfilePath (alternative Dockerfile)
|
||||
nocache?: boolean;
|
||||
'noparent-check'?: boolean;
|
||||
'registry-secrets'?: string;
|
||||
nogitignore?: boolean;
|
||||
nolive?: boolean;
|
||||
detached?: boolean;
|
||||
service?: string | string[];
|
||||
system?: boolean;
|
||||
env?: string | string[];
|
||||
'convert-eol'?: boolean;
|
||||
'noconvert-eol'?: boolean;
|
||||
}
|
||||
> = {
|
||||
signature: 'push <applicationOrDevice>',
|
||||
@ -142,12 +145,14 @@ export const push: CommandDefinition<
|
||||
When pushing to a local device a live session will be started.
|
||||
The project source folder is watched for filesystem events, and changes
|
||||
to files and folders are automatically synchronized to the running
|
||||
containers. The synchronisation is only in one direction, from this machine to
|
||||
containers. The synchronization is only in one direction, from this machine to
|
||||
the device, and changes made on the device itself may be overwritten.
|
||||
This feature requires a device running supervisor version v9.7.0 or greater.
|
||||
|
||||
${registrySecretsHelp.split('\n').join('\n\t\t')}
|
||||
|
||||
${dockerignoreHelp.split('\n').join('\n\t\t')}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena push myApp
|
||||
@ -168,7 +173,7 @@ export const push: CommandDefinition<
|
||||
signature: 'source',
|
||||
alias: 's',
|
||||
description:
|
||||
'The source that should be sent to the balena builder to be built (defaults to the current directory)',
|
||||
'Source directory to be sent to balenaCloud or balenaOS device (default: current working dir)',
|
||||
parameter: 'source',
|
||||
},
|
||||
{
|
||||
@ -189,6 +194,12 @@ export const push: CommandDefinition<
|
||||
description: "Don't use cache when building this project",
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'noparent-check',
|
||||
description:
|
||||
"Disable project validation check of 'docker-compose.yml' file in parent folder",
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'registry-secrets',
|
||||
alias: 'R',
|
||||
@ -203,7 +214,7 @@ export const push: CommandDefinition<
|
||||
boolean: true,
|
||||
description: stripIndent`
|
||||
Don't run a live session on this push. The filesystem will not be monitored, and changes
|
||||
will not be synchronised to any running containers. Note that both this flag and --detached
|
||||
will not be synchronized to any running containers. Note that both this flag and --detached
|
||||
and required to cause the process to end once the initial build has completed.`,
|
||||
},
|
||||
{
|
||||
@ -245,25 +256,52 @@ export const push: CommandDefinition<
|
||||
left hand side of the = character will be treated as the variable name.
|
||||
`,
|
||||
},
|
||||
{
|
||||
signature: 'convert-eol',
|
||||
alias: 'l',
|
||||
description: isV12()
|
||||
? 'No-op and deprecated since balena CLI v12.0.0'
|
||||
: stripIndent`
|
||||
On Windows only, convert line endings from CRLF (Windows format) to LF (Unix format).
|
||||
Source files are not modified.`,
|
||||
boolean: true,
|
||||
},
|
||||
...(isV12()
|
||||
? [
|
||||
{
|
||||
signature: 'noconvert-eol',
|
||||
description:
|
||||
"Don't convert line endings from CRLF (Windows format) to LF (Unix format).",
|
||||
boolean: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
signature: 'nogitignore',
|
||||
alias: 'G',
|
||||
description: isV12()
|
||||
? 'No-op and deprecated since balena CLI v12.0.0. See "balena help push".'
|
||||
: stripIndent`
|
||||
Disregard all .gitignore files, and consider only the .dockerignore file (if any)
|
||||
at the source directory. This will be the default behavior in an upcoming major
|
||||
version release. For more information, see 'balena help push'.
|
||||
`,
|
||||
boolean: true,
|
||||
},
|
||||
],
|
||||
async action(params, options, done) {
|
||||
const sdk = (await import('balena-sdk')).fromSharedOptions();
|
||||
async action(params, options) {
|
||||
const sdk = getBalenaSdk();
|
||||
const Bluebird = await import('bluebird');
|
||||
const isArray = await import('lodash/isArray');
|
||||
const remote = await import('../utils/remote-build');
|
||||
const deviceDeploy = await import('../utils/device/deploy');
|
||||
const { exitIfNotLoggedIn, exitWithExpectedError } = await import(
|
||||
'../utils/patterns'
|
||||
);
|
||||
const { validateSpecifiedDockerfile, getRegistrySecrets } = await import(
|
||||
'../utils/compose_ts'
|
||||
);
|
||||
const { checkLoggedIn } = await import('../utils/patterns');
|
||||
const { validateProjectDirectory } = await import('../utils/compose_ts');
|
||||
const { BuildError } = await import('../utils/device/errors');
|
||||
|
||||
const appOrDevice: string | null =
|
||||
params.applicationOrDevice_raw || params.applicationOrDevice;
|
||||
if (appOrDevice == null) {
|
||||
exitWithExpectedError('You must specify an application or a device');
|
||||
throw new ExpectedError('You must specify an application or a device');
|
||||
}
|
||||
|
||||
const source = options.source || '.';
|
||||
@ -271,43 +309,48 @@ export const push: CommandDefinition<
|
||||
console.error(`[debug] Using ${source} as build source`);
|
||||
}
|
||||
|
||||
const dockerfilePath = validateSpecifiedDockerfile(
|
||||
source,
|
||||
options.dockerfile,
|
||||
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
|
||||
sdk,
|
||||
{
|
||||
dockerfilePath: options.dockerfile,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
projectPath: source,
|
||||
registrySecretsPath: options['registry-secrets'],
|
||||
},
|
||||
);
|
||||
|
||||
const registrySecrets = await getRegistrySecrets(
|
||||
sdk,
|
||||
options['registry-secrets'],
|
||||
);
|
||||
const nogitignore = !!options.nogitignore || isV12();
|
||||
const convertEol = isV12()
|
||||
? !options['noconvert-eol']
|
||||
: !!options['convert-eol'];
|
||||
|
||||
const buildTarget = getBuildTarget(appOrDevice);
|
||||
switch (buildTarget) {
|
||||
case BuildTarget.Cloud:
|
||||
// Ensure that the live argument has not been passed to a cloud build
|
||||
if (options.nolive != null) {
|
||||
exitWithExpectedError(
|
||||
throw new ExpectedError(
|
||||
'The --nolive flag is only valid when pushing to a local mode device',
|
||||
);
|
||||
}
|
||||
if (options.service) {
|
||||
exitWithExpectedError(
|
||||
throw new ExpectedError(
|
||||
'The --service flag is only valid when pushing to a local mode device.',
|
||||
);
|
||||
}
|
||||
if (options.system) {
|
||||
exitWithExpectedError(
|
||||
throw new ExpectedError(
|
||||
'The --system flag is only valid when pushing to a local mode device.',
|
||||
);
|
||||
}
|
||||
if (options.env) {
|
||||
exitWithExpectedError(
|
||||
throw new ExpectedError(
|
||||
'The --env flag is only valid when pushing to a local mode device.',
|
||||
);
|
||||
}
|
||||
|
||||
const app = appOrDevice;
|
||||
await exitIfNotLoggedIn();
|
||||
await checkLoggedIn();
|
||||
await Bluebird.join(
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
@ -319,6 +362,7 @@ export const push: CommandDefinition<
|
||||
nocache: options.nocache || false,
|
||||
registrySecrets,
|
||||
headless: options.detached || false,
|
||||
convertEol,
|
||||
};
|
||||
const args = {
|
||||
app,
|
||||
@ -326,19 +370,19 @@ export const push: CommandDefinition<
|
||||
source,
|
||||
auth: token,
|
||||
baseUrl,
|
||||
nogitignore,
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
|
||||
return await remote.startRemoteBuild(args);
|
||||
},
|
||||
).nodeify(done);
|
||||
);
|
||||
break;
|
||||
case BuildTarget.Device:
|
||||
const device = appOrDevice;
|
||||
const servicesToDisplay =
|
||||
options.service != null
|
||||
? isArray(options.service)
|
||||
? Array.isArray(options.service)
|
||||
? options.service
|
||||
: [options.service]
|
||||
: undefined;
|
||||
@ -350,6 +394,8 @@ export const push: CommandDefinition<
|
||||
dockerfilePath,
|
||||
registrySecrets,
|
||||
nocache: options.nocache || false,
|
||||
nogitignore,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
nolive: options.nolive || false,
|
||||
detached: options.detached || false,
|
||||
services: servicesToDisplay,
|
||||
@ -358,24 +404,22 @@ export const push: CommandDefinition<
|
||||
typeof options.env === 'string'
|
||||
? [options.env]
|
||||
: options.env || [],
|
||||
convertEol,
|
||||
}),
|
||||
)
|
||||
.catch(BuildError, e => {
|
||||
exitWithExpectedError(e.toString());
|
||||
})
|
||||
.nodeify(done);
|
||||
).catch(BuildError, (e) => {
|
||||
throw new ExpectedError(e.toString());
|
||||
});
|
||||
break;
|
||||
default:
|
||||
exitWithExpectedError(
|
||||
throw new ExpectedError(
|
||||
stripIndent`
|
||||
Build target not recognised. Please provide either an application name or device address.
|
||||
Build target not recognized. 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1,100 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
dockerInfoProperties = [
|
||||
'Containers'
|
||||
'ContainersRunning'
|
||||
'ContainersPaused'
|
||||
'ContainersStopped'
|
||||
'Images'
|
||||
'Driver'
|
||||
'SystemTime'
|
||||
'KernelVersion'
|
||||
'OperatingSystem'
|
||||
'Architecture'
|
||||
]
|
||||
|
||||
dockerVersionProperties = [
|
||||
'Version'
|
||||
'ApiVersion'
|
||||
]
|
||||
|
||||
module.exports =
|
||||
signature: 'scan'
|
||||
description: 'Scan for balenaOS devices in your local network'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena scan
|
||||
$ balena scan --timeout 120
|
||||
$ balena scan --verbose
|
||||
'''
|
||||
options: [
|
||||
signature: 'verbose'
|
||||
boolean: true
|
||||
description: 'Display full info'
|
||||
alias: 'v'
|
||||
,
|
||||
signature: 'timeout'
|
||||
parameter: 'timeout'
|
||||
description: 'Scan timeout in seconds'
|
||||
alias: 't'
|
||||
]
|
||||
primary: true
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
prettyjson = require('prettyjson')
|
||||
{ discover } = require('balena-sync')
|
||||
{ SpinnerPromise } = require('resin-cli-visuals')
|
||||
{ dockerPort, dockerTimeout } = require('./local/common')
|
||||
dockerUtils = require('../utils/docker')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if options.timeout?
|
||||
options.timeout *= 1000
|
||||
|
||||
Promise.try ->
|
||||
new SpinnerPromise
|
||||
promise: discover.discoverLocalBalenaOsDevices(options.timeout)
|
||||
startMessage: 'Scanning for local balenaOS devices..'
|
||||
stopMessage: 'Reporting scan results'
|
||||
.filter ({ address }) ->
|
||||
Promise.try ->
|
||||
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
docker.pingAsync()
|
||||
.return(true)
|
||||
.catchReturn(false)
|
||||
.tap (devices) ->
|
||||
if _.isEmpty(devices)
|
||||
exitWithExpectedError('Could not find any balenaOS devices in the local network')
|
||||
.map ({ host, address }) ->
|
||||
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')
|
||||
.then ({ dockerInfo, dockerVersion }) ->
|
||||
|
||||
if not options.verbose
|
||||
dockerInfo = _.pick(dockerInfo, dockerInfoProperties) if _.isObject(dockerInfo)
|
||||
dockerVersion = _.pick(dockerVersion, dockerVersionProperties) if _.isObject(dockerVersion)
|
||||
|
||||
return { host, address, dockerInfo, dockerVersion }
|
||||
.then (devicesInfo) ->
|
||||
console.log(prettyjson.render(devicesInfo, noColor: true))
|
||||
.nodeify(done)
|
@ -1,39 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CommandDefinition } from 'capitano';
|
||||
|
||||
export const list: CommandDefinition = {
|
||||
signature: 'settings',
|
||||
description: 'print current settings',
|
||||
help: `\
|
||||
Use this command to display detected settings
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena settings\
|
||||
`,
|
||||
async action(_params, _options, done) {
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const prettyjson = await import('prettyjson');
|
||||
|
||||
return balena.settings
|
||||
.getAll()
|
||||
.then(prettyjson.render)
|
||||
.then(console.log)
|
||||
.nodeify(done);
|
||||
},
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user